parent
06bdab6440
commit
6d212b4561
@ -0,0 +1,26 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the `Example` table. If the table is not empty, all the data it contains will be lost.
|
||||
|
||||
*/
|
||||
-- CreateEnum
|
||||
CREATE TYPE "TodoStatus" AS ENUM ('INCOMPLETE', 'COMPLETE');
|
||||
|
||||
-- DropTable
|
||||
DROP TABLE "Example";
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Todo" (
|
||||
"id" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"text" TEXT NOT NULL,
|
||||
"status" "TodoStatus" NOT NULL DEFAULT 'INCOMPLETE',
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) 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 CASCADE ON UPDATE CASCADE;
|
@ -1,11 +0,0 @@
|
||||
// Src/pages/api/examples.ts
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
|
||||
import { prisma } from '../../server/db/client';
|
||||
|
||||
const examples = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const examplesFromDb = await prisma.example.findMany();
|
||||
res.status(200).json(examplesFromDb);
|
||||
};
|
||||
|
||||
export default examples;
|
@ -1,30 +0,0 @@
|
||||
import { trpc } from '~/utils/trpc';
|
||||
|
||||
export default function Example() {
|
||||
const hello = trpc.useQuery(['example.hello', { text: 'from tRPC!' }]);
|
||||
const getAll = trpc.useQuery(['example.getAll']);
|
||||
|
||||
return (
|
||||
<>
|
||||
<main className="flex-1 overflow-y-auto">
|
||||
{/* Primary column */}
|
||||
<section
|
||||
aria-labelledby="primary-heading"
|
||||
className="flex h-full min-w-0 flex-1 flex-col lg:order-last">
|
||||
<h1 className="sr-only" id="primary-heading">
|
||||
Photos
|
||||
</h1>
|
||||
<div className="pt-6 text-2xl text-blue-500 flex justify-center items-center w-full">
|
||||
{hello.data ? <p>{hello.data.greeting}</p> : <p>Loading..</p>}
|
||||
</div>
|
||||
<pre className="w-1/2">{JSON.stringify(getAll.data, null, 2)}</pre>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
{/* Secondary column (hidden on smaller screens) */}
|
||||
<aside className="hidden w-96 overflow-y-auto border-l border-gray-200 bg-white lg:block">
|
||||
{/* Your content */}
|
||||
</aside>
|
||||
</>
|
||||
);
|
||||
}
|
@ -0,0 +1,213 @@
|
||||
import Head from 'next/head';
|
||||
import Link from 'next/link';
|
||||
import { useSession } from 'next-auth/react';
|
||||
import { useRef, useState } from 'react';
|
||||
|
||||
import { trpc } from '~/utils/trpc';
|
||||
|
||||
export default function TodoList() {
|
||||
const { data } = useSession();
|
||||
|
||||
// Fetches todos when the component mounts.
|
||||
const todosQuery = trpc.useQuery(['todos.list']);
|
||||
// Context to be initialized here because of the rules of hooks.
|
||||
const trpcContext = trpc.useContext();
|
||||
|
||||
const todoUpdateMutation = trpc.useMutation(['todos.user.update'], {
|
||||
onSuccess: () => {
|
||||
// Since a Todo has been updated, we invalidate the query for
|
||||
// all the todos to trigger a refetch.
|
||||
trpcContext.invalidateQueries(['todos.list']);
|
||||
},
|
||||
});
|
||||
const todoDeleteMutation = trpc.useMutation(['todos.user.delete']);
|
||||
const [currentlyEditingTodo, setCurrentlyEditingTodo] = useState<
|
||||
string | null
|
||||
>(null);
|
||||
const formRef = useRef<HTMLFormElement | null>(null);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>Todo List</title>
|
||||
</Head>
|
||||
<div className="mx-auto px-4 py-8 sm:px-6 lg:px-8">
|
||||
<div className="sm:flex sm:items-center">
|
||||
<div className="sm:flex-auto">
|
||||
<h1 className="text-xl font-semibold text-gray-900">Todos</h1>
|
||||
<p className="mt-2 text-sm text-gray-700">
|
||||
A list of all Todos added by everyone.
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-4 sm:mt-0 sm:ml-16 sm:flex-none">
|
||||
<Link
|
||||
className="inline-flex items-center justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 sm:w-auto"
|
||||
href="/todos/new">
|
||||
Add Todo
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
{todosQuery.isLoading ? (
|
||||
<div>Loading...</div>
|
||||
) : (
|
||||
<div className="mt-8 flex flex-col">
|
||||
<div className="-my-2 -mx-4 overflow-x-auto sm:-mx-6 lg:-mx-8">
|
||||
<div className="inline-block min-w-full py-2 align-middle md:px-6 lg:px-8">
|
||||
<div className="overflow-hidden shadow ring-1 ring-black ring-opacity-5 md:rounded-lg">
|
||||
<table className="min-w-full divide-y divide-gray-300">
|
||||
<thead className="bg-gray-50">
|
||||
<tr className="divide-x divide-gray-200">
|
||||
<th
|
||||
className="py-3.5 pl-4 pr-4 text-left text-sm font-semibold text-gray-900 sm:pl-6"
|
||||
scope="col">
|
||||
Description
|
||||
</th>
|
||||
<th
|
||||
className="px-4 py-3.5 text-left text-sm font-semibold text-gray-900"
|
||||
scope="col">
|
||||
Creator
|
||||
</th>
|
||||
<th
|
||||
className="px-4 py-3.5 text-left text-sm font-semibold text-gray-900"
|
||||
scope="col">
|
||||
Last Updated
|
||||
</th>
|
||||
<th
|
||||
className="py-3.5 pl-4 pr-4 text-left text-sm font-semibold text-gray-900 sm:pr-6"
|
||||
scope="col">
|
||||
Status
|
||||
</th>
|
||||
<th
|
||||
className="py-3.5 pl-4 pr-4 text-left text-sm font-semibold text-gray-900 sm:pr-6"
|
||||
scope="col">
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200 bg-white">
|
||||
{todosQuery.data?.map((todo) => (
|
||||
<tr key={todo.id} className="divide-x divide-gray-200">
|
||||
<td className="whitespace-nowrap py-4 pl-4 pr-4 text-sm text-gray-500 sm:pl-6">
|
||||
{todo.id === currentlyEditingTodo ? (
|
||||
<form
|
||||
ref={formRef}
|
||||
onSubmit={async (event) => {
|
||||
event.preventDefault();
|
||||
if (!formRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData(
|
||||
formRef.current,
|
||||
);
|
||||
|
||||
// Trim todo text before submission.
|
||||
const text = (
|
||||
formData?.get('text') as string | null
|
||||
)?.trim();
|
||||
|
||||
// Ignore if the text is empty.
|
||||
if (!text) {
|
||||
return;
|
||||
}
|
||||
|
||||
await todoUpdateMutation.mutate({
|
||||
id: todo.id,
|
||||
text,
|
||||
});
|
||||
|
||||
setCurrentlyEditingTodo(null);
|
||||
}}>
|
||||
<input
|
||||
autoFocus={true}
|
||||
className="block w-full min-w-0 flex-1 rounded-md border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"
|
||||
defaultValue={todo.text}
|
||||
name="text"
|
||||
type="text"
|
||||
/>
|
||||
</form>
|
||||
) : (
|
||||
todo.text
|
||||
)}
|
||||
</td>
|
||||
<td className="whitespace-nowrap p-4 text-sm text-gray-500">
|
||||
{todo.user.name}
|
||||
</td>
|
||||
<td className="whitespace-nowrap p-4 text-sm text-gray-500">
|
||||
{todo.updatedAt.toLocaleString('en-US', {
|
||||
dateStyle: 'long',
|
||||
timeStyle: 'medium',
|
||||
})}
|
||||
</td>
|
||||
<td className="whitespace-nowrap py-4 pl-4 pr-4 text-sm text-gray-500 sm:pr-6">
|
||||
<input
|
||||
checked={todo.status === 'COMPLETE'}
|
||||
className="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
|
||||
type="checkbox"
|
||||
onChange={() => {
|
||||
todoUpdateMutation.mutate({
|
||||
id: todo.id,
|
||||
status:
|
||||
todo.status === 'COMPLETE'
|
||||
? 'INCOMPLETE'
|
||||
: 'COMPLETE',
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
<td className="space-x-4 whitespace-nowrap py-4 pl-4 pr-4 text-sm text-gray-500 sm:pr-6">
|
||||
{data?.user?.id === todo.userId && (
|
||||
<>
|
||||
{currentlyEditingTodo === todo.id ? (
|
||||
<a
|
||||
className="text-indigo-600 hover:text-indigo-900"
|
||||
href="#"
|
||||
onClick={() => {
|
||||
setCurrentlyEditingTodo(null);
|
||||
}}>
|
||||
Cancel
|
||||
</a>
|
||||
) : (
|
||||
<a
|
||||
className="text-indigo-600 hover:text-indigo-900"
|
||||
href="#"
|
||||
onClick={async () => {
|
||||
setCurrentlyEditingTodo(todo.id);
|
||||
}}>
|
||||
Edit
|
||||
</a>
|
||||
)}
|
||||
<a
|
||||
className="text-indigo-600 hover:text-indigo-900"
|
||||
href="#"
|
||||
onClick={async () => {
|
||||
const confirmDelete = window.confirm(
|
||||
'Are you sure you want to delete this Todo?',
|
||||
);
|
||||
|
||||
if (!confirmDelete) {
|
||||
return;
|
||||
}
|
||||
|
||||
todoDeleteMutation.mutate({
|
||||
id: todo.id,
|
||||
});
|
||||
}}>
|
||||
Delete
|
||||
</a>
|
||||
</>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
@ -0,0 +1,91 @@
|
||||
import Head from 'next/head';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useRef, useState } from 'react';
|
||||
|
||||
import { trpc } from '~/utils/trpc';
|
||||
|
||||
export default function TodosCreate() {
|
||||
const todoCreateMutation = trpc.useMutation('todos.user.create');
|
||||
const [todoText, setTodoText] = useState('');
|
||||
const router = useRouter();
|
||||
const formRef = useRef<HTMLFormElement | null>(null);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>Add Todo</title>
|
||||
</Head>
|
||||
<main className="flex-1 overflow-y-auto">
|
||||
{/* Primary column */}
|
||||
<section
|
||||
aria-labelledby="primary-heading"
|
||||
className="flex h-full min-w-0 flex-1 flex-col lg:order-last">
|
||||
<div className="mx-auto w-96 space-y-4 py-8">
|
||||
<h1 className="text-4xl font-bold" id="primary-heading">
|
||||
Add Todo
|
||||
</h1>
|
||||
<form
|
||||
ref={formRef}
|
||||
className="w-full space-y-8 divide-y divide-gray-200"
|
||||
onSubmit={async (event) => {
|
||||
event.preventDefault();
|
||||
if (!formRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData(formRef.current);
|
||||
// Trim todo text before submission.
|
||||
const text = (formData?.get('text') as string | null)?.trim();
|
||||
|
||||
// Ignore if the text is empty.
|
||||
if (!text) {
|
||||
return;
|
||||
}
|
||||
|
||||
await todoCreateMutation.mutate({
|
||||
text,
|
||||
});
|
||||
|
||||
// Redirect back to the todo list page after it has been added.
|
||||
router.push('/todos');
|
||||
}}>
|
||||
<div className="mt-6">
|
||||
<label
|
||||
className="block text-sm font-medium text-gray-700"
|
||||
htmlFor="text">
|
||||
Text
|
||||
</label>
|
||||
<div className="mt-1 flex rounded-md shadow-sm">
|
||||
<input
|
||||
autoFocus={true}
|
||||
className="block w-full min-w-0 flex-1 rounded-md border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"
|
||||
id="text"
|
||||
name="text"
|
||||
type="text"
|
||||
value={todoText}
|
||||
onChange={(event) => setTodoText(event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="pt-5">
|
||||
<div className="flex justify-end">
|
||||
<Link
|
||||
className="rounded-md border border-gray-300 bg-white py-2 px-4 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
|
||||
href="/todos">
|
||||
Cancel
|
||||
</Link>
|
||||
<button
|
||||
className="ml-3 inline-flex justify-center rounded-md border border-transparent bg-indigo-600 py-2 px-4 text-sm font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
|
||||
type="submit">
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</>
|
||||
);
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { createRouter } from './context';
|
||||
|
||||
export const exampleRouter = createRouter()
|
||||
.query('hello', {
|
||||
input: z
|
||||
.object({
|
||||
text: z.string().nullish(),
|
||||
})
|
||||
.nullish(),
|
||||
resolve({ input }) {
|
||||
return {
|
||||
greeting: `Hello ${input?.text ?? 'world'}`,
|
||||
};
|
||||
},
|
||||
})
|
||||
.query('getAll', {
|
||||
async resolve({ ctx }) {
|
||||
const items = await ctx.prisma.example.findMany();
|
||||
return items;
|
||||
},
|
||||
});
|
@ -1,14 +1,18 @@
|
||||
// Src/server/router/index.ts
|
||||
import superjson from 'superjson';
|
||||
|
||||
import { createRouter } from './context';
|
||||
import { exampleRouter } from './example';
|
||||
import { protectedExampleRouter } from './protected-example-router';
|
||||
import { todosRouter } from './todos';
|
||||
import { todosUserRouter } from './todos-user-router';
|
||||
|
||||
export const appRouter = createRouter()
|
||||
.transformer(superjson)
|
||||
.merge('example.', exampleRouter)
|
||||
.merge('auth.', protectedExampleRouter);
|
||||
|
||||
// All keys should be delimited by a period and end with a period.
|
||||
// Example routers. Learn more about tRPC routers: https://trpc.io/docs/v9/router
|
||||
.merge('auth.', protectedExampleRouter)
|
||||
.merge('todos.', todosRouter)
|
||||
.merge('todos.user.', todosUserRouter);
|
||||
|
||||
// Export type definition of API
|
||||
export type AppRouter = typeof appRouter;
|
||||
|
@ -0,0 +1,55 @@
|
||||
import { z } from 'zod';
|
||||
import { TodoStatus } from '@prisma/client';
|
||||
|
||||
import { createProtectedRouter } from './context';
|
||||
|
||||
// Example Todos router that can only be hit if the user requesting is signed in.
|
||||
export const todosUserRouter = createProtectedRouter()
|
||||
.mutation('create', {
|
||||
// Validate that the creation payload is of this shape using Zod: https://zod.dev.
|
||||
// This adds typesafety to the useMutation payloads on the client.
|
||||
input: z.object({
|
||||
text: z.string(),
|
||||
}),
|
||||
async resolve({ ctx, input }) {
|
||||
const userId = ctx.session?.user?.id;
|
||||
return await ctx.prisma.todo.create({
|
||||
data: {
|
||||
text: input.text,
|
||||
userId,
|
||||
},
|
||||
});
|
||||
},
|
||||
})
|
||||
.mutation('update', {
|
||||
input: z.object({
|
||||
id: z.string(),
|
||||
status: z.nativeEnum(TodoStatus).optional(),
|
||||
text: z.string().optional(),
|
||||
}),
|
||||
async resolve({ ctx, input }) {
|
||||
// TODO: Check if session user owns this Todo.
|
||||
return await ctx.prisma.todo.update({
|
||||
data: {
|
||||
status: input.status,
|
||||
text: input.text,
|
||||
},
|
||||
where: {
|
||||
id: input.id,
|
||||
},
|
||||
});
|
||||
},
|
||||
})
|
||||
.mutation('delete', {
|
||||
input: z.object({
|
||||
id: z.string(),
|
||||
}),
|
||||
async resolve({ ctx, input }) {
|
||||
// TODO: Check if session user owns this Todo.
|
||||
return await ctx.prisma.todo.delete({
|
||||
where: {
|
||||
id: input.id,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
@ -0,0 +1,18 @@
|
||||
import { createRouter } from './context';
|
||||
|
||||
export const todosRouter = createRouter().query('list', {
|
||||
async resolve({ ctx }) {
|
||||
return await ctx.prisma.todo.findMany({
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
Loading…
Reference in new issue