├── .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 │ ├── 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 ├── 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 │ │ └── AllAnswers.tsx │ ├── AnswersTab │ │ └── AnswersTab.tsx │ ├── EditDeleteAction │ │ └── EditDeleteAction.tsx │ ├── Filter │ │ ├── Filter.tsx │ │ └── JobsFilter.tsx │ ├── LeftSidebar │ │ └── LeftSidebar.tsx │ ├── Metric │ │ └── Metric.tsx │ ├── NoResult │ │ └── NoResult.tsx │ ├── Pagination │ │ └── Pagination.tsx │ ├── ParseHTML │ │ └── ParseHTML.tsx │ ├── ProfileLink │ │ └── ProfileLink.tsx │ ├── QuestionTab │ │ └── QuestionTab.tsx │ ├── RightSidebar │ │ ├── RenderTag.tsx │ │ └── RightSidebar.tsx │ ├── Stats │ │ └── Stats.tsx │ ├── Votes │ │ └── Votes.tsx │ ├── navbar │ │ ├── MobileNav.tsx │ │ ├── Navbar.tsx │ │ └── Theme.tsx │ └── search │ │ ├── GlobalFilters.tsx │ │ ├── GlobalSearch.tsx │ │ ├── GlobarResult.tsx │ │ └── LocalSearchBar.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 ├── context └── ThemeProvider.tsx ├── database ├── answer.model.ts ├── interaction.model.ts ├── question.model.ts ├── tag.model.ts └── user.model.ts ├── lib ├── actions │ ├── answer.action.ts │ ├── general.action.ts │ ├── interaction.action.ts │ ├── job.action.ts │ ├── question.action.ts │ ├── shared.types.d.ts │ ├── tag.action.ts │ └── user.action.ts ├── db │ └── 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 | { "reactSnippets.settings.importReactOnTop": false } 2 | -------------------------------------------------------------------------------- /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 { Button } from '@/components/ui/button'; 2 | import { Skeleton } from '@/components/ui/skeleton'; 3 | import { Link } from 'lucide-react'; 4 | 5 | const Loading = () => { 6 | return ( 7 |
8 |
9 |

All Questions

10 | 11 | 14 | 15 |
16 |
17 | 18 |
19 | 20 |
21 |
22 | 23 |
24 | 25 | 26 | 27 | 28 |
29 | 30 |
31 | {[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((item) => ( 32 | 33 | ))} 34 |
35 |
36 | ); 37 | }; 38 | export default Loading; 39 | -------------------------------------------------------------------------------- /app/(root)/(home)/page.tsx: -------------------------------------------------------------------------------- 1 | import LocalSearchBar from '@/components/shared/search/LocalSearchBar'; 2 | import { Button } from '@/components/ui/button'; 3 | import Link from 'next/link'; 4 | import { HomePageFilters } from '@/constants/filters'; 5 | import Filter from '@/components/shared/Filter/Filter'; 6 | import HomeFilters from '@/components/home/HomeFilters'; 7 | import NoResult from '@/components/shared/NoResult/NoResult'; 8 | import QuestionCard from '@/components/cards/QuestionCard'; 9 | import { 10 | getQuestions, 11 | getRecommendedQuestions 12 | } from '@/lib/actions/question.action'; 13 | import { SearchParamsProps } from '@/types'; 14 | import Pagination from '@/components/shared/Pagination/Pagination'; 15 | import type { Metadata } from 'next'; 16 | import { auth } from '@clerk/nextjs'; 17 | 18 | export const metadata: Metadata = { 19 | title: 'Home | Dev Overflow', 20 | description: 'Home page of Dev Overflow' 21 | }; 22 | 23 | export default async function Home({ searchParams }: SearchParamsProps) { 24 | const { userId } = auth(); 25 | let result; 26 | 27 | // ? fetch recomended questions 28 | 29 | if (searchParams?.filter === 'recommended') { 30 | if (userId) { 31 | result = await getRecommendedQuestions({ 32 | userId, 33 | searchQuery: searchParams?.q, 34 | page: searchParams?.page ? +searchParams?.page : 1 35 | }); 36 | } else { 37 | result = { 38 | questions: [], 39 | isNext: false 40 | }; 41 | } 42 | } else { 43 | result = await getQuestions({ 44 | searchQuery: searchParams?.q, 45 | filter: searchParams?.filter, 46 | page: searchParams?.page ? +searchParams?.page : 1 47 | }); 48 | } 49 | 50 | const pageNumber = searchParams?.page ? +searchParams?.page : 1; 51 | 52 | return ( 53 | <> 54 |
55 |

All Questions

56 | 57 | 60 | 61 |
62 |
63 | 70 | 71 | 76 |
77 | 78 | 79 | 80 | {/* Card section */} 81 |
82 | {/* looping through questions */} 83 | {result.questions.length > 0 ? ( 84 | result.questions?.map((question) => ( 85 | 96 | )) 97 | ) : ( 98 | 104 | )} 105 |
106 | 107 |
108 | 109 |
110 | 111 | ); 112 | } 113 | -------------------------------------------------------------------------------- /app/(root)/ask-question/page.tsx: -------------------------------------------------------------------------------- 1 | import Question from '@/components/forms/Question'; 2 | import { getUserById } from '@/lib/actions/user.action'; 3 | import { auth } from '@clerk/nextjs'; 4 | import { redirect } from 'next/navigation'; 5 | 6 | import type { Metadata } from 'next'; 7 | 8 | export const metadata: Metadata = { 9 | title: 'Ask-question | Dev Overflow', 10 | description: 'Ask quesiton page of Dev Overflow' 11 | }; 12 | 13 | const AskQuestion = async () => { 14 | const { userId } = auth(); 15 | 16 | if (!userId) redirect('/sign-in'); 17 | 18 | const mongoUser = await getUserById({ userId }); 19 | 20 | return ( 21 |
22 |

Ask a question

23 |
24 | 25 |
26 |
27 | ); 28 | }; 29 | export default AskQuestion; 30 | -------------------------------------------------------------------------------- /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 LocalSearchBar from '@/components/shared/search/LocalSearchBar'; 2 | import { QuestionFilters } from '@/constants/filters'; 3 | import Filter from '@/components/shared/Filter/Filter'; 4 | import NoResult from '@/components/shared/NoResult/NoResult'; 5 | import QuestionCard from '@/components/cards/QuestionCard'; 6 | import { getSavedQuestions } from '@/lib/actions/user.action'; 7 | import { auth } from '@clerk/nextjs'; 8 | import { SearchParamsProps } from '@/types'; 9 | import Pagination from '@/components/shared/Pagination/Pagination'; 10 | 11 | import type { Metadata } from 'next'; 12 | 13 | export const metadata: Metadata = { 14 | title: 'Collections | Dev Overflow', 15 | description: 'Collections page of Dev Overflow' 16 | }; 17 | 18 | export default async function Collection({ searchParams }: SearchParamsProps) { 19 | const { userId } = auth(); 20 | if (!userId) return null; 21 | const { questions, isNext } = await getSavedQuestions({ 22 | clerkId: userId, 23 | searchQuery: searchParams?.q, 24 | filter: searchParams?.filter, 25 | page: searchParams?.page ? +searchParams?.page : 1 26 | }); 27 | 28 | const pageNumber = searchParams?.page ? +searchParams?.page : 1; 29 | 30 | return ( 31 | <> 32 |

Saved Questions

33 | 34 |
35 | 42 | 43 | 47 |
48 | 49 | {/* Card section */} 50 |
51 | {/* // ? looping through questions */} 52 | {questions.length > 0 ? ( 53 | questions?.map((question: any) => ( 54 | 65 | )) 66 | ) : ( 67 | 73 | )} 74 |
75 |
76 | 77 |
78 | 79 | ); 80 | } 81 | -------------------------------------------------------------------------------- /app/(root)/community/loading.tsx: -------------------------------------------------------------------------------- 1 | import { Skeleton } from '@/components/ui/skeleton'; 2 | 3 | const Loading = () => { 4 | return ( 5 |
6 |

All Users

7 |
8 | 9 | 10 |
11 | 12 |
13 | {[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((item) => ( 14 | 18 | ))} 19 |
20 |
21 | ); 22 | }; 23 | export default Loading; 24 | -------------------------------------------------------------------------------- /app/(root)/community/page.tsx: -------------------------------------------------------------------------------- 1 | import UserCard from '@/components/cards/UserCard'; 2 | import Filter from '@/components/shared/Filter/Filter'; 3 | import Pagination from '@/components/shared/Pagination/Pagination'; 4 | import LocalSearchBar from '@/components/shared/search/LocalSearchBar'; 5 | import { UserFilters } from '@/constants/filters'; 6 | import { getAllUsers } from '@/lib/actions/user.action'; 7 | import { SearchParamsProps } from '@/types'; 8 | import Link from 'next/link'; 9 | 10 | import type { Metadata } from 'next'; 11 | 12 | export const metadata: Metadata = { 13 | title: 'Community| Dev Overflow', 14 | description: 'Community page of Dev Overflow' 15 | }; 16 | 17 | const Community = async ({ searchParams }: SearchParamsProps) => { 18 | const { users, isNext } = await getAllUsers({ 19 | searchQuery: searchParams?.q, 20 | filter: searchParams?.filter, 21 | page: searchParams?.page ? +searchParams?.page : 1 22 | }); 23 | 24 | const pageNumber = searchParams?.page ? +searchParams?.page : 1; 25 | 26 | return ( 27 | <> 28 |

All Users

29 | 30 |
31 | 38 | 39 | 43 |
44 | 45 |
46 | {users.length > 0 ? ( 47 | users.map((user) => ) 48 | ) : ( 49 |
50 |

No User yet

51 | 52 | Join to be the first! 53 | 54 |
55 | )} 56 |
57 | 58 |
59 | 60 |
61 | 62 | ); 63 | }; 64 | export default Community; 65 | -------------------------------------------------------------------------------- /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 JobsFilter from '@/components/shared/Filter/JobsFilter'; 3 | import Pagination from '@/components/shared/Pagination/Pagination'; 4 | import { 5 | fetchCountries, 6 | fetchJobs, 7 | fetchLocation 8 | } from '@/lib/actions/job.action'; 9 | import { Job } from '@/types'; 10 | 11 | interface Props { 12 | searchParams: { 13 | q: string; 14 | location: string; 15 | page: string; 16 | }; 17 | } 18 | 19 | const page = async ({ searchParams }: Props) => { 20 | const countries = await fetchCountries(); 21 | const userLocation = await fetchLocation(); 22 | 23 | const jobs = await fetchJobs({ 24 | query: 25 | `${searchParams?.q}, ${searchParams?.location}` ?? 26 | `Software Engineer in ${userLocation}`, 27 | page: searchParams.page ?? 1 28 | }); 29 | console.log(jobs); 30 | console.log(searchParams.location); 31 | const page = parseInt(searchParams.page ?? 1); 32 | 33 | return ( 34 | <> 35 |

Jobs

36 | 37 |
38 | 39 |
40 | 41 |
42 | {jobs?.length > 0 ? ( 43 | jobs?.map((job: Job) => { 44 | if (job.job_title && job.job_title.toLowerCase() !== 'undefined') 45 | return ; 46 | 47 | return null; 48 | }) 49 | ) : ( 50 |
51 | Oops! We couldn't find any jobs at the moment. Please try again 52 | later 53 |
54 | )} 55 |
56 | 57 | {jobs?.length > 0 && ( 58 | 59 | )} 60 | 61 | ); 62 | }; 63 | export default page; 64 | -------------------------------------------------------------------------------- /app/(root)/layout.tsx: -------------------------------------------------------------------------------- 1 | import Navbar from '@/components/shared/navbar/Navbar'; 2 | import React from 'react'; 3 | import LeftSidebar from '../../components/shared/LeftSidebar/LeftSidebar'; 4 | import RightSidebar from '@/components/shared/RightSidebar/RightSidebar'; 5 | import { Toaster } from '@/components/ui/toaster'; 6 | 7 | const Layout = ({ children }: { children: React.ReactNode }) => { 8 | return ( 9 |
10 | 11 |
12 | 13 |
14 |
{children}
15 |
16 | 17 |
18 | 19 |
20 | ); 21 | }; 22 | export default Layout; 23 | -------------------------------------------------------------------------------- /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 { getUserById } from '@/lib/actions/user.action'; 4 | import { ParamsProps } from '@/types'; 5 | import { auth } from '@clerk/nextjs'; 6 | 7 | import type { Metadata } from 'next'; 8 | 9 | export const metadata: Metadata = { 10 | title: 'Edit profile | Dev Overflow', 11 | description: 'Edit profile page of Dev Overflow' 12 | }; 13 | 14 | const ProfileEdit = async ({ params }: ParamsProps) => { 15 | const { userId } = auth(); 16 | if (!userId) return null; 17 | 18 | const mongoUser = await getUserById({ userId }); 19 | 20 | return ( 21 | <> 22 |

Edit Profile

23 |
24 | 25 |
26 | 27 | ); 28 | }; 29 | export default ProfileEdit; 30 | -------------------------------------------------------------------------------- /app/(root)/question/edit/[id]/page.tsx: -------------------------------------------------------------------------------- 1 | import Question from '@/components/forms/Question'; 2 | import { getQuestionById } from '@/lib/actions/question.action'; 3 | import { getUserById } from '@/lib/actions/user.action'; 4 | import { ParamsProps } from '@/types'; 5 | import { auth } from '@clerk/nextjs'; 6 | 7 | import type { Metadata } from 'next'; 8 | 9 | export const metadata: Metadata = { 10 | title: 'Edit Question | Dev Overflow', 11 | description: 'Edit quesiton page of Dev Overflow' 12 | }; 13 | 14 | const EditQuestion = async ({ params }: ParamsProps) => { 15 | const { userId } = auth(); 16 | if (!userId) return null; 17 | 18 | const mongoUser = await getUserById({ userId }); 19 | 20 | const { question } = await getQuestionById({ 21 | questionId: params.id 22 | }); 23 | 24 | return ( 25 | <> 26 |

Edit Question

27 |
28 | 33 |
34 | 35 | ); 36 | }; 37 | export default EditQuestion; 38 | -------------------------------------------------------------------------------- /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 QuestionCard from '@/components/cards/QuestionCard'; 2 | import NoResult from '@/components/shared/NoResult/NoResult'; 3 | import Pagination from '@/components/shared/Pagination/Pagination'; 4 | import LocalSearchBar from '@/components/shared/search/LocalSearchBar'; 5 | import { getQuestionsByTagId } from '@/lib/actions/tag.action'; 6 | import { URLProps } from '@/types'; 7 | 8 | const TagDetails = async ({ params, searchParams }: URLProps) => { 9 | const { tagTitle, questions, isNext } = await getQuestionsByTagId({ 10 | tagId: params.id, 11 | page: searchParams?.page ? +searchParams.page : 1, 12 | searchQuery: searchParams?.q 13 | }); 14 | 15 | const pageNumber = searchParams?.page ? +searchParams?.page : 1; 16 | 17 | return ( 18 | <> 19 |

{tagTitle}

20 | 21 |
22 | 29 |
30 | 31 | {/* Card section */} 32 |
33 | {/* //!Todo: looping through questions */} 34 | {questions.length > 0 ? ( 35 | questions?.map((question: any) => ( 36 | 47 | )) 48 | ) : ( 49 | 55 | )} 56 |
57 | 58 |
59 | 60 |
61 | 62 | ); 63 | }; 64 | export default TagDetails; 65 | -------------------------------------------------------------------------------- /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/Filter'; 2 | import NoResult from '@/components/shared/NoResult/NoResult'; 3 | import Pagination from '@/components/shared/Pagination/Pagination'; 4 | import LocalSearchBar from '@/components/shared/search/LocalSearchBar'; 5 | import { TagFilters } from '@/constants/filters'; 6 | import { getAllTags } from '@/lib/actions/tag.action'; 7 | import { SearchParamsProps } from '@/types'; 8 | import Link from 'next/link'; 9 | 10 | import type { Metadata } from 'next'; 11 | 12 | export const metadata: Metadata = { 13 | title: 'Tags | Dev Overflow', 14 | description: 'Quesiton tags page of Dev Overflow' 15 | }; 16 | 17 | const Tags = async ({ searchParams }: SearchParamsProps) => { 18 | const { tags, isNext } = await getAllTags({ 19 | searchQuery: searchParams?.q, 20 | filter: searchParams?.filter, 21 | page: searchParams?.page ? +searchParams.page : 1 22 | }); 23 | 24 | const pageNumber = searchParams?.page ? +searchParams?.page : 1; 25 | return ( 26 | <> 27 |

All Tags

28 | 29 |
30 | 37 | 38 | 42 |
43 | 44 |
45 | {tags.length > 0 ? ( 46 | tags.map((tag) => ( 47 | 52 |
53 |
54 |

55 | {tag.name} 56 |

57 |
58 |

59 | 60 | {tag.questions.length}+ 61 | {' '} 62 | Questions 63 |

64 |
65 | 66 | )) 67 | ) : ( 68 | 74 | )} 75 |
76 | 77 |
78 | 79 |
80 | 81 | ); 82 | }; 83 | export default Tags; 84 | -------------------------------------------------------------------------------- /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 | 'Your are a knowledgable assistant that provides quality informations' 20 | }, 21 | { 22 | role: 'user', 23 | content: `Tell me ${question}` 24 | } 25 | ] 26 | }) 27 | }); 28 | const responseData = await response.json(); 29 | const reply = responseData.choices[0].message.content; 30 | 31 | return NextResponse.json({ reply }); 32 | } catch (error: any) { 33 | return NextResponse.json({ 34 | error: error.message 35 | }); 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /app/api/webhook/route.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable camelcase */ 2 | import { Webhook } from 'svix'; 3 | import { headers } from 'next/headers'; 4 | import { WebhookEvent } from '@clerk/nextjs/server'; 5 | import { createUser, deleteUser, updateUser } from '@/lib/actions/user.action'; 6 | import { NextResponse } from 'next/server'; 7 | 8 | export async function POST(req: Request) { 9 | // You can find this in the Clerk Dashboard -> Webhooks -> choose the webhook 10 | // todo: add webhook secret to .env.local 11 | const WEBHOOK_SECRET = process.env.NEXT_CLERK_WEBHOOK_SECRET; 12 | 13 | if (!WEBHOOK_SECRET) { 14 | throw new Error( 15 | 'Please add WEBHOOK_SECRET from Clerk Dashboard to .env or .env.local' 16 | ); 17 | } 18 | 19 | // Get the headers 20 | const headerPayload = headers(); 21 | const svix_id = headerPayload.get('svix-id'); 22 | const svix_timestamp = headerPayload.get('svix-timestamp'); 23 | const svix_signature = headerPayload.get('svix-signature'); 24 | 25 | // If there are no headers, error out 26 | if (!svix_id || !svix_timestamp || !svix_signature) { 27 | return new Response('Error occured -- no svix headers', { 28 | status: 400 29 | }); 30 | } 31 | 32 | // Get the body 33 | const payload = await req.json(); 34 | const body = JSON.stringify(payload); 35 | 36 | // Create a new SVIX instance with your secret. 37 | const wh = new Webhook(WEBHOOK_SECRET); 38 | 39 | let evt: WebhookEvent; 40 | 41 | // Verify the payload with the headers 42 | try { 43 | evt = wh.verify(body, { 44 | 'svix-id': svix_id, 45 | 'svix-timestamp': svix_timestamp, 46 | 'svix-signature': svix_signature 47 | }) as WebhookEvent; 48 | } catch (err) { 49 | console.error('Error verifying webhook:', err); 50 | return new Response('Error occured', { 51 | status: 400 52 | }); 53 | } 54 | 55 | // Get the ID and type 56 | const eventType = evt.type; 57 | console.log('POST eventType:', { eventType }); 58 | 59 | // Handle to event 60 | if (eventType === 'user.created') { 61 | // get user data 62 | const { id, email_addresses, image_url, username, first_name, last_name } = 63 | evt.data; 64 | 65 | // create a server action to create a user in the database 66 | 67 | const mongoUser = await createUser({ 68 | clerkId: id, 69 | name: `${first_name} ${last_name ? ` ${last_name}` : ''}`, 70 | username: username!, 71 | email: email_addresses[0].email_address, 72 | picture: image_url 73 | }); 74 | 75 | return NextResponse.json({ messsage: 'OK', user: mongoUser }); 76 | } 77 | 78 | if (eventType === 'user.updated') { 79 | // get user data 80 | const { id, email_addresses, image_url, username, first_name, last_name } = 81 | evt.data; 82 | 83 | console.log('clerk id', id); 84 | 85 | // create a server action to create a user in the database 86 | 87 | const mongoUser = await updateUser({ 88 | clerkId: id, 89 | updateData: { 90 | name: `${first_name} ${last_name ? ` ${last_name}` : ''}`, 91 | username: username!, 92 | email: email_addresses[0].email_address, 93 | picture: image_url 94 | }, 95 | path: `/profile/${id}` 96 | }); 97 | 98 | return NextResponse.json({ messsage: 'OK', user: mongoUser }); 99 | } 100 | 101 | if (eventType === 'user.deleted') { 102 | const { id } = evt.data; 103 | const deletedUser = await deleteUser({ clerkId: id! }); 104 | 105 | return NextResponse.json({ messsage: 'OK', user: deletedUser }); 106 | } 107 | 108 | return NextResponse.json({ messsage: 'OK' }); 109 | } 110 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rakibtweets/dev_overflow_nextjs13/54d3ac89643761560da604d274ab869548905054/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: 3px; 87 | height: 3px; 88 | border-radius: 2px; 89 | } 90 | 91 | .custom-scrollbar::-webkit-scrollbar-track { 92 | background: #ffffff; 93 | } 94 | 95 | .custom-scrollbar::-webkit-scrollbar-thumb { 96 | background: #888; 97 | border-radius: 50px; 98 | } 99 | 100 | .custom-scrollbar::-webkit-scrollbar-thumb:hover { 101 | background: #555; 102 | } 103 | 104 | /* Markdown Start */ 105 | .markdown a { 106 | color: #1da1f2; 107 | } 108 | 109 | .markdown a, 110 | code { 111 | /* These are technically the same, but use both */ 112 | overflow-wrap: break-word; 113 | word-wrap: break-word; 114 | 115 | -ms-word-break: break-all; 116 | /* This is the dangerous one in WebKit, as it breaks things wherever */ 117 | word-break: break-all; 118 | /* Instead use this non-standard one: */ 119 | word-break: break-word; 120 | 121 | /* Adds a hyphen where the word breaks, if supported (No Blink) */ 122 | -ms-hyphens: auto; 123 | -moz-hyphens: auto; 124 | -webkit-hyphens: auto; 125 | hyphens: auto; 126 | 127 | padding: 2px; 128 | color: #ff7000 !important; 129 | } 130 | 131 | .markdown pre { 132 | display: grid; 133 | width: 100%; 134 | } 135 | 136 | .markdown pre code { 137 | width: 100%; 138 | display: block; 139 | overflow-x: auto; 140 | 141 | color: inherit !important; 142 | } 143 | /* Markdown End */ 144 | 145 | /* Clerk */ 146 | .cl-internal-b3fm6y { 147 | background: linear-gradient(129deg, #ff7000 0%, #e2995f 100%) !important; 148 | } 149 | 150 | .hash-span { 151 | margin-top: -140px; 152 | padding-bottom: 140px; 153 | display: block; 154 | } 155 | 156 | /* Hide scrollbar for Chrome, Safari and Opera */ 157 | .no-scrollbar::-webkit-scrollbar { 158 | display: none; 159 | } 160 | 161 | /* Hide scrollbar for IE, Edge and Firefox */ 162 | .no-scrollbar { 163 | -ms-overflow-style: none; /* IE and Edge */ 164 | scrollbar-width: none; /* Firefox */ 165 | } 166 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ClerkProvider } from '@clerk/nextjs'; 3 | // eslint-disable-next-line camelcase 4 | import { Inter, Space_Grotesk } from 'next/font/google'; 5 | import type { Metadata } from 'next'; 6 | import './globals.css'; 7 | import '../styles/prism.css'; 8 | import ThemeProvider from '@/context/ThemeProvider'; 9 | 10 | const inter = Inter({ 11 | subsets: ['latin'], 12 | weight: ['100', '200', '300', '400', '500', '600', '700', '800', '900'], 13 | variable: '--font-inter' 14 | }); 15 | const spaceGrotest = Space_Grotesk({ 16 | subsets: ['latin'], 17 | weight: ['300', '400', '500', '600', '700'], 18 | variable: '--font-spaceGrotesk' 19 | }); 20 | 21 | export const metadata: Metadata = { 22 | title: 'DevFlow', 23 | description: 24 | 'A community driven platform for developers asking and answering questions.Get help and answers to your questions about anything related to programming and software development from the best developers on the internet.', 25 | icons: { 26 | icon: '/assets/images/site-logo.svg' 27 | } 28 | }; 29 | 30 | export default function RootLayout({ 31 | children 32 | }: { 33 | children: React.ReactNode; 34 | }) { 35 | return ( 36 | 37 | 38 | 46 | {children} 47 | 48 | 49 | 50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /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 { formatAndDivideNumber, getTimeStamp } from '@/lib/utils'; 2 | import Metric from '../shared/Metric/Metric'; 3 | import Link from 'next/link'; 4 | import { SignedIn } from '@clerk/nextjs'; 5 | import EditDeleteAction from '../shared/EditDeleteAction/EditDeleteAction'; 6 | 7 | interface Props { 8 | clerkId?: string | null; 9 | _id: string; 10 | question: { 11 | _id: string; 12 | title: string; 13 | }; 14 | author: { 15 | _id: string; 16 | clerkId: string; 17 | name: string; 18 | picture: string; 19 | }; 20 | upvotes: number; 21 | createdAt: Date; 22 | } 23 | const AnswerCard = ({ 24 | clerkId, 25 | _id, 26 | question, 27 | author, 28 | upvotes, 29 | createdAt 30 | }: Props) => { 31 | const showActionButtons = clerkId && clerkId === author.clerkId; 32 | 33 | return ( 34 |
35 | 36 |
37 |
38 | 39 | {getTimeStamp(createdAt)} 40 | 41 |

42 | {question.title} 43 |

44 |
45 | 46 | {showActionButtons && ( 47 | 48 | )} 49 | 50 |
51 | 52 |
53 | 62 | 63 |
64 | 71 |
72 |
73 | 74 |
75 | ); 76 | }; 77 | export default AnswerCard; 78 | -------------------------------------------------------------------------------- /components/cards/QuestionCard.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | import RenderTag from '../shared/RightSidebar/RenderTag'; 3 | import Metric from '../shared/Metric/Metric'; 4 | import { formatAndDivideNumber, getTimeStamp } from '@/lib/utils'; 5 | import { SignedIn } from '@clerk/nextjs'; 6 | import EditDeleteAction from '../shared/EditDeleteAction/EditDeleteAction'; 7 | 8 | interface QuestionCardProps { 9 | _id: string; 10 | title: string; 11 | tags: { 12 | _id: string; 13 | name: string; 14 | }[]; 15 | author: { 16 | _id: string; 17 | clerkId: string; 18 | name: string; 19 | picture: string; 20 | }; 21 | upvotes: number; 22 | views: number; 23 | answers: Array; 24 | createdAt: Date; 25 | clerkId?: string | null; 26 | } 27 | 28 | const QuestionCard = ({ 29 | _id, 30 | clerkId, 31 | title, 32 | tags, 33 | author, 34 | upvotes, 35 | createdAt, 36 | views, 37 | answers 38 | }: QuestionCardProps) => { 39 | const showActionButtons = clerkId && clerkId === author.clerkId; 40 | return ( 41 |
42 |
43 |
44 | 45 | {getTimeStamp(createdAt)} 46 | 47 | 48 |

49 | {title} 50 |

51 | 52 |
53 | 54 | {/* //Todo: if sign-in edit delete actions */} 55 | 56 | {showActionButtons && ( 57 | 58 | )} 59 | 60 |
61 | 62 |
63 | {tags.map((tag) => ( 64 | 65 | ))} 66 |
67 | 68 |
69 | 78 | 79 |
80 | 88 | 96 | 104 |
105 |
106 |
107 | ); 108 | }; 109 | export default QuestionCard; 110 | -------------------------------------------------------------------------------- /components/cards/UserCard.tsx: -------------------------------------------------------------------------------- 1 | import { getTopInteractedTags } from '@/lib/actions/tag.action'; 2 | import Image from 'next/image'; 3 | import Link from 'next/link'; 4 | import { Badge } from '../ui/badge'; 5 | import RenderTag from '../shared/RightSidebar/RenderTag'; 6 | 7 | interface UserCardProps { 8 | user: { 9 | _id: string; 10 | clerkId: string; 11 | name: string; 12 | username: string; 13 | picture: string; 14 | }; 15 | } 16 | 17 | const UserCard = async ({ user }: UserCardProps) => { 18 | const interactedTags = await getTopInteractedTags({ userId: user._id }); 19 | 20 | return ( 21 | 25 |
26 | user-profile-picture 33 |
34 |

35 | {user.name} 36 |

37 |

38 | @{user.username} 39 |

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

{totalAnswers} Answers

38 | {/* Filters */} 39 | 40 |
41 | {/* Dispay answers */} 42 |
43 | {answers.map((answer) => ( 44 |
45 | {/* SPAN ID */} 46 |
47 | 51 | profile 58 |
59 |

60 | {answer.author.name}{' '} 61 |

62 |

63 | answered {getTimeStamp(answer.createdAt)} 64 |

65 |
66 | 67 | {/* Voting section */} 68 |
69 | 78 |
79 |
80 | 81 |
82 | ))} 83 |
84 |
85 | 86 |
87 |
88 | ); 89 | }; 90 | export default AllAnswers; 91 | -------------------------------------------------------------------------------- /components/shared/AnswersTab/AnswersTab.tsx: -------------------------------------------------------------------------------- 1 | import AnswerCard from '@/components/cards/AnswerCard'; 2 | import { getUserAnswers } from '@/lib/actions/user.action'; 3 | import Pagination from '../Pagination/Pagination'; 4 | 5 | interface AnswersTabProps { 6 | userId: string; 7 | clerkId?: string | null; 8 | searchProps?: { [key: string]: string | undefined }; 9 | } 10 | 11 | const AnswersTab = async ({ 12 | searchProps, 13 | userId, 14 | clerkId 15 | }: AnswersTabProps) => { 16 | const { userAnswers, isNextAnswer } = await getUserAnswers({ 17 | userId, 18 | page: searchProps?.page ? +searchProps?.page : 1 19 | }); 20 | 21 | const pageNumber = searchProps?.page ? +searchProps?.page : 1; 22 | 23 | return ( 24 | <> 25 | {userAnswers.map((answer) => ( 26 | 35 | ))} 36 | 37 |
38 | 39 |
40 | 41 | ); 42 | }; 43 | export default AnswersTab; 44 | -------------------------------------------------------------------------------- /components/shared/EditDeleteAction/EditDeleteAction.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { toast } from '@/components/ui/use-toast'; 4 | import { deleteAnswer } from '@/lib/actions/answer.action'; 5 | import { deleteQuestion } from '@/lib/actions/question.action'; 6 | import Image from 'next/image'; 7 | import { usePathname, useRouter } from 'next/navigation'; 8 | 9 | interface Props { 10 | type: string; 11 | itemId: string; 12 | } 13 | const EditDeleteAction = ({ type, itemId }: Props) => { 14 | const pathname = usePathname(); 15 | const router = useRouter(); 16 | const handleEdit = () => { 17 | router.push(`/question/edit/${JSON.parse(itemId)}`); 18 | }; 19 | const handleDelete = async () => { 20 | if (type === 'Question') { 21 | // delete Question 22 | await deleteQuestion({ questionId: JSON.parse(itemId), path: pathname }); 23 | 24 | toast({ 25 | title: ` Question Deleted `, 26 | variant: 'destructive', 27 | description: 'Your question has been deleted successfully' 28 | }); 29 | } else if (type === 'Answer') { 30 | // delete Answer 31 | await deleteAnswer({ answerId: JSON.parse(itemId), path: pathname }); 32 | 33 | toast({ 34 | title: `Answer Deleted `, 35 | variant: 'destructive', 36 | description: 'Your answer has been deleted successfully' 37 | }); 38 | } 39 | }; 40 | return ( 41 |
42 | {type === 'Question' && ( 43 | Edit 51 | )} 52 | Delele 60 |
61 | ); 62 | }; 63 | export default EditDeleteAction; 64 | -------------------------------------------------------------------------------- /components/shared/Filter/Filter.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 | import { formUrlQuery } from '@/lib/utils'; 12 | import { useRouter, useSearchParams } from 'next/navigation'; 13 | 14 | interface Props { 15 | filters: { name: string; value: string }[]; 16 | otherClasses?: string; 17 | containerClasses?: string; 18 | } 19 | 20 | const Filter = ({ filters, otherClasses, containerClasses }: Props) => { 21 | const searchParams = useSearchParams(); 22 | const router = useRouter(); 23 | 24 | const paramsFilter = searchParams.get('filter'); 25 | const handleUpdateParams = (value: string) => { 26 | const newUrl = formUrlQuery({ 27 | params: searchParams.toString(), 28 | key: 'filter', 29 | value 30 | }); 31 | router.push(newUrl, { scroll: false }); 32 | }; 33 | return ( 34 |
35 | 62 |
63 | ); 64 | }; 65 | export default Filter; 66 | -------------------------------------------------------------------------------- /components/shared/Filter/JobsFilter.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import Image from 'next/image'; 4 | import { usePathname, useRouter, useSearchParams } from 'next/navigation'; 5 | 6 | import { 7 | Select, 8 | SelectContent, 9 | SelectGroup, 10 | SelectItem, 11 | SelectTrigger, 12 | SelectValue 13 | } from '@/components/ui/select'; 14 | import { Country } from '@/types'; 15 | import { formUrlQuery } from '@/lib/utils'; 16 | import LocalSearchBar from '../search/LocalSearchBar'; 17 | 18 | interface JobsFilterProps { 19 | countriesList: Country[]; 20 | } 21 | 22 | const JobsFilter = ({ countriesList }: JobsFilterProps) => { 23 | const router = useRouter(); 24 | const pathname = usePathname(); 25 | const searchParams = useSearchParams(); 26 | 27 | const handleUpdateParams = (value: string) => { 28 | const newUrl = formUrlQuery({ 29 | params: searchParams.toString(), 30 | key: 'location', 31 | value 32 | }); 33 | 34 | router.push(newUrl, { scroll: false }); 35 | }; 36 | 37 | return ( 38 |
39 | 46 | 47 | 78 |
79 | ); 80 | }; 81 | 82 | export default JobsFilter; 83 | -------------------------------------------------------------------------------- /components/shared/LeftSidebar/LeftSidebar.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Button } from '@/components/ui/button'; 4 | import { sidebarLinks } from '@/constants'; 5 | import { SignedOut, useAuth } from '@clerk/nextjs'; 6 | import Image from 'next/image'; 7 | import Link from 'next/link'; 8 | import { usePathname } from 'next/navigation'; 9 | 10 | const LeftSidebar = () => { 11 | const pathname = usePathname(); 12 | const { userId } = useAuth(); 13 | return ( 14 |
15 |
16 | {sidebarLinks.map((link) => { 17 | const isActive = 18 | (pathname.includes(link.route) && link.route.length > 1) || 19 | pathname === link.route; 20 | 21 | // TODO: profile/id 22 | if (link.route === '/profile') { 23 | if (userId) { 24 | link.route = `/profile/${userId}`; 25 | } else { 26 | return null; 27 | } 28 | } 29 | 30 | return ( 31 | 40 | {link.label} 47 |

52 | {link.label} 53 |

54 | 55 | ); 56 | })} 57 |
58 | 59 |
60 | 61 | 73 | 74 | 75 | 85 | 86 |
87 |
88 | {/* 89 | 100 | */} 101 |
102 | ); 103 | }; 104 | export default LeftSidebar; 105 | -------------------------------------------------------------------------------- /components/shared/Metric/Metric.tsx: -------------------------------------------------------------------------------- 1 | import Image from 'next/image'; 2 | import Link from 'next/link'; 3 | 4 | interface MetricProps { 5 | imgUrl: string; 6 | title: string; 7 | alt: string; 8 | upvotes?: number; 9 | value: number | string; 10 | textStyles: string; 11 | isAuthor?: boolean; 12 | href?: string; 13 | } 14 | 15 | const Metric = ({ 16 | imgUrl, 17 | alt, 18 | title, 19 | upvotes, 20 | value, 21 | textStyles, 22 | isAuthor, 23 | href 24 | }: MetricProps) => { 25 | const metricContent = ( 26 | <> 27 | {alt} 34 |

35 | {value} 36 | 41 | {title} 42 | 43 |

44 | 45 | ); 46 | 47 | if (href) { 48 | return ( 49 | 50 | {metricContent} 51 | 52 | ); 53 | } 54 | 55 | return
{metricContent}
; 56 | }; 57 | export default Metric; 58 | -------------------------------------------------------------------------------- /components/shared/NoResult/NoResult.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from '@/components/ui/button'; 2 | import Image from 'next/image'; 3 | import Link from 'next/link'; 4 | 5 | interface NoResultProps { 6 | title: string; 7 | link: string; 8 | linkTitle: string; 9 | description: string; 10 | } 11 | 12 | const NoResult = ({ title, link, linkTitle, description }: NoResultProps) => { 13 | return ( 14 |
15 | Not-result-page 22 | Not-result-page 29 |

{title}

30 |

31 | {description} 32 |

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

{pageNumber}

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

{title}

24 | )} 25 |
26 | ); 27 | }; 28 | export default ProfileLink; 29 | -------------------------------------------------------------------------------- /components/shared/QuestionTab/QuestionTab.tsx: -------------------------------------------------------------------------------- 1 | import QuestionCard from '@/components/cards/QuestionCard'; 2 | import { getUserQuestions } from '@/lib/actions/user.action'; 3 | import Pagination from '../Pagination/Pagination'; 4 | 5 | interface QuestionTabProps { 6 | userId: string; 7 | clerkId?: string | null; 8 | searchProps?: { [key: string]: string | undefined }; 9 | } 10 | 11 | const QuestionTab = async ({ 12 | searchProps, 13 | userId, 14 | clerkId 15 | }: QuestionTabProps) => { 16 | const { userQuestions, isNextQuestion } = await getUserQuestions({ 17 | userId, 18 | page: searchProps?.page ? +searchProps.page : 1 19 | }); 20 | 21 | const pageNumber = searchProps?.page ? +searchProps?.page : 1; 22 | 23 | return ( 24 | <> 25 | {userQuestions.map((question) => ( 26 | 38 | ))} 39 |
40 | 41 |
42 | 43 | ); 44 | }; 45 | export default QuestionTab; 46 | -------------------------------------------------------------------------------- /components/shared/RightSidebar/RenderTag.tsx: -------------------------------------------------------------------------------- 1 | // interface example 2 | 3 | import Link from 'next/link'; 4 | import { Badge } from '@/components/ui/badge'; 5 | 6 | interface RenderTagProps { 7 | _id: string; 8 | name: string; 9 | totalQuestions?: number; 10 | showCount?: boolean; 11 | } 12 | 13 | const RenderTag = ({ 14 | _id, 15 | name, 16 | totalQuestions, 17 | showCount 18 | }: RenderTagProps) => { 19 | return ( 20 | 21 | 22 | {name} 23 | 24 | {showCount && ( 25 |

