├── .eslintrc.json ├── .gitignore ├── 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 │ ├── 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 │ ├── QuestionCard.tsx │ └── UserCard.tsx ├── forms │ ├── Answer.tsx │ ├── Profile.tsx │ └── Question.tsx ├── home │ └── HomeFilters.tsx ├── shared │ ├── AllAnswers.tsx │ ├── AnswersTab.tsx │ ├── EditDeleteAction.tsx │ ├── Filter.tsx │ ├── LeftSidebar.tsx │ ├── Metric.tsx │ ├── NoResult.tsx │ ├── Pagination.tsx │ ├── ParseHTML.tsx │ ├── ProfileLink.tsx │ ├── QuestionTab.tsx │ ├── RenderTag.tsx │ ├── RightSidebar.tsx │ ├── Stats.tsx │ ├── Votes.tsx │ ├── navbar │ │ ├── MobileNav.tsx │ │ ├── Navbar.tsx │ │ └── Theme.tsx │ └── search │ │ ├── GlobalFilters.tsx │ │ ├── GlobalResult.tsx │ │ ├── GlobalSearch.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.tsx ├── 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 │ ├── question.action.ts │ ├── shared.types.d.ts │ ├── tag.actions.ts │ └── user.action.ts ├── mongoose.ts ├── utils.ts └── validations.ts ├── middleware.ts ├── next.config.js ├── package-lock.json ├── package.json ├── pagination.js ├── 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": ["next/core-web-vitals", "standard", "plugin:tailwindcss/recommended", "prettier"] 3 | } 4 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Dev Overflow. 2 | 3 | ![Dev Overflow](https://i.ibb.co/x7FChRP/Thumbnail.jpg) 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /app/(auth)/sign-in/[[...sign-in]]/page.tsx: -------------------------------------------------------------------------------- 1 | import { SignIn } from "@clerk/nextjs"; 2 | 3 | export default function Page() { 4 | return ; 5 | } -------------------------------------------------------------------------------- /app/(auth)/sign-up/[[...sign-up]]/page.tsx: -------------------------------------------------------------------------------- 1 | import { SignUp } from "@clerk/nextjs"; 2 | 3 | export default function Page() { 4 | return ; 5 | } -------------------------------------------------------------------------------- /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 | 13 | 16 | 17 |
18 | 19 |
20 | 21 |
22 | 23 |
24 |
25 | 26 |
27 | 28 | 29 | 30 | 31 |
32 | 33 |
34 | {[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((item) => ( 35 | 36 | ))} 37 |
38 |
39 | ) 40 | } 41 | 42 | export default Loading -------------------------------------------------------------------------------- /app/(root)/(home)/page.tsx: -------------------------------------------------------------------------------- 1 | import QuestionCard from "@/components/cards/QuestionCard"; 2 | import HomeFilters from "@/components/home/HomeFilters"; 3 | import Filter from "@/components/shared/Filter"; 4 | import NoResult from "@/components/shared/NoResult"; 5 | import Pagination from "@/components/shared/Pagination"; 6 | import LocalSearchbar from "@/components/shared/search/LocalSearchbar"; 7 | import { Button } from "@/components/ui/button"; 8 | import { HomePageFilters } from "@/constants/filters"; 9 | import { getQuestions, getRecommendedQuestions } from "@/lib/actions/question.action"; 10 | import { SearchParamsProps } from "@/types"; 11 | import Link from "next/link"; 12 | 13 | import type { Metadata } from 'next'; 14 | import { auth } from "@clerk/nextjs"; 15 | 16 | export const metadata: Metadata = { 17 | title: 'Home | Dev Overflow', 18 | } 19 | 20 | export default async function Home({ searchParams }: SearchParamsProps) { 21 | const { userId } = auth(); 22 | 23 | let result; 24 | 25 | if(searchParams?.filter === 'recommended') { 26 | if(userId) { 27 | result = await getRecommendedQuestions({ 28 | userId, 29 | searchQuery: searchParams.q, 30 | page: searchParams.page ? +searchParams.page : 1, 31 | }); 32 | } else { 33 | result = { 34 | questions: [], 35 | isNext: false, 36 | } 37 | } 38 | } else { 39 | result = await getQuestions({ 40 | searchQuery: searchParams.q, 41 | filter: searchParams.filter, 42 | page: searchParams.page ? +searchParams.page : 1, 43 | }); 44 | } 45 | 46 | 47 | return ( 48 | <> 49 |
50 |

All Questions

51 | 52 | 53 | 56 | 57 |
58 | 59 |
60 | 67 | 68 | 73 |
74 | 75 | 76 | 77 |
78 | {result.questions.length > 0 ? 79 | result.questions.map((question) => ( 80 | 91 | )) 92 | : } 98 |
99 |
100 | 104 |
105 | 106 | ) 107 | } -------------------------------------------------------------------------------- /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 | import React from 'react' 6 | 7 | const Page = async () => { 8 | const { userId } = auth(); 9 | 10 | if (!userId) redirect('/sign-in'); 11 | 12 | const mongoUser = await getUserById({ userId }); 13 | 14 | return ( 15 |
16 |

Ask a question

17 | 18 |
19 | 20 |
21 |
22 | ) 23 | } 24 | 25 | export default Page -------------------------------------------------------------------------------- /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 QuestionCard from "@/components/cards/QuestionCard"; 2 | import Filter from "@/components/shared/Filter"; 3 | import NoResult from "@/components/shared/NoResult"; 4 | import Pagination from "@/components/shared/Pagination"; 5 | import LocalSearchbar from "@/components/shared/search/LocalSearchbar"; 6 | import { QuestionFilters } from "@/constants/filters"; 7 | import { getSavedQuestions } from "@/lib/actions/user.action"; 8 | import { SearchParamsProps } from "@/types"; 9 | import { auth } from '@clerk/nextjs' 10 | 11 | export default async function Home({ searchParams }: SearchParamsProps) { 12 | const { userId } = auth(); 13 | 14 | if(!userId) return null; 15 | 16 | const result = await getSavedQuestions({ 17 | clerkId: userId, 18 | searchQuery: searchParams.q, 19 | filter: searchParams.filter, 20 | page: searchParams.page ? +searchParams.page : 1, 21 | }); 22 | 23 | return ( 24 | <> 25 |

Saved Questions

26 | 27 |
28 | 35 | 36 | 40 |
41 | 42 |
43 | {result.questions.length > 0 ? 44 | result.questions.map((question: any) => ( 45 | 56 | )) 57 | : } 63 |
64 | 65 |
66 | 70 |
71 | 72 | ) 73 | } -------------------------------------------------------------------------------- /app/(root)/community/loading.tsx: -------------------------------------------------------------------------------- 1 | import { Skeleton } from '@/components/ui/skeleton' 2 | import React from 'react' 3 | 4 | const Loading = () => { 5 | return ( 6 |
7 |

All Users

8 | 9 |
10 | 11 | 12 |
13 | 14 |
15 | {[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((item) => ( 16 | 17 | ))} 18 |
19 |
20 | ) 21 | } 22 | 23 | export default Loading -------------------------------------------------------------------------------- /app/(root)/community/page.tsx: -------------------------------------------------------------------------------- 1 | import UserCard from '@/components/cards/UserCard' 2 | import Filter from '@/components/shared/Filter' 3 | import Pagination from '@/components/shared/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 | import type { Metadata } from 'next'; 10 | 11 | export const metadata: Metadata = { 12 | title: 'Community | Dev Overflow', 13 | } 14 | 15 | const Page = async ({ searchParams }: SearchParamsProps) => { 16 | const result = await getAllUsers({ 17 | searchQuery: searchParams.q, 18 | filter: searchParams.filter, 19 | page: searchParams.page ? +searchParams.page : 1, 20 | }) 21 | 22 | return ( 23 | <> 24 |

All Users

25 | 26 |
27 | 34 | 35 | 39 |
40 | 41 |
42 | {result.users.length > 0 ? ( 43 | result.users.map((user)=> ( 44 | 45 | )) 46 | ) : ( 47 |
48 |

No users yet

49 | 50 | Join to be the first! 51 | 52 |
53 | )} 54 |
55 | 56 |
57 | 61 |
62 | 63 | ) 64 | } 65 | 66 | export default Page -------------------------------------------------------------------------------- /app/(root)/layout.tsx: -------------------------------------------------------------------------------- 1 | import LeftSidebar from '@/components/shared/LeftSidebar' 2 | import RightSidebar from '@/components/shared/RightSidebar' 3 | import Navbar from '@/components/shared/navbar/Navbar' 4 | import { Toaster } from '@/components/ui/toaster' 5 | import React from 'react' 6 | 7 | const Layout = ({ children }: { children: React.ReactNode }) => { 8 | return ( 9 |
10 | 11 |
12 | 13 | 14 |
15 |
16 | {children} 17 |
18 |
19 | 20 | 21 |
22 | 23 | 24 |
25 | ) 26 | } 27 | 28 | export default Layout -------------------------------------------------------------------------------- /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/[id]/page.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from '@/components/ui/button' 2 | import { getUserInfo } from '@/lib/actions/user.action' 3 | import { URLProps } from '@/types' 4 | import { SignedIn, auth } from '@clerk/nextjs' 5 | import Image from 'next/image' 6 | import Link from 'next/link' 7 | import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" 8 | 9 | import React from 'react' 10 | import { getJoinedDate } from '@/lib/utils' 11 | import ProfileLink from '@/components/shared/ProfileLink' 12 | import Stats from '@/components/shared/Stats' 13 | import QuestionTab from '@/components/shared/QuestionTab' 14 | import AnswersTab from '@/components/shared/AnswersTab' 15 | 16 | const Page = async ({ params, searchParams}: URLProps) => { 17 | const { userId: clerkId } = auth(); 18 | const userInfo = await getUserInfo({ userId: params.id}) 19 | 20 | return ( 21 | <> 22 |
23 |
24 | profile picture 31 | 32 |
33 |

