Skip to content
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

Open
wants to merge 3 commits into
base: next
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions examples/.experimental/next-app-dir-new/.env
@@ -0,0 +1 @@
AUTH_SECRET=FlIbvvIt9KQXVBcg0wKpcrHMwGA5RNxpxn826LdbBF8=
Copy link
Member

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

Copy link
Member Author

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

Copy link
Member

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 :)

36 changes: 36 additions & 0 deletions examples/.experimental/next-app-dir-new/.gitignore
@@ -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
Copy link
Member

Choose a reason for hiding this comment

The 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?

Binary file not shown.
4 changes: 4 additions & 0 deletions examples/.experimental/next-app-dir-new/next.config.js
@@ -0,0 +1,4 @@
/** @type {import('next').NextConfig} */
const nextConfig = {};

export default nextConfig;
37 changes: 37 additions & 0 deletions examples/.experimental/next-app-dir-new/package.json
@@ -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"
}
}
3 changes: 3 additions & 0 deletions examples/.experimental/next-app-dir-new/postcss.config.cjs
@@ -0,0 +1,3 @@
module.exports = {
plugins: { tailwindcss: {} },
};
64 changes: 64 additions & 0 deletions examples/.experimental/next-app-dir-new/src/app/AddPostForm.tsx
@@ -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, {
Copy link
Member

Choose a reason for hiding this comment

The 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>
);
}
52 changes: 52 additions & 0 deletions examples/.experimental/next-app-dir-new/src/app/Form.tsx
@@ -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>
);
};
@@ -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>
);
}
36 changes: 36 additions & 0 deletions examples/.experimental/next-app-dir-new/src/app/_action.ts
@@ -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}`);
});
@@ -0,0 +1 @@
export { GET, POST } from '@/server/auth';
33 changes: 33 additions & 0 deletions examples/.experimental/next-app-dir-new/src/app/globals.css
@@ -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;
}
}
24 changes: 24 additions & 0 deletions examples/.experimental/next-app-dir-new/src/app/layout.tsx
@@ -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>
);
}