├── .eslintrc.json
├── .gitignore
├── .vscode
└── settings.json
├── README.md
├── app
├── (auth)
│ ├── layout.tsx
│ ├── sign-in
│ │ └── [[...sign-in]]
│ │ │ └── page.tsx
│ └── sign-up
│ │ └── [[...sign-up]]
│ │ └── page.tsx
├── (root)
│ ├── (home)
│ │ ├── loading.tsx
│ │ └── page.tsx
│ ├── ask-question
│ │ └── page.tsx
│ ├── collection
│ │ ├── loading.tsx
│ │ └── page.tsx
│ ├── community
│ │ ├── loading.tsx
│ │ └── page.tsx
│ ├── jobs
│ │ ├── loading.tsx
│ │ └── page.tsx
│ ├── layout.tsx
│ ├── profile
│ │ ├── [id]
│ │ │ ├── loading.tsx
│ │ │ └── page.tsx
│ │ └── edit
│ │ │ └── page.tsx
│ ├── question
│ │ ├── [id]
│ │ │ └── page.tsx
│ │ └── edit
│ │ │ └── [id]
│ │ │ └── page.tsx
│ └── tags
│ │ ├── [id]
│ │ ├── loading.tsx
│ │ └── page.tsx
│ │ ├── loading.tsx
│ │ └── page.tsx
├── api
│ ├── chatgpt
│ │ └── route.ts
│ └── webhook
│ │ └── route.ts
├── favicon.ico
├── globals.css
└── layout.tsx
├── components.json
├── components
├── cards
│ ├── AnswerCard.tsx
│ ├── JobCard.tsx
│ ├── QuestionCard.tsx
│ └── UserCard.tsx
├── forms
│ ├── Answer.tsx
│ ├── Profile.tsx
│ └── Question.tsx
├── home
│ └── HomeFilters.tsx
├── shared
│ ├── AllAnswers
│ │ └── AllAnswers.tsx
│ ├── AnswersTab
│ │ └── AnswersTab.tsx
│ ├── EditDeleteAction
│ │ └── EditDeleteAction.tsx
│ ├── Filter
│ │ ├── Filter.tsx
│ │ └── JobsFilter.tsx
│ ├── LeftSidebar
│ │ └── LeftSidebar.tsx
│ ├── Metric
│ │ └── Metric.tsx
│ ├── NoResult
│ │ └── NoResult.tsx
│ ├── Pagination
│ │ └── Pagination.tsx
│ ├── ParseHTML
│ │ └── ParseHTML.tsx
│ ├── ProfileLink
│ │ └── ProfileLink.tsx
│ ├── QuestionTab
│ │ └── QuestionTab.tsx
│ ├── RightSidebar
│ │ ├── RenderTag.tsx
│ │ └── RightSidebar.tsx
│ ├── Stats
│ │ └── Stats.tsx
│ ├── Votes
│ │ └── Votes.tsx
│ ├── navbar
│ │ ├── MobileNav.tsx
│ │ ├── Navbar.tsx
│ │ └── Theme.tsx
│ └── search
│ │ ├── GlobalFilters.tsx
│ │ ├── GlobalSearch.tsx
│ │ ├── GlobarResult.tsx
│ │ └── LocalSearchBar.tsx
└── ui
│ ├── badge.tsx
│ ├── button.tsx
│ ├── form.tsx
│ ├── input.tsx
│ ├── label.tsx
│ ├── menubar.tsx
│ ├── select.tsx
│ ├── sheet.tsx
│ ├── skeleton.tsx
│ ├── tabs.tsx
│ ├── textarea.tsx
│ ├── toast.tsx
│ ├── toaster.tsx
│ └── use-toast.ts
├── constants
├── filters.ts
└── index.ts
├── context
└── ThemeProvider.tsx
├── database
├── answer.model.ts
├── interaction.model.ts
├── question.model.ts
├── tag.model.ts
└── user.model.ts
├── lib
├── actions
│ ├── answer.action.ts
│ ├── general.action.ts
│ ├── interaction.action.ts
│ ├── job.action.ts
│ ├── question.action.ts
│ ├── shared.types.d.ts
│ ├── tag.action.ts
│ └── user.action.ts
├── db
│ └── mongoose.ts
├── utils.ts
└── validations.ts
├── middleware.ts
├── next.config.js
├── package-lock.json
├── package.json
├── postcss.config.js
├── public
├── assets
│ ├── icons
│ │ ├── account.svg
│ │ ├── arrow-left.svg
│ │ ├── arrow-right.svg
│ │ ├── arrow-up-right.svg
│ │ ├── au.svg
│ │ ├── avatar.svg
│ │ ├── bronze-medal.svg
│ │ ├── calendar.svg
│ │ ├── carbon-location.svg
│ │ ├── chevron-down.svg
│ │ ├── chevron-right.svg
│ │ ├── clock-2.svg
│ │ ├── clock.svg
│ │ ├── close.svg
│ │ ├── computer.svg
│ │ ├── currency-dollar-circle.svg
│ │ ├── downvote.svg
│ │ ├── downvoted.svg
│ │ ├── edit.svg
│ │ ├── eye.svg
│ │ ├── gold-medal.svg
│ │ ├── hamburger.svg
│ │ ├── home.svg
│ │ ├── job-search.svg
│ │ ├── like.svg
│ │ ├── link.svg
│ │ ├── location.svg
│ │ ├── message.svg
│ │ ├── mingcute-down-line.svg
│ │ ├── moon.svg
│ │ ├── question.svg
│ │ ├── search.svg
│ │ ├── sign-up.svg
│ │ ├── silver-medal.svg
│ │ ├── star-filled.svg
│ │ ├── star-red.svg
│ │ ├── star.svg
│ │ ├── stars.svg
│ │ ├── suitcase.svg
│ │ ├── sun.svg
│ │ ├── tag.svg
│ │ ├── trash.svg
│ │ ├── upvote.svg
│ │ ├── upvoted.svg
│ │ ├── user.svg
│ │ └── users.svg
│ └── images
│ │ ├── auth-dark.png
│ │ ├── auth-light.png
│ │ ├── dark-illustration.png
│ │ ├── default-logo.svg
│ │ ├── light-illustration.png
│ │ ├── logo-dark.svg
│ │ ├── logo-light.svg
│ │ ├── logo.png
│ │ └── site-logo.svg
├── next.svg
└── vercel.svg
├── styles
├── prism.css
└── theme.css
├── tailwind.config.ts
├── tsconfig.json
└── types
└── index.d.ts
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "next/core-web-vitals",
4 | "standard",
5 | "plugin:tailwindcss/recommended",
6 | "prettier"
7 | ]
8 | }
9 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 |
27 | # local env files
28 | .env*.local
29 |
30 | # vercel
31 | .vercel
32 |
33 | # typescript
34 | *.tsbuildinfo
35 | next-env.d.ts
36 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | { "reactSnippets.settings.importReactOnTop": false }
2 |
--------------------------------------------------------------------------------
/app/(auth)/layout.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const Layout = ({ children }: { children: React.ReactNode }) => {
4 | return (
5 |
6 | {children}
7 |
8 | );
9 | };
10 |
11 | export default Layout;
12 |
--------------------------------------------------------------------------------
/app/(auth)/sign-in/[[...sign-in]]/page.tsx:
--------------------------------------------------------------------------------
1 | import { SignIn } from '@clerk/nextjs';
2 |
3 | export default function Page() {
4 | return ;
5 | }
6 |
--------------------------------------------------------------------------------
/app/(auth)/sign-up/[[...sign-up]]/page.tsx:
--------------------------------------------------------------------------------
1 | import { SignUp } from '@clerk/nextjs';
2 |
3 | export default function Page() {
4 | return ;
5 | }
6 |
--------------------------------------------------------------------------------
/app/(root)/(home)/loading.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from '@/components/ui/button';
2 | import { Skeleton } from '@/components/ui/skeleton';
3 | import { Link } from 'lucide-react';
4 |
5 | const Loading = () => {
6 | return (
7 |
8 |
9 |
All Questions
10 |
11 |
14 |
15 |
16 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | {[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((item) => (
32 |
33 | ))}
34 |
35 |
36 | );
37 | };
38 | export default Loading;
39 |
--------------------------------------------------------------------------------
/app/(root)/(home)/page.tsx:
--------------------------------------------------------------------------------
1 | import LocalSearchBar from '@/components/shared/search/LocalSearchBar';
2 | import { Button } from '@/components/ui/button';
3 | import Link from 'next/link';
4 | import { HomePageFilters } from '@/constants/filters';
5 | import Filter from '@/components/shared/Filter/Filter';
6 | import HomeFilters from '@/components/home/HomeFilters';
7 | import NoResult from '@/components/shared/NoResult/NoResult';
8 | import QuestionCard from '@/components/cards/QuestionCard';
9 | import {
10 | getQuestions,
11 | getRecommendedQuestions
12 | } from '@/lib/actions/question.action';
13 | import { SearchParamsProps } from '@/types';
14 | import Pagination from '@/components/shared/Pagination/Pagination';
15 | import type { Metadata } from 'next';
16 | import { auth } from '@clerk/nextjs';
17 |
18 | export const metadata: Metadata = {
19 | title: 'Home | Dev Overflow',
20 | description: 'Home page of Dev Overflow'
21 | };
22 |
23 | export default async function Home({ searchParams }: SearchParamsProps) {
24 | const { userId } = auth();
25 | let result;
26 |
27 | // ? fetch recomended questions
28 |
29 | if (searchParams?.filter === 'recommended') {
30 | if (userId) {
31 | result = await getRecommendedQuestions({
32 | userId,
33 | searchQuery: searchParams?.q,
34 | page: searchParams?.page ? +searchParams?.page : 1
35 | });
36 | } else {
37 | result = {
38 | questions: [],
39 | isNext: false
40 | };
41 | }
42 | } else {
43 | result = await getQuestions({
44 | searchQuery: searchParams?.q,
45 | filter: searchParams?.filter,
46 | page: searchParams?.page ? +searchParams?.page : 1
47 | });
48 | }
49 |
50 | const pageNumber = searchParams?.page ? +searchParams?.page : 1;
51 |
52 | return (
53 | <>
54 |
55 |
All Questions
56 |
57 |
60 |
61 |
62 |
63 |
70 |
71 |
76 |
77 |
78 |
79 |
80 | {/* Card section */}
81 |
82 | {/* looping through questions */}
83 | {result.questions.length > 0 ? (
84 | result.questions?.map((question) => (
85 |
96 | ))
97 | ) : (
98 |
104 | )}
105 |
106 |
107 |
110 | >
111 | );
112 | }
113 |
--------------------------------------------------------------------------------
/app/(root)/ask-question/page.tsx:
--------------------------------------------------------------------------------
1 | import Question from '@/components/forms/Question';
2 | import { getUserById } from '@/lib/actions/user.action';
3 | import { auth } from '@clerk/nextjs';
4 | import { redirect } from 'next/navigation';
5 |
6 | import type { Metadata } from 'next';
7 |
8 | export const metadata: Metadata = {
9 | title: 'Ask-question | Dev Overflow',
10 | description: 'Ask quesiton page of Dev Overflow'
11 | };
12 |
13 | const AskQuestion = async () => {
14 | const { userId } = auth();
15 |
16 | if (!userId) redirect('/sign-in');
17 |
18 | const mongoUser = await getUserById({ userId });
19 |
20 | return (
21 |
22 |
Ask a question
23 |
24 |
25 |
26 |
27 | );
28 | };
29 | export default AskQuestion;
30 |
--------------------------------------------------------------------------------
/app/(root)/collection/loading.tsx:
--------------------------------------------------------------------------------
1 | import { Skeleton } from '@/components/ui/skeleton';
2 |
3 | const Loading = () => {
4 | return (
5 |
6 | Saved Questions
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | {[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((item) => (
15 |
16 | ))}
17 |
18 |
19 | );
20 | };
21 |
22 | export default Loading;
23 |
--------------------------------------------------------------------------------
/app/(root)/collection/page.tsx:
--------------------------------------------------------------------------------
1 | import LocalSearchBar from '@/components/shared/search/LocalSearchBar';
2 | import { QuestionFilters } from '@/constants/filters';
3 | import Filter from '@/components/shared/Filter/Filter';
4 | import NoResult from '@/components/shared/NoResult/NoResult';
5 | import QuestionCard from '@/components/cards/QuestionCard';
6 | import { getSavedQuestions } from '@/lib/actions/user.action';
7 | import { auth } from '@clerk/nextjs';
8 | import { SearchParamsProps } from '@/types';
9 | import Pagination from '@/components/shared/Pagination/Pagination';
10 |
11 | import type { Metadata } from 'next';
12 |
13 | export const metadata: Metadata = {
14 | title: 'Collections | Dev Overflow',
15 | description: 'Collections page of Dev Overflow'
16 | };
17 |
18 | export default async function Collection({ searchParams }: SearchParamsProps) {
19 | const { userId } = auth();
20 | if (!userId) return null;
21 | const { questions, isNext } = await getSavedQuestions({
22 | clerkId: userId,
23 | searchQuery: searchParams?.q,
24 | filter: searchParams?.filter,
25 | page: searchParams?.page ? +searchParams?.page : 1
26 | });
27 |
28 | const pageNumber = searchParams?.page ? +searchParams?.page : 1;
29 |
30 | return (
31 | <>
32 | Saved Questions
33 |
34 |
35 |
42 |
43 |
47 |
48 |
49 | {/* Card section */}
50 |
51 | {/* // ? looping through questions */}
52 | {questions.length > 0 ? (
53 | questions?.map((question: any) => (
54 |
65 | ))
66 | ) : (
67 |
73 | )}
74 |
75 |
78 | >
79 | );
80 | }
81 |
--------------------------------------------------------------------------------
/app/(root)/community/loading.tsx:
--------------------------------------------------------------------------------
1 | import { Skeleton } from '@/components/ui/skeleton';
2 |
3 | const Loading = () => {
4 | return (
5 |
6 | All Users
7 |
8 |
9 |
10 |
11 |
12 |
13 | {[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((item) => (
14 |
18 | ))}
19 |
20 |
21 | );
22 | };
23 | export default Loading;
24 |
--------------------------------------------------------------------------------
/app/(root)/community/page.tsx:
--------------------------------------------------------------------------------
1 | import UserCard from '@/components/cards/UserCard';
2 | import Filter from '@/components/shared/Filter/Filter';
3 | import Pagination from '@/components/shared/Pagination/Pagination';
4 | import LocalSearchBar from '@/components/shared/search/LocalSearchBar';
5 | import { UserFilters } from '@/constants/filters';
6 | import { getAllUsers } from '@/lib/actions/user.action';
7 | import { SearchParamsProps } from '@/types';
8 | import Link from 'next/link';
9 |
10 | import type { Metadata } from 'next';
11 |
12 | export const metadata: Metadata = {
13 | title: 'Community| Dev Overflow',
14 | description: 'Community page of Dev Overflow'
15 | };
16 |
17 | const Community = async ({ searchParams }: SearchParamsProps) => {
18 | const { users, isNext } = await getAllUsers({
19 | searchQuery: searchParams?.q,
20 | filter: searchParams?.filter,
21 | page: searchParams?.page ? +searchParams?.page : 1
22 | });
23 |
24 | const pageNumber = searchParams?.page ? +searchParams?.page : 1;
25 |
26 | return (
27 | <>
28 | All Users
29 |
30 |
31 |
38 |
39 |
43 |
44 |
45 |
46 | {users.length > 0 ? (
47 | users.map((user) => )
48 | ) : (
49 |
50 |
No User yet
51 |
52 | Join to be the first!
53 |
54 |
55 | )}
56 |
57 |
58 |
61 | >
62 | );
63 | };
64 | export default Community;
65 |
--------------------------------------------------------------------------------
/app/(root)/jobs/loading.tsx:
--------------------------------------------------------------------------------
1 | import { Skeleton } from '@/components/ui/skeleton';
2 |
3 | const Loading = () => {
4 | return (
5 |
6 | Jobs
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | {[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((item) => (
15 |
16 | ))}
17 |
18 |
19 | );
20 | };
21 |
22 | export default Loading;
23 |
--------------------------------------------------------------------------------
/app/(root)/jobs/page.tsx:
--------------------------------------------------------------------------------
1 | import JobCard from '@/components/cards/JobCard';
2 | import JobsFilter from '@/components/shared/Filter/JobsFilter';
3 | import Pagination from '@/components/shared/Pagination/Pagination';
4 | import {
5 | fetchCountries,
6 | fetchJobs,
7 | fetchLocation
8 | } from '@/lib/actions/job.action';
9 | import { Job } from '@/types';
10 |
11 | interface Props {
12 | searchParams: {
13 | q: string;
14 | location: string;
15 | page: string;
16 | };
17 | }
18 |
19 | const page = async ({ searchParams }: Props) => {
20 | const countries = await fetchCountries();
21 | const userLocation = await fetchLocation();
22 |
23 | const jobs = await fetchJobs({
24 | query:
25 | `${searchParams?.q}, ${searchParams?.location}` ??
26 | `Software Engineer in ${userLocation}`,
27 | page: searchParams.page ?? 1
28 | });
29 | console.log(jobs);
30 | console.log(searchParams.location);
31 | const page = parseInt(searchParams.page ?? 1);
32 |
33 | return (
34 | <>
35 | Jobs
36 |
37 |
38 |
39 |
40 |
41 |
42 | {jobs?.length > 0 ? (
43 | jobs?.map((job: Job) => {
44 | if (job.job_title && job.job_title.toLowerCase() !== 'undefined')
45 | return ;
46 |
47 | return null;
48 | })
49 | ) : (
50 |
51 | Oops! We couldn't find any jobs at the moment. Please try again
52 | later
53 |
54 | )}
55 |
56 |
57 | {jobs?.length > 0 && (
58 |
59 | )}
60 | >
61 | );
62 | };
63 | export default page;
64 |
--------------------------------------------------------------------------------
/app/(root)/layout.tsx:
--------------------------------------------------------------------------------
1 | import Navbar from '@/components/shared/navbar/Navbar';
2 | import React from 'react';
3 | import LeftSidebar from '../../components/shared/LeftSidebar/LeftSidebar';
4 | import RightSidebar from '@/components/shared/RightSidebar/RightSidebar';
5 | import { Toaster } from '@/components/ui/toaster';
6 |
7 | const Layout = ({ children }: { children: React.ReactNode }) => {
8 | return (
9 |
10 |
11 |
18 |
19 |
20 | );
21 | };
22 | export default Layout;
23 |
--------------------------------------------------------------------------------
/app/(root)/profile/[id]/loading.tsx:
--------------------------------------------------------------------------------
1 | import { Skeleton } from '@/components/ui/skeleton';
2 |
3 | const Loading = () => {
4 | return (
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 | {[1, 2, 3, 4, 5].map((item) => (
43 |
44 | ))}
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 | );
62 | };
63 |
64 | export default Loading;
65 |
--------------------------------------------------------------------------------
/app/(root)/profile/edit/page.tsx:
--------------------------------------------------------------------------------
1 | import Profile from '@/components/forms/Profile';
2 |
3 | import { getUserById } from '@/lib/actions/user.action';
4 | import { ParamsProps } from '@/types';
5 | import { auth } from '@clerk/nextjs';
6 |
7 | import type { Metadata } from 'next';
8 |
9 | export const metadata: Metadata = {
10 | title: 'Edit profile | Dev Overflow',
11 | description: 'Edit profile page of Dev Overflow'
12 | };
13 |
14 | const ProfileEdit = async ({ params }: ParamsProps) => {
15 | const { userId } = auth();
16 | if (!userId) return null;
17 |
18 | const mongoUser = await getUserById({ userId });
19 |
20 | return (
21 | <>
22 | Edit Profile
23 |
26 | >
27 | );
28 | };
29 | export default ProfileEdit;
30 |
--------------------------------------------------------------------------------
/app/(root)/question/edit/[id]/page.tsx:
--------------------------------------------------------------------------------
1 | import Question from '@/components/forms/Question';
2 | import { getQuestionById } from '@/lib/actions/question.action';
3 | import { getUserById } from '@/lib/actions/user.action';
4 | import { ParamsProps } from '@/types';
5 | import { auth } from '@clerk/nextjs';
6 |
7 | import type { Metadata } from 'next';
8 |
9 | export const metadata: Metadata = {
10 | title: 'Edit Question | Dev Overflow',
11 | description: 'Edit quesiton page of Dev Overflow'
12 | };
13 |
14 | const EditQuestion = async ({ params }: ParamsProps) => {
15 | const { userId } = auth();
16 | if (!userId) return null;
17 |
18 | const mongoUser = await getUserById({ userId });
19 |
20 | const { question } = await getQuestionById({
21 | questionId: params.id
22 | });
23 |
24 | return (
25 | <>
26 | Edit Question
27 |
28 |
33 |
34 | >
35 | );
36 | };
37 | export default EditQuestion;
38 |
--------------------------------------------------------------------------------
/app/(root)/tags/[id]/loading.tsx:
--------------------------------------------------------------------------------
1 | import { Skeleton } from '@/components/ui/skeleton';
2 |
3 | const Loading = () => {
4 | return (
5 |
6 |
7 |
8 |
9 |
10 |
11 | {[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((item) => (
12 |
13 | ))}
14 |
15 |
16 | );
17 | };
18 |
19 | export default Loading;
20 |
--------------------------------------------------------------------------------
/app/(root)/tags/[id]/page.tsx:
--------------------------------------------------------------------------------
1 | import QuestionCard from '@/components/cards/QuestionCard';
2 | import NoResult from '@/components/shared/NoResult/NoResult';
3 | import Pagination from '@/components/shared/Pagination/Pagination';
4 | import LocalSearchBar from '@/components/shared/search/LocalSearchBar';
5 | import { getQuestionsByTagId } from '@/lib/actions/tag.action';
6 | import { URLProps } from '@/types';
7 |
8 | const TagDetails = async ({ params, searchParams }: URLProps) => {
9 | const { tagTitle, questions, isNext } = await getQuestionsByTagId({
10 | tagId: params.id,
11 | page: searchParams?.page ? +searchParams.page : 1,
12 | searchQuery: searchParams?.q
13 | });
14 |
15 | const pageNumber = searchParams?.page ? +searchParams?.page : 1;
16 |
17 | return (
18 | <>
19 | {tagTitle}
20 |
21 |
22 |
29 |
30 |
31 | {/* Card section */}
32 |
33 | {/* //!Todo: looping through questions */}
34 | {questions.length > 0 ? (
35 | questions?.map((question: any) => (
36 |
47 | ))
48 | ) : (
49 |
55 | )}
56 |
57 |
58 |
61 | >
62 | );
63 | };
64 | export default TagDetails;
65 |
--------------------------------------------------------------------------------
/app/(root)/tags/loading.tsx:
--------------------------------------------------------------------------------
1 | import { Skeleton } from '@/components/ui/skeleton';
2 |
3 | const Loading = () => {
4 | return (
5 |
6 | Tags
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | {[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((item) => (
15 |
19 | ))}
20 |
21 |
22 | );
23 | };
24 |
25 | export default Loading;
26 |
--------------------------------------------------------------------------------
/app/(root)/tags/page.tsx:
--------------------------------------------------------------------------------
1 | import Filter from '@/components/shared/Filter/Filter';
2 | import NoResult from '@/components/shared/NoResult/NoResult';
3 | import Pagination from '@/components/shared/Pagination/Pagination';
4 | import LocalSearchBar from '@/components/shared/search/LocalSearchBar';
5 | import { TagFilters } from '@/constants/filters';
6 | import { getAllTags } from '@/lib/actions/tag.action';
7 | import { SearchParamsProps } from '@/types';
8 | import Link from 'next/link';
9 |
10 | import type { Metadata } from 'next';
11 |
12 | export const metadata: Metadata = {
13 | title: 'Tags | Dev Overflow',
14 | description: 'Quesiton tags page of Dev Overflow'
15 | };
16 |
17 | const Tags = async ({ searchParams }: SearchParamsProps) => {
18 | const { tags, isNext } = await getAllTags({
19 | searchQuery: searchParams?.q,
20 | filter: searchParams?.filter,
21 | page: searchParams?.page ? +searchParams.page : 1
22 | });
23 |
24 | const pageNumber = searchParams?.page ? +searchParams?.page : 1;
25 | return (
26 | <>
27 | All Tags
28 |
29 |
30 |
37 |
38 |
42 |
43 |
44 |
45 | {tags.length > 0 ? (
46 | tags.map((tag) => (
47 |
52 |
53 |
54 |
55 | {tag.name}
56 |
57 |
58 |
59 |
60 | {tag.questions.length}+
61 | {' '}
62 | Questions
63 |
64 |
65 |
66 | ))
67 | ) : (
68 |
74 | )}
75 |
76 |
77 |
80 | >
81 | );
82 | };
83 | export default Tags;
84 |
--------------------------------------------------------------------------------
/app/api/chatgpt/route.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from 'next/server';
2 |
3 | export const POST = async (request: Request) => {
4 | const { question } = await request.json();
5 |
6 | try {
7 | const response = await fetch('https://api.openai.com/v1/chat/completions', {
8 | method: 'POST',
9 | headers: {
10 | 'Content-Type': 'application/json',
11 | Authorization: `Bearer ${process.env.OPENAI_API_KEY}`
12 | },
13 | body: JSON.stringify({
14 | model: 'gpt-3.5-turbo',
15 | messages: [
16 | {
17 | role: 'system',
18 | content:
19 | 'Your are a knowledgable assistant that provides quality informations'
20 | },
21 | {
22 | role: 'user',
23 | content: `Tell me ${question}`
24 | }
25 | ]
26 | })
27 | });
28 | const responseData = await response.json();
29 | const reply = responseData.choices[0].message.content;
30 |
31 | return NextResponse.json({ reply });
32 | } catch (error: any) {
33 | return NextResponse.json({
34 | error: error.message
35 | });
36 | }
37 | };
38 |
--------------------------------------------------------------------------------
/app/api/webhook/route.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable camelcase */
2 | import { Webhook } from 'svix';
3 | import { headers } from 'next/headers';
4 | import { WebhookEvent } from '@clerk/nextjs/server';
5 | import { createUser, deleteUser, updateUser } from '@/lib/actions/user.action';
6 | import { NextResponse } from 'next/server';
7 |
8 | export async function POST(req: Request) {
9 | // You can find this in the Clerk Dashboard -> Webhooks -> choose the webhook
10 | // todo: add webhook secret to .env.local
11 | const WEBHOOK_SECRET = process.env.NEXT_CLERK_WEBHOOK_SECRET;
12 |
13 | if (!WEBHOOK_SECRET) {
14 | throw new Error(
15 | 'Please add WEBHOOK_SECRET from Clerk Dashboard to .env or .env.local'
16 | );
17 | }
18 |
19 | // Get the headers
20 | const headerPayload = headers();
21 | const svix_id = headerPayload.get('svix-id');
22 | const svix_timestamp = headerPayload.get('svix-timestamp');
23 | const svix_signature = headerPayload.get('svix-signature');
24 |
25 | // If there are no headers, error out
26 | if (!svix_id || !svix_timestamp || !svix_signature) {
27 | return new Response('Error occured -- no svix headers', {
28 | status: 400
29 | });
30 | }
31 |
32 | // Get the body
33 | const payload = await req.json();
34 | const body = JSON.stringify(payload);
35 |
36 | // Create a new SVIX instance with your secret.
37 | const wh = new Webhook(WEBHOOK_SECRET);
38 |
39 | let evt: WebhookEvent;
40 |
41 | // Verify the payload with the headers
42 | try {
43 | evt = wh.verify(body, {
44 | 'svix-id': svix_id,
45 | 'svix-timestamp': svix_timestamp,
46 | 'svix-signature': svix_signature
47 | }) as WebhookEvent;
48 | } catch (err) {
49 | console.error('Error verifying webhook:', err);
50 | return new Response('Error occured', {
51 | status: 400
52 | });
53 | }
54 |
55 | // Get the ID and type
56 | const eventType = evt.type;
57 | console.log('POST eventType:', { eventType });
58 |
59 | // Handle to event
60 | if (eventType === 'user.created') {
61 | // get user data
62 | const { id, email_addresses, image_url, username, first_name, last_name } =
63 | evt.data;
64 |
65 | // create a server action to create a user in the database
66 |
67 | const mongoUser = await createUser({
68 | clerkId: id,
69 | name: `${first_name} ${last_name ? ` ${last_name}` : ''}`,
70 | username: username!,
71 | email: email_addresses[0].email_address,
72 | picture: image_url
73 | });
74 |
75 | return NextResponse.json({ messsage: 'OK', user: mongoUser });
76 | }
77 |
78 | if (eventType === 'user.updated') {
79 | // get user data
80 | const { id, email_addresses, image_url, username, first_name, last_name } =
81 | evt.data;
82 |
83 | console.log('clerk id', id);
84 |
85 | // create a server action to create a user in the database
86 |
87 | const mongoUser = await updateUser({
88 | clerkId: id,
89 | updateData: {
90 | name: `${first_name} ${last_name ? ` ${last_name}` : ''}`,
91 | username: username!,
92 | email: email_addresses[0].email_address,
93 | picture: image_url
94 | },
95 | path: `/profile/${id}`
96 | });
97 |
98 | return NextResponse.json({ messsage: 'OK', user: mongoUser });
99 | }
100 |
101 | if (eventType === 'user.deleted') {
102 | const { id } = evt.data;
103 | const deletedUser = await deleteUser({ clerkId: id! });
104 |
105 | return NextResponse.json({ messsage: 'OK', user: deletedUser });
106 | }
107 |
108 | return NextResponse.json({ messsage: 'OK' });
109 | }
110 |
--------------------------------------------------------------------------------
/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rakibtweets/dev_overflow_nextjs13/54d3ac89643761560da604d274ab869548905054/app/favicon.ico
--------------------------------------------------------------------------------
/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @import url('../styles/theme.css');
6 |
7 | body {
8 | font-family: 'Inter', sans-serif;
9 | }
10 |
11 | @layer utilities {
12 | .flex-center {
13 | @apply flex justify-center items-center;
14 | }
15 |
16 | .flex-between {
17 | @apply flex justify-between items-center;
18 | }
19 |
20 | .flex-start {
21 | @apply flex justify-start items-center;
22 | }
23 |
24 | .card-wrapper {
25 | @apply bg-light-900 dark:dark-gradient shadow-light-100 dark:shadow-dark-100;
26 | }
27 |
28 | .btn {
29 | @apply bg-light-800 dark:bg-dark-300 !important;
30 | }
31 |
32 | .btn-secondary {
33 | @apply bg-light-800 dark:bg-dark-400 !important;
34 | }
35 |
36 | .btn-tertiary {
37 | @apply bg-light-700 dark:bg-dark-300 !important;
38 | }
39 |
40 | .markdown {
41 | @apply max-w-full prose dark:prose-p:text-light-700 dark:prose-ol:text-light-700 dark:prose-ul:text-light-500 dark:prose-strong:text-white dark:prose-headings:text-white prose-headings:text-dark-400 prose-h1:text-dark-300 prose-h2:text-dark-300 prose-p:text-dark-500 prose-ul:text-dark-500 prose-ol:text-dark-500;
42 | }
43 |
44 | .primary-gradient {
45 | background: linear-gradient(129deg, #ff7000 0%, #e2995f 100%);
46 | }
47 |
48 | .dark-gradient {
49 | background: linear-gradient(
50 | 232deg,
51 | rgba(23, 28, 35, 0.41) 0%,
52 | rgba(19, 22, 28, 0.7) 100%
53 | );
54 | }
55 |
56 | .tab {
57 | @apply min-h-full dark:bg-dark-400 bg-light-800 text-light-500 dark:data-[state=active]:bg-dark-300 data-[state=active]:bg-primary-100 data-[state=active]:text-primary-500 !important;
58 | }
59 | }
60 |
61 | .no-focus {
62 | @apply focus-visible:ring-0 focus-visible:ring-transparent focus-visible:ring-offset-0 !important;
63 | }
64 |
65 | .active-theme {
66 | filter: invert(53%) sepia(98%) saturate(3332%) hue-rotate(0deg)
67 | brightness(104%) contrast(106%) !important;
68 | }
69 |
70 | .light-gradient {
71 | background: linear-gradient(
72 | 132deg,
73 | rgba(247, 249, 255, 0.5) 0%,
74 | rgba(229, 237, 255, 0.25) 100%
75 | );
76 | }
77 |
78 | .primary-text-gradient {
79 | background: linear-gradient(129deg, #ff7000 0%, #e2995f 100%);
80 | background-clip: text;
81 | -webkit-background-clip: text;
82 | -webkit-text-fill-color: transparent;
83 | }
84 |
85 | .custom-scrollbar::-webkit-scrollbar {
86 | width: 3px;
87 | height: 3px;
88 | border-radius: 2px;
89 | }
90 |
91 | .custom-scrollbar::-webkit-scrollbar-track {
92 | background: #ffffff;
93 | }
94 |
95 | .custom-scrollbar::-webkit-scrollbar-thumb {
96 | background: #888;
97 | border-radius: 50px;
98 | }
99 |
100 | .custom-scrollbar::-webkit-scrollbar-thumb:hover {
101 | background: #555;
102 | }
103 |
104 | /* Markdown Start */
105 | .markdown a {
106 | color: #1da1f2;
107 | }
108 |
109 | .markdown a,
110 | code {
111 | /* These are technically the same, but use both */
112 | overflow-wrap: break-word;
113 | word-wrap: break-word;
114 |
115 | -ms-word-break: break-all;
116 | /* This is the dangerous one in WebKit, as it breaks things wherever */
117 | word-break: break-all;
118 | /* Instead use this non-standard one: */
119 | word-break: break-word;
120 |
121 | /* Adds a hyphen where the word breaks, if supported (No Blink) */
122 | -ms-hyphens: auto;
123 | -moz-hyphens: auto;
124 | -webkit-hyphens: auto;
125 | hyphens: auto;
126 |
127 | padding: 2px;
128 | color: #ff7000 !important;
129 | }
130 |
131 | .markdown pre {
132 | display: grid;
133 | width: 100%;
134 | }
135 |
136 | .markdown pre code {
137 | width: 100%;
138 | display: block;
139 | overflow-x: auto;
140 |
141 | color: inherit !important;
142 | }
143 | /* Markdown End */
144 |
145 | /* Clerk */
146 | .cl-internal-b3fm6y {
147 | background: linear-gradient(129deg, #ff7000 0%, #e2995f 100%) !important;
148 | }
149 |
150 | .hash-span {
151 | margin-top: -140px;
152 | padding-bottom: 140px;
153 | display: block;
154 | }
155 |
156 | /* Hide scrollbar for Chrome, Safari and Opera */
157 | .no-scrollbar::-webkit-scrollbar {
158 | display: none;
159 | }
160 |
161 | /* Hide scrollbar for IE, Edge and Firefox */
162 | .no-scrollbar {
163 | -ms-overflow-style: none; /* IE and Edge */
164 | scrollbar-width: none; /* Firefox */
165 | }
166 |
--------------------------------------------------------------------------------
/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { ClerkProvider } from '@clerk/nextjs';
3 | // eslint-disable-next-line camelcase
4 | import { Inter, Space_Grotesk } from 'next/font/google';
5 | import type { Metadata } from 'next';
6 | import './globals.css';
7 | import '../styles/prism.css';
8 | import ThemeProvider from '@/context/ThemeProvider';
9 |
10 | const inter = Inter({
11 | subsets: ['latin'],
12 | weight: ['100', '200', '300', '400', '500', '600', '700', '800', '900'],
13 | variable: '--font-inter'
14 | });
15 | const spaceGrotest = Space_Grotesk({
16 | subsets: ['latin'],
17 | weight: ['300', '400', '500', '600', '700'],
18 | variable: '--font-spaceGrotesk'
19 | });
20 |
21 | export const metadata: Metadata = {
22 | title: 'DevFlow',
23 | description:
24 | 'A community driven platform for developers asking and answering questions.Get help and answers to your questions about anything related to programming and software development from the best developers on the internet.',
25 | icons: {
26 | icon: '/assets/images/site-logo.svg'
27 | }
28 | };
29 |
30 | export default function RootLayout({
31 | children
32 | }: {
33 | children: React.ReactNode;
34 | }) {
35 | return (
36 |
37 |
38 |
46 | {children}
47 |
48 |
49 |
50 | );
51 | }
52 |
--------------------------------------------------------------------------------
/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "default",
4 | "rsc": true,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.js",
8 | "css": "app/globals.css",
9 | "baseColor": "slate",
10 | "cssVariables": true
11 | },
12 | "aliases": {
13 | "components": "@/components",
14 | "utils": "@/lib/utils"
15 | }
16 | }
--------------------------------------------------------------------------------
/components/cards/AnswerCard.tsx:
--------------------------------------------------------------------------------
1 | import { formatAndDivideNumber, getTimeStamp } from '@/lib/utils';
2 | import Metric from '../shared/Metric/Metric';
3 | import Link from 'next/link';
4 | import { SignedIn } from '@clerk/nextjs';
5 | import EditDeleteAction from '../shared/EditDeleteAction/EditDeleteAction';
6 |
7 | interface Props {
8 | clerkId?: string | null;
9 | _id: string;
10 | question: {
11 | _id: string;
12 | title: string;
13 | };
14 | author: {
15 | _id: string;
16 | clerkId: string;
17 | name: string;
18 | picture: string;
19 | };
20 | upvotes: number;
21 | createdAt: Date;
22 | }
23 | const AnswerCard = ({
24 | clerkId,
25 | _id,
26 | question,
27 | author,
28 | upvotes,
29 | createdAt
30 | }: Props) => {
31 | const showActionButtons = clerkId && clerkId === author.clerkId;
32 |
33 | return (
34 |
35 |
36 |
37 |
38 |
39 | {getTimeStamp(createdAt)}
40 |
41 |
42 | {question.title}
43 |
44 |
45 |
46 | {showActionButtons && (
47 |
48 | )}
49 |
50 |
51 |
52 |
53 |
62 |
63 |
64 |
71 |
72 |
73 |
74 |
75 | );
76 | };
77 | export default AnswerCard;
78 |
--------------------------------------------------------------------------------
/components/cards/QuestionCard.tsx:
--------------------------------------------------------------------------------
1 | import Link from 'next/link';
2 | import RenderTag from '../shared/RightSidebar/RenderTag';
3 | import Metric from '../shared/Metric/Metric';
4 | import { formatAndDivideNumber, getTimeStamp } from '@/lib/utils';
5 | import { SignedIn } from '@clerk/nextjs';
6 | import EditDeleteAction from '../shared/EditDeleteAction/EditDeleteAction';
7 |
8 | interface QuestionCardProps {
9 | _id: string;
10 | title: string;
11 | tags: {
12 | _id: string;
13 | name: string;
14 | }[];
15 | author: {
16 | _id: string;
17 | clerkId: string;
18 | name: string;
19 | picture: string;
20 | };
21 | upvotes: number;
22 | views: number;
23 | answers: Array