From 1baae608ba9ab50af1f191f7b7a56078b45b741f Mon Sep 17 00:00:00 2001 From: sripwoud Date: Tue, 29 Oct 2024 12:05:57 +0100 Subject: [PATCH 1/3] feat: add `author` column to `questions` --- apps/client/src/components/CreateQuestionForm.tsx | 5 ++++- apps/server/src/questions/dto/create-question.dto.ts | 1 + apps/server/src/questions/questions.service.ts | 4 ++-- apps/server/src/supabase/supabase.types.ts | 3 +++ .../migrations/20241023064101_create_questions_table.sql | 5 +++-- 5 files changed, 13 insertions(+), 5 deletions(-) diff --git a/apps/client/src/components/CreateQuestionForm.tsx b/apps/client/src/components/CreateQuestionForm.tsx index 15cdd63..e41b987 100644 --- a/apps/client/src/components/CreateQuestionForm.tsx +++ b/apps/client/src/components/CreateQuestionForm.tsx @@ -1,4 +1,5 @@ 'use client' +import { useUser } from '@account-kit/react' import { useForm } from '@tanstack/react-form' import { zodValidator } from '@tanstack/zod-form-adapter' import { clientConfig } from 'client/l/config' @@ -10,10 +11,12 @@ interface CreateQuestionFormProps { onClose: () => void } export const CreateQuestionForm: FC = ({ onClose }) => { + const user = useUser() const form = useForm({ defaultValues: { title: '' }, onSubmit: ({ value: { title } }) => { - createQuestion({ groupId: clientConfig.bandada.pseGroupId, title }) + if (user?.email === undefined) throw new Error('User not found') + createQuestion({ author: user.email, groupId: clientConfig.bandada.pseGroupId, title }) }, validatorAdapter: zodValidator(), validators: { onChange: CreateQuestionDto.pick({ title: true }) }, diff --git a/apps/server/src/questions/dto/create-question.dto.ts b/apps/server/src/questions/dto/create-question.dto.ts index edb0185..b6b5093 100644 --- a/apps/server/src/questions/dto/create-question.dto.ts +++ b/apps/server/src/questions/dto/create-question.dto.ts @@ -1,6 +1,7 @@ import { z } from 'zod' export const CreateQuestionDto = z.object({ + author: z.string().email(), groupId: z.string().min(1, { message: 'Group ID cannot be empty' }), title: z.string().min(10, { message: 'Title must be at least 10 characters long' }).includes('?', { message: 'Title must include a question mark', diff --git a/apps/server/src/questions/questions.service.ts b/apps/server/src/questions/questions.service.ts index 3144f6f..5109dbf 100644 --- a/apps/server/src/questions/questions.service.ts +++ b/apps/server/src/questions/questions.service.ts @@ -12,8 +12,8 @@ export class QuestionsService implements OnModuleInit { this.supabase.subscribe(this.resource) } - async create({ groupId: group_id, title }: CreateQuestionDto) { - return this.supabase.from(this.resource).insert({ group_id, title }) + async create({ author, groupId: group_id, title }: CreateQuestionDto) { + return this.supabase.from(this.resource).insert({ author, group_id, title }) } async find({ questionId }: FindQuestionDto) { diff --git a/apps/server/src/supabase/supabase.types.ts b/apps/server/src/supabase/supabase.types.ts index 7190e3a..8305b33 100644 --- a/apps/server/src/supabase/supabase.types.ts +++ b/apps/server/src/supabase/supabase.types.ts @@ -102,6 +102,7 @@ export type Database = { questions: { Row: { active: boolean + author: string created_at: string group_id: string id: number @@ -111,6 +112,7 @@ export type Database = { } Insert: { active?: boolean + author: string created_at?: string group_id: string id?: never @@ -120,6 +122,7 @@ export type Database = { } Update: { active?: boolean + author?: string created_at?: string group_id?: string id?: never diff --git a/supabase/migrations/20241023064101_create_questions_table.sql b/supabase/migrations/20241023064101_create_questions_table.sql index 249f883..2336fa7 100644 --- a/supabase/migrations/20241023064101_create_questions_table.sql +++ b/supabase/migrations/20241023064101_create_questions_table.sql @@ -1,10 +1,11 @@ create table public.questions ( id bigint generated always as identity primary key, + active boolean not null default true, + author text not null, created_at timestamptz not null default now(), title text not null, - active boolean not null default true, - yes integer not null default 0, no integer not null default 0, + yes integer not null default 0, group_id text not null -- TODO allow list of groupIds? Build dynamically union of groups on the fly in bandada? ); comment on table public.questions is 'Questions for users to give feedback on'; From 57d46ee837c41df5c57058c0b94b3d43b44ae66e Mon Sep 17 00:00:00 2001 From: sripwoud Date: Tue, 29 Oct 2024 13:37:03 +0100 Subject: [PATCH 2/3] feat: define question page --- .biome.jsonc | 7 +++- .../src/app/[groupId]/[questionId]/page.tsx | 40 +++++++++++++++++++ .../src/components/QuestionCard/YN/index.tsx | 5 ++- apps/server/src/questions/dto/index.ts | 1 + .../src/questions/dto/toggle-question.dto.ts | 8 ++++ apps/server/src/questions/questions.router.ts | 4 +- .../server/src/questions/questions.service.ts | 6 ++- 7 files changed, 67 insertions(+), 4 deletions(-) create mode 100644 apps/client/src/app/[groupId]/[questionId]/page.tsx create mode 100644 apps/server/src/questions/dto/toggle-question.dto.ts diff --git a/.biome.jsonc b/.biome.jsonc index b21f6bf..b387962 100644 --- a/.biome.jsonc +++ b/.biome.jsonc @@ -60,6 +60,7 @@ "apps/client/src/hooks/useIdentity.ts", "apps/client/src/hooks/useSendFeedback.ts", "apps/client/src/components/withAuth.tsx", + "apps/client/src/app/[groupId]/[questionId]/page.tsx", ], "linter": { "rules": { "correctness": { "useExhaustiveDependencies": "off" } } }, }, @@ -73,7 +74,11 @@ "linter": { "rules": { "style": { "noDefaultExport": "off" } } }, }, { - "include": ["apps/client/src/app/layout.tsx", "apps/client/src/app/[groupId]/page.tsx"], + "include": [ + "apps/client/src/app/layout.tsx", + "apps/client/src/app/[groupId]/page.tsx", + "apps/client/src/app/[groupId]/[questionId]/page.tsx", + ], "linter": { "rules": { "nursery": { "useComponentExportOnlyModules": "off" } } }, }, ], diff --git a/apps/client/src/app/[groupId]/[questionId]/page.tsx b/apps/client/src/app/[groupId]/[questionId]/page.tsx new file mode 100644 index 0000000..0155d7a --- /dev/null +++ b/apps/client/src/app/[groupId]/[questionId]/page.tsx @@ -0,0 +1,40 @@ +'use client' +import { useUser } from '@account-kit/react' +import { Loader } from 'client/c/Loader' +import { withAuth } from 'client/components/withAuth' +import { trpc } from 'client/l/trpc' +import { useEffect } from 'react' + +const QuestionDetails = ({ params: { questionId: questionIdStr } }: { params: { 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, + }) + + useEffect(() => { + refetch() + }, [isPending]) + + if (isLoading || question === undefined || question === null) return + return ( +
+

{question.title}

+

yes: {question.yes}

+

no: {question.no}

+ {user?.email === question.author && ( + + )} +
+ ) +} + +export default withAuth(QuestionDetails) diff --git a/apps/client/src/components/QuestionCard/YN/index.tsx b/apps/client/src/components/QuestionCard/YN/index.tsx index 1af289a..4a23bc8 100644 --- a/apps/client/src/components/QuestionCard/YN/index.tsx +++ b/apps/client/src/components/QuestionCard/YN/index.tsx @@ -1,6 +1,7 @@ import { YNQuestionStatus } from 'client/c/QuestionCard/YN/YNQuestionStatus' import { useSendFeedback } from 'client/h/useSendFeedback' import { ThumbsDown, ThumbsUp } from 'lucide-react' +import Link from 'next/link' import type { FC } from 'react' import type { Question } from 'server/questions/entities' @@ -31,7 +32,9 @@ export const YNQuestionCard: FC = ({ style={{ backgroundColor: '#fffae3', borderColor: '#5d576b', borderStyle: 'solid', borderWidth: '2px' }} >
-

{title}

+ +

{title}

+
diff --git a/apps/server/src/questions/dto/index.ts b/apps/server/src/questions/dto/index.ts index baaaa2b..8a04df2 100644 --- a/apps/server/src/questions/dto/index.ts +++ b/apps/server/src/questions/dto/index.ts @@ -1,3 +1,4 @@ export * from './create-question.dto' export * from './find-all-questions.dto' export * from './find-question.dto' +export * from './toggle-question.dto' diff --git a/apps/server/src/questions/dto/toggle-question.dto.ts b/apps/server/src/questions/dto/toggle-question.dto.ts new file mode 100644 index 0000000..8180b60 --- /dev/null +++ b/apps/server/src/questions/dto/toggle-question.dto.ts @@ -0,0 +1,8 @@ +import { z } from 'zod' + +export const ToggleQuestionDto = z.object({ + active: z.boolean(), + questionId: z.number().positive(), +}) + +export type ToggleQuestionDto = z.infer diff --git a/apps/server/src/questions/questions.router.ts b/apps/server/src/questions/questions.router.ts index a662158..9fd2c45 100644 --- a/apps/server/src/questions/questions.router.ts +++ b/apps/server/src/questions/questions.router.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common' import { on } from 'node:events' -import { CreateQuestionDto, FindAllQuestionsDto } from 'server/questions/dto' +import { CreateQuestionDto, FindAllQuestionsDto, FindQuestionDto, ToggleQuestionDto } from 'server/questions/dto' import { Question } from 'server/questions/entities' import { QuestionsService } from 'server/questions/questions.service' import { TrpcService } from 'server/trpc/trpc.service' @@ -14,6 +14,7 @@ export class QuestionsRouter { router = this.trpc.router({ create: this.trpc.procedure.input(CreateQuestionDto).mutation(async ({ input }) => this.questions.create(input)), + find: this.trpc.procedure.input(FindQuestionDto).query(async ({ input }) => this.questions.find(input)), findAll: this.trpc.procedure.input(FindAllQuestionsDto).query(async ({ input }) => this.questions.findAll(input)), // TODO: validate output/payload https://trpc.io/docs/server/subscriptions#output-validation onChange: this @@ -31,5 +32,6 @@ export class QuestionsRouter { yield payload as { type: 'INSERT' | 'UPDATE'; data: Question } } }), + toggle: this.trpc.procedure.input(ToggleQuestionDto).mutation(async ({ input }) => this.questions.toggle(input)), }) } diff --git a/apps/server/src/questions/questions.service.ts b/apps/server/src/questions/questions.service.ts index 5109dbf..fb3a730 100644 --- a/apps/server/src/questions/questions.service.ts +++ b/apps/server/src/questions/questions.service.ts @@ -1,5 +1,5 @@ import { Injectable, OnModuleInit } from '@nestjs/common' -import type { CreateQuestionDto, FindAllQuestionsDto, FindQuestionDto } from 'server/questions/dto' +import type { CreateQuestionDto, FindAllQuestionsDto, FindQuestionDto, ToggleQuestionDto } from 'server/questions/dto' import { SupabaseService } from 'server/supabase/supabase.service' @Injectable() @@ -33,4 +33,8 @@ export class QuestionsService implements OnModuleInit { if (data === null) throw new Error('This question does not exist') return data.active } + + async toggle({ active, questionId }: ToggleQuestionDto) { + return this.supabase.from(this.resource).update({ active }).eq('id', questionId) + } } From cbf0b1962eb0831de3f6c83c9df7a76484edd03a Mon Sep 17 00:00:00 2001 From: sripwoud Date: Tue, 29 Oct 2024 13:46:34 +0100 Subject: [PATCH 3/3] feat(client): set question to inactive --- .../src/app/[groupId]/[questionId]/page.tsx | 2 +- .../QuestionCard/YN/YNQuestionStatus.tsx | 6 +- .../src/components/QuestionCard/YN/index.tsx | 55 ++++++++++++------- 3 files changed, 38 insertions(+), 25 deletions(-) diff --git a/apps/client/src/app/[groupId]/[questionId]/page.tsx b/apps/client/src/app/[groupId]/[questionId]/page.tsx index 0155d7a..1c91dc6 100644 --- a/apps/client/src/app/[groupId]/[questionId]/page.tsx +++ b/apps/client/src/app/[groupId]/[questionId]/page.tsx @@ -20,7 +20,7 @@ const QuestionDetails = ({ params: { questionId: questionIdStr } }: { params: { if (isLoading || question === undefined || question === null) return return (
-

{question.title}

+

{question.title}

yes: {question.yes}

no: {question.no}

{user?.email === question.author && ( diff --git a/apps/client/src/components/QuestionCard/YN/YNQuestionStatus.tsx b/apps/client/src/components/QuestionCard/YN/YNQuestionStatus.tsx index 504c008..5a369a7 100644 --- a/apps/client/src/components/QuestionCard/YN/YNQuestionStatus.tsx +++ b/apps/client/src/components/QuestionCard/YN/YNQuestionStatus.tsx @@ -1,20 +1,18 @@ -import { Equal, Hourglass, ThumbsDown, ThumbsUp } from 'lucide-react' +import { Equal, ThumbsDown, ThumbsUp } from 'lucide-react' import type { FC } from 'react' interface YNQuestionStatusProps { - active: boolean no: number size: number yes: number } -export const YNQuestionStatus: FC = ({ active, no, size, yes }) => { +export const YNQuestionStatus: FC = ({ no, size, yes }) => { const hasVotes = (yes + no) > 0 const hasMoreYes = yes > no && hasVotes const hasMoreNo = no > yes && hasVotes const draw = yes === no && hasVotes - if (active) return if (hasMoreYes) return if (hasMoreNo) return if (draw) return diff --git a/apps/client/src/components/QuestionCard/YN/index.tsx b/apps/client/src/components/QuestionCard/YN/index.tsx index 4a23bc8..8455b51 100644 --- a/apps/client/src/components/QuestionCard/YN/index.tsx +++ b/apps/client/src/components/QuestionCard/YN/index.tsx @@ -35,28 +35,43 @@ export const YNQuestionCard: FC = ({

{title}

- +
- - + {active + ? ( + <> + + + + ) + : ( +
+
+ {yes} +
+
+ {no} +
+
+ )}
)