├── .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 |
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 |
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 |
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 |
27 |
28 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
18 | )
19 | })
20 | Textarea.displayName = "Textarea"
21 |
22 | export { Textarea }
23 |
--------------------------------------------------------------------------------
/components/ui/toaster.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { useToast } from "@/hooks/use-toast"
4 | import {
5 | Toast,
6 | ToastClose,
7 | ToastDescription,
8 | ToastProvider,
9 | ToastTitle,
10 | ToastViewport,
11 | } from "@/components/ui/toast"
12 |
13 | export function Toaster() {
14 | const { toasts } = useToast()
15 |
16 | return (
17 |
18 | {toasts.map(function ({ id, title, description, action, ...props }) {
19 | return (
20 |
21 |
22 | {title && {title}}
23 | {description && (
24 | {description}
25 | )}
26 |
27 | {action}
28 |
29 |
30 | )
31 | })}
32 |
33 |
34 | )
35 | }
36 |
--------------------------------------------------------------------------------
/components/user/EditDeleteAction.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import {
4 | AlertDialog,
5 | AlertDialogAction,
6 | AlertDialogCancel,
7 | AlertDialogContent,
8 | AlertDialogDescription,
9 | AlertDialogFooter,
10 | AlertDialogHeader,
11 | AlertDialogTitle,
12 | AlertDialogTrigger,
13 | } from "@/components/ui/alert-dialog";
14 | import { toast } from "@/hooks/use-toast";
15 | import { deleteAnswer } from "@/lib/actions/answer.action";
16 | import { deleteQuestion } from "@/lib/actions/question.action";
17 | import Image from "next/image";
18 | import { useRouter } from "next/navigation";
19 |
20 | interface Props {
21 | type: string;
22 | itemId: string;
23 | }
24 |
25 | const EditDeleteAction = ({ type, itemId }: Props) => {
26 | const router = useRouter();
27 |
28 | const handleEdit = async () => {
29 | router.push(`/questions/${itemId}/edit`);
30 | };
31 |
32 | const handleDelete = async () => {
33 | if (type === "Question") {
34 | // Call API to delete question
35 | await deleteQuestion({ questionId: itemId });
36 |
37 | toast({
38 | title: "Question deleted",
39 | description: "Your question has been deleted successfully.",
40 | });
41 | } else if (type === "Answer") {
42 | // Call API to delete answer
43 | await deleteAnswer({ answerId: itemId });
44 |
45 | toast({
46 | title: "Answer deleted",
47 | description: "Your answer has been deleted successfully.",
48 | });
49 | }
50 | };
51 |
52 | return (
53 |
56 | {type === "Question" && (
57 |
65 | )}
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 | Are you absolutely sure?
74 |
75 | This action cannot be undone. This will permanently delete your{" "}
76 | {type === "Question" ? "question" : "answer"} and remove it from
77 | our servers.
78 |
79 |
80 |
81 | Cancel
82 |
86 | Continue
87 |
88 |
89 |
90 |
91 |
92 | );
93 | };
94 |
95 | export default EditDeleteAction;
96 |
--------------------------------------------------------------------------------
/components/user/ProfileLink.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import Image from "next/image";
3 |
4 | interface Props {
5 | imgUrl: string;
6 | href?: string;
7 | title: string;
8 | }
9 |
10 | const ProfileLink = ({ imgUrl, href, title }: Props) => {
11 | return (
12 |
13 |
14 |
15 | {href ? (
16 |
22 | {title}
23 |
24 | ) : (
25 |
{title}
26 | )}
27 |
28 | );
29 | };
30 |
31 | export default ProfileLink;
32 |
--------------------------------------------------------------------------------
/components/user/Stats.tsx:
--------------------------------------------------------------------------------
1 | import Image from "next/image";
2 |
3 | import { formatNumber } from "@/lib/utils";
4 |
5 | interface StatsCardProps {
6 | imgUrl: string;
7 | value: number;
8 | title: string;
9 | }
10 |
11 | const StatsCard = ({ imgUrl, value, title }: StatsCardProps) => {
12 | return (
13 |
14 |
15 |
16 |
{value}
17 |
{title}
18 |
19 |
20 | );
21 | };
22 |
23 | interface Props {
24 | totalQuestions: number;
25 | totalAnswers: number;
26 | badges: BadgeCounts;
27 | reputationPoints: number;
28 | }
29 |
30 | const Stats = ({
31 | totalQuestions,
32 | totalAnswers,
33 | badges,
34 | reputationPoints,
35 | }: Props) => {
36 | return (
37 |
38 |
39 | Stats{" "}
40 |
41 | {formatNumber(reputationPoints)}
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 | {formatNumber(totalQuestions)}
50 |
51 |
Questions
52 |
53 |
54 |
55 |
56 | {formatNumber(totalAnswers)}
57 |
58 |
Answers
59 |
60 |
61 |
62 |
67 |
68 |
73 |
74 |
79 |
80 |
81 | );
82 | };
83 |
84 | export default Stats;
85 |
--------------------------------------------------------------------------------
/constants/filters.ts:
--------------------------------------------------------------------------------
1 | export const HomePageFilters = [
2 | { name: "Newest", value: "newest" },
3 | { name: "Popular", value: "popular" },
4 | { name: "Unanswered", value: "unanswered" },
5 | { name: "Recommended", value: "recommended" },
6 | ];
7 |
8 | export const AnswerFilters = [
9 | { name: "Newest", value: "latest" },
10 | { name: "Oldest", value: "oldest" },
11 | { name: "Popular", value: "popular" },
12 | ];
13 |
14 | export const CollectionFilters = [
15 | { name: "Oldest", value: "oldest" },
16 | { name: "Most Voted", value: "mostvoted" },
17 | { name: "Most Viewed", value: "mostviewed" },
18 | { name: "Most Recent", value: "mostrecent" },
19 | { name: "Most Answered", value: "mostanswered" },
20 | ];
21 |
22 | export const TagFilters = [
23 | { name: "A-Z", value: "name" },
24 | { name: "Recent", value: "recent" },
25 | { name: "Oldest", value: "oldest" },
26 | { name: "Popular", value: "popular" },
27 | ];
28 |
29 | export const UserFilters = [
30 | { name: "Newest", value: "newest" },
31 | { name: "Oldest", value: "oldest" },
32 | { name: "Popular", value: "popular" },
33 | ];
34 |
35 | export const GlobalSearchFilters = [
36 | { name: "Question", value: "question" },
37 | { name: "Answer", value: "answer" },
38 | { name: "User", value: "user" },
39 | { name: "Tag", value: "tag" },
40 | ];
41 |
--------------------------------------------------------------------------------
/constants/index.ts:
--------------------------------------------------------------------------------
1 | export const themes = [
2 | { value: "light", label: "Light", icon: "/icons/sun.svg" },
3 | { value: "dark", label: "Dark", icon: "/icons/moon.svg" },
4 | { value: "system", label: "System", icon: "/icons/computer.svg" },
5 | ];
6 |
7 | export const sidebarLinks = [
8 | {
9 | imgURL: "/icons/home.svg",
10 | route: "/",
11 | label: "Home",
12 | },
13 | {
14 | imgURL: "/icons/users.svg",
15 | route: "/community",
16 | label: "Community",
17 | },
18 | {
19 | imgURL: "/icons/star.svg",
20 | route: "/collection",
21 | label: "Collections",
22 | },
23 | {
24 | imgURL: "/icons/suitcase.svg",
25 | route: "/jobs",
26 | label: "Find Jobs",
27 | },
28 | {
29 | imgURL: "/icons/tag.svg",
30 | route: "/tags",
31 | label: "Tags",
32 | },
33 | {
34 | imgURL: "/icons/user.svg",
35 | route: "/profile",
36 | label: "Profile",
37 | },
38 | {
39 | imgURL: "/icons/question.svg",
40 | route: "/ask-question",
41 | label: "Ask a question",
42 | },
43 | ];
44 |
45 | export const BADGE_CRITERIA = {
46 | QUESTION_COUNT: {
47 | BRONZE: 10,
48 | SILVER: 50,
49 | GOLD: 100,
50 | },
51 | ANSWER_COUNT: {
52 | BRONZE: 10,
53 | SILVER: 50,
54 | GOLD: 100,
55 | },
56 | QUESTION_UPVOTES: {
57 | BRONZE: 10,
58 | SILVER: 50,
59 | GOLD: 100,
60 | },
61 | ANSWER_UPVOTES: {
62 | BRONZE: 10,
63 | SILVER: 50,
64 | GOLD: 100,
65 | },
66 | TOTAL_VIEWS: {
67 | BRONZE: 1000,
68 | SILVER: 10000,
69 | GOLD: 100000,
70 | },
71 | };
72 |
--------------------------------------------------------------------------------
/constants/routes.ts:
--------------------------------------------------------------------------------
1 | const ROUTES = {
2 | HOME: "/",
3 | SIGN_IN: "/sign-in",
4 | SIGN_UP: "/sign-up",
5 | ASK_QUESTION: "/ask-question",
6 | COLLECTION: "/collection",
7 | COMMUNITY: "/community",
8 | TAGS: "/tags",
9 | JOBS: "/jobs",
10 | PROFILE: (id: string) => `/profile/${id}`,
11 | QUESTION: (id: string) => `/questions/${id}`,
12 | TAG: (id: string) => `/tags/${id}`,
13 | SIGN_IN_WITH_OAUTH: `signin-with-oauth`,
14 | };
15 |
16 | export default ROUTES;
17 |
--------------------------------------------------------------------------------
/constants/states.ts:
--------------------------------------------------------------------------------
1 | import ROUTES from "./routes";
2 |
3 | export const DEFAULT_EMPTY = {
4 | title: "No Data Found",
5 | message:
6 | "Looks like the database is taking a nap. Wake it up with some new entries.",
7 | button: {
8 | text: "Add Data",
9 | href: ROUTES.HOME,
10 | },
11 | };
12 |
13 | export const DEFAULT_ERROR = {
14 | title: "Something Went Wrong",
15 | message: "Even our code can have a bad day. Give it another shot.",
16 | button: {
17 | text: "Retry Request",
18 | href: ROUTES.HOME,
19 | },
20 | };
21 |
22 | export const EMPTY_QUESTION = {
23 | title: "Ahh, No Questions Yet!",
24 | message:
25 | "The question board is empty. Maybe it’s waiting for your brilliant question to get things rolling",
26 | button: {
27 | text: "Ask a Question",
28 | href: ROUTES.ASK_QUESTION,
29 | },
30 | };
31 |
32 | export const EMPTY_TAGS = {
33 | title: "No Tags Found",
34 | message: "The tag cloud is empty. Add some keywords to make it rain.",
35 | button: {
36 | text: "Create Tag",
37 | href: ROUTES.TAGS,
38 | },
39 | };
40 |
41 | export const EMPTY_ANSWERS = {
42 | title: "No Answers Found",
43 | message:
44 | "The answer board is empty. Make it rain with your brilliant answer.",
45 | };
46 |
47 | export const EMPTY_COLLECTIONS = {
48 | title: "Collections Are Empty",
49 | message:
50 | "Looks like you haven’t created any collections yet. Start curating something extraordinary today",
51 | button: {
52 | text: "Save to Collection",
53 | href: ROUTES.COLLECTION,
54 | },
55 | };
56 |
57 | export const EMPTY_USERS = {
58 | title: "No Users Found",
59 | message: "You're ALONE. The only one here. More uses are coming soon!",
60 | };
61 |
--------------------------------------------------------------------------------
/context/Theme.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { ThemeProvider as NextThemesProvider } from "next-themes";
4 | import { ThemeProviderProps } from "next-themes/dist/types";
5 | import React from "react";
6 |
7 | const ThemeProvider = ({ children, ...props }: ThemeProviderProps) => {
8 | return {children};
9 | };
10 |
11 | export default ThemeProvider;
12 |
--------------------------------------------------------------------------------
/database/account.model.ts:
--------------------------------------------------------------------------------
1 | import { model, models, Schema, Types, Document } from "mongoose";
2 |
3 | export interface IAccount {
4 | userId: Types.ObjectId;
5 | name: string;
6 | image?: string;
7 | password?: string;
8 | provider: string;
9 | providerAccountId: string;
10 | }
11 |
12 | export interface IAccountDoc extends IAccount, Document {}
13 | const AccountSchema = new Schema(
14 | {
15 | userId: { type: Schema.Types.ObjectId, ref: "User", required: true },
16 | name: { type: String, required: true },
17 | image: { type: String },
18 | password: { type: String },
19 | provider: { type: String, required: true },
20 | providerAccountId: { type: String, required: true },
21 | },
22 | { timestamps: true }
23 | );
24 |
25 | const Account = models?.Account || model("Account", AccountSchema);
26 |
27 | export default Account;
28 |
--------------------------------------------------------------------------------
/database/answer.model.ts:
--------------------------------------------------------------------------------
1 | import { model, models, Schema, Types, Document } from "mongoose";
2 |
3 | export interface IAnswer {
4 | author: Types.ObjectId;
5 | question: Types.ObjectId;
6 | content: string;
7 | upvotes: number;
8 | downvotes: number;
9 | }
10 |
11 | export interface IAnswerDoc extends IAnswer, Document {}
12 | const AnswerSchema = new Schema(
13 | {
14 | author: { type: Schema.Types.ObjectId, ref: "User", required: true },
15 | question: { type: Schema.Types.ObjectId, ref: "Question", required: true },
16 | content: { type: String, required: true },
17 | upvotes: { type: Number, default: 0 },
18 | downvotes: { type: Number, default: 0 },
19 | },
20 | { timestamps: true }
21 | );
22 |
23 | const Answer = models?.Answer || model("Answer", AnswerSchema);
24 |
25 | export default Answer;
26 |
--------------------------------------------------------------------------------
/database/collection.model.ts:
--------------------------------------------------------------------------------
1 | import { model, models, Schema, Types, Document } from "mongoose";
2 |
3 | export interface ICollection {
4 | author: Types.ObjectId;
5 | question: Types.ObjectId;
6 | }
7 |
8 | export interface ICollectionDoc extends ICollection, Document {}
9 | const CollectionSchema = new Schema(
10 | {
11 | author: { type: Schema.Types.ObjectId, ref: "User", required: true },
12 | question: { type: Schema.Types.ObjectId, ref: "Question", required: true },
13 | },
14 | { timestamps: true }
15 | );
16 |
17 | const Collection =
18 | models?.Collection || model("Collection", CollectionSchema);
19 |
20 | export default Collection;
21 |
--------------------------------------------------------------------------------
/database/index.ts:
--------------------------------------------------------------------------------
1 | import Account from "./account.model";
2 | import Answer from "./answer.model";
3 | import Collection from "./collection.model";
4 | import Interaction from "./interaction.model";
5 | import Question from "./question.model";
6 | import TagQuestion from "./tag-question.model";
7 | import Tag from "./tag.model";
8 | import User from "./user.model";
9 | import Vote from "./vote.model";
10 |
11 | export {
12 | Account,
13 | Answer,
14 | Collection,
15 | Interaction,
16 | Question,
17 | TagQuestion,
18 | Tag,
19 | User,
20 | Vote,
21 | };
22 |
--------------------------------------------------------------------------------
/database/interaction.model.ts:
--------------------------------------------------------------------------------
1 | import { Schema, models, model, Types, Document } from "mongoose";
2 |
3 | export interface IInteraction {
4 | user: Types.ObjectId;
5 | action: string;
6 | actionId: Types.ObjectId;
7 | actionType: string;
8 | }
9 |
10 | export const InteractionActionEnums = [
11 | "view",
12 | "upvote",
13 | "downvote",
14 | "bookmark",
15 | "post",
16 | "edit",
17 | "delete",
18 | "search",
19 | ] as const;
20 |
21 | export interface IInteractionDoc extends IInteraction, Document {}
22 | const InteractionSchema = new Schema(
23 | {
24 | user: { type: Schema.Types.ObjectId, ref: "User", required: true },
25 | action: {
26 | type: String,
27 | enum: InteractionActionEnums,
28 | required: true,
29 | },
30 | actionId: { type: Schema.Types.ObjectId, required: true }, // 'questionId', 'answerId',
31 | actionType: { type: String, enum: ["question", "answer"], required: true },
32 | },
33 | { timestamps: true }
34 | );
35 |
36 | const Interaction =
37 | models?.Interaction || model("Interaction", InteractionSchema);
38 |
39 | export default Interaction;
40 |
--------------------------------------------------------------------------------
/database/question.model.ts:
--------------------------------------------------------------------------------
1 | import { model, models, Schema, Types, Document } from "mongoose";
2 |
3 | export interface IQuestion {
4 | title: string;
5 | content: string;
6 | tags: Types.ObjectId[];
7 | views: number;
8 | upvotes: number;
9 | downvotes: number;
10 | answers: number;
11 | author: Types.ObjectId;
12 | }
13 |
14 | export interface IQuestionDoc extends IQuestion, Document {}
15 | const QuestionSchema = new Schema(
16 | {
17 | title: { type: String, required: true },
18 | content: { type: String, required: true },
19 | tags: [{ type: Schema.Types.ObjectId, ref: "Tag" }],
20 | views: { type: Number, default: 0 },
21 | upvotes: { type: Number, default: 0 },
22 | downvotes: { type: Number, default: 0 },
23 | answers: { type: Number, default: 0 },
24 | author: { type: Schema.Types.ObjectId, ref: "User", required: true },
25 | },
26 | { timestamps: true }
27 | );
28 |
29 | const Question =
30 | models?.Question || model("Question", QuestionSchema);
31 |
32 | export default Question;
33 |
--------------------------------------------------------------------------------
/database/tag-question.model.ts:
--------------------------------------------------------------------------------
1 | import { model, models, Schema, Types, Document } from "mongoose";
2 |
3 | export interface ITagQuestion {
4 | tag: Types.ObjectId;
5 | question: Types.ObjectId;
6 | }
7 |
8 | export interface ITagQuestionDoc extends ITagQuestion, Document {}
9 | const TagQuestionSchema = new Schema(
10 | {
11 | tag: { type: Schema.Types.ObjectId, ref: "Tag", required: true },
12 | question: { type: Schema.Types.ObjectId, ref: "Question", required: true },
13 | },
14 | { timestamps: true }
15 | );
16 |
17 | const TagQuestion =
18 | models?.TagQuestion || model("TagQuestion", TagQuestionSchema);
19 |
20 | export default TagQuestion;
21 |
--------------------------------------------------------------------------------
/database/tag.model.ts:
--------------------------------------------------------------------------------
1 | import { model, models, Schema, Document } from "mongoose";
2 |
3 | export interface ITag {
4 | name: string;
5 | questions: number;
6 | }
7 |
8 | export interface ITagDoc extends ITag, Document {}
9 | const TagSchema = new Schema(
10 | {
11 | name: { type: String, required: true, unique: true },
12 | questions: { type: Number, default: 0 },
13 | },
14 | { timestamps: true }
15 | );
16 |
17 | const Tag = models?.Tag || model("Tag", TagSchema);
18 |
19 | export default Tag;
20 |
--------------------------------------------------------------------------------
/database/user.model.ts:
--------------------------------------------------------------------------------
1 | import { model, models, Schema, Document } from "mongoose";
2 |
3 | export interface IUser {
4 | name: string;
5 | username: string;
6 | email: string;
7 | bio?: string;
8 | image?: string;
9 | location?: string;
10 | portfolio?: string;
11 | reputation?: number;
12 | }
13 |
14 | export interface IUserDoc extends IUser, Document {}
15 | const UserSchema = new Schema(
16 | {
17 | name: { type: String, required: true },
18 | username: { type: String, required: true, unique: true },
19 | email: { type: String, required: true, unique: true },
20 | bio: { type: String },
21 | image: { type: String },
22 | location: { type: String },
23 | portfolio: { type: String },
24 | reputation: { type: Number, default: 0 },
25 | },
26 | { timestamps: true }
27 | );
28 |
29 | const User = models?.User || model("User", UserSchema);
30 |
31 | export default User;
32 |
--------------------------------------------------------------------------------
/database/vote.model.ts:
--------------------------------------------------------------------------------
1 | import { model, models, Schema, Types } from "mongoose";
2 |
3 | export interface IVote {
4 | author: Types.ObjectId;
5 | actionId: Types.ObjectId;
6 | actionType: "question" | "answer";
7 | voteType: "upvote" | "downvote";
8 | }
9 |
10 | export interface IVoteDoc extends IVote, Document {}
11 | const VoteSchema = new Schema(
12 | {
13 | author: { type: Schema.Types.ObjectId, ref: "User", required: true },
14 | actionId: { type: Schema.Types.ObjectId, required: true },
15 | actionType: { type: String, enum: ["question", "answer"], required: true },
16 | voteType: { type: String, enum: ["upvote", "downvote"], required: true },
17 | },
18 | { timestamps: true }
19 | );
20 |
21 | const Vote = models?.Vote || model("Vote", VoteSchema);
22 |
23 | export default Vote;
24 |
--------------------------------------------------------------------------------
/eslint.config.mjs:
--------------------------------------------------------------------------------
1 | import path from "node:path";
2 | import { fileURLToPath } from "node:url";
3 |
4 | import { FlatCompat } from "@eslint/eslintrc";
5 | import js from "@eslint/js";
6 |
7 | const __filename = fileURLToPath(import.meta.url);
8 | const __dirname = path.dirname(__filename);
9 | const compat = new FlatCompat({
10 | baseDirectory: __dirname,
11 | recommendedConfig: js.configs.recommended,
12 | allConfig: js.configs.all,
13 | });
14 |
15 | const config = [
16 | {
17 | ignores: ["components/ui/**/*"],
18 | },
19 | ...compat.extends(
20 | "next/core-web-vitals",
21 | "next/typescript",
22 | "standard",
23 | // "plugin:tailwindcss/recommended",
24 | "prettier"
25 | ),
26 | {
27 | rules: {
28 | "import/order": [
29 | "error",
30 | {
31 | groups: [
32 | "builtin",
33 | "external",
34 | "internal",
35 | ["parent", "sibling"],
36 | "index",
37 | "object",
38 | ],
39 |
40 | "newlines-between": "always",
41 |
42 | pathGroups: [
43 | {
44 | pattern: "@app/**",
45 | group: "external",
46 | position: "after",
47 | },
48 | ],
49 |
50 | pathGroupsExcludedImportTypes: ["builtin"],
51 |
52 | alphabetize: {
53 | order: "asc",
54 | caseInsensitive: true,
55 | },
56 | },
57 | ],
58 | "comma-dangle": "off",
59 | },
60 | },
61 | {
62 | files: ["**/*.ts", "**/*.tsx"],
63 |
64 | rules: {
65 | "no-undef": "off",
66 | },
67 | },
68 | ];
69 |
70 | export default config;
71 |
--------------------------------------------------------------------------------
/lib/actions/general.action.ts:
--------------------------------------------------------------------------------
1 | "use server";
2 |
3 | import { Answer, Question, Tag, User } from "@/database";
4 |
5 | import action from "../handlers/action";
6 | import handleError from "../handlers/error";
7 | import { GlobalSearchSchema } from "../validations";
8 |
9 | export async function globalSearch(params: GlobalSearchParams) {
10 | try {
11 | console.log("QUERY", params);
12 |
13 | const validationResult = await action({
14 | params,
15 | schema: GlobalSearchSchema,
16 | });
17 |
18 | if (validationResult instanceof Error) {
19 | return handleError(validationResult) as ErrorResponse;
20 | }
21 |
22 | const { query, type } = params;
23 | const regexQuery = { $regex: query, $options: "i" };
24 |
25 | let results = [];
26 |
27 | const modelsAndTypes = [
28 | { model: Question, searchField: "title", type: "question" },
29 | { model: User, searchField: "name", type: "user" },
30 | { model: Answer, searchField: "content", type: "answer" },
31 | { model: Tag, searchField: "name", type: "tag" },
32 | ];
33 |
34 | const typeLower = type?.toLowerCase();
35 |
36 | const SearchableTypes = ["question", "answer", "user", "tag"];
37 | if (!typeLower || !SearchableTypes.includes(typeLower)) {
38 | // If no type is specified, search in all models
39 | for (const { model, searchField, type } of modelsAndTypes) {
40 | const queryResults = await model
41 | .find({ [searchField]: regexQuery })
42 | .limit(2);
43 |
44 | results.push(
45 | ...queryResults.map((item) => ({
46 | title:
47 | type === "answer"
48 | ? `Answers containing ${query}`
49 | : item[searchField],
50 | type,
51 | id: type === "answer" ? item.question : item._id,
52 | }))
53 | );
54 | }
55 | } else {
56 | // Search in the specified model type
57 | const modelInfo = modelsAndTypes.find((item) => item.type === type);
58 |
59 | if (!modelInfo) {
60 | throw new Error("Invalid search type");
61 | }
62 |
63 | const queryResults = await modelInfo.model
64 | .find({ [modelInfo.searchField]: regexQuery })
65 | .limit(8);
66 |
67 | results = queryResults.map((item) => ({
68 | title:
69 | type === "answer"
70 | ? `Answers containing ${query}`
71 | : item[modelInfo.searchField],
72 | type,
73 | id: type === "answer" ? item.question : item._id,
74 | }));
75 | }
76 |
77 | console.log(results);
78 |
79 | return {
80 | success: true,
81 | data: JSON.parse(JSON.stringify(results)),
82 | };
83 | } catch (error) {
84 | return handleError(error) as ErrorResponse;
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/lib/actions/job.action.ts:
--------------------------------------------------------------------------------
1 | export const fetchLocation = async () => {
2 | const response = await fetch("http://ip-api.com/json/?fields=country");
3 | const location = await response.json();
4 | return location.country;
5 | };
6 |
7 | export const fetchCountries = async () => {
8 | try {
9 | const response = await fetch("https://restcountries.com/v3.1/all");
10 | const result = await response.json();
11 | return result;
12 | } catch (error) {
13 | console.log(error);
14 | }
15 | };
16 |
17 | export const fetchJobs = async (filters: JobFilterParams) => {
18 | const { query, page } = filters;
19 |
20 | const headers = {
21 | "X-RapidAPI-Key": process.env.NEXT_PUBLIC_RAPID_API_KEY ?? "",
22 | "X-RapidAPI-Host": "jsearch.p.rapidapi.com",
23 | };
24 |
25 | const response = await fetch(
26 | `https://jsearch.p.rapidapi.com/search?query=${query}&page=${page}`,
27 | {
28 | headers,
29 | }
30 | );
31 |
32 | const result = await response.json();
33 |
34 | return result.data;
35 | };
36 |
--------------------------------------------------------------------------------
/lib/actions/tag.actions.ts:
--------------------------------------------------------------------------------
1 | import { FilterQuery } from "mongoose";
2 | import action from "../handlers/action";
3 | import handleError from "../handlers/error";
4 | import { PaginatedSearchParamsSchema } from "../validations";
5 | import { Tag } from "@/database";
6 | import dbConnect from "../mongoose";
7 |
8 | export const getTags = async (
9 | params: PaginatedSearchParams
10 | ): Promise> => {
11 | const validationResult = await action({
12 | params,
13 | schema: PaginatedSearchParamsSchema,
14 | });
15 |
16 | if (validationResult instanceof Error) {
17 | return handleError(validationResult) as ErrorResponse;
18 | }
19 |
20 | const { page = 1, pageSize = 10, query, filter } = params;
21 |
22 | const skip = (Number(page) - 1) * pageSize;
23 | const limit = Number(pageSize);
24 |
25 | const filterQuery: FilterQuery = {};
26 |
27 | if (query) {
28 | filterQuery.$or = [{ name: { $regex: query, $options: "i" } }];
29 | }
30 |
31 | let sortCriteria = {};
32 |
33 | switch (filter) {
34 | case "popular":
35 | sortCriteria = { questions: -1 };
36 | break;
37 | case "recent":
38 | sortCriteria = { createdAt: -1 };
39 | break;
40 | case "oldest":
41 | sortCriteria = { createdAt: 1 };
42 | break;
43 | case "name":
44 | sortCriteria = { name: 1 };
45 | break;
46 | default:
47 | sortCriteria = { questions: -1 };
48 | break;
49 | }
50 |
51 | try {
52 | const totalTags = await Tag.countDocuments(filterQuery);
53 |
54 | const tags = await Tag.find(filterQuery)
55 | .sort(sortCriteria)
56 | .skip(skip)
57 | .limit(limit);
58 |
59 | const isNext = totalTags > skip + tags.length;
60 |
61 | return {
62 | success: true,
63 | data: {
64 | tags: JSON.parse(JSON.stringify(tags)),
65 | isNext,
66 | },
67 | };
68 | } catch (error) {
69 | return handleError(error) as ErrorResponse;
70 | }
71 | };
72 |
73 | export const getTopTags = async (): Promise> => {
74 | try {
75 | await dbConnect();
76 |
77 | const tags = await Tag.find().sort({ questions: -1 }).limit(5);
78 |
79 | return {
80 | success: true,
81 | data: JSON.parse(JSON.stringify(tags)),
82 | };
83 | } catch (error) {
84 | return handleError(error) as ErrorResponse;
85 | }
86 | };
87 |
--------------------------------------------------------------------------------
/lib/api.ts:
--------------------------------------------------------------------------------
1 | import ROUTES from "@/constants/routes";
2 | import { IAccount } from "@/database/account.model";
3 | import { IUser } from "@/database/user.model";
4 |
5 | import { fetchHandler } from "./handlers/fetch";
6 |
7 | const API_BASE_URL =
8 | process.env.NEXT_PUBLIC_API_BASE_URL || "http://localhost:3000/api";
9 |
10 | export const api = {
11 | auth: {
12 | oAuthSignIn: ({
13 | user,
14 | provider,
15 | providerAccountId,
16 | }: SignInWithOAuthParams) =>
17 | fetchHandler(`${API_BASE_URL}/auth/${ROUTES.SIGN_IN_WITH_OAUTH}`, {
18 | method: "POST",
19 | body: JSON.stringify({ user, provider, providerAccountId }),
20 | }),
21 | },
22 | users: {
23 | getAll: () => fetchHandler(`${API_BASE_URL}/users`),
24 | getById: (id: string) => fetchHandler(`${API_BASE_URL}/users/${id}`),
25 | getByEmail: (email: string) =>
26 | fetchHandler(`${API_BASE_URL}/users/email`, {
27 | method: "POST",
28 | body: JSON.stringify({ email }),
29 | }),
30 | create: (userData: Partial) =>
31 | fetchHandler(`${API_BASE_URL}/users`, {
32 | method: "POST",
33 | body: JSON.stringify(userData),
34 | }),
35 | update: (id: string, userData: Partial) =>
36 | fetchHandler(`${API_BASE_URL}/users/${id}`, {
37 | method: "PUT",
38 | body: JSON.stringify(userData),
39 | }),
40 | delete: (id: string) =>
41 | fetchHandler(`${API_BASE_URL}/users/${id}`, { method: "DELETE" }),
42 | },
43 | accounts: {
44 | getAll: () => fetchHandler(`${API_BASE_URL}/accounts`),
45 | getById: (id: string) => fetchHandler(`${API_BASE_URL}/accounts/${id}`),
46 | getByProvider: (providerAccountId: string) =>
47 | fetchHandler(`${API_BASE_URL}/accounts/provider`, {
48 | method: "POST",
49 | body: JSON.stringify({ providerAccountId }),
50 | }),
51 | create: (accountData: Partial) =>
52 | fetchHandler(`${API_BASE_URL}/accounts`, {
53 | method: "POST",
54 | body: JSON.stringify(accountData),
55 | }),
56 | update: (id: string, accountData: Partial) =>
57 | fetchHandler(`${API_BASE_URL}/accounts/${id}`, {
58 | method: "PUT",
59 | body: JSON.stringify(accountData),
60 | }),
61 | delete: (id: string) =>
62 | fetchHandler(`${API_BASE_URL}/accounts/${id}`, { method: "DELETE" }),
63 | },
64 | ai: {
65 | getAnswer: (
66 | question: string,
67 | content: string,
68 | userAnswer?: string
69 | ): APIResponse =>
70 | fetchHandler(`${API_BASE_URL}/ai/answers`, {
71 | method: "POST",
72 | body: JSON.stringify({ question, content, userAnswer }),
73 | }),
74 | },
75 | };
76 |
--------------------------------------------------------------------------------
/lib/handlers/action.ts:
--------------------------------------------------------------------------------
1 | "use server";
2 |
3 | import { Session } from "next-auth";
4 | import { ZodError, ZodSchema } from "zod";
5 |
6 | import { auth } from "@/auth";
7 |
8 | import { UnauthorizedError, ValidationError } from "../http-errors";
9 | import dbConnect from "../mongoose";
10 |
11 | type ActionOptions = {
12 | params?: T;
13 | schema?: ZodSchema;
14 | authorize?: boolean;
15 | };
16 |
17 | // 1. Checking whether the schema and params are provided and validated.
18 | // 2. Checking whether the user is authorized.
19 | // 3. Connecting to the database.
20 | // 4. Returning the params and session.
21 |
22 | async function action({
23 | params,
24 | schema,
25 | authorize = false,
26 | }: ActionOptions) {
27 | if (schema && params) {
28 | try {
29 | schema.parse(params);
30 | } catch (error) {
31 | if (error instanceof ZodError) {
32 | return new ValidationError(
33 | error.flatten().fieldErrors as Record
34 | );
35 | } else {
36 | return new Error("Schema validation failed");
37 | }
38 | }
39 | }
40 |
41 | let session: Session | null = null;
42 |
43 | if (authorize) {
44 | session = await auth();
45 |
46 | if (!session) {
47 | return new UnauthorizedError();
48 | }
49 | }
50 |
51 | await dbConnect();
52 |
53 | return { params, session };
54 | }
55 |
56 | export default action;
57 |
--------------------------------------------------------------------------------
/lib/handlers/error.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from "next/server";
2 | import { ZodError } from "zod";
3 |
4 | import { RequestError, ValidationError } from "../http-errors";
5 | import logger from "../logger";
6 |
7 | export type ResponseType = "api" | "server";
8 |
9 | const formatResponse = (
10 | responseType: ResponseType,
11 | status: number,
12 | message: string,
13 | errors?: Record | undefined
14 | ) => {
15 | const responseContent = {
16 | success: false,
17 | error: {
18 | message,
19 | details: errors,
20 | },
21 | };
22 |
23 | return responseType === "api"
24 | ? NextResponse.json(responseContent, { status })
25 | : { status, ...responseContent };
26 | };
27 |
28 | const handleError = (error: unknown, responseType: ResponseType = "server") => {
29 | if (error instanceof RequestError) {
30 | logger.error(
31 | { err: error },
32 | `${responseType.toUpperCase()} Error: ${error.message}`
33 | );
34 |
35 | return formatResponse(
36 | responseType,
37 | error.statusCode,
38 | error.message,
39 | error.errors
40 | );
41 | }
42 |
43 | if (error instanceof ZodError) {
44 | const validationError = new ValidationError(
45 | error.flatten().fieldErrors as Record
46 | );
47 |
48 | logger.error(
49 | { err: error },
50 | `Validation Error: ${validationError.message}`
51 | );
52 |
53 | return formatResponse(
54 | responseType,
55 | validationError.statusCode,
56 | validationError.message,
57 | validationError.errors
58 | );
59 | }
60 |
61 | if (error instanceof Error) {
62 | logger.error(error.message);
63 |
64 | return formatResponse(responseType, 500, error.message);
65 | }
66 |
67 | logger.error({ err: error }, "An unexpected error occurred");
68 | return formatResponse(responseType, 500, "An unexpected error occurred");
69 | };
70 |
71 | export default handleError;
72 |
--------------------------------------------------------------------------------
/lib/handlers/fetch.ts:
--------------------------------------------------------------------------------
1 | import { RequestError } from "../http-errors";
2 | import logger from "../logger";
3 | import handleError from "./error";
4 |
5 | interface FetchOptions extends RequestInit {
6 | timeout?: number;
7 | }
8 |
9 | function isError(error: unknown): error is Error {
10 | return error instanceof Error;
11 | }
12 |
13 | export async function fetchHandler(
14 | url: string,
15 | options: FetchOptions = {}
16 | ): Promise> {
17 | const {
18 | timeout = 100000,
19 | headers: customHeaders = {},
20 | ...restOptions
21 | } = options;
22 |
23 | const controller = new AbortController();
24 | const id = setTimeout(() => controller.abort(), timeout);
25 |
26 | const defaultHeaders: HeadersInit = {
27 | "Content-Type": "application/json",
28 | Accept: "application/json",
29 | };
30 |
31 | const headers: HeadersInit = { ...defaultHeaders, ...customHeaders };
32 | const config: RequestInit = {
33 | ...restOptions,
34 | headers,
35 | signal: controller.signal,
36 | };
37 |
38 | try {
39 | const response = await fetch(url, config);
40 |
41 | clearTimeout(id);
42 |
43 | if (!response.ok) {
44 | throw new RequestError(response.status, `HTTP error: ${response.status}`);
45 | }
46 |
47 | return await response.json();
48 | } catch (err) {
49 | const error = isError(err) ? err : new Error("Unknown error");
50 |
51 | if (error.name === "AbortError") {
52 | logger.warn(`Request to ${url} timed out`);
53 | } else {
54 | logger.error(`Error fetching ${url}: ${error.message}`);
55 | }
56 |
57 | return handleError(error) as ActionResponse;
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/lib/http-errors.ts:
--------------------------------------------------------------------------------
1 | export class RequestError extends Error {
2 | statusCode: number;
3 | errors?: Record;
4 |
5 | constructor(
6 | statusCode: number,
7 | message: string,
8 | errors?: Record
9 | ) {
10 | super(message);
11 | this.statusCode = statusCode;
12 | this.errors = errors;
13 | this.name = "RequestError";
14 | }
15 | }
16 |
17 | export class ValidationError extends RequestError {
18 | constructor(fieldErrors: Record) {
19 | const message = ValidationError.formatFieldErrors(fieldErrors);
20 | super(400, message, fieldErrors);
21 | this.name = "ValidationError";
22 | this.errors = fieldErrors;
23 | }
24 |
25 | static formatFieldErrors(errors: Record): string {
26 | const formattedMessages = Object.entries(errors).map(
27 | ([field, messages]) => {
28 | const fieldName = field.charAt(0).toUpperCase() + field.slice(1);
29 |
30 | if (messages[0] === "Required") {
31 | return `${fieldName} is required`;
32 | } else {
33 | return messages.join(" and ");
34 | }
35 | }
36 | );
37 |
38 | return formattedMessages.join(", ");
39 | }
40 | }
41 |
42 | export class NotFoundError extends RequestError {
43 | constructor(resource: string) {
44 | super(404, `${resource} not found`);
45 | this.name = "NotFoundError";
46 | }
47 | }
48 |
49 | export class ForbiddenError extends RequestError {
50 | constructor(message: string = "Forbidden") {
51 | super(403, message);
52 | this.name = "ForbiddenError";
53 | }
54 | }
55 |
56 | export class UnauthorizedError extends RequestError {
57 | constructor(message: string = "Unauthorized") {
58 | super(401, message);
59 | this.name = "UnauthorizedError";
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/lib/logger.ts:
--------------------------------------------------------------------------------
1 | import pino from "pino";
2 |
3 | const isEdge = process.env.NEXT_RUNTIME === "edge";
4 | const isProduction = process.env.NODE_ENV === "production";
5 |
6 | const logger = pino({
7 | level: process.env.LOG_LEVEL || "info",
8 | transport:
9 | !isEdge && !isProduction
10 | ? {
11 | target: "pino-pretty",
12 | options: {
13 | colorize: true,
14 | ignore: "pid,hostname",
15 | translateTime: "SYS:standard",
16 | },
17 | }
18 | : undefined,
19 | formatters: {
20 | level: (label) => ({ level: label.toUpperCase() }),
21 | },
22 | timestamp: pino.stdTimeFunctions.isoTime,
23 | });
24 |
25 | export default logger;
26 |
--------------------------------------------------------------------------------
/lib/mongoose.ts:
--------------------------------------------------------------------------------
1 | import mongoose, { Mongoose } from "mongoose";
2 |
3 | import logger from "./logger";
4 | import "@/database";
5 |
6 | const MONGODB_URI = process.env.MONGODB_URI as string;
7 |
8 | if (!MONGODB_URI) {
9 | throw new Error("MONGODB_URI is not defined");
10 | }
11 |
12 | interface MongooseCache {
13 | conn: Mongoose | null;
14 | promise: Promise | null;
15 | }
16 |
17 | declare global {
18 | // eslint-disable-next-line no-var
19 | var mongoose: MongooseCache;
20 | }
21 |
22 | let cached = global.mongoose;
23 |
24 | if (!cached) {
25 | cached = global.mongoose = { conn: null, promise: null };
26 | }
27 |
28 | const dbConnect = async (): Promise => {
29 | if (cached.conn) {
30 | logger.info("Using existing mongoose connection");
31 | return cached.conn;
32 | }
33 |
34 | if (!cached.promise) {
35 | cached.promise = mongoose
36 | .connect(MONGODB_URI, {
37 | dbName: "devflow",
38 | })
39 | .then((result) => {
40 | logger.info("Connected to MongoDB");
41 | return result;
42 | })
43 | .catch((error) => {
44 | logger.error("Error connecting to MongoDB", error);
45 | throw error;
46 | });
47 | }
48 |
49 | cached.conn = await cached.promise;
50 |
51 | return cached.conn;
52 | };
53 |
54 | export default dbConnect;
55 |
--------------------------------------------------------------------------------
/lib/url.ts:
--------------------------------------------------------------------------------
1 | import qs from "query-string";
2 |
3 | interface UrlQueryParams {
4 | params: string;
5 | key: string;
6 | value: string;
7 | }
8 |
9 | interface RemoveUrlQueryParams {
10 | params: string;
11 | keysToRemove: string[];
12 | }
13 |
14 | export const formUrlQuery = ({ params, key, value }: UrlQueryParams) => {
15 | const queryString = qs.parse(params);
16 |
17 | queryString[key] = value;
18 |
19 | return qs.stringifyUrl({
20 | url: window.location.pathname,
21 | query: queryString,
22 | });
23 | };
24 |
25 | export const removeKeysFromUrlQuery = ({
26 | params,
27 | keysToRemove,
28 | }: RemoveUrlQueryParams) => {
29 | const queryString = qs.parse(params);
30 |
31 | keysToRemove.forEach((key) => {
32 | delete queryString[key];
33 | });
34 |
35 | return qs.stringifyUrl(
36 | {
37 | url: window.location.pathname,
38 | query: queryString,
39 | },
40 | { skipNull: true }
41 | );
42 | };
43 |
--------------------------------------------------------------------------------
/middleware.ts:
--------------------------------------------------------------------------------
1 | export { auth as middleware } from "@/auth";
2 |
--------------------------------------------------------------------------------
/next.config.ts:
--------------------------------------------------------------------------------
1 | import type { NextConfig } from "next";
2 |
3 | const nextConfig: NextConfig = {
4 | serverExternalPackages: ["pino", "pino-pretty"],
5 | eslint: {
6 | ignoreDuringBuilds: true,
7 | },
8 | typescript: {
9 | ignoreBuildErrors: true,
10 | },
11 | images: {
12 | remotePatterns: [
13 | {
14 | protocol: "https",
15 | hostname: "images.unsplash.com",
16 | port: "",
17 | },
18 | {
19 | protocol: "https",
20 | hostname: "avatars.githubusercontent.com",
21 | port: "",
22 | },
23 | {
24 | protocol: "https",
25 | hostname: "lh3.googleusercontent.com",
26 | port: "",
27 | },
28 | {
29 | protocol: "https",
30 | hostname: "*",
31 | port: "",
32 | },
33 | ],
34 | },
35 | };
36 |
37 | export default nextConfig;
38 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ultimate-nextjs-course",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev --turbo",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "@ai-sdk/openai": "^1.1.9",
13 | "@hookform/resolvers": "^3.10.0",
14 | "@mdxeditor/editor": "^3.21.4",
15 | "@radix-ui/react-alert-dialog": "^1.1.7",
16 | "@radix-ui/react-avatar": "^1.1.2",
17 | "@radix-ui/react-dialog": "^1.1.5",
18 | "@radix-ui/react-dropdown-menu": "^2.1.5",
19 | "@radix-ui/react-icons": "^1.3.2",
20 | "@radix-ui/react-label": "^2.1.1",
21 | "@radix-ui/react-select": "^2.1.6",
22 | "@radix-ui/react-slot": "^1.2.0",
23 | "@radix-ui/react-tabs": "^1.1.3",
24 | "@radix-ui/react-toast": "^1.2.5",
25 | "@typescript-eslint/eslint-plugin": "^8.22.0",
26 | "ai": "^4.1.34",
27 | "bcryptjs": "^2.4.3",
28 | "bright": "^1.0.0",
29 | "class-variance-authority": "^0.7.1",
30 | "clsx": "^2.1.1",
31 | "cm6-theme-basic-dark": "^0.2.0",
32 | "dayjs": "^1.11.13",
33 | "eslint-config-prettier": "^10.0.1",
34 | "eslint-config-standard": "^17.1.0",
35 | "eslint-plugin-tailwindcss": "^3.18.0",
36 | "lucide-react": "^0.474.0",
37 | "mongoose": "^8.9.5",
38 | "next": "15.1.6",
39 | "next-auth": "^5.0.0-beta.25",
40 | "next-mdx-remote": "^5.0.0",
41 | "next-themes": "^0.4.4",
42 | "pino": "^9.6.0",
43 | "pino-pretty": "^13.0.0",
44 | "query-string": "^9.1.1",
45 | "react": "19.0.0",
46 | "react-dom": "19.0.0",
47 | "react-hook-form": "^7.54.2",
48 | "slugify": "^1.6.6",
49 | "tailwind-merge": "^2.5.5",
50 | "tailwindcss-animate": "^1.0.7",
51 | "zod": "^3.24.1"
52 | },
53 | "devDependencies": {
54 | "@eslint/compat": "^1.2.5",
55 | "@eslint/eslintrc": "^3.2.0",
56 | "@eslint/js": "^9.19.0",
57 | "@tailwindcss/postcss": "^4.1.7",
58 | "@tailwindcss/typography": "^0.5.15",
59 | "@types/bcryptjs": "^2.4.6",
60 | "@types/node": "^22",
61 | "@types/react": "^19",
62 | "@types/react-dom": "^19",
63 | "eslint": "^9",
64 | "eslint-config-next": "15.1.6",
65 | "eslint-plugin-import": "^2.31.0",
66 | "postcss": "^8",
67 | "prettier": "^3.4.2",
68 | "tailwindcss": "^4.1.7",
69 | "typescript": "^5"
70 | },
71 | "packageManager": "npm@10.9.2",
72 | "overrides": {
73 | "react": "$react",
74 | "react-dom": "$react-dom",
75 | "eslint": "$eslint"
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/postcss.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('postcss-load-config').Config} */
2 | const config = {
3 | plugins: {
4 | "@tailwindcss/postcss": {},
5 | },
6 | };
7 |
8 | export default config;
9 |
--------------------------------------------------------------------------------
/public/file.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/globe.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/icons/account.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/icons/arrow-left.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/icons/arrow-right.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/icons/arrow-up-right.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/icons/au.svg:
--------------------------------------------------------------------------------
1 |
21 |
--------------------------------------------------------------------------------
/public/icons/avatar.svg:
--------------------------------------------------------------------------------
1 |
4 |
5 |
--------------------------------------------------------------------------------
/public/icons/bronze-medal.svg:
--------------------------------------------------------------------------------
1 |
35 |
--------------------------------------------------------------------------------
/public/icons/carbon-location.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/public/icons/chevron-down.svg:
--------------------------------------------------------------------------------
1 |
11 |
12 |
--------------------------------------------------------------------------------
/public/icons/chevron-right.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/icons/clock-2.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/icons/clock.svg:
--------------------------------------------------------------------------------
1 |
11 |
--------------------------------------------------------------------------------
/public/icons/close.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/public/icons/computer.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/icons/currency-dollar-circle.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/icons/downvote.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/icons/downvoted.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/icons/edit.svg:
--------------------------------------------------------------------------------
1 |
12 |
--------------------------------------------------------------------------------
/public/icons/eye.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/public/icons/github.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/public/icons/gold-medal.svg:
--------------------------------------------------------------------------------
1 |
39 |
--------------------------------------------------------------------------------
/public/icons/google.svg:
--------------------------------------------------------------------------------
1 |
12 |
--------------------------------------------------------------------------------
/public/icons/hamburger.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/public/icons/home.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/icons/job-search.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/public/icons/like.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/public/icons/link.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/public/icons/location.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/public/icons/message.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/public/icons/mingcute-down-line.svg:
--------------------------------------------------------------------------------
1 |
11 |
--------------------------------------------------------------------------------
/public/icons/moon.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/public/icons/question.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/icons/search.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/icons/sign-up.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/icons/silver-medal.svg:
--------------------------------------------------------------------------------
1 |
39 |
--------------------------------------------------------------------------------
/public/icons/star-filled.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/icons/star-red.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/icons/star.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/icons/stars.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/icons/suitcase.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/icons/sun.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/icons/tag.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/icons/trash.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/public/icons/upvote.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/icons/upvoted.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/icons/user.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/images/auth-dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adrianhajdin/jsmasterypro_devflow/22efaff5f35920de0e4778d36d2fc20d70f04296/public/images/auth-dark.png
--------------------------------------------------------------------------------
/public/images/auth-light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adrianhajdin/jsmasterypro_devflow/22efaff5f35920de0e4778d36d2fc20d70f04296/public/images/auth-light.png
--------------------------------------------------------------------------------
/public/images/dark-error.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adrianhajdin/jsmasterypro_devflow/22efaff5f35920de0e4778d36d2fc20d70f04296/public/images/dark-error.png
--------------------------------------------------------------------------------
/public/images/dark-illustration.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adrianhajdin/jsmasterypro_devflow/22efaff5f35920de0e4778d36d2fc20d70f04296/public/images/dark-illustration.png
--------------------------------------------------------------------------------
/public/images/default-logo.svg:
--------------------------------------------------------------------------------
1 |
12 |
--------------------------------------------------------------------------------
/public/images/light-error.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adrianhajdin/jsmasterypro_devflow/22efaff5f35920de0e4778d36d2fc20d70f04296/public/images/light-error.png
--------------------------------------------------------------------------------
/public/images/light-illustration.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adrianhajdin/jsmasterypro_devflow/22efaff5f35920de0e4778d36d2fc20d70f04296/public/images/light-illustration.png
--------------------------------------------------------------------------------
/public/images/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adrianhajdin/jsmasterypro_devflow/22efaff5f35920de0e4778d36d2fc20d70f04296/public/images/logo.png
--------------------------------------------------------------------------------
/public/images/site-logo.svg:
--------------------------------------------------------------------------------
1 |
17 |
--------------------------------------------------------------------------------
/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/window.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "tailwindcss";
2 |
3 | const config: Config = {
4 | darkMode: ["class"],
5 | content: [
6 | "./pages/**/*.{js,ts,jsx,tsx,mdx}",
7 | "./components/**/*.{js,ts,jsx,tsx,mdx}",
8 | "./app/**/*.{js,ts,jsx,tsx,mdx}",
9 | ],
10 | theme: {
11 | extend: {
12 | colors: {
13 | primary: {
14 | "100": "#FFF1E6",
15 | "500": "#FF7000",
16 | },
17 | dark: {
18 | "100": "#000000",
19 | "200": "#0F1117",
20 | "300": "#151821",
21 | "400": "#212734",
22 | "500": "#101012",
23 | },
24 | light: {
25 | "400": "#858EAD",
26 | "500": "#7B8EC8",
27 | "700": "#DCE3F1",
28 | "800": "#F4F6F8",
29 | "850": "#FDFDFD",
30 | "900": "#FFFFFF",
31 | },
32 | link: {
33 | "100": "#1DA1F2",
34 | },
35 | },
36 | boxShadow: {
37 | "light-100":
38 | "0px 12px 20px 0px rgba(184, 184, 184, 0.03), 0px 6px 12px 0px rgba(184, 184, 184, 0.02), 0px 2px 4px 0px rgba(184, 184, 184, 0.03)",
39 | "light-200": "10px 10px 20px 0px rgba(218, 213, 213, 0.10)",
40 | "light-300": "-10px 10px 20px 0px rgba(218, 213, 213, 0.10)",
41 | "dark-100": "0px 2px 10px 0px rgba(46, 52, 56, 0.10)",
42 | "dark-200": "2px 0px 20px 0px rgba(39, 36, 36, 0.04)",
43 | },
44 | screens: {
45 | xs: "420px",
46 | },
47 | fontFamily: {
48 | inter: ["var(--font-inter)"],
49 | "space-grotesk": ["var(--font-space-grotesk)"],
50 | },
51 | borderRadius: {
52 | "2": "8px",
53 | "1.5": "6px",
54 | lg: "var(--radius)",
55 | md: "calc(var(--radius) - 2px)",
56 | sm: "calc(var(--radius) - 4px)",
57 | },
58 | backgroundImage: {
59 | "auth-dark": 'url("/images/auth-dark.png")',
60 | "auth-light": 'url("/images/auth-light.png")',
61 | },
62 | },
63 | },
64 | plugins: [require("tailwindcss-animate"), require("@tailwindcss/typography")],
65 | };
66 | export default config;
67 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2017",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "noEmit": true,
9 | "esModuleInterop": true,
10 | "module": "esnext",
11 | "moduleResolution": "bundler",
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "jsx": "preserve",
15 | "incremental": true,
16 | "plugins": [
17 | {
18 | "name": "next"
19 | }
20 | ],
21 | "paths": {
22 | "@/*": ["./*"]
23 | }
24 | },
25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
26 | "exclude": ["node_modules"]
27 | }
28 |
--------------------------------------------------------------------------------
/types/global.d.ts:
--------------------------------------------------------------------------------
1 | type ActionResponse = {
2 | success: boolean;
3 | data?: T;
4 | error?: {
5 | message: string;
6 | details?: Record;
7 | };
8 | status?: number;
9 | };
10 |
11 | type SuccessResponse = ActionResponse & { success: true };
12 | type ErrorResponse = ActionResponse & { success: false };
13 |
14 | type APIErrorResponse = NextResponse;
15 | type APIResponse = NextResponse | ErrorResponse>;
16 |
17 | interface UrlQueryParams {
18 | params: string;
19 | key: string;
20 | value: string | null;
21 | }
22 |
23 | interface RemoveUrlQueryParams {
24 | params: string;
25 | keysToRemove: string[];
26 | }
27 |
28 | interface Tag {
29 | _id: string;
30 | name: string;
31 | questions?: number;
32 | }
33 |
34 | interface Author {
35 | _id: string;
36 | name: string;
37 | image: string;
38 | }
39 |
40 | interface Question {
41 | _id: string;
42 | title: string;
43 | content: string;
44 | tags: Tag[];
45 | author: Author;
46 | createdAt: Date;
47 | upvotes: number;
48 | downvotes: number;
49 | answers: number;
50 | views: number;
51 | }
52 |
53 | interface Answer {
54 | _id: string;
55 | author: Author;
56 | content: string;
57 | upvotes: number;
58 | question: string;
59 | downvotes: number;
60 | createdAt: Date;
61 | }
62 |
63 | interface RouteParams {
64 | params: Promise>;
65 | searchParams: Promise>;
66 | }
67 |
68 | interface PaginatedSearchParams {
69 | page?: number;
70 | pageSize?: number;
71 | query?: string;
72 | filter?: string;
73 | sort?: string;
74 | }
75 |
76 | interface Collection {
77 | _id: string;
78 | author: string | Author;
79 | question: Question;
80 | }
81 |
82 | interface User {
83 | _id: string;
84 | name: string;
85 | username: string;
86 | email: string;
87 | bio?: string;
88 | image?: string;
89 | location?: string;
90 | portfolio?: string;
91 | reputation?: number;
92 | createdAt: Date;
93 | }
94 |
95 | interface Badges {
96 | GOLD: number;
97 | SILVER: number;
98 | BRONZE: number;
99 | }
100 |
101 | interface Job {
102 | id?: string;
103 | employer_name?: string;
104 | employer_logo?: string | undefined;
105 | employer_website?: string;
106 | job_employment_type?: string;
107 | job_title?: string;
108 | job_description?: string;
109 | job_apply_link?: string;
110 | job_city?: string;
111 | job_state?: string;
112 | job_country?: string;
113 | }
114 |
115 | interface Country {
116 | name: {
117 | common: string;
118 | };
119 | }
120 |
121 | interface GlobalSearchedItem {
122 | id: string;
123 | type: "question" | "answer" | "user" | "tag";
124 | title: string;
125 | }
126 |
--------------------------------------------------------------------------------