├── .gitignore ├── .prettierrc ├── .vscode └── settings.json ├── README.md ├── components.json ├── docs ├── connect-1.png ├── connection-string.png └── drivers.png ├── next-env.d.ts ├── next.config.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── prisma └── schema.prisma ├── public ├── favicon.ico └── vercel.svg ├── scripts ├── get-deps.js └── update-deps.sh ├── src ├── app │ ├── api │ │ ├── auth │ │ │ └── [...nextauth] │ │ │ │ └── route.ts │ │ └── preferences │ │ │ └── route.ts │ ├── error.tsx │ ├── globals.css │ ├── layout.tsx │ ├── not-found.tsx │ ├── page.tsx │ ├── posts │ │ ├── [id] │ │ │ ├── loading.tsx │ │ │ ├── page.tsx │ │ │ └── template.tsx │ │ ├── actions.ts │ │ ├── create │ │ │ ├── loading.tsx │ │ │ ├── page.tsx │ │ │ └── template.tsx │ │ ├── layout.tsx │ │ ├── loading.tsx │ │ ├── page.tsx │ │ └── template.tsx │ ├── profile │ │ ├── actions.ts │ │ ├── page.tsx │ │ └── update-profile.tsx │ └── template.tsx ├── components │ ├── animated-template.tsx │ ├── layout │ │ ├── auth-and-theme.tsx │ │ ├── auth-button.tsx │ │ ├── auth-provider.tsx │ │ ├── index.ts │ │ ├── links.ts │ │ ├── menu.tsx │ │ ├── navbar.tsx │ │ ├── signin-dialog.tsx │ │ ├── theme-provider.tsx │ │ └── theme-toggle.tsx │ ├── post │ │ ├── create-post-form.tsx │ │ ├── post-card-skeleton.tsx │ │ ├── post-card.tsx │ │ ├── post-delete-button.tsx │ │ ├── post-details.tsx │ │ └── post-form-skeleton.tsx │ └── ui │ │ ├── accordion.tsx │ │ ├── alert-dialog.tsx │ │ ├── alert.tsx │ │ ├── aspect-ratio.tsx │ │ ├── avatar.tsx │ │ ├── badge.tsx │ │ ├── breadcrumb.tsx │ │ ├── button.tsx │ │ ├── calendar.tsx │ │ ├── card.tsx │ │ ├── carousel.tsx │ │ ├── chart.tsx │ │ ├── checkbox.tsx │ │ ├── collapsible.tsx │ │ ├── command.tsx │ │ ├── context-menu.tsx │ │ ├── dialog.tsx │ │ ├── drawer.tsx │ │ ├── dropdown-menu.tsx │ │ ├── form.tsx │ │ ├── hover-card.tsx │ │ ├── index.ts │ │ ├── input-otp.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── menubar.tsx │ │ ├── navigation-menu.tsx │ │ ├── pagination.tsx │ │ ├── popover.tsx │ │ ├── progress.tsx │ │ ├── radio-group.tsx │ │ ├── resizable.tsx │ │ ├── scroll-area.tsx │ │ ├── select.tsx │ │ ├── separator.tsx │ │ ├── sheet.tsx │ │ ├── sidebar.tsx │ │ ├── skeleton.tsx │ │ ├── slider.tsx │ │ ├── sonner.tsx │ │ ├── switch.tsx │ │ ├── table.tsx │ │ ├── tabs.tsx │ │ ├── textarea.tsx │ │ ├── toast.tsx │ │ ├── toaster.tsx │ │ ├── toggle-group.tsx │ │ ├── toggle.tsx │ │ └── tooltip.tsx ├── hooks │ ├── index.ts │ ├── use-mobile.tsx │ └── use-toast.ts ├── lib │ ├── auth.ts │ ├── preferences.ts │ ├── prisma.ts │ └── utils.ts ├── schemas │ └── index.ts └── types │ └── index.ts ├── tailwind.config.js ├── tsconfig.json └── vercel.json /.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 | # testing 8 | /coverage 9 | next-env.d.ts 10 | # next.js 11 | /.next/ 12 | /out/ 13 | 14 | # production 15 | /build 16 | 17 | # misc 18 | .DS_Store 19 | *.pem 20 | .env 21 | yarn.lock 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env.local 30 | .env.development.local 31 | .env.test.local 32 | .env.production.local 33 | 34 | # vercel 35 | .vercel 36 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true, 4 | "trailingComma": "none", 5 | "jsxSingleQuote": true, 6 | "arrowParens": "avoid" 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules\\typescript\\lib" 3 | } 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### A Next.js template 2 | 3 | A Next.js app that uses Shadcn, Prisma ORM, MongoDB PostgreSQL and Next Auth 4 | 5 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fdanybeltran%2Fnextjs-typescript-and-mongodb) 6 | 7 | ### Updating deps 8 | 9 | To update the dependencies to their latest versions, run: 10 | 11 | ``` 12 | ./scripts/update-deps.sh 13 | ``` 14 | 15 | --- 16 | 17 | ### Development 18 | 19 | You need to pass an env. variable with the MongoDB connection string, as well as any variables required by `next-auth`: 20 | 21 | ``` 22 | NEXTAUTH_SECRET= 23 | GOOGLE_APP_CLIENT_ID= 24 | GOOGLE_APP_CLIENT_SECRET= 25 | NEXTAUTH_URL=http://localhost:3000 26 | # Connect to Supabase via connection pooling with Supavisor. 27 | DATABASE_URL= 28 | # Direct connection to the database. Used for migrations. 29 | DIRECT_URL= 30 | ``` 31 | 32 | (You don't need `NEXTAUTH_URL` if you are deploying to Vercel) 33 | 34 | How to get these variables? 35 | 36 | --- 37 | 38 | `DATABASE_URL` and `DIRECT_URL`: Visit the [Supabase documentation](https://supabase.com/partners/integrations/prisma) 39 | 40 | --- 41 | 42 | 64 | 65 | - [`GOOGLE_APP_CLIENT_ID` and `GOOGLE_APP_CLIENT_SECRET`](https://developers.google.com/identity/oauth2/web/guides/get-google-api-clientid) 66 | 67 | --- 68 | 69 | Use your preferred tool to generate the `NEXTAUTH_SECRET` hash: 70 | 71 | Using [This tool](https://generate-secret.vercel.app/32) is the quickest way to generate a hash. You can change the last segment of the url to get a hash of your preferred length, such as `https://generate-secret.vercel.app/44` 72 | 73 | **OpenSSL :** 74 | 75 | ```bash 76 | openssl rand -base64 32 77 | ``` 78 | 79 | **Urandom :** 80 | 81 | ```bash 82 | head -c 32 /dev/urandom | base64 83 | ``` 84 | 85 | **Python :** 86 | 87 | ```py 88 | import base64 89 | import os 90 | 91 | random_bytes = os.urandom(32) 92 | base64_string = base64.b64encode(random_bytes).decode('utf-8') 93 | print(base64_string) 94 | ``` 95 | 96 | **JavaScript :** 97 | 98 | ```js 99 | const crypto = require('crypto') 100 | 101 | const randomBytes = crypto.randomBytes(32) 102 | const base64String = randomBytes.toString('base64') 103 | console.log(base64String) 104 | ``` 105 | 106 | You can add those variables to a `.ENV` file (don't forget to add it to your `.gitignore` file!) 107 | 108 | Related documentation: 109 | 110 | - [`nextjs`](https://nextjs.org/docs) 111 | 112 | - [`next-auth`](https://next-auth.js.org/getting-started/introduction) 113 | 114 | - [`http-react`](https://httpr.vercel.app/docs) 115 | 116 | [Live preview](https://nextjs-typescript-and-mongodb-psi.vercel.app) 117 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.js", 8 | "css": "src/app/globals.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "atomic-utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } 22 | -------------------------------------------------------------------------------- /docs/connect-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danybeltran/nextjs-typescript-and-mongodb/3d21942264a6135c4a29e3cb9ac90e812456e0c4/docs/connect-1.png -------------------------------------------------------------------------------- /docs/connection-string.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danybeltran/nextjs-typescript-and-mongodb/3d21942264a6135c4a29e3cb9ac90e812456e0c4/docs/connection-string.png -------------------------------------------------------------------------------- /docs/drivers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danybeltran/nextjs-typescript-and-mongodb/3d21942264a6135c4a29e3cb9ac90e812456e0c4/docs/drivers.png -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. 6 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @type { import("next").NextConfig } 3 | */ 4 | module.exports = { 5 | reactStrictMode: false 6 | } 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "next-11", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "npx prisma generate dev --name init && npx prisma db push && next dev", 7 | "build": "npx prisma generate init && npx prisma db push && next build", 8 | "migrate:dev": "npx prisma generate dev --name init", 9 | "start": "next start" 10 | }, 11 | "repository": { 12 | "type": "github", 13 | "url": "https://github.com/danybeltran/nextjs-typescript-and-mongodb" 14 | }, 15 | "dependencies": { 16 | "@hookform/resolvers": "^3.9.1", 17 | "@prisma/client": "^6.1.0", 18 | "@radix-ui/react-accordion": "^1.2.2", 19 | "@radix-ui/react-alert-dialog": "^1.1.4", 20 | "@radix-ui/react-aspect-ratio": "^1.1.1", 21 | "@radix-ui/react-avatar": "^1.1.2", 22 | "@radix-ui/react-checkbox": "^1.1.3", 23 | "@radix-ui/react-collapsible": "^1.1.2", 24 | "@radix-ui/react-context-menu": "^2.2.4", 25 | "@radix-ui/react-dialog": "^1.1.4", 26 | "@radix-ui/react-dropdown-menu": "^2.1.4", 27 | "@radix-ui/react-hover-card": "^1.1.4", 28 | "@radix-ui/react-label": "^2.1.1", 29 | "@radix-ui/react-menubar": "^1.1.4", 30 | "@radix-ui/react-navigation-menu": "^1.2.3", 31 | "@radix-ui/react-popover": "^1.1.4", 32 | "@radix-ui/react-progress": "^1.1.1", 33 | "@radix-ui/react-radio-group": "^1.2.2", 34 | "@radix-ui/react-scroll-area": "^1.2.2", 35 | "@radix-ui/react-select": "^2.1.4", 36 | "@radix-ui/react-separator": "^1.1.1", 37 | "@radix-ui/react-slider": "^1.2.2", 38 | "@radix-ui/react-slot": "^1.1.1", 39 | "@radix-ui/react-switch": "^1.1.2", 40 | "@radix-ui/react-tabs": "^1.1.2", 41 | "@radix-ui/react-toast": "^1.2.4", 42 | "@radix-ui/react-toggle": "^1.1.1", 43 | "@radix-ui/react-toggle-group": "^1.1.1", 44 | "@radix-ui/react-tooltip": "^1.1.6", 45 | "atomic-utils": "^1.5.8", 46 | "autoprefixer": "^10.4.20", 47 | "bs-icon": "^0.0.8", 48 | "class-variance-authority": "^0.7.1", 49 | "clsx": "^2.1.1", 50 | "cmdk": "^1.0.4", 51 | "date-fns": "^4.1.0", 52 | "easymde": "^2.18.0", 53 | "embla-carousel-react": "^8.5.1", 54 | "framer-motion": "^11.15.0", 55 | "geist": "^1.3.1", 56 | "input-otp": "^1.4.1", 57 | "js-cookie": "^3.0.5", 58 | "lucide-react": "^0.469.0", 59 | "next": "^15.1.3", 60 | "next-auth": "^4.24.11", 61 | "next-themes": "^0.4.4", 62 | "postcss": "^8.4.49", 63 | "react": "^19.0.0", 64 | "react-day-picker": "^9.4.4", 65 | "react-dom": "^19.0.0", 66 | "react-hook-form": "^7.54.2", 67 | "react-icons": "^5.4.0", 68 | "react-markdown": "^9.0.1", 69 | "react-resizable-panels": "^2.1.7", 70 | "recharts": "^2.15.0", 71 | "sonner": "^1.7.1", 72 | "tailwind-merge": "^2.6.0", 73 | "tailwindcss": "^3.4.17", 74 | "tailwindcss-animate": "^1.0.7", 75 | "vaul": "^1.1.2", 76 | "zod": "^3.24.1" 77 | }, 78 | "devDependencies": { 79 | "@tailwindcss/typography": "^0.5.15", 80 | "@types/js-cookie": "^3.0.6", 81 | "@types/node": "^22.10.2", 82 | "@types/react": "^19.0.2", 83 | "@types/react-dom": "^19.0.2", 84 | "prisma": "^6.1.0", 85 | "typescript": "^5.7.2" 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | datasource db { 2 | provider = "postgresql" 3 | url = env("DATABASE_URL") 4 | directUrl = env("DIRECT_URL") 5 | } 6 | 7 | generator client { 8 | provider = "prisma-client-js" 9 | } 10 | 11 | model Post { 12 | id Int @id @default(autoincrement()) 13 | title String 14 | date DateTime @default(now()) 15 | content String 16 | } 17 | 18 | model Preferences { 19 | id Int @id @default(autoincrement()) 20 | user_email String 21 | user_fullname String 22 | user_profile_picture String 23 | username String 24 | user_description String 25 | // Add more customizations 26 | } 27 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danybeltran/nextjs-typescript-and-mongodb/3d21942264a6135c4a29e3cb9ac90e812456e0c4/public/favicon.ico -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /scripts/get-deps.js: -------------------------------------------------------------------------------- 1 | const { dependencies, devDependencies } = require('../package.json') 2 | const fs = require('fs') 3 | const path = require('path') 4 | 5 | const versions = {} 6 | const versionsDev = {} 7 | 8 | console.log('------- DEPENDENCIES -------') 9 | const installDeps = 10 | 'npm install ' + 11 | Object.keys(dependencies) 12 | .map(dep => { 13 | console.log(dep + ` : (${dependencies[dep]})`) 14 | versions[dep] = dependencies[dep] 15 | return dep 16 | }) 17 | .join('@latest ') + 18 | '@latest -f' 19 | 20 | console.log('\n------- DEV DEPENDENCIES -------') 21 | 22 | const installDevDeps = 23 | 'npm install -D ' + 24 | Object.keys(devDependencies) 25 | .map(dep => { 26 | console.log(dep + ` : (${devDependencies[dep]})`) 27 | versionsDev[dep] = devDependencies[dep] 28 | return dep 29 | }) 30 | .join('@latest ') + 31 | '@latest -f' 32 | 33 | fs.writeFile( 34 | path.join(__dirname, './installdeps.sh'), 35 | `${installDeps} && ${installDevDeps}`, 36 | () => {} 37 | ) 38 | -------------------------------------------------------------------------------- /scripts/update-deps.sh: -------------------------------------------------------------------------------- 1 | echo "Getting dependencies" 2 | echo 3 | node ./scripts/get-deps.js 4 | chmod +x ./scripts/installdeps.sh 5 | echo 6 | ./scripts/installdeps.sh 7 | echo 8 | rm ./scripts/installdeps.sh 9 | -------------------------------------------------------------------------------- /src/app/api/auth/[...nextauth]/route.ts: -------------------------------------------------------------------------------- 1 | import NextAuth from 'next-auth' 2 | import { authOptions } from '@/lib/auth' 3 | 4 | const handler = NextAuth(authOptions) 5 | 6 | export { handler as GET, handler as POST } 7 | -------------------------------------------------------------------------------- /src/app/api/preferences/route.ts: -------------------------------------------------------------------------------- 1 | import { getUserPreferences } from '@/lib/preferences' 2 | 3 | export async function GET() { 4 | const preferences = await getUserPreferences() 5 | 6 | return Response.json(preferences) 7 | } 8 | -------------------------------------------------------------------------------- /src/app/error.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { Button } from '@/components/ui' 4 | import Icon from 'bs-icon' 5 | 6 | export default function ErrorPage() { 7 | return ( 8 |
9 |

