├── .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 | ![Screenshot (135)](https://github.com/sougata-github/DevOverFlow/assets/102734212/bacbe7f8-ddf1-4ac9-8a6c-e9e7b83338a5) 8 | ### Community 9 | ![Screenshot (136)](https://github.com/sougata-github/DevOverFlow/assets/102734212/7c819a7e-1035-468c-b775-9c945b0d8011) 10 | ### Jobs 11 | ![Screenshot (137)](https://github.com/sougata-github/DevOverFlow/assets/102734212/cf9627cd-624e-48a0-9923-c690448aac4a) 12 | ### Profile 13 | ![Screenshot (138)](https://github.com/sougata-github/DevOverFlow/assets/102734212/4ee56d23-d339-4fcb-a0f5-96b8c682a329) 14 | ### Ask a question 15 | ![Screenshot (139)](https://github.com/sougata-github/DevOverFlow/assets/102734212/96af161c-0791-4597-91cd-57759e27f59e) 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 |
19 | 20 |
21 | 22 |
23 |
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 |
108 | 112 |
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 |
76 | 80 |
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 |
56 | 60 |
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 |
14 | 15 |
16 |
{children}
17 |
18 | 19 |
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 |
20 | 21 |
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 | profile 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 |
56 | 60 |
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 |
67 | 71 |
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; 27 | createdAt: Date; 28 | } 29 | 30 | const QuestionCard = ({ 31 | clerkId, 32 | _id, 33 | title, 34 | tags, 35 | author, 36 | views, 37 | upvotes, 38 | createdAt, 39 | answers, 40 | }: Props) => { 41 | const showActionButtons = clerkId && clerkId === author.clerkId; 42 | 43 | return ( 44 |
45 |
46 |
47 | 48 | {getTimeStamp(createdAt)} 49 | 50 | 51 |

52 | {title} 53 |

54 | 55 |
56 | 57 | 58 | {showActionButtons && ( 59 | 60 | )} 61 | 62 |
63 |
64 | {tags.map((tag) => ( 65 | 66 | ))} 67 |
68 |
69 | 77 |
78 | 85 | 92 | 99 |
100 |
101 |
102 | ); 103 | }; 104 | 105 | export default QuestionCard; 106 | -------------------------------------------------------------------------------- /components/cards/UserCard.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | import Link from "next/link"; 3 | 4 | import { Badge } from "../ui/badge"; 5 | import RenderTags from "../shared/RenderTags"; 6 | 7 | import { getTopInteractedTags } from "@/lib/actions/tags.actions"; 8 | 9 | interface Props { 10 | user: { 11 | _id: string; 12 | clerkId: string; 13 | picture: string; 14 | name: string; 15 | username: string; 16 | }; 17 | } 18 | 19 | const UserCard = async ({ user }: Props) => { 20 | const interactedTags = await getTopInteractedTags({ userId: user._id }); 21 | return ( 22 |
23 |
24 | 28 | user profile picture 35 | 36 |
37 |

38 | {user.name} 39 |

40 |

@{user.username}

41 |
42 | 43 | 44 |
45 | {interactedTags.length > 0 ? ( 46 |
47 | {interactedTags.map((tag) => ( 48 | 54 | ))} 55 |
56 | ) : ( 57 | No tags yet. 58 | )} 59 |
60 |
61 |
62 | ); 63 | }; 64 | export default UserCard; 65 | -------------------------------------------------------------------------------- /components/home/HomeFilters.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Button } from "../ui/button"; 4 | 5 | import { useState } from "react"; 6 | import { useSearchParams, useRouter } from "next/navigation"; 7 | 8 | import { HomePageFilters } from "@/constants/filters"; 9 | 10 | import { formUrlQuery } from "@/lib/utils"; 11 | 12 | const HomeFilters = () => { 13 | const searchParams = useSearchParams(); 14 | const router = useRouter(); 15 | 16 | const [active, setActive] = useState(""); 17 | 18 | const handleTypeClick = (item: string) => { 19 | if (active === item) { 20 | setActive(""); 21 | const newUrl = formUrlQuery({ 22 | params: searchParams.toString(), 23 | key: "filter", 24 | value: null, // reset 25 | }); 26 | router.push(newUrl, { scroll: false }); 27 | } else { 28 | setActive(item); 29 | const newUrl = formUrlQuery({ 30 | params: searchParams.toString(), 31 | key: "filter", 32 | value: item.toLowerCase(), 33 | }); 34 | router.push(newUrl, { scroll: false }); 35 | } 36 | }; 37 | 38 | return ( 39 |
40 | {HomePageFilters.map((item) => ( 41 | 52 | ))} 53 |
54 | ); 55 | }; 56 | 57 | export default HomeFilters; 58 | -------------------------------------------------------------------------------- /components/shared/AllAnswers.tsx: -------------------------------------------------------------------------------- 1 | import { AnswerFilters } from "@/constants/filters"; 2 | 3 | import Link from "next/link"; 4 | import Image from "next/image"; 5 | 6 | import { getTimeStamp } from "@/lib/utils"; 7 | import { getAnswers } from "@/lib/actions/answer.actions"; 8 | 9 | import Votes from "./Votes"; 10 | import Filter from "./Filter"; 11 | import ParseHTML from "./ParseHTML"; 12 | import Pagination from "./Pagination"; 13 | 14 | interface Props { 15 | questionId: string; 16 | user: string; 17 | totalAnswers: number; 18 | page?: number; 19 | filter?: string; 20 | } 21 | 22 | const AllAnswers = async ({ 23 | questionId, 24 | user, 25 | totalAnswers, 26 | page, 27 | filter, 28 | }: Props) => { 29 | const result = await getAnswers({ 30 | questionId, 31 | page: page ? +page : 1, 32 | sortBy: filter, 33 | }); 34 | 35 | return ( 36 |
37 |
38 |

39 | {totalAnswers} {totalAnswers > 1 ? "Answers" : "Answer"}{" "} 40 |

41 | 42 | 43 |
44 | 45 |
46 | {result!.answers.length > 0 ? ( 47 | result!.answers.map((answer) => ( 48 |
49 |
50 |
51 | 55 | profile 62 |
63 |

64 | {answer.author.name} 65 |

66 | 67 |

68 | {" "} -{" "} 69 | answered {" "} {getTimeStamp(answer.createdAt)} 70 |

71 |
72 | 73 |
74 |
75 | 84 |
85 |
86 | 87 |
88 | )) 89 | ) : ( 90 |

No Answers yet

91 | )} 92 |
93 |
94 | 95 |
96 |
97 | ); 98 | }; 99 | 100 | export default AllAnswers; 101 | -------------------------------------------------------------------------------- /components/shared/AnswersTab.tsx: -------------------------------------------------------------------------------- 1 | import { SearchParamsProps } from "@/types"; 2 | 3 | import Pagination from "./Pagination"; 4 | import AnswerCard from "../cards/AnswerCard"; 5 | 6 | import { getUserAnswers } from "@/lib/actions/user.actions"; 7 | 8 | interface Props extends SearchParamsProps { 9 | userId: string; 10 | clerkId?: string | null; 11 | } 12 | 13 | const AnswersTab = async ({ searchParams, userId, clerkId }: Props) => { 14 | const result = await getUserAnswers({ 15 | userId, 16 | page: searchParams.page ? +searchParams.page : 1, 17 | }); 18 | 19 | return ( 20 | <> 21 | {result.answers.map((answer) => ( 22 | 31 | ))} 32 |
33 | 37 |
38 | 39 | ); 40 | }; 41 | 42 | export default AnswersTab; 43 | -------------------------------------------------------------------------------- /components/shared/EditDeleteAction.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { deleteAnswer } from "@/lib/actions/answer.actions"; 4 | import { deleteQuestion } from "@/lib/actions/question.action"; 5 | 6 | import Image from "next/image"; 7 | 8 | import { usePathname, useRouter } from "next/navigation"; 9 | 10 | import { toast } from "../ui/use-toast"; 11 | 12 | interface Props { 13 | type: string; 14 | itemId: string; 15 | } 16 | 17 | const EditDeleteAction = ({ type, itemId }: Props) => { 18 | const pathname = usePathname(); 19 | const router = useRouter(); 20 | 21 | const handleEdit = () => { 22 | router.push(`/question/edit/${JSON.parse(itemId)}`); 23 | }; 24 | 25 | const handleDelete = async () => { 26 | if (type === "Question") { 27 | await deleteQuestion({ 28 | questionId: JSON.parse(itemId), 29 | path: pathname, 30 | }); 31 | } else if (type === "Answer") { 32 | await deleteAnswer({ 33 | answerId: JSON.parse(itemId), 34 | path: pathname, 35 | }); 36 | } 37 | 38 | return toast({ 39 | title: `${type} Deleted`, 40 | variant: "default", 41 | }); 42 | }; 43 | 44 | return ( 45 |
46 | {type === "Question" && ( 47 | Edit 55 | )} 56 | Delete 64 |
65 | ); 66 | }; 67 | 68 | export default EditDeleteAction; 69 | -------------------------------------------------------------------------------- /components/shared/Filter.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { 3 | Select, 4 | SelectContent, 5 | SelectGroup, 6 | SelectItem, 7 | SelectTrigger, 8 | SelectValue, 9 | } from "../ui/select"; 10 | 11 | import { formUrlQuery } from "@/lib/utils"; 12 | 13 | import { useSearchParams, useRouter } from "next/navigation"; 14 | 15 | interface Props { 16 | filters: { 17 | name: string; 18 | value: string; 19 | }[]; 20 | otherClasses?: string; 21 | containerClasses?: string; 22 | } 23 | 24 | const Filter = ({ filters, otherClasses, containerClasses }: Props) => { 25 | const searchParams = useSearchParams(); 26 | const router = useRouter(); 27 | 28 | const paramFilter = searchParams.get("filter"); 29 | 30 | const handleUpdateParams = (value: string) => { 31 | const newUrl = formUrlQuery({ 32 | params: searchParams.toString(), 33 | key: "filter", 34 | value, 35 | }); 36 | 37 | router.push(newUrl, { scroll: false }); 38 | }; 39 | 40 | return ( 41 |
42 | 67 |
68 | ); 69 | }; 70 | 71 | export default Filter; 72 | -------------------------------------------------------------------------------- /components/shared/JobFilters.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { 4 | Select, 5 | SelectContent, 6 | SelectGroup, 7 | SelectItem, 8 | SelectTrigger, 9 | SelectValue, 10 | } from "@/components/ui/select"; 11 | 12 | import Image from "next/image"; 13 | 14 | import { Country } from "@/types"; 15 | 16 | import { formUrlQuery } from "@/lib/utils"; 17 | 18 | import { useRouter, useSearchParams } from "next/navigation"; 19 | 20 | interface JobsFilterProps { 21 | countriesList: Country[]; 22 | } 23 | 24 | const JobFilters = ({ countriesList }: JobsFilterProps) => { 25 | const searchParams = useSearchParams(); 26 | const router = useRouter(); 27 | 28 | const paramFilter = searchParams.get("location"); 29 | 30 | function handleUpdateParams(value: any) { 31 | const newUrl = formUrlQuery({ 32 | params: searchParams.toString(), 33 | key: "location", 34 | value, 35 | }); 36 | 37 | router.push(newUrl, { scroll: false }); 38 | } 39 | 40 | return ( 41 |
42 | 75 |
76 | ); 77 | }; 78 | 79 | export default JobFilters; 80 | -------------------------------------------------------------------------------- /components/shared/LeftSidebar.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { sidebarLinks } from "@/constants"; 4 | 5 | import Link from "next/link"; 6 | import Image from "next/image"; 7 | 8 | import { Button } from "../ui/button"; 9 | 10 | import { usePathname } from "next/navigation"; 11 | import { SignedOut, useAuth } from "@clerk/nextjs"; 12 | 13 | const LeftSidebar = () => { 14 | const { userId } = useAuth(); 15 | const pathname = usePathname(); 16 | 17 | return ( 18 |
19 |
20 | {sidebarLinks.map((item) => { 21 | const isActive = 22 | (pathname.includes(item.route) && item.route.length > 1) || 23 | pathname === item.route; 24 | 25 | if (item.route === "/profile") { 26 | if (userId) { 27 | item.route = `${item.route}/${userId}`; 28 | } else { 29 | return null; 30 | } 31 | } 32 | 33 | return ( 34 | 43 | {item.label} 50 |

55 | {item.label} 56 |

57 | 58 | ); 59 | })} 60 |
61 | 62 | 63 |
64 | 65 | 77 | 78 | 79 | 80 | 90 | 91 |
92 |
93 |
94 | ); 95 | }; 96 | 97 | export default LeftSidebar; 98 | -------------------------------------------------------------------------------- /components/shared/Metric.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import Image from "next/image"; 3 | 4 | interface MetricProps { 5 | imgUrl: string; 6 | alt: string; 7 | value: string | number; 8 | title: string; 9 | href?: string; 10 | textStyles?: string; 11 | isAuthor?: boolean; 12 | } 13 | 14 | const Metric = ({ 15 | imgUrl, 16 | alt, 17 | value, 18 | title, 19 | href, 20 | textStyles, 21 | isAuthor, 22 | }: MetricProps) => { 23 | const MetricContent = () => { 24 | return ( 25 | <> 26 | {alt} 33 |

34 | {value} 35 | 40 | {title} 41 | 42 |

43 | 44 | ); 45 | }; 46 | 47 | if (href) { 48 | return ( 49 | 50 | 51 | 52 | ); 53 | } 54 | return ( 55 |
56 | 57 |
58 | ); 59 | }; 60 | 61 | export default Metric; 62 | -------------------------------------------------------------------------------- /components/shared/NoResult.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import Image from "next/image"; 3 | 4 | import { Button } from "../ui/button"; 5 | 6 | interface Props { 7 | title: string; 8 | description: string; 9 | link: string; 10 | linkTitle: string; 11 | } 12 | 13 | const NoResult = ({ title, description, link, linkTitle }: Props) => { 14 | return ( 15 |
16 | No result illustration 23 | 24 | No result illustration 31 |

{title}

32 |

33 | {description} 34 |

35 | 36 | 39 | 40 |
41 | ); 42 | }; 43 | 44 | export default NoResult; 45 | -------------------------------------------------------------------------------- /components/shared/Pagination.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Button } from "../ui/button"; 4 | 5 | import { formUrlQuery } from "@/lib/utils"; 6 | 7 | import { useRouter, useSearchParams } from "next/navigation"; 8 | 9 | interface Props { 10 | pageNumber: number; 11 | isNext: boolean; 12 | } 13 | 14 | const Pagination = ({ pageNumber, isNext }: Props) => { 15 | const router = useRouter(); 16 | const searchParams = useSearchParams(); 17 | 18 | // modify url 19 | const handleNavigation = (direction: string) => { 20 | const nextPageNumber = 21 | direction === "prev" ? pageNumber - 1 : pageNumber + 1; 22 | const newUrl = formUrlQuery({ 23 | params: searchParams.toString(), 24 | key: "page", 25 | value: nextPageNumber.toString(), 26 | }); 27 | router.push(newUrl); 28 | }; 29 | 30 | if (!isNext && pageNumber === 1) return null; 31 | 32 | return ( 33 |
34 | 41 |
42 |

{pageNumber}

43 |
44 | 51 |
52 | ); 53 | }; 54 | 55 | export default Pagination; 56 | -------------------------------------------------------------------------------- /components/shared/ParseHTML.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useEffect } from "react"; 4 | 5 | import Prism from "prismjs"; 6 | import parse from "html-react-parser"; 7 | 8 | import "prismjs/components/prism-python"; 9 | import "prismjs/components/prism-java"; 10 | import "prismjs/components/prism-c"; 11 | import "prismjs/components/prism-cpp"; 12 | import "prismjs/components/prism-csharp"; 13 | import "prismjs/components/prism-aspnet"; 14 | import "prismjs/components/prism-sass"; 15 | import "prismjs/components/prism-jsx"; 16 | import "prismjs/components/prism-typescript"; 17 | import "prismjs/components/prism-solidity"; 18 | import "prismjs/components/prism-json"; 19 | import "prismjs/components/prism-dart"; 20 | import "prismjs/components/prism-ruby"; 21 | import "prismjs/components/prism-rust"; 22 | import "prismjs/components/prism-r"; 23 | import "prismjs/components/prism-kotlin"; 24 | import "prismjs/components/prism-go"; 25 | import "prismjs/components/prism-bash"; 26 | import "prismjs/components/prism-sql"; 27 | import "prismjs/components/prism-mongodb"; 28 | import "prismjs/plugins/line-numbers/prism-line-numbers.js"; 29 | import "prismjs/plugins/line-numbers/prism-line-numbers.css"; 30 | 31 | interface Props { 32 | data: string; 33 | } 34 | 35 | const ParseHTML = ({ data }: Props) => { 36 | useEffect(() => { 37 | Prism.highlightAll(); 38 | }, []); 39 | 40 | return ( 41 |
{parse(data)}
42 | ); 43 | }; 44 | 45 | export default ParseHTML; 46 | -------------------------------------------------------------------------------- /components/shared/ProfileLink.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import Image from "next/image"; 3 | 4 | interface ProfileLinkProps { 5 | imgUrl: string; 6 | href?: string; 7 | title: string; 8 | } 9 | 10 | const ProfileLink = ({ imgUrl, href, title }: ProfileLinkProps) => { 11 | return ( 12 |
13 | icon 14 | {href ? ( 15 | 20 | {title} 21 | 22 | ) : ( 23 |

{title}

24 | )} 25 |
26 | ); 27 | }; 28 | 29 | export default ProfileLink; 30 | -------------------------------------------------------------------------------- /components/shared/QuestionsTab.tsx: -------------------------------------------------------------------------------- 1 | import { SearchParamsProps } from "@/types"; 2 | 3 | import Pagination from "./Pagination"; 4 | import QuestionCard from "../cards/QuestionCard"; 5 | 6 | import { getUserQuestions } from "@/lib/actions/user.actions"; 7 | 8 | interface Props extends SearchParamsProps { 9 | userId: string; 10 | clerkId?: string | null; 11 | } 12 | 13 | const QuestionsTab = async ({ searchParams, userId, clerkId }: Props) => { 14 | const result = await getUserQuestions({ 15 | userId, 16 | page: searchParams.page ? +searchParams.page : 1, 17 | }); 18 | 19 | return ( 20 | <> 21 | {result.questions.map((question) => ( 22 | 34 | ))} 35 |
36 | 40 |
41 | 42 | ); 43 | }; 44 | 45 | export default QuestionsTab; 46 | -------------------------------------------------------------------------------- /components/shared/RenderTags.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Link from "next/link"; 4 | 5 | import { Badge } from "../ui/badge"; 6 | 7 | interface Props { 8 | _id: string; 9 | name: string; 10 | totalQuestions?: number; 11 | showCount?: boolean; 12 | isTruncated?: boolean; 13 | } 14 | 15 | const RenderTags = ({ 16 | _id, 17 | name, 18 | totalQuestions, 19 | showCount, 20 | isTruncated, 21 | }: Props) => { 22 | return ( 23 | 24 | 28 | {isTruncated 29 | ? name.length > 4 30 | ? name.slice(0, 4).concat("...") 31 | : name 32 | : name} 33 | 34 | {showCount && ( 35 |
36 |

{totalQuestions}

37 |
38 | )} 39 | 40 | ); 41 | }; 42 | 43 | export default RenderTags; 44 | -------------------------------------------------------------------------------- /components/shared/RightSidebar.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import Image from "next/image"; 3 | 4 | import RenderTags from "./RenderTags"; 5 | 6 | import { getTopTags } from "@/lib/actions/tags.actions"; 7 | import { getHotQuestions } from "@/lib/actions/question.action"; 8 | 9 | const RightSidebar = async () => { 10 | const hotQuestions = await getHotQuestions(); 11 | 12 | const popularTags = await getTopTags(); 13 | 14 | return ( 15 |
16 |
17 |

Top Questions

18 |
19 | {hotQuestions.map((question) => ( 20 | 25 |

26 | {question.title} 27 |

28 | chevron-right 35 | 36 | ))} 37 |
38 |
39 | 40 |
41 |

Popular Tags

42 | 43 |
44 | {popularTags.map((tag) => ( 45 | 52 | ))} 53 |
54 |
55 |
56 | ); 57 | }; 58 | 59 | export default RightSidebar; 60 | -------------------------------------------------------------------------------- /components/shared/Stats.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | 3 | import { BadgeCounts } from "@/types"; 4 | 5 | import { formatNumber } from "@/lib/utils"; 6 | 7 | interface Props { 8 | totalQuestions: number; 9 | totalAnswers: number; 10 | badgeCounts: BadgeCounts; 11 | reputation: number; 12 | } 13 | 14 | interface StatsCardProps { 15 | imgUrl: string; 16 | value: number; 17 | title: string; 18 | } 19 | 20 | const StatsCard = ({ imgUrl, value, title }: StatsCardProps) => { 21 | return ( 22 |
23 | {title} 24 |
25 |

{value}

26 |

{title}

27 |
28 |
29 | ); 30 | }; 31 | 32 | const Stats = ({ 33 | totalQuestions, 34 | totalAnswers, 35 | badgeCounts, 36 | reputation, 37 | }: Props) => { 38 | return ( 39 |
40 |

41 | Stats - {reputation}{" "} 42 |

43 |
44 |
45 |
46 |

47 | {formatNumber(totalQuestions)} 48 |

49 |

Ques

50 |
51 |
52 |

53 | {formatNumber(totalAnswers)} 54 |

55 |

Ans

56 |
57 |
58 | 59 | 64 | 69 | 74 |
75 |
76 | ); 77 | }; 78 | 79 | export default Stats; 80 | -------------------------------------------------------------------------------- /components/shared/navbar/MobileNav.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { 4 | Sheet, 5 | SheetClose, 6 | SheetContent, 7 | SheetTrigger, 8 | } from "@/components/ui/sheet"; 9 | import { Button } from "@/components/ui/button"; 10 | 11 | import Link from "next/link"; 12 | import Image from "next/image"; 13 | 14 | import { sidebarLinks } from "@/constants"; 15 | 16 | import { SignedOut } from "@clerk/nextjs"; 17 | 18 | import { usePathname } from "next/navigation"; 19 | 20 | import { Menu } from "lucide-react"; 21 | 22 | const NavContent = () => { 23 | const pathname = usePathname(); 24 | return ( 25 |
26 | {sidebarLinks.map((item) => { 27 | const isActive = 28 | (pathname.includes(item.route) && item.route.length > 1) || 29 | pathname === item.route; 30 | 31 | return ( 32 | 33 | 41 | {item.label} 48 |

49 | {item.label} 50 |

51 | 52 |
53 | ); 54 | })} 55 |
56 | ); 57 | }; 58 | 59 | const MobileNav = () => { 60 | return ( 61 | 62 | 63 | 64 | 65 | 69 | 70 | DevOverFlow 76 |

77 | DevOverFlow 78 |

79 | 80 | 81 |
82 | 83 | 84 | 85 | 86 | 87 |
88 | 89 | 90 | 93 | 94 | 95 | 96 | 97 | 98 | 104 | 105 | 106 |
107 |
108 |
109 |
110 | 111 | ); 112 | }; 113 | 114 | export default MobileNav; 115 | -------------------------------------------------------------------------------- /components/shared/navbar/Navbar.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import Image from "next/image"; 3 | 4 | import Theme from "./Theme"; 5 | import MobileNav from "./MobileNav"; 6 | import GlobalSearch from "../search/GlobalSearch"; 7 | 8 | import { SignedIn, UserButton } from "@clerk/nextjs"; 9 | 10 | const Navbar = () => { 11 | return ( 12 | 43 | ); 44 | }; 45 | 46 | export default Navbar; 47 | -------------------------------------------------------------------------------- /components/shared/navbar/Theme.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { 4 | Menubar, 5 | MenubarContent, 6 | MenubarMenu, 7 | MenubarTrigger, 8 | MenubarItem, 9 | } from "@/components/ui/menubar"; 10 | 11 | import Image from "next/image"; 12 | 13 | import { themes } from "@/constants"; 14 | 15 | import { useTheme } from "next-themes"; 16 | 17 | import { Loader } from "lucide-react"; 18 | 19 | import React, { useEffect, useState } from "react"; 20 | 21 | const Theme = () => { 22 | // const { mode, setMode } = useTheme(); 23 | 24 | const [mounted, setMounted] = useState(false); 25 | 26 | const { setTheme, resolvedTheme } = useTheme(); 27 | 28 | useEffect(() => setMounted(true), []); 29 | 30 | if (!mounted) 31 | return ( 32 | // Loading Light/Dark Toggle 41 | 42 | ); 43 | 44 | return ( 45 | 46 | 47 | 48 | {resolvedTheme === "light" ? ( 49 | sun 56 | ) : ( 57 | moon 64 | )} 65 | 66 | 67 | {themes.map((theme) => ( 68 | { 71 | setTheme(theme.value); 72 | }} 73 | className="flex cursor-pointer items-center gap-4 px-2.5 py-2 focus:bg-gray-100 dark:focus:bg-dark-400" 74 | > 75 | {theme.value} 82 |

89 | {theme.label} 90 |

91 |
92 | ))} 93 |
94 |
95 |
96 | ); 97 | }; 98 | 99 | export default Theme; 100 | -------------------------------------------------------------------------------- /components/shared/search/GlobalFilters.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Button } from "@/components/ui/button"; 4 | 5 | import { useState } from "react"; 6 | import { useSearchParams, useRouter } from "next/navigation"; 7 | 8 | import { formUrlQuery } from "@/lib/utils"; 9 | 10 | import { GlobalSearchFilters } from "@/constants/filters"; 11 | 12 | const GlobalFilters = () => { 13 | const router = useRouter(); 14 | const searchParams = useSearchParams(); 15 | 16 | const typeParams = searchParams.get("type"); 17 | 18 | const [active, setActive] = useState(typeParams || ""); 19 | 20 | const handleTypeClick = (item: string) => { 21 | if (active === item) { 22 | setActive(""); 23 | const newUrl = formUrlQuery({ 24 | params: searchParams.toString(), 25 | key: "type", 26 | value: null, // reset 27 | }); 28 | router.push(newUrl, { scroll: false }); 29 | } else { 30 | setActive(item); 31 | const newUrl = formUrlQuery({ 32 | params: searchParams.toString(), 33 | key: "type", 34 | value: item.toLowerCase(), 35 | }); 36 | router.push(newUrl, { scroll: false }); 37 | } 38 | }; 39 | 40 | return ( 41 |
42 |

Type:

43 |
44 | {GlobalSearchFilters.map((item) => ( 45 | 57 | ))} 58 |
59 |
60 | ); 61 | }; 62 | 63 | export default GlobalFilters; 64 | -------------------------------------------------------------------------------- /components/shared/search/GlobalResult.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useEffect, useState } from "react"; 4 | import { useSearchParams } from "next/navigation"; 5 | 6 | import Link from "next/link"; 7 | import Image from "next/image"; 8 | 9 | import GlobalFilters from "./GlobalFilters"; 10 | 11 | import { ReloadIcon } from "@radix-ui/react-icons"; 12 | 13 | import { globalSearch } from "@/lib/actions/general.action"; 14 | 15 | const GlobalResult = () => { 16 | const searchParams = useSearchParams(); 17 | 18 | const [isLoading, setIsLoading] = useState(false); 19 | const [result, setResult] = useState([ 20 | { type: "question", id: 1, title: "NextJS questions" }, 21 | { type: "question", id: 1, title: "NextJS questions" }, 22 | { type: "question", id: 1, title: "NextJS questions" }, 23 | ]); 24 | 25 | const global = searchParams.get("global"); 26 | const type = searchParams.get("type"); 27 | 28 | useEffect(() => { 29 | const fetchResult = async () => { 30 | setResult([]); 31 | setIsLoading(true); 32 | try { 33 | const res = await globalSearch({ 34 | query: global, 35 | type, 36 | }); 37 | setResult(JSON.parse(res)); 38 | } catch (error) { 39 | console.log(error); 40 | throw error; 41 | } finally { 42 | setIsLoading(false); 43 | } 44 | }; 45 | 46 | if (global) { 47 | fetchResult(); 48 | } 49 | }, [global, type]); 50 | 51 | const renderLink = (type: string, id: string) => { 52 | switch (type) { 53 | case "question": 54 | return `/question/${id}`; 55 | case "answer": 56 | return `/question/${id}`; 57 | case "user": 58 | return `/profile/${id}`; 59 | case "tag": 60 | return `/tags/${id}`; 61 | default: 62 | return `/`; 63 | } 64 | }; 65 | 66 | return ( 67 |
68 | 69 |
70 |
71 |

72 | Top Match 73 |

74 | 75 | {isLoading ? ( 76 |
77 | 78 |

79 | Browsing the entire database... 80 |

81 |
82 | ) : ( 83 |
84 | {result.length > 0 ? ( 85 | result.map((item: any, index: number) => ( 86 | 91 | tags 98 |
99 |

100 | {item.title} 101 |

102 |

103 | {item.type} 104 |

105 |
106 | 107 | )) 108 | ) : ( 109 |
110 |

111 | Oops! no results found. 112 |

113 |
114 | )} 115 |
116 | )} 117 |
118 |
119 | ); 120 | }; 121 | 122 | export default GlobalResult; 123 | -------------------------------------------------------------------------------- /components/shared/search/GlobalSearch.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Image from "next/image"; 4 | 5 | import { useEffect, useRef, useState } from "react"; 6 | import { usePathname, useSearchParams, useRouter } from "next/navigation"; 7 | 8 | import GlobalResult from "./GlobalResult"; 9 | import { Input } from "@/components/ui/input"; 10 | 11 | import { formUrlQuery, removeKeysFromQuery } from "@/lib/utils"; 12 | 13 | const GlobalSearch = () => { 14 | const router = useRouter(); 15 | const pathname = usePathname(); 16 | const searchParams = useSearchParams(); 17 | const searchContainerRef = useRef(null); 18 | 19 | const query = searchParams.get("q"); 20 | 21 | const [search, setSearch] = useState(query || ""); 22 | const [isOpen, setIsOpen] = useState(false); 23 | 24 | useEffect(() => { 25 | const handleOutsideClick = (event: any) => { 26 | if ( 27 | searchContainerRef.current && 28 | // @ts-ignore 29 | !searchContainerRef.current.contains(event.target) 30 | ) { 31 | setIsOpen(false); 32 | setSearch(""); 33 | } 34 | }; 35 | 36 | setIsOpen(false); 37 | 38 | document.addEventListener("click", handleOutsideClick); 39 | 40 | return () => document.removeEventListener("click", handleOutsideClick); 41 | }, [pathname]); 42 | 43 | useEffect(() => { 44 | const delayDebounceFn = setTimeout(() => { 45 | if (search) { 46 | const newUrl = formUrlQuery({ 47 | params: searchParams.toString(), 48 | key: "global", 49 | value: search, 50 | }); 51 | router.push(newUrl, { scroll: false }); 52 | } else { 53 | const newUrl = removeKeysFromQuery({ 54 | params: searchParams.toString(), 55 | keysToRemove: ["global", "type"], 56 | }); 57 | router.push(newUrl, { scroll: false }); 58 | } 59 | }, 300); 60 | 61 | return () => clearTimeout(delayDebounceFn); 62 | }, [search, pathname, searchParams, router]); 63 | 64 | return ( 65 |
69 |
70 | search 77 | { 81 | setSearch(e.target.value); 82 | if (!isOpen) setIsOpen(true); 83 | if (e.target.value === "" && isOpen) setIsOpen(false); 84 | }} 85 | placeholder="Search globally" 86 | className="paragraph-regular no-focus 87 | text-dark400_light700 placeholder border-none bg-transparent shadow-none outline-none" 88 | /> 89 |
90 | {isOpen && } 91 |
92 | ); 93 | }; 94 | 95 | export default GlobalSearch; 96 | -------------------------------------------------------------------------------- /components/shared/search/LocalSearch.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Image from "next/image"; 4 | 5 | import { Input } from "@/components/ui/input"; 6 | 7 | import React, { useEffect, useState } from "react"; 8 | import { usePathname, useRouter, useSearchParams } from "next/navigation"; 9 | 10 | import { formUrlQuery, removeKeysFromQuery } from "@/lib/utils"; 11 | 12 | interface CustomInputProps { 13 | route: string; 14 | iconPosition: string; 15 | imgSrc: string; 16 | placeholder: string; 17 | otherClasses?: string; 18 | } 19 | 20 | const LocalSearch = ({ 21 | route, 22 | iconPosition, 23 | imgSrc, 24 | placeholder, 25 | otherClasses, 26 | }: CustomInputProps) => { 27 | const router = useRouter(); 28 | const pathname = usePathname(); 29 | const searchParams = useSearchParams(); 30 | 31 | const query = searchParams.get("q"); 32 | 33 | const [search, setSearch] = useState(query || ""); 34 | 35 | useEffect(() => { 36 | const delayDebounceFn = setTimeout(() => { 37 | if (search) { 38 | const newUrl = formUrlQuery({ 39 | params: searchParams.toString(), 40 | key: "q", 41 | value: search, 42 | }); 43 | router.push(newUrl, { scroll: false }); 44 | } else { 45 | if (pathname === route) { 46 | const newUrl = removeKeysFromQuery({ 47 | params: searchParams.toString(), 48 | keysToRemove: ["q"], 49 | }); 50 | router.push(newUrl, { scroll: false }); 51 | } 52 | } 53 | }, 300); 54 | 55 | return () => clearTimeout(delayDebounceFn); 56 | }, [search, route, pathname, searchParams, query, router]); 57 | 58 | return ( 59 |
62 | {iconPosition === "left" && ( 63 | search icon 70 | )} 71 | 72 | { 76 | setSearch(e.target.value); 77 | }} 78 | placeholder={placeholder} 79 | className="paragraph-regular no-focus 80 | placeholder text-dark400_light700 border-none bg-transparent shadow-none outline-none " 81 | /> 82 | 83 | {iconPosition === "right" && ( 84 | search icon 91 | )} 92 |
93 | ); 94 | }; 95 | 96 | export default LocalSearch; 97 | -------------------------------------------------------------------------------- /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-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", 8 | { 9 | variants: { 10 | variant: { 11 | default: 12 | "border-transparent bg-primary text-primary-foreground hover:bg-primary/80", 13 | secondary: 14 | "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", 15 | destructive: 16 | "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", 17 | outline: "text-foreground", 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 rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", 9 | { 10 | variants: { 11 | variant: { 12 | default: "bg-primary text-primary-foreground hover:bg-primary/90", 13 | destructive: 14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90", 15 | outline: 16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground", 17 | secondary: 18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80", 19 | ghost: "hover:bg-accent hover:text-accent-foreground", 20 | link: "text-primary underline-offset-4 hover:underline", 21 | }, 22 | size: { 23 | default: "h-10 px-4 py-2", 24 | sm: "h-9 rounded-md px-3", 25 | lg: "h-11 rounded-md px-8", 26 | icon: "h-10 w-10", 27 | }, 28 | }, 29 | defaultVariants: { 30 | variant: "default", 31 | size: "default", 32 | }, 33 | } 34 | ) 35 | 36 | export interface ButtonProps 37 | extends React.ButtonHTMLAttributes, 38 | VariantProps { 39 | asChild?: boolean 40 | } 41 | 42 | const Button = React.forwardRef( 43 | ({ className, variant, size, asChild = false, ...props }, ref) => { 44 | const Comp = asChild ? Slot : "button" 45 | return ( 46 | 51 | ) 52 | } 53 | ) 54 | Button.displayName = "Button" 55 | 56 | export { Button, buttonVariants } 57 | -------------------------------------------------------------------------------- /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 | /* eslint-disable no-undef */ 2 | import { cn } from "@/lib/utils"; 3 | 4 | function Skeleton({ 5 | className, 6 | ...props 7 | }: React.HTMLAttributes) { 8 | return ( 9 |
16 | ); 17 | } 18 | 19 | export { Skeleton }; 20 | -------------------------------------------------------------------------------- /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 | export interface TextareaProps 6 | extends React.TextareaHTMLAttributes {} 7 | 8 | const Textarea = React.forwardRef( 9 | ({ className, ...props }, ref) => { 10 | return ( 11 |