├── .env.setupApiKeys ├── .eslintrc.json ├── .gitignore ├── .prettierrc.json ├── CONTRIBUTING.md ├── LICENSE ├── 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.ts ├── constants ├── filter.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.action.ts │ └── user.action.ts ├── mongoose.ts ├── utils.ts └── validations.ts ├── middleware.ts ├── next.config.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── public ├── assets │ ├── icons │ │ ├── account.svg │ │ ├── arrow-left.svg │ │ ├── arrow-right.svg │ │ ├── arrow-up-right.svg │ │ ├── au.svg │ │ ├── avatar.svg │ │ ├── bronze-medal.svg │ │ ├── calendar.svg │ │ ├── carbon-location.svg │ │ ├── chevron-down.svg │ │ ├── chevron-right.svg │ │ ├── clock-2.svg │ │ ├── clock.svg │ │ ├── close.svg │ │ ├── computer.svg │ │ ├── currency-dollar-circle.svg │ │ ├── downvote.svg │ │ ├── downvoted.svg │ │ ├── edit.svg │ │ ├── eye.svg │ │ ├── gold-medal.svg │ │ ├── hamburger.svg │ │ ├── home.svg │ │ ├── job-search.svg │ │ ├── like.svg │ │ ├── link.svg │ │ ├── location.svg │ │ ├── message.svg │ │ ├── mingcute-down-line.svg │ │ ├── moon.svg │ │ ├── question.svg │ │ ├── search.svg │ │ ├── sign-up.svg │ │ ├── silver-medal.svg │ │ ├── star-filled.svg │ │ ├── star-red.svg │ │ ├── star.svg │ │ ├── stars.svg │ │ ├── suitcase.svg │ │ ├── sun.svg │ │ ├── tag.svg │ │ ├── trash.svg │ │ ├── upvote.svg │ │ ├── upvoted.svg │ │ ├── user.svg │ │ └── users.svg │ └── images │ │ ├── auth-dark.png │ │ ├── auth-light.png │ │ ├── dark-illustration.png │ │ ├── default-logo.svg │ │ ├── favicon.ico │ │ ├── light-illustration.png │ │ ├── pinoy-overflow.png │ │ └── po-logo.png ├── next.svg └── vercel.svg ├── styles ├── prism.css └── theme.css ├── tailwind.config.ts ├── tsconfig.json └── types └── index.d.ts /.env.setupApiKeys: -------------------------------------------------------------------------------- 1 | # INSTRUCTIONS: 2 | # 1. Rename this file it to .env.local 3 | # 2. Replace the values with your own 4 | # 3. Make sure to add .env.local to your .gitignore file 5 | # 4. Restart your server 6 | 7 | NEXT_PUBLIC_SERVER_URL=http://localhost:3000 8 | 9 | NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in 10 | NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up 11 | NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/ 12 | NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/ 13 | 14 | # CLERK 15 | # INSTRUCTIONS: 16 | # 1. Create a Clerk account at https://clerk.com 17 | # 2. Create a new Clerk application name Pinoy Overflow 18 | # 3. Copy the values from the "Client API Keys" section below 19 | # 4. Replace the values below with your own 20 | 21 | NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=CLERK_PUBLISHABLE_KEY 22 | CLERK_SECRET_KEY=CLERK_SECRET_KEY 23 | 24 | # (OPTION 1) Using localtunnel 25 | # 5. Run: npx localtunnel --port 3000 26 | # 6. Copy the generated url 27 | # 7. Add Endpoint -> ${your-url}/api/webhook 28 | 29 | # (OPTION 2 | PREFERRED) Deploying to vercel 30 | # 5. Deploy your app to Vercel 31 | # 6. Get the DOMAIN_NAME of your app 32 | # 7. Add Endpoint -> https://.vercel.app/api/webhook 33 | 34 | # 8. Go to dashboard.clerk.com 35 | # 9. Click on "Webhooks" in the left sidebar 36 | # 10. Copy the value from the "Signing Secret" section below 37 | # 11. Replace the value below with your own 38 | NEXT_CLERK_WEBHOOK_SECRET=SIGNING_SECRET 39 | 40 | # Go to https://www.tiny.cloud and sign up and copy API Key 41 | NEXT_PUBLIC_TINY_EDITOR_API_KEY=TINY_EDITOR_API_KEY 42 | 43 | # MongoDB 44 | # INSRTUCTIONS: 45 | # 1. Create a MongoDB account at https://cloud.mongodb.com 46 | # 2 Go to Network Access and add your IP Address and add check if there's 0.0.0.0/0 47 | # 3. Go to Database Access and create a new user with username and password 48 | # 3. Create a new project name pinoyoverflow 49 | # 4. Create a deployment and choose Free Tier 50 | # 5. Add you current IP address 51 | # 6. Go to Database and click Connect -> Drivers -> Copy the #3 step and change the password 52 | MONGODB_URL=MONGODB_URL 53 | 54 | # Go to https://openai.com/blog/openai-api/ and sign up 55 | # Go to https://platform.openai.com/api-keys create new secret key copy the values and replace below 56 | OPENAI_API_KEY=SECRET_KEY 57 | # If it's not working, your Credit Grants might expired. Try to create a new account. -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "next/core-web-vitals", 4 | "standard", 5 | "plugin:tailwindcss/recommended", 6 | "prettier"] 7 | } 8 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": false 6 | } 7 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## To contribute, here are some steps you can follow: 2 | 3 | ### 1. Fork the Repository: 4 | 5 | Start by forking the repository on GitHub. This will create a copy of the project under your GitHub account. 6 | 7 | ### 2. Clone your Fork: 8 | 9 | Clone the repository to your local machine using the following command (replace [username] with your GitHub username): 10 | 11 | ``` 12 | git clone https://github.com/[username]/pinoy-overflow.git 13 | ``` 14 | 15 | ### 3. Setup your env: 16 | 17 | rename .env.setupApiKeys => .env.local from your root directory and supply the necessary values 18 | 19 | ### 4. Create a New Branch: 20 | 21 | Create a new branch for your documentation changes. This helps keep your changes isolated from the main codebase. 22 | 23 | ``` 24 | git checkout -b doc-contribution 25 | ``` 26 | 27 | ### 5. Make Changes: 28 | 29 | Edit or create the documentation files in the project. This might involve adding new Markdown files or updating existing ones. Ensure that your documentation is clear, concise. 30 | 31 | ### 6. Test Locally: 32 | 33 | If your documentation includes code samples or instructions, test them locally to ensure accuracy. Make sure that any commands or examples provided work as expected. 34 | 35 | ### 7. Commit your Changes: 36 | 37 | Once you're satisfied with your changes, commit them using Git. 38 | 39 | ``` 40 | git add . 41 | git commit -m "Add documentation for feature X" 42 | ``` 43 | 44 | ### 8. Push Changes: 45 | 46 | Push your changes to your forked repository on GitHub. 47 | 48 | ``` 49 | git push origin doc-contribution 50 | ``` 51 | 52 | ### 9. Create a Pull Request: 53 | 54 | Visit your fork on GitHub and create a pull request. This will notify the project maintainers that you have changes you'd like them to review. 55 | 56 | ### 10. Describe your Changes: 57 | 58 | When creating the pull request, provide a clear and detailed description of your changes. Explain the purpose of the documentation, the context, and any relevant information. 59 | 60 | ### 11. Respond to Feedback: 61 | 62 | Be responsive to any feedback or questions from the maintainers. They may suggest changes or ask for clarification. 63 | 64 | ### 12. Update your Branch: 65 | 66 | If necessary, make any additional changes based on the feedback and push them to your branch. The pull request will automatically update. 67 | 68 | ### 13. Merge: 69 | 70 | Once your changes have been reviewed and approved, they will be merged into the main repository. Congratulations on your contribution! :) 71 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Pinoy Overflow 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pinoy Overflow 2 | 3 | ### Introducing the Pinoy Overflow! 4 | 5 | It's not just a website; it's a community-driven platform designed to empower newcomers to the world of web development. Pinoy Overflow serves as a "Stack Overflow" for those seeking answers and guidance in their web development journey. 6 | 7 | Discord Channel: https://discord.gg/mmmqtKfh 8 | 9 | ![thumb](https://github.com/jerommiole/pinoy-overflow/assets/8807678/6102e7de-e72f-4fd3-8b0f-157289a6ae9d) 10 | 11 | ## Key Features: 12 | 13 | - React and TypeScript: These technologies ensure a smooth and interactive user experience, making it easy to browse and ask questions. 14 | - Next.js: With Next.js, the site is not only fast but also optimized for search engines, helping users find answers to their web development queries. 15 | - Tailwind CSS: The clean and modern design, thanks to Tailwind CSS, enhances the overall usability of Pinoy Overflow. 16 | - MongoDB: MongoDB helps manage user data and content effectively, making it a valuable resource for web development queries. 17 | 18 | ![1](https://github.com/jerommiole/pinoy-overflow/assets/8807678/de51fc8c-91a9-4eb4-bd20-d581a867cb42) 19 | 20 | ## Dark Mode Theme 21 | 22 | ![dark](https://github.com/jerommiole/pinoy-overflow/assets/8807678/36abfe0c-da86-4e97-9863-c62ddf29f613) 23 | 24 | Pinoy Overflow is more than just a website; it's a vibrant community of web enthusiasts helping each other grow. Whether you're a newbie or an experienced developer, you'll find a welcoming space to ask questions, share knowledge, and collaborate. 25 | 26 | Join me in embracing the power of community-driven learning and development. Start your journey with Pinoy Overflow today! 27 | 28 | ## Contributing 29 | 30 | Please refer to the [CONTRIBUTING.md](CONTRIBUTING.md) file for steps. 31 | -------------------------------------------------------------------------------- /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 "next/link"; 4 | import React from "react"; 5 | 6 | const Loading = () => { 7 | return ( 8 |
9 |
10 |

