Skip to content

Commit

Permalink
[WIP] Feat/login (#23)
Browse files Browse the repository at this point in the history
* [backend] Add password field to user model
* [frontend] Add password input to user create form
* [frontend] Refactor directory structure (c.f. next-learn)
* [format] Format code with prettier
  • Loading branch information
usatie authored Nov 3, 2023
1 parent 9cce0e9 commit 3fa414d
Show file tree
Hide file tree
Showing 17 changed files with 222 additions and 77 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ prod:

.PHONY: fmt
fmt:
npx prettier --write frontend/src backend/src
npx prettier --write frontend backend

.PHONY: update
update:
Expand Down
2 changes: 1 addition & 1 deletion backend/.prettierrc
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"singleQuote": true,
"trailingComma": "all"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/*
Warnings:
- Added the required column `password` to the `User` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "User" ADD COLUMN "password" TEXT NOT NULL;
1 change: 1 addition & 0 deletions backend/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,5 @@ model User {
id Int @id @default(autoincrement())
email String @unique
name String?
password String
}
4 changes: 2 additions & 2 deletions backend/src/user/user.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export class UserController {

@Post()
create(
@Body() userData: { name?: string; email: string },
@Body() userData: { name?: string; email: string; password: string },
): Promise<UserModel> {
return this.userService.create(userData);
}
Expand All @@ -35,7 +35,7 @@ export class UserController {
@Patch(':id')
update(
@Param('id') id: string,
@Body() userData: { name?: string; email?: string },
@Body() userData: { name?: string; email?: string; password?: string },
) {
return this.userService.update(+id, userData);
}
Expand Down
2 changes: 1 addition & 1 deletion backend/test/jest-e2e.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"moduleNameMapper": {
"^src/(.*)$": "<rootDir>/../src/$1"
"^src/(.*)$": "<rootDir>/../src/$1"
},
"moduleFileExtensions": ["js", "json", "ts"],
"rootDir": ".",
Expand Down
4 changes: 3 additions & 1 deletion frontend/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ import "./globals.css";

// components
import { ThemeProvider } from "@/components/theme-provider";
import Nav from "@/components/Nav";
import { Toaster } from "@/components/ui/toaster";

// ui
import Nav from "@/app/ui/nav";

const inter = Inter({ subsets: ["latin"] });

export const metadata: Metadata = {
Expand Down
31 changes: 31 additions & 0 deletions frontend/app/lib/client-actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"use client";

import { RedirectType, redirect } from "next/navigation";
import { toast } from "@/components/ui/use-toast";

export async function createUser(formData: FormData) {
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/user`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
name: formData.get("name"),
email: formData.get("email"),
password: formData.get("password"),
}),
});
const data = await res.json();
if (!res.ok) {
toast({
title: res.status + " " + res.statusText,
description: data.message,
});
} else {
toast({
title: "Success",
description: "User created successfully.",
});
redirect("/user", RedirectType.push);
}
}
6 changes: 2 additions & 4 deletions frontend/components/Nav.tsx → frontend/app/ui/nav.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import Image from "next/image";
import { ModeToggle } from "./toggle-mode";
import { ModeToggle } from "@/components/toggle-mode";
import Link from "next/link";

export default function Nav() {
Expand All @@ -18,9 +18,7 @@ export default function Nav() {
/>
</li>
<li className="flex gap-8 items-center">
<Link href="/" className="">
Home
</Link>
<Link href="/">Home</Link>
<Link href="/user">User List</Link>
<Link href="/user/signup">Sign Up</Link>
<Link href="/playground/pong.html" target="_blank">
Expand Down
116 changes: 116 additions & 0 deletions frontend/app/ui/user/card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
"use client";

import { redirect, RedirectType } from "next/navigation";

// components
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Button } from "@/components/ui/button";
import { toast } from "@/components/ui/use-toast";

export type User = { id: number; name?: string; email?: string };

export default function UserCard({ user }: { user: User }) {
async function updateUser(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
const { id, ...updateData } = Object.fromEntries(
new FormData(event.currentTarget),
);
const res = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/user/${user.id}`,
{
method: "PATCH",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(updateData),
},
);
const data = await res.json();
if (!res.ok) {
toast({
title: res.status + " " + res.statusText,
description: data.message,
});
} else {
toast({
title: "Success",
description: "User updated successfully.",
});
redirect("/user", RedirectType.push);
}
}
async function deleteUser(event: React.SyntheticEvent) {
event.preventDefault();
const res = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/user/${user.id}`,
{
method: "DELETE",
},
);
const data = await res.json();
if (!res.ok) {
toast({
title: res.status + " " + res.statusText,
description: data.message,
});
} else {
toast({
title: "Success",
description: "User deleted successfully.",
});
redirect("/user", RedirectType.push);
}
}
return (
<>
<Card className="w-[300px]">
<CardHeader>ID: {user.id}</CardHeader>
<CardContent>
<form onSubmit={updateUser} id={"UpdateUserForm." + user.id}>
<div className="grid w-full items-center gap-4">
<div className="flex flex-col space-y-1.5">
<Label htmlFor="name">Name</Label>
<Input
id="name"
type="text"
name="name"
defaultValue={user.name}
/>
</div>
<div className="flex flex-col space-y-1.5">
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
name="email"
defaultValue={user.email}
/>
</div>
</div>
</form>
</CardContent>
<CardFooter className="flex justify-between">
<Button type="button" onClick={deleteUser}>
Delete
</Button>
<Button
variant="outline"
type="submit"
form={"UpdateUserForm." + user.id}
>
Update
</Button>
</CardFooter>
</Card>
</>
);
}
40 changes: 40 additions & 0 deletions frontend/app/ui/user/create-form.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
"use client";

import { createUser } from "@/app/lib/client-actions";

// components
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Button } from "@/components/ui/button";

export default function UserCreateForm() {
return (
<form action={createUser}>
<div className="grid w-full items-center gap-8">
<div className="flex flex-col space-y-1.5">
<Label htmlFor="name">User Name</Label>
<Input id="name" type="text" name="name" placeholder="e.g. nop" />
</div>
<div className="flex flex-col space-y-1.5">
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
name="email"
placeholder="e.g. [email protected]"
/>
</div>
<div className="flex flex-col space-y-1.5">
<Label htmlFor="password">Password</Label>
<Input
id="password"
type="password"
name="password"
placeholder="Must have at least 6 characters"
/>
</div>
<Button type="submit">Sign Up</Button>
</div>
</form>
);
}
2 changes: 1 addition & 1 deletion frontend/app/user/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import UserCard from "@/components/UserCard";
import UserCard from "@/app/ui/user/card";

async function getUser(id: number) {
const res = await fetch(`${process.env.API_URL}/user/${id}`, {
Expand Down
61 changes: 5 additions & 56 deletions frontend/app/user/signup/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
"use client";

import { useRouter } from "next/navigation";

// components
import {
Card,
Expand All @@ -11,63 +7,16 @@ import {
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Button } from "@/components/ui/button";
import { useToast } from "@/components/ui/use-toast";
import Form from "@/app/ui/user/create-form";

export default function SignUp() {
const router = useRouter();
const { toast } = useToast();
async function createUser(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/user`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(
Object.fromEntries(new FormData(event.currentTarget)),
),
});
const data = await res.json();
if (!res.ok) {
toast({
title: res.status + " " + res.statusText,
description: data.message,
});
} else {
toast({
title: "Success",
description: "User created successfully.",
});
router.push("/user");
router.refresh();
}
}

return (
<Card className="w-[300px]">
<CardHeader>Create Account</CardHeader>
<CardHeader>
<CardTitle>Create Account</CardTitle>
</CardHeader>
<CardContent>
<form onSubmit={createUser}>
<div className="grid w-full items-center gap-8">
<div className="flex flex-col space-y-1.5">
<Label htmlFor="name">Name</Label>
<Input id="name" type="text" name="name" placeholder="nop" />
</div>
<div className="flex flex-col space-y-1.5">
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
name="email"
placeholder="[email protected]"
/>
</div>
<Button type="submit">Sign Up</Button>
</div>
</form>
<Form />
</CardContent>
</Card>
);
Expand Down
2 changes: 1 addition & 1 deletion frontend/components.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,4 @@
"components": "@/components",
"utils": "@/lib/utils"
}
}
}
4 changes: 2 additions & 2 deletions frontend/next.config.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/** @type {import('next').NextConfig} */
const nextConfig = {}
const nextConfig = {};

module.exports = nextConfig
module.exports = nextConfig;
2 changes: 1 addition & 1 deletion frontend/postcss.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ module.exports = {
tailwindcss: {},
autoprefixer: {},
},
}
};
Loading

0 comments on commit 3fa414d

Please sign in to comment.