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 superjson from 'superjson';
|
||||||
|
|
||||||
import { createRouter } from './context';
|
import { createRouter } from './context';
|
||||||
import { exampleRouter } from './example';
|
|
||||||
import { protectedExampleRouter } from './protected-example-router';
|
import { protectedExampleRouter } from './protected-example-router';
|
||||||
|
import { todosRouter } from './todos';
|
||||||
|
import { todosUserRouter } from './todos-user-router';
|
||||||
|
|
||||||
export const appRouter = createRouter()
|
export const appRouter = createRouter()
|
||||||
.transformer(superjson)
|
.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 definition of API
|
||||||
export type AppRouter = typeof appRouter;
|
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