All Questions

11 | 12 | 15 | 16 |
17 | 18 |
19 | 20 |
21 | 22 |
23 |
24 | 25 |
26 | {[1, 2, 3, 4].map((item) => ( 27 | 28 | ))} 29 |
30 | 31 |
32 | {[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((item) => ( 33 | 34 | ))} 35 |
36 |
37 | ); 38 | }; 39 | 40 | export default Loading; 41 | -------------------------------------------------------------------------------- /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/filter"; 9 | import { 10 | getQuestions, 11 | getRecommendedQuestions, 12 | } from "@/lib/actions/question.action"; 13 | import { SearchParamsProps } from "@/types"; 14 | import { auth } from "@clerk/nextjs"; 15 | import { Metadata } from "next"; 16 | import Link from "next/link"; 17 | 18 | export const metadata: Metadata = { 19 | title: "Home | Pinoy Overflow", 20 | description: 21 | "A community-driven platform for asking and answering programming questions. Get help, share knowlede, and collaborate with developers from around the world. Explore topics in web development, mobile app development, algorithms, data structures, and more.", 22 | icons: { 23 | icon: "/assets/images/favicon.ico", 24 | }, 25 | }; 26 | 27 | export default async function Home({ searchParams }: SearchParamsProps) { 28 | const { userId } = auth(); 29 | let result; 30 | 31 | if (searchParams?.filter === "recommended") { 32 | if (userId) { 33 | result = await getRecommendedQuestions({ 34 | userId, 35 | searchQuery: searchParams.q, 36 | page: searchParams.page ? +searchParams.page : 1, 37 | }); 38 | } else { 39 | result = { 40 | questions: [], 41 | isNext: false, 42 | }; 43 | } 44 | } else { 45 | result = await getQuestions({ 46 | searchQuery: searchParams.q, 47 | filter: searchParams.filter, 48 | page: searchParams.page ? +searchParams.page : 1, 49 | }); 50 | } 51 | 52 | return ( 53 | <> 54 |
55 |

