Expose who has liked the parts #192 #199

Merged
yencolon merged 5 commits from main into main 2021-01-24 03:14:50 +01:00
10 changed files with 187 additions and 11 deletions

View File

@@ -13,6 +13,7 @@ export const schema = gql`
type Query {
partReactions: [PartReaction!]!
partReaction(id: String!): PartReaction
partReactionsByPartId(partId: String!): [PartReaction!]!
}
input TogglePartReactionInput {

View File

@@ -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()

View File

@@ -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>
</>

View File

@@ -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>
</>
)
}

View 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

View 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>
Irev-Dev commented 2021-01-20 20:22:32 +01:00 (Migrated from github.com)
Review

Let's make the empty state look a little nicer, even just

<div className="text-center py-8 font-roboto text-gray-700">
  No reactions to this part yet 😕
</div>

Should be good

Let's make the empty state look a little nicer, even just ``` <div className="text-center py-8 font-roboto text-gray-700"> No reactions to this part yet 😕 </div> ``` Should be good
export const Failure = ({ error }) => <div>Error: {error.message}</div>
export const Success = ({ partReactionsByPartId }) => {
return <PartReactions reactions={partReactionsByPartId} />
}

View File

@@ -0,0 +1,6 @@
// Define your own mock data here:
export const standard = (/* vars, { ctx, req } */) => ({
partReactions: {
id: 42,
},
})

View File

@@ -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' }

View File

@@ -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()
})
})