├── . babelrc ├── .cursorrules ├── .eslintrc.js ├── .github └── workflows │ └── codeql.yml ├── .gitignore ├── .husky └── pre-commit ├── .npmrc ├── .prettierignore ├── README.md ├── apps ├── docs │ ├── .eslintrc.js │ ├── .gitignore │ ├── README.md │ ├── app │ │ ├── layout.tsx │ │ └── page.tsx │ ├── next-env.d.ts │ ├── next.config.js │ ├── package.json │ └── tsconfig.json └── web │ ├── .env.example │ ├── .eslintrc.js │ ├── .gitignore │ ├── README.md │ ├── components.json │ ├── env.example │ ├── next-env.d.ts │ ├── next.config.mjs │ ├── package.json │ ├── postcss.config.js │ ├── public │ ├── assets │ │ ├── logo.png │ │ └── logo.svg │ ├── favicon.ico │ └── uploads │ │ └── 2024-08-13 │ │ ├── large-4c7e9cf4-b12f-4ae6-9576-12cb4484cd72.jpg │ │ ├── medium-4c7e9cf4-b12f-4ae6-9576-12cb4484cd72.jpg │ │ └── thumbnail-4c7e9cf4-b12f-4ae6-9576-12cb4484cd72.jpg │ ├── src │ ├── actions │ │ ├── auth │ │ │ ├── index.ts │ │ │ └── type.ts │ │ └── protect │ │ │ └── postAction.ts │ ├── app │ │ ├── [lang] │ │ │ ├── (auth) │ │ │ │ ├── forgot-password │ │ │ │ │ └── page.tsx │ │ │ │ ├── reset-password │ │ │ │ │ └── page.tsx │ │ │ │ ├── signin │ │ │ │ │ └── page.tsx │ │ │ │ ├── signup │ │ │ │ │ └── page.tsx │ │ │ │ └── verify-email │ │ │ │ │ └── index.tsx │ │ │ ├── (protected) │ │ │ │ ├── layout.tsx │ │ │ │ └── user │ │ │ │ │ ├── posts │ │ │ │ │ └── page.tsx │ │ │ │ │ ├── profile │ │ │ │ │ └── page.tsx │ │ │ │ │ └── settings │ │ │ │ │ └── page.tsx │ │ │ ├── (protected-post) │ │ │ │ ├── layout.tsx │ │ │ │ └── user │ │ │ │ │ └── posts │ │ │ │ │ ├── [postId] │ │ │ │ │ └── edit │ │ │ │ │ │ └── page.tsx │ │ │ │ │ ├── create │ │ │ │ │ └── page.tsx │ │ │ │ │ └── loading.tsx │ │ │ ├── (public) │ │ │ │ ├── about-us │ │ │ │ │ └── page.tsx │ │ │ │ ├── contact │ │ │ │ │ └── page.tsx │ │ │ │ ├── layout.tsx │ │ │ │ ├── loading.tsx │ │ │ │ ├── page.tsx │ │ │ │ ├── search │ │ │ │ │ └── page.tsx │ │ │ │ ├── tags │ │ │ │ │ └── page.tsx │ │ │ │ └── ui │ │ │ │ │ └── page.tsx │ │ │ ├── (public-fullwidth) │ │ │ │ ├── author │ │ │ │ │ └── [authorId] │ │ │ │ │ │ ├── followers │ │ │ │ │ │ └── page.tsx │ │ │ │ │ │ └── page.tsx │ │ │ │ ├── posts │ │ │ │ │ ├── [postId] │ │ │ │ │ │ ├── page.tsx │ │ │ │ │ │ └── tocbot.css │ │ │ │ │ └── page.tsx │ │ │ │ └── tags │ │ │ │ │ └── [tagId] │ │ │ │ │ ├── follower │ │ │ │ │ └── page.tsx │ │ │ │ │ └── page.tsx │ │ │ ├── globals.css │ │ │ ├── layout.tsx │ │ │ └── not-found.tsx │ │ └── api │ │ │ ├── auth │ │ │ └── [...nextauth] │ │ │ │ └── route.ts │ │ │ ├── protected │ │ │ ├── comment │ │ │ │ └── route.ts │ │ │ ├── images │ │ │ │ └── route.ts │ │ │ ├── tags │ │ │ │ └── route.ts │ │ │ └── user │ │ │ │ └── [userId] │ │ │ │ ├── follower │ │ │ │ └── route.ts │ │ │ │ ├── followers │ │ │ │ └── route.ts │ │ │ │ ├── followings │ │ │ │ └── route.ts │ │ │ │ └── route.ts │ │ │ └── public │ │ │ ├── post │ │ │ └── [postIdOrSlug] │ │ │ │ ├── comments │ │ │ │ └── route.ts │ │ │ │ └── route.ts │ │ │ └── tags │ │ │ └── route.ts │ ├── configs │ │ └── auth.ts │ ├── constants │ │ ├── apis.ts │ │ ├── index.ts │ │ ├── order.ts │ │ ├── routes.ts │ │ └── upload.ts │ ├── emails │ │ ├── index.ts │ │ └── verify-email.tsx │ ├── font │ │ └── index.ts │ ├── hooks │ │ ├── useFollowTag.ts │ │ ├── useFollowUser.ts │ │ ├── useGetImages.ts │ │ ├── useInfinityScroll.ts │ │ └── useUploadImage.ts │ ├── i18n.ts │ ├── libs │ │ ├── resend │ │ │ └── index.tsx │ │ └── validationAction │ │ │ └── index.ts │ ├── messages │ │ ├── en.json │ │ └── fr.json │ ├── middleware.ts │ ├── molecules │ │ ├── auth │ │ │ ├── auth-form.tsx │ │ │ ├── forgot-password │ │ │ │ └── index.tsx │ │ │ ├── reset-password │ │ │ │ └── index.tsx │ │ │ ├── sign-in │ │ │ │ └── index.tsx │ │ │ └── sign-up │ │ │ │ └── index.tsx │ │ ├── contact-form │ │ │ └── contact-form.tsx │ │ ├── editor-js │ │ │ ├── index.css │ │ │ ├── index.tsx │ │ │ ├── menu-bar.tsx │ │ │ └── menu-item.tsx │ │ ├── follower │ │ │ ├── followers │ │ │ │ ├── follower-item.tsx │ │ │ │ └── index.tsx │ │ │ └── user-profile │ │ │ │ ├── follow-button.tsx │ │ │ │ └── index.tsx │ │ ├── footer │ │ │ ├── index.tsx │ │ │ └── theme-toggle.tsx │ │ ├── home │ │ │ └── filter │ │ │ │ ├── filter-item.tsx │ │ │ │ └── index.tsx │ │ ├── infinite-scroll │ │ │ └── index.tsx │ │ ├── input-title │ │ │ └── index.tsx │ │ ├── language-switcher │ │ │ └── index.tsx │ │ ├── list-summary │ │ │ └── index.tsx │ │ ├── nav │ │ │ ├── index.module.css │ │ │ ├── index.tsx │ │ │ ├── logo.tsx │ │ │ ├── search-bar.tsx │ │ │ └── theme-toggle.tsx │ │ ├── no-item-founded │ │ │ └── index.tsx │ │ ├── page-title │ │ │ └── index.tsx │ │ ├── pagination │ │ │ └── index.tsx │ │ ├── post-form │ │ │ └── index.tsx │ │ ├── posts │ │ │ ├── post-detail │ │ │ │ ├── comments │ │ │ │ │ ├── comment-detail.tsx │ │ │ │ │ ├── comment-header.tsx │ │ │ │ │ ├── comment-input.tsx │ │ │ │ │ ├── comment-list.tsx │ │ │ │ │ └── index.tsx │ │ │ │ ├── edit-post-button │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── toggle-post.tsx │ │ │ │ ├── index.tsx │ │ │ │ ├── like-button │ │ │ │ │ ├── LikeButton.tsx │ │ │ │ │ ├── Likers.tsx │ │ │ │ │ └── index.tsx │ │ │ │ ├── post-content │ │ │ │ │ └── index.tsx │ │ │ │ ├── share-button │ │ │ │ │ └── index.tsx │ │ │ │ └── table-of-contents │ │ │ │ │ └── index.tsx │ │ │ ├── post-item │ │ │ │ ├── bookmark-button │ │ │ │ │ ├── BookmarkButton.tsx │ │ │ │ │ └── index.tsx │ │ │ │ ├── comment-button │ │ │ │ │ └── index.tsx │ │ │ │ ├── index.tsx │ │ │ │ ├── like-button │ │ │ │ │ └── index.tsx │ │ │ │ └── post-meta │ │ │ │ │ └── index.tsx │ │ │ ├── post-list │ │ │ │ └── index.tsx │ │ │ └── post-pagination │ │ │ │ └── index.tsx │ │ ├── profile │ │ │ └── index.tsx │ │ ├── sidebar-item │ │ │ └── index.tsx │ │ ├── square-advertisement │ │ │ └── index.tsx │ │ ├── tag │ │ │ ├── filter │ │ │ │ └── index.tsx │ │ │ ├── tag-badge │ │ │ │ └── index.tsx │ │ │ ├── tag-detail │ │ │ │ └── index.tsx │ │ │ ├── tag-item │ │ │ │ └── index.tsx │ │ │ ├── tag-list-meta │ │ │ │ └── index.tsx │ │ │ └── tag-list │ │ │ │ └── index.tsx │ │ ├── top-tags │ │ │ ├── NumberIndex.tsx │ │ │ └── index.tsx │ │ ├── upload │ │ │ ├── AssetsManagement.tsx │ │ │ ├── FileManagerContainer.tsx │ │ │ ├── FileUploader.tsx │ │ │ ├── ImageItem.tsx │ │ │ ├── ImageList.tsx │ │ │ ├── ImageSearchBar.tsx │ │ │ ├── Loading.tsx │ │ │ ├── UploadImageButton.tsx │ │ │ └── index.tsx │ │ ├── user-nav │ │ │ ├── LogoutMenu.tsx │ │ │ └── index.tsx │ │ └── user │ │ │ └── posts │ │ │ ├── filter.tsx │ │ │ ├── post-item.tsx │ │ │ └── post-meta.tsx │ ├── providers │ │ ├── authProvider.tsx │ │ └── index.tsx │ ├── types │ │ ├── comment.ts │ │ ├── index.ts │ │ └── users.ts │ └── utils │ │ ├── generatePath.ts │ │ ├── index.ts │ │ ├── navigation.ts │ │ └── text.ts │ ├── tailwind.config.js │ ├── tsconfig.json │ └── types │ └── next-auth.d.ts ├── docker-compose.yml ├── docs ├── comment-prisma-queries.md ├── database.md ├── post-prisma-queries.md ├── tag-primsa-queries.md └── user-prisma-queries.md ├── images └── home-screen.png ├── package.json ├── packages ├── database │ ├── .env.example │ ├── .gitignore │ ├── package.json │ ├── prisma │ │ ├── migrations │ │ │ ├── 20241223151826_add_counter_triggers.sql │ │ │ ├── 20241223151827_add_full_text_search.sql │ │ │ ├── 20241223151828_add_follower_count_triggers.sql │ │ │ ├── 20250312144706_init │ │ │ │ └── migration.sql │ │ │ └── migration_lock.toml │ │ ├── schema.prisma │ │ └── seeds │ │ │ └── seed.mjs │ ├── src │ │ ├── constant.ts │ │ ├── images │ │ │ ├── queries.ts │ │ │ ├── selects.ts │ │ │ └── type.ts │ │ ├── index.ts │ │ ├── posts │ │ │ ├── queries.ts │ │ │ ├── selects.ts │ │ │ └── utils.ts │ │ ├── prisma.ts │ │ ├── shared │ │ │ └── type.ts │ │ ├── tags │ │ │ ├── queries.ts │ │ │ └── selects.ts │ │ └── users │ │ │ ├── queries.ts │ │ │ └── selects.ts │ └── tsconfig.json ├── eslint-config-custom │ ├── .babelrc │ ├── index.js │ └── package.json ├── prettier-config-custom │ └── package.json ├── tailwind-config │ ├── package.json │ └── tailwind.config.js ├── tsconfig │ ├── base.json │ ├── nextjs.json │ ├── package.json │ └── react-library.json └── ui │ ├── .gitignore │ ├── components.json │ ├── index.ts │ ├── package.json │ ├── postcss.config.js │ ├── src │ ├── components │ │ ├── molecules │ │ │ └── skeleton │ │ │ │ └── posts.tsx │ │ └── ui │ │ │ ├── accordion.tsx │ │ │ ├── alert-dialog.tsx │ │ │ ├── alert.tsx │ │ │ ├── avatar.tsx │ │ │ ├── badge.tsx │ │ │ ├── breadcrumb.tsx │ │ │ ├── button.tsx │ │ │ ├── calendar.tsx │ │ │ ├── card.tsx │ │ │ ├── carousel.tsx │ │ │ ├── checkbox.tsx │ │ │ ├── collapsible.tsx │ │ │ ├── command.tsx │ │ │ ├── dialog.tsx │ │ │ ├── drawer.tsx │ │ │ ├── dropdown-menu.tsx │ │ │ ├── form.tsx │ │ │ ├── hover-card.tsx │ │ │ ├── input.tsx │ │ │ ├── label.tsx │ │ │ ├── loading-button.tsx │ │ │ ├── menubar.tsx │ │ │ ├── navigation-menu.tsx │ │ │ ├── pagination.tsx │ │ │ ├── popover.tsx │ │ │ ├── progress.tsx │ │ │ ├── radio-group.tsx │ │ │ ├── select.tsx │ │ │ ├── separator.tsx │ │ │ ├── skeleton.tsx │ │ │ ├── switch.tsx │ │ │ ├── table.tsx │ │ │ ├── tabs.tsx │ │ │ ├── textarea.tsx │ │ │ ├── toast.tsx │ │ │ ├── toaster.tsx │ │ │ ├── toggle-group.tsx │ │ │ ├── toggle.tsx │ │ │ ├── tooltip.tsx │ │ │ ├── typography.tsx │ │ │ └── use-toast.ts │ ├── globals.css │ └── lib │ │ └── utils.ts │ ├── tailwind.config.js │ ├── tsconfig.json │ └── tsup.config.ts ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── prettier.config.js ├── pull_request_template.md └── turbo.json /. babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "next/babel" 4 | ], 5 | "plugins": [] 6 | } 7 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | // This tells ESLint to load the config from the package `eslint-config-custom` 4 | extends: ["custom"], 5 | settings: { 6 | next: { 7 | rootDir: ["apps/*/", "packages/*/"], 8 | }, 9 | }, 10 | parserOptions: { 11 | babelOptions: { 12 | presets: [require.resolve('next/babel')], 13 | }, 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /.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 | build 15 | 16 | # misc 17 | .DS_Store 18 | *.pem 19 | 20 | # debug 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | # local env files 26 | .env 27 | .env.local 28 | .env.development.local 29 | .env.test.local 30 | .env.production.local 31 | 32 | # turbo 33 | .turbo 34 | 35 | # vercel 36 | .vercel 37 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | pnpm format -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | auto-install-peers = true 2 | public-hoist-pattern[] = *prisma* 3 | public-hoist-pattern[] = *next* -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | pnpm-lock.yaml 3 | .next 4 | .turbo 5 | dist -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 |

