Skip to content
This repository was archived by the owner on Apr 25, 2023. It is now read-only.

Commit 7701570

Browse files
authored
1 parent 69b92f1 commit 7701570

File tree

7 files changed

+789
-34
lines changed

7 files changed

+789
-34
lines changed

components/Comments/Comments.tsx

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import { createClient } from '@supabase/supabase-js'
2+
import { useCallback, useEffect, useState } from 'react'
3+
import { v4 as uuid } from 'uuid'
4+
import { CommentsBox } from './CommentsBox'
5+
6+
const Channel = 'blog-posts'
7+
8+
const supabase = createClient(
9+
'https://zegkgkeylxjinmkhvszq.supabase.co',
10+
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlhdCI6MTYzNDYwOTE1MiwiZXhwIjoxOTUwMTg1MTUyfQ.Hk_ghyYR5dVn5ILfdB5iFX5vxQnwIBwegWJcp7TJloE',
11+
{
12+
realtime: {
13+
params: {
14+
eventsPerSecond: 10,
15+
},
16+
},
17+
},
18+
)
19+
20+
export interface Comment {
21+
id: string
22+
created_at: Date
23+
blog_id: string
24+
name?: string
25+
email?: string
26+
text?: string
27+
vote?: number
28+
image?: string
29+
}
30+
31+
export const Comments = function ({ slug }: { slug: string }) {
32+
const [comments, setComments] = useState<Comment[]>([])
33+
34+
const shouldAddComment = useCallback(
35+
(comment: Comment): boolean => {
36+
const seen = new Set<string>()
37+
comments.forEach((c) => seen.add(c.id))
38+
return !seen.has(comment.id)
39+
},
40+
[comments],
41+
)
42+
43+
useEffect(() => {
44+
supabase
45+
.channel(Channel)
46+
.on(
47+
'postgres_changes',
48+
{
49+
event: 'INSERT',
50+
schema: 'public',
51+
table: 'comments',
52+
filter: `blog_id=eq.${slug}`,
53+
},
54+
(payload) => {
55+
const comment = payload.new as Comment
56+
if (shouldAddComment(comment)) {
57+
setComments((prev) => [...prev, comment])
58+
}
59+
},
60+
)
61+
.subscribe()
62+
return () => {
63+
supabase.channel(Channel).unsubscribe()
64+
}
65+
}, [shouldAddComment, setComments, slug])
66+
67+
const onComment = async ({ email, name, body }: { email: string; name: string; body: string }) => {
68+
const unique_id = uuid()
69+
const comment = {
70+
id: unique_id,
71+
blog_id: slug,
72+
name,
73+
email,
74+
vote: 0,
75+
text: body,
76+
} as Comment
77+
if (shouldAddComment(comment)) {
78+
setComments((prev) => [...prev, comment])
79+
}
80+
await supabase.from('comments').insert(comment)
81+
}
82+
83+
const onUpvote = async (id: string) => {
84+
const promises: any[] = []
85+
const updatedComments: Comment[] = []
86+
for (const comment of comments) {
87+
if (comment.id === id) {
88+
comment.vote = (comment?.vote || 0) + 1
89+
promises.push(supabase.from('comments').update(comment).eq('id', comment.id))
90+
}
91+
updatedComments.push(comment)
92+
}
93+
setComments(updatedComments)
94+
await Promise.all(promises)
95+
}
96+
97+
// for populating comments
98+
useEffect(() => {
99+
const getComments = async () => {
100+
return (await supabase.from('comments').select().eq('blog_id', slug)).data as Comment[]
101+
}
102+
getComments().then((d) => d && setComments(d))
103+
}, [setComments, slug])
104+
105+
return <CommentsBox comments={comments} onSubmit={onComment} onUpvote={onUpvote} />
106+
}

