├── .env.example ├── .eslintrc.json ├── .gitignore ├── .prettierrc ├── .vscode └── settings.json ├── README.md ├── bun.lockb ├── drizzle.config.ts ├── global.d.ts ├── next.config.mjs ├── package.json ├── postcss.config.cjs ├── public ├── favicon.ico ├── nextjs-dark.svg ├── nextjs.svg └── vercel.svg ├── src ├── app │ ├── (actions) │ │ └── contacts.ts │ ├── (components) │ │ ├── delete-form.tsx │ │ ├── edit-form.tsx │ │ ├── favorite-form.tsx │ │ ├── logo.tsx │ │ ├── nav-link.tsx │ │ ├── new-contact-form.tsx │ │ ├── sidebar-form.tsx │ │ ├── sidebar-page.tsx │ │ └── sidebar.tsx │ ├── (models) │ │ └── contact.ts │ └── (routes) │ │ ├── (app) │ │ ├── @sidebar │ │ │ ├── contacts │ │ │ │ ├── [id] │ │ │ │ │ ├── edit │ │ │ │ │ │ └── page.tsx │ │ │ │ │ └── page.tsx │ │ │ │ └── new │ │ │ │ │ └── page.tsx │ │ │ ├── default.tsx │ │ │ └── page.tsx │ │ ├── contacts │ │ │ ├── [id] │ │ │ │ ├── edit │ │ │ │ │ └── page.tsx │ │ │ │ ├── layout.tsx │ │ │ │ ├── not-found.tsx │ │ │ │ └── page.tsx │ │ │ └── new │ │ │ │ ├── layout.tsx │ │ │ │ └── page.tsx │ │ ├── layout.tsx │ │ └── page.tsx │ │ ├── global-error.tsx │ │ ├── globals.css │ │ ├── layout.tsx │ │ └── not-found.tsx ├── env.mjs └── lib │ ├── constants.ts │ ├── db.ts │ ├── functions.ts │ ├── schema │ └── contact.sql.ts │ ├── server-utils.ts │ ├── types.ts │ └── validators.ts ├── tailwind.config.cjs └── tsconfig.json /.env.example: -------------------------------------------------------------------------------- 1 | TURSO_DB_TOKEN="" 2 | TURSO_DB_URL="libsql://-.turso.io" -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next/core-web-vitals", "next/babel"] 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | .env 31 | 32 | # vercel 33 | .vercel 34 | 35 | # typescript 36 | *.tsbuildinfo 37 | next-env.d.ts 38 | 39 | .vscode 40 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fredkiss3/nextjs-13-react-router-contacts/c8a7ec71a7e2b3ec2162fce7f792f632ea43939c/.prettierrc -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Next 13 contacts 2 | 3 | This is a [Next.js](https://nextjs.org/) project bootstrapped with `create-next-app`. 4 | 5 | ## Pre-requisities 6 | 7 | - A [turso](https://turso.tech/) database 8 | - [Bun](https://bun.sh/) installed on your project 9 | - Node >= v18 10 | 11 | ## Getting Started 12 | 13 | 1. Install the packages : 14 | 15 | ```bash 16 | bun install 17 | ``` 18 | 19 | 1. Run the nextjs development server: 20 | 21 | ```bash 22 | bun dev 23 | ``` 24 | 25 | Open with your browser to see the result. 26 | 27 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 28 | 29 | ## Learn More 30 | 31 | To learn more about Next.js, take a look at the following resources: 32 | 33 | - [Next.js Documentation](https://beta.nextjs.org/docs) - learn about Next.js features and API. 34 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 35 | 36 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 37 | 38 | ## Deploy on Vercel 39 | 40 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 41 | 42 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fredkiss3/nextjs-13-react-router-contacts/c8a7ec71a7e2b3ec2162fce7f792f632ea43939c/bun.lockb -------------------------------------------------------------------------------- /drizzle.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "drizzle-kit"; 2 | 3 | export default { 4 | schema: "./src/lib/schema/*.sql.ts", 5 | out: "./drizzle", 6 | driver: "turso", 7 | dbCredentials: { 8 | url: process.env.TURSO_DB_URL!, 9 | authToken: process.env.TURSO_DB_TOKEN!, 10 | }, 11 | } satisfies Config; 12 | -------------------------------------------------------------------------------- /global.d.ts: -------------------------------------------------------------------------------- 1 | import * as ReactDOM from "react-dom"; 2 | // globals.d.ts 3 | 4 | declare module "react-dom" { 5 | function experimental_useFormState( 6 | action: (state: S, payload: P) => Promise, 7 | initialState?: S, 8 | url?: string 9 | ): initialState extends undefined 10 | ? [S | undefined, (payload: P) => Promise] 11 | : [S, (payload: P) => Promise]; 12 | } 13 | -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | import { env } from "./src/env.mjs"; 2 | /** @type {import('next').NextConfig} */ 3 | const nextConfig = { 4 | reactStrictMode: true, 5 | experimental: { 6 | serverActions: true, 7 | logging: { 8 | level: "verbose", 9 | // @ts-expect-error this is normally a boolean 10 | fullUrl: true, 11 | }, 12 | }, 13 | images: { 14 | remotePatterns: [ 15 | { 16 | protocol: "https", 17 | hostname: "avatars.githubusercontent.com", 18 | }, 19 | ], 20 | }, 21 | }; 22 | 23 | export default nextConfig; 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@next13-test/webapp", 3 | "version": "0.1.0", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "dev": "next dev", 8 | "clean-dev": "rm -rf .next && next dev", 9 | "build": "next build", 10 | "start": "next start", 11 | "lint": "next lint", 12 | "info": "next info", 13 | "db:push": "set -a; source .env; set +a && drizzle-kit push:sqlite", 14 | "db:studio": "set -a; source .env; set +a && drizzle-kit studio" 15 | }, 16 | "dependencies": { 17 | "@libsql/client": "0.3.5", 18 | "@t3-oss/env-nextjs": "0.6.1", 19 | "drizzle-orm": "0.28.6", 20 | "next": "13.5.4-canary.2", 21 | "react": "18.2.0", 22 | "react-dom": "18.2.0", 23 | "react-markdown": "^9.0.0", 24 | "remark-gfm": "^3.0.1", 25 | "remarkable": "^2.0.1", 26 | "server-only": "^0.0.1", 27 | "zod": "^3.22.2" 28 | }, 29 | "devDependencies": { 30 | "@types/node": "20.3.3", 31 | "@types/react": "18.2.14", 32 | "@types/react-dom": "18.2.6", 33 | "@types/remarkable": "^2.0.3", 34 | "autoprefixer": "^10.4.16", 35 | "drizzle-kit": "^0.19.3", 36 | "eslint": "8.26.0", 37 | "eslint-config-next": "13.0.0", 38 | "postcss": "^8.4.30", 39 | "tailwindcss": "^3.3.3", 40 | "typescript": "5.1.6" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fredkiss3/nextjs-13-react-router-contacts/c8a7ec71a7e2b3ec2162fce7f792f632ea43939c/public/favicon.ico -------------------------------------------------------------------------------- /public/nextjs-dark.svg: -------------------------------------------------------------------------------- 1 | 3 | 5 | 8 | 11 | 13 | 16 | 19 | 22 | 25 | -------------------------------------------------------------------------------- /public/nextjs.svg: -------------------------------------------------------------------------------- 1 | 2 | Next.js Logo 3 | 4 | 7 | 10 | 11 | 12 | 14 | 17 | 20 | 23 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /src/app/(actions)/contacts.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { revalidatePath } from "next/cache"; 4 | import { redirect } from "next/navigation"; 5 | import { ssrRedirect } from "~/lib/server-utils"; 6 | import { 7 | createContact, 8 | deleteContact, 9 | getContactByGithubHandle, 10 | toggleFavoriteContact, 11 | updateContact, 12 | } from "~/app/(models)/contact"; 13 | import { contactSchema } from "~/lib/validators"; 14 | 15 | export type ActionResult = 16 | | { 17 | type: "success"; 18 | data: T; 19 | message: string; 20 | } 21 | | { 22 | type: "error"; 23 | errors: Record; 24 | } 25 | | { type?: undefined; message: null }; 26 | 27 | export async function removeContact(formData: FormData) { 28 | const id = formData.get("id")!.toString(); 29 | await deleteContact(Number(id)); 30 | 31 | revalidatePath("/"); 32 | ssrRedirect("/"); 33 | } 34 | 35 | export async function newContact(_: ActionResult, formData: FormData) { 36 | const result = contactSchema.safeParse(Object.fromEntries(formData)); 37 | if (!result.success) { 38 | return { 39 | type: "error", 40 | errors: result.error.flatten().fieldErrors, 41 | } satisfies ActionResult; 42 | } 43 | 44 | const existingContacts = await getContactByGithubHandle( 45 | result.data.github_handle 46 | ); 47 | if (existingContacts.length > 0) { 48 | return { 49 | type: "error", 50 | errors: { 51 | github_handle: ["A user with this handle already exists in DB"], 52 | }, 53 | } satisfies ActionResult; 54 | } 55 | 56 | const id = await createContact(result.data); 57 | revalidatePath("/"); 58 | redirect(`/contacts/${id}`); 59 | } 60 | 61 | export async function editContact(_: any, formData: FormData) { 62 | const id = formData.get("id")!.toString(); 63 | const result = contactSchema.safeParse(Object.fromEntries(formData)); 64 | 65 | if (!result.success) { 66 | redirect("/"); 67 | } 68 | 69 | await updateContact(result.data, Number(id)); 70 | 71 | revalidatePath("/"); 72 | 73 | ssrRedirect(`/contacts/${id}`); 74 | } 75 | 76 | export async function favoriteContact(formData: FormData) { 77 | const id = formData.get("id")!.toString(); 78 | 79 | await toggleFavoriteContact(Number(id)); 80 | 81 | revalidatePath("/"); 82 | 83 | ssrRedirect(`/contacts/${id}`); 84 | } 85 | -------------------------------------------------------------------------------- /src/app/(components)/delete-form.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import { useRouter } from "next/navigation"; 5 | import { removeContact } from "~/app/(actions)/contacts"; 6 | 7 | export function DeleteForm({ contactId }: { contactId: number }) { 8 | const router = useRouter(); 9 | const [isPending, startTransition] = React.useTransition(); 10 | 11 | return ( 12 |
{ 15 | e.preventDefault(); 16 | if (!confirm("Please confirm you want to delete this record.")) { 17 | return; 18 | } 19 | 20 | startTransition(() => 21 | removeContact(new FormData(e.currentTarget)).then(() => { 22 | // FIXME: until this issue is fixed : https://github.com/vercel/next.js/issues/52075 23 | router.replace("/"); 24 | router.refresh(); 25 | }) 26 | ); 27 | }} 28 | > 29 | 30 | 33 |
34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /src/app/(components)/edit-form.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import * as React from "react"; 3 | // components 4 | import Link from "next/link"; 5 | 6 | // utils 7 | import { experimental_useFormState as useFormState } from "react-dom"; 8 | import { experimental_useFormStatus as useFormStatus } from "react-dom"; 9 | import { newContact } from "~/app/(actions)/contacts"; 10 | 11 | // types 12 | import type { Contact } from "~/lib/schema/contact.sql"; 13 | 14 | type EditFormProps = { 15 | contact?: Contact; 16 | }; 17 | 18 | export function EditForm({ contact }: EditFormProps) { 19 | const [state, formAction] = useFormState(newContact, { 20 | message: null, 21 | }); 22 | 23 | return ( 24 | <> 25 |
26 |

27 | Name 28 | 35 | 42 |

43 | 71 | 100 |