├── .gitignore ├── .vscode └── settings.json ├── README.md ├── app ├── (auth) │ ├── layout.tsx │ ├── sign-in │ │ └── page.tsx │ └── sign-up │ │ └── page.tsx ├── (root) │ ├── ask-question │ │ └── page.tsx │ ├── collection │ │ └── page.tsx │ ├── community │ │ ├── error.tsx │ │ ├── loading.tsx │ │ └── page.tsx │ ├── jobs │ │ └── page.tsx │ ├── layout.tsx │ ├── page.tsx │ ├── profile │ │ ├── [id] │ │ │ └── page.tsx │ │ └── edit │ │ │ └── page.tsx │ ├── questions │ │ └── [id] │ │ │ ├── edit │ │ │ └── page.tsx │ │ │ └── page.tsx │ └── tags │ │ ├── [id] │ │ └── page.tsx │ │ └── page.tsx ├── api │ ├── accounts │ │ ├── [id] │ │ │ └── route.ts │ │ ├── provider │ │ │ └── route.ts │ │ └── route.ts │ ├── ai │ │ └── answers │ │ │ └── route.ts │ ├── auth │ │ ├── [...nextauth] │ │ │ └── route.ts │ │ └── signin-with-oauth │ │ │ └── route.ts │ └── users │ │ ├── [id] │ │ └── route.ts │ │ ├── email │ │ └── route.ts │ │ └── route.ts ├── favicon.ico ├── fonts │ ├── InterVF.ttf │ └── SpaceGroteskVF.ttf ├── globals.css └── layout.tsx ├── auth.ts ├── components.json ├── components ├── DataRenderer.tsx ├── GlobalResult.tsx ├── Metric.tsx ├── Pagination.tsx ├── UserAvatar.tsx ├── answers │ └── AllAnswers.tsx ├── cards │ ├── AnswerCard.tsx │ ├── JobCard.tsx │ ├── QuestionCard.tsx │ ├── TagCard.tsx │ └── UserCard.tsx ├── editor │ ├── Preview.tsx │ ├── dark-editor.css │ ├── index.tsx │ └── question.mdx ├── filters │ ├── CommonFilter.tsx │ ├── GlobalFilter.tsx │ ├── HomeFilter.tsx │ └── JobFilter.tsx ├── forms │ ├── AnswerForm.tsx │ ├── AuthForm.tsx │ ├── ProfileForm.tsx │ ├── QuestionForm.tsx │ └── SocialAuthForm.tsx ├── navigation │ ├── LeftSidebar.tsx │ ├── RightSidebar.tsx │ └── navbar │ │ ├── MobileNavigation.tsx │ │ ├── NavLinks.tsx │ │ ├── Theme.tsx │ │ └── index.tsx ├── questions │ └── SaveQuestion.tsx ├── search │ ├── GlobalSearch.tsx │ └── LocalSearch.tsx ├── ui │ ├── alert-dialog.tsx │ ├── avatar.tsx │ ├── badge.tsx │ ├── button.tsx │ ├── dropdown-menu.tsx │ ├── form.tsx │ ├── input.tsx │ ├── label.tsx │ ├── select.tsx │ ├── sheet.tsx │ ├── skeleton.tsx │ ├── tabs.tsx │ ├── textarea.tsx │ ├── toast.tsx │ └── toaster.tsx ├── user │ ├── EditDeleteAction.tsx │ ├── ProfileLink.tsx │ └── Stats.tsx └── votes │ └── Votes.tsx ├── constants ├── filters.ts ├── index.ts ├── routes.ts ├── states.ts └── techMap.ts ├── context └── Theme.tsx ├── database ├── account.model.ts ├── answer.model.ts ├── collection.model.ts ├── index.ts ├── interaction.model.ts ├── question.model.ts ├── tag-question.model.ts ├── tag.model.ts ├── user.model.ts └── vote.model.ts ├── eslint.config.mjs ├── hooks └── use-toast.ts ├── lib ├── actions │ ├── answer.action.ts │ ├── auth.action.ts │ ├── collection.action.ts │ ├── general.action.ts │ ├── interaction.action.ts │ ├── job.action.ts │ ├── question.action.ts │ ├── tag.action.ts │ ├── tag.actions.ts │ ├── user.action.ts │ └── vote.action.ts ├── api.ts ├── handlers │ ├── action.ts │ ├── error.ts │ └── fetch.ts ├── http-errors.ts ├── logger.ts ├── mongoose.ts ├── url.ts ├── utils.ts └── validations.ts ├── middleware.ts ├── next.config.ts ├── package-lock.json ├── package.json ├── postcss.config.mjs ├── public ├── file.svg ├── globe.svg ├── icons │ ├── account.svg │ ├── arrow-left.svg │ ├── arrow-right.svg │ ├── arrow-up-right.svg │ ├── au.svg │ ├── avatar.svg │ ├── bronze-medal.svg │ ├── calendar.svg │ ├── carbon-location.svg │ ├── chevron-down.svg │ ├── chevron-right.svg │ ├── clock-2.svg │ ├── clock.svg │ ├── close.svg │ ├── computer.svg │ ├── currency-dollar-circle.svg │ ├── downvote.svg │ ├── downvoted.svg │ ├── edit.svg │ ├── eye.svg │ ├── github.svg │ ├── gold-medal.svg │ ├── google.svg │ ├── hamburger.svg │ ├── home.svg │ ├── job-search.svg │ ├── like.svg │ ├── link.svg │ ├── location.svg │ ├── message.svg │ ├── mingcute-down-line.svg │ ├── moon.svg │ ├── question.svg │ ├── search.svg │ ├── sign-up.svg │ ├── silver-medal.svg │ ├── star-filled.svg │ ├── star-red.svg │ ├── star.svg │ ├── stars.svg │ ├── suitcase.svg │ ├── sun.svg │ ├── tag.svg │ ├── trash.svg │ ├── upvote.svg │ ├── upvoted.svg │ ├── user.svg │ └── users.svg ├── images │ ├── auth-dark.png │ ├── auth-light.png │ ├── dark-error.png │ ├── dark-illustration.png │ ├── default-logo.svg │ ├── light-error.png │ ├── light-illustration.png │ ├── logo-dark.svg │ ├── logo-light.svg │ ├── logo.png │ └── site-logo.svg ├── next.svg ├── vercel.svg └── window.svg ├── tailwind.config.ts ├── tsconfig.json └── types ├── action.d.ts └── global.d.ts /.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.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | 32 | # env files (can opt-in for commiting if needed) 33 | .env* 34 | 35 | # vercel 36 | .vercel 37 | 38 | # typescript 39 | *.tsbuildinfo 40 | next-env.d.ts 41 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.defaultFormatter": "esbenp.prettier-vscode", 3 | "editor.formatOnSave": true, 4 | "editor.codeActionsOnSave": { 5 | "source.fixAll.eslint": "explicit", 6 | "source.addMissingImports": "explicit" 7 | }, 8 | "prettier.tabWidth": 2, 9 | "prettier.useTabs": false, 10 | "prettier.semi": true, 11 | "prettier.singleQuote": false, 12 | "prettier.jsxSingleQuote": false, 13 | "prettier.trailingComma": "es5", 14 | "prettier.arrowParens": "always", 15 | "[json]": { 16 | "editor.defaultFormatter": "esbenp.prettier-vscode" 17 | }, 18 | "[typescript]": { 19 | "editor.defaultFormatter": "esbenp.prettier-vscode" 20 | }, 21 | "[typescriptreact]": { 22 | "editor.defaultFormatter": "esbenp.prettier-vscode" 23 | }, 24 | "[javascriptreact]": { 25 | "editor.defaultFormatter": "esbenp.prettier-vscode" 26 | }, 27 | "typescript.tsdk": "node_modules/typescript/lib" 28 | } 29 | -------------------------------------------------------------------------------- /app/(auth)/layout.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | import { ReactNode } from "react"; 3 | 4 | import SocialAuthForm from "@/components/forms/SocialAuthForm"; 5 | 6 | const AuthLayout = ({ children }: { children: ReactNode }) => { 7 | return ( 8 |
9 |
10 |
11 |
12 |

Join DevFlow

13 |

14 | To get your questions answered 15 |