{userInfo.user.name}

34 |

@{userInfo.user.username}

35 | 36 |
37 | {userInfo.user.portfolioWebsite && ( 38 | 43 | )} 44 | 45 | {userInfo.user.location && ( 46 | 50 | )} 51 | 52 | 56 |
57 | 58 | {userInfo.user.bio && ( 59 |

60 | {userInfo.user.bio} 61 |

62 | )} 63 |
64 |
65 | 66 |
67 | 68 | {clerkId === userInfo.user.clerkId && ( 69 | 70 | 73 | 74 | )} 75 | 76 |
77 |
78 | 79 | 85 | 86 |
87 | 88 | 89 | Top Posts 90 | Answers 91 | 92 | 93 | 98 | 99 | 100 | 105 | 106 | 107 |
108 | 109 | ) 110 | } 111 | 112 | export default Page -------------------------------------------------------------------------------- /app/(root)/profile/edit/page.tsx: -------------------------------------------------------------------------------- 1 | import Profile from '@/components/forms/Profile'; 2 | import { getUserById } from '@/lib/actions/user.action'; 3 | import { ParamsProps } from '@/types'; 4 | import { auth } from '@clerk/nextjs' 5 | 6 | const Page = async ({ params }: ParamsProps) => { 7 | const { userId } = auth(); 8 | 9 | if(!userId) return null; 10 | 11 | const mongoUser = await getUserById({ userId }) 12 | 13 | return ( 14 | <> 15 |

Edit Profile

16 | 17 |
18 | 22 |
23 | 24 | ) 25 | } 26 | 27 | export default Page -------------------------------------------------------------------------------- /app/(root)/question/[id]/page.tsx: -------------------------------------------------------------------------------- 1 | import Answer from '@/components/forms/Answer'; 2 | import AllAnswers from '@/components/shared/AllAnswers'; 3 | import Metric from '@/components/shared/Metric'; 4 | import ParseHTML from '@/components/shared/ParseHTML'; 5 | import RenderTag from '@/components/shared/RenderTag'; 6 | import Votes from '@/components/shared/Votes'; 7 | import { getQuestionById } from '@/lib/actions/question.action'; 8 | import { getUserById } from '@/lib/actions/user.action'; 9 | import { formatAndDivideNumber, getTimestamp } from '@/lib/utils'; 10 | import { auth } from '@clerk/nextjs'; 11 | import Image from 'next/image'; 12 | import Link from 'next/link'; 13 | import React from 'react' 14 | 15 | const Page = async ({ params, searchParams }: any) => { 16 | const { userId: clerkId } = auth(); 17 | 18 | let mongoUser; 19 | 20 | if(clerkId) { 21 | mongoUser = await getUserById({ userId: clerkId }) 22 | } 23 | 24 | const result = await getQuestionById({ questionId: params.id }); 25 | 26 | return ( 27 | <> 28 |
29 |
30 | 32 | profile 39 |

40 | {result.author.name} 41 |

42 | 43 |
44 | 54 |
55 |
56 |

57 | {result.title} 58 |

59 |
60 | 61 |
62 | 69 | 76 | 83 |
84 | 85 | 86 | 87 |
88 | {result.tags.map((tag: any) => ( 89 | 95 | ))} 96 |
97 | 98 | 105 | 106 | 111 | 112 | ) 113 | } 114 | 115 | export default Page -------------------------------------------------------------------------------- /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 | const Page = async ({ params }: ParamsProps) => { 8 | const { userId } = auth(); 9 | 10 | if(!userId) return null; 11 | 12 | const mongoUser = await getUserById({ userId }) 13 | const result = await getQuestionById({ questionId: params.id}) 14 | 15 | return ( 16 | <> 17 |

Edit Question

18 | 19 |
20 | 25 |
26 | 27 | ) 28 | } 29 | 30 | export default Page -------------------------------------------------------------------------------- /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' 3 | import Pagination from '@/components/shared/Pagination' 4 | import LocalSearchbar from '@/components/shared/search/LocalSearchbar' 5 | import { getQuestionsByTagId } from '@/lib/actions/tag.actions' 6 | import { URLProps } from '@/types' 7 | 8 | const Page = async ({ params, searchParams }: URLProps) => { 9 | const result = await getQuestionsByTagId({ 10 | tagId: params.id, 11 | page: searchParams.page ? +searchParams.page : 1, 12 | searchQuery: searchParams.q 13 | }) 14 | 15 | return ( 16 | <> 17 |