Internal server error

10 |

The page you requested failed to load.

11 | 14 |
15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0 0% 100%; 8 | --foreground: 0 0% 3.9%; 9 | --card: 0 0% 100%; 10 | --card-foreground: 0 0% 3.9%; 11 | --popover: 0 0% 100%; 12 | --popover-foreground: 0 0% 3.9%; 13 | --primary: 0 0% 9%; 14 | --primary-foreground: 0 0% 98%; 15 | --secondary: 0 0% 96.1%; 16 | --secondary-foreground: 0 0% 9%; 17 | --muted: 0 0% 96.1%; 18 | --muted-foreground: 0 0% 45.1%; 19 | --accent: 0 0% 96.1%; 20 | --accent-foreground: 0 0% 9%; 21 | --destructive: 0 84.2% 60.2%; 22 | --destructive-foreground: 0 0% 98%; 23 | --border: 0 0% 89.8%; 24 | --input: 0 0% 89.8%; 25 | --ring: 0 0% 3.9%; 26 | --radius: 0.5rem; 27 | --sidebar-background: 0 0% 98%; 28 | --sidebar-foreground: 240 5.3% 26.1%; 29 | --sidebar-primary: 240 5.9% 10%; 30 | --sidebar-primary-foreground: 0 0% 98%; 31 | --sidebar-accent: 240 4.8% 95.9%; 32 | --sidebar-accent-foreground: 240 5.9% 10%; 33 | --sidebar-border: 220 13% 91%; 34 | --sidebar-ring: 217.2 91.2% 59.8%; 35 | --chart-1: 12 76% 61%; 36 | --chart-2: 173 58% 39%; 37 | --chart-3: 197 37% 24%; 38 | --chart-4: 43 74% 66%; 39 | --chart-5: 27 87% 67%; 40 | } 41 | 42 | .dark { 43 | --background: 0 0% 3.9%; 44 | --foreground: 0 0% 98%; 45 | --card: 0 0% 3.9%; 46 | --card-foreground: 0 0% 98%; 47 | --popover: 0 0% 3.9%; 48 | --popover-foreground: 0 0% 98%; 49 | --primary: 0 0% 98%; 50 | --primary-foreground: 0 0% 9%; 51 | --secondary: 0 0% 14.9%; 52 | --secondary-foreground: 0 0% 98%; 53 | --muted: 0 0% 14.9%; 54 | --muted-foreground: 0 0% 63.9%; 55 | --accent: 0 0% 14.9%; 56 | --accent-foreground: 0 0% 98%; 57 | --destructive: 0 62.8% 30.6%; 58 | --destructive-foreground: 0 0% 98%; 59 | --border: 0 0% 14.9%; 60 | --input: 0 0% 14.9%; 61 | --ring: 0 0% 83.1%; 62 | --sidebar-background: 240 5.9% 10%; 63 | --sidebar-foreground: 240 4.8% 95.9%; 64 | --sidebar-primary: 224.3 76.3% 48%; 65 | --sidebar-primary-foreground: 0 0% 100%; 66 | --sidebar-accent: 240 3.7% 15.9%; 67 | --sidebar-accent-foreground: 240 4.8% 95.9%; 68 | --sidebar-border: 240 3.7% 15.9%; 69 | --sidebar-ring: 217.2 91.2% 59.8%; 70 | --chart-1: 220 70% 50%; 71 | --chart-2: 160 60% 45%; 72 | --chart-3: 30 80% 55%; 73 | --chart-4: 280 65% 60%; 74 | --chart-5: 340 75% 55%; 75 | } 76 | } 77 | 78 | @layer base { 79 | * { 80 | @apply border-border; 81 | } 82 | body { 83 | @apply bg-background text-foreground; 84 | } 85 | } 86 | 87 | body { 88 | --sb-track-color: #111619; 89 | --sb-thumb-color: rgb(94, 94, 94); 90 | --sb-size: 6px; 91 | scrollbar-color: var(--sb-thumb-color) var(--sb-track-color); 92 | scrollbar-gutter: stable; 93 | } 94 | 95 | body::-webkit-scrollbar { 96 | width: var(--sb-size); 97 | } 98 | 99 | body::-webkit-scrollbar-track { 100 | background: var(--sb-track-color); 101 | border-radius: 0px; 102 | } 103 | 104 | body::-webkit-scrollbar-thumb { 105 | background: var(--sb-thumb-color); 106 | border-radius: 2px; 107 | } 108 | -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import { GeistSans } from 'geist/font/sans' 2 | import type { Metadata } from 'next' 3 | import './globals.css' 4 | import 'bs-icon/icons.css' 5 | 6 | import { AuthProvider, Navbar, ThemeProvider } from '@/components/layout' 7 | 8 | import { AtomicState } from 'atomic-utils' 9 | import { FetchConfig } from 'atomic-utils' 10 | import { getServerSession } from 'next-auth' 11 | import { getUserPreferences } from '@/lib/preferences' 12 | import { cookies } from 'next/headers' 13 | 14 | export const metadata: Metadata = { 15 | title: 'Home', 16 | description: 'Home page ' 17 | } 18 | 19 | export default async function MainLayout({ children }) { 20 | const session = await getServerSession() 21 | 22 | const preferences = await getUserPreferences() 23 | 24 | const serverTheme = (await cookies()).get('theme')?.value ?? 'system' 25 | 26 | return ( 27 | 28 | 29 | Next.js starter 30 | 31 | 32 | 33 | 34 | 35 |
36 | 37 | 42 | 49 | 50 |
51 | {children} 52 |
53 |
54 |
55 |
56 |
57 |
58 | 59 | 60 | ) 61 | } 62 | -------------------------------------------------------------------------------- /src/app/not-found.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from '@/components/ui' 2 | import Icon from 'bs-icon' 3 | import Link from 'next/link' 4 | 5 | export default function NotFound() { 6 | return ( 7 |
8 |

9 | The page you requested was not found 10 |

11 | 12 | 13 | 14 |
15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | import { GoPlus } from 'react-icons/go' 3 | import { FaReact } from 'react-icons/fa' 4 | import { BiLogoMongodb } from 'react-icons/bi' 5 | import { TbBrandNextjs } from 'react-icons/tb' 6 | import { SiPrisma, SiTailwindcss } from 'react-icons/si' 7 | import { IoLogoGithub, IoLogoVercel } from 'react-icons/io5' 8 | 9 | import { Button } from '@/components/ui' 10 | 11 | export default function Page() { 12 | return ( 13 | <> 14 |
15 |
16 |

17 | Hello world 18 |

19 |

This is a starter project

20 |
21 | 26 | 30 | 31 | 35 | 38 | 39 |
40 |
41 |
42 |
46 |
47 |

48 | Features 49 |

50 |
51 |
52 |
53 |
54 | 55 |
56 |

Next.js 14

57 |

App dir

58 |
59 |
60 |
61 |
62 |
63 | 64 |
65 |

React 18

66 |

67 | Server and Client Components 68 |

69 |
70 |
71 |
72 |
73 |
74 |
75 | 76 | 77 | 78 |
79 |
80 |

Database

81 |

82 | Prisma + MongoDB 83 |

84 |
85 |
86 |
87 |
88 |
89 | 90 |
91 |

ShadCN

92 |

93 | UI components built with TailwindCSS and Radix UI 94 |

95 |
96 |
97 |
98 |
99 |
100 |
104 |
105 |

106 | The code is available on{' '} 107 | 113 | GitHub 114 | 115 | .{' '} 116 |

117 |
118 |
119 | 120 | ) 121 | } 122 | -------------------------------------------------------------------------------- /src/app/posts/[id]/loading.tsx: -------------------------------------------------------------------------------- 1 | import { ArrowLeft } from 'lucide-react' 2 | 3 | import { Skeleton } from '@/components/ui' 4 | 5 | export default function PostIdLoading() { 6 | return ( 7 |
8 |
9 | 10 | Back 11 |
12 |
13 |
14 |
15 |
16 | 17 | 18 |
19 | 20 |
21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 |
36 |
37 |
38 | 39 |
40 |
41 |
42 | ) 43 | } 44 | -------------------------------------------------------------------------------- /src/app/posts/[id]/page.tsx: -------------------------------------------------------------------------------- 1 | import { cache } from 'react' 2 | import Link from 'next/link' 3 | import { ArrowLeft } from 'lucide-react' 4 | 5 | import PostDeleteButton from '@/components/post/post-delete-button' 6 | import PostDetails from '@/components/post/post-details' 7 | 8 | import { prisma } from '@/lib/prisma' 9 | 10 | export async function generateMetadata(props: PostPageProps) { 11 | const params = await props.params 12 | const post = await getPost(params) 13 | 14 | return { 15 | title: 'Post: ' + post?.title, 16 | description: 'Details for post ' + post?.id 17 | } 18 | } 19 | 20 | export default async function PostDetailsPage(props: PostPageProps) { 21 | const params = await props.params 22 | 23 | const post = await getPost(params) 24 | 25 | return ( 26 |
27 | 28 | 29 | Back 30 | 31 | {post ? ( 32 |
33 |
34 | 35 |
36 |
37 | 38 |
39 |
40 | ) : ( 41 |
42 | Post not found 43 |
44 | )} 45 |
46 | ) 47 | } 48 | 49 | const getPost = cache(async (params: { id: string }) => { 50 | try { 51 | return prisma.post.findFirst({ 52 | where: { 53 | id: parseInt(params.id) 54 | } 55 | }) 56 | } catch (err) { 57 | return null 58 | } 59 | }) 60 | 61 | interface PostPageProps { 62 | params: Promise<{ id: string }> 63 | } 64 | -------------------------------------------------------------------------------- /src/app/posts/[id]/template.tsx: -------------------------------------------------------------------------------- 1 | export { default } from '@/components/animated-template' 2 | -------------------------------------------------------------------------------- /src/app/posts/actions.ts: -------------------------------------------------------------------------------- 1 | 'use server' 2 | import { revalidatePath } from 'next/cache' 3 | import { actionData } from 'atomic-utils' 4 | 5 | import { prisma } from '@/lib/prisma' 6 | import { postSchema } from '@/schemas' 7 | 8 | export async function createPost(post: any) { 9 | try { 10 | const validation = postSchema.safeParse(post) 11 | 12 | if (validation.success) { 13 | const newPost = await prisma.post.create({ 14 | data: post 15 | }) 16 | 17 | revalidatePath('/posts') 18 | 19 | return actionData(newPost) 20 | } 21 | 22 | return actionData(validation.error.format(), { 23 | status: 400 24 | }) 25 | } catch { 26 | return { 27 | status: 500 28 | } 29 | } 30 | } 31 | 32 | export async function deletePost(id: number) { 33 | try { 34 | const deletedPost = await prisma.post.delete({ 35 | where: { 36 | id: id 37 | } 38 | }) 39 | 40 | revalidatePath('/posts') 41 | 42 | /** 43 | * actionResult formats a response so http-react can read data, status code and error 44 | * The code below will be formated as { data: deletedPost, status: 200 }. 45 | * You can ommit the status part like this `return actionResult(deletedPost)` 46 | */ 47 | return actionData(deletedPost, { 48 | status: 200 49 | }) 50 | } catch { 51 | return { 52 | status: 500 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/app/posts/create/loading.tsx: -------------------------------------------------------------------------------- 1 | import { ArrowLeft } from 'lucide-react' 2 | 3 | import PostFormSkeleton from '@/components/post/post-form-skeleton' 4 | 5 | export default function CreateLoading() { 6 | return ( 7 |
8 |
9 | 10 | Back 11 |
12 |
13 |
14 |

Add Post

15 |
16 | 17 |
18 |
19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /src/app/posts/create/page.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | import { ArrowLeft } from 'lucide-react' 3 | 4 | import PostForm from '@/components/post/create-post-form' 5 | 6 | export default function Create() { 7 | return ( 8 |
9 | 10 | 11 | Back 12 | 13 |
14 |
15 |

Add Post

16 |
17 | 18 |
19 |
20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /src/app/posts/create/template.tsx: -------------------------------------------------------------------------------- 1 | export { default } from '@/components/animated-template' 2 | -------------------------------------------------------------------------------- /src/app/posts/layout.tsx: -------------------------------------------------------------------------------- 1 | export { default } from '@/components/animated-template' 2 | -------------------------------------------------------------------------------- /src/app/posts/loading.tsx: -------------------------------------------------------------------------------- 1 | import { ArrowLeft } from 'lucide-react' 2 | 3 | import PostCardSkeleton from '@/components/post/post-card-skeleton' 4 | import { Button } from '@/components/ui' 5 | 6 | export default function PostsLoading() { 7 | return ( 8 |
9 |
10 | 11 | Back 12 |
13 |
14 |

All Posts

15 | 18 |
19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 |
28 | ) 29 | } 30 | -------------------------------------------------------------------------------- /src/app/posts/page.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | import { ArrowLeft } from 'lucide-react' 3 | import { headers } from 'next/headers' 4 | 5 | import { Button } from '@/components/ui' 6 | import PostCard from '@/components/post/post-card' 7 | 8 | import { prisma } from '@/lib/prisma' 9 | import { RenderList } from 'atomic-utils' 10 | 11 | export const revalidate = 0 12 | 13 | export default async function Posts() { 14 | headers() 15 | 16 | const posts = (await prisma.post.findMany({})).reverse() 17 | 18 | return ( 19 |
20 | 21 | 22 | Back 23 | 24 |
25 |

All Posts

26 | 27 | 30 | 31 |
32 | 33 | {posts.length === 0 ? ( 34 |
35 |

No posts to show

36 |
37 | ) : ( 38 |
39 | } 42 | /> 43 |
44 | )} 45 |
46 | ) 47 | } 48 | -------------------------------------------------------------------------------- /src/app/posts/template.tsx: -------------------------------------------------------------------------------- 1 | export { default } from '@/components/animated-template' 2 | -------------------------------------------------------------------------------- /src/app/profile/actions.ts: -------------------------------------------------------------------------------- 1 | 'use server' 2 | 3 | import { getUserPreferences } from '@/lib/preferences' 4 | import { prisma } from '@/lib/prisma' 5 | import { UpdatePreferencesPayload } from '@/schemas' 6 | import { actionData } from 'atomic-utils' 7 | 8 | export async function updateUserPreferences(payload: UpdatePreferencesPayload) { 9 | const preferences = await getUserPreferences() 10 | 11 | const updatedPreferences = await prisma.preferences.update({ 12 | where: { 13 | id: preferences.id 14 | }, 15 | data: { 16 | user_fullname: payload.user_fullname.trim() || preferences.user_fullname, 17 | user_description: 18 | payload.user_description.trim() || preferences.user_description 19 | } 20 | }) 21 | 22 | return actionData(updatedPreferences) 23 | } 24 | -------------------------------------------------------------------------------- /src/app/profile/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { usePreferences, useUser } from '@/hooks' 4 | import Link from 'next/link' 5 | 6 | import { Button } from '@/components/ui' 7 | import SigninDialog from '@/components/layout/signin-dialog' 8 | import { revalidate } from 'atomic-utils' 9 | import UpdateProfile from './update-profile' 10 | 11 | export default function ProfilePage() { 12 | const { data: user } = useUser() 13 | const { data: preferences } = usePreferences() 14 | 15 | if (!user) { 16 | return ( 17 |
18 |
19 |

You haven't logged in

20 | 21 | 22 | 23 | 24 |
25 |
26 | ) 27 | } 28 | 29 | return ( 30 |
31 | 32 |
33 | 38 |

39 | Name: {preferences.user_fullname} 40 |

41 |

42 | Email: {preferences.user_email} 43 |

44 |
45 | Bio:
46 |

{preferences.user_description}

47 |
48 |
49 | 50 |
51 |

Raw data

52 |
{JSON.stringify({ user, preferences }, null, 2)}
53 |
54 |
55 | ) 56 | } 57 | -------------------------------------------------------------------------------- /src/app/profile/update-profile.tsx: -------------------------------------------------------------------------------- 1 | import { revalidate, useObject, useServerAction } from 'atomic-utils' 2 | import { updateUserPreferences } from './actions' 3 | import { usePreferences, useUser } from '@/hooks' 4 | import { Button, Input, Textarea } from '@/components/ui' 5 | import { VscLoading } from 'react-icons/vsc' 6 | import { UpdatePreferencesPayload } from '@/schemas' 7 | 8 | export default function UpdateProfile() { 9 | const { data: preferences } = usePreferences() 10 | 11 | const [newPreferences, newPreferencesActions] = 12 | useObject({ 13 | user_fullname: preferences.user_fullname, 14 | user_description: preferences.user_description 15 | }) 16 | 17 | const { reFetch: savePreferences, isPending: savingPreferences } = 18 | useServerAction(updateUserPreferences, { 19 | params: newPreferences, 20 | onResolve() { 21 | revalidate('Preferences') 22 | } 23 | }) 24 | 25 | const hasChanges = ['user_fullname', 'user_description'].some( 26 | preference => preferences[preference] !== newPreferences[preference] 27 | ) 28 | 29 | return ( 30 |
{ 34 | if (!hasChanges) e.preventDefault() 35 | }} 36 | > 37 |

Update preferences

38 |
39 | 51 |
52 | 53 |
54 |