16 |
17 | DevFlow Logo 24 |
25 | 26 | {children} 27 | 28 | 29 |
30 |
31 | ); 32 | }; 33 | 34 | export default AuthLayout; 35 | -------------------------------------------------------------------------------- /app/(auth)/sign-in/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | 5 | import AuthForm from "@/components/forms/AuthForm"; 6 | import { signInWithCredentials } from "@/lib/actions/auth.action"; 7 | import { SignInSchema } from "@/lib/validations"; 8 | 9 | const SignIn = () => { 10 | return ( 11 | 17 | ); 18 | }; 19 | 20 | export default SignIn; 21 | -------------------------------------------------------------------------------- /app/(auth)/sign-up/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | 5 | import AuthForm from "@/components/forms/AuthForm"; 6 | import { signUpWithCredentials } from "@/lib/actions/auth.action"; 7 | import { SignUpSchema } from "@/lib/validations"; 8 | 9 | const SignUp = () => { 10 | return ( 11 | 17 | ); 18 | }; 19 | 20 | export default SignUp; 21 | -------------------------------------------------------------------------------- /app/(root)/ask-question/page.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from "next/navigation"; 2 | import React from "react"; 3 | 4 | import { auth } from "@/auth"; 5 | import QuestionForm from "@/components/forms/QuestionForm"; 6 | 7 | const AskQuestion = async () => { 8 | const session = await auth(); 9 | 10 | if (!session) return redirect("/sign-in"); 11 | 12 | return ( 13 | <> 14 |

Ask a question

15 | 16 |
17 | 18 |
19 | 20 | ); 21 | }; 22 | 23 | export default AskQuestion; 24 | -------------------------------------------------------------------------------- /app/(root)/collection/page.tsx: -------------------------------------------------------------------------------- 1 | import QuestionCard from "@/components/cards/QuestionCard"; 2 | import DataRenderer from "@/components/DataRenderer"; 3 | import CommonFilter from "@/components/filters/CommonFilter"; 4 | import Pagination from "@/components/Pagination"; 5 | import LocalSearch from "@/components/search/LocalSearch"; 6 | import { CollectionFilters } from "@/constants/filters"; 7 | import ROUTES from "@/constants/routes"; 8 | import { EMPTY_QUESTION } from "@/constants/states"; 9 | import { getSavedQuestions } from "@/lib/actions/collection.action"; 10 | 11 | interface SearchParams { 12 | searchParams: Promise<{ [key: string]: string }>; 13 | } 14 | 15 | const Collections = async ({ searchParams }: SearchParams) => { 16 | const { page, pageSize, query, filter } = await searchParams; 17 | 18 | const { success, data, error } = await getSavedQuestions({ 19 | page: Number(page) || 1, 20 | pageSize: Number(pageSize) || 10, 21 | query: query || "", 22 | filter: filter || "", 23 | }); 24 | 25 | const { collection, isNext } = data || {}; 26 | 27 | return ( 28 | <> 29 |

Saved Questions

30 | 31 |
32 | 38 | 39 | 43 |
44 | 45 | ( 51 |
52 | {collection.map((item) => ( 53 | 54 | ))} 55 |
56 | )} 57 | /> 58 | 59 | 60 | 61 | ); 62 | }; 63 | 64 | export default Collections; 65 | -------------------------------------------------------------------------------- /app/(root)/community/error.tsx: -------------------------------------------------------------------------------- 1 | "use client"; // Error boundaries must be Client Components 2 | 3 | import { useEffect } from "react"; 4 | 5 | export default function Error({ 6 | error, 7 | reset, 8 | }: { 9 | error: Error & { digest?: string }; 10 | reset: () => void; 11 | }) { 12 | useEffect(() => { 13 | // Log the error to an error reporting service 14 | console.error(error); 15 | }, [error]); 16 | 17 | return ( 18 |
19 |

Something went wrong!

