Merge pull request #199 from yencolon/main
Expose who has liked the parts #192
This commit was merged in pull request #199.
This commit is contained in:
@@ -13,6 +13,7 @@ export const schema = gql`
|
|||||||
type Query {
|
type Query {
|
||||||
partReactions: [PartReaction!]!
|
partReactions: [PartReaction!]!
|
||||||
partReaction(id: String!): PartReaction
|
partReaction(id: String!): PartReaction
|
||||||
|
partReactionsByPartId(partId: String!): [PartReaction!]!
|
||||||
}
|
}
|
||||||
|
|
||||||
input TogglePartReactionInput {
|
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 }) => {
|
export const togglePartReaction = async ({ input }) => {
|
||||||
// if write fails emote_userId_partId @@unique constraint, then delete it instead
|
// if write fails emote_userId_partId @@unique constraint, then delete it instead
|
||||||
requireAuth()
|
requireAuth()
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ const EmojiReaction = ({
|
|||||||
emotes,
|
emotes,
|
||||||
userEmotes,
|
userEmotes,
|
||||||
onEmote = () => {},
|
onEmote = () => {},
|
||||||
|
onShowPartReactions,
|
||||||
className,
|
className,
|
||||||
}) => {
|
}) => {
|
||||||
const { currentUser } = useAuth()
|
const { currentUser } = useAuth()
|
||||||
@@ -107,17 +108,20 @@ const EmojiReaction = ({
|
|||||||
horizontal: 'left',
|
horizontal: 'left',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="p-2 pr-3 flex">
|
<div className="p-2 pr-3 flex flex-col">
|
||||||
{emojiMenu.map((emoji, i) => (
|
<div className="inline-flex">
|
||||||
<button
|
{emojiMenu.map((emoji, i) => (
|
||||||
className="p-2"
|
<button
|
||||||
style={textShadow}
|
className="p-2"
|
||||||
key={`${emoji}-${i}}`}
|
style={textShadow}
|
||||||
onClick={() => handleEmojiClick(emoji)}
|
key={`${emoji}-${i}}`}
|
||||||
>
|
onClick={() => handleEmojiClick(emoji)}
|
||||||
{emoji}
|
>
|
||||||
</button>
|
{emoji}
|
||||||
))}
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<button onClick={onShowPartReactions}>View Reactions</button>
|
||||||
</div>
|
</div>
|
||||||
</Popover>
|
</Popover>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -2,12 +2,14 @@ import { useState, useEffect, useRef } from 'react'
|
|||||||
import { useAuth } from '@redwoodjs/auth'
|
import { useAuth } from '@redwoodjs/auth'
|
||||||
import { Link, navigate, routes } from '@redwoodjs/router'
|
import { Link, navigate, routes } from '@redwoodjs/router'
|
||||||
import Editor from 'rich-markdown-editor'
|
import Editor from 'rich-markdown-editor'
|
||||||
|
import Dialog from '@material-ui/core/Dialog'
|
||||||
|
|
||||||
import ImageUploader from 'src/components/ImageUploader'
|
import ImageUploader from 'src/components/ImageUploader'
|
||||||
import ConfirmDialog from 'src/components/ConfirmDialog'
|
import ConfirmDialog from 'src/components/ConfirmDialog'
|
||||||
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 PartReactionsCell from '../PartReactionsCell'
|
||||||
import { countEmotes } from 'src/helpers/emote'
|
import { countEmotes } from 'src/helpers/emote'
|
||||||
import { getActiveClasses } from 'get-active-classes'
|
import { getActiveClasses } from 'get-active-classes'
|
||||||
|
|
||||||
@@ -21,6 +23,7 @@ const PartProfile = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const [comment, setComment] = useState('')
|
const [comment, setComment] = useState('')
|
||||||
const [isConfirmDialogOpen, setIsConfirmDialogOpen] = useState(false)
|
const [isConfirmDialogOpen, setIsConfirmDialogOpen] = useState(false)
|
||||||
|
const [isReactionsModalOpen, setIsReactionsModalOpen] = useState(false)
|
||||||
const [isInvalid, setIsInvalid] = useState(false)
|
const [isInvalid, setIsInvalid] = useState(false)
|
||||||
const { currentUser } = useAuth()
|
const { currentUser } = useAuth()
|
||||||
const editorRef = useRef(null)
|
const editorRef = useRef(null)
|
||||||
@@ -94,6 +97,7 @@ const PartProfile = ({
|
|||||||
emotes={emotes}
|
emotes={emotes}
|
||||||
userEmotes={userEmotes}
|
userEmotes={userEmotes}
|
||||||
onEmote={onReaction}
|
onEmote={onReaction}
|
||||||
|
onShowPartReactions={() => setIsReactionsModalOpen(true)}
|
||||||
/>
|
/>
|
||||||
<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"
|
||||||
@@ -272,6 +276,14 @@ const PartProfile = ({
|
|||||||
onConfirm={onDelete}
|
onConfirm={onDelete}
|
||||||
message="Are you sure you want to delete? This action cannot be undone."
|
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