{result.tagTitle}

18 | 19 |
20 | 27 |
28 | 29 |
30 | {result.questions.length > 0 ? 31 | result.questions.map((question: any) => ( 32 | 43 | )) 44 | : } 50 |
51 | 52 |
53 | 57 |
58 | 59 | ) 60 | } 61 | 62 | export default Page -------------------------------------------------------------------------------- /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 Pagination from '@/components/shared/Pagination' 4 | import LocalSearchbar from '@/components/shared/search/LocalSearchbar' 5 | import { TagFilters } from '@/constants/filters' 6 | import { getAllTags } from '@/lib/actions/tag.actions' 7 | import { SearchParamsProps } from '@/types' 8 | import Link from 'next/link' 9 | 10 | const Page = async ({ searchParams }: SearchParamsProps) => { 11 | const result = await getAllTags({ 12 | searchQuery: searchParams.q, 13 | filter: searchParams.filter, 14 | page: searchParams.page ? +searchParams.page : 1, 15 | }) 16 | 17 | return ( 18 | <> 19 |

All Tags

20 | 21 |
22 | 29 | 30 | 34 |
35 | 36 |
37 | {result.tags.length > 0 ? ( 38 | result.tags.map((tag)=> ( 39 | 40 |
41 |
42 |

43 | {tag.name} 44 |

45 |
46 | 47 |

48 | {tag.questions.length}+ Questions 49 |

50 |
51 | 52 | )) 53 | ) : ( 54 | 60 | )} 61 |
62 | 63 |
64 | 68 |
69 | 70 | ) 71 | } 72 | 73 | export default Page -------------------------------------------------------------------------------- /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: 'You are a knowlegeable assistant that provides quality information.' 19 | }, { 20 | role: 'user', 21 | content: `Tell me ${question}` 22 | } 23 | ] 24 | }) 25 | }) 26 | 27 | const responseData = await response.json(); 28 | const reply = responseData.choices[0].message.content; 29 | 30 | return NextResponse.json({ reply }) 31 | } catch (error: any) { 32 | return NextResponse.json({ error: error.message }) 33 | } 34 | } -------------------------------------------------------------------------------- /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 | 10 | // You can find this in the Clerk Dashboard -> Webhooks -> choose the webhook 11 | const WEBHOOK_SECRET = process.env.NEXT_CLERK_WEBHOOK_SECRET 12 | 13 | if (!WEBHOOK_SECRET) { 14 | throw new Error('Please add WEBHOOK_SECRET from Clerk Dashboard to .env or .env.local') 15 | } 16 | 17 | // Get the headers 18 | const headerPayload = headers(); 19 | const svix_id = headerPayload.get("svix-id"); 20 | const svix_timestamp = headerPayload.get("svix-timestamp"); 21 | const svix_signature = headerPayload.get("svix-signature"); 22 | 23 | // If there are no headers, error out 24 | if (!svix_id || !svix_timestamp || !svix_signature) { 25 | return new Response('Error occured -- no svix headers', { 26 | status: 400 27 | }) 28 | } 29 | 30 | // Get the body 31 | const payload = await req.json() 32 | const body = JSON.stringify(payload); 33 | 34 | // Create a new SVIX instance with your secret. 35 | const wh = new Webhook(WEBHOOK_SECRET); 36 | 37 | let evt: WebhookEvent 38 | 39 | // Verify the payload with the headers 40 | try { 41 | evt = wh.verify(body, { 42 | "svix-id": svix_id, 43 | "svix-timestamp": svix_timestamp, 44 | "svix-signature": svix_signature, 45 | }) as WebhookEvent 46 | } catch (err) { 47 | console.error('Error verifying webhook:', err); 48 | return new Response('Error occured', { 49 | status: 400 50 | }) 51 | } 52 | 53 | const eventType = evt.type; 54 | 55 | if(eventType === 'user.created') { 56 | const { id, email_addresses, image_url, username, first_name, last_name } = evt.data; 57 | 58 | // Create a new user in your database 59 | const mongoUser = await createUser({ 60 | clerkId: id, 61 | name: `${first_name}${last_name ? ` ${last_name}` : ''}`, 62 | username: username!, 63 | email: email_addresses[0].email_address, 64 | picture: image_url, 65 | }) 66 | 67 | return NextResponse.json({ message: 'OK', user: mongoUser}) 68 | } 69 | 70 | if(eventType === 'user.updated') { 71 | const { id, email_addresses, image_url, username, first_name, last_name } = evt.data; 72 | 73 | // Create a new user in your database 74 | const mongoUser = await updateUser({ 75 | clerkId: id, 76 | updateData: { 77 | name: `${first_name}${last_name ? ` ${last_name}` : ''}`, 78 | username: username!, 79 | email: email_addresses[0].email_address, 80 | picture: image_url, 81 | }, 82 | path: `/profile/${id}` 83 | }) 84 | 85 | return NextResponse.json({ message: 'OK', user: mongoUser}) 86 | } 87 | 88 | if(eventType === 'user.deleted') { 89 | const { id } = evt.data; 90 | 91 | const deletedUser = await deleteUser({ 92 | clerkId: id!, 93 | }) 94 | 95 | return NextResponse.json({ message: 'OK', user: deletedUser}) 96 | } 97 | 98 | return NextResponse.json({ message: 'OK' }) 99 | } 100 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/starcluster18/stack_overflow_nextjs14/38e6525b23190fe8dbb1ce6f0e0f21e9d01e072e/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 | /* eslint-disable camelcase */ 2 | import React from 'react' 3 | import { ClerkProvider } from '@clerk/nextjs' 4 | import { Inter, Space_Grotesk } from 'next/font/google' 5 | import type { Metadata } from 'next'; 6 | 7 | import './globals.css'; 8 | import '../styles/prism.css'; 9 | import { ThemeProvider } from '@/context/ThemeProvider'; 10 | 11 | const inter = Inter({ 12 | subsets: ['latin'], 13 | weight: ['100', '200', '300', '400', '500', '600', '700', '800', '900'], 14 | variable: '--font-inter' 15 | }) 16 | 17 | const spaceGrotesk = Space_Grotesk({ 18 | subsets: ['latin'], 19 | weight: ['300', '400', '500', '600', '700'], 20 | variable: '--font-spaceGrotesk' 21 | }) 22 | 23 | export const metadata: Metadata = { 24 | title: 'DevFlow', 25 | description: 'A community-driven platform for asking and answering programming questions. Get help, share knowledge, and collaborate with developers from around the world. Explore topics in web development, mobile app development, algorithms, data structures, and more.', 26 | icons: { 27 | icon: '/assets/images/site-logo.svg' 28 | } 29 | } 30 | 31 | export default function RootLayout({ 32 | children, 33 | }: { 34 | children: React.ReactNode 35 | }) { 36 | return ( 37 | 38 | 39 | 47 | 48 | {children} 49 | 50 | 51 | 52 | 53 | ) 54 | } -------------------------------------------------------------------------------- /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 Metric from "../shared/Metric"; 4 | import { formatAndDivideNumber, getTimestamp } from "@/lib/utils"; 5 | import { SignedIn } from "@clerk/nextjs"; 6 | import EditDeleteAction from "../shared/EditDeleteAction"; 7 | 8 | interface Props { 9 | clerkId?: string | null; 10 | _id: string; 11 | question: { 12 | _id: string; 13 | title: string; 14 | }; 15 | author: { 16 | _id: string; 17 | clerkId: string; 18 | name: string; 19 | picture: string; 20 | }; 21 | upvotes: number; 22 | createdAt: Date; 23 | } 24 | 25 | const AnswerCard = ({ 26 | clerkId, 27 | _id, 28 | question, 29 | author, 30 | upvotes, 31 | createdAt, 32 | }: Props) => { 33 | const showActionButtons = clerkId && clerkId === author.clerkId; 34 | 35 | return ( 36 | 40 |
41 |
42 | 43 | {getTimestamp(createdAt)} 44 | 45 |

