Skip to content

Commit

Permalink
feat: lucky draw
Browse files Browse the repository at this point in the history
  • Loading branch information
qin-guan committed Jul 15, 2023
1 parent 30e6307 commit 6fe40ad
Show file tree
Hide file tree
Showing 7 changed files with 209 additions and 42 deletions.
132 changes: 99 additions & 33 deletions pages/dashboard/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,17 @@ import InputText from 'primevue/inputtext'
import InputSwitch from 'primevue/inputswitch'
import Badge from 'primevue/badge'
import Skeleton from 'primevue/skeleton'
import { useToast } from 'primevue/usetoast'
import { TRPCClientError } from '@trpc/client'
useSeoMeta({
title: 'Dashboard',
})
const toast = useToast()
const { $client } = useNuxtApp()
const { data: surveys, pending: surveysPending, error: surveysError } = await $client.survey.list.useQuery(undefined, { lazy: true })
const { data: pastWinners, pending: pastWinnersPending, error: pastWinnersError, refresh: pastWinnersRefresh } = await $client.luckyDraw.pastWinners.useQuery(undefined, { lazy: true })

Check warning on line 21 in pages/dashboard/index.vue

View workflow job for this annotation

GitHub Actions / ci

'pastWinnersPending' is assigned a value but never used. Allowed unused vars must match /^_/u

Check warning on line 21 in pages/dashboard/index.vue

View workflow job for this annotation

GitHub Actions / ci

