Skip to content

Commit 03879ac

Browse files
authored
feat: integrating search (#21)
1 parent 0430c67 commit 03879ac

39 files changed

+1549
-272
lines changed

DONE.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,10 @@
145145
- [x] Integrate home upcoming games
146146
- [x] Integrate home most liked games
147147
- [x] Integrate game details
148+
- [x] Integrate heart button
149+
- [x] Heart games
150+
- [x] Heart comments and replies
151+
- [x] Heart blog posts
148152

149153
### Post MVP
150154

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import type { Meta, StoryObj } from '@storybook/react'
2+
import { Provider } from 'react-redux'
3+
import { MemoryRouter } from 'react-router-dom'
4+
5+
import { MOCK_GAME_DETAILS } from '@/mocks'
6+
import store from '@/store'
7+
8+
import Comments from './Comments'
9+
10+
const meta: Meta<typeof Comments> = {
11+
title: 'Components/Comments',
12+
component: Comments,
13+
args: {
14+
defaultComments: MOCK_GAME_DETAILS.comments,
15+
},
16+
}
17+
18+
type Story = StoryObj<typeof meta>
19+
20+
export const Default: Story = {
21+
render: (args) => (
22+
<Provider store={store}>
23+
<MemoryRouter>
24+
<Comments {...args} />
25+
</MemoryRouter>
26+
</Provider>
27+
),
28+
}
29+
30+
export default meta

src/components/Comments/Comments.tsx

Lines changed: 40 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,9 @@ import {
1010
} from '@mui/material'
1111
import { formatRelative } from 'date-fns'
1212
import { useEffect, useState } from 'react'
13-
import {
14-
IoChatboxEllipsesOutline,
15-
IoHeart,
16-
IoHeartOutline,
17-
IoSendOutline,
18-
} from 'react-icons/io5'
13+
import { IoChatboxEllipsesOutline, IoSendOutline } from 'react-icons/io5'
1914

20-
import { HeartsUp, Input } from '@/components'
15+
import { HeartButton, HeartsUp, Input } from '@/components'
2116
import { Comment } from '@/types'
2217

2318
interface CommentsProps {
@@ -38,24 +33,20 @@ function Comments(props: CommentsProps) {
3833
const handleHeartClick = (item: Comment, parentId?: number) => {
3934
const isReply = Boolean(parentId)
4035
const itemId = isReply ? `${parentId}-${item.id}` : item.id
41-
const isLiked = likedComments.has(itemId)
4236

4337
setLikedComments((prev) => {
4438
const updatedLikes = new Set(prev)
39+
const isLiked = updatedLikes.has(itemId)
4540

4641
if (isLiked) {
4742
updatedLikes.delete(itemId)
4843
} else {
4944
updatedLikes.add(itemId)
5045
}
5146

52-
return updatedLikes
53-
})
54-
55-
if (isReply && parentId) {
5647
setComments((prevComments) =>
5748
prevComments.map((comment) =>
58-
comment.id === parentId
49+
isReply && parentId && comment.id === parentId
5950
? {
6051
...comment,
6152
replies: comment.replies.map((reply) =>
@@ -69,27 +60,18 @@ function Comments(props: CommentsProps) {
6960
: reply,
7061
),
7162
}
72-
: comment,
63+
: !isReply && comment.id === item.id
64+
? {
65+
...comment,
66+
hearts_count: comment.hearts_count + (isLiked ? -1 : 1),
67+
is_hearted: !isLiked,
68+
}
69+
: comment,
7370
),
7471
)
75-
} else {
76-
setComments((prevComments) =>
77-
prevComments.map((c) =>
78-
c.id === item.id
79-
? {
80-
...c,
81-
hearts_count: c.hearts_count + (isLiked ? -1 : 1),
82-
is_hearted: !isLiked,
83-
}
84-
: c,
85-
),
86-
)
87-
}
8872

89-
if (!isLiked) {
90-
const newHearts = Array.from({ length: 10 }, (_, i) => i * 100)
91-
setHeartPops((prev) => [...prev, ...newHearts])
92-
}
73+
return updatedLikes
74+
})
9375
}
9476

9577
const handleSendMessage = () => {
@@ -227,18 +209,18 @@ function Comments(props: CommentsProps) {
227209
</Tooltip>
228210

229211
<Box className="flex items-center gap-2">
230-
<Tooltip title="Like this comment">
231-
<IconButton
232-
onClick={() => handleHeartClick(comment)}
233-
className="relative transition-transform duration-300 hover:scale-125 ease-in-out">
234-
{likedComments.has(comment.id) ||
235-
comment.is_hearted ? (
236-
<IoHeart className="text-red-500" />
237-
) : (
238-
<IoHeartOutline className="text-theme-red-900 dark:text-gray-400" />
239-
)}
240-
</IconButton>
241-
</Tooltip>
212+
<HeartButton
213+
heartable_id={comment.id}
214+
heartable_type="commentables"
215+
setHeartPops={setHeartPops}
216+
isHearted={
217+
likedComments.has(comment.id) || comment.is_hearted
218+
}
219+
type="icon"
220+
setHearts={() => {}}
221+
onHeartToggle={() => handleHeartClick(comment)}
222+
size={28}
223+
/>
242224
<Typography
243225
variant="body2"
244226
className="dark:text-white text-gray-800">
@@ -306,21 +288,22 @@ function Comments(props: CommentsProps) {
306288
<IoChatboxEllipsesOutline />
307289
</IconButton>
308290
</Tooltip>
309-
<Tooltip title="Like this comment">
310-
<IconButton
311-
onClick={() =>
312-
handleHeartClick(reply, comment.id)
313-
}
314-
className="transition-transform duration-300 hover:scale-125 ease-in-out">
315-
{likedComments.has(
291+
<HeartButton
292+
heartable_id={reply.id}
293+
heartable_type="commentables"
294+
setHeartPops={setHeartPops}
295+
isHearted={
296+
likedComments.has(
316297
`${comment.id}-${reply.id}`,
317-
) || reply.is_hearted ? (
318-
<IoHeart className="text-red-500" />
319-
) : (
320-
<IoHeartOutline className="text-theme-red-900 dark:text-gray-400" />
321-
)}
322-
</IconButton>
323-
</Tooltip>
298+
) || reply.is_hearted
299+
}
300+
type="icon"
301+
setHearts={() => {}}
302+
onHeartToggle={() =>
303+
handleHeartClick(reply, comment.id)
304+
}
305+
size={28}
306+
/>
324307
<Typography
325308
variant="body2"
326309
className="dark:text-white text-gray-800">
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import type { Meta, StoryObj } from '@storybook/react'
2+
3+
import CookieConsent from './CookieConsent'
4+
5+
const meta: Meta<typeof CookieConsent> = {
6+
title: 'Components/CookieConsent',
7+
component: CookieConsent,
8+
args: {
9+
visible: true,
10+
},
11+
argTypes: {
12+
visible: {
13+
type: 'boolean',
14+
control: {
15+
type: 'boolean',
16+
},
17+
},
18+
},
19+
}
20+
21+
type Story = StoryObj<typeof meta>
22+
23+
export const Default: Story = {
24+
render: (args) => <CookieConsent {...args} />,
25+
}
26+
27+
export default meta
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { fireEvent, render } from '@testing-library/react'
2+
import { useCookies } from 'react-cookie'
3+
4+
import CookieConsent from './CookieConsent'
5+
6+
jest.mock('react-cookie', () => ({
7+
useCookies: jest.fn(),
8+
}))
9+
10+
const renderCookieConsent = () => {
11+
return render(<CookieConsent />)
12+
}
13+
14+
describe('CookieConsent Component', () => {
15+
beforeEach(() => {
16+
jest.clearAllMocks()
17+
})
18+
19+
it('renders cookie consent when cookie is not set', () => {
20+
;(useCookies as jest.Mock).mockReturnValue([{}, jest.fn()])
21+
22+
const { getByText, getByRole } = renderCookieConsent()
23+
24+
expect(getByText(/we need cookies!/i)).toBeInTheDocument()
25+
expect(
26+
getByText(/to provide the best experience/i),
27+
).toBeInTheDocument()
28+
expect(getByRole('button', { name: /accept/i })).toBeInTheDocument()
29+
expect(getByRole('button', { name: /decline/i })).toBeInTheDocument()
30+
})
31+
32+
it('hides the consent message when the "Accept" button is clicked', () => {
33+
const setCookieMock = jest.fn()
34+
;(useCookies as jest.Mock).mockReturnValue([{}, setCookieMock])
35+
36+
const { queryByText, getByRole } = renderCookieConsent()
37+
38+
fireEvent.click(getByRole('button', { name: /accept/i }))
39+
40+
expect(setCookieMock).toHaveBeenCalledWith(
41+
'_gc_accept_cookies',
42+
'1',
43+
expect.objectContaining({
44+
path: '/',
45+
maxAge: 365 * 24 * 60 * 60,
46+
}),
47+
)
48+
expect(queryByText(/we need cookies!/i)).not.toBeInTheDocument()
49+
})
50+
51+
it('hides the consent message when the "Decline" button is clicked', () => {
52+
const setCookieMock = jest.fn()
53+
;(useCookies as jest.Mock).mockReturnValue([{}, setCookieMock])
54+
55+
const { queryByText, getByRole } = renderCookieConsent()
56+
57+
fireEvent.click(getByRole('button', { name: /decline/i }))
58+
59+
expect(setCookieMock).toHaveBeenCalledWith(
60+
'_gc_accept_cookies',
61+
'1',
62+
expect.objectContaining({
63+
path: '/',
64+
maxAge: 365 * 24 * 60 * 60,
65+
}),
66+
)
67+
expect(queryByText(/we need cookies!/i)).not.toBeInTheDocument()
68+
})
69+
70+
it('does not render the consent message if the cookie is already set', () => {
71+
;(useCookies as jest.Mock).mockReturnValue([
72+
{ _gc_accept_cookies: '1' },
73+
jest.fn(),
74+
])
75+
76+
const { queryByText } = renderCookieConsent()
77+
78+
expect(queryByText(/we need cookies!/i)).not.toBeInTheDocument()
79+
})
80+
})

src/components/CookieConsent/CookieConsent.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,15 @@ import { useEffect, useState } from 'react'
33
import { useCookies } from 'react-cookie'
44
import { IoCheckmarkCircleOutline } from 'react-icons/io5'
55

6-
import Logo from '../Logo'
6+
import { Logo } from '..'
77

8-
const CookieConsent = () => {
9-
const [isVisible, setIsVisible] = useState(false)
8+
export interface CookieConsentProps {
9+
visible?: boolean
10+
}
11+
12+
function CookieConsent(props: CookieConsentProps) {
13+
const { visible = false } = props
14+
const [isVisible, setIsVisible] = useState<boolean>(visible)
1015
const [cookies, setCookie] = useCookies(['_gc_accept_cookies'])
1116

1217
useEffect(() => {

src/components/GameCard/GameCard.stories.tsx

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import type { Meta, StoryObj } from '@storybook/react'
2+
import { Provider } from 'react-redux'
3+
import { MemoryRouter } from 'react-router-dom'
24

35
import { MOCK_HOT_GAMES } from '@/mocks'
6+
import store from '@/store'
47
import { GameList } from '@/types'
58

69
import GameCard from './GameCard'
@@ -24,10 +27,14 @@ type Story = StoryObj<typeof meta>
2427

2528
export const Default: Story = {
2629
render: (args) => (
27-
<div
28-
className={`container grid ${args.view === 'grid' ? 'lg:grid-cols-3 sm:grid-cols-2 grid-cols-1' : 'grid-cols-1'}`}>
29-
<GameCard key={1} {...args} />
30-
</div>
30+
<Provider store={store}>
31+
<MemoryRouter>
32+
<div
33+
className={`container grid ${args.view === 'grid' ? 'lg:grid-cols-3 sm:grid-cols-2 grid-cols-1' : 'grid-cols-1'}`}>
34+
<GameCard key={1} {...args} />
35+
</div>
36+
</MemoryRouter>
37+
</Provider>
3138
),
3239
}
3340

0 commit comments

Comments
 (0)