46 | {question.title} 47 |

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

51 | {title} 52 |

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

33 | {user.name} 34 |

35 |

@{user.username}

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

{totalAnswers} Answers

31 | 32 | 33 |
34 | 35 |
36 | {result.answers.map((answer) => ( 37 |
38 |
39 | 40 | profile 47 |
48 |

49 | {answer.author.name} 50 |

51 | 52 |

53 | answered {" "} 54 | {getTimestamp(answer.createdAt)} 55 |

56 |
57 | 58 |
59 | 68 |
69 | 70 |
71 | 72 |
73 | ))} 74 |
75 | 76 |
77 | 81 |
82 |
83 | ) 84 | } 85 | 86 | export default AllAnswers -------------------------------------------------------------------------------- /components/shared/AnswersTab.tsx: -------------------------------------------------------------------------------- 1 | import { getUserAnswers } from '@/lib/actions/user.action'; 2 | import { SearchParamsProps } from '@/types' 3 | import AnswerCard from '../cards/AnswerCard'; 4 | import Pagination from './Pagination'; 5 | 6 | interface Props extends SearchParamsProps { 7 | userId: string; 8 | clerkId?: string | null; 9 | } 10 | 11 | const AnswersTab = async ({ searchParams, userId, clerkId }: Props) => { 12 | const result = await getUserAnswers({ 13 | userId, 14 | page: searchParams.page ? +searchParams.page : 1, 15 | }) 16 | 17 | return ( 18 | <> 19 | {result.answers.map((item) => ( 20 | 29 | ))} 30 | 31 |
32 | 36 |
37 | 38 | ) 39 | } 40 | 41 | export default AnswersTab -------------------------------------------------------------------------------- /components/shared/EditDeleteAction.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { deleteAnswer } from "@/lib/actions/answer.action"; 4 | import { deleteQuestion } from "@/lib/actions/question.action"; 5 | import Image from "next/image"; 6 | import { usePathname, useRouter } from "next/navigation"; 7 | 8 | interface Props { 9 | type: string; 10 | itemId: string; 11 | } 12 | 13 | const EditDeleteAction = ({ type, itemId }: Props) => { 14 | const pathname = usePathname(); 15 | const router = useRouter(); 16 | 17 | const handleEdit = () => { 18 | router.push(`/question/edit/${JSON.parse(itemId)}`) 19 | }; 20 | 21 | const handleDelete = async () => { 22 | if(type === 'Question') { 23 | // Delete question 24 | await deleteQuestion({ 25 | questionId: JSON.parse(itemId), 26 | path: pathname 27 | }) 28 | } else if(type === 'Answer') { 29 | // Delete answer 30 | await deleteAnswer({ 31 | answerId: JSON.parse(itemId), 32 | path: pathname 33 | }) 34 | } 35 | }; 36 | 37 | return ( 38 |
39 | {type === 'Question' && ( 40 | Edit 48 | )} 49 | 50 | Delete 58 |
59 | ) 60 | } 61 | 62 | export default EditDeleteAction -------------------------------------------------------------------------------- /components/shared/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: { 16 | name: string, 17 | value: string, 18 | }[]; 19 | otherClasses?: string; 20 | containerClasses?: string; 21 | } 22 | 23 | const Filter = ({ filters, otherClasses, containerClasses }: Props) => { 24 | const searchParams = useSearchParams(); 25 | const router = useRouter(); 26 | 27 | const paramFilter = searchParams.get('filter'); 28 | 29 | const handleUpdateParams = (value: string) => { 30 | const newUrl = formUrlQuery({ 31 | params: searchParams.toString(), 32 | key: 'filter', 33 | value 34 | }) 35 | 36 | router.push(newUrl, { scroll: false }) 37 | } 38 | 39 | return ( 40 |
41 | 60 |
61 | ) 62 | } 63 | 64 | export default Filter -------------------------------------------------------------------------------- /components/shared/LeftSidebar.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { sidebarLinks } from '@/constants'; 4 | import Image from 'next/image'; 5 | import Link from 'next/link'; 6 | import { usePathname } from 'next/navigation'; 7 | import { Button } from '../ui/button'; 8 | import { SignedOut, useAuth } from '@clerk/nextjs'; 9 | 10 | const LeftSidebar = () => { 11 | const { userId } = useAuth(); 12 | const pathname = usePathname(); 13 | 14 | return ( 15 |
16 |
17 | {sidebarLinks.map((item) => { 18 | const isActive = (pathname.includes(item.route) && item.route.length > 1) || pathname === item.route; 19 | 20 | if(item.route === '/profile') { 21 | if(userId) { 22 | item.route = `${item.route}/${userId}` 23 | } else { 24 | return null; 25 | } 26 | } 27 | 28 | return ( 29 | 38 | {item.label} 45 |

{item.label}

46 | 47 | ) 48 | })} 49 |
50 | 51 | 52 |
53 | 54 | 64 | 65 | 66 | 67 | 68 | 78 | 79 |
80 |
81 |
82 | ) 83 | } 84 | 85 | export default LeftSidebar -------------------------------------------------------------------------------- /components/shared/Metric.tsx: -------------------------------------------------------------------------------- 1 | import Image from 'next/image'; 2 | import Link from 'next/link'; 3 | import React from 'react' 4 | 5 | interface MetricProps { 6 | imgUrl: string; 7 | alt: string; 8 | value: string | number; 9 | title: string; 10 | href?: string; 11 | textStyles?: string; 12 | isAuthor?: boolean; 13 | } 14 | 15 | const Metric = ({ 16 | imgUrl, 17 | alt, 18 | value, 19 | title, 20 | href, 21 | textStyles, 22 | isAuthor, 23 | }: MetricProps) => { 24 | const metricContent = ( 25 | <> 26 | {alt} 33 | 34 |

35 | {value} 36 | 37 | 38 | {title} 39 | 40 |

41 | 42 | ) 43 | 44 | if(href) { 45 | return ( 46 | 47 | {metricContent} 48 | 49 | ) 50 | } 51 | 52 | return ( 53 |
54 | {metricContent} 55 |
56 | ) 57 | } 58 | 59 | export default Metric -------------------------------------------------------------------------------- /components/shared/NoResult.tsx: -------------------------------------------------------------------------------- 1 | import Image from 'next/image' 2 | import Link from 'next/link' 3 | import React from 'react' 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 | 32 |

