diff --git a/package.json b/package.json index 28de61d0..85a37bea 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "react-icons": "^4.12.0", "redis": "^4.6.13", "square": "^34.0.1", + "swr": "^2.2.5", "zod": "^3.22.4", "zustand": "^4.4.7" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8013f694..2aacd516 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -56,6 +56,9 @@ dependencies: square: specifier: ^34.0.1 version: 34.0.1 + swr: + specifier: ^2.2.5 + version: 2.2.5(react@18.2.0) zod: specifier: ^3.22.4 version: 3.22.4 @@ -5778,6 +5781,16 @@ packages: use-sync-external-store: 1.2.0(react@18.2.0) dev: false + /swr@2.2.5(react@18.2.0): + resolution: {integrity: sha512-QtxqyclFeAsxEUeZIYmsaQ0UjimSq1RZ9Un7I68/0ClKK/U3LoyQunwkQfJZr2fc22DfIXLNDc2wFyTEikCUpg==} + peerDependencies: + react: ^16.11.0 || ^17.0.0 || ^18.0.0 + dependencies: + client-only: 0.0.1 + react: 18.2.0 + use-sync-external-store: 1.2.0(react@18.2.0) + dev: false + /synckit@0.8.8: resolution: {integrity: sha512-HwOKAP7Wc5aRGYdKH+dw0PRRpbO841v2DENBtjnR5HFWoiNByAl7vrx3p0G/rCyYXQsrxqtX48TImFtPcIHSpQ==} engines: {node: ^14.18.0 || >=16.0.0} diff --git a/src/app/(account)/join/steps/StepFour.tsx b/src/app/(account)/join/steps/StepFour.tsx index 0bf72554..dab55abe 100644 --- a/src/app/(account)/join/steps/StepFour.tsx +++ b/src/app/(account)/join/steps/StepFour.tsx @@ -1,7 +1,9 @@ import Button from '@/components/Button'; import Field from '@/components/Field'; import { fetcher } from '@/lib/fetcher'; -import React, { useState } from 'react'; +import { useRouter } from 'next/navigation'; +import { useState } from 'react'; +import useSWRMutation from 'swr/mutation'; import { useJoinUsStep, useJoinUsStudentInfo, useSetJoinUsHeading } from '../store'; export default function StepFour() { @@ -16,19 +18,23 @@ export default function StepFour() { const { prevStep } = useJoinUsStep(); const { studentInfo } = useJoinUsStudentInfo(); - const handleSignUp = async (e: React.ChangeEvent) => { + const router = useRouter(); + const createMember = useSWRMutation('member', fetcher.post, { + onError: () => { + setAgreementError('Server error.'); + }, + onSuccess: () => { + router.push('/settings'); + }, + }); + + const handleSignUp = async () => { setAgreementError(null); if (!agreement) { setAgreementError('Please agree to the terms'); return; } - // TODO(#17): Payment - try { - const res = await fetcher.post('member', { json: studentInfo }).json(); - console.log(res); - } catch { - setAgreementError('Server error.'); - } + createMember.trigger(studentInfo); }; const toggleAgreement = () => setAgreement(!agreement); @@ -48,7 +54,7 @@ export default function StepFour() { - diff --git a/src/app/api/member/route.ts b/src/app/api/member/route.ts index 3feb3939..3ccf4e32 100644 --- a/src/app/api/member/route.ts +++ b/src/app/api/member/route.ts @@ -1,12 +1,12 @@ import { db } from '@/db'; -import { members } from '@/db/schema'; +import { memberTable } from '@/db/schema'; import { currentUser } from '@clerk/nextjs'; import { createInsertSchema } from 'drizzle-zod'; import { z } from 'zod'; export async function POST(request: Request) { const req = await request.json(); - const schema = createInsertSchema(members, { + const schema = createInsertSchema(memberTable, { clerkId: z.undefined(), email: z.undefined(), }); @@ -21,7 +21,7 @@ export async function POST(request: Request) { return new Response(JSON.stringify(reqBody.error.format()), { status: 400 }); } - await db.insert(members).values({ + await db.insert(memberTable).values({ clerkId: user.id, email: user.emailAddresses[0].emailAddress, ...reqBody.data, diff --git a/src/components/Button.tsx b/src/components/Button.tsx index 7a047bf7..73c1f249 100644 --- a/src/components/Button.tsx +++ b/src/components/Button.tsx @@ -7,29 +7,24 @@ interface ButtonProps { children: React.ReactNode; colour: Colour; href?: string; - onClick?: (e: React.MouseEvent) => void | Promise; + onClick?: () => void; type?: 'button' | 'submit' | 'reset'; width?: string; + loading?: boolean; } -const Button = ({ children, colour, href, onClick, width, type }: ButtonProps) => { +const Button = ({ children, colour, href, onClick, width, type, loading }: ButtonProps) => { const isAnchor = !!href; const Component = isAnchor ? 'a' : 'button'; - const buttonStyles = `whitespace-nowrap py-4 px-12 md:py-1 md:px-2 lg:py-2 lg:px-6 border-2 border-black font-bold hover:bg-yellow transition-colors duration-300 ${BG_COLOURS[colour]} text-lg md:text-base`; - const buttonClasses = width ? `${buttonStyles} ${width}` : buttonStyles; - return ( - + | React.MouseEvent - ) => void | Promise - } + onClick={onClick} type={isAnchor ? undefined : type} - className={buttonClasses} + className={`${width} ${BG_COLOURS[colour]} ${isAnchor ? 'hover:bg-yellow' : 'hover:enabled:bg-yellow'} whitespace-nowrap border-2 border-black px-12 py-4 text-lg font-bold transition-colors duration-300 disabled:cursor-wait disabled:grayscale md:px-2 md:py-1 md:text-base lg:px-6 lg:py-2`} + disabled={loading} > {children} diff --git a/src/db/schema.ts b/src/db/schema.ts index 70f802ea..55f32ead 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -9,7 +9,7 @@ import { sql } from 'drizzle-orm'; import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'; import { nanoid } from 'nanoid'; -export const members = sqliteTable('members', { +export const memberTable = sqliteTable('members', { id: text('id') .$defaultFn(() => nanoid()) .primaryKey(), @@ -23,8 +23,8 @@ export const members = sqliteTable('members', { studentId: text('student_id'), gender: text('gender', { enum: GENDERS }).notNull(), ageBracket: text('age_bracket', { enum: AGE_BRACKETS }).notNull(), - degree: text('degree', { enum: DEGREES }), - studentType: text('student_type', { enum: STUDENT_TYPES }), + degree: text('degree', { enum: [...DEGREES, ''] }), + studentType: text('student_type', { enum: [...STUDENT_TYPES, ''] }), emailPreferences: text('email_preferences', { mode: 'json' }), diff --git a/src/lib/fetcher.ts b/src/lib/fetcher.ts index 5b403084..26dd0222 100644 --- a/src/lib/fetcher.ts +++ b/src/lib/fetcher.ts @@ -1,3 +1,10 @@ import ky from 'ky'; -export const fetcher = ky.create({ prefixUrl: '/api' }); +const kyInstance = ky.create({ prefixUrl: '/api' }); + +export const fetcher = { + get: async (args: Parameters) => + (await kyInstance.get(...args).json()) as any, + post: async (url: string, { arg }: { arg: unknown }) => + (await kyInstance.post(url, { json: arg }).json()) as any, +};