NEXT FORUM

5 | 6 |

7 | 8 | # About next-forum 9 | 10 | next-form is a minimal next-forum build with nextjs with AI 11 | 12 | # About technology 13 | 14 | - This project uses [Turborepo](https://turbo.build/) alongside [Next.js](https://nextjs.org/). 15 | - The database is powered by PostgreSQL, managed with [Prisma](https://www.prisma.io/). 16 | - The user interface is styled using [TailwindCSS](https://tailwindcss.com/) for utility-first CSS and [shadcn/ui](https://shadcn.dev/) for component design. 17 | 18 | # Libraries 19 | 20 | - ReactJS - 19. 21 | - TypeScript 22 | - NextJS 15. - App router and server actions 23 | - next-auth 5. 24 | - Prisma ORM 25 | - Postgres 26 | - Turborepo 27 | - TailwindCSS 28 | - shadcn 29 | - next-themes 30 | - Zod validation 31 | - React Form Hook 32 | - Tsup 33 | - EditorJs 34 | - react-toastify 35 | - react-textarea-autosize 36 | - lucide-react icon 37 | - dayjs 38 | - Eslint 39 | - Husky 40 | - Prettier 41 | 42 | # Installation 43 | 44 | Install 45 | 46 | ``` 47 | turbo install 48 | ``` 49 | 50 | In the `apps/web` folder, copy the env.example to env.local and enter the environment values 51 | 52 | In the `packages/database`, copy the env.example to .env and enter the DATABASE_URL 53 | 54 | Migration 55 | 56 | ``` 57 | db:migrate 58 | ``` 59 | 60 | Start 61 | 62 | ``` 63 | turbo dev 64 | ``` 65 | 66 | # Folder structure 67 | 68 | ## Front side functions 69 | 70 | - [x] Register by email or github 71 | - [x] Login by email, github or magic link 72 | - [x] User logout 73 | - [x] CRUD post 74 | - [x] List post: Search & filter by top or hot week, month, year, infinity 75 | - [x] Like post 76 | - [ ] Comment on post 77 | - [x] Share post 78 | - [x] Manage tag 79 | - [x] Follow user 80 | - [x] Multiple theme & dark mode or light mode 81 | - [x] Multiple language 82 | - [x] Follow tag 83 | - [x] Manage user profile 84 | - [ ] Multiple type: post/question 85 | 86 | ## Admin functions 87 | 88 | - [x] Dashboard 89 | - [x] CRUD tags 90 | - [ ] CRUD users 91 | - [ ] Manage posts 92 | - [ ] Manage images 93 | - [ ] Settings: Header/Menu 94 | - [ ] Manage roles and permission 95 | -------------------------------------------------------------------------------- /apps/docs/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: ["custom"], 4 | }; 5 | -------------------------------------------------------------------------------- /apps/docs/.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 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | -------------------------------------------------------------------------------- /apps/docs/README.md: -------------------------------------------------------------------------------- 1 | ## Getting Started 2 | 3 | First, run the development server: 4 | 5 | ```bash 6 | yarn dev 7 | ``` 8 | 9 | Open [http://localhost:3001](http://localhost:3001) with your browser to see the result. 10 | 11 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 12 | 13 | To create [API routes](https://nextjs.org/docs/app/building-your-application/routing/router-handlers) add an `api/` directory to the `app/` directory with a `route.ts` file. For individual endpoints, create a subfolder in the `api` directory, like `api/hello/route.ts` would map to [http://localhost:3001/api/hello](http://localhost:3001/api/hello). 14 | 15 | ## Learn More 16 | 17 | To learn more about Next.js, take a look at the following resources: 18 | 19 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 20 | - [Learn Next.js](https://nextjs.org/learn/foundations/about-nextjs) - an interactive Next.js tutorial. 21 | 22 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 23 | 24 | ## Deploy on Vercel 25 | 26 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_source=github.com&utm_medium=referral&utm_campaign=turborepo-readme) from the creators of Next.js. 27 | 28 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 29 | -------------------------------------------------------------------------------- /apps/docs/app/layout.tsx: -------------------------------------------------------------------------------- 1 | export default function RootLayout({ children }: { children: React.ReactNode }) { 2 | return ( 3 | 4 | {children} 5 | 6 | ) 7 | } 8 | -------------------------------------------------------------------------------- /apps/docs/app/page.tsx: -------------------------------------------------------------------------------- 1 | export default function Page() { 2 | return ( 3 | <> 4 |