{title}

33 |

{description}

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

{pageNumber} 43 |

44 |
45 | 52 |
53 | ) 54 | } 55 | 56 | export default Pagination -------------------------------------------------------------------------------- /components/shared/ParseHTML.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import React, { 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 |
42 | {parse(data)} 43 |
44 | ) 45 | } 46 | 47 | export default ParseHTML -------------------------------------------------------------------------------- /components/shared/ProfileLink.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | import Link from "next/link"; 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 | 15 | {href ? ( 16 | 17 | {title} 18 | 19 | ) : ( 20 |

21 | {title} 22 |

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

{totalQuestions}

19 | )} 20 | 21 | ) 22 | } 23 | 24 | export default RenderTag -------------------------------------------------------------------------------- /components/shared/RightSidebar.tsx: -------------------------------------------------------------------------------- 1 | import Image from 'next/image'; 2 | import Link from 'next/link'; 3 | import React from 'react' 4 | import RenderTag from './RenderTag'; 5 | import { getHotQuestions } from '@/lib/actions/question.action'; 6 | import { getTopPopularTags } from '@/lib/actions/tag.actions'; 7 | 8 | const RightSidebar = async () => { 9 | const hotQuestions = await getHotQuestions(); 10 | const popularTags = await getTopPopularTags(); 11 | 12 | return ( 13 |
14 |
15 |

Top Questions

16 |
17 | {hotQuestions.map((question) => ( 18 | 23 |

{question.title}

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

Popular Tags

37 |
38 | {popularTags.map((tag) => ( 39 | 46 | ))} 47 |
48 |
49 |
50 | ) 51 | } 52 | 53 | export default RightSidebar -------------------------------------------------------------------------------- /components/shared/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 | 11 | const StatsCard = ({ imgUrl, value, title }: StatsCardProps) => { 12 | return ( 13 |
14 | {title} 15 |
16 |

17 | {value} 18 |

19 |

{title}

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

Stats - {reputation}

36 | 37 |
38 |
39 |
40 |

41 | {formatAndDivideNumber(totalQuestions)} 42 |

43 |

Questions

44 |
45 |
46 |

47 | {formatAndDivideNumber(totalAnswers)} 48 |

49 |

Answers

50 |
51 |
52 | 53 | 58 | 59 | 64 | 65 | 70 |
71 |
72 | ) 73 | } 74 | 75 | export default Stats -------------------------------------------------------------------------------- /components/shared/navbar/MobileNav.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { 4 | Sheet, 5 | SheetContent, 6 | SheetClose, 7 | SheetTrigger, 8 | } from "@/components/ui/sheet" 9 | import Image from 'next/image' 10 | import Link from 'next/link' 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 | 16 | const NavContent = () => { 17 | const pathname = usePathname(); 18 | 19 | return ( 20 |
21 | {sidebarLinks.map((item) => { 22 | const isActive = (pathname.includes(item.route) && item.route.length > 1) || pathname === item.route; 23 | 24 | // TODO 25 | 26 | return ( 27 | 28 | 35 | {item.label} 42 |

{item.label}

43 | 44 |
45 | ) 46 | })} 47 |
48 | ) 49 | } 50 | 51 | const MobileNav = () => { 52 | return ( 53 | 54 | 55 | Menu 62 | 63 | 64 | 65 | DevFlow 71 | 72 |

Dev Overflow

73 | 74 |
75 | 76 | 77 | 78 | 79 | 80 |
81 | 82 | 83 | 86 | 87 | 88 | 89 | 90 | 91 | 94 | 95 | 96 |
97 |
98 |
99 |
100 |
101 | ) 102 | } 103 | 104 | export default MobileNav -------------------------------------------------------------------------------- /components/shared/navbar/Navbar.tsx: -------------------------------------------------------------------------------- 1 | import { SignedIn, UserButton } from '@clerk/nextjs' 2 | import Image from 'next/image' 3 | import Link from 'next/link' 4 | import React from 'react' 5 | import Theme from './Theme' 6 | import MobileNav from './MobileNav' 7 | import GlobalSearch from '../search/GlobalSearch' 8 | 9 | const Navbar = () => { 10 | return ( 11 | 45 | ) 46 | } 47 | 48 | export default Navbar -------------------------------------------------------------------------------- /components/shared/navbar/Theme.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import React from 'react' 4 | 5 | import { useTheme } from '@/context/ThemeProvider'; 6 | 7 | import { 8 | Menubar, 9 | MenubarContent, 10 | MenubarItem, 11 | MenubarMenu, 12 | MenubarTrigger, 13 | } from "@/components/ui/menubar" 14 | import Image from 'next/image'; 15 | import { themes } from '@/constants'; 16 | 17 | const Theme = () => { 18 | const { mode, setMode } = useTheme(); 19 | 20 | return ( 21 | 22 | 23 | 24 | {mode === 'light' ? ( 25 | sun 30 | ): ( 31 | moon 36 | )} 37 | 38 | 39 | {themes.map((item) => ( 40 | // @ts-ignore 41 | { 45 | setMode(item.value); 46 | if (item.value !== "system") { 47 | localStorage.theme = item.value; 48 | } else { 49 | localStorage.removeItem("theme"); 50 | } 51 | }} 52 | > 53 | {item.value} 60 |

{item.label}

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

Type:

43 |
44 | {GlobalSearchFilters.map((item) => ( 45 | 58 | ))} 59 |
60 |
61 | ) 62 | } 63 | 64 | export default GlobalFilters -------------------------------------------------------------------------------- /components/shared/search/GlobalResult.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import React, { 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 GlobalResult = () => { 12 | const searchParams = useSearchParams(); 13 | 14 | const [isLoading, setIsLoading] = useState(false); 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 | useEffect(() => { 25 | const fetchResult = async () => { 26 | setResult([]); 27 | setIsLoading(true); 28 | 29 | try { 30 | const res = await globalSearch({ query: global, type }) 31 | 32 | setResult(JSON.parse(res)); 33 | } catch (error) { 34 | console.error(error); 35 | throw error; 36 | } finally { 37 | setIsLoading(false); 38 | } 39 | } 40 | 41 | if(global) { 42 | fetchResult(); 43 | } 44 | }, [global, type]) 45 | 46 | const renderLink = (type: string, id: string) => { 47 | switch (type) { 48 | case 'question': 49 | return `/question/${id}`; 50 | case 'answer': 51 | return `/question/${id}`; 52 | case 'user': 53 | return `/profile/${id}`; 54 | case 'tag': 55 | return `/tags/${id}`; 56 | default: 57 | return '/' 58 | } 59 | } 60 | 61 | 62 | return ( 63 |
64 | 65 |
66 | 67 |
68 |

69 | Top Match 70 |

71 | 72 | {isLoading ? ( 73 |
74 | 75 |

Browsing the entire database

76 |
77 | ): ( 78 |
79 | {result.length > 0 ? ( 80 | result.map((item: any, index: number) => ( 81 | 86 | tags 93 | 94 |
95 |

{item.title}

96 |

{item.type}

97 |
98 | 99 | )) 100 | ) : ( 101 |
102 |

Oops, no results found

103 |
104 | )} 105 |
106 | )} 107 |
108 |
109 | ) 110 | } 111 | 112 | export default GlobalResult -------------------------------------------------------------------------------- /components/shared/search/GlobalSearch.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import Image from 'next/image' 4 | import React, { useEffect, useRef, useState } from 'react' 5 | import { Input } from "@/components/ui/input" 6 | import { usePathname, useRouter, useSearchParams } from 'next/navigation' 7 | import { formUrlQuery, removeKeysFromQuery } from '@/lib/utils' 8 | import GlobalResult from './GlobalResult' 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('q'); 17 | 18 | const [search, setSearch] = useState(query || ''); 19 | const [isOpen, setIsOpen] = useState(false); 20 | 21 | useEffect(() => { 22 | const handleOutsideClick = (event: any) => { 23 | if(searchContainerRef.current && 24 | // @ts-ignore 25 | !searchContainerRef.current.contains(event.target) 26 | ) { 27 | setIsOpen(false); 28 | setSearch('') 29 | } 30 | } 31 | 32 | setIsOpen(false); 33 | 34 | document.addEventListener("click", handleOutsideClick); 35 | 36 | return () => { 37 | document.removeEventListener("click", handleOutsideClick) 38 | } 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 | 50 | router.push(newUrl, { scroll: false }); 51 | } else { 52 | if(query) { 53 | const newUrl = removeKeysFromQuery({ 54 | params: searchParams.toString(), 55 | keysToRemove: ['global', 'type'] 56 | }) 57 | 58 | router.push(newUrl, { scroll: false }); 59 | } 60 | 61 | } 62 | }, 300); 63 | 64 | return () => clearTimeout(delayDebounceFn) 65 | }, [search, router, pathname, searchParams, query]) 66 | 67 | return ( 68 |
69 |
70 | search 77 | 78 | { 83 | setSearch(e.target.value); 84 | 85 | if(!isOpen) setIsOpen(true); 86 | if(e.target.value === '' && isOpen) setIsOpen(false); 87 | }} 88 | className="paragraph-regular no-focus placeholder text-dark400_light700 border-none bg-transparent shadow-none outline-none" 89 | /> 90 |
91 | {isOpen && } 92 |
93 | ) 94 | } 95 | 96 | export default GlobalSearch -------------------------------------------------------------------------------- /components/shared/search/LocalSearchbar.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { Input } from '@/components/ui/input' 4 | import Image from 'next/image' 5 | import React, { useEffect, useState } from 'react' 6 | import { usePathname, useRouter, useSearchParams } from 'next/navigation' 7 | import { formUrlQuery, removeKeysFromQuery } from '@/lib/utils' 8 | 9 | interface CustomInputProps { 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 | }: CustomInputProps) => { 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 | 41 | router.push(newUrl, { scroll: false }); 42 | } else { 43 | console.log(route, pathname) 44 | if(pathname === route) { 45 | const newUrl = removeKeysFromQuery({ 46 | params: searchParams.toString(), 47 | keysToRemove: ['q'] 48 | }) 49 | 50 | router.push(newUrl, { scroll: false }); 51 | } 52 | 53 | } 54 | }, 300); 55 | 56 | return () => clearTimeout(delayDebounceFn) 57 | }, [search, route, pathname, router, searchParams, query]) 58 | 59 | return ( 60 |
61 | {iconPosition === 'left' && ( 62 | search icon 69 | )} 70 | 71 | setSearch(e.target.value)} 76 | className="paragraph-regular no-focus placeholder text-dark400_light700 border-none bg-transparent shadow-none outline-none" 77 | /> 78 | 79 | {iconPosition === 'right' && ( 80 | search icon 87 | )} 88 |
89 | ) 90 | } 91 | 92 | export default LocalSearchbar -------------------------------------------------------------------------------- /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 | import { cn } from "@/lib/utils" 2 | import React from "react" 3 | 4 | function Skeleton({ 5 | className, 6 | ...props 7 | }: React.HTMLAttributes) { 8 | return ( 9 |
13 | ) 14 | } 15 | 16 | export { Skeleton } 17 | -------------------------------------------------------------------------------- /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 |