All Questions

56 | 57 | 58 | 61 | 62 |
63 | 64 |
65 | 72 | 73 | 78 |
79 | 80 | 81 | 82 |
83 | {result.questions.length > 0 ? ( 84 | result.questions.map((question: any) => ( 85 | 96 | )) 97 | ) : ( 98 | 104 | )} 105 |
106 | 107 |
108 | 112 |
113 | 114 | ); 115 | } 116 | -------------------------------------------------------------------------------- /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 { Metadata } from "next"; 5 | import { redirect } from "next/navigation"; 6 | import React from "react"; 7 | 8 | export const metadata: Metadata = { 9 | title: "Ask a Question | Pinoy Overflow", 10 | }; 11 | 12 | const Page = async () => { 13 | const { userId } = auth(); 14 | 15 | if (!userId) redirect("/sign-in"); 16 | 17 | const mongoUser = await getUserById({ userId }); 18 | 19 | console.log(mongoUser); 20 | 21 | return ( 22 |
23 |

Ask a question

24 | 25 |
26 | 27 |
28 |
29 | ); 30 | }; 31 | 32 | export default Page; 33 | -------------------------------------------------------------------------------- /app/(root)/collection/loading.tsx: -------------------------------------------------------------------------------- 1 | import { Skeleton } from "@/components/ui/skeleton"; 2 | import React from "react"; 3 | 4 | const Loading = () => { 5 | return ( 6 |
7 |

Saved Questions

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; 24 | -------------------------------------------------------------------------------- /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/filter"; 7 | import { getSavedQuestions } from "@/lib/actions/user.action"; 8 | import { SearchParamsProps } from "@/types"; 9 | import { auth } from "@clerk/nextjs"; 10 | import { Metadata } from "next"; 11 | 12 | export const metadata: Metadata = { 13 | title: "Collections | Pinoy Overflow", 14 | }; 15 | 16 | export default async function Home({ searchParams }: SearchParamsProps) { 17 | const { userId } = auth(); 18 | 19 | if (!userId) return null; 20 | 21 | const result = await getSavedQuestions({ 22 | clerkId: userId, 23 | searchQuery: searchParams.q, 24 | filter: searchParams.filter, 25 | page: searchParams.page ? +searchParams.page : 1, 26 | }); 27 | 28 | return ( 29 | <> 30 |

Saved Questions

31 | 32 |
33 | 40 | 41 | 45 |
46 | 47 |
48 | {result.questions.length > 0 ? ( 49 | result.questions.map((question: any) => ( 50 | 61 | )) 62 | ) : ( 63 | 69 | )} 70 |
71 | 72 |
73 | 77 |
78 | 79 | ); 80 | } 81 | -------------------------------------------------------------------------------- /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 | {[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12].map((item) => ( 15 | 19 | ))} 20 |
21 |
22 | ); 23 | }; 24 | 25 | export default Loading; 26 | -------------------------------------------------------------------------------- /app/(root)/community/page.tsx: -------------------------------------------------------------------------------- 1 | import LocalSearchbar from "@/components/shared/search/LocalSearchbar"; 2 | import { UserFilters } from "@/constants/filter"; 3 | import Filter from "@/components/shared/Filter"; 4 | import { getAllUsers } from "@/lib/actions/user.action"; 5 | import Link from "next/link"; 6 | import UserCard from "@/components/cards/UserCard"; 7 | import { SearchParamsProps } from "@/types"; 8 | import Pagination from "@/components/shared/Pagination"; 9 | import { Metadata } from "next"; 10 | 11 | export const metadata: Metadata = { 12 | title: "Community | Pinoy 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 |

No users found

47 | 48 | Join to be the first! 49 | 50 |
51 | )} 52 |
53 | 54 |
55 | 59 |
60 | 61 | ); 62 | }; 63 | 64 | export default Page; 65 | -------------------------------------------------------------------------------- /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 |
{children}
15 |
16 | 17 |
18 | 19 | 20 |
21 | ); 22 | }; 23 | 24 | export default Layout; 25 | -------------------------------------------------------------------------------- /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 | import { getUserById } from "@/lib/actions/user.action"; 3 | import { ParamsProps } from "@/types"; 4 | import { auth } from "@clerk/nextjs"; 5 | import { Metadata } from "next"; 6 | 7 | export const metadata: Metadata = { 8 | title: "Edit Profile | Pinoy Overflow", 9 | }; 10 | 11 | const Page = async ({ params }: ParamsProps) => { 12 | const { userId } = auth(); 13 | 14 | if (!userId) return null; 15 | 16 | const mongoUser = await getUserById({ userId }); 17 | return ( 18 | <> 19 |

Edit Profile

20 | 21 |
22 | 23 |
24 | 25 | ); 26 | }; 27 | 28 | export default Page; 29 | -------------------------------------------------------------------------------- /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; 31 | -------------------------------------------------------------------------------- /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.action"; 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 | ) : ( 45 | 51 | )} 52 |
53 | 54 |
55 | 59 |
60 | 61 | ); 62 | }; 63 | 64 | export default Page; 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 LocalSearchbar from "@/components/shared/search/LocalSearchbar"; 2 | import { TagFilters } from "@/constants/filter"; 3 | import Filter from "@/components/shared/Filter"; 4 | import { getAllTags } from "@/lib/actions/tag.action"; 5 | import Link from "next/link"; 6 | import NoResult from "@/components/shared/NoResult"; 7 | import { SearchParamsProps } from "@/types"; 8 | import Pagination from "@/components/shared/Pagination"; 9 | import { Metadata } from "next"; 10 | 11 | export const metadata: Metadata = { 12 | title: "Tags | Pinoy Overflow", 13 | }; 14 | 15 | const Page = async ({ searchParams }: SearchParamsProps) => { 16 | const result = await getAllTags({ 17 | searchQuery: searchParams.q, 18 | filter: searchParams.filter, 19 | page: searchParams.page ? +searchParams.page : 1, 20 | }); 21 | 22 | return ( 23 | <> 24 |

All Tags

25 | 26 |
27 | 34 | 35 | 39 |
40 | 41 |
42 | {result.tags.length > 0 ? ( 43 | result.tags.map((tag: any) => ( 44 | 49 |
50 |
51 |

52 | {tag.name} 53 |

54 |
55 | 56 |

57 | 58 | {tag.questions.length}+ 59 | {" "} 60 | Questions 61 |

62 |
63 | 64 | )) 65 | ) : ( 66 | 72 | )} 73 |
74 | 75 |
76 | 80 |
81 | 82 | ); 83 | }; 84 | 85 | export default Page; 86 | -------------------------------------------------------------------------------- /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 | console.log(process.env.OPENAI_API_KEY); 6 | 7 | try { 8 | const response = await fetch("https://api.openai.com/v1/chat/completions", { 9 | method: "POST", 10 | headers: { 11 | "Content-Type": "application/json", 12 | Authorization: `Bearer ${process.env.OPENAI_API_KEY}`, 13 | }, 14 | body: JSON.stringify({ 15 | model: "gpt-3.5-turbo", 16 | messages: [ 17 | { 18 | role: "system", 19 | content: 20 | "You are a knowlegeable assistant that provides quality information.", 21 | }, 22 | { 23 | role: "user", 24 | content: `Tell me ${question}`, 25 | }, 26 | ], 27 | }), 28 | }); 29 | 30 | const responseData = await response.json(); 31 | console.log(responseData); 32 | const reply = responseData.choices[0].message.content; 33 | 34 | return NextResponse.json({ reply }); 35 | } catch (error: any) { 36 | return NextResponse.json({ error: error.message }); 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /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 | const WEBHOOK_SECRET = process.env.NEXT_CLERK_WEBHOOK_SECRET; 11 | 12 | if (!WEBHOOK_SECRET) { 13 | throw new Error( 14 | "Please add WEBHOOK_SECRET from Clerk Dashboard to .env or .env.local" 15 | ); 16 | } 17 | 18 | // Get the headers 19 | const headerPayload = headers(); 20 | const svix_id = headerPayload.get("svix-id"); 21 | const svix_timestamp = headerPayload.get("svix-timestamp"); 22 | const svix_signature = headerPayload.get("svix-signature"); 23 | 24 | // If there are no headers, error out 25 | if (!svix_id || !svix_timestamp || !svix_signature) { 26 | return new Response("Error occured -- no svix headers", { 27 | status: 400, 28 | }); 29 | } 30 | 31 | // Get the body 32 | const payload = await req.json(); 33 | const body = JSON.stringify(payload); 34 | 35 | // Create a new SVIX instance with your secret. 36 | const wh = new Webhook(WEBHOOK_SECRET); 37 | 38 | let evt: WebhookEvent; 39 | 40 | // Verify the payload with the headers 41 | try { 42 | evt = wh.verify(body, { 43 | "svix-id": svix_id, 44 | "svix-timestamp": svix_timestamp, 45 | "svix-signature": svix_signature, 46 | }) as WebhookEvent; 47 | } catch (err) { 48 | console.error("Error verifying webhook:", err); 49 | return new Response("Error occured", { 50 | status: 400, 51 | }); 52 | } 53 | 54 | const eventType = evt.type; 55 | 56 | if (eventType === "user.created") { 57 | const { id, email_addresses, image_url, username, first_name, last_name } = 58 | evt.data; 59 | 60 | // Create a new user in your database 61 | const mongoUser = await createUser({ 62 | clerkId: id, 63 | name: `${first_name}${last_name ? ` ${last_name}` : ""}`, 64 | username: username || id, 65 | email: email_addresses[0].email_address, 66 | picture: image_url, 67 | }); 68 | 69 | return NextResponse.json({ message: "OK", user: mongoUser }); 70 | } 71 | 72 | if (eventType === "user.updated") { 73 | const { id, email_addresses, image_url, username, first_name, last_name } = 74 | evt.data; 75 | 76 | // Create a new user in your database 77 | const mongoUser = await updateUser({ 78 | clerkId: id, 79 | updateData: { 80 | name: `${first_name}${last_name ? ` ${last_name}` : ""}`, 81 | username: username!, 82 | email: email_addresses[0].email_address, 83 | picture: image_url, 84 | }, 85 | path: `/profile/${id}`, 86 | }); 87 | 88 | return NextResponse.json({ message: "OK", user: mongoUser }); 89 | } 90 | 91 | if (eventType === "user.deleted") { 92 | const { id } = evt.data; 93 | 94 | const deletedUser = await deleteUser({ 95 | clerkId: id!, 96 | }); 97 | 98 | return NextResponse.json({ message: "OK", user: deletedUser }); 99 | } 100 | 101 | return NextResponse.json({ message: "OK" }); 102 | } 103 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jerommiole/pinoy-overflow/3b8cbb9c11a830c9c91de7a866b693dfa6312d58/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, #2986ff 0%, #6babff 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, #2986ff 0%, #6babff 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: #2986ff !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, #2986ff 0%, #6babff 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: "Pinoy Overflow", 25 | description: 26 | "A community-driven platform for asking and answering programming questions. Get help, share knowlede, and collaborate with developers from around the world. Explore topics in web development, mobile app development, algorithms, data structures, and more.", 27 | icons: { 28 | icon: "/assets/images/po-logo.png", 29 | }, 30 | }; 31 | 32 | export default function RootLayout({ 33 | children, 34 | }: { 35 | children: React.ReactNode; 36 | }) { 37 | return ( 38 | 39 | 40 | 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 | export default QuestionCard; 108 | -------------------------------------------------------------------------------- /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 React from "react"; 5 | import { Badge } from "../ui/badge"; 6 | import RenderTag from "../shared/RenderTag"; 7 | 8 | interface Props { 9 | user: { 10 | _id: string; 11 | clerkId: string; 12 | picture: string; 13 | name: string; 14 | username: string; 15 | }; 16 | } 17 | 18 | const UserCard = async ({ user }: Props) => { 19 | const interactedTags = await getTopInteractedTags({ userId: user._id }); 20 | 21 | return ( 22 | 26 |
27 | user profile picture 34 | 35 |
36 |

37 | {user.name} 38 |

39 |

40 | @{user.username} 41 |

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

{totalAnswers} Answers

54 | 55 |
56 | 57 |
58 | {result.answers.map((answer) => ( 59 |
63 |
64 | 68 | profile 75 |
76 |

77 | {answer.author.name}{" "} 78 |

79 |

80 | - answered{" "} 81 | {getTimestamp(answer.createdAt)} 82 |

83 |
84 | 85 |
86 | id.toString())} 91 | downvotes={answer.downvotes.map((id) => id.toString())} 92 | /> 93 |
94 |
95 | 96 | 97 |
98 | ))} 99 |
100 | 101 |
102 | 106 |
107 |
108 | ); 109 | }; 110 | 111 | export default AllAnswers; 112 | -------------------------------------------------------------------------------- /components/shared/AnswersTab.tsx: -------------------------------------------------------------------------------- 1 | import { getUserAnswers } from "@/lib/actions/user.action"; 2 | import React from "react"; 3 | import AnswerCard from "../cards/AnswerCard"; 4 | import Pagination from "./Pagination"; 5 | import { SearchParamsProps } from "@/types"; 6 | 7 | interface Props extends SearchParamsProps { 8 | userId: string; 9 | clerkId?: string | null; 10 | } 11 | 12 | const AnswersTab = async ({ userId, clerkId, searchParams }: Props) => { 13 | const result = await getUserAnswers({ 14 | userId, 15 | page: searchParams.page ? +searchParams.page : 1, 16 | }); 17 | 18 | console.log(result.answers); 19 | return ( 20 | <> 21 | {result.answers.map((item: any) => ( 22 | 31 | ))} 32 | 33 |
34 | 38 |
39 | 40 | ); 41 | }; 42 | 43 | export default AnswersTab; 44 | -------------------------------------------------------------------------------- /components/shared/EditDeleteAction.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { deleteAnswer } from "@/lib/actions/answer.action"; 3 | import { deleteQuestion } from "@/lib/actions/question.action"; 4 | import Image from "next/image"; 5 | import { usePathname, useRouter } from "next/navigation"; 6 | import React from "react"; 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 | await deleteQuestion({ questionId: JSON.parse(itemId), path: pathname }); 24 | } else if (type === "Answer") { 25 | await deleteAnswer({ answerId: JSON.parse(itemId), path: pathname }); 26 | } 27 | }; 28 | return ( 29 |
30 | {type === "Question" && ( 31 | Edit 39 | )} 40 | Delete 48 |
49 | ); 50 | }; 51 | 52 | export default EditDeleteAction; 53 | -------------------------------------------------------------------------------- /components/shared/Filter.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | 5 | import { 6 | Select, 7 | SelectContent, 8 | SelectGroup, 9 | SelectItem, 10 | SelectTrigger, 11 | SelectValue, 12 | } from "@/components/ui/select"; 13 | import { formUrlQuery } from "@/lib/utils"; 14 | import { useRouter } from "next/navigation"; 15 | 16 | interface Props { 17 | filters: { 18 | name: string; 19 | value: string; 20 | }[]; 21 | otherClasses?: string; 22 | containerClasses?: string; 23 | } 24 | 25 | const Filter = ({ filters, otherClasses, containerClasses }: Props) => { 26 | const searchParams = new URLSearchParams(); 27 | const router = useRouter(); 28 | 29 | const paramFilter = searchParams.get("filter"); 30 | 31 | const handleUpdateParams = (value: string) => { 32 | const newUrl = formUrlQuery({ 33 | params: searchParams.toString(), 34 | key: "filter", 35 | value, 36 | }); 37 | router.push(newUrl, { scroll: false }); 38 | }; 39 | 40 | return ( 41 |
42 | 67 |
68 | ); 69 | }; 70 | 71 | export default Filter; 72 | -------------------------------------------------------------------------------- /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 = 19 | (pathname.includes(item.route) && item.route.length > 1) || 20 | pathname === item.route; 21 | 22 | if (item.route === "/profile") { 23 | if (userId) { 24 | item.route = `${item.route}/${userId}`; 25 | } else { 26 | return null; 27 | } 28 | } 29 | 30 | return ( 31 | 40 | {item.label} 47 |

52 | {item.label} 53 |

54 | 55 | ); 56 | })} 57 |
58 | 59 | 60 |
61 | 62 | 74 | 75 | 76 | 77 | 87 | 88 |
89 |
90 |
91 | ); 92 | }; 93 | 94 | export default LeftSidebar; 95 | -------------------------------------------------------------------------------- /components/shared/Metric.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import Image from "next/image"; 3 | import React from "react"; 4 | 5 | interface MetricProps { 6 | imgUrl: string; 7 | alt: string; 8 | value: number | string; 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 | 42 | {title} 43 | 44 |

45 | 46 | ); 47 | 48 | if (href) { 49 | return ( 50 | 51 | {metricContent} 52 | 53 | ); 54 | } 55 | return
{metricContent}
; 56 | }; 57 | 58 | export default Metric; 59 | -------------------------------------------------------------------------------- /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 |

