-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
chore: init new example #5595
base: next
Are you sure you want to change the base?
chore: init new example #5595
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
AUTH_SECRET=FlIbvvIt9KQXVBcg0wKpcrHMwGA5RNxpxn826LdbBF8= | ||
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. | ||
|
||
# dependencies | ||
/node_modules | ||
/.pnp | ||
.pnp.js | ||
.yarn/install-state.gz | ||
|
||
# testing | ||
/coverage | ||
|
||
# next.js | ||
/.next/ | ||
/out/ | ||
|
||
# production | ||
/build | ||
|
||
# misc | ||
.DS_Store | ||
*.pem | ||
|
||
# debug | ||
npm-debug.log* | ||
yarn-debug.log* | ||
yarn-error.log* | ||
|
||
# local env files | ||
.env*.local | ||
|
||
# vercel | ||
.vercel | ||
|
||
# typescript | ||
*.tsbuildinfo | ||
next-env.d.ts |
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should the db really be checked in? shouldn't that be lazily created with migrations or smth? can we do something that works to deploy on vercel? like sqlite locally and pg on vercel? |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
/** @type {import('next').NextConfig} */ | ||
const nextConfig = {}; | ||
|
||
export default nextConfig; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
{ | ||
"name": "examples-next-app-dir-new", | ||
"private": true, | ||
"type": "module", | ||
"scripts": { | ||
"db:push": "drizzle-kit push:sqlite --config src/db/config.ts", | ||
"dev": "next dev", | ||
"build": "next build", | ||
"start": "next start", | ||
"lint": "next lint" | ||
}, | ||
"dependencies": { | ||
"@hookform/resolvers": "npm:@hookform/resolvers@3.3.4", | ||
"@trpc/server": "npm:@trpc/server@next", | ||
"better-sqlite3": "^9.4.3", | ||
"drizzle-orm": "^0.30.5", | ||
"drizzle-zod": "^0.5.1", | ||
"next": "npm:next@14.2.0-canary.42", | ||
"next-auth": "npm:next-auth@5.0.0-beta.16", | ||
"react": "^18.2.0", | ||
"react-dom": "^18.2.0", | ||
"react-hook-form": "^7.43.3", | ||
"server-only": "0.0.1", | ||
"sonner": "^1.4.41", | ||
"zod": "^3.0.0" | ||
}, | ||
"devDependencies": { | ||
"@types/node": "^20.10.0", | ||
"@types/react": "^18.2.33", | ||
"@types/react-dom": "^18.2.14", | ||
"drizzle-kit": "^0.20.14", | ||
"eslint": "^8.56.0", | ||
"postcss": "^8.4.14", | ||
"tailwindcss": "^3.3.0", | ||
"typescript": "^5.4.0" | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
module.exports = { | ||
plugins: { tailwindcss: {} }, | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
'use client'; | ||
|
||
import { addPostSchema } from '@/db/schema'; | ||
import { toast } from 'sonner'; | ||
import { addPost } from './_action'; | ||
import { Form, useZodForm } from './Form'; | ||
|
||
export function AddPostForm() { | ||
const form = useZodForm({ | ||
schema: addPostSchema, | ||
defaultValues: { | ||
title: 'hello world', | ||
content: 'this is a test post', | ||
}, | ||
}); | ||
|
||
console.log(form.formState.errors); | ||
|
||
return ( | ||
<Form | ||
className="flex max-w-md flex-col gap-2" | ||
form={form} | ||
handleSubmit={async (values) => { | ||
const promise = addPost(values); | ||
return toast.promise(promise, { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Won't this toast twice on errors? 🤔 |
||
loading: 'Adding post...', | ||
success: 'Post added!', | ||
error: (error) => 'Failed to add post: ' + error.message, | ||
}); | ||
}} | ||
> | ||
<div> | ||
<input | ||
className="rounded bg-zinc-300 p-1 text-zinc-900" | ||
type="text" | ||
{...form.register('title')} | ||
defaultValue={form.control._defaultValues.title} | ||
/> | ||
{form.formState.errors.title && ( | ||
<div>Invalid title: {form.formState.errors.title.message}</div> | ||
)} | ||
</div> | ||
|
||
<div> | ||
<input | ||
className="rounded bg-zinc-300 p-1 text-zinc-900" | ||
type="text" | ||
{...form.register('content')} | ||
defaultValue={form.control._defaultValues.content} | ||
/> | ||
{form.formState.errors.content && ( | ||
<div>Invalid content: {form.formState.errors.content.message}</div> | ||
)} | ||
</div> | ||
<button | ||
className="rounded bg-zinc-800 p-1" | ||
type="submit" | ||
disabled={form.formState.isSubmitting} | ||
> | ||
Add post | ||
</button> | ||
</Form> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
'use client'; | ||
|
||
import { zodResolver } from '@hookform/resolvers/zod'; | ||
import type { FieldValues, UseFormProps, UseFormReturn } from 'react-hook-form'; | ||
import { FormProvider, useForm } from 'react-hook-form'; | ||
import { toast } from 'sonner'; | ||
import type { z } from 'zod'; | ||
|
||
/** | ||
* Reusable hook for zod + react-hook-form | ||
*/ | ||
export function useZodForm<TInput extends FieldValues>( | ||
props: Omit<UseFormProps<TInput>, 'resolver'> & { | ||
schema: z.ZodType<any, any, TInput>; | ||
}, | ||
) { | ||
const form = useForm<TInput>({ | ||
...props, | ||
resolver: zodResolver(props.schema, undefined, { | ||
raw: true, | ||
}), | ||
}); | ||
|
||
return form; | ||
} | ||
|
||
export const Form = <TInput extends FieldValues = never>(props: { | ||
children: React.ReactNode; | ||
form: UseFormReturn<TInput>; | ||
handleSubmit: (values: NoInfer<TInput>) => Promise<unknown>; | ||
className?: string; | ||
}) => { | ||
return ( | ||
<FormProvider {...props.form}> | ||
<form | ||
className={props.className} | ||
onSubmit={(event) => { | ||
return props.form.handleSubmit(async (values) => { | ||
try { | ||
await props.handleSubmit(values); | ||
} catch (error) { | ||
console.error('Uncaught error in form', error); | ||
toast.error('Failed to submit form'); | ||
} | ||
})(event); | ||
}} | ||
> | ||
{props.children} | ||
</form> | ||
</FormProvider> | ||
); | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
import { db } from '@/db/client'; | ||
import { Post } from '@/db/schema'; | ||
import { publicAction } from '@/server/trpc'; | ||
import { eq } from 'drizzle-orm'; | ||
import Link from 'next/link'; | ||
import { notFound } from 'next/navigation'; | ||
import { cache } from 'react'; | ||
import { z } from 'zod'; | ||
|
||
const postById = cache( | ||
publicAction.input(z.string()).query(async (opts) => { | ||
const post = await db.query.Post.findFirst({ | ||
where: eq(Post.id, opts.input), | ||
}); | ||
|
||
if (!post) notFound(); | ||
return post; | ||
}), | ||
); | ||
|
||
export default async function Page(props: { | ||
params: { | ||
postId: string; | ||
}; | ||
}) { | ||
const post = await postById(props.params.postId); | ||
|
||
return ( | ||
<div className="p-16"> | ||
<Link href="/" className="rounded bg-zinc-800 p-1">{`< Home`}</Link> | ||
<h1>{post.title}</h1> | ||
|
||
<p>{post.content}</p> | ||
</div> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
'use server'; | ||
|
||
import { randomUUID } from 'node:crypto'; | ||
import { db } from '@/db/client'; | ||
import { addPostSchema, Post } from '@/db/schema'; | ||
import { protectedAction, redirect } from '@/server/trpc'; | ||
import { eq } from 'drizzle-orm'; | ||
import { revalidatePath } from 'next/cache'; | ||
|
||
export const addPost = protectedAction | ||
.input( | ||
addPostSchema.superRefine(async (it, ctx) => { | ||
const posts = await db.query.Post.findFirst({ | ||
where: eq(Post.title, it.title), | ||
}); | ||
if (posts) { | ||
ctx.addIssue({ | ||
code: 'custom', | ||
message: 'Title already exists', | ||
path: ['title'], | ||
}); | ||
} | ||
}), | ||
) | ||
.mutation(async (opts) => { | ||
const id = randomUUID().replace(/-/g, ''); | ||
console.log('run'); | ||
const res = await db.insert(Post).values({ | ||
...opts.input, | ||
id, | ||
authorId: opts.ctx.user.id, | ||
}); | ||
console.log('res', res); | ||
revalidatePath('/'); | ||
return redirect(`/${id}`); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export { GET, POST } from '@/server/auth'; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
@tailwind base; | ||
@tailwind components; | ||
@tailwind utilities; | ||
|
||
:root { | ||
--foreground-rgb: 0, 0, 0; | ||
--background-start-rgb: 214, 219, 220; | ||
--background-end-rgb: 255, 255, 255; | ||
} | ||
|
||
@media (prefers-color-scheme: dark) { | ||
:root { | ||
--foreground-rgb: 255, 255, 255; | ||
--background-start-rgb: 0, 0, 0; | ||
--background-end-rgb: 0, 0, 0; | ||
} | ||
} | ||
|
||
body { | ||
color: rgb(var(--foreground-rgb)); | ||
background: linear-gradient( | ||
to bottom, | ||
transparent, | ||
rgb(var(--background-end-rgb)) | ||
) | ||
rgb(var(--background-start-rgb)); | ||
} | ||
|
||
@layer utilities { | ||
.text-balance { | ||
text-wrap: balance; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
import type { Metadata } from 'next'; | ||
import { Inter } from 'next/font/google'; | ||
import './globals.css'; | ||
import { Toaster } from 'sonner'; | ||
|
||
const inter = Inter({ subsets: ['latin'] }); | ||
|
||
export const metadata: Metadata = { | ||
title: 'Create Next App', | ||
description: 'Generated by create next app', | ||
}; | ||
|
||
export default function RootLayout({ | ||
children, | ||
}: Readonly<{ | ||
children: React.ReactNode; | ||
}>) { | ||
return ( | ||
<html lang="en"> | ||
<body className={inter.className}>{children}</body> | ||
<Toaster /> | ||
</html> | ||
); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't store stuff like this in git
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Haha just wanted the example to start right up 😅😅 not like that auth token does anythign secret
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, but if people clone the project they will get the a bad setup,
.env
should be in.gitignore
:)