20 | 28 |
29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /app/(root)/community/loading.tsx: -------------------------------------------------------------------------------- 1 | import { Skeleton } from "@/components/ui/skeleton"; 2 | 3 | const Loading = () => { 4 | return ( 5 |
6 |

All Users

7 | 8 |
9 | 10 | 11 |
12 | 13 |
14 | {[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((item) => ( 15 | 19 | ))} 20 |
21 |
22 | ); 23 | }; 24 | 25 | export default Loading; 26 | -------------------------------------------------------------------------------- /app/(root)/community/page.tsx: -------------------------------------------------------------------------------- 1 | import UserCard from "@/components/cards/UserCard"; 2 | import DataRenderer from "@/components/DataRenderer"; 3 | import CommonFilter from "@/components/filters/CommonFilter"; 4 | import Pagination from "@/components/Pagination"; 5 | import LocalSearch from "@/components/search/LocalSearch"; 6 | import { UserFilters } from "@/constants/filters"; 7 | import ROUTES from "@/constants/routes"; 8 | import { EMPTY_USERS } from "@/constants/states"; 9 | import { getUsers } from "@/lib/actions/user.action"; 10 | 11 | const Community = async ({ searchParams }: RouteParams) => { 12 | const { page, pageSize, query, filter } = await searchParams; 13 | 14 | const { success, data, error } = await getUsers({ 15 | page: Number(page) || 1, 16 | pageSize: Number(pageSize) || 10, 17 | query, 18 | filter, 19 | }); 20 | 21 | const { users, isNext } = data || {}; 22 | 23 | return ( 24 |
25 |

All Users

26 | 27 |
28 | 35 | 36 | 40 |
41 | 42 | ( 48 |
49 | {users.map((user) => ( 50 | 51 | ))} 52 |
53 | )} 54 | /> 55 | 56 | 57 |
58 | ); 59 | }; 60 | 61 | export default Community; 62 | -------------------------------------------------------------------------------- /app/(root)/jobs/page.tsx: -------------------------------------------------------------------------------- 1 | import JobCard from "@/components/cards/JobCard"; 2 | import JobsFilter from "@/components/filters/JobFilter"; 3 | import Pagination from "@/components/Pagination"; 4 | import { 5 | fetchCountries, 6 | fetchJobs, 7 | fetchLocation, 8 | } from "@/lib/actions/job.action"; 9 | 10 | const Page = async ({ searchParams }: RouteParams) => { 11 | const { query, location, page } = await searchParams; 12 | const userLocation = await fetchLocation(); 13 | 14 | const jobs = await fetchJobs({ 15 | query: `${query}, ${location}` || `Software Engineer in ${userLocation}`, 16 | page: page ?? 1, 17 | }); 18 | 19 | const countries = await fetchCountries(); 20 | const parsedPage = parseInt(page ?? 1); 21 | 22 | console.log(jobs); 23 | 24 | return ( 25 | <> 26 |

Jobs

27 | 28 |
29 | 30 |
31 | 32 |
33 | {jobs?.length > 0 ? ( 34 | jobs 35 | ?.filter((job: Job) => job.job_title) 36 | .map((job: Job) => ) 37 | ) : ( 38 |
39 | Oops! We couldn't find any jobs at the moment. Please try again 40 | later 41 |
42 | )} 43 |
44 | 45 | {jobs?.length > 0 && ( 46 | 47 | )} 48 | 49 | ); 50 | }; 51 | 52 | export default Page; 53 | -------------------------------------------------------------------------------- /app/(root)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react"; 2 | 3 | import Navbar from "@/components/navigation/navbar"; 4 | import LeftSidebar from "@/components/navigation/LeftSidebar"; 5 | import RightSidebar from "@/components/navigation/RightSidebar"; 6 | 7 | const RootLayout = ({ children }: { children: ReactNode }) => { 8 | return ( 9 |
10 | 11 | 12 |
13 | 14 | 15 |
16 |
{children}
17 |
18 | 19 | 20 |
21 |
22 | ); 23 | }; 24 | 25 | export default RootLayout; 26 | -------------------------------------------------------------------------------- /app/(root)/page.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from "next"; 2 | import Link from "next/link"; 3 | 4 | import QuestionCard from "@/components/cards/QuestionCard"; 5 | import DataRenderer from "@/components/DataRenderer"; 6 | import CommonFilter from "@/components/filters/CommonFilter"; 7 | import HomeFilter from "@/components/filters/HomeFilter"; 8 | import Pagination from "@/components/Pagination"; 9 | import LocalSearch from "@/components/search/LocalSearch"; 10 | import { Button } from "@/components/ui/button"; 11 | import { HomePageFilters } from "@/constants/filters"; 12 | import ROUTES from "@/constants/routes"; 13 | import { EMPTY_QUESTION } from "@/constants/states"; 14 | import { getQuestions } from "@/lib/actions/question.action"; 15 | 16 | export const metadata: Metadata = { 17 | title: "Dev Overflow | Home", 18 | description: 19 | "Discover different programming questions and answers with recommendations from the community.", 20 | }; 21 | 22 | async function Home({ searchParams }: RouteParams) { 23 | const { page, pageSize, query, filter } = await searchParams; 24 | 25 | const { success, data, error } = await getQuestions({ 26 | page: Number(page) || 1, 27 | pageSize: Number(pageSize) || 10, 28 | query, 29 | filter, 30 | }); 31 | 32 | const { questions, isNext } = data || {}; 33 | 34 | return ( 35 | <> 36 |
37 |

All Questions

38 | 46 |
47 | 48 |
49 | 56 | 57 | 62 |
63 | 64 | 65 | 66 | ( 72 |
73 | {questions.map((question) => ( 74 | 75 | ))} 76 |
77 | )} 78 | /> 79 | 80 | 81 | 82 | ); 83 | } 84 | 85 | export default Home; 86 | -------------------------------------------------------------------------------- /app/(root)/profile/edit/page.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from "next/navigation"; 2 | 3 | import { auth } from "@/auth"; 4 | import ProfileForm from "@/components/forms/ProfileForm"; 5 | import ROUTES from "@/constants/routes"; 6 | import { getUser } from "@/lib/actions/user.action"; 7 | 8 | const Page = async () => { 9 | const session = await auth(); 10 | if (!session?.user?.id) redirect(ROUTES.SIGN_IN); 11 | 12 | const { success, data } = await getUser({ userId: session.user.id }); 13 | if (!success) redirect(ROUTES.SIGN_IN); 14 | 15 | return ( 16 | <> 17 |

Edit Profile

18 | 19 | 20 | 21 | ); 22 | }; 23 | 24 | export default Page; 25 | -------------------------------------------------------------------------------- /app/(root)/questions/[id]/edit/page.tsx: -------------------------------------------------------------------------------- 1 | import { notFound, redirect } from "next/navigation"; 2 | import React from "react"; 3 | 4 | import { auth } from "@/auth"; 5 | import QuestionForm from "@/components/forms/QuestionForm"; 6 | import ROUTES from "@/constants/routes"; 7 | import { getQuestion } from "@/lib/actions/question.action"; 8 | 9 | const EditQuestion = async ({ params }: RouteParams) => { 10 | const { id } = await params; 11 | if (!id) return notFound(); 12 | 13 | const session = await auth(); 14 | if (!session) return redirect("/sign-in"); 15 | 16 | const { data: question, success } = await getQuestion({ questionId: id }); 17 | if (!success) return notFound(); 18 | 19 | if (question?.author._id.toString() !== session?.user?.id) 20 | redirect(ROUTES.QUESTION(id)); 21 | 22 | return ( 23 |
24 | 25 |
26 | ); 27 | }; 28 | 29 | export default EditQuestion; 30 | -------------------------------------------------------------------------------- /app/(root)/tags/[id]/page.tsx: -------------------------------------------------------------------------------- 1 | import QuestionCard from "@/components/cards/QuestionCard"; 2 | import DataRenderer from "@/components/DataRenderer"; 3 | import Pagination from "@/components/Pagination"; 4 | import LocalSearch from "@/components/search/LocalSearch"; 5 | import ROUTES from "@/constants/routes"; 6 | import { EMPTY_QUESTION } from "@/constants/states"; 7 | import { getTagQuestions } from "@/lib/actions/tag.action"; 8 | 9 | const Page = async ({ params, searchParams }: RouteParams) => { 10 | const { id } = await params; 11 | const { page, pageSize, query } = await searchParams; 12 | 13 | const { success, data, error } = await getTagQuestions({ 14 | tagId: id, 15 | page: Number(page) || 1, 16 | pageSize: Number(pageSize) || 10, 17 | query, 18 | }); 19 | 20 | const { tag, questions, isNext } = data || {}; 21 | 22 | return ( 23 | <> 24 |
25 |

{tag?.name}

26 |
27 | 28 |
29 | 35 |
36 | 37 | ( 43 |
44 | {questions.map((question) => ( 45 | 46 | ))} 47 |
48 | )} 49 | /> 50 | 51 | 52 | 53 | ); 54 | }; 55 | 56 | export default Page; 57 | -------------------------------------------------------------------------------- /app/(root)/tags/page.tsx: -------------------------------------------------------------------------------- 1 | import TagCard from "@/components/cards/TagCard"; 2 | import DataRenderer from "@/components/DataRenderer"; 3 | import CommonFilter from "@/components/filters/CommonFilter"; 4 | import Pagination from "@/components/Pagination"; 5 | import LocalSearch from "@/components/search/LocalSearch"; 6 | import { TagFilters } from "@/constants/filters"; 7 | import ROUTES from "@/constants/routes"; 8 | import { EMPTY_TAGS } from "@/constants/states"; 9 | import { getTags } from "@/lib/actions/tag.action"; 10 | 11 | const Tags = async ({ searchParams }: RouteParams) => { 12 | const { page, pageSize, query, filter } = await searchParams; 13 | 14 | const { success, data, error } = await getTags({ 15 | page: Number(page) || 1, 16 | pageSize: Number(pageSize) || 10, 17 | query, 18 | filter, 19 | }); 20 | 21 | const { tags, isNext } = data || {}; 22 | 23 | return ( 24 | <> 25 |

Tags

26 | 27 |
28 | 34 | 35 | 39 |
40 | 41 | ( 47 |
48 | {tags.map((tag) => ( 49 | 50 | ))} 51 |
52 | )} 53 | /> 54 | 55 | 56 | 57 | ); 58 | }; 59 | 60 | export default Tags; 61 | -------------------------------------------------------------------------------- /app/api/accounts/[id]/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | 3 | import Account from "@/database/account.model"; 4 | import handleError from "@/lib/handlers/error"; 5 | import { NotFoundError, ValidationError } from "@/lib/http-errors"; 6 | import dbConnect from "@/lib/mongoose"; 7 | import { AccountSchema } from "@/lib/validations"; 8 | 9 | // GET /api/users/[id] 10 | export async function GET( 11 | _: Request, 12 | { params }: { params: Promise<{ id: string }> } 13 | ) { 14 | const { id } = await params; 15 | if (!id) throw new NotFoundError("Account"); 16 | 17 | try { 18 | await dbConnect(); 19 | 20 | const account = await Account.findById(id); 21 | if (!account) throw new NotFoundError("Account"); 22 | 23 | return NextResponse.json({ success: true, data: account }, { status: 200 }); 24 | } catch (error) { 25 | return handleError(error, "api") as APIErrorResponse; 26 | } 27 | } 28 | 29 | // DELETE /api/users/[id] 30 | export async function DELETE( 31 | _: Request, 32 | { params }: { params: Promise<{ id: string }> } 33 | ) { 34 | const { id } = await params; 35 | if (!id) throw new NotFoundError("Account"); 36 | 37 | try { 38 | await dbConnect(); 39 | 40 | const account = await Account.findByIdAndDelete(id); 41 | if (!account) throw new NotFoundError("Account"); 42 | 43 | return NextResponse.json({ success: true, data: account }, { status: 200 }); 44 | } catch (error) { 45 | return handleError(error, "api") as APIErrorResponse; 46 | } 47 | } 48 | 49 | // PUT /api/users/[id] 50 | export async function PUT( 51 | request: Request, 52 | { params }: { params: Promise<{ id: string }> } 53 | ) { 54 | const { id } = await params; 55 | if (!id) throw new NotFoundError("Account"); 56 | 57 | try { 58 | await dbConnect(); 59 | 60 | const body = await request.json(); 61 | const validatedData = AccountSchema.partial().safeParse(body); 62 | 63 | if (!validatedData.success) 64 | throw new ValidationError(validatedData.error.flatten().fieldErrors); 65 | 66 | const updatedAccount = await Account.findByIdAndUpdate(id, validatedData, { 67 | new: true, 68 | }); 69 | 70 | if (!updatedAccount) throw new NotFoundError("Account"); 71 | 72 | return NextResponse.json( 73 | { success: true, data: updatedAccount }, 74 | { status: 200 } 75 | ); 76 | } catch (error) { 77 | return handleError(error, "api") as APIErrorResponse; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /app/api/accounts/provider/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | 3 | import Account from "@/database/account.model"; 4 | import handleError from "@/lib/handlers/error"; 5 | import { NotFoundError, ValidationError } from "@/lib/http-errors"; 6 | import dbConnect from "@/lib/mongoose"; 7 | import { AccountSchema } from "@/lib/validations"; 8 | 9 | export async function POST(request: Request) { 10 | const { providerAccountId } = await request.json(); 11 | 12 | try { 13 | await dbConnect(); 14 | 15 | const validatedData = AccountSchema.partial().safeParse({ 16 | providerAccountId, 17 | }); 18 | 19 | if (!validatedData.success) 20 | throw new ValidationError(validatedData.error.flatten().fieldErrors); 21 | 22 | const account = await Account.findOne({ providerAccountId }); 23 | if (!account) throw new NotFoundError("Account"); 24 | 25 | return NextResponse.json( 26 | { 27 | success: true, 28 | data: account, 29 | }, 30 | { status: 200 } 31 | ); 32 | } catch (error) { 33 | return handleError(error, "api") as APIErrorResponse; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/api/accounts/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | 3 | import Account from "@/database/account.model"; 4 | import handleError from "@/lib/handlers/error"; 5 | import { ForbiddenError } from "@/lib/http-errors"; 6 | import dbConnect from "@/lib/mongoose"; 7 | import { AccountSchema } from "@/lib/validations"; 8 | 9 | export async function GET() { 10 | try { 11 | await dbConnect(); 12 | 13 | const accounts = await Account.find(); 14 | 15 | return NextResponse.json( 16 | { success: true, data: accounts }, 17 | { status: 200 } 18 | ); 19 | } catch (error) { 20 | return handleError(error, "api") as APIErrorResponse; 21 | } 22 | } 23 | 24 | export async function POST(request: Request) { 25 | try { 26 | await dbConnect(); 27 | const body = await request.json(); 28 | 29 | const validatedData = AccountSchema.parse(body); 30 | 31 | const existingAccount = await Account.findOne({ 32 | provider: validatedData.provider, 33 | providerAccountId: validatedData.providerAccountId, 34 | }); 35 | 36 | if (existingAccount) 37 | throw new ForbiddenError( 38 | "An account with the same provider already exists" 39 | ); 40 | 41 | const newAccount = await Account.create(validatedData); 42 | 43 | return NextResponse.json( 44 | { success: true, data: newAccount }, 45 | { status: 201 } 46 | ); 47 | } catch (error) { 48 | return handleError(error, "api") as APIErrorResponse; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /app/api/ai/answers/route.ts: -------------------------------------------------------------------------------- 1 | import { openai } from "@ai-sdk/openai"; 2 | import { generateText } from "ai"; 3 | import { NextResponse } from "next/server"; 4 | 5 | import handleError from "@/lib/handlers/error"; 6 | import { ValidationError } from "@/lib/http-errors"; 7 | import { AIAnswerSchema } from "@/lib/validations"; 8 | 9 | export async function POST(req: Request) { 10 | const { question, content, userAnswer } = await req.json(); 11 | 12 | try { 13 | const validatedData = AIAnswerSchema.safeParse({ 14 | question, 15 | content, 16 | }); 17 | 18 | if (!validatedData.success) { 19 | throw new ValidationError(validatedData.error.flatten().fieldErrors); 20 | } 21 | 22 | const { text } = await generateText({ 23 | model: openai("gpt-4-turbo"), 24 | prompt: `Generate a markdown-formatted response to the following question: "${question}". 25 | 26 | Consider the provided context: 27 | **Context:** ${content} 28 | 29 | Also, prioritize and incorporate the user's answer when formulating your response: 30 | **User's Answer:** ${userAnswer} 31 | 32 | Prioritize the user's answer only if it's correct. If it's incomplete or incorrect, improve or correct it while keeping the response concise and to the point. 33 | Provide the final answer in markdown format.`, 34 | system: 35 | "You are a helpful assistant that provides informative responses in markdown format. Use appropriate markdown syntax for headings, lists, code blocks, and emphasis where necessary. For code blocks, use short-form smaller case language identifiers (e.g., 'js' for JavaScript, 'py' for Python, 'ts' for TypeScript, 'html' for HTML, 'css' for CSS, etc.).", 36 | }); 37 | 38 | return NextResponse.json({ success: true, data: text }, { status: 200 }); 39 | } catch (error) { 40 | return handleError(error, "api") as APIErrorResponse; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/api/auth/[...nextauth]/route.ts: -------------------------------------------------------------------------------- 1 | import { handlers } from "@/auth"; 2 | export const { GET, POST } = handlers; 3 | -------------------------------------------------------------------------------- /app/api/auth/signin-with-oauth/route.ts: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | import { NextResponse } from "next/server"; 3 | import slugify from "slugify"; 4 | 5 | import Account from "@/database/account.model"; 6 | import User from "@/database/user.model"; 7 | import handleError from "@/lib/handlers/error"; 8 | import { ValidationError } from "@/lib/http-errors"; 9 | import dbConnect from "@/lib/mongoose"; 10 | import { SignInWithOAuthSchema } from "@/lib/validations"; 11 | 12 | export async function POST(request: Request) { 13 | const { provider, providerAccountId, user } = await request.json(); 14 | 15 | await dbConnect(); 16 | 17 | const session = await mongoose.startSession(); 18 | session.startTransaction(); 19 | 20 | try { 21 | const validatedData = SignInWithOAuthSchema.safeParse({ 22 | provider, 23 | providerAccountId, 24 | user, 25 | }); 26 | 27 | if (!validatedData.success) 28 | throw new ValidationError(validatedData.error.flatten().fieldErrors); 29 | 30 | const { name, username, email, image } = user; 31 | 32 | const slugifiedUsername = slugify(username, { 33 | lower: true, 34 | strict: true, 35 | trim: true, 36 | }); 37 | 38 | let existingUser = await User.findOne({ email }).session(session); 39 | 40 | if (!existingUser) { 41 | [existingUser] = await User.create( 42 | [{ name, username: slugifiedUsername, email, image }], 43 | { session } 44 | ); 45 | } else { 46 | const updatedData: { name?: string; image?: string } = {}; 47 | 48 | if (existingUser.name !== name) updatedData.name = name; 49 | if (existingUser.image !== image) updatedData.image = image; 50 | 51 | if (Object.keys(updatedData).length > 0) { 52 | await User.updateOne( 53 | { _id: existingUser._id }, 54 | { $set: updatedData } 55 | ).session(session); 56 | } 57 | } 58 | 59 | const existingAccount = await Account.findOne({ 60 | userId: existingUser._id, 61 | provider, 62 | providerAccountId, 63 | }).session(session); 64 | 65 | if (!existingAccount) { 66 | await Account.create( 67 | [ 68 | { 69 | userId: existingUser._id, 70 | name, 71 | image, 72 | provider, 73 | providerAccountId, 74 | }, 75 | ], 76 | { session } 77 | ); 78 | } 79 | 80 | await session.commitTransaction(); 81 | 82 | return NextResponse.json({ success: true }); 83 | } catch (error: unknown) { 84 | await session.abortTransaction(); 85 | return handleError(error, "api") as APIErrorResponse; 86 | } finally { 87 | session.endSession(); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /app/api/users/[id]/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | 3 | import User from "@/database/user.model"; 4 | import handleError from "@/lib/handlers/error"; 5 | import { NotFoundError } from "@/lib/http-errors"; 6 | import dbConnect from "@/lib/mongoose"; 7 | import { UserSchema } from "@/lib/validations"; 8 | 9 | // GET /api/users/[id] 10 | export async function GET( 11 | _: Request, 12 | { params }: { params: Promise<{ id: string }> } 13 | ) { 14 | const { id } = await params; 15 | if (!id) throw new NotFoundError("User"); 16 | 17 | try { 18 | await dbConnect(); 19 | 20 | const user = await User.findById(id); 21 | if (!user) throw new NotFoundError("User"); 22 | 23 | return NextResponse.json({ success: true, data: user }, { status: 200 }); 24 | } catch (error) { 25 | return handleError(error, "api") as APIErrorResponse; 26 | } 27 | } 28 | 29 | // DELETE /api/users/[id] 30 | export async function DELETE( 31 | _: Request, 32 | { params }: { params: Promise<{ id: string }> } 33 | ) { 34 | const { id } = await params; 35 | if (!id) throw new NotFoundError("User"); 36 | 37 | try { 38 | await dbConnect(); 39 | 40 | const user = await User.findByIdAndDelete(id); 41 | if (!user) throw new NotFoundError("User"); 42 | 43 | return NextResponse.json({ success: true, data: user }, { status: 200 }); 44 | } catch (error) { 45 | return handleError(error, "api") as APIErrorResponse; 46 | } 47 | } 48 | 49 | // PUT /api/users/[id] 50 | export async function PUT( 51 | request: Request, 52 | { params }: { params: Promise<{ id: string }> } 53 | ) { 54 | const { id } = await params; 55 | if (!id) throw new NotFoundError("User"); 56 | 57 | try { 58 | await dbConnect(); 59 | 60 | const body = await request.json(); 61 | const validatedData = UserSchema.partial().parse(body); 62 | 63 | const updatedUser = await User.findByIdAndUpdate(id, validatedData, { 64 | new: true, 65 | }); 66 | 67 | if (!updatedUser) throw new NotFoundError("User"); 68 | 69 | return NextResponse.json( 70 | { success: true, data: updatedUser }, 71 | { status: 200 } 72 | ); 73 | } catch (error) { 74 | return handleError(error, "api") as APIErrorResponse; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /app/api/users/email/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | 3 | import User from "@/database/user.model"; 4 | import handleError from "@/lib/handlers/error"; 5 | import { NotFoundError, ValidationError } from "@/lib/http-errors"; 6 | import dbConnect from "@/lib/mongoose"; 7 | import { UserSchema } from "@/lib/validations"; 8 | 9 | export async function POST(request: Request) { 10 | const { email } = await request.json(); 11 | 12 | try { 13 | await dbConnect(); 14 | 15 | const validatedData = UserSchema.partial().safeParse({ email }); 16 | 17 | if (!validatedData.success) 18 | throw new ValidationError(validatedData.error.flatten().fieldErrors); 19 | 20 | const user = await User.findOne({ email }); 21 | if (!user) throw new NotFoundError("User"); 22 | 23 | return NextResponse.json( 24 | { 25 | success: true, 26 | data: user, 27 | }, 28 | { status: 200 } 29 | ); 30 | } catch (error) { 31 | return handleError(error, "api") as APIErrorResponse; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/api/users/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | 3 | import User from "@/database/user.model"; 4 | import handleError from "@/lib/handlers/error"; 5 | import { ValidationError } from "@/lib/http-errors"; 6 | import dbConnect from "@/lib/mongoose"; 7 | import { UserSchema } from "@/lib/validations"; 8 | 9 | export async function GET() { 10 | try { 11 | await dbConnect(); 12 | 13 | const users = await User.find(); 14 | 15 | return NextResponse.json({ success: true, data: users }, { status: 200 }); 16 | } catch (error) { 17 | return handleError(error, "api") as APIErrorResponse; 18 | } 19 | } 20 | 21 | export async function POST(request: Request) { 22 | try { 23 | await dbConnect(); 24 | const body = await request.json(); 25 | 26 | const validatedData = UserSchema.safeParse(body); 27 | 28 | if (!validatedData.success) { 29 | throw new ValidationError(validatedData.error.flatten().fieldErrors); 30 | } 31 | 32 | const { email, username } = validatedData.data; 33 | 34 | const existingUser = await User.findOne({ email }); 35 | if (existingUser) throw new Error("User already exists"); 36 | 37 | const existingUsername = await User.findOne({ username }); 38 | if (existingUsername) throw new Error("Username already exists"); 39 | 40 | const newUser = await User.create(validatedData.data); 41 | 42 | return NextResponse.json({ success: true, data: newUser }, { status: 201 }); 43 | } catch (error) { 44 | return handleError(error, "api") as APIErrorResponse; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrianhajdin/jsmasterypro_devflow/22efaff5f35920de0e4778d36d2fc20d70f04296/app/favicon.ico -------------------------------------------------------------------------------- /app/fonts/InterVF.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrianhajdin/jsmasterypro_devflow/22efaff5f35920de0e4778d36d2fc20d70f04296/app/fonts/InterVF.ttf -------------------------------------------------------------------------------- /app/fonts/SpaceGroteskVF.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrianhajdin/jsmasterypro_devflow/22efaff5f35920de0e4778d36d2fc20d70f04296/app/fonts/SpaceGroteskVF.ttf -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import localFont from "next/font/local"; 3 | import { SessionProvider } from "next-auth/react"; 4 | import { ReactNode } from "react"; 5 | 6 | import "./globals.css"; 7 | import { auth } from "@/auth"; 8 | import { Toaster } from "@/components/ui/toaster"; 9 | import ThemeProvider from "@/context/Theme"; 10 | 11 | const inter = localFont({ 12 | src: "./fonts/InterVF.ttf", 13 | variable: "--font-inter", 14 | weight: "100 200 300 400 500 700 800 900", 15 | }); 16 | 17 | const spaceGrotesk = localFont({ 18 | src: "./fonts/SpaceGroteskVF.ttf", 19 | variable: "--font-space-grotesk", 20 | weight: "300 400 500 700", 21 | }); 22 | 23 | export const metadata: Metadata = { 24 | title: "DevFlow", 25 | description: 26 | "A community-driven platform for asking and answering programming questions. Get help, share knowledge, and collaborate with developers from around the world. Explore topics in web development, mobile app development, algorithms, data structures, and more.", 27 | icons: { 28 | icon: "/images/site-logo.svg", 29 | }, 30 | }; 31 | 32 | const RootLayout = async ({ children }: { children: ReactNode }) => { 33 | const session = await auth(); 34 | 35 | return ( 36 | 37 | 38 | 43 | 44 | 45 | 48 | 54 | {children} 55 | 56 | 57 | 58 | 59 | 60 | ); 61 | }; 62 | 63 | export default RootLayout; 64 | -------------------------------------------------------------------------------- /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.ts", 8 | "css": "app/globals.css", 9 | "baseColor": "slate", 10 | "cssVariables": false, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | } 20 | } -------------------------------------------------------------------------------- /components/Metric.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils"; 2 | import Image from "next/image"; 3 | import Link from "next/link"; 4 | import React from "react"; 5 | 6 | interface Props { 7 | imgUrl: string; 8 | alt: string; 9 | value: string | number; 10 | title: string; 11 | href?: string; 12 | textStyles: string; 13 | imgStyles?: string; 14 | isAuthor?: boolean; 15 | titleStyles?: string; 16 | } 17 | 18 | const Metric = ({ 19 | imgUrl, 20 | alt, 21 | value, 22 | title, 23 | href, 24 | textStyles, 25 | imgStyles, 26 | isAuthor, 27 | titleStyles, 28 | }: Props) => { 29 | const metricContent = ( 30 | <> 31 | {alt} 38 | 39 |

