Expose who has liked the parts #192 #199
@@ -13,6 +13,7 @@ export const schema = gql`
|
||||
type Query {
|
||||
partReactions: [PartReaction!]!
|
||||
partReaction(id: String!): PartReaction
|
||||
partReactionsByPartId(partId: String!): [PartReaction!]!
|
||||
}
|
||||
|
||||
input TogglePartReactionInput {
|
||||
|
||||
@@ -15,6 +15,12 @@ export const partReaction = ({ id }) => {
|
||||
})
|
||||
}
|
||||
|
||||
export const partReactionsByPartId = ({ partId }) => {
|
||||
return db.partReaction.findMany({
|
||||
where: { partId: partId },
|
||||
})
|
||||
}
|
||||
|
||||
export const togglePartReaction = async ({ input }) => {
|
||||
// if write fails emote_userId_partId @@unique constraint, then delete it instead
|
||||
requireAuth()
|
||||
|
||||
@@ -20,6 +20,7 @@ const EmojiReaction = ({
|
||||
emotes,
|
||||
userEmotes,
|
||||
onEmote = () => {},
|
||||
onShowPartReactions,
|
||||
className,
|
||||
}) => {
|
||||
const { currentUser } = useAuth()
|
||||
@@ -107,17 +108,20 @@ const EmojiReaction = ({
|
||||
horizontal: 'left',
|
||||
}}
|
||||
>
|
||||
<div className="p-2 pr-3 flex">
|
||||
{emojiMenu.map((emoji, i) => (
|
||||
<button
|
||||
className="p-2"
|
||||
style={textShadow}
|
||||
key={`${emoji}-${i}}`}
|
||||
onClick={() => handleEmojiClick(emoji)}
|
||||
>
|
||||
{emoji}
|
||||
</button>
|
||||
))}
|
||||
<div className="p-2 pr-3 flex flex-col">
|
||||
<div className="inline-flex">
|
||||
{emojiMenu.map((emoji, i) => (
|
||||
<button
|
||||
className="p-2"
|
||||
style={textShadow}
|
||||
key={`${emoji}-${i}}`}
|
||||
onClick={() => handleEmojiClick(emoji)}
|
||||
>
|
||||
{emoji}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<button onClick={onShowPartReactions}>View Reactions</button>
|
||||
</div>
|
||||
</Popover>
|
||||
</>
|
||||
|
||||
@@ -2,12 +2,14 @@ import { useState, useEffect, useRef } from 'react'
|
||||
import { useAuth } from '@redwoodjs/auth'
|
||||
import { Link, navigate, routes } from '@redwoodjs/router'
|
||||
import Editor from 'rich-markdown-editor'
|
||||
import Dialog from '@material-ui/core/Dialog'
|
||||
|
||||
import ImageUploader from 'src/components/ImageUploader'
|
||||
import ConfirmDialog from 'src/components/ConfirmDialog'
|
||||
import Breadcrumb from 'src/components/Breadcrumb'
|
||||
import EmojiReaction from 'src/components/EmojiReaction'
|
||||
import Button from 'src/components/Button'
|
||||
import PartReactionsCell from '../PartReactionsCell'
|
||||
import { countEmotes } from 'src/helpers/emote'
|
||||
import { getActiveClasses } from 'get-active-classes'
|
||||
|
||||
@@ -21,6 +23,7 @@ const PartProfile = ({
|
||||
}) => {
|
||||
const [comment, setComment] = useState('')
|
||||
const [isConfirmDialogOpen, setIsConfirmDialogOpen] = useState(false)
|
||||
const [isReactionsModalOpen, setIsReactionsModalOpen] = useState(false)
|
||||
const [isInvalid, setIsInvalid] = useState(false)
|
||||
const { currentUser } = useAuth()
|
||||
const editorRef = useRef(null)
|
||||
@@ -94,6 +97,7 @@ const PartProfile = ({
|
||||
emotes={emotes}
|
||||
userEmotes={userEmotes}
|
||||
onEmote={onReaction}
|
||||
onShowPartReactions={() => setIsReactionsModalOpen(true)}
|
||||
/>
|
||||
<Button
|
||||
className="mt-6 ml-auto hover:shadow-lg bg-gradient-to-r from-transparent to-indigo-100"
|
||||
@@ -272,6 +276,14 @@ const PartProfile = ({
|
||||
onConfirm={onDelete}
|
||||
message="Are you sure you want to delete? This action cannot be undone."
|
||||
/>
|
||||
<Dialog
|
||||
open={isReactionsModalOpen}
|
||||
onClose={() => setIsReactionsModalOpen(false)}
|
||||
fullWidth={true}
|
||||
maxWidth={'sm'}
|
||||
>
|
||||
<PartReactionsCell partId={userPart?.Part?.id} />
|
||||
</Dialog>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
75
web/src/components/PartReactions/PartReactions.js
Normal file
75
web/src/components/PartReactions/PartReactions.js
Normal file
@@ -0,0 +1,75 @@
|
||||
import { useState } from 'react'
|
||||
import Tab from '@material-ui/core/Tab'
|
||||
import Tabs from '@material-ui/core/Tabs'
|
||||
import { Link, routes } from '@redwoodjs/router'
|
||||
import { countEmotes } from 'src/helpers/emote'
|
||||
import ImageUploader from 'src/components/ImageUploader'
|
||||
|
||||
const PartReactions = ({ reactions }) => {
|
||||
const emotes = countEmotes(reactions)
|
||||
const [tab, setTab] = useState(0)
|
||||
const onTabChange = (_, newValue) => {
|
||||
setTab(newValue)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-gray-100 p-4 min-h-md rounded-lg shadow-lg">
|
||||
<Tabs
|
||||
value={tab}
|
||||
onChange={onTabChange}
|
||||
variant="scrollable"
|
||||
scrollButtons="off"
|
||||
textColor="primary"
|
||||
indicatorColor="primary"
|
||||
>
|
||||
<Tab label="All" style={{ minWidth: 100 }} />
|
||||
{emotes.map((emote, i) => (
|
||||
<Tab
|
||||
label={`${emote.emoji} ${emote.count}`}
|
||||
key={`${emote.emoji}-${i}}`}
|
||||
style={{ minWidth: 100 }}
|
||||
/>
|
||||
))}
|
||||
</Tabs>
|
||||
<ul>
|
||||
{reactions
|
||||
.filter((reaction) =>
|
||||
tab === 0 ? true : reaction.emote === emotes[tab - 1].emoji
|
||||
)
|
||||
.map((reactionPart, i) => (
|
||||
<li
|
||||
className="flex flex-row justify-between p-3 items-center"
|
||||
key={`${reactionPart.emote}-${i}}`}
|
||||
>
|
||||
<div className="flex items-center justify-center">
|
||||
<div className="w-8 h-8 overflow-hidden rounded-full border border-indigo-300 shadow flex-shrink-0">
|
||||
<ImageUploader
|
||||
className=""
|
||||
aspectRatio={1}
|
||||
imageUrl={reactionPart.user?.image}
|
||||
width={50}
|
||||
/>
|
||||
</div>
|
||||
<div className="ml-4 font-roboto">
|
||||
<div className="text-gray-800 font-normal text-md mb-1">
|
||||
<Link
|
||||
to={routes.user({
|
||||
userName: reactionPart.user?.userName,
|
||||
})}
|
||||
>
|
||||
{reactionPart.user?.userName}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<span>{reactionPart.emote}</span>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default PartReactions
|
||||
26
web/src/components/PartReactionsCell/PartReactionsCell.js
Normal file
26
web/src/components/PartReactionsCell/PartReactionsCell.js
Normal file
@@ -0,0 +1,26 @@
|
||||
import PartReactions from 'src/components/PartReactions'
|
||||
|
||||
export const QUERY = gql`
|
||||
query PartReactionsQuery($partId: String!) {
|
||||
partReactionsByPartId(partId: $partId) {
|
||||
id
|
||||
emote
|
||||
user {
|
||||
id
|
||||
userName
|
||||
image
|
||||
}
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const Loading = () => <div>Loading...</div>
|
||||
|
||||
export const Empty = () => <div>Empty</div>
|
||||
|
|
||||
|
||||
export const Failure = ({ error }) => <div>Error: {error.message}</div>
|
||||
|
||||
export const Success = ({ partReactionsByPartId }) => {
|
||||
return <PartReactions reactions={partReactionsByPartId} />
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// Define your own mock data here:
|
||||
export const standard = (/* vars, { ctx, req } */) => ({
|
||||
partReactions: {
|
||||
id: 42,
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,20 @@
|
||||
import { Loading, Empty, Failure, Success } from './PartReactionsCell'
|
||||
import { standard } from './PartReactionsCell.mock'
|
||||
|
||||
export const loading = () => {
|
||||
return Loading ? <Loading /> : null
|
||||
}
|
||||
|
||||
export const empty = () => {
|
||||
return Empty ? <Empty /> : null
|
||||
}
|
||||
|
||||
export const failure = () => {
|
||||
return Failure ? <Failure error={new Error('Oh no')} /> : null
|
||||
}
|
||||
|
||||
export const success = () => {
|
||||
return Success ? <Success {...standard()} /> : null
|
||||
}
|
||||
|
||||
export default { title: 'Cells/PartReactionsCell' }
|
||||
@@ -0,0 +1,26 @@
|
||||
import { render, screen } from '@redwoodjs/testing'
|
||||
import { Loading, Empty, Failure, Success } from './PartReactionsCell'
|
||||
import { standard } from './PartReactionsCell.mock'
|
||||
|
||||
describe('PartReactionsCell', () => {
|
||||
test('Loading renders successfully', () => {
|
||||
render(<Loading />)
|
||||
// Use screen.debug() to see output
|
||||
expect(screen.getByText('Loading...')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
test('Empty renders successfully', async () => {
|
||||
render(<Empty />)
|
||||
expect(screen.getByText('Empty')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
test('Failure renders successfully', async () => {
|
||||
render(<Failure error={new Error('Oh no')} />)
|
||||
expect(screen.getByText(/Oh no/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
test('Success renders successfully', async () => {
|
||||
render(<Success partReactions={standard().partReactions} />)
|
||||
expect(screen.getByText(/42/i)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user
Let's make the empty state look a little nicer, even just
Should be good