Skip to content
This repository was archived by the owner on Sep 22, 2025. It is now read-only.
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .biome.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@
"include": [
"apps/client/src/hooks/useAuth.ts",
"apps/client/src/hooks/useIdentity.ts",
"apps/client/src/hooks/useSubscribeQuestions.ts",
"apps/client/src/hooks/useSubscribeStats.ts",
"apps/client/src/hooks/useSendFeedback.ts",
"apps/client/src/components/withAuth.tsx",
"apps/client/src/app/[groupId]/[questionId]/page.tsx",
Expand All @@ -81,5 +83,9 @@
],
"linter": { "rules": { "nursery": { "useComponentExportOnlyModules": "off" } } },
},
{
"include": ["apps/client/src/state/questions/atom.ts"],
"linter": { "rules": { "style": { "noNonNullAssertion": "off" } } },
},
],
}
1 change: 1 addition & 0 deletions apps/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"@trpc/server": "^11.0.0-rc.593",
"@types/react-modal": "^3.16.3",
"encoding": "^0.1.13",
"fast-deep-equal": "^3.1.3",
"jotai": "^2.10.0",
"jotai-trpc": "^0.7.0",
"lucide-react": "^0.453.0",
Expand Down
17 changes: 17 additions & 0 deletions apps/client/src/app/[groupId]/[questionId]/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
'use client'
import { useSubscribeStats } from 'client/h/useSubscribeStats'
import type { ReactNode } from 'react'

