Skip to content

Commit

Permalink
Implement simple feedback for wzulfikar#8.
Browse files Browse the repository at this point in the history
  • Loading branch information
wzulfikar committed Apr 28, 2021
1 parent 50e0b86 commit 9988c32
Show file tree
Hide file tree
Showing 7 changed files with 242 additions and 6 deletions.
20 changes: 16 additions & 4 deletions components/NotionPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import { PageSocial } from './PageSocial'
import { GitHubShareButton } from './GitHubShareButton'
import { ReactUtterances } from './ReactUtterances'
import { ViewCounter } from './ViewCounter'
import { SimpleFeedback } from './SimpleFeedback'

import styles from './styles.module.css'

Expand Down Expand Up @@ -213,14 +214,25 @@ export const NotionPage: React.FC<types.PageProps> = ({
<>
<div
style={{
marginLeft: 'auto',
marginTop: '1rem',
display: 'flex',
justifyContent: 'space-between',
width: '100%',
alignItems: 'center'
}}
>
<FiBarChart2 style={{ marginRight: 3, marginBottom: 2 }} />
<ViewCounter slug={slug} />
<div>
<SimpleFeedback slug={slug} />
</div>
<div
style={{
marginLeft: 'auto',
display: 'flex',
alignItems: 'center'
}}
>
<FiBarChart2 style={{ marginRight: 3, marginBottom: 2 }} />
<ViewCounter slug={slug} />
</div>
</div>
<ReactUtterances
repo={config.utterancesGitHubRepo}
Expand Down
144 changes: 144 additions & 0 deletions components/SimpleFeedback.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import { useState, useEffect, useRef } from 'react'
import format from 'comma-number'

export function SimpleFeedback({ slug }) {
const [helpful, setHelpful] = useState(null)
const [count, setCount] = useState({ helpful: 0, not_helpful: 0 })

const uuidRef = useRef(null)
const mountedRef = useRef(null)

useEffect(() => {
mountedRef.current = true
import('device-uuid').then((mod) => {
const uuid = new mod.DeviceUUID().get()
uuidRef.current = uuid
syncFeedback(uuid)
})

return () => {
mountedRef.current = false
}
}, [slug])

function syncFeedback(uuid) {
fetch(`/api/feedbacks/${slug}?uuid=${uuid}`)
.then((res) => res.json())
.then(
({ helpful: countHelpful, not_helpful: countNotHelpful, feedback }) => {
if (!mountedRef.current) return

const userFeedback =
feedback === 'helpful'
? true
: feedback === 'not_helpful'
? false
: null

if (userFeedback !== helpful) {
setHelpful(userFeedback)
}

if (
count.helpful !== countHelpful ||
count.not_helpful !== countNotHelpful
) {
setCount({ helpful: countHelpful, not_helpful: countNotHelpful })
}
}
)
}

function sendFeedback(isHelpful) {
// Add feedback or remove
const prevState =
helpful === true ? 'helpful' : helpful === false ? 'not_helpful' : null
const newVal = helpful === isHelpful ? null : isHelpful

setHelpful(newVal)

// Optimistic update
const newCount = { ...count }
if (newVal === true) {
newCount.helpful++
} else if (newVal === false) {
newCount.not_helpful++
}

if (prevState !== null) {
newCount[prevState]--
}
setCount(newCount)

fetch(`/api/feedbacks/${slug}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
uuid: uuidRef.current,
helpful: newVal,
prevState: prevState
})
}).then((res) => {
syncFeedback(uuidRef.current)
})
}

return (
<>
<style jsx>{`
.container {
display: flex;
align-items: baseline;
}
.btn-feedback {
margin: 0px 5px;
padding: 0;
background: none;
border: none;
cursor: pointer;
font-size: unset;
}
@media only screen and (max-width: 600px) {
.container {
flex-direction: column;
}
.btn-feedback:first-child {
margin-left: 0px;
}
}
`}</style>
<div className='container'>
<div>Was this helpful?</div>
<div>
<button
className='btn-feedback'
onClick={() => sendFeedback(true)}
style={{
color: helpful === true ? '#4c8bf3' : 'unset',
textDecoration: helpful === true ? 'underline 2px' : undefined
}}
>
Yes{' '}
{helpful === null ||
(count.helpful > 0 && `(${format(count.helpful)})`)}
</button>
<span>·</span>
<button
className='btn-feedback'
onClick={() => sendFeedback(false)}
style={{
color: helpful === false ? '#4c8bf3' : 'unset',
textDecoration: helpful === false ? 'underline 2px' : undefined
}}
>
No{' '}
{helpful === null ||
(count.not_helpful > 0 && `(${format(count.not_helpful)})`)}
</button>
</div>
</div>
</>
)
}
5 changes: 5 additions & 0 deletions lib/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,11 @@ export const firebaseCollectionPageviews = getEnv(
'pageviews'
)

export const firebaseCollectionFeedbacks = getEnv(
'FIREBASE_COLLECTION_FEEDBACKS',
'feedbacks'
)

// this hack is necessary because vercel doesn't support secret files so we need to encode our google
// credentials a base64-encoded string of the JSON-ified content
function getGoogleApplicationCredentials() {
Expand Down
12 changes: 10 additions & 2 deletions lib/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,21 @@ import * as config from './config'
export let db: firestore.Firestore = null
export let images: firestore.CollectionReference = null
export let pageviews: firestore.CollectionReference = null
export let feedbacks: firestore.CollectionReference = null

export const collections = {
images: config.firebaseCollectionImages,
pageviews: config.firebaseCollectionPageviews,
feedbacks: config.firebaseCollectionFeedbacks
}

if (config.isPreviewImageSupportEnabled) {
db = new firestore.Firestore({
projectId: config.googleProjectId,
credentials: config.googleApplicationCredentials
})

images = db.collection(config.firebaseCollectionImages)
pageviews = db.collection(config.firebaseCollectionPageviews)
images = db.collection(collections.images)
pageviews = db.collection(collections.pageviews)
feedbacks = db.collection(collections.feedbacks)
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"colorthief": "^2.3.2",
"comma-number": "^2.0.1",
"date-fns": "^2.19.0",
"device-uuid": "^1.0.4",
"fathom-client": "^3.0.0",
"got": "^11.8.1",
"isomorphic-unfetch": "^3.1.0",
Expand Down
61 changes: 61 additions & 0 deletions pages/api/feedbacks/[slug].ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import * as firestore from '@google-cloud/firestore'
import * as db from '@lib/db'

export default async function handler(req, res) {
if (req.method === 'POST') {
const { uuid, helpful, prevState } = req.body

const increment = firestore.FieldValue.increment(1)
const decrement = firestore.FieldValue.increment(-1)

const slugRef = db.feedbacks.doc(req.query.slug)
const voterRef = db.db
.collection(`/${db.collections.feedbacks}/${req.query.slug}/voters`)
.doc(uuid)

await db.db.runTransaction(async (tx) => {
const feedback =
helpful === true ? 'helpful' : helpful === false ? 'not_helpful' : null

const payload = {
count: helpful === null ? decrement : increment
} as any

if (feedback === 'helpful') {
payload.helpful = increment
} else if (feedback === 'not_helpful') {
payload.not_helpful = increment
}

if (prevState !== null) {
payload[prevState] = decrement
}

tx.set(slugRef, payload, { merge: true })
tx.set(voterRef, { feedback }, { merge: true })
})

const data = (await slugRef.get()).data()

return res.status(200).json({
helpful: data.helpful,
not_helpful: data.not_helpful
})
}

if (req.method === 'GET') {
const { slug, uuid } = req.query
const [snapshot, voter] = await Promise.all([
db.feedbacks.doc(slug).get(),
db.db
.collection(`/${db.collections.feedbacks}/${req.query.slug}/voters`)
.doc(uuid)
.get()
])

const { helpful, not_helpful } = snapshot.data() || {}
const feedback = voter.data()?.feedback || null

return res.status(200).json({ helpful, not_helpful, feedback })
}
}
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1675,6 +1675,11 @@ detect-libc@^1.0.3:
resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b"
integrity sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=

device-uuid@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/device-uuid/-/device-uuid-1.0.4.tgz#f6973e58f19b92a237aeca7b16a395ae7195a883"
integrity sha1-9pc+WPGbkqI3rsp7FqOVrnGVqIM=

diffie-hellman@^5.0.0:
version "5.0.3"
resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875"
Expand Down

0 comments on commit 9988c32

Please sign in to comment.