├── .eslintrc.json
├── .gitignore
├── .vscode
└── settings.json
├── README.md
├── app
├── (auth)
│ ├── layout.tsx
│ ├── sign-in
│ │ └── [[...sign-in]]
│ │ │ └── page.tsx
│ └── sign-up
│ │ └── [[...sign-up]]
│ │ └── page.tsx
├── (root)
│ ├── (home)
│ │ ├── loading.tsx
│ │ └── page.tsx
│ ├── ask-question
│ │ └── page.tsx
│ ├── collection
│ │ ├── loading.tsx
│ │ └── page.tsx
│ ├── community
│ │ ├── loading.tsx
│ │ └── page.tsx
│ ├── error.tsx
│ ├── jobs
│ │ ├── loading.tsx
│ │ └── page.tsx
│ ├── layout.tsx
│ ├── profile
│ │ ├── [id]
│ │ │ ├── loading.tsx
│ │ │ └── page.tsx
│ │ └── edit
│ │ │ └── page.tsx
│ ├── question
│ │ ├── [id]
│ │ │ └── page.tsx
│ │ └── edit
│ │ │ └── [id]
│ │ │ └── page.tsx
│ └── tags
│ │ ├── [id]
│ │ ├── loading.tsx
│ │ └── page.tsx
│ │ ├── loading.tsx
│ │ └── page.tsx
├── Providers.tsx
├── api
│ ├── chatgpt
│ │ └── route.ts
│ └── webhook
│ │ └── route.ts
├── favicon.ico
├── globals.css
└── layout.tsx
├── components.json
├── components
├── cards
│ ├── AnswerCard.tsx
│ ├── JobCard.tsx
│ ├── QuestionCard.tsx
│ └── UserCard.tsx
├── forms
│ ├── Answer.tsx
│ ├── Profile.tsx
│ └── Question.tsx
├── home
│ └── HomeFilters.tsx
├── shared
│ ├── AllAnswers.tsx
│ ├── AnswersTab.tsx
│ ├── EditDeleteAction.tsx
│ ├── Filter.tsx
│ ├── JobFilters.tsx
│ ├── LeftSidebar.tsx
│ ├── Metric.tsx
│ ├── NoResult.tsx
│ ├── Pagination.tsx
│ ├── ParseHTML.tsx
│ ├── ProfileLink.tsx
│ ├── QuestionsTab.tsx
│ ├── RenderTags.tsx
│ ├── RightSidebar.tsx
│ ├── Stats.tsx
│ ├── Votes.tsx
│ ├── navbar
│ │ ├── MobileNav.tsx
│ │ ├── Navbar.tsx
│ │ └── Theme.tsx
│ └── search
│ │ ├── GlobalFilters.tsx
│ │ ├── GlobalResult.tsx
│ │ ├── GlobalSearch.tsx
│ │ └── LocalSearch.tsx
└── ui
│ ├── badge.tsx
│ ├── button.tsx
│ ├── form.tsx
│ ├── input.tsx
│ ├── label.tsx
│ ├── menubar.tsx
│ ├── select.tsx
│ ├── sheet.tsx
│ ├── skeleton.tsx
│ ├── tabs.tsx
│ ├── textarea.tsx
│ ├── toast.tsx
│ ├── toaster.tsx
│ └── use-toast.ts
├── constants
├── filters.ts
└── index.ts
├── database
├── answer.model.ts
├── interaction.model.ts
├── question.model.ts
├── tag.model.ts
└── user.model.ts
├── lib
├── actions
│ ├── answer.actions.ts
│ ├── general.action.ts
│ ├── interaction.action.ts
│ ├── job.action.ts
│ ├── question.action.ts
│ ├── shared.d.ts
│ ├── tags.actions.ts
│ └── user.actions.ts
├── mongoose.ts
├── utils.ts
└── validations.ts
├── middleware.ts
├── next.config.js
├── package-lock.json
├── package.json
├── postcss.config.js
├── public
├── assets
│ ├── 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
│ │ ├── gold-medal.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-illustration.png
│ │ ├── default-logo.svg
│ │ ├── light-illustration.png
│ │ ├── logo-dark.svg
│ │ ├── logo-light.svg
│ │ ├── logo.png
│ │ └── site-logo.svg
├── next.svg
└── vercel.svg
├── styles
├── prism.css
└── theme.css
├── tailwind.config.ts
├── tsconfig.json
└── types
└── index.d.ts
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "next/core-web-vitals",
4 | "standard",
5 | "plugin:tailwindcss/recommended",
6 | "prettier"
7 | ]
8 | }
9 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 |
27 | # local env files
28 | .env*.local
29 |
30 | # vercel
31 | .vercel
32 |
33 | # typescript
34 | *.tsbuildinfo
35 | next-env.d.ts
36 |
--------------------------------------------------------------------------------
/.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 | "[typescriptreact]": {
9 | "editor.defaultFormatter": "esbenp.prettier-vscode"
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # DevOverFlow
2 |
3 |
A community-driven Q&A platform tailored for programming enthusiasts.
4 |
5 | ## Preview
6 | ### Home
7 | 
8 | ### Community
9 | 
10 | ### Jobs
11 | 
12 | ### Profile
13 | 
14 | ### Ask a question
15 | 
16 |
17 | ## Key Features
18 |
19 | - Ask questions and answer questions.
20 | - Upvote, Downvote, and save questions.
21 | - Include code snippets in your answers.
22 | - Searching and filtering.
23 | - View Top Questions and Popular Tags.
24 | - Built-in recommendation algorithm.
25 | - Global Search across the database.
26 | - View all tags and tag-related questions.
27 | - View and Edit your profile.
28 | - Built-in badge system for earning badges.
29 | - View, search jobs or filter by location.
30 | - Light and Dark Mode.
31 |
32 | ## Tech Stack
33 |
34 | - Next.js 14
35 | - TypeScript
36 | - Tailwind CSS
37 | - MongoDB
38 | - Mongoose
39 | - Clerk for Authentication
40 | - Shadcn UI for reusable components
41 | - PrismJS for syntax highlighting
42 | - React Icons
43 | - Zod for Form validation
44 | - TinyMCE for the editor
45 | - Query String
46 | - Next themes for theme management
47 | - JSearch API for job searching
48 | - Vercel for deployment
49 |
--------------------------------------------------------------------------------
/app/(auth)/layout.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const Layout = ({ children }: { children: React.ReactNode }) => {
4 | return (
5 |
6 | {children}
7 |
8 | );
9 | };
10 |
11 | export default Layout;
12 |
--------------------------------------------------------------------------------
/app/(auth)/sign-in/[[...sign-in]]/page.tsx:
--------------------------------------------------------------------------------
1 | import { SignIn } from "@clerk/nextjs";
2 |
3 | export default function Page() {
4 | return ;
5 | }
6 |
--------------------------------------------------------------------------------
/app/(auth)/sign-up/[[...sign-up]]/page.tsx:
--------------------------------------------------------------------------------
1 | import { SignUp } from "@clerk/nextjs";
2 |
3 | export default function Page() {
4 | return ;
5 | }
6 |
--------------------------------------------------------------------------------
/app/(root)/(home)/loading.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 |
3 | import { Button } from "@/components/ui/button";
4 | import { Skeleton } from "@/components/ui/skeleton";
5 |
6 | const Loading = () => {
7 | return (
8 |
9 |
10 |
All Questions
11 |
12 |
15 |
16 |
17 |
18 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | {[1, 2, 3, 4, 5, 6, 10].map((item) => (
34 |
35 | ))}
36 |
37 |
38 | );
39 | };
40 |
41 | export default Loading;
42 |
--------------------------------------------------------------------------------
/app/(root)/(home)/page.tsx:
--------------------------------------------------------------------------------
1 | import Filter from "@/components/shared/Filter";
2 | import { Button } from "@/components/ui/button";
3 | import NoResult from "@/components/shared/NoResult";
4 | import HomeFilters from "@/components/home/HomeFilters";
5 | import Pagination from "@/components/shared/Pagination";
6 | import QuestionCard from "@/components/cards/QuestionCard";
7 | import LocalSearch from "@/components/shared/search/LocalSearch";
8 |
9 | import {
10 | getQuestions,
11 | getRecommendedQuestions,
12 | } from "@/lib/actions/question.action";
13 |
14 | import { auth } from "@clerk/nextjs";
15 |
16 | import Link from "next/link";
17 |
18 | import type { Metadata } from "next";
19 |
20 | import { SearchParamsProps } from "@/types";
21 |
22 | import { HomePageFilters } from "@/constants/filters";
23 |
24 | export const metadata: Metadata = {
25 | title: "Home | DevOverFlow",
26 | };
27 |
28 | export default async function Home({ searchParams }: SearchParamsProps) {
29 | const { userId } = auth();
30 |
31 | let result;
32 |
33 | if (searchParams?.filter === "recommended") {
34 | if (userId) {
35 | result = await getRecommendedQuestions({
36 | userId,
37 | searchQuery: searchParams.q,
38 | page: searchParams.page ? +searchParams.page : 1,
39 | });
40 | } else {
41 | result = {
42 | questions: [],
43 | isNext: false,
44 | };
45 | }
46 | } else {
47 | result = await getQuestions({
48 | searchQuery: searchParams.q,
49 | filter: searchParams.filter,
50 | page: searchParams.page ? +searchParams.page : 1,
51 | });
52 | }
53 |
54 | return (
55 | <>
56 |
57 |
All Questions
58 |
59 |
60 |
63 |
64 |
65 |
66 |
67 |
74 |
79 |
80 |
81 |
82 |
83 |
84 | {result.questions.length > 0 ? (
85 | result.questions.map((question) => (
86 |
97 | ))
98 | ) : (
99 |
105 | )}
106 |
107 |
113 | >
114 | );
115 | }
116 |
--------------------------------------------------------------------------------
/app/(root)/ask-question/page.tsx:
--------------------------------------------------------------------------------
1 | import Question from "@/components/forms/Question";
2 |
3 | import { auth } from "@clerk/nextjs";
4 |
5 | import { redirect } from "next/navigation";
6 |
7 | import type { Metadata } from "next";
8 |
9 | import { getUserById } from "@/lib/actions/user.actions";
10 |
11 | export const metadata: Metadata = {
12 | title: "Ask a Question | DevOverFlow",
13 | };
14 |
15 | const Page = async () => {
16 | const { userId } = auth();
17 |
18 | if (!userId) {
19 | redirect("/sign-in");
20 | }
21 |
22 | const mongoUser = await getUserById({ userId });
23 |
24 | // console.log(mongoUser);
25 |
26 | return (
27 |
28 |
Ask a Question
29 |
30 |
31 |
32 |
33 | );
34 | };
35 |
36 | export default Page;
37 |
--------------------------------------------------------------------------------
/app/(root)/collection/loading.tsx:
--------------------------------------------------------------------------------
1 | import { Skeleton } from "@/components/ui/skeleton";
2 |
3 | const Loading = () => {
4 | return (
5 |
6 | Saved Questions
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | {[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((item) => (
15 |
16 | ))}
17 |
18 |
19 | );
20 | };
21 |
22 | export default Loading;
23 |
--------------------------------------------------------------------------------
/app/(root)/collection/page.tsx:
--------------------------------------------------------------------------------
1 | import Filter from "@/components/shared/Filter";
2 | import NoResult from "@/components/shared/NoResult";
3 | import Pagination from "@/components/shared/Pagination";
4 | import QuestionCard from "@/components/cards/QuestionCard";
5 | import LocalSearch from "@/components/shared/search/LocalSearch";
6 |
7 | import { QuestionFilters } from "@/constants/filters";
8 |
9 | import { getSavedQuestions } from "@/lib/actions/user.actions";
10 |
11 | import { SearchParamsProps } from "@/types";
12 |
13 | import { auth } from "@clerk/nextjs";
14 |
15 | import type { Metadata } from "next";
16 |
17 | export const metadata: Metadata = {
18 | title: "Saved Questions | DevOverFlow",
19 | };
20 |
21 | export default async function Home({ searchParams }: SearchParamsProps) {
22 | const { userId } = auth();
23 |
24 | if (!userId) return null;
25 |
26 | const result = await getSavedQuestions({
27 | clerkId: userId,
28 | searchQuery: searchParams.q,
29 | filter: searchParams.filter,
30 | page: searchParams.page ? +searchParams.page : 1,
31 | });
32 |
33 | return (
34 | <>
35 | Saved Questions
36 |
37 |
38 |
45 |
49 |
50 |
51 |
52 | {result.questions.length > 0 ? (
53 | result.questions.map((question: any) => (
54 |
65 | ))
66 | ) : (
67 |
73 | )}
74 |
75 |
81 | >
82 | );
83 | }
84 |
--------------------------------------------------------------------------------
/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 | {[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((item) => (
13 |
17 | ))}
18 |
19 |
20 | );
21 | };
22 |
23 | export default Loading;
24 |
--------------------------------------------------------------------------------
/app/(root)/community/page.tsx:
--------------------------------------------------------------------------------
1 | import Filter from "@/components/shared/Filter";
2 | import { UserFilters } from "@/constants/filters";
3 | import UserCard from "@/components/cards/UserCard";
4 | import Pagination from "@/components/shared/Pagination";
5 | import LocalSearch from "@/components/shared/search/LocalSearch";
6 |
7 | import { getAllUsers } from "@/lib/actions/user.actions";
8 |
9 | import { SearchParamsProps } from "@/types";
10 |
11 | import type { Metadata } from "next";
12 |
13 | import Link from "next/link";
14 |
15 | export const metadata: Metadata = {
16 | title: "Community | DevOverFlow",
17 | };
18 |
19 | const Page = async ({ searchParams }: SearchParamsProps) => {
20 | const results = await getAllUsers({
21 | searchQuery: searchParams.q,
22 | filter: searchParams.filter,
23 | page: searchParams.page ? +searchParams.page : 1,
24 | });
25 |
26 | return (
27 | <>
28 | All Users
29 |
30 |
37 |
41 |
42 |
43 |
44 | {results.users.length > 0 ? (
45 | results.users.map((user) => )
46 | ) : (
47 |
48 |
No Users yet
49 |
50 | Join to be the first!
51 |
52 |
53 | )}
54 |
55 |
61 | >
62 | );
63 | };
64 |
65 | export default Page;
66 |
--------------------------------------------------------------------------------
/app/(root)/error.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | const error = () => {
4 | return (
5 |
6 |
7 | Oops! Something went wrong. Please try again later.
8 |
9 |
10 | );
11 | };
12 |
13 | export default error;
14 |
--------------------------------------------------------------------------------
/app/(root)/jobs/loading.tsx:
--------------------------------------------------------------------------------
1 | import { Skeleton } from "@/components/ui/skeleton";
2 |
3 | const Loading = () => {
4 | return (
5 |
6 | Jobs
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | {[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((item) => (
15 |
16 | ))}
17 |
18 |
19 | );
20 | };
21 |
22 | export default Loading;
23 |
--------------------------------------------------------------------------------
/app/(root)/jobs/page.tsx:
--------------------------------------------------------------------------------
1 | import JobCard from "@/components/cards/JobCard";
2 | import JobFilters from "@/components/shared/JobFilters";
3 | import Pagination from "@/components/shared/Pagination";
4 | import LocalSearch from "@/components/shared/search/LocalSearch";
5 |
6 | import {
7 | fetchCountries,
8 | fetchJobs,
9 | fetchLocation,
10 | } from "@/lib/actions/job.action";
11 |
12 | import { Job } from "@/types";
13 |
14 | import type { Metadata } from "next";
15 |
16 | export const metadata: Metadata = {
17 | title: "Jobs | DevOverFlow",
18 | };
19 |
20 | interface Props {
21 | searchParams: {
22 | q: string;
23 | location: string;
24 | page: string;
25 | };
26 | }
27 |
28 | const Page = async ({ searchParams }: Props) => {
29 | const userLocation = await fetchLocation();
30 |
31 | const jobs = await fetchJobs({
32 | query:
33 | `${searchParams.q}, ${searchParams.location}` ??
34 | `Software Engineer in ${userLocation}`,
35 | page: searchParams.page ?? 1,
36 | });
37 |
38 | const countries = await fetchCountries();
39 |
40 | const page = parseInt(searchParams.page ?? 1);
41 |
42 | return (
43 | <>
44 |
45 |
Jobs
46 |
47 |
48 |
55 |
56 |
57 |
58 |
59 |
60 | {jobs.length > 0 ? (
61 | jobs.map((job: Job) => {
62 | if (job.job_title && job.job_title.toLowerCase() !== "undefined")
63 | return ;
64 |
65 | return null;
66 | })
67 | ) : (
68 |
69 | Oops! We couldn't find any jobs at the moment. Please try again
70 | later
71 |
72 | )}
73 |
74 |
75 | {jobs.length > 0 && (
76 |
77 | )}
78 | >
79 | );
80 | };
81 |
82 | export default Page;
83 |
--------------------------------------------------------------------------------
/app/(root)/layout.tsx:
--------------------------------------------------------------------------------
1 | import Navbar from "@/components/shared/navbar/Navbar";
2 | import LeftSidebar from "@/components/shared/LeftSidebar";
3 | import RightSidebar from "@/components/shared/RightSidebar";
4 |
5 | import { Toaster } from "@/components/ui/toaster";
6 |
7 | import React from "react";
8 |
9 | const Layout = ({ children }: { children: React.ReactNode }) => {
10 | return (
11 |
12 |
13 |
20 |
21 |
22 | );
23 | };
24 |
25 | export default Layout;
26 |
--------------------------------------------------------------------------------
/app/(root)/profile/[id]/loading.tsx:
--------------------------------------------------------------------------------
1 | import { Skeleton } from "@/components/ui/skeleton";
2 |
3 | const Loading = () => {
4 | return (
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 | {[1, 2, 3, 4, 5].map((item) => (
43 |
44 | ))}
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 | );
62 | };
63 |
64 | export default Loading;
65 |
--------------------------------------------------------------------------------
/app/(root)/profile/edit/page.tsx:
--------------------------------------------------------------------------------
1 | import Profile from "@/components/forms/Profile";
2 |
3 | import { ParamsProps } from "@/types";
4 |
5 | import { auth } from "@clerk/nextjs";
6 |
7 | import { getUserById } from "@/lib/actions/user.actions";
8 |
9 | const Page = async ({ params }: ParamsProps) => {
10 | const { userId } = auth();
11 |
12 | if (!userId) return null;
13 |
14 | const mongoUser = await getUserById({ userId });
15 |
16 | return (
17 | <>
18 | Edit Profile
19 |
22 | >
23 | );
24 | };
25 |
26 | export default Page;
27 |
--------------------------------------------------------------------------------
/app/(root)/question/[id]/page.tsx:
--------------------------------------------------------------------------------
1 | import Votes from "@/components/shared/Votes";
2 | import Answer from "@/components/forms/Answer";
3 | import Metric from "@/components/shared/Metric";
4 | import ParseHTML from "@/components/shared/ParseHTML";
5 | import AllAnswers from "@/components/shared/AllAnswers";
6 | import RenderTags from "@/components/shared/RenderTags";
7 |
8 | import { getUserById } from "@/lib/actions/user.actions";
9 | import { formatNumber, getTimeStamp } from "@/lib/utils";
10 | import { getQuestionById } from "@/lib/actions/question.action";
11 |
12 | import { auth } from "@clerk/nextjs";
13 |
14 | import Link from "next/link";
15 | import Image from "next/image";
16 |
17 | const Page = async ({ params, searchParams }: any) => {
18 | const result = await getQuestionById({ questionId: params.id });
19 |
20 | const { userId: clerkId } = auth();
21 |
22 | let mongoUser;
23 |
24 | if (clerkId) {
25 | mongoUser = await getUserById({ userId: clerkId });
26 | }
27 |
28 | return (
29 | <>
30 |
31 |
32 |
36 |
43 |
44 | {result.author.name}
45 |
46 |
47 |
48 |
58 |
59 |
60 |
61 | {result.title}
62 |
63 |
64 |
65 |
66 |
73 |
80 |
87 |
88 |
89 |
90 |
91 |
92 | {result.tags.map((tag: any) => (
93 |
99 | ))}
100 |
101 |
102 |
109 |
110 |
115 | >
116 | );
117 | };
118 |
119 | export default Page;
120 |
--------------------------------------------------------------------------------
/app/(root)/question/edit/[id]/page.tsx:
--------------------------------------------------------------------------------
1 | import Question from "@/components/forms/Question";
2 |
3 | import { ParamsProps } from "@/types";
4 |
5 | import { getUserById } from "@/lib/actions/user.actions";
6 | import { getQuestionById } from "@/lib/actions/question.action";
7 |
8 | import { auth } from "@clerk/nextjs";
9 |
10 | const Page = async ({ params }: ParamsProps) => {
11 | const { userId } = auth();
12 |
13 | if (!userId) return null;
14 |
15 | const mongoUser = await getUserById({ userId });
16 | const result = await getQuestionById({ questionId: params.id });
17 |
18 | return (
19 | <>
20 | Edit Question
21 |
22 |
27 |
28 | >
29 | );
30 | };
31 |
32 | export default Page;
33 |
--------------------------------------------------------------------------------
/app/(root)/tags/[id]/loading.tsx:
--------------------------------------------------------------------------------
1 | import { Skeleton } from "@/components/ui/skeleton";
2 |
3 | const Loading = () => {
4 | return (
5 |
6 |
7 |
8 |
9 |
10 |
11 | {[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((item) => (
12 |
13 | ))}
14 |
15 |
16 | );
17 | };
18 |
19 | export default Loading;
20 |
--------------------------------------------------------------------------------
/app/(root)/tags/[id]/page.tsx:
--------------------------------------------------------------------------------
1 | import NoResult from "@/components/shared/NoResult";
2 | import Pagination from "@/components/shared/Pagination";
3 | import QuestionCard from "@/components/cards/QuestionCard";
4 | import LocalSearch from "@/components/shared/search/LocalSearch";
5 |
6 | import { getQuestionsByTagId } from "@/lib/actions/tags.actions";
7 |
8 | import { URLProps } from "@/types";
9 |
10 | const Page = async ({ params, searchParams }: URLProps) => {
11 | const result = await getQuestionsByTagId({
12 | tagId: params.id,
13 | searchQuery: searchParams.q,
14 | page: searchParams.page ? +searchParams.page : 1,
15 | });
16 |
17 | return (
18 | <>
19 | {result.tagTitle}
20 |
21 |
22 |
29 |
30 |
31 |
32 | {result.questions.length > 0 ? (
33 | result.questions.map((question: any) => (
34 |
45 | ))
46 | ) : (
47 |
53 | )}
54 |
55 |
61 | >
62 | );
63 | };
64 |
65 | export default Page;
66 |
--------------------------------------------------------------------------------
/app/(root)/tags/loading.tsx:
--------------------------------------------------------------------------------
1 | import { Skeleton } from "@/components/ui/skeleton";
2 |
3 | const Loading = () => {
4 | return (
5 |
6 | Tags
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)/tags/page.tsx:
--------------------------------------------------------------------------------
1 | import Filter from "@/components/shared/Filter";
2 | import NoResult from "@/components/shared/NoResult";
3 | import LocalSearch from "@/components/shared/search/LocalSearch";
4 | import { TagFilters } from "@/constants/filters";
5 | import { getAllTags } from "@/lib/actions/tags.actions";
6 | import Link from "next/link";
7 | import { SearchParamsProps } from "@/types";
8 | import Pagination from "@/components/shared/Pagination";
9 | import type { Metadata } from "next";
10 |
11 | export const metadata: Metadata = {
12 | title: "Tags | DevOverFlow",
13 | };
14 |
15 | const Page = async ({ searchParams }: SearchParamsProps) => {
16 | const results = await getAllTags({
17 | searchQuery: searchParams.q,
18 | filter: searchParams.filter,
19 | page: searchParams.page ? +searchParams.page : 1,
20 | });
21 | return (
22 | <>
23 | All Tags
24 |
25 |
32 |
36 |
37 |
38 |
39 | {results.tags.length > 0 ? (
40 | results.tags.map((tag) => (
41 |
42 |
43 |
44 |
45 | {tag.name}
46 |
47 |
48 |
49 |
50 | {tag.questions.length}
51 | {" "}
52 | {tag.questions.length > 1 ? "+Questions" : "Question"}
53 |
54 |
55 |
56 | ))
57 | ) : (
58 |
64 | )}
65 |
66 |
72 | >
73 | );
74 | };
75 |
76 | export default Page;
77 |
--------------------------------------------------------------------------------
/app/Providers.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React from "react";
4 |
5 | import { ThemeProvider } from "next-themes";
6 |
7 | export function Providers({ children }: { children: React.ReactNode }) {
8 | return (
9 |
10 | {children}
11 |
12 | );
13 | }
14 |
--------------------------------------------------------------------------------
/app/api/chatgpt/route.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from "next/server";
2 |
3 | export const POST = async (request: Request) => {
4 | const { question } = await request.json();
5 |
6 | try {
7 | const response = await fetch("https://api.openai.com/v1/chat/completions", {
8 | method: "POST",
9 | headers: {
10 | "Content-Type": "application/json",
11 | Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
12 | },
13 | body: JSON.stringify({
14 | model: "gpt-3.5-turbo",
15 | messages: [
16 | {
17 | role: "system",
18 | content:
19 | "You are a knowlegeable assistant that provides quality information.",
20 | },
21 | {
22 | role: "user",
23 | content: `Tell me ${question}`,
24 | },
25 | ],
26 | }),
27 | });
28 |
29 | const responseData = await response.json();
30 | const reply = responseData.choices[0].message.content;
31 |
32 | return NextResponse.json({ reply });
33 | } catch (error: any) {
34 | return NextResponse.json({ error: error.message });
35 | }
36 | };
37 |
--------------------------------------------------------------------------------
/app/api/webhook/route.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable camelcase */
2 | import { Webhook } from "svix";
3 |
4 | import { headers } from "next/headers";
5 |
6 | import { NextResponse } from "next/server";
7 |
8 | import { WebhookEvent } from "@clerk/nextjs/server";
9 |
10 | import { createUser, deleteUser, updateUser } from "@/lib/actions/user.actions";
11 |
12 | export async function POST(req: Request) {
13 | // You can find this in the Clerk Dashboard -> Webhooks -> choose the webhook
14 | const WEBHOOK_SECRET = process.env.NEXT_CLERK_WEBHOOK_SECRET;
15 |
16 | if (!WEBHOOK_SECRET) {
17 | throw new Error(
18 | "Please add WEBHOOK_SECRET from Clerk Dashboard to .env or .env.local"
19 | );
20 | }
21 |
22 | // Get the headers
23 | const headerPayload = headers();
24 | const svix_id = headerPayload.get("svix-id");
25 | const svix_timestamp = headerPayload.get("svix-timestamp");
26 | const svix_signature = headerPayload.get("svix-signature");
27 |
28 | // If there are no headers, error out
29 | if (!svix_id || !svix_timestamp || !svix_signature) {
30 | return new Response("Error occured -- no svix headers", {
31 | status: 400,
32 | });
33 | }
34 |
35 | // Get the body
36 | const payload = await req.json();
37 | const body = JSON.stringify(payload);
38 |
39 | // Create a new SVIX instance with your secret.
40 | const wh = new Webhook(WEBHOOK_SECRET);
41 |
42 | let evt: WebhookEvent;
43 |
44 | // Verify the payload with the headers
45 | try {
46 | evt = wh.verify(body, {
47 | "svix-id": svix_id,
48 | "svix-timestamp": svix_timestamp,
49 | "svix-signature": svix_signature,
50 | }) as WebhookEvent;
51 | } catch (err) {
52 | console.error("Error verifying webhook:", err);
53 | return new Response("Error occured", {
54 | status: 400,
55 | });
56 | }
57 |
58 | // Get the ID and type
59 | // const { id } = evt.data;
60 | const eventType = evt.type;
61 |
62 | if (eventType === "user.created") {
63 | // from Clerk
64 | const { id, email_addresses, image_url, username, first_name, last_name } =
65 | evt.data;
66 |
67 | // create a new user in your database
68 | const mongoUser = await createUser({
69 | clerkId: id,
70 | name: `${first_name} ${last_name ? `${last_name}` : ""}`,
71 | username: username!,
72 | email: email_addresses[0].email_address,
73 | picture: image_url,
74 | });
75 |
76 | return NextResponse.json({ message: "OK", user: mongoUser });
77 | }
78 |
79 | if (eventType === "user.updated") {
80 | // from Clerk
81 | const { id, email_addresses, image_url, username, first_name, last_name } =
82 | evt.data;
83 |
84 | // create a new user in your database
85 | const mongoUser = await updateUser({
86 | clerkId: id,
87 | updateData: {
88 | name: `${first_name} ${last_name ? `${last_name}` : ""}`,
89 | username: username!,
90 | email: email_addresses[0].email_address,
91 | picture: image_url,
92 | },
93 | path: `/profile/${id}`,
94 | });
95 |
96 | return NextResponse.json({ message: "OK", user: mongoUser });
97 | }
98 |
99 | if (eventType === "user.deleted") {
100 | const { id } = evt.data;
101 |
102 | const deletedUser = await deleteUser({
103 | clerkId: id!,
104 | });
105 |
106 | return NextResponse.json({ message: "OK", user: deletedUser });
107 | }
108 |
109 | return NextResponse.json({ message: "OK" });
110 | }
111 |
--------------------------------------------------------------------------------
/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sougata-github/DevOverFlow/3b952925a2ce9059c942b9ecc6e2587ac94f2bda/app/favicon.ico
--------------------------------------------------------------------------------
/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @import url("../styles/theme.css");
6 |
7 | body {
8 | font-family: "Inter", sans-serif;
9 | }
10 |
11 | @layer utilities {
12 | .flex-center {
13 | @apply flex justify-center items-center;
14 | }
15 |
16 | .flex-between {
17 | @apply flex justify-between items-center;
18 | }
19 |
20 | .flex-start {
21 | @apply flex justify-start items-center;
22 | }
23 |
24 | .card-wrapper {
25 | @apply bg-light-900 dark:dark-gradient shadow-light-100 dark:shadow-dark-100;
26 | }
27 |
28 | .btn {
29 | @apply bg-light-800 dark:bg-dark-300 !important;
30 | }
31 |
32 | .btn-secondary {
33 | @apply bg-light-800 dark:bg-dark-400 !important;
34 | }
35 |
36 | .btn-tertiary {
37 | @apply bg-light-700 dark:bg-dark-300 !important;
38 | }
39 |
40 | .markdown {
41 | @apply max-w-full prose dark:prose-p:text-light-700 dark:prose-ol:text-light-700 dark:prose-ul:text-light-500 dark:prose-strong:text-white dark:prose-headings:text-white prose-headings:text-dark-400 prose-h1:text-dark-300 prose-h2:text-dark-300 prose-p:text-dark-500 prose-ul:text-dark-500 prose-ol:text-dark-500;
42 | }
43 |
44 | .primary-gradient {
45 | background: linear-gradient(129deg, #ff7000 0%, #e2995f 100%);
46 | }
47 |
48 | .dark-gradient {
49 | background: linear-gradient(
50 | 232deg,
51 | rgba(23, 28, 35, 0.41) 0%,
52 | rgba(19, 22, 28, 0.7) 100%
53 | );
54 | }
55 |
56 | .tab {
57 | @apply min-h-full dark:bg-dark-400 bg-light-800 text-light-500 dark:data-[state=active]:bg-dark-300 data-[state=active]:bg-primary-100 data-[state=active]:text-primary-500 !important;
58 | }
59 | }
60 |
61 | .no-focus {
62 | @apply focus-visible:ring-0 focus-visible:ring-transparent focus-visible:ring-offset-0 !important;
63 | }
64 |
65 | .active-theme {
66 | filter: invert(53%) sepia(98%) saturate(3332%) hue-rotate(0deg)
67 | brightness(104%) contrast(106%) !important;
68 | }
69 |
70 | .light-gradient {
71 | background: linear-gradient(
72 | 132deg,
73 | rgba(247, 249, 255, 0.5) 0%,
74 | rgba(229, 237, 255, 0.25) 100%
75 | );
76 | }
77 |
78 | .primary-text-gradient {
79 | background: linear-gradient(129deg, #ff7000 0%, #e2995f 100%);
80 | background-clip: text;
81 | -webkit-background-clip: text;
82 | -webkit-text-fill-color: transparent;
83 | }
84 |
85 | .custom-scrollbar::-webkit-scrollbar {
86 | width: 5px;
87 | height: 3px;
88 | border-radius: 2px;
89 | }
90 |
91 | .custom-scrollbar::-webkit-scrollbar-track {
92 | background: transparent;
93 | border-radius: 50px;
94 | }
95 |
96 | .custom-scrollbar::-webkit-scrollbar-thumb {
97 | background: #888;
98 | border-radius: 50px;
99 | }
100 |
101 | .scrollbar-hidden::-webkit-scrollbar {
102 | display: none;
103 | }
104 |
105 | /* Markdown Start */
106 | .markdown a {
107 | color: #1da1f2;
108 | }
109 |
110 | .markdown a,
111 | code {
112 | /* These are technically the same, but use both */
113 | overflow-wrap: break-word;
114 | word-wrap: break-word;
115 |
116 | -ms-word-break: break-all;
117 | /* This is the dangerous one in WebKit, as it breaks things wherever */
118 | word-break: break-all;
119 | /* Instead use this non-standard one: */
120 | word-break: break-word;
121 |
122 | /* Adds a hyphen where the word breaks, if supported (No Blink) */
123 | -ms-hyphens: auto;
124 | -moz-hyphens: auto;
125 | -webkit-hyphens: auto;
126 | hyphens: auto;
127 |
128 | padding: 2px;
129 | color: #ff7000 !important;
130 | }
131 |
132 | .markdown pre {
133 | display: grid;
134 | width: 100%;
135 | }
136 |
137 | .markdown pre code {
138 | width: 100%;
139 | display: block;
140 | overflow-x: auto;
141 |
142 | color: inherit !important;
143 | }
144 | /* Markdown End */
145 |
146 | /* Clerk */
147 | .cl-internal-b3fm6y {
148 | background: linear-gradient(129deg, #ff7000 0%, #e2995f 100%) !important;
149 | }
150 |
151 | .hash-span {
152 | margin-top: -140px;
153 | padding-bottom: 140px;
154 | display: block;
155 | }
156 |
157 | /* Hide scrollbar for Chrome, Safari and Opera */
158 | .no-scrollbar::-webkit-scrollbar {
159 | display: none;
160 | }
161 |
162 | /* Hide scrollbar for IE, Edge and Firefox */
163 | .no-scrollbar {
164 | -ms-overflow-style: none; /* IE and Edge */
165 | scrollbar-width: none; /* Firefox */
166 | }
167 |
--------------------------------------------------------------------------------
/app/layout.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable camelcase */
2 | import React from "react";
3 |
4 | import "./globals.css";
5 | import "../styles/prism.css";
6 |
7 | import { Providers } from "./Providers";
8 |
9 | import { ClerkProvider } from "@clerk/nextjs";
10 |
11 | import type { Metadata } from "next";
12 |
13 | import { Space_Grotesk } from "next/font/google";
14 |
15 | // const inter = Inter({
16 | // subsets: ["latin"],
17 | // weight: ["100", "200", "300", "400", "500", "600", "700", "800", "900"],
18 | // variable: "--font-inter",
19 | // });
20 |
21 | const spaceGrotesk = Space_Grotesk({
22 | subsets: ["latin"],
23 | weight: ["300", "400", "500", "600", "700"],
24 | variable: "--font-spaceGrotesk ",
25 | });
26 |
27 | export const metadata: Metadata = {
28 | title: "DevOverFlow",
29 | description:
30 | "A community-driven platform for asking and answering programming questions. Get help, share knowledge, and collaborate with developers from around the world.",
31 | icons: {
32 | icon: "/assets/images/site-logo.svg",
33 | },
34 | };
35 |
36 | export default function RootLayout({
37 | children,
38 | }: {
39 | children: React.ReactNode;
40 | }) {
41 | return (
42 |
43 |
44 |
52 | {children}
53 |
54 |
55 |
56 | );
57 | }
58 |
--------------------------------------------------------------------------------
/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "default",
4 | "rsc": true,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.js",
8 | "css": "app/globals.css",
9 | "baseColor": "slate",
10 | "cssVariables": true
11 | },
12 | "aliases": {
13 | "components": "@/components",
14 | "utils": "@/lib/utils"
15 | }
16 | }
--------------------------------------------------------------------------------
/components/cards/AnswerCard.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 |
3 | import { SignedIn } from "@clerk/nextjs";
4 |
5 | import { formatNumber, getTimeStamp } from "@/lib/utils";
6 |
7 | import Metric from "../shared/Metric";
8 | import EditDeleteAction from "../shared/EditDeleteAction";
9 |
10 | interface Props {
11 | clerkId?: string | null;
12 | _id: string;
13 | question: {
14 | _id: string;
15 | title: string;
16 | };
17 | author: {
18 | _id: string;
19 | clerkId: string;
20 | name: string;
21 | picture: string;
22 | };
23 | upvotes: string[];
24 | createdAt: Date;
25 | }
26 |
27 | const AnswerCard = ({
28 | clerkId,
29 | _id,
30 | question,
31 | author,
32 | upvotes,
33 | createdAt,
34 | }: Props) => {
35 | const showActionButtons = clerkId && clerkId === author.clerkId;
36 |
37 | return (
38 |
39 |
40 |
41 |
42 | {getTimeStamp(createdAt)}
43 |
44 |
45 |
46 | {question.title}
47 |
48 |
49 |
50 |
51 |
52 | {showActionButtons && (
53 |
54 | )}
55 |
56 |
57 |
58 |
59 |
68 |
69 |
70 |
77 |
78 |
79 |
80 | );
81 | };
82 |
83 | export default AnswerCard;
84 |
--------------------------------------------------------------------------------
/components/cards/QuestionCard.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 |
3 | import Metric from "../shared/Metric";
4 | import RenderTags from "../shared/RenderTags";
5 | import EditDeleteAction from "../shared/EditDeleteAction";
6 |
7 | import { SignedIn } from "@clerk/nextjs";
8 |
9 | import { formatNumber, getTimeStamp } from "@/lib/utils";
10 |
11 | interface Props {
12 | clerkId?: string | null;
13 | _id: string;
14 | title: string;
15 | tags: {
16 | _id: string;
17 | name: string;
18 | }[];
19 | author: {
20 | clerkId: string;
21 | name: string;
22 | picture: string;
23 | };
24 | views: number;
25 | upvotes: string[];
26 | answers: Array