Merge pull request #75 from Irev-Dev/rebuild-ui
Hook up reactions data
This commit was merged in pull request #75.
This commit is contained in:
@@ -15,7 +15,7 @@ export const schema = gql`
|
|||||||
partReaction(id: String!): PartReaction
|
partReaction(id: String!): PartReaction
|
||||||
}
|
}
|
||||||
|
|
||||||
input CreatePartReactionInput {
|
input TogglePartReactionInput {
|
||||||
emote: String!
|
emote: String!
|
||||||
userId: String!
|
userId: String!
|
||||||
partId: String!
|
partId: String!
|
||||||
@@ -28,7 +28,7 @@ export const schema = gql`
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Mutation {
|
type Mutation {
|
||||||
createPartReaction(input: CreatePartReactionInput!): PartReaction!
|
togglePartReaction(input: TogglePartReactionInput!): PartReaction!
|
||||||
updatePartReaction(
|
updatePartReaction(
|
||||||
id: String!
|
id: String!
|
||||||
input: UpdatePartReactionInput!
|
input: UpdatePartReactionInput!
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export const schema = gql`
|
|||||||
user: User!
|
user: User!
|
||||||
userId: String!
|
userId: String!
|
||||||
Comment: [Comment]!
|
Comment: [Comment]!
|
||||||
Reaction: [PartReaction]!
|
Reaction(userId: String): [PartReaction]!
|
||||||
}
|
}
|
||||||
|
|
||||||
type Query {
|
type Query {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { AuthenticationError, ForbiddenError, parseJWT } from '@redwoodjs/api'
|
import { AuthenticationError, ForbiddenError } from '@redwoodjs/api'
|
||||||
import { db } from 'src/lib/db'
|
import { db } from 'src/lib/db'
|
||||||
|
|
||||||
export const requireOwnership = async ({ userId, userName, partId } = {}) => {
|
export const requireOwnership = async ({ userId, userName, partId } = {}) => {
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
import { UserInputError } from '@redwoodjs/api'
|
||||||
|
|
||||||
|
import { requireAuth } from 'src/lib/auth'
|
||||||
|
import { requireOwnership } from 'src/lib/owner'
|
||||||
import { db } from 'src/lib/db'
|
import { db } from 'src/lib/db'
|
||||||
import { foreignKeyReplacement } from 'src/services/helpers'
|
import { foreignKeyReplacement } from 'src/services/helpers'
|
||||||
|
|
||||||
@@ -11,10 +15,26 @@ export const partReaction = ({ id }) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createPartReaction = ({ input }) => {
|
export const togglePartReaction = async ({ input }) => {
|
||||||
return db.partReaction.create({
|
// if write fails emote_userId_partId @@unique constraint, then delete it instead
|
||||||
data: foreignKeyReplacement(input),
|
requireAuth()
|
||||||
})
|
await requireOwnership({userId: input?.userId})
|
||||||
|
const legalReactions = ['❤️', '👍', '😄', '🙌'] // TODO figure out a way of sharing code between FE and BE, so this is consistent with web/src/components/EmojiReaction/EmojiReaction.js
|
||||||
|
if(!legalReactions.includes(input.emote)) {
|
||||||
|
throw new UserInputError(`You can't react with '${input.emote}', only the following are allowed: ${legalReactions.join(', ')}`)
|
||||||
|
}
|
||||||
|
let dbPromise
|
||||||
|
const inputClone = {...input} // TODO foreignKeyReplacement mutates input, which I should fix but am lazy right now
|
||||||
|
try{
|
||||||
|
dbPromise = await db.partReaction.create({
|
||||||
|
data: foreignKeyReplacement(input),
|
||||||
|
})
|
||||||
|
} catch(e) {
|
||||||
|
dbPromise = db.partReaction.delete({
|
||||||
|
where: { emote_userId_partId: inputClone},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return dbPromise
|
||||||
}
|
}
|
||||||
|
|
||||||
export const updatePartReaction = ({ id, input }) => {
|
export const updatePartReaction = ({ id, input }) => {
|
||||||
|
|||||||
@@ -57,5 +57,5 @@ export const Part = {
|
|||||||
Comment: (_obj, { root }) =>
|
Comment: (_obj, { root }) =>
|
||||||
db.part.findOne({ where: { id: root.id } }).Comment(),
|
db.part.findOne({ where: { id: root.id } }).Comment(),
|
||||||
Reaction: (_obj, { root }) =>
|
Reaction: (_obj, { root }) =>
|
||||||
db.part.findOne({ where: { id: root.id } }).Reaction(),
|
db.part.findOne({ where: { id: root.id } }).Reaction({where: {userId: _obj.userId}}),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ const noEmotes =[{
|
|||||||
|
|
||||||
const textShadow = {textShadow: '0 4px 6px rgba(0, 0, 0, 0.3)'}
|
const textShadow = {textShadow: '0 4px 6px rgba(0, 0, 0, 0.3)'}
|
||||||
|
|
||||||
const EmojiReaction = ({ emotes, onEmote = () => {}, className }) => {
|
const EmojiReaction = ({ emotes, userEmotes, onEmote = () => {}, className }) => {
|
||||||
const [isOpen, setIsOpen] = useState(false)
|
const [isOpen, setIsOpen] = useState(false)
|
||||||
const [anchorEl, setAnchorEl] = useState(null)
|
const [anchorEl, setAnchorEl] = useState(null)
|
||||||
const [popoverId, setPopoverId] = useState(undefined)
|
const [popoverId, setPopoverId] = useState(undefined)
|
||||||
@@ -59,7 +59,10 @@ const EmojiReaction = ({ emotes, onEmote = () => {}, className }) => {
|
|||||||
<div className="whitespace-no-wrap absolute right-0 inset-y-0 flex items-center flex-row-reverse">
|
<div className="whitespace-no-wrap absolute right-0 inset-y-0 flex items-center flex-row-reverse">
|
||||||
{(emotes.length ? emotes : noEmotes).map((emote, i) => (
|
{(emotes.length ? emotes : noEmotes).map((emote, i) => (
|
||||||
<span
|
<span
|
||||||
className="rounded-full tracking-wide hover:bg-indigo-100 p-1 mx-px transform hover:-translate-y-px transition-all duration-150"
|
className={getActiveClasses(
|
||||||
|
"rounded-full tracking-wide hover:bg-indigo-100 p-1 mx-px transform hover:-translate-y-px transition-all duration-150 border-indigo-400",
|
||||||
|
{'border': userEmotes.includes(emote.emoji)}
|
||||||
|
)}
|
||||||
style={textShadow}
|
style={textShadow}
|
||||||
key={`${emote.emoji}--${i}`}
|
key={`${emote.emoji}--${i}`}
|
||||||
onClick={() => handleEmojiClick(emote.emoji)}
|
onClick={() => handleEmojiClick(emote.emoji)}
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import { navigate, routes } from '@redwoodjs/router'
|
|||||||
import PartReactionForm from 'src/components/PartReactionForm'
|
import PartReactionForm from 'src/components/PartReactionForm'
|
||||||
|
|
||||||
const CREATE_PART_REACTION_MUTATION = gql`
|
const CREATE_PART_REACTION_MUTATION = gql`
|
||||||
mutation CreatePartReactionMutation($input: CreatePartReactionInput!) {
|
mutation TogglePartReactionMutation($input: TogglePartReactionInput!) {
|
||||||
createPartReaction(input: $input) {
|
togglePartReaction(input: $input) {
|
||||||
id
|
id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -12,7 +12,7 @@ const CREATE_PART_REACTION_MUTATION = gql`
|
|||||||
|
|
||||||
const NewPartReaction = () => {
|
const NewPartReaction = () => {
|
||||||
const { addMessage } = useFlash()
|
const { addMessage } = useFlash()
|
||||||
const [createPartReaction, { loading, error }] = useMutation(
|
const [togglePartReaction, { loading, error }] = useMutation(
|
||||||
CREATE_PART_REACTION_MUTATION,
|
CREATE_PART_REACTION_MUTATION,
|
||||||
{
|
{
|
||||||
onCompleted: () => {
|
onCompleted: () => {
|
||||||
@@ -23,7 +23,7 @@ const NewPartReaction = () => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const onSave = (input) => {
|
const onSave = (input) => {
|
||||||
createPartReaction({ variables: { input } })
|
togglePartReaction({ variables: { input } })
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { useMutation, useFlash } from '@redwoodjs/web'
|
import { useMutation, useFlash } from '@redwoodjs/web'
|
||||||
import { navigate, routes } from '@redwoodjs/router'
|
import { navigate, routes } from '@redwoodjs/router'
|
||||||
|
import { useAuth } from '@redwoodjs/auth'
|
||||||
|
|
||||||
import PartProfile from 'src/components/PartProfile'
|
import PartProfile from 'src/components/PartProfile'
|
||||||
|
|
||||||
export const QUERY = gql`
|
export const QUERY = gql`
|
||||||
query FIND_PART_BY_USERNAME_TITLE($userName: String!, $partTitle: String!) {
|
query FIND_PART_BY_USERNAME_TITLE($userName: String!, $partTitle: String!, $currentUserId: String!) {
|
||||||
userPart: userName(userName: $userName) {
|
userPart: userName(userName: $userName) {
|
||||||
id
|
id
|
||||||
name
|
name
|
||||||
@@ -20,6 +21,12 @@ export const QUERY = gql`
|
|||||||
createdAt
|
createdAt
|
||||||
updatedAt
|
updatedAt
|
||||||
userId
|
userId
|
||||||
|
Reaction {
|
||||||
|
emote
|
||||||
|
}
|
||||||
|
userReactions: Reaction(userId: $currentUserId) {
|
||||||
|
emote
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -37,6 +44,14 @@ const UPDATE_PART_MUTATION = gql`
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
const TOGGLE_REACTION_MUTATION = gql`
|
||||||
|
mutation ToggleReactionMutation($input: TogglePartReactionInput!) {
|
||||||
|
togglePartReaction(input: $input){
|
||||||
|
id
|
||||||
|
emote
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
export const Loading = () => <div>Loading...</div>
|
export const Loading = () => <div>Loading...</div>
|
||||||
|
|
||||||
@@ -44,7 +59,8 @@ export const Empty = () => <div>Empty</div>
|
|||||||
|
|
||||||
export const Failure = ({ error }) => <div>Error: {error.message}</div>
|
export const Failure = ({ error }) => <div>Error: {error.message}</div>
|
||||||
|
|
||||||
export const Success = ({ userPart, variables: {isEditable} }) => {
|
export const Success = ({ userPart, variables: {isEditable}, refetch}) => {
|
||||||
|
const { currentUser } = useAuth()
|
||||||
const { addMessage } = useFlash()
|
const { addMessage } = useFlash()
|
||||||
const [updateUser, { loading, error }] = useMutation(UPDATE_PART_MUTATION, {
|
const [updateUser, { loading, error }] = useMutation(UPDATE_PART_MUTATION, {
|
||||||
onCompleted: ({updatePart}) => {
|
onCompleted: ({updatePart}) => {
|
||||||
@@ -52,16 +68,25 @@ export const Success = ({ userPart, variables: {isEditable} }) => {
|
|||||||
addMessage('Part updated.', { classes: 'rw-flash-success' })
|
addMessage('Part updated.', { classes: 'rw-flash-success' })
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const onSave = (id, input) => {
|
const onSave = (id, input) => {
|
||||||
updateUser({ variables: { id, input } })
|
updateUser({ variables: { id, input } })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const [toggleReaction] = useMutation(TOGGLE_REACTION_MUTATION, {
|
||||||
|
onCompleted: (hey) => refetch()
|
||||||
|
})
|
||||||
|
const onReaction = (emote) => toggleReaction({variables: {input: {
|
||||||
|
emote,
|
||||||
|
userId: currentUser.sub,
|
||||||
|
partId: userPart?.Part?.id,
|
||||||
|
}}})
|
||||||
|
|
||||||
return <PartProfile
|
return <PartProfile
|
||||||
userPart={userPart}
|
userPart={userPart}
|
||||||
onSave={onSave}
|
onSave={onSave}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
error={error}
|
error={error}
|
||||||
isEditable={isEditable}
|
isEditable={isEditable}
|
||||||
|
onReaction={onReaction}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,11 +7,14 @@ import ImageUploader from 'src/components/ImageUploader'
|
|||||||
import Breadcrumb from 'src/components/Breadcrumb'
|
import Breadcrumb from 'src/components/Breadcrumb'
|
||||||
import EmojiReaction from 'src/components/EmojiReaction'
|
import EmojiReaction from 'src/components/EmojiReaction'
|
||||||
import Button from 'src/components/Button'
|
import Button from 'src/components/Button'
|
||||||
|
import { countEmotes } from 'src/helpers/emote'
|
||||||
|
|
||||||
const PartProfile = ({userPart, isEditable, onSave, loading, error}) => {
|
const PartProfile = ({userPart, isEditable, onSave, loading, error, onReaction}) => {
|
||||||
const { currentUser } = useAuth()
|
const { currentUser } = useAuth()
|
||||||
const canEdit = currentUser?.sub === userPart.id
|
const canEdit = currentUser?.sub === userPart.id
|
||||||
const part = userPart?.Part
|
const part = userPart?.Part
|
||||||
|
const emotes = countEmotes(part?.Reaction)
|
||||||
|
const userEmotes = part?.userReactions.map(({emote}) => emote)
|
||||||
useEffect(() => {isEditable &&
|
useEffect(() => {isEditable &&
|
||||||
!canEdit &&
|
!canEdit &&
|
||||||
navigate(routes.part2({userName: userPart.userName, partTitle: part.title}))},
|
navigate(routes.part2({userName: userPart.userName, partTitle: part.title}))},
|
||||||
@@ -50,12 +53,10 @@ const PartProfile = ({userPart, isEditable, onSave, loading, error}) => {
|
|||||||
/>
|
/>
|
||||||
<h4 className="text-indigo-800 text-xl underline text-right py-4">{userPart?.name}</h4>
|
<h4 className="text-indigo-800 text-xl underline text-right py-4">{userPart?.name}</h4>
|
||||||
<div className="h-px bg-indigo-200 mb-4" />
|
<div className="h-px bg-indigo-200 mb-4" />
|
||||||
{/* TODO hook up to emoji data properly */}
|
|
||||||
<EmojiReaction
|
<EmojiReaction
|
||||||
// emotes={[{emoji: '❤️',count: 3},{emoji: '😁',count: 2},{emoji: '😜',count: 2},{emoji: '🤩',count: 2},{emoji: '🤣',count: 2},{emoji: '🙌',count: 2},{emoji: '🚀',count: 2}]}
|
emotes={emotes}
|
||||||
emotes={[{emoji: '❤️',count: 3},{emoji: '😁',count: 2}]}
|
userEmotes={userEmotes}
|
||||||
// emotes={[]}
|
onEmote={onReaction}
|
||||||
onEmote={() => {}}
|
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
className="mt-6 ml-auto hover:shadow-lg bg-gradient-to-r from-transparent to-indigo-100"
|
className="mt-6 ml-auto hover:shadow-lg bg-gradient-to-r from-transparent to-indigo-100"
|
||||||
|
|||||||
@@ -1,17 +1,10 @@
|
|||||||
import { useMemo } from 'react'
|
|
||||||
import { Link, routes } from '@redwoodjs/router'
|
import { Link, routes } from '@redwoodjs/router'
|
||||||
|
|
||||||
|
import { countEmotes } from 'src/helpers/emote'
|
||||||
import ImageUploader from 'src/components/ImageUploader'
|
import ImageUploader from 'src/components/ImageUploader'
|
||||||
|
|
||||||
const PartsList = ({ parts }) => {
|
const PartsList = ({ parts }) => {
|
||||||
const countEmotes = (reactions) => {
|
|
||||||
// would be good to do this sever side
|
|
||||||
// counting unique emojis, and limiting to the 5 largest
|
|
||||||
const emoteCounts = {}
|
|
||||||
reactions.forEach(({emote}) => {
|
|
||||||
emoteCounts[emote] = emoteCounts[emote] ? emoteCounts[emote] + 1 : 1
|
|
||||||
})
|
|
||||||
return Object.entries(emoteCounts).map(([emoji, count]) => ({emoji, count})).sort((a,b) => a.count-b.count).slice(-5)
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<section className="max-w-6xl mx-auto mt-20">
|
<section className="max-w-6xl mx-auto mt-20">
|
||||||
<ul className="grid gap-x-8 gap-y-12 items-center mx-4 relative" style={{gridTemplateColumns: 'repeat(auto-fit, minmax(16rem, 1fr))'}}>
|
<ul className="grid gap-x-8 gap-y-12 items-center mx-4 relative" style={{gridTemplateColumns: 'repeat(auto-fit, minmax(16rem, 1fr))'}}>
|
||||||
|
|||||||
10
web/src/helpers/emote.js
Normal file
10
web/src/helpers/emote.js
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
export const countEmotes = (reactions) => {
|
||||||
|
// would be good to do this sever side
|
||||||
|
// counting unique emojis, and limiting to the 5 largest
|
||||||
|
const emoteCounts = {}
|
||||||
|
reactions.forEach(({emote}) => {
|
||||||
|
emoteCounts[emote] = emoteCounts[emote] ? emoteCounts[emote] + 1 : 1
|
||||||
|
})
|
||||||
|
// TODO the sort is causing the emotes to jump around after the user clicks one, not ideal
|
||||||
|
return Object.entries(emoteCounts).map(([emoji, count]) => ({emoji, count})).sort((a,b) => a.count-b.count).slice(-5)
|
||||||
|
}
|
||||||
@@ -1,10 +1,18 @@
|
|||||||
|
import { useAuth } from '@redwoodjs/auth'
|
||||||
|
|
||||||
import MainLayout from 'src/layouts/MainLayout'
|
import MainLayout from 'src/layouts/MainLayout'
|
||||||
import Part2Cell from 'src/components/Part2Cell'
|
import Part2Cell from 'src/components/Part2Cell'
|
||||||
|
|
||||||
const EditPart2Page = ({userName, partTitle}) => {
|
const EditPart2Page = ({userName, partTitle}) => {
|
||||||
|
const { currentUser } = useAuth()
|
||||||
return (
|
return (
|
||||||
<MainLayout>
|
<MainLayout>
|
||||||
<Part2Cell userName={userName} partTitle={partTitle} isEditable/>
|
<Part2Cell
|
||||||
|
userName={userName}
|
||||||
|
partTitle={partTitle}
|
||||||
|
currentUserId={currentUser.sub}
|
||||||
|
isEditable
|
||||||
|
/>
|
||||||
</MainLayout>
|
</MainLayout>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,17 @@
|
|||||||
|
import { useAuth } from '@redwoodjs/auth'
|
||||||
|
|
||||||
import MainLayout from 'src/layouts/MainLayout'
|
import MainLayout from 'src/layouts/MainLayout'
|
||||||
import Part2Cell from 'src/components/Part2Cell'
|
import Part2Cell from 'src/components/Part2Cell'
|
||||||
|
|
||||||
const Part2Page = ({userName, partTitle}) => {
|
const Part2Page = ({userName, partTitle}) => {
|
||||||
|
const { currentUser } = useAuth()
|
||||||
return (
|
return (
|
||||||
<MainLayout>
|
<MainLayout>
|
||||||
<Part2Cell userName={userName} partTitle={partTitle}/>
|
<Part2Cell
|
||||||
|
userName={userName}
|
||||||
|
partTitle={partTitle}
|
||||||
|
currentUserId={currentUser.sub}
|
||||||
|
/>
|
||||||
</MainLayout>
|
</MainLayout>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user