├── .devcontainer ├── Dockerfile └── devcontainer.json ├── .env ├── .eslintrc.json ├── .gitignore ├── .vscode └── settings.json ├── README.md ├── app ├── Header.tsx ├── api │ ├── notes │ │ ├── [id] │ │ │ └── route.ts │ │ └── route.ts │ └── settings │ │ └── route.tsx ├── favicon.ico ├── globals.css ├── help │ ├── faq │ │ └── page.tsx │ ├── layout.tsx │ └── tos │ │ └── page.tsx ├── layout.tsx ├── notes │ ├── NoteList.tsx │ ├── [id] │ │ ├── Note.tsx │ │ ├── edit │ │ │ ├── EditNote.tsx │ │ │ └── page.tsx │ │ ├── getNote.ts │ │ └── page.tsx │ ├── new │ │ ├── NewNote.tsx │ │ └── page.tsx │ ├── page.tsx │ └── type.ts ├── page.tsx ├── settings │ ├── EditSettings.tsx │ └── page.tsx └── type.ts ├── components ├── ErrorBoundary.tsx ├── FetchError.tsx ├── Loading.tsx └── Nl2br.tsx ├── constants └── api.ts ├── globals └── db.ts ├── next.config.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── prisma ├── .gitignore ├── migrations │ ├── 20230301183727_init │ │ └── migration.sql │ └── migration_lock.toml ├── schema.prisma └── seed.ts ├── public ├── cover.jpeg └── next.svg ├── tailwind.config.js └── tsconfig.json /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18 2 | 3 | # Install basic development tools 4 | RUN apt update && apt install -y less man-db sudo 5 | 6 | # Ensure default `node` user has access to `sudo` 7 | ARG USERNAME=node 8 | RUN echo $USERNAME ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/$USERNAME \ 9 | && chmod 0440 /etc/sudoers.d/$USERNAME 10 | 11 | # Set `DEVCONTAINER` environment variable to help with orientation 12 | ENV DEVCONTAINER=true 13 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // See https://containers.dev/implementors/json_reference/ for configuration reference 2 | { 3 | "name": "Next.js App Directory Sample", 4 | "build": { 5 | "dockerfile": "Dockerfile" 6 | }, 7 | "remoteUser": "node", 8 | "customizations": { 9 | "vscode": { 10 | "extensions": [ 11 | "dbaeumer.vscode-eslint", 12 | "bradlc.vscode-tailwindcss", 13 | "csstools.postcss", 14 | "Prisma.prisma" 15 | ] 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | # Environment variables declared in this file are automatically made available to Prisma. 2 | # See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema 3 | 4 | # Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB. 5 | # See the documentation for all the connection string options: https://pris.ly/d/connection-strings 6 | 7 | DATABASE_URL="file:./dev.db" -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 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 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib", 3 | "typescript.enablePromptUseWorkspaceTsdk": true, 4 | "editor.tabSize": 2, 5 | "editor.insertSpaces": true, 6 | "editor.formatOnSave": true, 7 | "editor.codeActionsOnSave": { 8 | "source.organizeImports": true 9 | }, 10 | "files.insertFinalNewline": true 11 | } 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Demo 2 | 3 | https://user-images.githubusercontent.com/36380433/225842591-da2d58ee-a36f-46e4-9da5-747de513a577.mov 4 | 5 | --- 6 | 7 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 8 | 9 | ## Getting Started 10 | 11 | First, run the development server: 12 | 13 | ```bash 14 | npm run dev 15 | # or 16 | yarn dev 17 | # or 18 | pnpm dev 19 | ``` 20 | 21 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 22 | 23 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 24 | 25 | [API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`. 26 | 27 | The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. 28 | 29 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 30 | 31 | ## Learn More 32 | 33 | To learn more about Next.js, take a look at the following resources: 34 | 35 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 36 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 37 | 38 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 39 | 40 | ## Deploy on Vercel 41 | 42 | 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. 43 | 44 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 45 | -------------------------------------------------------------------------------- /app/Header.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import { Suspense } from "react"; 3 | import "server-only"; 4 | import { prisma } from "../globals/db"; 5 | import { zVersion } from "./type"; 6 | 7 | const Header: React.FC = () => { 8 | const title = 'Awesome Note App' 9 | return ( 10 |
11 |
12 |
13 | 14 | 15 | {title} 16 | 17 | 18 | 23 | 24 |
25 | 26 | 27 | {/* @ts-expect-error Server Component */} 28 | 29 | 30 | 31 |
32 | 33 |
34 |
35 |
36 | ) 37 | }; 38 | 39 | const Version = async () => { 40 | // versionをDBから取得 41 | const metadata = await prisma.metadata.findUniqueOrThrow({ 42 | where: { 43 | key: "version" 44 | } 45 | }); 46 | const version = zVersion.parse(metadata.value); 47 | return `v${version}`; 48 | }; 49 | 50 | export default Header; 51 | -------------------------------------------------------------------------------- /app/api/notes/[id]/route.ts: -------------------------------------------------------------------------------- 1 | import { zUpsertNote } from "@/app/notes/type"; 2 | import { prisma } from "@/globals/db"; 3 | import { NextRequest, NextResponse } from "next/server"; 4 | 5 | export async function GET(_req: NextRequest, { params }: { params: { id: string } }) { 6 | const note = await prisma.note.findUnique({ 7 | where: { id: Number(params.id) }, 8 | }); 9 | if (note === null) { 10 | return new NextResponse(null, { status: 404 }) 11 | } 12 | return NextResponse.json(note) 13 | } 14 | 15 | export async function PUT(req: NextRequest, { params }: { params: { id: string } }) { 16 | const data = await req.json(); 17 | const parcedData = zUpsertNote.parse(data); 18 | const note = await prisma.note.update({ 19 | where: { id: Number(params.id) }, 20 | data: { title: parcedData.title, body: parcedData.body, updatedAt: new Date() }, 21 | }); 22 | return new NextResponse(null, { status: 204 }) 23 | } 24 | 25 | export async function DELETE(req: NextRequest, { params }: { params: { id: string } }) { 26 | const note = await prisma.note.delete({ 27 | where: { id: Number(params.id) }, 28 | }); 29 | return new NextResponse(null, { status: 204 }) 30 | } 31 | 32 | -------------------------------------------------------------------------------- /app/api/notes/route.ts: -------------------------------------------------------------------------------- 1 | import { zUpsertNote } from "@/app/notes/type"; 2 | import { prisma } from "@/globals/db"; 3 | import { NextRequest, NextResponse } from "next/server"; 4 | 5 | export const dynamic = 'force-dynamic'; 6 | 7 | export async function GET() { 8 | const notes = await prisma.note.findMany(); 9 | return NextResponse.json(notes) 10 | } 11 | 12 | export async function POST(req: NextRequest) { 13 | const data = await req.json(); 14 | const parcedData = zUpsertNote.parse(data); 15 | const note = await prisma.note.create({ 16 | data: { title: parcedData.title, body: parcedData.body }, 17 | }); 18 | return new NextResponse(`${note.id}`, { status: 201 }) 19 | } 20 | -------------------------------------------------------------------------------- /app/api/settings/route.tsx: -------------------------------------------------------------------------------- 1 | import { zSettings } from "@/app/type"; 2 | import { prisma } from "@/globals/db"; 3 | import { NextRequest, NextResponse } from "next/server"; 4 | 5 | export async function PUT(req: NextRequest) { 6 | const data = await req.json(); 7 | const parcedData = zSettings.parse(data); 8 | await prisma.$transaction([ 9 | prisma.metadata.update({ 10 | where: { key: "version" }, 11 | data: { value: parcedData.version }, 12 | }), 13 | prisma.metadata.update({ 14 | where: { key: "faq" }, 15 | data: { value: parcedData.faq }, 16 | }), 17 | prisma.metadata.update({ 18 | where: { key: "tos" }, 19 | data: { value: parcedData.tos }, 20 | }), 21 | ]); 22 | return new NextResponse(null, { status: 204 }) 23 | } 24 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tim0401/nextjs-app-directory-demo/0d0572a7042fc5125fc92da6a6dd6b9dbdbe9c9e/app/favicon.ico -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /app/help/faq/page.tsx: -------------------------------------------------------------------------------- 1 | import Nl2br from "@/components/Nl2br"; 2 | import { prisma } from "@/globals/db"; 3 | 4 | export const revalidate = 30; 5 | 6 | export default async function Page() { 7 | const data = await prisma.metadata.findUniqueOrThrow({ 8 | where: { key: "faq" }, 9 | }); 10 | return ( 11 |
12 |

Frequently Asked Questions

13 |

The following text is a sample.

14 | {data.value} 15 |
16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /app/help/layout.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import Link from "next/link"; 3 | import { usePathname } from "next/navigation"; 4 | 5 | export default function Layout({ 6 | children, 7 | }: { 8 | children: React.ReactNode, 9 | }) { 10 | const pathname = usePathname(); 11 | return ( 12 |
13 | {/* Include shared UI here e.g. a header or sidebar */} 14 | 18 | {children} 19 |
20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /app/help/tos/page.tsx: -------------------------------------------------------------------------------- 1 | import Nl2br from "@/components/Nl2br"; 2 | import { prisma } from "@/globals/db"; 3 | 4 | export const revalidate = 0; 5 | 6 | export default async function Page() { 7 | const data = await prisma.metadata.findUniqueOrThrow({ 8 | where: { key: "tos" }, 9 | }); 10 | return ( 11 |
12 |

Terms of Service

13 |

The following text is a sample.

14 | {data.value} 15 |
16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Noto_Sans_JP } from 'next/font/google'; 2 | import './globals.css'; 3 | import Header from './Header'; 4 | 5 | const NotoSansJP = Noto_Sans_JP({ 6 | weight: ["400", "700"], 7 | subsets: ["latin"], 8 | preload: true, 9 | }); 10 | 11 | export const metadata = { 12 | title: 'Next.js Awesome Note App', 13 | description: 'Generated by create next app', 14 | } 15 | 16 | export default function RootLayout({ 17 | children, 18 | }: { 19 | children: React.ReactNode 20 | }) { 21 | return ( 22 | 23 | 24 |
25 | {children} 26 | 27 | 28 | ) 29 | } 30 | -------------------------------------------------------------------------------- /app/notes/NoteList.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import Link from "next/link"; 3 | import useSWR from "swr"; 4 | import { Note, zNotes } from "./type"; 5 | 6 | type Props = { 7 | initialState: Note[]; 8 | } 9 | 10 | const fetcher = (url: string) => fetch(url).then(async (res) => { 11 | const data = await res.json(); 12 | const notes = zNotes.parse(data); 13 | return notes; 14 | }); 15 | 16 | const NoteList: React.FC = ({ initialState }) => { 17 | const { data } = useSWR('/api/notes', fetcher, { suspense: true, fallbackData: initialState }) 18 | return ( 19 |
20 | {data.map(note => )} 21 |
22 | ) 23 | } 24 | 25 | type NoteProps = { 26 | item: Note; 27 | } 28 | 29 | const NoteItem: React.FC = ({ item }) => { 30 | return ( 31 |
32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 |

{item.title}

41 | 42 |

{item.body}

43 |
44 | ); 45 | }; 46 | 47 | export default NoteList; 48 | -------------------------------------------------------------------------------- /app/notes/[id]/Note.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import Link from "next/link"; 3 | import { useRouter } from "next/navigation"; 4 | import { useCallback } from "react"; 5 | import { Note } from "../type"; 6 | 7 | type Props = { 8 | item: Note; 9 | } 10 | 11 | const Note: React.FC = ({ item }) => { 12 | const router = useRouter(); 13 | const deleteNote = useCallback(async () => { 14 | const res = await fetch(`/api/notes/${item.id}`, { 15 | method: 'DELETE', 16 | headers: { 17 | 'Content-Type': 'application/json' 18 | } 19 | }); 20 | if (res.ok) { 21 | alert('Note deleted'); 22 | router.push(`/notes`); 23 | router.refresh(); 24 | } else { 25 | alert('Note failed to delete'); 26 | } 27 | }, [item.id, router]); 28 | 29 | return ( 30 |
31 |

{item.title}

32 |

{item.body}

33 | 34 |
35 | Edit 36 | 37 |
38 |
39 | ); 40 | } 41 | 42 | export default Note; 43 | -------------------------------------------------------------------------------- /app/notes/[id]/edit/EditNote.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import Link from "next/link"; 3 | import { useRouter } from "next/navigation"; 4 | import { useCallback, useState } from "react"; 5 | import { Note } from "../../type"; 6 | 7 | type Props = { 8 | item: Note; 9 | } 10 | 11 | const EditNote: React.FC = ({ item }) => { 12 | const router = useRouter(); 13 | const [title, setTitle] = useState(item.title); 14 | const [body, setBody] = useState(item.body); 15 | const updateNote = useCallback(async () => { 16 | const res = await fetch(`/api/notes/${item.id}`, { 17 | method: 'PUT', 18 | body: JSON.stringify({ title, body }), 19 | headers: { 20 | 'Content-Type': 'application/json' 21 | } 22 | }); 23 | if (res.ok) { 24 | alert('Note updated'); 25 | router.push(`/notes/${item.id}`); 26 | router.refresh(); 27 | } else { 28 | alert('Note failed to update'); 29 | } 30 | }, [body, item.id, router, title]); 31 | 32 | return ( 33 |
34 |
35 | 36 | setTitle(e.target.value)} 41 | /> 42 |
43 | 44 |
45 | 46 | 52 |
53 | 54 |
55 | Cancel 56 | 57 |
58 |
59 | ); 60 | } 61 | 62 | export default EditNote; 63 | -------------------------------------------------------------------------------- /app/notes/[id]/edit/page.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | import { Metadata } from 'next/types'; 3 | import { getNote } from '../getNote'; 4 | import EditNote from './EditNote'; 5 | export const revalidate = 0; 6 | 7 | export async function generateMetadata({ params }: { params: { id: string } }): Promise { 8 | const note = await getNote(params.id); 9 | return { title: note.title } 10 | } 11 | 12 | export default async function Page({ params }: { params: { id: string } }) { 13 | const note = await getNote(params.id); 14 | return ( 15 |
16 | ← back 17 |

Edit Note

18 | 19 |
20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /app/notes/[id]/getNote.ts: -------------------------------------------------------------------------------- 1 | import { apiUrl } from "@/constants/api"; 2 | import "server-only"; 3 | import { zNote } from "../type"; 4 | 5 | export const getNote = async (id: string) => { 6 | const res = await fetch(`${apiUrl}/notes/${id}`, { cache: 'no-store' }); 7 | const data = await res.json(); 8 | const note = zNote.parse(data); 9 | return note; 10 | }; 11 | -------------------------------------------------------------------------------- /app/notes/[id]/page.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | import { Metadata } from 'next/types'; 3 | import { getNote } from './getNote'; 4 | import Note from './Note'; 5 | 6 | export const revalidate = 0; 7 | 8 | export async function generateMetadata({ params }: { params: { id: string } }): Promise { 9 | const note = await getNote(params.id); 10 | return { title: note.title } 11 | } 12 | 13 | export default async function Page({ params }: { params: { id: string } }) { 14 | const note = await getNote(params.id); 15 | return ( 16 |
17 | ← back 18 |

View Note

19 | 20 |
21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /app/notes/new/NewNote.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import Link from "next/link"; 3 | import { useRouter } from "next/navigation"; 4 | import { useCallback, useState } from "react"; 5 | import { z } from "zod"; 6 | 7 | const NewNote: React.FC = () => { 8 | const router = useRouter(); 9 | const [title, setTitle] = useState(""); 10 | const [body, setBody] = useState(""); 11 | const createNote = useCallback(async () => { 12 | const res = await fetch(`/api/notes`, { 13 | method: 'POST', 14 | body: JSON.stringify({ title, body }), 15 | headers: { 16 | 'Content-Type': 'application/json' 17 | } 18 | }); 19 | if (res.ok) { 20 | const id = z.number().parse(await res.json()); 21 | alert('Note created'); 22 | router.push(`/notes/${id}`); 23 | router.refresh(); 24 | } else { 25 | alert('Note failed to create'); 26 | } 27 | }, [body, router, title]); 28 | 29 | return ( 30 |
31 |
32 | 33 | setTitle(e.target.value)} 38 | /> 39 |
40 | 41 |
42 | 43 | 49 |
50 | 51 |
52 | Cancel 53 | 54 |
55 |
56 | ); 57 | } 58 | 59 | export default NewNote; 60 | -------------------------------------------------------------------------------- /app/notes/new/page.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | import NewNote from './NewNote'; 3 | 4 | export const metadata = { 5 | title: "New Note", 6 | }; 7 | 8 | export default async function Page() { 9 | return ( 10 |
11 | ← back 12 |

New Note

13 | 14 |
15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /app/notes/page.tsx: -------------------------------------------------------------------------------- 1 | import ErrorBoundary from '@/components/ErrorBoundary'; 2 | import FetchError from '@/components/FetchError'; 3 | import Loading from '@/components/Loading'; 4 | import { apiUrl } from "@/constants/api"; 5 | import Link from 'next/link'; 6 | import { Suspense } from 'react'; 7 | import "server-only"; 8 | import NoteList from './NoteList'; 9 | import { zNotes } from "./type"; 10 | 11 | export const revalidate = 0; 12 | 13 | export const metadata = { 14 | title: "List Notes", 15 | } 16 | 17 | export default async function Page() { 18 | const notes = await getNotes(); 19 | return ( 20 |
21 | 22 | 23 | New Note 24 | 25 |

List Notes

26 | }> 27 | }> 28 | 29 | 30 | 31 |
32 | ) 33 | } 34 | 35 | const getNotes = async () => { 36 | const res = await fetch(`${apiUrl}/notes`, { cache: 'no-store' }); 37 | const data = await res.json(); 38 | const notes = zNotes.parse(data); 39 | return notes; 40 | }; 41 | -------------------------------------------------------------------------------- /app/notes/type.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | export const zNote = z.object({ 4 | id: z.number().int(), 5 | title: z.string(), 6 | body: z.string(), 7 | createdAt: z.string().datetime(), 8 | updatedAt: z.string().datetime(), 9 | }); 10 | export const zNotes = z.array(zNote); 11 | export const zUpsertNote = z.object({ 12 | title: z.string(), 13 | body: z.string(), 14 | }); 15 | 16 | export type Note = z.infer; 17 | export type Notes = z.infer; 18 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | import { prisma } from '@/globals/db'; 2 | import Image from 'next/image'; 3 | import Link from 'next/link'; 4 | import coverPic from '../public/cover.jpeg'; 5 | import { zVersion } from './type'; 6 | 7 | export default async function Page() { 8 | const metadata = await prisma.metadata.findUniqueOrThrow({ 9 | where: { 10 | key: "version" 11 | } 12 | }); 13 | const version = zVersion.parse(metadata.value); 14 | return ( 15 |
16 |
17 |
18 | 19 |
20 |
21 |

Introducing the App Directory

22 | 23 |

Revolutionary way to build the web

24 | 25 |

Learn about the new features of Next.js {version} through building a note application.

26 |

Front-end development will be more fun.

27 | 28 |
29 | Add new 30 | View list 31 |
32 |
33 | 34 |
35 | Photo by Fakurian Design 36 |
37 | 38 |
39 |
40 |
41 |
42 | ) 43 | } 44 | -------------------------------------------------------------------------------- /app/settings/EditSettings.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import { useRouter } from "next/navigation"; 3 | import { useCallback, useState } from "react"; 4 | import { Settings } from "../type"; 5 | type Props = { 6 | value: Settings; 7 | } 8 | const EditSettings: React.FC = ({ value }) => { 9 | const router = useRouter(); 10 | const [version, setVersion] = useState(value.version); 11 | const [faq, setFaq] = useState(value.faq); 12 | const [tos, setTos] = useState(value.tos); 13 | const updateSettings = useCallback(async () => { 14 | const res = await fetch(`/api/settings`, { 15 | method: 'PUT', 16 | body: JSON.stringify({ version: version, faq: faq, tos: tos }), 17 | headers: { 18 | 'Content-Type': 'application/json' 19 | } 20 | }); 21 | if (res.ok) { 22 | alert('Settings updated'); 23 | router.refresh(); 24 | } else { 25 | alert('Settings failed to update'); 26 | } 27 | }, [faq, router, tos, version]); 28 | 29 | return ( 30 |
31 |
32 | 33 | setVersion(e.target.value)} 38 | /> 39 |
40 | 41 |
42 | 43 | 49 |
50 | 51 |
52 | 53 | 59 |
60 | 61 |
62 | 63 |
64 |
65 | ); 66 | } 67 | 68 | export default EditSettings; 69 | -------------------------------------------------------------------------------- /app/settings/page.tsx: -------------------------------------------------------------------------------- 1 | import { prisma } from "@/globals/db"; 2 | import "server-only"; 3 | import { zSettings } from "../type"; 4 | import EditSettings from "./EditSettings"; 5 | 6 | export const revalidate = 0; 7 | 8 | export const metadata = { 9 | title: 'Settings', 10 | } 11 | 12 | export default async function Page() { 13 | const settings = await getSettings(); 14 | return ( 15 |
16 |

Settings

17 | 18 |
19 | ) 20 | } 21 | 22 | const getSettings = async () => { 23 | const settings = await prisma.metadata.findMany(); 24 | const data = settings.reduce>((acc, cur) => { 25 | acc[cur.key] = cur.value; 26 | return acc; 27 | }, {}); 28 | const parsedData = zSettings.parse(data); 29 | return parsedData; 30 | } 31 | -------------------------------------------------------------------------------- /app/type.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | export const zVersion = z.string().regex(/^\d+\.\d+\.\d+$/); 4 | export const zSettings = z.object({ 5 | version: zVersion, 6 | faq: z.string(), 7 | tos: z.string(), 8 | }); 9 | export type Settings = z.infer; 10 | -------------------------------------------------------------------------------- /components/ErrorBoundary.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import React, { ReactNode } from "react"; 3 | 4 | type Props = { fallback: ReactNode, children: ReactNode }; 5 | 6 | class ErrorBoundary extends React.Component { 7 | constructor(props: Props) { 8 | super(props); 9 | this.state = { 10 | hasError: false, 11 | }; 12 | } 13 | static getDerivedStateFromError(): { hasError: boolean } { 14 | return { hasError: true }; 15 | } 16 | 17 | render() { 18 | if (this.state.hasError) { 19 | return <>{this.props.fallback}; 20 | } 21 | return <>{this.props.children}; 22 | } 23 | } 24 | 25 | export default ErrorBoundary; 26 | -------------------------------------------------------------------------------- /components/FetchError.tsx: -------------------------------------------------------------------------------- 1 | const FetchError: React.FC = () => { 2 | return ( 3 |
4 |
5 |

Error while fetching data.

6 | Something seriously bad happened. 7 |
8 |
9 | ) 10 | } 11 | export default FetchError; 12 | 13 | -------------------------------------------------------------------------------- /components/Loading.tsx: -------------------------------------------------------------------------------- 1 | const Loading: React.FC = () => { 2 | return ( 3 |
4 |
5 |
6 |
7 |
8 | ) 9 | } 10 | 11 | export default Loading; 12 | -------------------------------------------------------------------------------- /components/Nl2br.tsx: -------------------------------------------------------------------------------- 1 | const Nl2br = ({ children }: { children: string }) => ( 2 | <> 3 | {children.split(/(\n)/g).map((t, index) => (t === '\n' ?
: t))} 4 | 5 | ) 6 | 7 | export default Nl2br; 8 | -------------------------------------------------------------------------------- /constants/api.ts: -------------------------------------------------------------------------------- 1 | import "server-only"; 2 | export const apiUrl = "http://127.0.0.1:3000/api"; 3 | -------------------------------------------------------------------------------- /globals/db.ts: -------------------------------------------------------------------------------- 1 | // https://www.prisma.io/docs/guides/database/troubleshooting-orm/help-articles/nextjs-prisma-client-dev-practices 2 | import { PrismaClient } from '@prisma/client'; 3 | import "server-only"; 4 | 5 | const globalForPrisma = global as unknown as { prisma: PrismaClient } 6 | 7 | export const prisma = 8 | globalForPrisma.prisma || 9 | new PrismaClient({ 10 | log: ['query'], 11 | }) 12 | 13 | if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma 14 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | experimental: { 4 | appDir: true, 5 | typedRoutes: true, 6 | }, 7 | } 8 | 9 | module.exports = nextConfig 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nextjs-app-directory-demo", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "prisma": { 12 | "seed": "ts-node --compiler-options {\"module\":\"CommonJS\"} prisma/seed.ts" 13 | }, 14 | "dependencies": { 15 | "@prisma/client": "^4.11.0", 16 | "@types/node": "18.14.2", 17 | "@types/react": "18.0.28", 18 | "@types/react-dom": "18.0.11", 19 | "eslint": "8.35.0", 20 | "eslint-config-next": "13.2.1", 21 | "next": "13.2.1", 22 | "react": "18.2.0", 23 | "react-dom": "18.2.0", 24 | "server-only": "^0.0.1", 25 | "sharp": "^0.31.3", 26 | "swr": "^2.0.4", 27 | "typescript": "4.9.5", 28 | "zod": "^3.20.6" 29 | }, 30 | "devDependencies": { 31 | "autoprefixer": "^10.4.13", 32 | "postcss": "^8.4.21", 33 | "prisma": "^4.11.0", 34 | "tailwindcss": "^3.2.7", 35 | "ts-node": "^10.9.1" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /prisma/.gitignore: -------------------------------------------------------------------------------- 1 | dev.db* 2 | -------------------------------------------------------------------------------- /prisma/migrations/20230301183727_init/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "metadata" ( 3 | "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, 4 | "key" TEXT NOT NULL, 5 | "value" TEXT NOT NULL 6 | ); 7 | 8 | -- CreateTable 9 | CREATE TABLE "notes" ( 10 | "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, 11 | "title" TEXT NOT NULL, 12 | "body" TEXT NOT NULL, 13 | "created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, 14 | "updated_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP 15 | ); 16 | 17 | -- CreateIndex 18 | CREATE UNIQUE INDEX "metadata_key_key" ON "metadata"("key"); 19 | -------------------------------------------------------------------------------- /prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "sqlite" -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | generator client { 5 | provider = "prisma-client-js" 6 | } 7 | 8 | datasource db { 9 | provider = "sqlite" 10 | url = env("DATABASE_URL") 11 | } 12 | 13 | model Metadata { 14 | id Int @id @default(autoincrement()) 15 | key String @unique 16 | value String 17 | 18 | @@map("metadata") 19 | } 20 | 21 | model Note { 22 | id Int @id @default(autoincrement()) 23 | title String 24 | body String 25 | createdAt DateTime @default(now()) @map("created_at") 26 | updatedAt DateTime @default(now()) @map("updated_at") 27 | 28 | @@map("notes") 29 | } 30 | -------------------------------------------------------------------------------- /prisma/seed.ts: -------------------------------------------------------------------------------- 1 | import { Prisma, PrismaClient } from '@prisma/client'; 2 | 3 | const prisma = new PrismaClient() 4 | 5 | async function main() { 6 | // delete all 7 | await prisma.metadata.deleteMany(); 8 | await prisma.note.deleteMany(); 9 | // seeding 10 | const metadatas: Prisma.MetadataCreateInput[] = [ 11 | { 12 | key: "version", 13 | value: "13.2.1", 14 | }, 15 | { 16 | key: "faq", 17 | value: faq, 18 | }, 19 | { 20 | key: "tos", 21 | value: tos, 22 | }, 23 | ]; 24 | for (const metadata of metadatas) { 25 | await prisma.metadata.create({ 26 | data: metadata 27 | }); 28 | } 29 | 30 | const notes: Prisma.NoteCreateInput[] = [ 31 | { 32 | title: "First note", 33 | body: "This is the first note.", 34 | }, 35 | { 36 | title: "Second note", 37 | body: "This is the second note.", 38 | }, 39 | { 40 | title: "Third note", 41 | body: "This is the third note.", 42 | }, 43 | { 44 | title: "Fourth note", 45 | body: "This is the fourth note.", 46 | }, 47 | ]; 48 | for (const note of notes) { 49 | await prisma.note.create({ 50 | data: note 51 | }) 52 | } 53 | } 54 | 55 | main() 56 | .then(async () => { 57 | await prisma.$disconnect() 58 | }) 59 | .catch(async (e) => { 60 | console.error(e) 61 | await prisma.$disconnect() 62 | process.exit(1) 63 | }); 64 | 65 | const faq = ` 66 | Q: How do I create a new note? 67 | A: To create a new note, click the "New Note" button located in the top left corner of the screen. This will open a blank note where you can begin typing. 68 | 69 | Q: Can I customize the appearance of my notes? 70 | A: Yes, you can customize the appearance of your notes by changing the font, font size, and background color. Simply click the "Settings" button and select "Appearance" to make these changes. 71 | 72 | Q: Can I share my notes with others? 73 | A: Yes, you can share your notes with others by clicking the "Share" button located at the bottom of the note. You can then enter the email address of the person you wish to share the note with. 74 | 75 | Q: How do I delete a note? 76 | A: To delete a note, click on the note you wish to delete and then click the "Delete" button located at the bottom of the note. 77 | 78 | Q: Is my data secure? 79 | A: Yes, we take the security and privacy of your data very seriously. All notes are stored on secure servers and are encrypted for added protection. 80 | 81 | Q: Can I access my notes on multiple devices? 82 | A: Yes, you can access your notes on multiple devices by logging into your account on our website. All notes will be synced across all devices. 83 | 84 | Q: What happens if I forget my password? 85 | A: If you forget your password, you can reset it by clicking the "Forgot Password" link located on the login page. You will then be prompted to enter your email address to receive instructions on how to reset your password. 86 | 87 | Q: Do you offer a mobile app? 88 | A: Yes, we offer a mobile app for both iOS and Android devices. You can download the app from the App Store or Google Play. 89 | ` 90 | const tos = ` 91 | Welcome to our website. These Terms of Service ("TOS") govern your use of our website, including any content, functionality, and services offered on or through the website. By using our website, you accept and agree to be bound by these TOS. If you do not agree with these TOS, you may not use our website. 92 | 93 | User Conduct 94 | You agree to use our website only for lawful purposes and in a manner that does not violate the rights of any third party. You agree not to use our website in any way that could damage, disable, overburden, or impair our servers or networks. You also agree not to access or attempt to access any information or data on our website that you are not authorized to access. 95 | 96 | Intellectual Property 97 | All content on our website, including text, graphics, logos, images, and software, is owned by us or our licensors and is protected by copyright and other intellectual property laws. You may not copy, distribute, modify, or create derivative works of any content on our website without our prior written consent. 98 | 99 | Disclaimer of Warranties 100 | Our website is provided "as is" and without warranties of any kind, either express or implied. We do not warrant that our website will be uninterrupted or error-free, that defects will be corrected, or that our website or the servers that make it available are free of viruses or other harmful components. 101 | 102 | Limitation of Liability 103 | In no event shall we be liable for any direct, indirect, incidental, consequential, special, or exemplary damages arising from or in connection with your use of our website, even if we have been advised of the possibility of such damages. Our liability to you for any cause whatsoever, and regardless of the form of the action, will at all times be limited to the amount paid by you, if any, to access our website. 104 | 105 | Indemnification 106 | You agree to indemnify, defend, and hold us harmless from any claim, demand, or damage, including reasonable attorneys' fees, arising out of your use of our website, your violation of these TOS, or your violation of any rights of another. 107 | 108 | Governing Law and Jurisdiction 109 | These TOS and any disputes arising out of or related to your use of our website will be governed by and construed in accordance with the laws of [insert jurisdiction], without giving effect to any principles of conflicts of law. Any legal action or proceeding arising out of or related to these TOS or your use of our website shall be brought exclusively in [insert court of jurisdiction], and you consent to the jurisdiction of such courts. 110 | 111 | Modifications to these TOS 112 | We reserve the right to modify these TOS at any time without notice. Your continued use of our website following any such modification constitutes your agreement to be bound by the modified TOS. 113 | `; 114 | -------------------------------------------------------------------------------- /public/cover.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tim0401/nextjs-app-directory-demo/0d0572a7042fc5125fc92da6a6dd6b9dbdbe9c9e/public/cover.jpeg -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | "./app/**/*.{js,ts,jsx,tsx}", 5 | "./components/**/*.{js,ts,jsx,tsx}", 6 | ], 7 | theme: { 8 | extend: {}, 9 | }, 10 | plugins: [], 11 | } 12 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "plugins": [ 18 | { 19 | "name": "next" 20 | } 21 | ], 22 | "paths": { 23 | "@/*": ["./*"] 24 | } 25 | }, 26 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 27 | "exclude": ["node_modules"] 28 | } 29 | --------------------------------------------------------------------------------