components/Comments/CommentsBox.tsx

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import { Button, message } from 'antd'
2+
import classNames from 'classnames'
3+
import Image from 'next/image'
4+
import { useState } from 'react'
5+
import { Typography } from '../common/Typography/Typography'
6+
import type { Comment } from './Comments'
7+
8+
const upvoteButton = classNames('bg-[#131032] rounded border-2 border-[#1E2848] p-2')
9+
const section = classNames('col-4 border-2 border-[#1E2848] rounded-xl p-6 my-3')
10+
const input = classNames(
11+
'p-3 h-12 leading-none bg-transparent outline-none text-copy-on-dark text-[17px] rounded-xl bg-[#72E4FC0F] border-2 border-[#72E4FC1C]',
12+
)
13+
14+
export const CommentsBox = function ({
15+
comments,
16+
onUpvote,
17+
onSubmit,
18+
}: {
19+
comments: Comment[]
20+
onUpvote: (id: string) => void
21+
onSubmit: (c: { email: string; name: string; body: string }) => void
22+
}) {
23+
const [name, setName] = useState<string>()
24+
const [email, setEmail] = useState<string>()
25+
const [body, setBody] = useState<string>()
26+
27+
const numComments = comments.length
28+
return (
29+
<div className="w-full flex justify-center">
30+
<div className="grid-flow-row w-[810px]">
31+
<div className={section}>
32+
<div className="w-full flex justify-between">
33+
<Typography type="copy1">Comments ({numComments})</Typography>
34+
</div>
35+
</div>
36+
{numComments ? (
37+
<div className={section}>
38+
<div className="w-full flex flex-col gap-4">
39+
{comments.map((c) => (
40+
<div key={c.id} className="flex w-full gap-4">
41+
{c.image && (
42+
<Image className="rounded-full h-8 w-8" alt={'profile pic'} src={c.image} width={32} height={32} />
43+
)}
44+
<div className="flex flex-col gap-2">
45+
<div className="flex flex-col">
46+
<Typography type="copy4" emphasis>
47+
{c.name}
48+
</Typography>
49+
<Typography type="copy4" className="text-[#FFFFFF8F]">
50+
{c.email}
51+
</Typography>
52+
</div>
53+
<Typography type="copy2">{c.text}</Typography>
54+
<div className="flex flex-row gap-2">
55+
<button className={upvoteButton} onClick={() => onUpvote(c.id)}>
56+
👍 <Typography type="copy4">{c.vote}</Typography>
57+
</button>
58+
</div>
59+
</div>
60+
</div>
61+
))}
62+
</div>
63+
</div>
64+
) : null}
65+
<div className={section}>
66+
<div className="w-full flex flex-col gap-5">
67+
<div className="grid grid-cols-2 gap-5">
68+
<div className="flex flex-col gap-1">
69+
<Typography type="copy4" className="text-[#FFFFFF8F]">
70+
Name
71+
</Typography>
72+
<input
73+
type="text"
74+
placeholder={`Brian`}
75+
value={name}
76+
onChange={(ev) => setName(ev.currentTarget.value)}
77+
className={input}
78+
/>
79+
</div>
80+
<div className="flex flex-col gap-1">
81+
<Typography type="copy4" className="text-[#FFFFFF8F]">
82+
Email
83+
</Typography>
84+
<input
85+
type="email"
86+
placeholder={`[email protected]`}
87+
value={email}
88+
onChange={(ev) => setEmail(ev.currentTarget.value)}
89+
className={input}
90+
/>
91+
</div>
92+
</div>
93+
<div className="flex flex-col gap-1">
94+
<Typography type="copy4" className="text-[#FFFFFF8F]">
95+
Your Message
96+
</Typography>
97+
<input
98+
type="text"
99+
placeholder={`hello there...`}
100+
value={body}
101+
onChange={(ev) => setBody(ev.currentTarget.value)}
102+
className={input}
103+
/>
104+
</div>
105+
<div className="flex w-full gap-4">
106+
<Button
107+
className="bg-blue-cta text-black px-3 py-1 rounded-lg"
108+
size={'small'}
109+
onClick={() => {
110+
if (!email || !name || !body) {
111+
message.warn('Please fill out the name, email, and comment body.', 5)
112+
return
113+
}
114+
onSubmit({
115+
email,
116+
name,
117+
body,
118+
})
119+
}}
120+
>
121+
<Typography type="copy3" emphasis={true}>
122+
New Comment
123+
</Typography>
124+
</Button>
125+
</div>
126+
</div>
127+
</div>
128+
</div>
129+
</div>
130+
)
131+
}