export default function QuestionLayout(
{ children, params: { groupId, questionId } }: {
children: ReactNode
params: { groupId: string; questionId: string }
},
) {
useSubscribeStats({ groupId, questionId: Number.parseInt(questionId) })
return (
<>
{children}
</>
)
}
20 changes: 9 additions & 11 deletions apps/client/src/app/[groupId]/[questionId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,22 @@
import { useUser } from '@account-kit/react'
import { Loader } from 'client/c/Loader'
import { withAuth } from 'client/components/withAuth'
import { useQuestionStats } from 'client/h/useQuestionStats'
import { trpc } from 'client/l/trpc'
import { useEffect } from 'react'
import { questionAtom } from 'client/state/questions/atom'
import { statsByQuestionAtom } from 'client/state/stats/atom'
import { useAtomValue } from 'jotai'

const QuestionDetails = ({ params: { questionId: questionIdStr } }: { params: { questionId: string } }) => {
const QuestionDetails = (
{ params: { groupId, questionId: questionIdStr } }: { params: { groupId: string; questionId: string } },
) => {
const questionId = Number.parseInt(questionIdStr)
const user = useUser()
const { mutate: toggle, isPending } = trpc.questions.toggle.useMutation()
const { data: question, isLoading, refetch } = trpc.questions.find.useQuery({ questionId }, {
select: ({ data }) => data,
})
const { data: { no, yes } } = useQuestionStats({ questionId })
const { no, yes } = useAtomValue(statsByQuestionAtom)(questionId)
const question = useAtomValue(questionAtom)({ groupId, questionId })

useEffect(() => {
refetch()
}, [isPending])
if (isPending) return <Loader />

if (isLoading || question === undefined || question === null) return <Loader />
return (
<div>
<h1 className='text-2xl'>{question.title}</h1>
Expand Down
10 changes: 10 additions & 0 deletions apps/client/src/app/[groupId]/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
'use client'
import { useSubscribeQuestions } from 'client/h/useSubscribeQuestions'
import type { ReactNode } from 'react'

export default function GroupLayout(
{ children, params: { groupId } }: { children: ReactNode; params: { groupId: string } },
) {
useSubscribeQuestions(groupId)
return <>{children}</>
}
26 changes: 3 additions & 23 deletions apps/client/src/app/[groupId]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,15 @@
'use client'
import { CreateQuestionModal } from 'client/c/CreateQuestionModal'
import { ExternalLink } from 'client/c/ExternalLink'
import { Loader } from 'client/c/Loader'
import { YNQuestionCard } from 'client/c/QuestionCard/YN'
import { withAuth } from 'client/c/withAuth'
import { clientConfig } from 'client/l/config'
import { trpc } from 'client/l/trpc'
import { questionsByGroupAtom } from 'client/state/questions/atom'
import { useAtomValue } from 'jotai'
import { ExternalLinkIcon } from 'lucide-react'
import { useEffect, useState } from 'react'
import type { Question } from 'server/questions/entities'

const Dashboard = ({ params: { groupId } }: { params: { groupId: string } }) => {
const [questions, setQuestions] = useState<Question[]>([])
const { data, isLoading } = trpc.questions.findAll.useQuery({ groupId })

useEffect(() => {
if (data !== undefined) setQuestions(data)
}, [data])

trpc.questions.onChange.useSubscription(undefined, {
onData: ({ type, data: newQuestion }) => {
if (type === 'INSERT') setQuestions((prev) => [newQuestion, ...prev])
if (type === 'UPDATE')
setQuestions((prev) => prev.map((oldQuestion) => oldQuestion.id === newQuestion.id ? newQuestion : oldQuestion))
},
onError: (error) => {
console.error(error)
},
})

if (isLoading || questions === undefined) return <Loader />
const questions = useAtomValue(questionsByGroupAtom)(groupId)

return (
<div className='flex flex-col justify-center items-center space-y-4 h-full'>
Expand Down
18 changes: 9 additions & 9 deletions apps/client/src/components/QuestionCard/YN/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { YNQuestionStatus } from 'client/c/QuestionCard/YN/YNQuestionStatus'
import { useSendFeedback } from 'client/h/useSendFeedback'
import { useQuestionStats } from 'client/hooks/useQuestionStats'
import { ThumbsDown, ThumbsUp } from 'lucide-react'
import Link from 'next/link'
import type { FC } from 'react'
Expand All @@ -12,7 +11,8 @@ export const YNQuestionCard: FC<Question> = ({
title,
active,
}) => {
const { data: { no, yes } } = useQuestionStats({ questionId })
// TODO
// const { data: { no, yes } } = useQuestionStats({ questionId })
const { sendFeedback, isSending, errors } = useSendFeedback({ groupId, questionId })

if (errors.length > 0)
Expand All @@ -27,7 +27,7 @@ export const YNQuestionCard: FC<Question> = ({
<Link href={`${groupId}/${questionId.toString()}`}>
<h3 className='text-xl font-bold mb-2'>{title}</h3>
</Link>
<YNQuestionStatus yes={yes} no={no} size={20} />
<YNQuestionStatus yes={0} no={0} size={20} />
</div>
<div className='flex justify-center items-center text-gray-600'>
{active
Expand All @@ -37,30 +37,30 @@ export const YNQuestionCard: FC<Question> = ({
className='mr-2'
type='button'
onClick={() => {
sendFeedback('true')
sendFeedback('yes')
}}
disabled={isSending}
>
{yes} <ThumbsUp className='inline-block' size={20} />
#yes TODO <ThumbsUp className='inline-block' size={20} />
</button>
<button
type='button'
disabled={isSending}
onClick={() => {
sendFeedback('false')
sendFeedback('no')
}}
>
{no} <ThumbsDown className='inline-block' size={20} />
#no TODO <ThumbsDown className='inline-block' size={20} />
</button>
</>
)
: (
<div className='flex space-x-4'>
<div>
{yes} <ThumbsUp className='inline-block' size={20} />
#yes TOD <ThumbsUp className='inline-block' size={20} />
</div>
<div>
{no} <ThumbsDown className='inline-block' size={20} />
#no TODO <ThumbsDown className='inline-block' size={20} />
</div>
</div>
)}
Expand Down
9 changes: 0 additions & 9 deletions apps/client/src/hooks/useQuestionStats.ts

This file was deleted.

24 changes: 24 additions & 0 deletions apps/client/src/hooks/useSubscribeQuestions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { trpc } from 'client/l/trpc'
import * as actions from 'client/state/questions/actions'
import { questionsAtomFamily } from 'client/state/questions/atom'
import { useSetAtom } from 'jotai'
import { useEffect } from 'react'

export const useSubscribeQuestions = (groupId: string) => {
const dispatch = useSetAtom(questionsAtomFamily(groupId))
const { data: questions } = trpc.questions.findAll.useQuery({ groupId })

useEffect(() => {
if (questions === undefined) return
dispatch(actions.init(questions))
}, [questions])

trpc.questions.onChange.useSubscription({ groupId }, {
onData: ({ data }) => {
dispatch(actions.update(data))
},
onError: (error) => {
console.error(error)
},
})
}
26 changes: 26 additions & 0 deletions apps/client/src/hooks/useSubscribeStats.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { trpc } from 'client/l/trpc'
import { questionTypeAtom } from 'client/state/questions/atom'
import * as actions from 'client/state/stats/actions'
import { statsAtomFamily } from 'client/state/stats/atom'
import { useAtomValue, useSetAtom } from 'jotai'
import { useEffect } from 'react'

export const useSubscribeStats = ({ groupId, questionId }: { groupId: string; questionId: number }) => {
const dispatch = useSetAtom(statsAtomFamily(questionId))
const questionType = useAtomValue(questionTypeAtom)({ groupId, questionId })
const { data: stats } = trpc.questions.stats.useQuery({ questionId, type: questionType })

useEffect(() => {
if (stats === undefined) return
dispatch(actions.init(stats))
}, [stats])

trpc.feedbacks.onInsert.useSubscription({ questionId }, {
onData: ({ data: { feedback, question_id } }) => {
dispatch(actions.update({ feedback, question_id, type: questionType }))
},
onError: (error) => {
console.error(error)
},
})
}
12 changes: 12 additions & 0 deletions apps/client/src/state/questions/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { type Questions, type QuestionsAction, QuestionsActionType } from 'client/state/questions/types'
import type { Question } from 'server/questions/entities'

export const init = (payload: Questions): QuestionsAction => ({
type: QuestionsActionType.INIT,
payload,
})

export const update = (payload: Question): QuestionsAction => ({
type: QuestionsActionType.UPDATE,
payload,
})
20 changes: 20 additions & 0 deletions apps/client/src/state/questions/atom.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { initialQuestionsState, questionsReducer } from 'client/state/questions/reducer'
import type { Questions, QuestionsAction } from 'client/state/questions/types'
import deepEqual from 'fast-deep-equal'
import { atom, type WritableAtom } from 'jotai'
import { atomFamily, atomWithReducer } from 'jotai/utils'

export const questionsAtomFamily = atomFamily<string, WritableAtom<Questions, [QuestionsAction], void>>(
(_groupId) => atomWithReducer<Questions, QuestionsAction>(initialQuestionsState, questionsReducer),
deepEqual,
)

export const questionsByGroupAtom = atom((get) => (groupId: string) => Object.values(get(questionsAtomFamily(groupId))))

export const questionTypeAtom = atom((get) => ({ groupId, questionId }: { groupId: string; questionId: number }) =>
get(questionsAtomFamily(groupId))[questionId]!.type
)

export const questionAtom = atom(get => ({ groupId, questionId }: { groupId: string; questionId: number }) =>
get(questionsAtomFamily(groupId))[questionId]!
)
14 changes: 14 additions & 0 deletions apps/client/src/state/questions/reducer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type { Questions, QuestionsAction } from 'client/state/questions/types'

export const initialQuestionsState: Questions = {}

export const questionsReducer = (state: Questions, { type, payload }: QuestionsAction): Questions => {
switch (type) {
case 'INIT':
return payload
case 'UPDATE':
return { ...state, [payload.id]: payload }
default:
return state
}
}
18 changes: 18 additions & 0 deletions apps/client/src/state/questions/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { inferRouterOutputs } from '@trpc/server'
import type { Question } from 'server/questions/entities'
import type { Router } from 'server/trpc/trpc.router'

export type Questions = inferRouterOutputs<Router>['questions']['findAll']

export enum QuestionsActionType {
INIT = 'INIT',
UPDATE = 'UPDATE',
}

export type QuestionsAction = {
type: QuestionsActionType.INIT
payload: Questions
} | {
type: QuestionsActionType.UPDATE
payload: Question
}
11 changes: 11 additions & 0 deletions apps/client/src/state/stats/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { type Stats, type StatsAction, StatsActionType, type StatsUpdatePayload } from 'client/state/stats/types'

export const init = (payload: Stats): StatsAction => ({
type: StatsActionType.INIT,
payload,
})

export const update = (payload: StatsUpdatePayload): StatsAction => ({
type: StatsActionType.UPDATE,
payload,
})
12 changes: 12 additions & 0 deletions apps/client/src/state/stats/atom.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { initialStatsState, statsReducer } from 'client/state/stats/reducer'
import type { Stats, StatsAction } from 'client/state/stats/types'
import deepEqual from 'fast-deep-equal'
import { atom, type WritableAtom } from 'jotai'
import { atomFamily, atomWithReducer } from 'jotai/utils'

export const statsAtomFamily = atomFamily<number, WritableAtom<Stats, [StatsAction], void>>(
(_questionId) => atomWithReducer<Stats, StatsAction>(initialStatsState, statsReducer),
deepEqual,
)

export const statsByQuestionAtom = atom((get) => (questionId: number) => get(statsAtomFamily(questionId)))
26 changes: 26 additions & 0 deletions apps/client/src/state/stats/reducer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { type Stats, type StatsAction, StatsActionType } from 'client/state/stats/types'

export const initialStatsState: Stats = { no: 0, yes: 0 }

export const statsReducer = (state: Stats, { type, payload }: StatsAction): Stats => {
switch (type) {
case StatsActionType.INIT:
return payload
case StatsActionType.UPDATE:
switch (payload.type) {
case 'boolean':
switch (payload.feedback) {
case 'yes':
return { ...state, yes: state.yes + 1 }
case 'no':
return { ...state, no: state.no + 1 }
default:
throw new Error('feedback value incompatible with question type')
}
default:
throw new Error('Unsupported question type')
}
default:
return state
}
}
21 changes: 21 additions & 0 deletions apps/client/src/state/stats/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { inferRouterOutputs } from '@trpc/server'
import type { Feedback } from 'server/feedbacks/entities'
import type { Question } from 'server/questions/entities'
import type { Router } from 'server/trpc/trpc.router'

export type Stats = inferRouterOutputs<Router>['questions']['stats']

export enum StatsActionType {
INIT = 'INIT',
UPDATE = 'UPDATE',
}

export type StatsUpdatePayload = Pick<Feedback, 'question_id' | 'feedback'> & { type: Question['type'] }

export type StatsAction = {
type: StatsActionType.INIT
payload: Stats
} | {
type: StatsActionType.UPDATE
payload: StatsUpdatePayload
}
Loading