34 | {description} 35 |

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

{pageNumber}

41 |
42 | 49 |
50 | ); 51 | }; 52 | 53 | export default Pagination; 54 | -------------------------------------------------------------------------------- /components/shared/ParseHTML.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React, { useEffect } from "react"; 4 | import Prism from "prismjs"; 5 | import parse from "html-react-parser"; 6 | 7 | import "prismjs/components/prism-python"; 8 | import "prismjs/components/prism-java"; 9 | import "prismjs/components/prism-c"; 10 | import "prismjs/components/prism-cpp"; 11 | import "prismjs/components/prism-csharp"; 12 | import "prismjs/components/prism-aspnet"; 13 | import "prismjs/components/prism-sass"; 14 | import "prismjs/components/prism-jsx"; 15 | import "prismjs/components/prism-typescript"; 16 | import "prismjs/components/prism-solidity"; 17 | import "prismjs/components/prism-json"; 18 | import "prismjs/components/prism-dart"; 19 | import "prismjs/components/prism-ruby"; 20 | import "prismjs/components/prism-rust"; 21 | import "prismjs/components/prism-r"; 22 | import "prismjs/components/prism-kotlin"; 23 | import "prismjs/components/prism-go"; 24 | import "prismjs/components/prism-bash"; 25 | import "prismjs/components/prism-sql"; 26 | import "prismjs/components/prism-mongodb"; 27 | import "prismjs/plugins/line-numbers/prism-line-numbers.js"; 28 | import "prismjs/plugins/line-numbers/prism-line-numbers.css"; 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 | 42 | export default ParseHTML; 43 | -------------------------------------------------------------------------------- /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 | 21 | {title} 22 | 23 | ) : ( 24 |

{title}

25 | )} 26 |
27 | ); 28 | }; 29 | 30 | export default ProfileLink; 31 | -------------------------------------------------------------------------------- /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 ({ userId, clerkId, searchParams }: 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: any) => ( 21 | 33 | ))} 34 | 35 |
36 | 40 |
41 | 42 | ); 43 | }; 44 | 45 | export default QuestionTab; 46 | -------------------------------------------------------------------------------- /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 | 16 | {name} 17 | 18 | 19 | {showCount && ( 20 |

{totalQuestions}

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

Top Questions

17 |
18 | {hotQuestions.map((question: any) => ( 19 | 24 |

25 | {question.title} 26 |

27 | chevron-right 34 | 35 | ))} 36 |
37 |
38 |

Popular Tags

39 |
40 | {popularTags.map((tag: any) => ( 41 | 48 | ))} 49 |
50 |
51 |
52 | ); 53 | }; 54 | 55 | export default RightSidebar; 56 | -------------------------------------------------------------------------------- /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 |