components/Home/CompaniesReel/CompaniesReel.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,12 @@ export const CompaniesReel = () => {
2828
</div>
2929
<div className={styles.customerReel}>
3030
<div className={styles.companies}>
31-
<Image src={Pipe} alt="Pipe" className={styles.scaleHeight} style={{ transform: 'scale(0.9)' }} />
31+
<Image
32+
src={Pipe}
33+
alt="Pipe"
34+
className={styles.scaleHeight}
35+
style={{ transform: 'scale(0.9)' }}
36+
/>
3237
<Image src={Portal} alt="Portal" className={styles.scaleHeight} />
3338
<Image src={Dripos} alt="Dripos" className={styles.scaleHeight} />
3439
<Image src={Knock} alt="Knock" className={styles.scaleHeight} />

next.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ const nextConfig = {
1515
},
1616
compress: true,
1717
images: {
18-
domains: ['media.graphassets.com', 'lh3.googleusercontent.com'],
18+
domains: ['media.graphassets.com', 'lh3.googleusercontent.com', 'picsum.photos'],
1919
},
2020
productionBrowserSourceMaps: true,
2121
reactStrictMode: true,

package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"@heroicons/react": "^2.0.13",
2020
"@highlight-run/next": "^2.2.3",
2121
"@radix-ui/react-slider": "^1.1.0",
22+
"@supabase/supabase-js": "^2.7.1",
2223
"@types/js-cookie": "^3.0.2",
2324
"@vercel/og": "^0.0.27",
2425
"antd": "^4.24.7",
@@ -62,7 +63,9 @@
6263
"remark-gfm": "^3.0.1",
6364
"remove-markdown": "^0.5.0",
6465
"rudder-sdk-js": "^2.20.0",
65-
"sass": "^1.56.1"
66+
"sass": "^1.56.1",
67+
"sharp": "^0.31.3",
68+
"uuidv4": "^6.2.13"
6669
},
6770
"devDependencies": {
6871
"@svgr/webpack": "^6.5.1",

pages/blog/[slug].tsx

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,28 @@
1-
import Image from 'next/legacy/image'
2-
import PlayButton from '../../public/images/playButton.svg'
3-
import homeStyles from '../../components/Home/Home.module.scss'
4-
import styles from '../../components/Blog/Blog.module.scss'
5-
import { Section } from '../../components/common/Section/Section'
6-
import Footer from '../../components/common/Footer/Footer'
7-
import { gql } from 'graphql-request'
81
import classNames from 'classnames'
2+
import { gql } from 'graphql-request'
3+
import Image from 'next/legacy/image'
94
import { GetStaticPaths, GetStaticProps } from 'next/types'
5+
import YouTube from 'react-youtube'
6+
import styles from '../../components/Blog/Blog.module.scss'
107
import { FooterCallToAction } from '../../components/common/CallToAction/FooterCallToAction'
11-
import Link from 'next/link'
12-
import YouTube, { YouTubeProps } from 'react-youtube'
8+
import Footer from '../../components/common/Footer/Footer'
9+
import { Section } from '../../components/common/Section/Section'
10+
import homeStyles from '../../components/Home/Home.module.scss'
1311

1412
import { RichText } from '@graphcms/rich-text-react-renderer'
15-
import { Typography } from '../../components/common/Typography/Typography'
13+
import { ElementNode } from '@graphcms/rich-text-types'
1614
import { createElement, useEffect, useRef, useState } from 'react'
15+
import { PostAuthor } from '../../components/Blog/Author'
1716
import BlogNavbar from '../../components/Blog/BlogNavbar/BlogNavbar'
18-
import { BlogCallToAction } from '../../components/common/CallToAction/BlogCallToAction'
19-
import { SuggestedBlogPost } from '../../components/Blog/SuggestedBlogPost/SuggestedBlogPost'
20-
import { ElementNode } from '@graphcms/rich-text-types'
2117
import { Post } from '../../components/Blog/BlogPost/BlogPost'
18+
import { SuggestedBlogPost } from '../../components/Blog/SuggestedBlogPost/SuggestedBlogPost'
19+
import { PostTag } from '../../components/Blog/Tag'
20+
import { Comments } from '../../components/Comments/Comments'
21+
import { BlogCallToAction } from '../../components/common/CallToAction/BlogCallToAction'
2222
import { Meta } from '../../components/common/Head/Meta'
23-
import ReturnIcon from '../../public/images/ReturnIcon'
23+
import { Typography } from '../../components/common/Typography/Typography'
2424
import { HighlightCodeBlock } from '../../components/Docs/HighlightCodeBlock/HighlightCodeBlock'
2525
import { GraphQLRequest } from '../../utils/graphql'
26-
import { getTagUrl, PostTag } from '../../components/Blog/Tag'
27-
import { PostAuthor } from '../../components/Blog/Author'
2826

2927
const NUM_SUGGESTED_POSTS = 3
3028

@@ -366,6 +364,9 @@ const PostPage = ({
366364
<Section>
367365
<div className={styles.postBodyDivider}></div>
368366
</Section>
367+
<Section>
368+
<Comments slug={post.slug} />
369+
</Section>
369370
<Section>
370371
<div className={classNames(homeStyles.anchorTitle, styles.postBody)}>
371372
<h3 className={styles.otherArticlesHeader}>Other articles you may like</h3>

0 commit comments

Comments
 (0)