diff --git a/Dockerfile.dev b/Dockerfile.dev new file mode 100644 index 0000000..4467c5f --- /dev/null +++ b/Dockerfile.dev @@ -0,0 +1,17 @@ +FROM node:22-alpine@sha256:41e4389f3d988d2ed55392df4db1420ad048ae53324a8e2b7c6d19508288107e + +WORKDIR /app + +RUN corepack enable +COPY package.json yarn.lock .yarnrc.yml ./ +RUN yarn install --frozen-lockfile + +COPY . . + +ENV NODE_ENV=development +ENV NEXT_TELEMETRY_DISABLED=1 + +EXPOSE 3000 +ENV HOSTNAME="0.0.0.0" + +CMD ["/bin/ash", "entrypoint.dev.sh"] \ No newline at end of file diff --git a/README.md b/README.md index 1f474b5..d9ca71b 100644 --- a/README.md +++ b/README.md @@ -104,7 +104,7 @@ This project is built with a modern tech stack: yarn prisma:generate ``` ```bash - yarn prisa:db:push + yarn prisma:db:push ``` - Run the following command to apply migrations and generate Prisma Client: ```bash @@ -129,6 +129,14 @@ This project is built with a modern tech stack: password: password ``` +**Docker Development Environment:** + +- The docker development environment can be started with the following command: + +```bash +yarn dev_container +``` + **Self-Hosting with Docker (Planned):** - A Docker image and `docker-compose.yml` file will be provided in the future to allow for easy self-hosting of the MeetUP application. This setup will also include database services. Instructions will be updated here once available. diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 0000000..3aa4174 --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,27 @@ +services: + app: + build: + context: . + dockerfile: Dockerfile.dev + ports: + - '3000:3000' + environment: + - AUTH_SECRET=secret + - AUTH_URL=http://localhost:3000 + - DATABASE_URL=file:/data/db.sqlite + env_file: + - .env.local + volumes: + - ./data:/data + develop: + watch: + - action: sync + path: ./src + target: /app/src + ignore: + - node_modules/ + - action: rebuild + path: package.json + - action: sync+restart + path: prisma + target: /app/prisma diff --git a/entrypoint.dev.sh b/entrypoint.dev.sh new file mode 100644 index 0000000..ec103bf --- /dev/null +++ b/entrypoint.dev.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +echo "Running start script with user $(whoami) and NODE_ENV $NODE_ENV" +if [ -d "prisma" ]; then + echo "Syncing Prisma database" + yarn prisma:generate + yarn prisma:db:push +fi + +exec yarn dev diff --git a/package.json b/package.json index a2687f3..50be21b 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,8 @@ "prisma:generate": "dotenv -e .env.local -- prisma generate", "prisma:studio": "dotenv -e .env.local -- prisma studio", "prisma:db:push": "dotenv -e .env.local -- prisma db push", - "prisma:migrate:reset": "dotenv -e .env.local -- prisma migrate reset" + "prisma:migrate:reset": "dotenv -e .env.local -- prisma migrate reset", + "dev_container": "docker compose -f docker-compose.dev.yml up --watch --build" }, "dependencies": { "@auth/prisma-adapter": "^2.9.1", @@ -48,8 +49,8 @@ "devDependencies": { "@eslint/eslintrc": "3.3.1", "@tailwindcss/postcss": "4.1.8", - "@types/node": "22.15.30", - "@types/react": "19.1.6", + "@types/node": "22.15.31", + "@types/react": "19.1.7", "@types/react-dom": "19.1.6", "dotenv-cli": "8.0.0", "eslint": "9.28.0", diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx index 5dd3000..2933872 100644 --- a/src/app/login/page.tsx +++ b/src/app/login/page.tsx @@ -26,12 +26,12 @@ export default async function LoginPage() { } return ( -
-
-
+
+
+
-
+
diff --git a/src/app/logout/page.tsx b/src/app/logout/page.tsx index c819a45..38311da 100644 --- a/src/app/logout/page.tsx +++ b/src/app/logout/page.tsx @@ -25,11 +25,7 @@ export default function SignOutPage() { - diff --git a/src/components/custom-ui/button.tsx b/src/components/custom-ui/button.tsx index 5b53423..2a25f66 100644 --- a/src/components/custom-ui/button.tsx +++ b/src/components/custom-ui/button.tsx @@ -5,7 +5,7 @@ import { cva, type VariantProps } from 'class-variance-authority'; import { cn } from '@/lib/utils'; const buttonVariants = cva( - "radius-lg inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-button transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", + "radius-lg inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-button transition-all cursor-pointer disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", { variants: { variant: { diff --git a/src/components/labeled-input.tsx b/src/components/labeled-input.tsx index bf6d222..ea26e51 100644 --- a/src/components/labeled-input.tsx +++ b/src/components/labeled-input.tsx @@ -7,6 +7,7 @@ export default function LabeledInput({ placeholder, value, name, + autocomplete, error, ...rest }: { @@ -15,6 +16,7 @@ export default function LabeledInput({ placeholder?: string; value?: string; name?: string; + autocomplete?: string; error?: string; } & React.InputHTMLAttributes) { return ( @@ -27,6 +29,7 @@ export default function LabeledInput({ defaultValue={value} id={name} name={name} + autoComplete={autocomplete} {...rest} /> {error &&

{error}

} diff --git a/src/components/user/login-form.tsx b/src/components/user/login-form.tsx index 5cf6ba8..463f7c1 100644 --- a/src/components/user/login-form.tsx +++ b/src/components/user/login-form.tsx @@ -1,14 +1,22 @@ 'use client'; +import React, { useState, useRef } from 'react'; +import { useRouter } from 'next/navigation'; + import LabeledInput from '@/components/labeled-input'; import { Button } from '@/components/custom-ui/button'; - import useZodForm from '@/hooks/useZodForm'; -import { loginSchema } from '@/validation'; +import { loginSchema, registerSchema } from '@/validation'; import { loginAction } from '@/login'; -import { useRouter } from 'next/navigation'; +import { registerAction } from '@/register'; -export default function LoginForm() { +function LoginFormElement({ + setIsSignUp, + formRef, +}: { + setIsSignUp: (value: boolean | ((prev: boolean) => boolean)) => void; + formRef?: React.RefObject; +}) { const { handleSubmit, formState, register, setError } = useZodForm(loginSchema); const router = useRouter(); @@ -40,7 +48,7 @@ export default function LoginForm() { }); return ( -
+ Login -
@@ -71,3 +86,139 @@ export default function LoginForm() { ); } + +function RegisterFormElement({ + setIsSignUp, + formRef, +}: { + setIsSignUp: (value: boolean | ((prev: boolean) => boolean)) => void; + formRef?: React.RefObject; +}) { + const { handleSubmit, formState, register, setError } = + useZodForm(registerSchema); + + const onSubmit = handleSubmit(async (data) => { + try { + const { error } = await registerAction(data); + + if (error) { + setError('root', { + message: error, + }); + return; + } else { + formRef?.current?.reset(); + setIsSignUp(false); + // TODO: Show registration success message (reminder to verify email) + return; + } + } catch (error: unknown) { + if (error instanceof Error) + setError('root', { + message: error?.message, + }); + else + setError('root', { + message: 'An unknown error occurred.', + }); + } + }); + + return ( +
+ + + + + + +
+ + +
+
+ {formState.errors.root?.message && ( +

{formState.errors.root?.message}

+ )} +
+ + ); +} + +export default function LoginForm() { + const [isSignUp, setIsSignUp] = useState(false); + + const formRef = useRef(null); + + if (isSignUp) { + return ( + + ); + } + return ( + + ); +} diff --git a/src/register.ts b/src/register.ts index 10a2a0e..7ef06ca 100644 --- a/src/register.ts +++ b/src/register.ts @@ -15,7 +15,7 @@ export async function registerAction(data: z.infer) { }; } - const { email, password, username } = result.data; + const { email, password, firstName, lastName, username } = result.data; const user = await prisma.user.findUnique({ where: { @@ -37,6 +37,9 @@ export async function registerAction(data: z.infer) { email, name: username, password_hash: passwordHash, + first_name: firstName, + last_name: lastName, + emailVerified: new Date(), // TODO: handle email verification }, }); diff --git a/src/validation.ts b/src/validation.ts index 2c501e6..c265c9d 100644 --- a/src/validation.ts +++ b/src/validation.ts @@ -10,6 +10,14 @@ export const loginSchema = zod.object({ export const registerSchema = zod .object({ + firstName: zod + .string() + .min(1, 'First name is required') + .max(32, 'First name must be at most 32 characters long'), + lastName: zod + .string() + .min(1, 'Last name is required') + .max(32, 'Last name must be at most 32 characters long'), email: zod .string() .email('Invalid email address') @@ -35,6 +43,15 @@ export const registerSchema = zod message: 'Passwords do not match', path: ['confirmPassword'], }) - .refine((data) => !data.password.includes(data.username), { - message: 'Password cannot contain username', - }); + .refine( + (data) => + !data.password.includes(data.firstName) && + !data.password.includes(data.lastName) && + !data.password.includes(data.email) && + !data.password.includes(data.username), + { + message: + 'Username cannot contain your first name, last name, email, or username itself', + path: ['password'], + }, + ); diff --git a/yarn.lock b/yarn.lock index bf464b7..a09881e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1659,12 +1659,12 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:22.15.30": - version: 22.15.30 - resolution: "@types/node@npm:22.15.30" +"@types/node@npm:22.15.31": + version: 22.15.31 + resolution: "@types/node@npm:22.15.31" dependencies: undici-types: "npm:~6.21.0" - checksum: 10c0/ca330ac0e7fd502686d6df115fcc606aba46fd334220f749bbba2f639accdadcb23f7900603ceccdc8240be736739cad5c0b87c0fa92c9255a4dff245f07d664 + checksum: 10c0/ef7d5dc890da41cfd554d35ab8998bc18be9e3a0caa642e720599ac4410a94a4879766e52b3c9cafa06c66b7b8aebdc51f322cf67df23a6489927890196a316d languageName: node linkType: hard @@ -1677,12 +1677,12 @@ __metadata: languageName: node linkType: hard -"@types/react@npm:19.1.6": - version: 19.1.6 - resolution: "@types/react@npm:19.1.6" +"@types/react@npm:19.1.7": + version: 19.1.7 + resolution: "@types/react@npm:19.1.7" dependencies: csstype: "npm:^3.0.2" - checksum: 10c0/8b10b198e28997b3c57559750f8bcf5ae7b33c554b16b6f4fe2ece1d4de6a2fc8cb53e7effe08ec9cb939d2f479eb97c5e08aac2cf83b10a90164fe451cc8ea2 + checksum: 10c0/3bb8fb865debad4328b0d623e1c669f2ee90e9302638a64e65a0a1c61efca4f4ef91f58b55ff94075358c190d80bb8472a5823c6901d8cdc9009dd436a1dcab1 languageName: node linkType: hard @@ -3890,8 +3890,8 @@ __metadata: "@radix-ui/react-switch": "npm:^1.2.4" "@radix-ui/react-tabs": "npm:^1.1.11" "@tailwindcss/postcss": "npm:4.1.8" - "@types/node": "npm:22.15.30" - "@types/react": "npm:19.1.6" + "@types/node": "npm:22.15.31" + "@types/react": "npm:19.1.7" "@types/react-dom": "npm:19.1.6" bcryptjs: "npm:^3.0.2" class-variance-authority: "npm:^0.7.1" @@ -4995,9 +4995,9 @@ __metadata: linkType: hard "tailwind-merge@npm:^3.2.0": - version: 3.3.0 - resolution: "tailwind-merge@npm:3.3.0" - checksum: 10c0/a50cd141100486f98541dfab3705712af5860556689b7496dc6b0284374f02d12d5471f0f40035f6bb8b1c749c422060a1f3e5f8900057d8a7786b111c8472e6 + version: 3.3.1 + resolution: "tailwind-merge@npm:3.3.1" + checksum: 10c0/b84c6a78d4669fa12bf5ab8f0cdc4400a3ce0a7c006511af4af4be70bb664a27466dbe13ee9e3b31f50ddf6c51d380e8192ce0ec9effce23ca729d71a9f63818 languageName: node linkType: hard