{totalQuestions}

26 | )} 27 | 28 | ); 29 | }; 30 | export default RenderTag; 31 | -------------------------------------------------------------------------------- /components/shared/RightSidebar/RightSidebar.tsx: -------------------------------------------------------------------------------- 1 | import Image from 'next/image'; 2 | import Link from 'next/link'; 3 | import RenderTag from './RenderTag'; 4 | import { getTopQuestions } from '@/lib/actions/question.action'; 5 | import { getTopPopularTags } from '@/lib/actions/tag.action'; 6 | 7 | const RightSidebar = async () => { 8 | const topQuestions = await getTopQuestions(); 9 | const popularTags = await getTopPopularTags(); 10 | return ( 11 |
12 |
13 |

Top Questions

14 |
15 | {topQuestions.map((question) => ( 16 | 21 |

22 | {question.title} 23 |

24 | chevron right 31 | 32 | ))} 33 |
34 |
35 |

Popular Tags

36 |
37 | {popularTags.map((tag) => ( 38 | 45 | ))} 46 |
47 |
48 |
49 |
50 | ); 51 | }; 52 | export default RightSidebar; 53 | -------------------------------------------------------------------------------- /components/shared/Stats/Stats.tsx: -------------------------------------------------------------------------------- 1 | import { formatAndDivideNumber } from '@/lib/utils'; 2 | import { BadgeCounts } from '@/types'; 3 | import Image from 'next/image'; 4 | 5 | interface StatsCardProps { 6 | imgUrl: string; 7 | value: number; 8 | title: string; 9 | } 10 | const StatsCard = ({ imgUrl, value, title }: StatsCardProps) => { 11 | return ( 12 |
13 | {title} 14 |
15 |

16 | {formatAndDivideNumber(value)} 17 |

18 |

{title}

19 |
20 |
21 | ); 22 | }; 23 | 24 | interface StatsProps { 25 | totalQuestions: number; 26 | totalAnswers: number; 27 | badges: BadgeCounts; 28 | reputation: number; 29 | } 30 | 31 | const Stats = ({ 32 | totalQuestions, 33 | totalAnswers, 34 | badges, 35 | reputation 36 | }: StatsProps) => { 37 | return ( 38 |
39 |

40 | Stats - {reputation} 41 |

42 |
43 |
44 |
45 |

46 | {formatAndDivideNumber(totalQuestions)} 47 |

48 |

Questions

49 |
50 |
51 |

52 | {formatAndDivideNumber(totalAnswers)} 53 |

54 |

Answers

55 |
56 |
57 | {/* show badges */} 58 | 63 | 68 | 73 |
74 |
75 | ); 76 | }; 77 | export default Stats; 78 | -------------------------------------------------------------------------------- /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 Link from 'next/link'; 10 | import Image from 'next/image'; 11 | import { SignedOut } from '@clerk/nextjs'; 12 | import { Button } from '@/components/ui/button'; 13 | import { sidebarLinks } from '@/constants'; 14 | import { usePathname } from 'next/navigation'; 15 | const NavContent = () => { 16 | const pathname = usePathname(); 17 | return ( 18 |
19 | {sidebarLinks.map((link) => { 20 | const isActive = 21 | (pathname.includes(link.route) && link.route.length > 1) || 22 | pathname === link.route; 23 | return ( 24 | 25 | 33 | {link.label} 40 |

41 | {link.label} 42 |

43 | 44 |
45 | ); 46 | })} 47 |
48 | ); 49 | }; 50 | 51 | const MobileNav = () => { 52 | return ( 53 | 54 | 55 | Humburger menu 62 | 63 | 67 | 68 | Devflow 74 |

75 | Dev Overflow 76 |

77 | 78 | 79 |
80 | 81 | 82 | 83 | 84 | 85 |
86 | 87 | 88 | 91 | 92 | 93 | 94 | 95 | 98 | 99 | 100 |
101 |
102 |
103 |
104 |
105 | ); 106 | }; 107 | export default MobileNav; 108 | -------------------------------------------------------------------------------- /components/shared/navbar/Navbar.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | import Image from 'next/image'; 3 | import { SignedIn, UserButton } from '@clerk/nextjs'; 4 | import Theme from './Theme'; 5 | import MobileNav from './MobileNav'; 6 | import GlobalSearch from '../search/GlobalSearch'; 7 | 8 | const Navbar = () => { 9 | return ( 10 | 43 | ); 44 | }; 45 | export default Navbar; 46 | -------------------------------------------------------------------------------- /components/shared/navbar/Theme.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useTheme } from '@/context/ThemeProvider'; 4 | import { 5 | Menubar, 6 | MenubarContent, 7 | MenubarItem, 8 | MenubarMenu, 9 | MenubarTrigger 10 | } from '@/components/ui/menubar'; 11 | import Image from 'next/image'; 12 | import { themes } from '@/constants'; 13 | 14 | const Theme = () => { 15 | const { mode, setMode } = useTheme(); 16 | return ( 17 | 18 | 19 | 20 | {mode === 'light' ? ( 21 | sun 28 | ) : ( 29 | moon 36 | )} 37 | 38 | 39 | {themes.map((item) => ( 40 | { 44 | setMode(item.value); 45 | 46 | if (item.value !== 'system') { 47 | localStorage.theme = item.value; 48 | } else { 49 | localStorage.removeItem('theme'); 50 | } 51 | }} 52 | > 53 | {item.value} 60 |

67 | {item.label} 68 |

69 |
70 | ))} 71 |
72 |
73 |
74 | ); 75 | }; 76 | export default Theme; 77 | -------------------------------------------------------------------------------- /components/shared/search/GlobalFilters.tsx: -------------------------------------------------------------------------------- 1 | import { GlobalSearchFilters } from '@/constants/filters'; 2 | import { formUrlQuery } from '@/lib/utils'; 3 | import { useRouter, useSearchParams } from 'next/navigation'; 4 | import { useState } from 'react'; 5 | 6 | const GlobalFilters = () => { 7 | const router = useRouter(); 8 | const searchParams = useSearchParams(); 9 | const typeParams = searchParams.get('type'); 10 | const [active, setActive] = useState(typeParams || ''); 11 | 12 | const handleFilterClick = (item: string) => { 13 | if (active === item) { 14 | setActive(''); 15 | const newUrl = formUrlQuery({ 16 | params: searchParams.toString(), 17 | key: 'type', 18 | value: null 19 | }); 20 | router.push(newUrl, { scroll: false }); 21 | } else { 22 | setActive(item); 23 | const newUrl = formUrlQuery({ 24 | params: searchParams.toString(), 25 | key: 'type', 26 | value: item.toLowerCase() 27 | }); 28 | router.push(newUrl, { scroll: false }); 29 | } 30 | }; 31 | return ( 32 |
33 |

Type:

34 |
35 | {GlobalSearchFilters.map((item) => ( 36 | 48 | ))} 49 |
50 |
51 | ); 52 | }; 53 | export default GlobalFilters; 54 | -------------------------------------------------------------------------------- /components/shared/search/GlobalSearch.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Input } from '@/components/ui/input'; 4 | import { formUrlQuery, removeKeysFromQuery } from '@/lib/utils'; 5 | import Image from 'next/image'; 6 | import { usePathname, useRouter, useSearchParams } from 'next/navigation'; 7 | import { useEffect, useRef, useState } from 'react'; 8 | import GlobarResult from './GlobarResult'; 9 | 10 | const GlobalSearch = () => { 11 | const router = useRouter(); 12 | const pathname = usePathname(); 13 | const searchParams = useSearchParams(); 14 | const searchContainerRef = useRef(null); 15 | 16 | const query = searchParams.get('global'); 17 | 18 | const [search, setSearch] = useState(query || ''); 19 | const [isModalOpen, setIsModalOpen] = useState(false); 20 | 21 | // close modal useEffect 22 | useEffect(() => { 23 | const handleOutsideClick = (e: any) => { 24 | if ( 25 | searchContainerRef.current && 26 | // @ts-ignore 27 | !searchContainerRef.current.contains(e.target) 28 | ) { 29 | setIsModalOpen(false); 30 | setSearch(''); 31 | } 32 | }; 33 | 34 | setIsModalOpen(false); 35 | 36 | document.addEventListener('click', handleOutsideClick); 37 | 38 | return () => document.removeEventListener('click', handleOutsideClick); 39 | }, [pathname]); 40 | 41 | useEffect(() => { 42 | const delayDebounceFn = setTimeout(() => { 43 | if (search) { 44 | const newUrl = formUrlQuery({ 45 | params: searchParams.toString(), 46 | key: 'global', 47 | value: search 48 | }); 49 | router.push(newUrl, { scroll: false }); 50 | } else { 51 | if (query) { 52 | const newUrl = removeKeysFromQuery({ 53 | params: searchParams.toString(), 54 | keysToRemove: ['global', 'type'] 55 | }); 56 | 57 | router.push(newUrl, { scroll: false }); 58 | } 59 | } 60 | }, 300); 61 | 62 | return () => clearTimeout(delayDebounceFn); 63 | }, [query, search, router, pathname, searchParams]); 64 | 65 | return ( 66 |
70 |
71 | search 78 | { 82 | setSearch(e.target.value); 83 | if (!isModalOpen) { 84 | setIsModalOpen(true); 85 | } 86 | if (e.target.value === '' && isModalOpen) { 87 | setIsModalOpen(false); 88 | } 89 | }} 90 | placeholder="Search globally" 91 | className="paragraph-regular text-dark400_light700 no-focus placeholder border-none bg-transparent shadow-none outline-none" 92 | /> 93 |
94 | {isModalOpen && } 95 |
96 | ); 97 | }; 98 | export default GlobalSearch; 99 | -------------------------------------------------------------------------------- /components/shared/search/GlobarResult.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useEffect, useState } from 'react'; 4 | import { ReloadIcon } from '@radix-ui/react-icons'; 5 | import { useSearchParams } from 'next/navigation'; 6 | import Link from 'next/link'; 7 | import Image from 'next/image'; 8 | import GlobalFilters from './GlobalFilters'; 9 | import { globalSearch } from '@/lib/actions/general.action'; 10 | 11 | const GlobarResult = () => { 12 | const searchParams = useSearchParams(); 13 | const [isLoading, setIsLoading] = useState(false); 14 | 15 | const [result, setResult] = useState([ 16 | { type: 'question', id: 1, title: 'Next.js question' }, 17 | { type: 'tag', id: 1, title: 'Nextjs' }, 18 | { type: 'user', id: 1, title: 'jsm' } 19 | ]); 20 | 21 | const global = searchParams.get('global'); 22 | const type = searchParams.get('type'); 23 | 24 | // global search useEffect 25 | useEffect(() => { 26 | const fetchResult = async () => { 27 | setResult([]); 28 | setIsLoading(true); 29 | try { 30 | // fetch everything everywhere all at once => globalSearch, filter 31 | const res = await globalSearch({ 32 | query: global, 33 | type 34 | }); 35 | 36 | setResult(JSON.parse(res)); 37 | } catch (error) { 38 | console.log(error); 39 | throw error; 40 | } finally { 41 | setIsLoading(false); 42 | } 43 | }; 44 | 45 | if (global) { 46 | fetchResult(); 47 | } 48 | }, [global, type]); 49 | 50 | const renderLink = (type: string, id: string) => { 51 | switch (type) { 52 | case 'question': 53 | return `/question/${id}`; 54 | 55 | case 'answer': 56 | return `/question/${id}`; 57 | 58 | case 'tag': 59 | return `/tags/${id}`; 60 | 61 | case 'user': 62 | return `/profile/${id}`; 63 | 64 | default: 65 | return '/'; 66 | } 67 | }; 68 | 69 | return ( 70 |
71 |

72 | 73 |

74 |
75 |
76 |

77 | Top Match 78 |

79 | 80 | {isLoading ? ( 81 |
82 | 83 |

84 | Browsing the entire database 85 |

86 |
87 | ) : ( 88 |
89 | {result.length > 0 ? ( 90 | result.map((item: any, index: number) => ( 91 | 96 | tags 103 |
104 |

105 | {item.title} 106 |

107 |

108 | {item.type} 109 |

110 |
111 | 112 | )) 113 | ) : ( 114 |
115 |

116 | Opps! No result found ! 117 |

118 |
119 | )} 120 |
121 | )} 122 |
123 |
124 | ); 125 | }; 126 | export default GlobarResult; 127 | -------------------------------------------------------------------------------- /components/shared/search/LocalSearchBar.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Input } from '@/components/ui/input'; 4 | import { formUrlQuery, removeKeysFromQuery } from '@/lib/utils'; 5 | import Image from 'next/image'; 6 | import { usePathname, useRouter, useSearchParams } from 'next/navigation'; 7 | import { useEffect, useState } from 'react'; 8 | 9 | interface CustomeInputProps { 10 | route: string; 11 | iconPosition: string; 12 | imgSrc: string; 13 | placeholder: string; 14 | otherClasses?: string; 15 | } 16 | 17 | const LocalSearchBar = ({ 18 | route, 19 | iconPosition, 20 | imgSrc, 21 | placeholder, 22 | otherClasses 23 | }: CustomeInputProps) => { 24 | const router = useRouter(); 25 | const pathname = usePathname(); 26 | const searchParams = useSearchParams(); 27 | 28 | const query = searchParams.get('q'); 29 | 30 | const [search, setSearch] = useState(query || ''); 31 | 32 | useEffect(() => { 33 | const delayDebounceFn = setTimeout(() => { 34 | if (search) { 35 | const newUrl = formUrlQuery({ 36 | params: searchParams.toString(), 37 | key: 'q', 38 | value: search 39 | }); 40 | router.push(newUrl, { scroll: false }); 41 | } else { 42 | if (pathname === route) { 43 | const newUrl = removeKeysFromQuery({ 44 | params: searchParams.toString(), 45 | keysToRemove: ['q'] 46 | }); 47 | router.push(newUrl, { scroll: false }); 48 | } 49 | } 50 | }, 300); 51 | 52 | return () => clearTimeout(delayDebounceFn); 53 | }, [query, search, route, router, pathname, searchParams]); 54 | 55 | return ( 56 |
59 | {iconPosition === 'left' && ( 60 | search icon 67 | )} 68 | setSearch(e.target.value)} 73 | className="paragraph-regular text-dark400_light700 no-focus placeholder border-none bg-transparent shadow-none outline-none" 74 | /> 75 | {iconPosition === 'right' && ( 76 | search icon 83 | )} 84 |
85 | ); 86 | }; 87 | export default LocalSearchBar; 88 | -------------------------------------------------------------------------------- /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 | 'ring-offset-background focus-visible:ring-ring inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 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-input bg-background hover:bg-accent hover:text-accent-foreground border', 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 | import { cn } from '@/lib/utils'; 2 | 3 | import React from 'react'; 4 | 5 | function Skeleton({ 6 | className, 7 | ...props 8 | }: React.HTMLAttributes) { 9 | return ( 10 |
17 | ); 18 | } 19 | 20 | export { Skeleton }; 21 | -------------------------------------------------------------------------------- /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 |