'pastWinnersError' is assigned a value but never used. Allowed unused vars must match /^_/u
const visible = ref(false)
const createForm = reactive({
Expand All @@ -41,10 +45,44 @@ async function create() {
createForm.pending = false
}
}
async function draw() {
try {
await $client.luckyDraw.draw.mutate()
await pastWinnersRefresh()
}
catch (err) {
console.error(err)
if (err instanceof TRPCClientError) {
toast.add({
severity: 'error',
summary: err.message,
})
}
}
}
async function deleteWinner(id: string) {
try {
await $client.luckyDraw.deleteWinner.mutate({
id,
})
await pastWinnersRefresh()
}
catch (err) {
console.error(err)
if (err instanceof TRPCClientError) {
toast.add({
severity: 'error',
summary: err.message,
})
}
}
}
</script>

<template>
<main mx-auto p-6 container>
<main mx-auto flex flex-col gap10 p-6 container>
<Dialog v-model:visible="visible" modal header="New survey" style="min-width: 300px;">
<form @submit.prevent="create">
<div class="flex flex-col gap-2">
Expand Down Expand Up @@ -72,44 +110,72 @@ async function create() {
</form>
</Dialog>

<h1 text-3xl font-bold>
Surveys
</h1>
<section>
<h2 text-3xl font-bold>
Surveys
</h2>

<br>
<br>

<Skeleton v-if="surveysPending" height="300px" />
<DashboardError v-else-if="surveysError" v-bind="surveysError" />
<Skeleton v-if="surveysPending" height="300px" />
<DashboardError v-else-if="surveysError" v-bind="surveysError" />

<template v-else>
<div v-if="surveys.length === 0" mt-20 flex flex-col items-center>
<span text-xl font-semibold>No surveys</span>
<br>
<Button size="small" label="Create new" @click="visible = true" />
</div>
<template v-else>
<div v-if="surveys.length === 0" mt-20 flex flex-col items-center>
<span text-xl font-semibold>No surveys</span>
<br>
<Button size="small" label="Create new" @click="visible = true" />
</div>

<DataTable v-else :value="surveys" table-style="width: 100%;">
<Column field="id" header="ID" style="width: 20%;" />
<Column field="title" header="Title" style="width: 50%" />
<Column field="workshop" header="Type" style="width: 20%">
<template #body="slotProps">
<Badge v-if="slotProps.data.workshop" value="Workshop" />
<Badge v-else value="Booth" severity="warning" />
</template>
</Column>
<Column header="Actions">
<template #body="slotProps">
<NuxtLink :to="`/dashboard/surveys/${slotProps.data.id}`">
<Button label="Edit" size="small" link />
</NuxtLink>
</template>
</Column>
</DataTable>
<DataTable v-else :value="surveys" table-style="width: 100%;">
<Column field="id" header="ID" style="width: 20%;" />
<Column field="title" header="Title" style="width: 50%" />
<Column field="workshop" header="Type" style="width: 20%">
<template #body="slotProps">
<Badge v-if="slotProps.data.workshop" value="Workshop" />
<Badge v-else value="Booth" severity="warning" />
</template>
</Column>
<Column header="Actions">
<template #body="slotProps">
<NuxtLink :to="`/dashboard/surveys/${slotProps.data.id}`">
<Button label="Edit" size="small" link />
</NuxtLink>
</template>
</Column>
</DataTable>

<br>
<br>

<Button v-if="surveys.length !== 0" size="small" label="Create new" @click="visible = true" />
</template>
<Button v-if="surveys.length !== 0" size="small" label="Create new" @click="visible = true" />
</template>
</section>
<section>
<div flex items-center justify-between>
<h2 text-3xl font-bold>
Lucky draw
</h2>
<Button label="Draw" @click="draw" />
</div>

<div mt-6 flex flex-col gap3>
<h3 text-xl font-semibold>
Past winners
</h3>

<DataTable v-if="pastWinners" :value="pastWinners">
<Column field="id" header="ID" style="width: 30%" />
<Column field="name" header="Name" style="width 20%" />
<Column field="nric" header="NRIC" style="width: 20%" />
<Column field="phone" header="Phone" style="width: 20%" />
<Column header="Delete" style="width: 10%">
<template #body="slotProps">
<Button label="Delete" size="small" severity="danger" @click="deleteWinner(slotProps.data.id)" />
</template>
</Column>
</DataTable>
</div>
</section>
</main>
</template>

Expand Down
4 changes: 3 additions & 1 deletion pages/s/[id].vue
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,9 @@ async function submit() {
<DashboardError v-if="surveyError" v-bind="surveyError" />
<template v-else-if="survey">
<div mx-auto max-w-3xl container>
<NuxtImg preload height="135px" src="/images/logo.webp" />
<div>
<NuxtImg preload height="135px" width="auto" densities="x1 x2 x3" src="/images/logo.webp" />
</div>

<div mt10>
<h1 text-5xl font-bold>
Expand Down
12 changes: 4 additions & 8 deletions pages/thanks.vue
Original file line number Diff line number Diff line change
Expand Up @@ -90,15 +90,11 @@ async function create() {

<template>
<div p-6 lg:px-30>
<header
class="h-24 flex items-center justify-between"
>
<NuxtLink to="/">
<span class="font-semibold">{{ config.public.appName }}</span>
</NuxtLink>
</header>

<main flex flex-col gap10>
<div>
<NuxtImg preload height="135px" width="auto" densities="x1 x2 x3" src="/images/logo.webp" />
</div>

<section>
<Skeleton v-if="surveyPending" />
<span v-else flex flex-col>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "User" ADD COLUMN "won" BOOL;
1 change: 1 addition & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ model User {
phone String?
admin Boolean
won Boolean?
surveyResponses Response[]
}
Expand Down
2 changes: 2 additions & 0 deletions server/trpc/routers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { meRouter } from './me'
import { surveyRouter } from './survey'
import { analyticsRouter } from './analytics'
import { responseRouter } from './response'
import { luckyDrawRouter } from './luckydraw'

export const appRouter = router({
hello: publicProcedure
Expand All @@ -23,6 +24,7 @@ export const appRouter = router({
survey: surveyRouter,
response: responseRouter,
analytics: analyticsRouter,
luckyDraw: luckyDrawRouter,
me: meRouter,
})

Expand Down
98 changes: 98 additions & 0 deletions server/trpc/routers/luckydraw/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { getRandomValues } from 'node:crypto'
import { TRPCError } from '@trpc/server'
import { z } from 'zod'
import { protectedProcedure, router } from '../../trpc'

function getRandomIntInclusive(min: number, max: number) {
const randomBuffer = new Uint32Array(1)

getRandomValues(randomBuffer)

const randomNumber = randomBuffer[0] / (0xFFFFFFFF + 1)

min = Math.ceil(min)
max = Math.floor(max)
return Math.floor(randomNumber * (max - min + 1)) + min
}

export const luckyDrawRouter = router({
pastWinners: protectedProcedure.query(async ({ ctx }) => {
return await ctx.prisma.user.findMany({
where: {
won: true,
},
})
}),
deleteWinner: protectedProcedure.input(
z.object({
id: z.string(),
}),
).mutation(async ({ ctx, input }) => {
return await ctx.prisma.user.update({
where: {
id: input.id,
},
data: {
won: false,
},
})
}),
draw: protectedProcedure.mutation(async ({ ctx }) => {
const [byBooths, byWorkshops, previousWinners] = await Promise.all([
ctx.prisma.response.groupBy({
by: ['respondentId'],
where: {
survey: {
workshop: false,
},
},
having: {
surveyId: {
_count: {
gt: 1,
},
},
},
}).then(r => r.map(j => j.respondentId)),
ctx.prisma.response.groupBy({
by: ['respondentId'],
where: {
survey: {
workshop: true,
},
},
having: {
surveyId: {
_count: {
gt: 0,
},
},
},
}).then(r => r.map(j => j.respondentId)),
ctx.prisma.user.findMany({
where: {
won: true,
},
}).then(u => u.map(j => j.id)),
])

const possibilities = (Array.from(new Set([...byBooths, ...byWorkshops]))).filter(j => !previousWinners.includes(j))
if (possibilities.length === 0) {
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'No users satisfy condition',
})
}

const number = getRandomIntInclusive(0, possibilities.length - 1)

return await ctx.prisma.user.update({
where: {
id: possibilities[number],
},
data: {
won: true,
},
})
}),
})

0 comments on commit 6fe40ad

Please sign in to comment.