40 | {value} 41 | 42 | {title ? ( 43 | 44 | {title} 45 | 46 | ) : null} 47 |

48 | 49 | ); 50 | 51 | return href ? ( 52 | 53 | {metricContent} 54 | 55 | ) : ( 56 |
{metricContent}
57 | ); 58 | }; 59 | 60 | export default Metric; 61 | -------------------------------------------------------------------------------- /components/Pagination.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useRouter, useSearchParams } from "next/navigation"; 4 | 5 | import { formUrlQuery } from "@/lib/url"; 6 | import { cn } from "@/lib/utils"; 7 | 8 | import { Button } from "./ui/button"; 9 | 10 | interface Props { 11 | page: number | undefined | string; 12 | isNext: boolean; 13 | containerClasses?: string; 14 | } 15 | 16 | const Pagination = ({ page = 1, isNext, containerClasses }: Props) => { 17 | const searchParams = useSearchParams(); 18 | const router = useRouter(); 19 | 20 | const handleNavigation = (type: "prev" | "next") => { 21 | const nextPageNumber = 22 | type === "prev" ? Number(page) - 1 : Number(page) + 1; 23 | 24 | const newUrl = formUrlQuery({ 25 | params: searchParams.toString(), 26 | key: "page", 27 | value: nextPageNumber.toString(), 28 | }); 29 | 30 | router.push(newUrl); 31 | }; 32 | 33 | return ( 34 |
40 | {/* Previous Page Button */} 41 | {Number(page) > 1 && ( 42 | 48 | )} 49 | 50 |
51 |

{page}

52 |
53 | 54 | {/* Next Page Button */} 55 | {isNext && ( 56 | 62 | )} 63 |
64 | ); 65 | }; 66 | 67 | export default Pagination; 68 | -------------------------------------------------------------------------------- /components/UserAvatar.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | import Link from "next/link"; 3 | import React from "react"; 4 | 5 | import ROUTES from "@/constants/routes"; 6 | import { cn } from "@/lib/utils"; 7 | 8 | import { Avatar, AvatarFallback } from "./ui/avatar"; 9 | 10 | interface Props { 11 | id: string; 12 | name: string; 13 | imageUrl?: string | null; 14 | className?: string; 15 | fallbackClassName?: string; 16 | } 17 | 18 | const UserAvatar = ({ 19 | id, 20 | name, 21 | imageUrl, 22 | className = "h-9 w-9", 23 | fallbackClassName, 24 | }: Props) => { 25 | const initials = name 26 | .split(" ") 27 | .map((word: string) => word[0]) 28 | .join("") 29 | .toUpperCase() 30 | .slice(0, 2); 31 | 32 | return ( 33 | 34 | 35 | {imageUrl ? ( 36 | {name} 43 | ) : ( 44 | 50 | {initials} 51 | 52 | )} 53 | 54 | 55 | ); 56 | }; 57 | 58 | export default UserAvatar; 59 | -------------------------------------------------------------------------------- /components/answers/AllAnswers.tsx: -------------------------------------------------------------------------------- 1 | import { AnswerFilters } from "@/constants/filters"; 2 | import { EMPTY_ANSWERS } from "@/constants/states"; 3 | 4 | import AnswerCard from "../cards/AnswerCard"; 5 | import DataRenderer from "../DataRenderer"; 6 | import CommonFilter from "../filters/CommonFilter"; 7 | import Pagination from "../Pagination"; 8 | 9 | interface Props extends ActionResponse { 10 | page: number; 11 | isNext: boolean; 12 | totalAnswers: number; 13 | } 14 | 15 | const AllAnswers = ({ 16 | page, 17 | isNext, 18 | data, 19 | success, 20 | error, 21 | totalAnswers, 22 | }: Props) => { 23 | return ( 24 |
25 |
26 |

27 | {totalAnswers} {totalAnswers === 1 ? "Answer" : "Answers"} 28 |

29 | 34 |
35 | 36 | 42 | answers.map((answer) => ) 43 | } 44 | /> 45 | 46 | 47 |
48 | ); 49 | }; 50 | 51 | export default AllAnswers; 52 | -------------------------------------------------------------------------------- /components/cards/QuestionCard.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import React from "react"; 3 | 4 | import ROUTES from "@/constants/routes"; 5 | import { getTimeStamp } from "@/lib/utils"; 6 | 7 | import TagCard from "./TagCard"; 8 | import Metric from "../Metric"; 9 | import EditDeleteAction from "../user/EditDeleteAction"; 10 | 11 | interface Props { 12 | question: Question; 13 | showActionBtns?: boolean; 14 | } 15 | 16 | const QuestionCard = ({ 17 | question: { _id, title, tags, author, createdAt, upvotes, answers, views }, 18 | showActionBtns = false, 19 | }: Props) => { 20 | return ( 21 |
22 |
23 |
24 | 25 | {getTimeStamp(createdAt)} 26 | 27 | 28 | 29 |

30 | {title} 31 |

32 | 33 |
34 | 35 | {showActionBtns && } 36 |
37 | 38 |
39 | {tags.map((tag: Tag) => ( 40 | 41 | ))} 42 |
43 | 44 |
45 | 55 | 56 |
57 | 64 | 71 | 78 |
79 |
80 |
81 | ); 82 | }; 83 | 84 | export default QuestionCard; 85 | -------------------------------------------------------------------------------- /components/cards/UserCard.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | import ROUTES from "@/constants/routes"; 4 | 5 | import UserAvatar from "../UserAvatar"; 6 | 7 | const UserCard = ({ _id, name, image, username }: User) => ( 8 |
9 |
10 | 17 | 18 | 19 |
20 |

{name}

21 |

@{username}

22 |
23 | 24 |
25 |
26 | ); 27 | 28 | export default UserCard; 29 | -------------------------------------------------------------------------------- /components/editor/Preview.tsx: -------------------------------------------------------------------------------- 1 | import { Code } from "bright"; 2 | import { MDXRemote } from "next-mdx-remote/rsc"; 3 | 4 | Code.theme = { 5 | light: "github-light", 6 | dark: "github-dark", 7 | lightSelector: "html.light", 8 | }; 9 | 10 | export const Preview = ({ content }: { content: string }) => { 11 | const formattedContent = content.replace(/\\/g, "").replace(/ /g, ""); 12 | 13 | return ( 14 |
15 | ( 19 | 24 | ), 25 | }} 26 | /> 27 |
28 | ); 29 | }; 30 | -------------------------------------------------------------------------------- /components/editor/dark-editor.css: -------------------------------------------------------------------------------- 1 | @import url("@radix-ui/colors/tomato-dark.css"); 2 | @import url("@radix-ui/colors/mauve-dark.css"); 3 | 4 | .dark .dark-editor { 5 | --accentBase: var(--tomato-1); 6 | --accentBgSubtle: var(--tomato-2); 7 | --accentBg: var(--tomato-3); 8 | --accentBgHover: var(--tomato-4); 9 | --accentBgActive: var(--tomato-5); 10 | --accentLine: var(--tomato-6); 11 | --accentBorder: var(--tomato-7); 12 | --accentBorderHover: var(--tomato-8); 13 | --accentSolid: var(--tomato-9); 14 | --accentSolidHover: var(--tomato-10); 15 | --accentText: var(--tomato-11); 16 | --accentTextContrast: var(--tomato-12); 17 | 18 | --baseBase: var(--mauve-1); 19 | --baseBgSubtle: var(--mauve-2); 20 | --baseBg: var(--mauve-3); 21 | --baseBgHover: var(--mauve-4); 22 | --baseBgActive: var(--mauve-5); 23 | --baseLine: var(--mauve-6); 24 | --baseBorder: var(--mauve-7); 25 | --baseBorderHover: var(--mauve-8); 26 | --baseSolid: var(--mauve-9); 27 | --baseSolidHover: var(--mauve-10); 28 | --baseText: var(--mauve-11); 29 | --baseTextContrast: var(--mauve-12); 30 | 31 | --admonitionTipBg: var(--cyan4); 32 | --admonitionTipBorder: var(--cyan8); 33 | 34 | --admonitionInfoBg: var(--grass4); 35 | --admonitionInfoBorder: var(--grass8); 36 | 37 | --admonitionCautionBg: var(--amber4); 38 | --admonitionCautionBorder: var(--amber8); 39 | 40 | --admonitionDangerBg: var(--red4); 41 | --admonitionDangerBorder: var(--red8); 42 | 43 | --admonitionNoteBg: var(--mauve-4); 44 | --admonitionNoteBorder: var(--mauve-8); 45 | 46 | font-family: 47 | system-ui, 48 | -apple-system, 49 | BlinkMacSystemFont, 50 | "Segoe UI", 51 | Roboto, 52 | Oxygen, 53 | Ubuntu, 54 | Cantarell, 55 | "Open Sans", 56 | "Helvetica Neue", 57 | sans-serif; 58 | --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 59 | "Liberation Mono", "Courier New", monospace; 60 | 61 | color: var(--baseText); 62 | --basePageBg: black; 63 | background: var(--basePageBg); 64 | } 65 | -------------------------------------------------------------------------------- /components/editor/question.mdx: -------------------------------------------------------------------------------- 1 | The react-query DevIcon icon is not getting rendered. Instead I'm getting a default DevIcon icon 2 | 3 | Can anyone help? 4 | -------------------------------------------------------------------------------- /components/filters/CommonFilter.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useRouter, useSearchParams } from "next/navigation"; 4 | 5 | import { 6 | Select, 7 | SelectItem, 8 | SelectTrigger, 9 | SelectValue, 10 | SelectContent, 11 | SelectGroup, 12 | } from "@/components/ui/select"; 13 | import { formUrlQuery } from "@/lib/url"; 14 | import { cn } from "@/lib/utils"; 15 | 16 | interface Filter { 17 | name: string; 18 | value: string; 19 | } 20 | 21 | interface Props { 22 | filters: Filter[]; 23 | otherClasses?: string; 24 | containerClasses?: string; 25 | } 26 | 27 | const CommonFilter = ({ 28 | filters, 29 | otherClasses = "", 30 | containerClasses = "", 31 | }: Props) => { 32 | const router = useRouter(); 33 | const searchParams = useSearchParams(); 34 | 35 | const paramsFilter = searchParams.get("filter"); 36 | 37 | const handleUpdateParams = (value: string) => { 38 | const newUrl = formUrlQuery({ 39 | params: searchParams.toString(), 40 | key: "filter", 41 | value, 42 | }); 43 | 44 | router.push(newUrl, { scroll: false }); 45 | }; 46 | 47 | return ( 48 |
49 | 75 |
76 | ); 77 | }; 78 | 79 | export default CommonFilter; 80 | -------------------------------------------------------------------------------- /components/filters/GlobalFilter.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useSearchParams, useRouter } from "next/navigation"; 4 | import { useState } from "react"; 5 | 6 | import { GlobalSearchFilters } from "@/constants/filters"; 7 | import { formUrlQuery } from "@/lib/url"; 8 | 9 | const GlobalFilter = () => { 10 | const router = useRouter(); 11 | const searchParams = useSearchParams(); 12 | 13 | const typeParams = searchParams.get("type"); 14 | 15 | const [active, setActive] = useState(typeParams || ""); 16 | 17 | const handleTypeClick = (item: string) => { 18 | let newUrl = ""; 19 | 20 | if (active === item) { 21 | setActive(""); 22 | 23 | newUrl = formUrlQuery({ 24 | params: searchParams.toString(), 25 | key: "type", 26 | value: null, 27 | }); 28 | 29 | router.push(newUrl, { scroll: false }); 30 | } else { 31 | setActive(item); 32 | 33 | newUrl = formUrlQuery({ 34 | params: searchParams.toString(), 35 | key: "type", 36 | value: item.toLowerCase(), 37 | }); 38 | } 39 | 40 | router.push(newUrl, { scroll: false }); 41 | }; 42 | 43 | return ( 44 |
45 |

Type:

46 |
47 | {GlobalSearchFilters.map((item) => ( 48 | 60 | ))} 61 |
62 |
63 | ); 64 | }; 65 | 66 | export default GlobalFilter; 67 | -------------------------------------------------------------------------------- /components/filters/HomeFilter.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useSearchParams, useRouter } from "next/navigation"; 4 | import React, { useState } from "react"; 5 | 6 | import { formUrlQuery, removeKeysFromUrlQuery } from "@/lib/url"; 7 | import { cn } from "@/lib/utils"; 8 | 9 | import { Button } from "../ui/button"; 10 | 11 | const filters = [ 12 | { name: "Newest", value: "newest" }, 13 | { name: "Popular", value: "popular" }, 14 | { name: "Unanswered", value: "unanswered" }, 15 | { name: "Recommeded", value: "recommended" }, 16 | ]; 17 | 18 | const HomeFilter = () => { 19 | const router = useRouter(); 20 | const searchParams = useSearchParams(); 21 | const filterParams = searchParams.get("filter"); 22 | const [active, setActive] = useState(filterParams || ""); 23 | 24 | const handleTypeClick = (filter: string) => { 25 | let newUrl = ""; 26 | 27 | if (filter === active) { 28 | setActive(""); 29 | 30 | newUrl = removeKeysFromUrlQuery({ 31 | params: searchParams.toString(), 32 | keysToRemove: ["filter"], 33 | }); 34 | } else { 35 | setActive(filter); 36 | 37 | newUrl = formUrlQuery({ 38 | params: searchParams.toString(), 39 | key: "filter", 40 | value: filter.toLowerCase(), 41 | }); 42 | } 43 | 44 | router.push(newUrl, { scroll: false }); 45 | }; 46 | 47 | return ( 48 |
49 | {filters.map((filter) => ( 50 | 62 | ))} 63 |
64 | ); 65 | }; 66 | 67 | export default HomeFilter; 68 | -------------------------------------------------------------------------------- /components/filters/JobFilter.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Image from "next/image"; 4 | import { usePathname, useRouter, useSearchParams } from "next/navigation"; 5 | 6 | import { 7 | Select, 8 | SelectContent, 9 | SelectGroup, 10 | SelectItem, 11 | SelectTrigger, 12 | SelectValue, 13 | } from "@/components/ui/select"; 14 | import { formUrlQuery } from "@/lib/url"; 15 | 16 | import LocalSearch from "../search/LocalSearch"; 17 | 18 | interface JobsFilterProps { 19 | countriesList: Country[]; 20 | } 21 | 22 | const JobsFilter = ({ countriesList }: JobsFilterProps) => { 23 | const router = useRouter(); 24 | const pathname = usePathname(); 25 | const searchParams = useSearchParams(); 26 | 27 | const handleUpdateParams = (value: string) => { 28 | const newUrl = formUrlQuery({ 29 | params: searchParams.toString(), 30 | key: "location", 31 | value, 32 | }); 33 | 34 | router.push(newUrl, { scroll: false }); 35 | }; 36 | 37 | return ( 38 |
39 | 46 | 47 | 78 |
79 | ); 80 | }; 81 | 82 | export default JobsFilter; 83 | -------------------------------------------------------------------------------- /components/forms/SocialAuthForm.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Image from "next/image"; 4 | import { signIn } from "next-auth/react"; 5 | import React from "react"; 6 | 7 | import ROUTES from "@/constants/routes"; 8 | import { toast } from "@/hooks/use-toast"; 9 | 10 | import { Button } from "../ui/button"; 11 | 12 | const SocialAuthForm = () => { 13 | const buttonClass = 14 | "background-dark400_light900 body-medium text-dark200_light800 min-h-12 flex-1 rounded-2 px-4 py-3.5"; 15 | 16 | const handleSignIn = async (provider: "github" | "google") => { 17 | try { 18 | await signIn(provider, { 19 | callbackUrl: ROUTES.HOME, 20 | redirect: false, 21 | }); 22 | } catch (error) { 23 | console.log(error); 24 | 25 | toast({ 26 | title: "Sign-in Failed", 27 | description: 28 | error instanceof Error 29 | ? error.message 30 | : "An error occured during sign-in", 31 | variant: "destructive", 32 | }); 33 | } 34 | }; 35 | 36 | return ( 37 |
38 | 48 | 49 | 59 |
60 | ); 61 | }; 62 | 63 | export default SocialAuthForm; 64 | -------------------------------------------------------------------------------- /components/navigation/LeftSidebar.tsx: -------------------------------------------------------------------------------- 1 | import { LogOut } from "lucide-react"; 2 | import Image from "next/image"; 3 | import Link from "next/link"; 4 | import React from "react"; 5 | 6 | import { auth, signOut } from "@/auth"; 7 | import ROUTES from "@/constants/routes"; 8 | 9 | import NavLinks from "./navbar/NavLinks"; 10 | import { Button } from "../ui/button"; 11 | 12 | const LeftSidebar = async () => { 13 | const session = await auth(); 14 | const userId = session?.user?.id; 15 | 16 | return ( 17 |
18 |
19 | 20 |
21 | 22 |
23 | {userId ? ( 24 |
{ 26 | "use server"; 27 | 28 | await signOut(); 29 | }} 30 | > 31 | 40 |
41 | ) : ( 42 | <> 43 | 60 | 61 | 76 | 77 | )} 78 |
79 |
80 | ); 81 | }; 82 | 83 | export default LeftSidebar; 84 | -------------------------------------------------------------------------------- /components/navigation/RightSidebar.tsx: -------------------------------------------------------------------------------- 1 | import ROUTES from "@/constants/routes"; 2 | import Image from "next/image"; 3 | import Link from "next/link"; 4 | import React from "react"; 5 | import TagCard from "../cards/TagCard"; 6 | import { getHotQuestions } from "@/lib/actions/question.action"; 7 | import DataRenderer from "../DataRenderer"; 8 | import { getTopTags } from "@/lib/actions/tag.actions"; 9 | 10 | const RightSidebar = async () => { 11 | const [ 12 | { success, data: hotQuestions, error }, 13 | { success: tagSuccess, data: tags, error: tagError }, 14 | ] = await Promise.all([getHotQuestions(), getTopTags()]); 15 | 16 | return ( 17 |
18 |
19 |

Top Questions

20 | 21 | ( 30 |
31 | {hotQuestions.map(({ _id, title }) => ( 32 | 37 |

38 | {title} 39 |

40 | 41 | Chevron 48 | 49 | ))} 50 |
51 | )} 52 | /> 53 |
54 | 55 |
56 |

Popular Tags

57 | 58 | ( 67 |
68 | {tags.map(({ _id, name, questions }) => ( 69 | 77 | ))} 78 |
79 | )} 80 | /> 81 |
82 |
83 | ); 84 | }; 85 | 86 | export default RightSidebar; 87 | -------------------------------------------------------------------------------- /components/navigation/navbar/NavLinks.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Image from "next/image"; 4 | import Link from "next/link"; 5 | import { usePathname } from "next/navigation"; 6 | import React from "react"; 7 | 8 | import { SheetClose } from "@/components/ui/sheet"; 9 | import { sidebarLinks } from "@/constants"; 10 | import { cn } from "@/lib/utils"; 11 | 12 | const NavLinks = ({ 13 | isMobileNav = false, 14 | userId, 15 | }: { 16 | isMobileNav?: boolean; 17 | userId?: string; 18 | }) => { 19 | const pathname = usePathname(); 20 | 21 | return ( 22 | <> 23 | {sidebarLinks.map((item) => { 24 | const isActive = 25 | (pathname.includes(item.route) && item.route.length > 1) || 26 | pathname === item.route; 27 | 28 | if (item.route === "/profile") { 29 | if (userId) item.route = `${item.route}/${userId}`; 30 | else return null; 31 | } 32 | 33 | const LinkComponent = ( 34 | 44 | {item.label} 51 |

57 | {item.label} 58 |

59 | 60 | ); 61 | 62 | return isMobileNav ? ( 63 | 64 | {LinkComponent} 65 | 66 | ) : ( 67 | {LinkComponent} 68 | ); 69 | })} 70 | 71 | ); 72 | }; 73 | 74 | export default NavLinks; 75 | -------------------------------------------------------------------------------- /components/navigation/navbar/Theme.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { MoonIcon, SunIcon } from "@radix-ui/react-icons"; 4 | import { useTheme } from "next-themes"; 5 | import * as React from "react"; 6 | 7 | import { Button } from "@/components/ui/button"; 8 | import { 9 | DropdownMenu, 10 | DropdownMenuContent, 11 | DropdownMenuItem, 12 | DropdownMenuTrigger, 13 | } from "@/components/ui/dropdown-menu"; 14 | 15 | const Theme = () => { 16 | const { setTheme } = useTheme(); 17 | 18 | return ( 19 | 20 | 21 | 26 | 27 | 28 | setTheme("light")}> 29 | Light 30 | 31 | setTheme("dark")}> 32 | Dark 33 | 34 | setTheme("system")}> 35 | System 36 | 37 | 38 | 39 | ); 40 | }; 41 | 42 | export default Theme; 43 | -------------------------------------------------------------------------------- /components/navigation/navbar/index.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | import Link from "next/link"; 3 | 4 | import { auth } from "@/auth"; 5 | import GlobalSearch from "@/components/search/GlobalSearch"; 6 | import UserAvatar from "@/components/UserAvatar"; 7 | import ROUTES from "@/constants/routes"; 8 | 9 | import MobileNavigation from "./MobileNavigation"; 10 | import Theme from "./Theme"; 11 | 12 | const Navbar = async () => { 13 | const session = await auth(); 14 | 15 | return ( 16 | 45 | ); 46 | }; 47 | 48 | export default Navbar; 49 | -------------------------------------------------------------------------------- /components/questions/SaveQuestion.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Image from "next/image"; 4 | import { useSession } from "next-auth/react"; 5 | import { use, useState } from "react"; 6 | 7 | import { toast } from "@/hooks/use-toast"; 8 | import { toggleSaveQuestion } from "@/lib/actions/collection.action"; 9 | 10 | const SaveQuestion = ({ 11 | questionId, 12 | hasSavedQuestionPromise, 13 | }: { 14 | questionId: string; 15 | hasSavedQuestionPromise: Promise>; 16 | }) => { 17 | const session = useSession(); 18 | const userId = session?.data?.user?.id; 19 | 20 | const { data } = use(hasSavedQuestionPromise); 21 | 22 | const { saved: hasSaved } = data || {}; 23 | 24 | const [isLoading, setIsLoading] = useState(false); 25 | 26 | const handleSave = async () => { 27 | if (isLoading) return; 28 | if (!userId) 29 | return toast({ 30 | title: "You need to be logged in to save a question", 31 | variant: "destructive", 32 | }); 33 | 34 | setIsLoading(true); 35 | 36 | try { 37 | const { success, data, error } = await toggleSaveQuestion({ questionId }); 38 | 39 | if (!success) throw new Error(error?.message || "An error occurred"); 40 | 41 | toast({ 42 | title: `Question ${data?.saved ? "saved" : "unsaved"} successfully`, 43 | }); 44 | } catch (error) { 45 | toast({ 46 | title: "Error", 47 | description: 48 | error instanceof Error ? error.message : "An error occurred", 49 | variant: "destructive", 50 | }); 51 | } finally { 52 | setIsLoading(false); 53 | } 54 | }; 55 | 56 | return ( 57 | save 66 | ); 67 | }; 68 | 69 | export default SaveQuestion; 70 | -------------------------------------------------------------------------------- /components/search/LocalSearch.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Image from "next/image"; 4 | import { useSearchParams, useRouter, usePathname } from "next/navigation"; 5 | import { useEffect, useState } from "react"; 6 | 7 | import { formUrlQuery, removeKeysFromUrlQuery } from "@/lib/url"; 8 | 9 | import { Input } from "../ui/input"; 10 | 11 | interface Props { 12 | route: string; 13 | imgSrc: string; 14 | placeholder: string; 15 | otherClasses?: string; 16 | iconPosition?: "left" | "right"; 17 | } 18 | 19 | const LocalSearch = ({ 20 | route, 21 | imgSrc, 22 | placeholder, 23 | otherClasses, 24 | iconPosition = "left", 25 | }: Props) => { 26 | const pathname = usePathname(); 27 | const router = useRouter(); 28 | const searchParams = useSearchParams(); 29 | const query = searchParams.get("query") || ""; 30 | 31 | const [searchQuery, setSearchQuery] = useState(query); 32 | 33 | useEffect(() => { 34 | const delayDebounceFn = setTimeout(() => { 35 | if (searchQuery) { 36 | const newUrl = formUrlQuery({ 37 | params: searchParams.toString(), 38 | key: "query", 39 | value: searchQuery, 40 | }); 41 | 42 | router.push(newUrl, { scroll: false }); 43 | } else { 44 | if (pathname === route) { 45 | const newUrl = removeKeysFromUrlQuery({ 46 | params: searchParams.toString(), 47 | keysToRemove: ["query"], 48 | }); 49 | 50 | router.push(newUrl, { scroll: false }); 51 | } 52 | } 53 | }, 300); 54 | 55 | return () => clearTimeout(delayDebounceFn); 56 | }, [searchQuery, router, route, searchParams, pathname]); 57 | 58 | return ( 59 |
62 | {iconPosition === "left" && ( 63 | Search 70 | )} 71 | 72 | setSearchQuery(e.target.value)} 77 | className="paragraph-regular no-focus placeholder text-dark400_light700 border-none shadow-none outline-none" 78 | /> 79 | 80 | {iconPosition === "right" && ( 81 | Search 88 | )} 89 |
90 | ); 91 | }; 92 | 93 | export default LocalSearch; 94 | -------------------------------------------------------------------------------- /components/ui/avatar.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as AvatarPrimitive from "@radix-ui/react-avatar" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Avatar = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, ...props }, ref) => ( 12 | 20 | )) 21 | Avatar.displayName = AvatarPrimitive.Root.displayName 22 | 23 | const AvatarImage = React.forwardRef< 24 | React.ElementRef, 25 | React.ComponentPropsWithoutRef 26 | >(({ className, ...props }, ref) => ( 27 | 32 | )) 33 | AvatarImage.displayName = AvatarPrimitive.Image.displayName 34 | 35 | const AvatarFallback = React.forwardRef< 36 | React.ElementRef, 37 | React.ComponentPropsWithoutRef 38 | >(({ className, ...props }, ref) => ( 39 | 47 | )) 48 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName 49 | 50 | export { Avatar, AvatarImage, AvatarFallback } 51 | -------------------------------------------------------------------------------- /components/ui/badge.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { cva, type VariantProps } from "class-variance-authority" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | const badgeVariants = cva( 7 | "inline-flex items-center rounded-md border border-slate-200 px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-slate-950 focus:ring-offset-2 dark:border-slate-800 dark:focus:ring-slate-300", 8 | { 9 | variants: { 10 | variant: { 11 | default: 12 | "border-transparent bg-slate-900 text-slate-50 shadow hover:bg-slate-900/80 dark:bg-slate-50 dark:text-slate-900 dark:hover:bg-slate-50/80", 13 | secondary: 14 | "border-transparent bg-slate-100 text-slate-900 hover:bg-slate-100/80 dark:bg-slate-800 dark:text-slate-50 dark:hover:bg-slate-800/80", 15 | destructive: 16 | "border-transparent bg-red-500 text-slate-50 shadow hover:bg-red-500/80 dark:bg-red-900 dark:text-slate-50 dark:hover:bg-red-900/80", 17 | outline: "text-slate-950 dark:text-slate-50", 18 | }, 19 | }, 20 | defaultVariants: { 21 | variant: "default", 22 | }, 23 | } 24 | ) 25 | 26 | export interface BadgeProps 27 | extends React.HTMLAttributes, 28 | VariantProps {} 29 | 30 | function Badge({ className, variant, ...props }: BadgeProps) { 31 | return ( 32 |
33 | ) 34 | } 35 | 36 | export { Badge, badgeVariants } 37 | -------------------------------------------------------------------------------- /components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-slate-950 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 dark:focus-visible:ring-slate-300", 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | "bg-slate-900 text-slate-50 shadow hover:bg-slate-900/90 dark:bg-slate-50 dark:text-slate-900 dark:hover:bg-slate-50/90", 14 | destructive: 15 | "bg-red-500 text-slate-50 shadow-sm hover:bg-red-500/90 dark:bg-red-900 dark:text-slate-50 dark:hover:bg-red-900/90", 16 | outline: 17 | "border border-slate-200 bg-white shadow-sm hover:bg-slate-100 hover:text-slate-900 dark:border-slate-800 dark:bg-slate-950 dark:hover:bg-slate-800 dark:hover:text-slate-50", 18 | secondary: 19 | "bg-slate-100 text-slate-900 shadow-sm hover:bg-slate-100/80 dark:bg-slate-800 dark:text-slate-50 dark:hover:bg-slate-800/80", 20 | ghost: "hover:bg-slate-100 hover:text-slate-900 dark:hover:bg-slate-800 dark:hover:text-slate-50", 21 | link: "text-slate-900 underline-offset-4 hover:underline dark:text-slate-50", 22 | }, 23 | size: { 24 | default: "h-9 px-4 py-2", 25 | sm: "h-8 rounded-md px-3 text-xs", 26 | lg: "h-10 rounded-md px-8", 27 | icon: "h-9 w-9", 28 | }, 29 | }, 30 | defaultVariants: { 31 | variant: "default", 32 | size: "default", 33 | }, 34 | } 35 | ) 36 | 37 | export interface ButtonProps 38 | extends React.ButtonHTMLAttributes, 39 | VariantProps { 40 | asChild?: boolean 41 | } 42 | 43 | const Button = React.forwardRef( 44 | ({ className, variant, size, asChild = false, ...props }, ref) => { 45 | const Comp = asChild ? Slot : "button" 46 | return ( 47 | 52 | ) 53 | } 54 | ) 55 | Button.displayName = "Button" 56 | 57 | export { Button, buttonVariants } 58 | -------------------------------------------------------------------------------- /components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | export interface InputProps 6 | extends React.InputHTMLAttributes {} 7 | 8 | const Input = React.forwardRef( 9 | ({ className, type, ...props }, ref) => { 10 | return ( 11 | 20 | ) 21 | } 22 | ) 23 | Input.displayName = "Input" 24 | 25 | export { Input } 26 | -------------------------------------------------------------------------------- /components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as LabelPrimitive from "@radix-ui/react-label" 5 | import { cva, type VariantProps } from "class-variance-authority" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const labelVariants = cva( 10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" 11 | ) 12 | 13 | const Label = React.forwardRef< 14 | React.ElementRef, 15 | React.ComponentPropsWithoutRef & 16 | VariantProps 17 | >(({ className, ...props }, ref) => ( 18 | 23 | )) 24 | Label.displayName = LabelPrimitive.Root.displayName 25 | 26 | export { Label } 27 | -------------------------------------------------------------------------------- /components/ui/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils" 2 | 3 | function Skeleton({ 4 | className, 5 | ...props 6 | }: React.HTMLAttributes) { 7 | return ( 8 |
12 | ) 13 | } 14 | 15 | export { Skeleton } 16 | -------------------------------------------------------------------------------- /components/ui/tabs.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as TabsPrimitive from "@radix-ui/react-tabs" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Tabs = TabsPrimitive.Root 9 | 10 | const TabsList = React.forwardRef< 11 | React.ElementRef, 12 | React.ComponentPropsWithoutRef 13 | >(({ className, ...props }, ref) => ( 14 | 22 | )) 23 | TabsList.displayName = TabsPrimitive.List.displayName 24 | 25 | const TabsTrigger = React.forwardRef< 26 | React.ElementRef, 27 | React.ComponentPropsWithoutRef 28 | >(({ className, ...props }, ref) => ( 29 | 37 | )) 38 | TabsTrigger.displayName = TabsPrimitive.Trigger.displayName 39 | 40 | const TabsContent = React.forwardRef< 41 | React.ElementRef, 42 | React.ComponentPropsWithoutRef 43 | >(({ className, ...props }, ref) => ( 44 | 52 | )) 53 | TabsContent.displayName = TabsPrimitive.Content.displayName 54 | 55 | export { Tabs, TabsList, TabsTrigger, TabsContent } 56 | -------------------------------------------------------------------------------- /components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | const Textarea = React.forwardRef< 6 | HTMLTextAreaElement, 7 | React.ComponentProps<"textarea"> 8 | >(({ className, ...props }, ref) => { 9 | return ( 10 |