Skip to content

Instantly share code, notes, and snippets.

@rudzainy
Last active April 5, 2024 13:10
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rudzainy/debc9e64270b56150985ba4678b13bd1 to your computer and use it in GitHub Desktop.
Save rudzainy/debc9e64270b56150985ba4678b13bd1 to your computer and use it in GitHub Desktop.
Hujah Card
import React from 'react'
import { Link } from 'react-router-dom'
import Linkify from 'react-linkify'
import AgreeIcon from '../Icons/agree'
import NeutralIcon from '../Icons/neutral'
import DisagreeIcon from '../Icons/disagree'
import ViewsIcon from '../Icons/views'
import VotesIcon from '../Icons/votes'
import HujahIcon from '../Icons/hujah'
import HujahCardHeader from './card_header'
import ButtonAddHujah from '../Layouts/button_add_hujah'
class HujahCard extends React.Component {
constructor(props) {
super(props)
const { hujah } = this.props
const { agree_count, neutral_count, disagree_count } = hujah.attributes
this.state = {
hujah: hujah,
hujahParentAvailable: hujah.attributes.hasOwnProperty("parent"),
showAddHujahButton: hujah.attributes.current_user_vote !== null,
totalVoteCount: agree_count + neutral_count + disagree_count
}
}
updateVote(vote) {
const url = "/api/v1/votes/create"
const body = {
vote: vote,
hujah_id: this.state.hujah.id,
user_id: this.props.currentUser.id
}
const token = document.querySelector('meta[name="csrf-token"]').content
fetch(url, {
method: "POST",
headers: {
"X-CSRF-Token": token,
"Content-Type": "application/json"
},
body: JSON.stringify(body)
})
.then(response => {
if (response.ok) {
return response.json()
}
throw new Error("Network response was not ok.")
})
.catch(error => console.log(error.message))
}
calculatePercentage(voteCount) {
const percentage = voteCount * 100 / this.state.totalVoteCount
return `${percentage}%`
}
handleVoteAgree() {
if(this.userNotLoggedIn()) {
return this.redirectToLogin()
}
const newHujahState = Object.assign({}, this.state.hujah)
var addToTotalVoteCount = 0
if(newHujahState.attributes.current_user_vote == "agree") {
return
} else {
newHujahState.attributes.agree_count = newHujahState.attributes.agree_count + 1
if(newHujahState.attributes.current_user_vote == "neutral") {
newHujahState.attributes.neutral_count = newHujahState.attributes.neutral_count - 1
} else if(newHujahState.attributes.current_user_vote == "disagree") {
newHujahState.attributes.disagree_count = newHujahState.attributes.disagree_count - 1
}
if(newHujahState.attributes.current_user_vote == null) {
addToTotalVoteCount = 1
}
newHujahState.attributes.current_user_vote = "agree"
}
this.setState((prevState, props) => {
return {
hujah: newHujahState,
totalVoteCount: prevState.totalVoteCount + addToTotalVoteCount,
showAddHujahButton: true
}
})
this.updateVote(1)
}
handleVoteNeutral() {
if(this.userNotLoggedIn()) {
return this.redirectToLogin()
}
const newHujahState = Object.assign({}, this.state.hujah)
var addToTotalVoteCount = 0
if(newHujahState.attributes.current_user_vote == "neutral") {
return
} else {
newHujahState.attributes.neutral_count = newHujahState.attributes.neutral_count + 1
if(newHujahState.attributes.current_user_vote == "agree") {
newHujahState.attributes.agree_count = newHujahState.attributes.agree_count - 1
} else if(newHujahState.attributes.current_user_vote == "disagree") {
newHujahState.attributes.disagree_count = newHujahState.attributes.disagree_count - 1
}
if(newHujahState.attributes.current_user_vote == null) {
addToTotalVoteCount = 1
}
newHujahState.attributes.current_user_vote = "neutral"
}
this.setState((prevState, props) => {
return {
hujah: newHujahState,
totalVoteCount: prevState.totalVoteCount + addToTotalVoteCount,
showAddHujahButton: true
}
})
this.updateVote(2)
}
handleVoteDisagree() {
if(this.userNotLoggedIn()) {
return this.redirectToLogin()
}
const newHujahState = Object.assign({}, this.state.hujah)
var addToTotalVoteCount = 0
if(newHujahState.attributes.current_user_vote == "disagree") {
return
} else {
newHujahState.attributes.disagree_count = newHujahState.attributes.disagree_count + 1
if(newHujahState.attributes.current_user_vote == "agree") {
newHujahState.attributes.agree_count = newHujahState.attributes.agree_count - 1
} else if(newHujahState.attributes.current_user_vote == "neutral") {
newHujahState.attributes.neutral_count = newHujahState.attributes.neutral_count - 1
}
if(newHujahState.attributes.current_user_vote == null) {
addToTotalVoteCount = 1
}
newHujahState.attributes.current_user_vote = "disagree"
}
this.setState((prevState, props) => {
return {
hujah: newHujahState,
totalVoteCount: prevState.totalVoteCount + addToTotalVoteCount,
showAddHujahButton: true
}
})
this.updateVote(3)
}
userNotLoggedIn() {
return !this.props.loggedInStatus
}
redirectToLogin() {
this.props.history.push('/start/login')
}
render() {
// || this.state.hujah.attributes.children_count == 1
if(this.state.hujahParentAvailable) {
return null
}
const { hujah, hujahParentAvailable, showAddHujahButton, totalVoteCount } = this.state
const { agree_count, neutral_count, disagree_count, body, current_user_vote, children_count, user, slug } = hujah.attributes
const showVoteBar = (
<div className="card-body p-0">
<div className="d-flex justify-content-around vote-bar">
<div className="vote bg-agree" style={{ width: this.calculatePercentage(agree_count) }}></div>
<div className="vote bg-neutral" style={{ width: this.calculatePercentage(neutral_count) }}></div>
<div className="vote bg-disagree" style={{ width: this.calculatePercentage(disagree_count) }}></div>
</div>
</div>
)
return(
<div className="shadow card border-0 rounded-0 mb-2">
<HujahCardHeader hujah={hujah} hujahParentAvailable={hujahParentAvailable} />
<div className="card-body pb-0">
<Link to={`/hoojah/${slug}`}>
<h5 className="card-title text-black text-regular" dangerouslySetInnerHTML={{ __html: body }}></h5>
</Link>
</div>
<div className={`card-body pt-0 ${showAddHujahButton ? "d-flex justify-content-between" : null}`}>
<div className={`d-flex justify-content-${showAddHujahButton ? "between" : "around"}`}>
<button className={`shadow btn btn-outline-agree btn-lg btn-circle btn-icon-16 fill-agree ${current_user_vote == "agree" ? "voted" : null} ${showAddHujahButton ? "mr-2" : null}`} onClick={() => this.handleVoteAgree()}><AgreeIcon /></button>
<button className={`shadow btn btn-outline-neutral btn-lg btn-circle btn-icon-16 fill-neutral neutral ${current_user_vote == "neutral" ? "voted" : null} ${showAddHujahButton ? "mr-2" : null}`} onClick={() => this.handleVoteNeutral()}><NeutralIcon /></button>
<button className={`shadow btn btn-outline-disagree btn-lg btn-circle btn-icon-16 fill-disagree ${current_user_vote == "disagree" ? "voted" : null}`} onClick={() => this.handleVoteDisagree()}><DisagreeIcon /></button>
</div>
{showAddHujahButton ? <ButtonAddHujah hujahParent={hujah} user={user} vote={current_user_vote} /> : null}
</div>
{totalVoteCount > 0 ? showVoteBar : null}
<div className="card-footer d-flex justify-content-between text-grey">
<div className="d-flex align-items-center">
<Link to={`/hoojah/${hujah.slug}`} className="text-14 btn-icon-14 fill-grey no-underscore text-grey">
<VotesIcon />
<span className="ml-1">{totalVoteCount}</span>
<span className="mx-2">·</span>
<HujahIcon />
<span className="ml-1">{children_count}</span>
</Link>
</div>
</div>
</div>
)
}
}
export default HujahCard
import React, { Fragment } from 'react'
import { Link } from 'react-router-dom'
import ShareIcon from '../Icons/share'
import HujahCardParent from './card_parent'
import {
EmailShareButton,
EmailIcon,
FacebookShareButton,
FacebookIcon,
RedditShareButton,
RedditIcon,
TelegramShareButton,
TelegramIcon,
TwitterShareButton,
TwitterIcon,
WhatsappShareButton,
WhatsappIcon
} from "react-share"
class HujahCardHeader extends React.Component {
render() {
const { hujah, hujahParentAvailable } = this.props
const { parent, user, body } = hujah.attributes
const { full_name, username, photo } = user.attributes
const messageForSocialMedia = `"${body} (by @${username})" Do you AGREE? NEUTRAL? DISAGREE?`
const socialMediaButtonUrl = `${window.location.origin}/hoojah/${hujah.attributes.slug}`
const socialMediaButtonClass = "px-3 py-1"
return(
<Fragment>
{ hujahParentAvailable ? <HujahCardParent hujah={parent} /> : null }
<div className="card-header border-bottom-0 pb-0 d-flex justify-content-between align-items-center">
<div className="media">
<Link to={`/${username}`}><img src={photo} className="rounded-circle mr-3 avatar" /></Link>
<div className="media-body">
<div className="d-flex flex-column">
<Link to={`/${username}`} className="mt-0 mb-0 text-primary">{full_name}</Link>
<a className="no-underscore handle">
<small className="text-muted">{`@${username}`}</small>
</a>
</div>
</div>
</div>
<div className="dropdown">
<button className="btn btn-icon-16 fill-light-grey p-0" type="button" id="moreAction" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<ShareIcon />
</button>
<div className="dropdown-menu dropdown-menu-right" aria-labelledby="moreAction">
<h6 className="dropdown-header">Share this hoojah via</h6>
<FacebookShareButton
url={socialMediaButtonUrl}
quote={messageForSocialMedia}
hashtag="#hoojah"
className={socialMediaButtonClass}>
<FacebookIcon size={24} borderRadius={48} className="mr-1" /> Facebook
</FacebookShareButton>
<TwitterShareButton
url={socialMediaButtonUrl}
title={messageForSocialMedia}
via="hoojah_my"
hashtags={["hoojah", "discussions", "malaysia"]}
className={socialMediaButtonClass}>
<TwitterIcon size={24} borderRadius={48} className="mr-1" /> Twitter
</TwitterShareButton>
<WhatsappShareButton
url={socialMediaButtonUrl}
title={messageForSocialMedia}
className={socialMediaButtonClass}>
<WhatsappIcon size={24} borderRadius={48} className="mr-1" /> WhatsApp
</WhatsappShareButton>
<TelegramShareButton
url={socialMediaButtonUrl}
title={messageForSocialMedia}
className={socialMediaButtonClass}>
<TelegramIcon size={24} borderRadius={48} className="mr-1" /> Telegram
</TelegramShareButton>
<RedditShareButton
url={socialMediaButtonUrl}
title={messageForSocialMedia}
className={socialMediaButtonClass}>
<RedditIcon size={24} borderRadius={48} className="mr-1" /> Reddit
</RedditShareButton>
<EmailShareButton
url={socialMediaButtonUrl}
subject={messageForSocialMedia}
body="hello@hoojah.my"
className={socialMediaButtonClass}>
<EmailIcon size={24} borderRadius={48} className="mr-1" /> Email
</EmailShareButton>
</div>
</div>
</div>
</Fragment>
)
}
}
export default HujahCardHeader
import React from 'react'
import { Link } from 'react-router-dom'
class HujahParentCard extends React.Component {
render() {
const hujah = this.props.hujah
const { user, body, slug } = hujah.attributes
const { full_name, photo } = user.attributes
return(
<Link to={`/hoojah/${slug}`} className="no-underscore">
<div id="parent-card-container" className="shadow card-body border-bottom border-light-grey px-3 py-1 d-flex align-items-center mb-0">
<div className="media pt-1">
<img src={photo} className="rounded-circle mr-2 avatar-small" />
<div className="media-body">
<div className="d-flex flex-column">
<small className="text-grey">
Response to <span className="text-primary">{full_name}</span>'s hoojah
</small>
<div className="mb-0 text-grey text-14 text-truncate d-block d-sm-none" style={{ maxWidth: '270px' }} dangerouslySetInnerHTML={{ __html: body }}></div>
<div className="mb-0 text-grey text-14 text-truncate d-none d-sm-block d-md-none" style={{ maxWidth: '310px' }} dangerouslySetInnerHTML={{ __html: body }}></div>
<div className="mb-0 text-grey text-14 text-truncate d-none d-md-block" style={{ maxWidth: '350px' }} dangerouslySetInnerHTML={{ __html: body }}></div>
</div>
</div>
</div>
</div>
</Link>
)
}
}
export default HujahParentCard
import React from "react"
import { Link } from 'react-router-dom'
import AgreeIcon from '../Icons/agree'
import NeutralIcon from '../Icons/neutral'
import DisagreeIcon from '../Icons/disagree'
class HujahCardSmall extends React.Component {
parseVote(vote) {
if(vote == 1) {
return "agree"
} else if(vote == 2) {
return "neutral"
} else if(vote == 3) {
return "disagree"
} else {
return "primary"
}
}
render() {
const hujah = this.props.hujah
const { body, vote, agree_count, neutral_count, disagree_count, user, slug } = hujah.attributes
const { full_name, username, photo } = user.attributes
return(
<Link to={`/hoojah/${slug}`} className="no-underscore">
<div className={`shadow card-body border-left-8 border-${this.parseVote(vote)} border-bottom-0 py-0 pl-0 pr-2 d-flex align-items-center mb-1`}>
<div className="media py-1">
<img src={photo} className="rounded-circle mx-2 avatar-small" />
<div className="media-body">
<div className="d-flex flex-column">
<div>
<span className="my-0 mr-1 text-primary">{full_name}</span>
<span className="handle">
<small className="text-muted">{`@${username}`}</small>
</span>
</div>
<p className="mb-0 text-black" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: body }}></p>
<div className="d-flex align-items-center text-14 card-body btn-icon-14 text-light-grey fill-light-grey p-0">
<AgreeIcon />
<span className="ml-1">{agree_count}</span>
<span className="mx-2">·</span>
<NeutralIcon />
<span className="ml-1">{neutral_count}</span>
<span className="mx-2">·</span>
<DisagreeIcon />
<span className="ml-1">{disagree_count}</span>
</div>
</div>
</div>
</div>
</div>
</Link>
)
}
}
export default HujahCardSmall
class Hujah < ApplicationRecord
belongs_to :user
has_many :votes, dependent: :destroy
has_many :flags, dependent: :destroy
has_many :children, class_name: "Hujah", foreign_key: "parent_id", dependent: :destroy
belongs_to :parent, class_name: "Hujah", optional: true
validates :body, presence: true
slug :set_slug
def is_parent?
self.parent == nil
end
def has_parent?
self.parent != nil
end
def has_children?
self.children != 0
end
def set_slug
re = /<("[^"]*"|'[^']*'|[^'">])*>/
self.slug = self.body.gsub(re, '').parameterize
end
def current_user_vote(logged_in: nil, current_user_id: nil)
if logged_in
if votes.joins(:user).find_by(user_id: current_user_id).nil?
nil
else
vote = votes.joins(:user).find_by(user_id: current_user_id).vote.last
if vote == 1
"agree"
elsif vote == 2
"neutral"
elsif vote == 3
"disagree"
end
end
else
nil
end
end
end
class Api::V1::HujahsController < ApplicationController
def index
hujahs = Hujah.all.order(updated_at: :desc)
serialized_hujahs = HujahSerializer.new(hujahs, params: {logged_in: logged_in?, current_user_id: current_user&.id }).serializable_hash
render json: serialized_hujahs
end
def create
hujah = current_user.hujahs.create(hujah_params)
if hujah
if hujah.has_parent?
Notification.create!(user_id: hujah.parent.user.id, category: 3, hujah_id: hujah.parent.id, subject_user_id: current_user.id)
end
render json: hujah
else
render json: hujah.errors
end
end
def show
if hujah
serialized_hujah = HujahSerializer.new(hujah, params: {logged_in: logged_in?, current_user_id: current_user&.id }).serializable_hash
render json: serialized_hujah
end
end
def destroy
hujah&.destroy
render json: { message: 'Hoojah deleted!' }
end
def new
end
private
def hujah_params
params.permit(:body, :parent_id, :vote)
end
def hujah
@hujah ||= Hujah.find_by_slug(params[:slug])
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment