├── .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 | 
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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