Skip to content

Commit

Permalink
Add todo example
Browse files Browse the repository at this point in the history
  • Loading branch information
mikealche committed Dec 2, 2021
1 parent 55341eb commit e68cdf8
Show file tree
Hide file tree
Showing 11 changed files with 295 additions and 13 deletions.
2 changes: 1 addition & 1 deletion packages/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"build": "tsc",
"start": "node dist/start",
"dev": "dotenv -e .env.development nodemon src/start.ts",
"migrate:dev": "yarn prisma migrate dev"
"migrate:dev": "dotenv -e .env.development yarn prisma migrate dev"
},
"keywords": [],
"author": "Mike Alche",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
-- CreateEnum
CREATE TYPE "TodoStatus" AS ENUM ('PENDING', 'FINISHED');

-- CreateTable
CREATE TABLE "Todo" (
"id" SERIAL NOT NULL,
"text" TEXT NOT NULL,
"status" "TodoStatus" NOT NULL,
"userId" INTEGER NOT NULL,

CONSTRAINT "Todo_pkey" PRIMARY KEY ("id")
);

-- AddForeignKey
ALTER TABLE "Todo" ADD CONSTRAINT "Todo_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
16 changes: 16 additions & 0 deletions packages/backend/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,27 @@ model User {
role Role @default(USER)
todos Todo[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}

model Todo {
id Int @id @default(autoincrement())
text String
status TodoStatus
User User @relation(fields: [userId], references: [id])
userId Int
}

enum TodoStatus {
PENDING
FINISHED
}

enum Role {
SUPERADMIN
ADMIN
Expand Down
62 changes: 62 additions & 0 deletions packages/backend/src/controllers/TodosController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { Context, protectedRoute } from "../Context";
import * as trpc from "@trpc/server";
import * as Yup from "yup";
import db from "../db";

const TodosController = trpc.router<Context>().merge(
"",
protectedRoute
.query("own", {
resolve: async ({ ctx }) => {
const todos = db.todo.findMany({
where: {
userId: ctx.user?.id!,
},
});
return todos;
},
})
.mutation("complete", {
input: Yup.object({
todoId: Yup.number(),
}),
resolve: async ({ ctx, input }) => {
return await db.todo.update({
where: {
id: input.todoId,
},
data: {
status: "FINISHED",
},
});
},
})
.mutation("create", {
input: Yup.object({
text: Yup.string().required(),
}),
resolve: async ({ ctx, input }) => {
return await db.todo.create({
data: {
status: "PENDING",
text: input.text,
userId: ctx.user!.id,
},
});
},
})
.mutation("delete", {
input: Yup.object({
todoId: Yup.number().required(),
}),
resolve: async ({ ctx, input }) => {
await db.todo.delete({
where: {
id: input.todoId,
},
});
},
})
);

export default TodosController;
6 changes: 5 additions & 1 deletion packages/backend/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,12 @@ import { createHTTPServer } from "@trpc/server/adapters/standalone";
import createContext, { Context } from "./Context";
import AuthController from "./controllers/AuthController";
import http from "http";
import TodosController from "./controllers/TodosController";

export const appRouter = trpc.router<Context>().merge("auth/", AuthController);
export const appRouter = trpc
.router<Context>()
.merge("auth/", AuthController)
.merge("todos/", TodosController);

export type AppRouter = typeof appRouter;

Expand Down
53 changes: 53 additions & 0 deletions packages/web/components/AddTodoForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { Formik } from "formik";
import { useQueryClient } from "react-query";

import { trpc } from "../api/APIProvider";
import { useErrorNotificationToast } from "../hooks/useErrorNotificationToast";

const AddTodoForm = () => {
const queryClient = useQueryClient();

const createTodoMutation = trpc.useMutation("todos/create", {
onSuccess: () => {
queryClient.invalidateQueries(["todos/own"], { exact: false });
},
});
useErrorNotificationToast(createTodoMutation.error?.message);

return (
<Formik
initialValues={{ text: "" }}
onSubmit={async (values, { resetForm }) => {
try {
await createTodoMutation.mutateAsync(values);
resetForm();
} catch (err) {}
}}
>
{({
values,
errors,
touched,
handleChange,
handleBlur,
handleSubmit,
}) => (
<form onSubmit={handleSubmit}>
<div className="">
<input
type="text"
name="text"
onChange={handleChange}
onBlur={handleBlur}
value={values.text}
placeholder="Enter to submit"
className="block text-gray-700 text-sm font-bold mb-2 py-2 border px-4 bg-gray-50 w-full"
/>
{touched.text && errors.text}
</div>
</form>
)}
</Formik>
);
};
export default AddTodoForm;
23 changes: 15 additions & 8 deletions packages/web/components/Navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,26 @@ export const Navbar = () => {
const { logout } = useAuth();
return (
<div className="w-full flex justify-between py-5">
<div className="px-5 py-2 border bg-white font-bold rounded shadow-sm">
<Link href="/">
<a>Home</a>
</Link>
<div className="flex gap-4">
<div className="px-5 py-2 border bg-white font-bold rounded">
<Link href="/">
<a>Home</a>
</Link>
</div>
<div className="px-5 py-2 border bg-white font-bold rounded">
<Link href="/my-todos">
<a>My Todos</a>
</Link>
</div>
</div>
<UnauthenticatedOnly>
<div className="flex">
<div className="ml-10 px-5 py-2 border bg-white font-bold rounded shadow-sm">
<div className="ml-10 px-5 py-2 border bg-white font-bold rounded">
<Link href="/signup">
<a>Sign Up</a>
</Link>
</div>
<div className="ml-10 px-5 py-2 border bg-white font-bold rounded shadow-sm">
<div className="ml-10 px-5 py-2 border bg-white font-bold rounded">
<Link href="/login">
<a>Log In</a>
</Link>
Expand All @@ -31,10 +38,10 @@ export const Navbar = () => {
</UnauthenticatedOnly>
<AuthenticatedOnly>
<div className="flex">
<div className="ml-10 px-5 py-2 border bg-white font-bold rounded shadow-sm">
<div className="ml-10 px-5 py-2 border bg-white font-bold rounded">
<UserInfo />
</div>
<div className="ml-10 px-5 py-2 border bg-white font-bold rounded shadow-sm">
<div className="ml-10 px-5 py-2 border bg-white font-bold rounded">
<button className="font-bold" onClick={logout}>
Log Out
</button>
Expand Down
15 changes: 15 additions & 0 deletions packages/web/hooks/useRequiresAuth.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { useRouter } from "next/dist/client/router";
import { useEffect } from "react";
import { useAuth } from "../contexts/auth";

const useRequiresAuth = () => {
const { user, isAuthenticated } = useAuth();
const router = useRouter();
useEffect(() => {
if (!isAuthenticated) {
router.push("/login");
}
}, [isAuthenticated]);
};

export default useRequiresAuth;
2 changes: 1 addition & 1 deletion packages/web/layouts/MainLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Navbar } from "../components/Navbar";

const MainLayout = ({ children }: { children: JSX.Element }) => {
return (
<div className="w-screen bg-gray-100 h-screen p-10">
<div className="w-screen bg-blue-50 h-screen p-10">
<Navbar />
{children}
</div>
Expand Down
7 changes: 5 additions & 2 deletions packages/web/pages/login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@ import Image from "next/image";
import { trpc } from "../api/APIProvider";
import Button from "../components/Button";
import { useAuth } from "../contexts/auth";
import { useErrorNotificationToast } from "../hooks/useErrorNotificationToast";
import MainLayout, { Card } from "../layouts/MainLayout";

const Signup: NextPage = () => {
const loginMutation = trpc.useMutation("auth/login");
const { authenticate } = useAuth();
useErrorNotificationToast(loginMutation.error?.message);

return (
<MainLayout>
<Card className="max-w-max px-20 mx-auto">
Expand Down Expand Up @@ -45,7 +48,7 @@ const Signup: NextPage = () => {
placeholder="email"
className="block text-gray-700 text-sm font-bold mb-2 py-2 border px-4 bg-gray-50 w-full"
/>
{errors.email && touched.email && errors.email}
{touched.email && errors.email}
<input
type="password"
name="password"
Expand All @@ -55,7 +58,7 @@ const Signup: NextPage = () => {
placeholder="password"
className="block text-gray-700 text-sm font-bold mb-2 py-2 border px-4 bg-gray-50 w-full"
/>
{errors.password && touched.password && errors.password}
{touched.password && errors.password}
</div>
<Button type="submit" disabled={isSubmitting}>
Submit
Expand Down
107 changes: 107 additions & 0 deletions packages/web/pages/my-todos.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { useQueryClient } from "react-query";

import { trpc } from "../api/APIProvider";
import AddTodoForm from "../components/AddTodoForm";
import { useAuth } from "../contexts/auth";
import { useErrorNotificationToast } from "../hooks/useErrorNotificationToast";
import useRequiresAuth from "../hooks/useRequiresAuth";
import MainLayout, { Card } from "../layouts/MainLayout";

import type { NextPage } from "next";
const Home: NextPage = () => {
const { user, isAuthenticated } = useAuth();
const { data: ownTodos, refetch } = trpc.useQuery(["todos/own"], {
enabled: isAuthenticated,
});

useRequiresAuth();

const queryClient = useQueryClient();
const completeTodoMutation = trpc.useMutation("todos/complete", {
onSuccess: () => {
queryClient.invalidateQueries(["todos/own"], { exact: false });
},
});
useErrorNotificationToast(completeTodoMutation.error?.message);

const deleteTodoMutation = trpc.useMutation("todos/delete", {
onSuccess: () => {
queryClient.invalidateQueries(["todos/own"], { exact: false });
},
});
useErrorNotificationToast(deleteTodoMutation.error?.message);

return (
<MainLayout>
<Card>
<>
<h1 className="font-black text-3xl mb-10">My Todos</h1>
<div className="max-w-xl">
{ownTodos?.map((todo) => (
<div className="flex justify-between mb-5">
<p
key={todo.id}
className={
todo.status === "FINISHED"
? "line-through text-green-400"
: ""
}
>
{todo.text}
</p>
<div className="flex gap-4">
<button
onClick={() =>
completeTodoMutation.mutateAsync({ todoId: todo.id })
}
>
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M5 13l4 4L19 7"
/>
</svg>
</button>
<button
onClick={() =>
deleteTodoMutation.mutateAsync({ todoId: todo.id })
}
>
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
/>
</svg>
</button>
</div>
</div>
))}
</div>

<div className="max-w-sm">
<AddTodoForm />
</div>
</>
</Card>
</MainLayout>
);
};

export default Home;

0 comments on commit e68cdf8

Please sign in to comment.