{value}

17 |

{title}

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

34 | Stats - {reputation} 35 |

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 | 63 | 68 |
69 |
70 | ); 71 | }; 72 | 73 | export default Stats; 74 | -------------------------------------------------------------------------------- /components/shared/navbar/MobileNav.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | import Image from "next/image"; 5 | import Link from "next/link"; 6 | import { SignedOut } from "@clerk/nextjs"; 7 | 8 | import { 9 | Sheet, 10 | SheetClose, 11 | SheetContent, 12 | SheetTrigger, 13 | } from "@/components/ui/sheet"; 14 | import { Button } from "@/components/ui/button"; 15 | import { sidebarLinks } from "@/constants"; 16 | import { usePathname } from "next/navigation"; 17 | 18 | const NavContent = () => { 19 | const pathname = usePathname(); 20 | 21 | return ( 22 |
23 | {sidebarLinks.map((item) => { 24 | const isActive = 25 | (pathname.includes(item.route) && item.route.length > 1) || 26 | pathname === item.route; 27 | return ( 28 | 29 | 37 | {item.label} 44 |

45 | {item.label} 46 |

47 | 48 |
49 | ); 50 | })} 51 |
52 | ); 53 | }; 54 | 55 | const MobileNav = () => { 56 | return ( 57 | 58 | 59 | Menu 66 | 67 | 71 | 72 | Pinoy Overflow 78 | 79 |

80 | Pinoy Overflow 81 |

82 | 83 |
84 | 85 | 86 | 87 | 88 | 89 |
90 | 91 | 92 | 95 | 96 | 97 | 98 | 99 | 100 | 103 | 104 | 105 |
106 |
107 |
108 |
109 |
110 | ); 111 | }; 112 | 113 | export default MobileNav; 114 | -------------------------------------------------------------------------------- /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 | 46 | ); 47 | }; 48 | 49 | export default Navbar; 50 | -------------------------------------------------------------------------------- /components/shared/navbar/Theme.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | import Image from "next/image"; 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 { themes } from "@/constants"; 15 | 16 | const Theme = () => { 17 | const { mode, setMode } = useTheme(); 18 | 19 | return ( 20 | 21 | 22 | 23 | {mode === "light" ? ( 24 | Sun 31 | ) : ( 32 | Moon 39 | )} 40 | 41 | 42 | {themes.map((item) => ( 43 | { 46 | setMode(item.value); 47 | 48 | if (item.value !== "system") { 49 | localStorage.theme = item.value; 50 | } else { 51 | localStorage.removeItem("theme"); 52 | } 53 | }} 54 | className="flex cursor-pointer items-center gap-4 px-2.5 py-2 focus:bg-light-800 dark:focus:bg-dark-400" 55 | > 56 | {item.value} 63 |

70 | {item.label} 71 |

72 |
73 | ))} 74 |
75 |
76 |
77 | ); 78 | }; 79 | 80 | export default Theme; 81 | -------------------------------------------------------------------------------- /components/shared/search/GlobalFilters.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { GlobalSearchFilters } from "@/constants/filter"; 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 | const newUrl = formUrlQuery({ 20 | params: searchParams.toString(), 21 | key: "type", 22 | value: null, 23 | }); 24 | 25 | router.push(newUrl, { scroll: false }); 26 | } else { 27 | setActive(item); 28 | const newUrl = formUrlQuery({ 29 | params: searchParams.toString(), 30 | key: "type", 31 | value: item.toLowerCase(), 32 | }); 33 | router.push(newUrl, { scroll: false }); 34 | } 35 | }; 36 | 37 | return ( 38 |
39 |

Type:

40 |
41 | {GlobalSearchFilters.map((item) => ( 42 | 56 | ))} 57 |
58 |
59 | ); 60 | }; 61 | 62 | export default GlobalFilters; 63 | -------------------------------------------------------------------------------- /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 Image from "next/image"; 7 | import Link from "next/link"; 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 | 16 | const [result, setResult] = useState([ 17 | { type: "question", id: "1", title: "Next.js question" }, 18 | { type: "question", id: "2", title: "Next.js zxc" }, 19 | { type: "question", id: "3", title: "Next.js asd" }, 20 | ]); 21 | 22 | const global = searchParams.get("global"); 23 | const type = searchParams.get("type"); 24 | 25 | useEffect(() => { 26 | const fetchResult = async () => { 27 | setResult([]); 28 | setIsLoading(true); 29 | try { 30 | const res = await globalSearch({ query: global, type }); 31 | setResult(JSON.parse(res)); 32 | } catch (error) { 33 | console.log(error); 34 | throw error; 35 | } finally { 36 | setIsLoading(false); 37 | } 38 | }; 39 | 40 | if (global) { 41 | fetchResult(); 42 | } 43 | }, [global, type]); 44 | 45 | const renderLink = (type: string, id: string) => { 46 | switch (type) { 47 | case "question": 48 | return `/question/${id}`; 49 | case "answer": 50 | return `/question/${id}`; 51 | case "user": 52 | return `/profile/${id}`; 53 | case "tag": 54 | return `/tags/${id}`; 55 | default: 56 | return "/"; 57 | } 58 | }; 59 | 60 | return ( 61 |
62 |

63 | 64 |

65 |
66 |
67 |

68 | Top Match 69 |

70 | {isLoading ? ( 71 |
72 | 73 |

74 | Browsing the entire database 75 |

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

96 | {item.title} 97 |

98 |

99 | {item.type} 100 |

101 |
102 | 103 | )) 104 | ) : ( 105 |
106 |

107 | Oops, no results found. 108 |

109 |
110 | )} 111 |
112 | )} 113 |
114 |
115 | ); 116 | }; 117 | 118 | export default GlobalResult; 119 | -------------------------------------------------------------------------------- /components/shared/search/GlobalSearch.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Image from "next/image"; 4 | import React, { useEffect, useRef, useState } from "react"; 5 | 6 | import { Input } from "@/components/ui/input"; 7 | import { usePathname, useRouter, useSearchParams } from "next/navigation"; 8 | import { formUrlQuery, removeKeysFromQuery } from "@/lib/utils"; 9 | import GlobalResult from "./GlobalResult"; 10 | 11 | const GlobalSearch = () => { 12 | const router = useRouter(); 13 | const pathname = usePathname(); 14 | const searchParams = useSearchParams(); 15 | const searchContainerRef = useRef(null); 16 | 17 | const query = searchParams.get("q"); 18 | 19 | const [search, setSearch] = useState(query || ""); 20 | const [isOpen, setIsOpen] = useState(false); 21 | 22 | useEffect(() => { 23 | const handleOutsideClick = (event: any) => { 24 | if ( 25 | searchContainerRef.current && 26 | // @ts-ignore 27 | !searchContainerRef.current.contains(event.target) 28 | ) { 29 | setIsOpen(false); 30 | setSearch(""); 31 | } 32 | }; 33 | 34 | setIsOpen(false); 35 | 36 | document.addEventListener("click", handleOutsideClick); 37 | 38 | return () => { 39 | document.removeEventListener("click", handleOutsideClick); 40 | }; 41 | }, [pathname]); 42 | 43 | useEffect(() => { 44 | const delayDebounceFn = setTimeout(() => { 45 | if (search) { 46 | const newUrl = formUrlQuery({ 47 | params: searchParams.toString(), 48 | key: "global", 49 | value: search, 50 | }); 51 | 52 | router.push(newUrl, { scroll: false }); 53 | } else { 54 | if (query) { 55 | const newUrl = removeKeysFromQuery({ 56 | params: searchParams.toString(), 57 | keysToRemove: ["global", "type"], 58 | }); 59 | 60 | router.push(newUrl, { scroll: false }); 61 | } 62 | } 63 | }, 300); 64 | 65 | return () => clearTimeout(delayDebounceFn); 66 | }, [search, router, pathname, searchParams, query]); 67 | 68 | return ( 69 |
73 |
74 | Search 81 | { 85 | setSearch(e.target.value); 86 | 87 | if (!isOpen) setIsOpen(true); 88 | if (e.target.value === "" && isOpen) setIsOpen(false); 89 | }} 90 | placeholder="Search globally" 91 | className="paragraph-regular no-focus placeholder text-dark400_light700 border-none bg-transparent shadow-none outline-none" 92 | /> 93 |
94 | {isOpen && } 95 |
96 | ); 97 | }; 98 | 99 | export default GlobalSearch; 100 | -------------------------------------------------------------------------------- /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 | }, 300); 54 | 55 | return () => clearTimeout(delayDebounceFn); 56 | }, [search, route, pathname, router, searchParams, query]); 57 | 58 | return ( 59 |
62 | {iconPosition === "left" && ( 63 | search icon 70 | )} 71 | 72 | setSearch(e.target.value)} 77 | className="paragraph-regular no-focus placeholder text-dark400_light700 border-none bg-transparent shadow-none outline-none" 78 | /> 79 | 80 | {iconPosition === "right" && ( 81 | search icon 88 | )} 89 |
90 | ); 91 | }; 92 | 93 | export default LocalSearchbar; 94 | -------------------------------------------------------------------------------- /components/ui/badge.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable tailwindcss/no-custom-classname */ 2 | import * as React from "react"; 3 | import { cva, type VariantProps } from "class-variance-authority"; 4 | 5 | import { cn } from "@/lib/utils"; 6 | 7 | const badgeVariants = cva( 8 | "focus:ring-ring 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-offset-2", 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | "bg-primary text-primary-foreground hover:bg-primary/80 border-transparent", 14 | secondary: 15 | "bg-secondary text-secondary-foreground hover:bg-secondary/80 border-transparent", 16 | destructive: 17 | "bg-destructive text-destructive-foreground hover:bg-destructive/80 border-transparent", 18 | outline: "text-foreground", 19 | }, 20 | }, 21 | defaultVariants: { 22 | variant: "default", 23 | }, 24 | } 25 | ); 26 | 27 | export interface BadgeProps 28 | extends React.HTMLAttributes, 29 | VariantProps {} 30 | 31 | function Badge({ className, variant, ...props }: BadgeProps) { 32 | return ( 33 |
34 | ); 35 | } 36 | 37 | export { Badge, badgeVariants }; 38 | -------------------------------------------------------------------------------- /components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable tailwindcss/no-custom-classname */ 2 | import * as React from "react"; 3 | import { Slot } from "@radix-ui/react-slot"; 4 | import { cva, type VariantProps } from "class-variance-authority"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | const buttonVariants = cva( 9 | "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", 10 | { 11 | variants: { 12 | variant: { 13 | default: "bg-primary text-primary-foreground hover:bg-primary/90", 14 | destructive: 15 | "bg-destructive text-destructive-foreground hover:bg-destructive/90", 16 | outline: 17 | "border-input bg-background hover:bg-accent hover:text-accent-foreground border", 18 | secondary: 19 | "bg-secondary text-secondary-foreground hover:bg-secondary/80", 20 | ghost: "hover:bg-accent hover:text-accent-foreground", 21 | link: "text-primary underline-offset-4 hover:underline", 22 | }, 23 | size: { 24 | default: "h-10 px-4 py-2", 25 | sm: "h-9 rounded-md px-3", 26 | lg: "h-11 rounded-md px-8", 27 | icon: "h-10 w-10", 28 | }, 29 | }, 30 | defaultVariants: { 31 | variant: "default", 32 | size: "default", 33 | }, 34 | } 35 | ); 36 | 37 | export interface ButtonProps 38 | extends React.ButtonHTMLAttributes, 39 | VariantProps { 40 | asChild?: boolean; 41 | } 42 | 43 | const Button = React.forwardRef( 44 | ({ className, variant, size, asChild = false, ...props }, ref) => { 45 | const Comp = asChild ? Slot : "button"; 46 | return ( 47 | 52 | ); 53 | } 54 | ); 55 | Button.displayName = "Button"; 56 | 57 | export { Button, buttonVariants }; 58 | -------------------------------------------------------------------------------- /components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | export interface InputProps 6 | extends React.InputHTMLAttributes {} 7 | 8 | const Input = React.forwardRef( 9 | ({ className, type, ...props }, ref) => { 10 | return ( 11 | 20 | ) 21 | } 22 | ) 23 | Input.displayName = "Input" 24 | 25 | export { Input } 26 | -------------------------------------------------------------------------------- /components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as LabelPrimitive from "@radix-ui/react-label" 5 | import { cva, type VariantProps } from "class-variance-authority" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const labelVariants = cva( 10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" 11 | ) 12 | 13 | const Label = React.forwardRef< 14 | React.ElementRef, 15 | React.ComponentPropsWithoutRef & 16 | VariantProps 17 | >(({ className, ...props }, ref) => ( 18 | 23 | )) 24 | Label.displayName = LabelPrimitive.Root.displayName 25 | 26 | export { Label } 27 | -------------------------------------------------------------------------------- /components/ui/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils"; 2 | import React from "react"; 3 | 4 | function Skeleton({ 5 | className, 6 | ...props 7 | }: React.HTMLAttributes) { 8 | return ( 9 |
16 | ); 17 | } 18 | 19 | export { Skeleton }; 20 | -------------------------------------------------------------------------------- /components/ui/tabs.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as TabsPrimitive from "@radix-ui/react-tabs" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Tabs = TabsPrimitive.Root 9 | 10 | const TabsList = React.forwardRef< 11 | React.ElementRef, 12 | React.ComponentPropsWithoutRef 13 | >(({ className, ...props }, ref) => ( 14 | 22 | )) 23 | TabsList.displayName = TabsPrimitive.List.displayName 24 | 25 | const TabsTrigger = React.forwardRef< 26 | React.ElementRef, 27 | React.ComponentPropsWithoutRef 28 | >(({ className, ...props }, ref) => ( 29 | 37 | )) 38 | TabsTrigger.displayName = TabsPrimitive.Trigger.displayName 39 | 40 | const TabsContent = React.forwardRef< 41 | React.ElementRef, 42 | React.ComponentPropsWithoutRef 43 | >(({ className, ...props }, ref) => ( 44 | 52 | )) 53 | TabsContent.displayName = TabsPrimitive.Content.displayName 54 | 55 | export { Tabs, TabsList, TabsTrigger, TabsContent } 56 | -------------------------------------------------------------------------------- /components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | export interface TextareaProps 6 | extends React.TextareaHTMLAttributes {} 7 | 8 | const Textarea = React.forwardRef( 9 | ({ className, ...props }, ref) => { 10 | return ( 11 |