Hello World

5 | 6 | ) 7 | } 8 | -------------------------------------------------------------------------------- /apps/docs/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /apps/docs/next.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | reactStrictMode: true, 3 | transpilePackages: ["ui"], 4 | }; 5 | -------------------------------------------------------------------------------- /apps/docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docs", 3 | "version": "1.0.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev --port 3001", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "next": "^13.4.1", 13 | "react": "^18.2.0", 14 | "react-dom": "^18.2.0", 15 | "ui": "workspace:*" 16 | }, 17 | "devDependencies": { 18 | "@types/node": "^17.0.12", 19 | "@types/react": "^18.0.22", 20 | "@types/react-dom": "^18.0.7", 21 | "eslint-config-custom": "workspace:*", 22 | "tsconfig": "workspace:*", 23 | "typescript": "^5.1.6" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /apps/docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tsconfig/nextjs.json", 3 | "compilerOptions": { 4 | "plugins": [ 5 | { 6 | "name": "next" 7 | } 8 | ] 9 | }, 10 | "include": [ 11 | "next-env.d.ts", 12 | "**/*.ts", 13 | "**/*.tsx", 14 | ".next/types/**/*.ts" 15 | ], 16 | "exclude": [ 17 | "node_modules" 18 | ] 19 | } -------------------------------------------------------------------------------- /apps/web/.env.example: -------------------------------------------------------------------------------- 1 | NEXTAUTH_SECRET= 2 | NEXTAUTH_URL= 3 | 4 | NEXT_PUBLIC_FRONTEND_URL= 5 | 6 | GITHUB_ID= 7 | GITHUB_SECRET= 8 | 9 | JWT_SECRET= 10 | 11 | NEXT_PUBLIC_FB_APP_ID= 12 | 13 | RESEND_AUDIENCE_ID= 14 | -------------------------------------------------------------------------------- /apps/web/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: ["custom"], 4 | parserOptions: { 5 | babelOptions: { 6 | presets: [require.resolve('next/babel')], 7 | }, 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /apps/web/.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 | /public/uploads/ 9 | 10 | # testing 11 | /coverage 12 | 13 | # next.js 14 | /.next/ 15 | /out/ 16 | 17 | # production 18 | /build 19 | 20 | # misc 21 | .DS_Store 22 | *.pem 23 | 24 | # debug 25 | npm-debug.log* 26 | yarn-debug.log* 27 | yarn-error.log* 28 | 29 | # local env files 30 | .env.local 31 | .env.development.local 32 | .env.test.local 33 | .env.production.local 34 | 35 | # vercel 36 | .vercel 37 | -------------------------------------------------------------------------------- /apps/web/README.md: -------------------------------------------------------------------------------- 1 | ## Getting Started 2 | 3 | First, run the development server: 4 | 5 | ```bash 6 | yarn dev 7 | ``` 8 | 9 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 10 | 11 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 12 | 13 | To create [API routes](https://nextjs.org/docs/app/building-your-application/routing/router-handlers) add an `api/` directory to the `app/` directory with a `route.ts` file. For individual endpoints, create a subfolder in the `api` directory, like `api/hello/route.ts` would map to [http://localhost:3000/api/hello](http://localhost:3000/api/hello). 14 | 15 | ## Learn More 16 | 17 | To learn more about Next.js, take a look at the following resources: 18 | 19 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 20 | - [Learn Next.js](https://nextjs.org/learn/foundations/about-nextjs) - an interactive Next.js tutorial. 21 | 22 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 23 | 24 | ## Deploy on Vercel 25 | 26 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_source=github.com&utm_medium=referral&utm_campaign=turborepo-readme) from the creators of Next.js. 27 | 28 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 29 | -------------------------------------------------------------------------------- /apps/web/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.js", 8 | "css": "app/globals.css", 9 | "baseColor": "slate", 10 | "cssVariables": true 11 | }, 12 | "aliases": { 13 | "ui": "@/components/ui", 14 | "components": "@/components", 15 | "utils": "../../lib/utils", 16 | "molecules": "@/components/molecules" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /apps/web/env.example: -------------------------------------------------------------------------------- 1 | NEXTAUTH_SECRET= 2 | NEXTAUTH_URL= 3 | 4 | NEXT_PUBLIC_FRONTEND_URL= 5 | 6 | GITHUB_ID= 7 | GITHUB_SECRET= 8 | 9 | JWT_SECRET= 10 | 11 | NEXT_PUBLIC_FB_APP_ID= -------------------------------------------------------------------------------- /apps/web/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. 6 | -------------------------------------------------------------------------------- /apps/web/next.config.mjs: -------------------------------------------------------------------------------- 1 | import createNextIntlPlugin from 'next-intl/plugin'; 2 | 3 | const withNextIntl = createNextIntlPlugin(); 4 | 5 | /** @type {import('next').NextConfig} */ 6 | const nextConfig = { 7 | reactStrictMode: true, 8 | transpilePackages: ["ui", "database"], 9 | images: { 10 | remotePatterns: [ 11 | { 12 | protocol: 'https', 13 | hostname: '**', 14 | }, 15 | { 16 | protocol: 'http', 17 | hostname: 'localhost', 18 | }, 19 | ], 20 | }, 21 | experimental: { 22 | turbo: true, 23 | }, 24 | webpack: (config) => { 25 | config.module.rules.push({ 26 | test: /\.svg$/i, 27 | use: ['@svgr/webpack'], 28 | }); 29 | return config; 30 | }, 31 | }; 32 | 33 | export default withNextIntl(nextConfig); 34 | 35 | -------------------------------------------------------------------------------- /apps/web/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | 'postcss-import': {}, 4 | 'tailwindcss/nesting': {}, 5 | tailwindcss: {}, 6 | autoprefixer: {}, 7 | }, 8 | }; -------------------------------------------------------------------------------- /apps/web/public/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luannguyenQV/turborepo-nextjs-prisma-postgres/0d0dbfa8a681ab25290f52462def1f44ab6875c5/apps/web/public/assets/logo.png -------------------------------------------------------------------------------- /apps/web/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luannguyenQV/turborepo-nextjs-prisma-postgres/0d0dbfa8a681ab25290f52462def1f44ab6875c5/apps/web/public/favicon.ico -------------------------------------------------------------------------------- /apps/web/public/uploads/2024-08-13/large-4c7e9cf4-b12f-4ae6-9576-12cb4484cd72.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luannguyenQV/turborepo-nextjs-prisma-postgres/0d0dbfa8a681ab25290f52462def1f44ab6875c5/apps/web/public/uploads/2024-08-13/large-4c7e9cf4-b12f-4ae6-9576-12cb4484cd72.jpg -------------------------------------------------------------------------------- /apps/web/public/uploads/2024-08-13/medium-4c7e9cf4-b12f-4ae6-9576-12cb4484cd72.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luannguyenQV/turborepo-nextjs-prisma-postgres/0d0dbfa8a681ab25290f52462def1f44ab6875c5/apps/web/public/uploads/2024-08-13/medium-4c7e9cf4-b12f-4ae6-9576-12cb4484cd72.jpg -------------------------------------------------------------------------------- /apps/web/public/uploads/2024-08-13/thumbnail-4c7e9cf4-b12f-4ae6-9576-12cb4484cd72.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luannguyenQV/turborepo-nextjs-prisma-postgres/0d0dbfa8a681ab25290f52462def1f44ab6875c5/apps/web/public/uploads/2024-08-13/thumbnail-4c7e9cf4-b12f-4ae6-9576-12cb4484cd72.jpg -------------------------------------------------------------------------------- /apps/web/src/actions/auth/index.ts: -------------------------------------------------------------------------------- 1 | "use server" 2 | 3 | import bcryptjs from "bcryptjs" 4 | import { Prisma } from "database" 5 | import { createUser } from "database/src/users/queries" 6 | import VerifyEmail from "emails/verify-email" 7 | 8 | import { signIn, signOut } from "@/configs/auth" 9 | import { sendEmail } from "@/emails" 10 | import { redirect } from "@/utils/navigation" 11 | 12 | import { SignUpDataOutput, signUpSchema } from "./type" 13 | 14 | export const signInWithCredentials = async (email: string, password: string) => { 15 | await signIn("credentials", { 16 | email, 17 | password, 18 | }) 19 | } 20 | 21 | export const signInWithGithub = async () => { 22 | await signIn("github") 23 | } 24 | 25 | export const onSignOut = async () => { 26 | await signOut({ 27 | redirectTo: "/", 28 | }) 29 | } 30 | 31 | // SIGN UP 32 | export const signUp = async ( 33 | data: Pick 34 | ): Promise => { 35 | try { 36 | // hash password 37 | const { email, password } = data 38 | const hashedPassword = await bcryptjs.hash(password, 10) 39 | 40 | await createUser({ 41 | email, 42 | password: hashedPassword, 43 | name: email.split("@")[0], 44 | }) 45 | 46 | // create verification code 47 | const token = crypto.randomUUID() 48 | await prisma.verificationToken.create({ 49 | data: { 50 | token, // 6 digits code 51 | identifier: email, 52 | expires: new Date(Date.now() + 1000 * 60 * 60 * 24), // 1 day 53 | }, 54 | }) 55 | 56 | // send email 57 | await sendEmail({ 58 | email, 59 | subject: "Welcome to Next Forum", 60 | react: VerifyEmail({ 61 | token, 62 | email, 63 | }), 64 | }) 65 | } catch (error) { 66 | if (error?.error?.code === "P2002") { 67 | return { 68 | formErrors: null, 69 | fieldErrors: { 70 | email: ["Email already exists"], // TODO: localize error message 71 | }, 72 | } 73 | } 74 | 75 | return { 76 | formErrors: [error?.message], 77 | fieldErrors: {}, 78 | } 79 | } 80 | 81 | // TODO: white this redirect not work 82 | redirect("/login") 83 | } 84 | -------------------------------------------------------------------------------- /apps/web/src/actions/auth/type.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod" 2 | 3 | export const signUpSchema = z.object({ 4 | email: z.string().email("Email is invalid"), 5 | password: z.string().min(8), 6 | confirmPassword: z.string().min(8), 7 | }) 8 | 9 | export type SignUpDataInput = z.infer 10 | 11 | export type SignUpDataOutput = z.inferFlattenedErrors 12 | -------------------------------------------------------------------------------- /apps/web/src/app/[lang]/(auth)/forgot-password/page.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | 3 | const ForgotPasswordPage = () => { 4 | return ( 5 |
6 |

Forgot Password

7 |
8 | 12 | 13 |
14 |
15 | ) 16 | } 17 | 18 | export default ForgotPasswordPage 19 | -------------------------------------------------------------------------------- /apps/web/src/app/[lang]/(auth)/reset-password/page.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | 3 | const ResetPasswordPage = () => { 4 | return ( 5 |
6 |

Forgot Password

7 |
8 | 12 | 13 |
14 |
15 | ) 16 | } 17 | 18 | export default ResetPasswordPage 19 | -------------------------------------------------------------------------------- /apps/web/src/app/[lang]/(auth)/signin/page.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { redirect } from "next/navigation" 3 | 4 | import { getTranslations } from "next-intl/server" 5 | 6 | import { auth } from "@/configs/auth" 7 | import SignIn from "@/molecules/auth/sign-in" 8 | 9 | export async function generateMetadata() { 10 | const t = await getTranslations() 11 | 12 | return { 13 | title: t("auth.sign_in.title"), 14 | description: t("auth.sign_in.description"), 15 | } 16 | } 17 | 18 | export default async function Page() { 19 | const session = await auth() 20 | 21 | if (session) { 22 | redirect("/") 23 | } 24 | 25 | return ( 26 |
27 | 28 |
29 | ) 30 | } 31 | -------------------------------------------------------------------------------- /apps/web/src/app/[lang]/(auth)/signup/page.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | 3 | import SignUp from "@/molecules/auth/sign-up" 4 | 5 | const RegisterPage = () => { 6 | return ( 7 |
8 | 9 |
10 | ) 11 | } 12 | 13 | export default RegisterPage 14 | -------------------------------------------------------------------------------- /apps/web/src/app/[lang]/(auth)/verify-email/index.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useSearchParams } from "next/navigation" 4 | 5 | export default function VerifyEmail() { 6 | const searchParams = useSearchParams() 7 | const code = searchParams.get("code") 8 | 9 | return
Verify Email
10 | } 11 | -------------------------------------------------------------------------------- /apps/web/src/app/[lang]/(protected)/layout.tsx: -------------------------------------------------------------------------------- 1 | import SidebarItem, { SidebarItemProps } from "@/molecules/sidebar-item" 2 | 3 | const SIDE_BAR = [ 4 | { 5 | label: "Profile", 6 | link: "/user/profile", 7 | }, 8 | { 9 | label: "Posts", 10 | link: "/user/posts", 11 | }, 12 | // { 13 | // label: "Password", 14 | // link: "/user/change-password", 15 | // }, 16 | // { 17 | // label: "Setting", 18 | // link: "/user/settings", 19 | // }, 20 | ] as Array 21 | 22 | export default async function ProtectedLayout({ children }: { children: React.ReactNode }) { 23 | return ( 24 |
25 |
26 | {SIDE_BAR.map((item) => ( 27 | 31 | ))} 32 |
33 |
{children}
34 |
35 | ) 36 | } 37 | -------------------------------------------------------------------------------- /apps/web/src/app/[lang]/(protected)/user/posts/page.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { Metadata } from "next/types" 3 | 4 | import { getUser } from "database" 5 | 6 | import { auth } from "@/configs/auth" 7 | import PageTitle from "@/molecules/page-title" 8 | 9 | export async function generateMetadata(): Promise { 10 | const session = await auth() 11 | const user = await getUser({ where: { id: session?.user?.id } }) 12 | 13 | return { 14 | title: `Posts - ${user?.data?.username}`, 15 | description: `Posts of ${user?.data?.username}`, 16 | } 17 | } 18 | 19 | export default async function Page({ searchParams }) { 20 | const session = await auth() 21 | 22 | return ( 23 |
24 | 25 | 26 |
27 |
TOTO
28 |
29 |
30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /apps/web/src/app/[lang]/(protected)/user/profile/page.tsx: -------------------------------------------------------------------------------- 1 | import { auth } from "@/configs/auth" 2 | import APP_APIS from "@/constants/apis" 3 | import PageTitle from "@/molecules/page-title" 4 | import Profile from "@/molecules/profile" 5 | import { generatePath } from "@/utils/generatePath" 6 | 7 | export default async function Page() { 8 | let currentUser = null 9 | try { 10 | const session = await auth() 11 | 12 | const userRaw = await fetch( 13 | `${process.env.NEXT_PUBLIC_FRONTEND_URL}${generatePath(APP_APIS.protected.user.GET, { 14 | userId: session?.user?.id, 15 | })}`, 16 | { 17 | cache: "no-cache", 18 | } 19 | ) 20 | 21 | currentUser = await userRaw.json() 22 | } catch (error) { 23 | // 24 | } 25 | 26 | return ( 27 |
28 | 32 | 33 | 34 |
35 | ) 36 | } 37 | -------------------------------------------------------------------------------- /apps/web/src/app/[lang]/(protected)/user/settings/page.tsx: -------------------------------------------------------------------------------- 1 | export default function Page() { 2 | return
Setting
3 | } 4 | -------------------------------------------------------------------------------- /apps/web/src/app/[lang]/(protected-post)/layout.tsx: -------------------------------------------------------------------------------- 1 | export default async function PublicLayout({ children }: { children: React.ReactNode }) { 2 | return
{children}
3 | } 4 | -------------------------------------------------------------------------------- /apps/web/src/app/[lang]/(protected-post)/user/posts/[postId]/edit/page.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from "next" 2 | 3 | import { findPostBySlugOrId } from "database" 4 | 5 | import PostForm from "@/molecules/post-form" 6 | 7 | export async function generateMetadata(props): Promise { 8 | const params = await props.params 9 | const post = await findPostBySlugOrId(params?.postId) 10 | 11 | return { 12 | title: post?.data?.title, 13 | description: "", // post?.content.slice(0, 160), 14 | } 15 | } 16 | 17 | export default async function Page(props: { params: Promise<{ postId: string }> }) { 18 | const params = await props.params 19 | const post = await findPostBySlugOrId(params?.postId) 20 | 21 | return 22 | } 23 | -------------------------------------------------------------------------------- /apps/web/src/app/[lang]/(protected-post)/user/posts/create/page.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from "next/navigation" 2 | 3 | import { auth } from "@/configs/auth" 4 | import APP_ROUTES from "@/constants/routes" 5 | import PostForm from "@/molecules/post-form" 6 | 7 | export const metadata = { 8 | title: "Create Post", 9 | description: "Create a new post", 10 | } 11 | 12 | export default async function Page() { 13 | const session = await auth() 14 | 15 | if (!session) { 16 | redirect(APP_ROUTES.LOGIN) 17 | } 18 | 19 | return 20 | } 21 | -------------------------------------------------------------------------------- /apps/web/src/app/[lang]/(protected-post)/user/posts/loading.tsx: -------------------------------------------------------------------------------- 1 | import { Skeleton } from "ui" 2 | 3 | export default function UserPostLoading() { 4 | return ( 5 |
6 |
7 | 8 |
9 |
10 | {Array.from({ length: 5 }).map((_, i) => ( 11 |
15 | 16 |
17 | 18 | 19 |
20 |
21 | ))} 22 |
23 |
24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /apps/web/src/app/[lang]/(public)/about-us/page.tsx: -------------------------------------------------------------------------------- 1 | export default async function AboutUs() { 2 | return
About
3 | } 4 | -------------------------------------------------------------------------------- /apps/web/src/app/[lang]/(public)/contact/page.tsx: -------------------------------------------------------------------------------- 1 | import ContactForm from "@/molecules/contact-form/contact-form" 2 | import PageTitle from "@/molecules/page-title" 3 | 4 | export default async function ContactUs() { 5 | return ( 6 |
7 | 11 | 12 | 13 |
14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /apps/web/src/app/[lang]/(public)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { BookCopy, HomeIcon, Smartphone, TagIcon } from "lucide-react" 2 | import { useTranslations } from "next-intl" 3 | 4 | import SidebarItem, { SidebarItemProps } from "@/molecules/sidebar-item" 5 | import SquareAdvertisement from "@/molecules/square-advertisement" 6 | import TopTag from "@/molecules/top-tags" 7 | 8 | export default function PublicLayout({ children }: { children: React.ReactNode }) { 9 | const t = useTranslations("common") 10 | 11 | const SIDE_BAR = [ 12 | { 13 | label: t("home"), 14 | link: "/", 15 | icons: , 16 | }, 17 | { 18 | label: t("tags"), 19 | link: "/tags", 20 | icons: , 21 | }, 22 | { 23 | label: t("contact"), 24 | link: "/contact", 25 | icons: , 26 | }, 27 | { 28 | label: t("about"), 29 | link: "/about-us", 30 | icons: , 31 | }, 32 | ] as Array 33 | 34 | return ( 35 |
36 |
37 | {SIDE_BAR.map((item) => ( 38 | 42 | ))} 43 | 44 | {/* */} 45 |
46 |
{children}
47 |
48 | 49 |
50 |
51 | ) 52 | } 53 | -------------------------------------------------------------------------------- /apps/web/src/app/[lang]/(public)/loading.tsx: -------------------------------------------------------------------------------- 1 | export default function Loading() { 2 | return
Loading...
3 | } 4 | -------------------------------------------------------------------------------- /apps/web/src/app/[lang]/(public)/page.tsx: -------------------------------------------------------------------------------- 1 | import { Suspense } from "react" 2 | import { Metadata } from "next" 3 | 4 | import { Typography } from "ui" 5 | 6 | import PostPagination from "@/molecules/posts/post-pagination" 7 | 8 | export const metadata: Metadata = { 9 | title: "Home", 10 | description: "Welcome to our community forum", 11 | } 12 | 13 | export default function HomePage() { 14 | return ( 15 |
16 | Loading posts...
}> 17 | 18 | 19 | 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /apps/web/src/app/[lang]/(public)/search/page.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from "next" 2 | 3 | import { getTranslations } from "next-intl/server" 4 | import { Typography } from "ui" 5 | 6 | import Filter from "@/molecules/home/filter" 7 | import SearchBar from "@/molecules/nav/search-bar" 8 | import PostList from "@/molecules/posts/post-list" 9 | 10 | export async function generateMetadata(props): Promise { 11 | const searchParams = await props.searchParams 12 | return { 13 | title: `${searchParams?.search} - Search results`, 14 | description: `Search results for "${searchParams?.search}"`, 15 | } 16 | } 17 | 18 | export default async function Page(props) { 19 | const searchParams = await props.searchParams 20 | const t = await getTranslations({ 21 | namespace: "common", 22 | }) 23 | 24 | return ( 25 |
26 | 30 | {t("search_results_for")} 31 | {`"${searchParams?.search}"`} 32 | 33 | 34 | 35 | 36 | 37 | 38 | 43 |
44 | ) 45 | } 46 | -------------------------------------------------------------------------------- /apps/web/src/app/[lang]/(public)/tags/page.tsx: -------------------------------------------------------------------------------- 1 | import PageTitle from "@/molecules/page-title" 2 | import Filter from "@/molecules/tag/filter" 3 | import TagList from "@/molecules/tag/tag-list" 4 | 5 | export const metadata = { 6 | title: "Tags", 7 | description: 8 | "A tag is a keyword or label that categorizes your question with other, similar questions. Using the right tags makes it easier for others to find and answer your question.", 9 | } 10 | 11 | export default async function Page() { 12 | return ( 13 |
14 | 18 | 19 | 20 | 21 | 22 |
23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /apps/web/src/app/[lang]/(public-fullwidth)/author/[authorId]/followers/page.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from "next" 2 | 3 | import APP_APIS from "@/constants/apis" 4 | import Followers from "@/molecules/follower/followers" 5 | import UserProfile from "@/molecules/follower/user-profile" 6 | import { TUserItem } from "@/types/users" 7 | import { generatePath } from "@/utils/generatePath" 8 | 9 | export async function generateMetadata(props: { 10 | params: Promise<{ authorId: string }> 11 | }): Promise { 12 | const params = await props.params 13 | const rawAuthor = await fetch( 14 | `${process.env.NEXT_PUBLIC_FRONTEND_URL}${generatePath(APP_APIS.protected.user.GET, { 15 | userId: params?.authorId, 16 | })}`, 17 | { 18 | method: "GET", 19 | cache: "no-cache", 20 | headers: { 21 | "Content-Type": "application/json", 22 | }, 23 | } 24 | ) 25 | const author: TUserItem = await rawAuthor?.json() 26 | 27 | return { 28 | title: `${author.name} - Followers`, 29 | description: author.bio, 30 | } 31 | } 32 | 33 | export default async function Page(props: { params: Promise<{ authorId: string }> }) { 34 | const params = await props.params 35 | return ( 36 |
37 | 38 | 39 | 40 |
41 | ) 42 | } 43 | -------------------------------------------------------------------------------- /apps/web/src/app/[lang]/(public-fullwidth)/author/[authorId]/page.tsx: -------------------------------------------------------------------------------- 1 | import { getUser } from "database" 2 | 3 | import UserProfile from "@/molecules/follower/user-profile" 4 | import PostList from "@/molecules/posts/post-list" 5 | 6 | export const generateMetadata = async (props) => { 7 | const params = await props.params 8 | const { data: author, error } = await getUser({ 9 | where: { 10 | id: params?.authorId, 11 | }, 12 | }) 13 | 14 | return { 15 | title: author?.name, 16 | description: author?.bio, 17 | } 18 | } 19 | 20 | export default async function Page(props) { 21 | const searchParams = await props.searchParams 22 | const params = await props.params 23 | return ( 24 |
25 | 26 | 33 |
34 | ) 35 | } 36 | -------------------------------------------------------------------------------- /apps/web/src/app/[lang]/(public-fullwidth)/posts/[postId]/page.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from "next" 2 | import { notFound } from "next/navigation" 3 | 4 | import PostDetail from "@/molecules/posts/post-detail" 5 | import Comments from "@/molecules/posts/post-detail/comments" 6 | import LikeButton from "@/molecules/posts/post-detail/like-button" 7 | import TableOfContents from "@/molecules/posts/post-detail/table-of-contents" 8 | import BookmarkButton from "@/molecules/posts/post-item/bookmark-button" 9 | 10 | import "./tocbot.css" 11 | 12 | import { findPostBySlugOrId, PostStatus } from "database" 13 | 14 | import { auth } from "@/configs/auth" 15 | import { TSearchParams } from "@/types" 16 | 17 | export async function generateMetadata(props): Promise { 18 | const params = await props.params 19 | const post = await findPostBySlugOrId(params?.postId) 20 | 21 | return { 22 | title: post?.data?.title, 23 | description: "", //post?.content.slice(0, 160), 24 | } 25 | } 26 | 27 | export default async function Page(props: { 28 | params: Promise<{ postId: string }> 29 | searchParams: Promise 30 | }) { 31 | const searchParams = await props.searchParams 32 | const params = await props.params 33 | const post = await findPostBySlugOrId(params?.postId) 34 | const session = await auth() 35 | 36 | if ( 37 | !post || 38 | (post.data?.postStatus === PostStatus.DRAFT && session?.user?.id !== post?.data?.author?.id) 39 | ) { 40 | return notFound() 41 | } 42 | 43 | return ( 44 |
45 |
46 |
47 | 48 | 52 |
53 | 54 |
55 | 56 | 60 |
61 |
62 | 63 |
64 | 65 |
66 |
67 | ) 68 | } 69 | -------------------------------------------------------------------------------- /apps/web/src/app/[lang]/(public-fullwidth)/posts/[postId]/tocbot.css: -------------------------------------------------------------------------------- 1 | .toc { 2 | overflow-y: auto 3 | } 4 | 5 | .toc>.toc-list { 6 | overflow: hidden; 7 | position: relative 8 | } 9 | 10 | .toc>.toc-list li { 11 | list-style: none 12 | } 13 | 14 | .js-toc { 15 | overflow-y: hidden 16 | } 17 | 18 | .toc-list { 19 | margin: 0; 20 | padding-left: 10px 21 | } 22 | 23 | a.toc-link { 24 | color: currentColor; 25 | height: 100%; 26 | height: 32px; 27 | display: flex; 28 | border-radius: 4px; 29 | justify-content: flex-start; 30 | align-items: center; 31 | padding: 0 8px; 32 | } 33 | 34 | a.toc-link:hover { 35 | /* background-color: #f5f5f5; */ 36 | color: #000; 37 | } 38 | 39 | .is-collapsible { 40 | max-height: 1000px; 41 | overflow: hidden; 42 | transition: all 300ms ease-in-out 43 | } 44 | 45 | .is-collapsed { 46 | max-height: 0 47 | } 48 | 49 | .is-position-fixed { 50 | position: fixed !important; 51 | top: 0 52 | } 53 | 54 | .is-active-link { 55 | font-weight: 700; 56 | /* background-color: #f5f5f5; */ 57 | } 58 | 59 | .toc-link::before { 60 | content: ' '; 61 | display: inline-block; 62 | height: inherit; 63 | left: 0; 64 | margin-top: -1px; 65 | position: absolute; 66 | width: 2px 67 | } 68 | 69 | /* .is-active-link::before { 70 | background-color: #54BC4B 71 | } */ -------------------------------------------------------------------------------- /apps/web/src/app/[lang]/(public-fullwidth)/posts/page.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from "next/navigation" 2 | 3 | export default async function Page() { 4 | redirect("/") 5 | 6 | return null 7 | } 8 | -------------------------------------------------------------------------------- /apps/web/src/app/[lang]/(public-fullwidth)/tags/[tagId]/follower/page.tsx: -------------------------------------------------------------------------------- 1 | // import { getTagById } from "@/actions/public/tags" 2 | import TagDetail from "@/molecules/tag/tag-detail" 3 | 4 | export const metadata = { 5 | title: "Tags", 6 | description: "A list of tags used in the blog posts", 7 | } 8 | 9 | export default async function Page(props: { params: Promise<{ tagId: string }> }) { 10 | const params = await props.params 11 | // const tag = await getTagById(params?.tagId as string) 12 | 13 | return
{/* */}
14 | } 15 | -------------------------------------------------------------------------------- /apps/web/src/app/[lang]/(public-fullwidth)/tags/[tagId]/page.tsx: -------------------------------------------------------------------------------- 1 | import { getTag } from "database" 2 | 3 | import PostList from "@/molecules/posts/post-list" 4 | import TagDetail from "@/molecules/tag/tag-detail" 5 | 6 | export const generateMetadata = async (props) => { 7 | const params = await props.params 8 | const { data: tag, error } = await getTag({ 9 | tagIdOrSlug: params?.tagId, 10 | }) 11 | 12 | return { 13 | title: tag?.name, 14 | description: tag?.description, 15 | } 16 | } 17 | 18 | export default async function Page(props) { 19 | const params = await props.params 20 | return ( 21 |
22 | 23 | 24 | 30 |
31 | ) 32 | } 33 | -------------------------------------------------------------------------------- /apps/web/src/app/[lang]/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0 0% 100%; 8 | --foreground: 222.2 47.4% 11.2%; 9 | 10 | --muted: 210 40% 96.1%; 11 | --muted-foreground: 215.4 16.3% 46.9%; 12 | 13 | --popover: 0 0% 100%; 14 | --popover-foreground: 222.2 47.4% 11.2%; 15 | 16 | --border: 214.3 31.8% 91.4%; 17 | --input: 214.3 31.8% 91.4%; 18 | 19 | --card: 0 0% 100%; 20 | --card-foreground: 222.2 47.4% 11.2%; 21 | 22 | --primary: 222.2 47.4% 11.2%; 23 | --primary-foreground: 210 40% 98%; 24 | 25 | --secondary: 210 40% 96.1%; 26 | --secondary-foreground: 222.2 47.4% 11.2%; 27 | 28 | --accent: 210 40% 96.1%; 29 | --accent-foreground: 222.2 47.4% 11.2%; 30 | 31 | --destructive: 0 100% 50%; 32 | --destructive-foreground: 210 40% 98%; 33 | 34 | --ring: 215 20.2% 65.1%; 35 | 36 | --radius: 0.5rem; 37 | } 38 | 39 | .dark { 40 | --background: 224 71% 4%; 41 | --foreground: 213 31% 91%; 42 | 43 | --muted: 223 47% 11%; 44 | --muted-foreground: 215.4 16.3% 56.9%; 45 | 46 | --accent: 216 34% 17%; 47 | --accent-foreground: 210 40% 98%; 48 | 49 | --popover: 224 71% 4%; 50 | --popover-foreground: 215 20.2% 65.1%; 51 | 52 | --border: 216 34% 17%; 53 | --input: 216 34% 17%; 54 | 55 | --card: 224 71% 4%; 56 | --card-foreground: 213 31% 91%; 57 | 58 | --primary: 210 40% 98%; 59 | --primary-foreground: 222.2 47.4% 1.2%; 60 | 61 | --secondary: 222.2 47.4% 11.2%; 62 | --secondary-foreground: 210 40% 98%; 63 | 64 | --destructive: 0 63% 31%; 65 | --destructive-foreground: 210 40% 98%; 66 | 67 | --ring: 216 34% 17%; 68 | 69 | --radius: 0.5rem; 70 | } 71 | } 72 | 73 | @layer base { 74 | * { 75 | @apply border-border; 76 | } 77 | 78 | body { 79 | @apply bg-background text-foreground; 80 | font-feature-settings: 81 | "rlig" 1, 82 | "calt" 1; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /apps/web/src/app/[lang]/layout.tsx: -------------------------------------------------------------------------------- 1 | import { use } from "react" 2 | 3 | import "./globals.css" 4 | import "ui/dist/index.css" 5 | import "react-toastify/dist/ReactToastify.css" 6 | 7 | import { NextIntlClientProvider, useMessages } from "next-intl" 8 | import { ToastContainer } from "react-toastify" 9 | 10 | import Footer from "@/molecules/footer" 11 | import Nav from "@/molecules/nav" 12 | import { Providers } from "@/providers" 13 | 14 | export const metadata = { 15 | icons: { 16 | icon: "/assets/logo.png", 17 | }, 18 | } 19 | 20 | export default function RootLayout(props: { 21 | params: Promise<{ lang: string }> 22 | children: React.ReactNode 23 | }) { 24 | const params = use(props.params) 25 | 26 | const { lang } = params 27 | 28 | const { children } = props 29 | 30 | const messages = useMessages() 31 | 32 | return ( 33 | 37 | 38 | 42 | 43 |