) {
5 | return (
6 |
13 | );
14 | }
15 |
16 | export { Skeleton };
--------------------------------------------------------------------------------
/app/(root)/collection/loading.tsx:
--------------------------------------------------------------------------------
1 | import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server";
2 |
3 | const isProtectedRoute = createRouteMatcher([
4 | '/ask-question(.*)',
5 | '/collections(.*)'
6 | ]);
7 |
8 | export default clerkMiddleware((auth, req) => {
9 | if (isProtectedRoute(req)) auth().protect();
10 | });
11 |
12 | export const config = {
13 | matcher: ["/((?!.*\\..*|_next).*)", "/", "/(api|trpc)(.*)"],
14 | };
--------------------------------------------------------------------------------
/next.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | experimental: {
4 | mdxRs: true,
5 | serverComponentsExternalPackages: ['mongoose']
6 | },
7 | images: {
8 | remotePatterns: [
9 | {
10 | protocol: 'https',
11 | hostname: "*"
12 | },
13 | {
14 | protocol: "http",
15 | hostname: '*'
16 | }
17 | ]
18 | }
19 | };
20 |
21 | export default nextConfig;
22 |
--------------------------------------------------------------------------------
/public/assets/icons/avatar.svg:
--------------------------------------------------------------------------------
1 |
4 |
5 |
--------------------------------------------------------------------------------
/public/assets/icons/computer.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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;
--------------------------------------------------------------------------------
/.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 | .yarn/install-state.gz
8 |
9 | # testing
10 | /coverage
11 |
12 | # next.js
13 | /.next/
14 | /out/
15 |
16 | # production
17 | /build
18 |
19 | # misc
20 | .DS_Store
21 | *.pem
22 |
23 | # debug
24 | npm-debug.log*
25 | yarn-debug.log*
26 | yarn-error.log*
27 |
28 | # local env files
29 | .env*.local
30 |
31 | # vercel
32 | .vercel
33 |
34 | # typescript
35 | *.tsbuildinfo
36 | next-env.d.ts
37 |
--------------------------------------------------------------------------------
/public/assets/icons/moon.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/lib/validations.ts:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 |
3 | export const QuestionsSchema = z.object({
4 | title: z.string().min(5).max(130),
5 | explaination: z.string().min(100),
6 | tags: z.array(z.string().min(1).max(15)).min(1).max(3),
7 | });
8 |
9 | export const AnswerSchema = z.object({
10 | answer: z.string().min(100),
11 | });
12 |
13 | export const ProfileSchema = z.object({
14 | name: z.string().min(5).max(50),
15 | username: z.string().min(5).max(50),
16 | bio: z.string().min(10).max(150),
17 | portfolioWebsite: z.string().url(),
18 | location: z.string().min(5).max(100),
19 | });
20 |
--------------------------------------------------------------------------------
/public/assets/icons/arrow-up-right.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/assets/icons/clock-2.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/assets/icons/clock.svg:
--------------------------------------------------------------------------------
1 |
11 |
--------------------------------------------------------------------------------
/public/assets/icons/hamburger.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/public/assets/icons/message.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/public/assets/icons/search.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["dom", "dom.iterable", "esnext"],
4 | "allowJs": true,
5 | "skipLibCheck": true,
6 | "strict": true,
7 | "noEmit": true,
8 | "esModuleInterop": true,
9 | "module": "esnext",
10 | "moduleResolution": "bundler",
11 | "resolveJsonModule": true,
12 | "isolatedModules": true,
13 | "jsx": "preserve",
14 | "incremental": true,
15 | "plugins": [
16 | {
17 | "name": "next"
18 | }
19 | ],
20 | "paths": {
21 | "@/*": ["./*"]
22 | }
23 | },
24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
25 | "exclude": ["node_modules"]
26 | }
27 |
--------------------------------------------------------------------------------
/lib/mongoose.ts:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose";
2 |
3 | let isConnected: boolean = false;
4 |
5 | export const connectToDatabase = async () => {
6 | mongoose.set("strictQuery", true);
7 |
8 | if (!process.env.MONGODB_URL) {
9 | return console.log("MISSING MOGODB_URL");
10 | }
11 |
12 | if (isConnected) {
13 | // console.log("connected");
14 | }
15 |
16 | // we have a url and we are not connected
17 | try {
18 | await mongoose.connect(process.env.MONGODB_URL, {
19 | dbName: "devforum",
20 | });
21 |
22 | isConnected = true;
23 | console.log("mongodb is connected.");
24 | } catch (error) {
25 | console.log("Couldn't connect to mongodb ", error);
26 | }
27 | };
--------------------------------------------------------------------------------
/database/tag.model.ts:
--------------------------------------------------------------------------------
1 | import { Schema, model, models, Document } from "mongoose";
2 |
3 | export interface ITag extends Document {
4 | name: string;
5 | description: string;
6 | questions: Schema.Types.ObjectId[];
7 | followers: Schema.Types.ObjectId[];
8 | createdOn: Date;
9 | }
10 |
11 | const TagSchema = new Schema({
12 | name: { type: String, required: true, unique: true },
13 | description: { type: String, required: true },
14 | questions: [{ type: Schema.Types.ObjectId, ref: "Question" }],
15 | followers: [{ type: Schema.Types.ObjectId, ref: "User" }],
16 | createdOn: { type: Date, default: Date.now },
17 | });
18 |
19 | const Tag = models.Tag || model("Tag", TagSchema);
20 |
21 | export default Tag;
--------------------------------------------------------------------------------
/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/server";
4 | import { redirect } from "next/navigation";
5 | import React from "react";
6 |
7 | export default async function AskQuestion() {
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 |
--------------------------------------------------------------------------------
/app/(root)/community/loading.tsx:
--------------------------------------------------------------------------------
1 | import { Skeleton } from "@/components/ui/skeleton";
2 |
3 | export default function Loading() {
4 | return (
5 |
6 | All Users
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 |
--------------------------------------------------------------------------------
/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/server";
5 |
6 | export default async function page({ params }: ParamsProps) {
7 | const { userId } = auth();
8 | if (!userId) return null;
9 |
10 | const mongoUser = await getUserById({ userId });
11 |
12 | return (
13 | <>
14 | Edit Profile
15 |
16 |
22 | >
23 | );
24 | }
25 |
--------------------------------------------------------------------------------
/components/shared/ProfileLink.tsx:
--------------------------------------------------------------------------------
1 | import Image from "next/image";
2 | import Link from "next/link";
3 |
4 | interface Props {
5 | imgUrl: string;
6 | href?: string;
7 | title: string;
8 | }
9 |
10 | export default function ProfileLink({ imgUrl, href, title }: Props) {
11 | return (
12 |
13 |
14 |
15 | {href ? (
16 |
21 | {title}
22 |
23 | ) : (
24 |
{title}
25 | )}
26 |
27 | );
28 | }
29 |
--------------------------------------------------------------------------------
/public/assets/icons/trash.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/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;
--------------------------------------------------------------------------------
/components/shared/RenderTag.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import { Badge } from "../ui/badge";
3 |
4 | interface Props {
5 | _id: string;
6 | name: string;
7 | totalQuestions?: string;
8 | showCount?: boolean;
9 | }
10 |
11 | export default function RenderTag({
12 | _id,
13 | name,
14 | totalQuestions,
15 | showCount,
16 | }: Props) {
17 | return (
18 |
19 |
20 | {name}
21 |
22 |
23 | {showCount && (
24 | {totalQuestions}
25 | )}
26 |
27 | );
28 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/public/assets/icons/star-red.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/assets/icons/star-filled.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/database/answer.model.ts:
--------------------------------------------------------------------------------
1 | import { Schema, models, model, Document } from "mongoose";
2 |
3 | export interface IAnswer extends Document {
4 | author: Schema.Types.ObjectId;
5 | question: Schema.Types.ObjectId;
6 | content: string;
7 | upvotes: Schema.Types.ObjectId[];
8 | downvotes: Schema.Types.ObjectId[];
9 | createdAt: Date;
10 | }
11 |
12 | const AnswerSchema = new Schema({
13 | author: { type: Schema.Types.ObjectId, ref: "User", required: true },
14 | question: { type: Schema.Types.ObjectId, ref: "Question", required: true },
15 | content: { type: String, required: true },
16 | upvotes: [{ type: Schema.Types.ObjectId, ref: "User" }],
17 | downvotes: [{ type: Schema.Types.ObjectId, ref: "User" }],
18 | createdAt: { type: Date, default: Date.now },
19 | });
20 |
21 | const Answer = models.Answer || model("Answer", AnswerSchema);
22 |
23 | export default Answer;
--------------------------------------------------------------------------------
/app/(root)/layout.tsx:
--------------------------------------------------------------------------------
1 | import LeftSidebar from "@/components/shared/LeftSidebar";
2 | import Navbar from "@/components/shared/navbar/Navbar";
3 | import RightSidebar from "@/components/shared/RightSidebar";
4 | import { Toaster } from "@/components/ui/toaster";
5 | import { ReactNode } from "react";
6 |
7 | export default function layout({ children }: { children: ReactNode }) {
8 | return (
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | {children}
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | );
26 | }
27 |
--------------------------------------------------------------------------------
/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 |
19 | )
20 | }
21 | )
22 | Textarea.displayName = "Textarea"
23 |
24 | export { Textarea }
25 |
--------------------------------------------------------------------------------
/database/interaction.model.ts:
--------------------------------------------------------------------------------
1 | import { Schema, model, models, Document } from "mongoose";
2 |
3 | export interface IInteraction extends Document {
4 | user: Schema.Types.ObjectId;
5 | action: string;
6 | question: Schema.Types.ObjectId;
7 | answer: Schema.Types.ObjectId;
8 | tags: Schema.Types.ObjectId[];
9 | createdAt: Date;
10 | }
11 |
12 | const InteractionSchema = new Schema({
13 | user: { type: Schema.Types.ObjectId, ref: "User", required: true },
14 | action: { type: String, required: true },
15 | question: { type: Schema.Types.ObjectId, ref: "Question" },
16 | answer: { type: Schema.Types.ObjectId, ref: "Answer" },
17 | tags: [{ type: Schema.Types.ObjectId, ref: "Tag" }],
18 | createdAt: { type: Date, default: Date.now },
19 | });
20 |
21 | const Interaction =
22 | models.Interaction || model("Interaction", InteractionSchema);
23 |
24 | export default Interaction;
25 |
--------------------------------------------------------------------------------
/public/assets/icons/downvote.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/assets/icons/upvote.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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/server";
6 |
7 | export default async function page({ params }: ParamsProps) {
8 | const { userId } = auth();
9 | if (!userId) return null;
10 |
11 | const mongoUser = await getUserById({ userId });
12 | const result = await getQuestionById({ questionId: params.id });
13 |
14 | return (
15 | <>
16 | Edit Question
17 |
18 |
19 |
24 |
25 | >
26 | );
27 | }
28 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/public/assets/icons/currency-dollar-circle.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/assets/icons/edit.svg:
--------------------------------------------------------------------------------
1 |
12 |
--------------------------------------------------------------------------------
/components/ui/toaster.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import {
4 | Toast,
5 | ToastClose,
6 | ToastDescription,
7 | ToastProvider,
8 | ToastTitle,
9 | ToastViewport,
10 | } from "@/components/ui/toast"
11 | import { useToast } from "@/hooks/use-toast"
12 |
13 | export function Toaster() {
14 | const { toasts } = useToast()
15 |
16 | return (
17 |
18 | {toasts.map(function ({ id, title, description, action, ...props }) {
19 | return (
20 |
21 |
22 | {title && {title}}
23 | {description && (
24 | {description}
25 | )}
26 |
27 | {action}
28 |
29 |
30 | )
31 | })}
32 |
33 |
34 | )
35 | }
36 |
--------------------------------------------------------------------------------
/public/assets/icons/downvoted.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/assets/icons/upvoted.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/assets/icons/job-search.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/database/question.model.ts:
--------------------------------------------------------------------------------
1 | import { Schema, models, model, Document } from "mongoose";
2 |
3 | export interface IQuestion extends Document {
4 | title: string;
5 | content: string;
6 | tags: Schema.Types.ObjectId[];
7 | views: number;
8 | upvotes: Schema.Types.ObjectId[];
9 | downvotes: Schema.Types.ObjectId[];
10 | author: Schema.Types.ObjectId;
11 | answers: Schema.Types.ObjectId[];
12 | createdAt: Date;
13 | }
14 |
15 | const QuestionSchema = new Schema({
16 | title: { type: String, required: true },
17 | content: { type: String, required: true },
18 | tags: [{ type: Schema.Types.ObjectId, ref: "Tag" }],
19 | views: { type: Number, default: 0 },
20 | upvotes: [{ type: Schema.Types.ObjectId, ref: "User" }],
21 | downvotes: [{ type: Schema.Types.ObjectId, ref: "User" }],
22 | author: { type: Schema.Types.ObjectId, ref: "User" },
23 | answers: [{ type: Schema.Types.ObjectId, ref: "Answer" }],
24 | createdAt: { type: Date, default: Date.now },
25 | });
26 |
27 | const Question = models.Question || model("Question", QuestionSchema);
28 |
29 | export default Question;
--------------------------------------------------------------------------------
/public/assets/icons/chevron-down.svg:
--------------------------------------------------------------------------------
1 |
11 |
12 |
--------------------------------------------------------------------------------
/lib/actions/interaction.action.ts:
--------------------------------------------------------------------------------
1 | "use server";
2 |
3 | import Question from "@/database/question.model";
4 | import { connectToDatabase } from "../mongoose";
5 | import { ViewQuestionParams } from "./shared.types";
6 | import Interaction from "@/database/interaction.model";
7 |
8 | export async function viewQuestion(params: ViewQuestionParams) {
9 | try {
10 | connectToDatabase();
11 |
12 | const { questionId, userId } = params;
13 |
14 | //update view count
15 | await Question.findByIdAndUpdate(questionId, { $inc: { views: 1 } });
16 |
17 | if (userId) {
18 | const existingInteraction = await Interaction.findOne({
19 | user: userId,
20 | action: "view",
21 | question: questionId,
22 | });
23 |
24 | if(existingInteraction) return console.log("User has already viewed");
25 |
26 | //create interaction
27 | await Interaction.create({
28 | user: userId,
29 | action: "view",
30 | question: questionId
31 | })
32 | }
33 | } catch (error) {
34 | console.log(error);
35 | throw error;
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/public/assets/icons/mingcute-down-line.svg:
--------------------------------------------------------------------------------
1 |
11 |
--------------------------------------------------------------------------------
/database/user.model.ts:
--------------------------------------------------------------------------------
1 | import { Schema, models, model, Document } from "mongoose";
2 |
3 | export interface IUser extends Document {
4 | clerkId: string;
5 | name: string;
6 | username: string;
7 | email: string;
8 | password?: string;
9 | bio?: string;
10 | picture: string;
11 | location?: string;
12 | portfolioWebsite?: string;
13 | reputation?: number;
14 | saved: Schema.Types.ObjectId[];
15 | joinedAt: Date;
16 | }
17 |
18 | const UserSchema = new Schema({
19 | clerkId: { type: String, required: true },
20 | name: { type: String, required: true },
21 | username: { type: String, required: true, unique: true },
22 | email: { type: String, required: true, unique: true },
23 | password: { type: String },
24 | bio: { type: String },
25 | picture: { type: String, required: true },
26 | location: { type: String },
27 | portfolioWebsite: { type: String },
28 | reputation: { type: Number, default: 0 },
29 | saved: [{ type: Schema.Types.ObjectId, ref: "Question" }],
30 | joinedAt: { type: Date, default: Date.now },
31 | });
32 |
33 | const User = models.User || model("User", UserSchema);
34 |
35 | export default User;
--------------------------------------------------------------------------------
/public/assets/icons/link.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/public/assets/icons/chevron-right.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/assets/icons/sign-out.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/components/shared/AnswerTab.tsx:
--------------------------------------------------------------------------------
1 | import { getUserAnswers } from "@/lib/actions/user.action";
2 | import { SearchParamsProps } from "@/types";
3 | import AnswerCard from "../cards/AnswerCard";
4 | import Pagination from "./Pagination";
5 |
6 | interface Props extends SearchParamsProps {
7 | userId: string;
8 | clerkId?: string | null;
9 | }
10 |
11 | export default async function AnswerTab({
12 | searchParams,
13 | userId,
14 | clerkId,
15 | }: Props) {
16 | const result = await getUserAnswers({
17 | userId,
18 | page: searchParams.page ? +searchParams.page : 1,
19 | });
20 | return (
21 | <>
22 | {result.answers.map((item) => (
23 |
32 | ))}
33 |
34 |
40 | >
41 | );
42 | }
43 |
--------------------------------------------------------------------------------
/components/shared/Metric.tsx:
--------------------------------------------------------------------------------
1 | import Image from "next/image";
2 | import Link from "next/link";
3 |
4 | interface MetricProps {
5 | imgUrl: string;
6 | alt: string;
7 | value: string | number;
8 | title: string;
9 | href?: string;
10 | textStyles?: string;
11 | isAuthor?: boolean;
12 | }
13 |
14 | export default function Metric({
15 | imgUrl,
16 | alt,
17 | value,
18 | title,
19 | href,
20 | textStyles,
21 | isAuthor,
22 | }: MetricProps) {
23 | const metricContent = (
24 | <>
25 |
32 |
33 |
34 | {value}
35 |
40 | {title}
41 |
42 | >
43 | );
44 |
45 | if (href) {
46 | return (
47 |
48 | {metricContent}
49 |
50 | );
51 | }
52 |
53 | return {metricContent}
;
54 | }
--------------------------------------------------------------------------------
/components/ui/badge.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { cva, type VariantProps } from "class-variance-authority"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const badgeVariants = cva(
7 | "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
8 | {
9 | variants: {
10 | variant: {
11 | default:
12 | "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
13 | secondary:
14 | "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
15 | destructive:
16 | "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
17 | outline: "text-foreground",
18 | },
19 | },
20 | defaultVariants: {
21 | variant: "default",
22 | },
23 | }
24 | )
25 |
26 | export interface BadgeProps
27 | extends React.HTMLAttributes,
28 | VariantProps {}
29 |
30 | function Badge({ className, variant, ...props }: BadgeProps) {
31 | return (
32 |
33 | )
34 | }
35 |
36 | export { Badge, badgeVariants }
37 |
--------------------------------------------------------------------------------
/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/components/shared/QuestionTab.tsx:
--------------------------------------------------------------------------------
1 | import { getUserQuestions } from "@/lib/actions/user.action";
2 | import { SearchParamsProps } from "@/types";
3 | import QuestionCard from "../cards/QuestionCard";
4 | import Pagination from "./Pagination";
5 |
6 | interface Props extends SearchParamsProps {
7 | userId: string;
8 | clerkId?: string | null;
9 | }
10 |
11 | export default async function QuestionTab({
12 | searchParams,
13 | userId,
14 | clerkId,
15 | }: Props) {
16 | const result = await getUserQuestions({
17 | userId,
18 | page: searchParams.page ? +searchParams.page : 1,
19 | });
20 |
21 | return (
22 | <>
23 | {result.questions.map((question) => (
24 |
36 | ))}
37 |
38 |
44 | >
45 | );
46 | }
47 |
--------------------------------------------------------------------------------
/public/assets/icons/location.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/public/assets/icons/close.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/types/index.d.ts:
--------------------------------------------------------------------------------
1 | import { BADGE_CRITERIA } from "@/constants";
2 |
3 | export type ThemeName = "light" | "dark" | "system";
4 |
5 | export interface IThemes {
6 | value: ThemeName;
7 | label: string;
8 | icon: string;
9 | }
10 |
11 | export interface SidebarLink {
12 | imgURL: string;
13 | route: string;
14 | label: string;
15 | }
16 |
17 | export interface Job {
18 | id?: string;
19 | employer_name?: string;
20 | employer_logo?: string | undefined;
21 | employer_website?: string;
22 | job_employment_type?: string;
23 | job_title?: string;
24 | job_description?: string;
25 | job_apply_link?: string;
26 | job_city?: string;
27 | job_state?: string;
28 | job_country?: string;
29 | }
30 |
31 | export interface Country {
32 | name: {
33 | common: string;
34 | };
35 | }
36 |
37 | export interface ParamsProps {
38 | params: { id: string };
39 | }
40 |
41 | export interface SearchParamsProps {
42 | searchParams: { [key: string]: string | undefined };
43 | }
44 |
45 | export interface URLProps {
46 | params: { id: string };
47 | searchParams: { [key: string]: string | undefined };
48 | }
49 |
50 | export interface BadgeCounts {
51 | GOLD: number;
52 | SILVER: number;
53 | BRONZE: number;
54 | }
55 |
56 | export interface IFilterOptions {
57 | name: string;
58 | value: string;
59 | }
60 |
61 | export type BadgeCriteriaType = keyof typeof BADGE_CRITERIA;
--------------------------------------------------------------------------------
/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 Theme from "./Theme";
5 | import MobileNav from "./MobileNav";
6 | import GlobalSearch from "@/components/shared/search/GlobalSearch";
7 |
8 | export default function Navbar() {
9 | return (
10 |
40 | );
41 | }
42 |
--------------------------------------------------------------------------------
/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 | export default function NoResult({title, description, link, linkTitle}: Props) {
14 | return (
15 |
16 |
23 |
24 |
31 |
32 |
{title}
33 |
34 | {description}
35 |
36 |
37 |
38 |
39 |
40 |
41 | );
42 | }
--------------------------------------------------------------------------------
/public/assets/icons/suitcase.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/components/shared/ParseHTML.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import Prism from "prismjs";
4 | import parse from "html-react-parser"
5 |
6 | import "prismjs/components/prism-python";
7 | import "prismjs/components/prism-java";
8 | import "prismjs/components/prism-c";
9 | import "prismjs/components/prism-cpp";
10 | import "prismjs/components/prism-csharp";
11 | import "prismjs/components/prism-aspnet";
12 | import "prismjs/components/prism-sass";
13 | import "prismjs/components/prism-jsx";
14 | import "prismjs/components/prism-typescript";
15 | import "prismjs/components/prism-solidity";
16 | import "prismjs/components/prism-json";
17 | import "prismjs/components/prism-dart";
18 | import "prismjs/components/prism-ruby";
19 | import "prismjs/components/prism-rust";
20 | import "prismjs/components/prism-r";
21 | import "prismjs/components/prism-kotlin";
22 | import "prismjs/components/prism-go";
23 | import "prismjs/components/prism-bash";
24 | import "prismjs/components/prism-sql";
25 | import "prismjs/components/prism-mongodb";
26 | import "prismjs/plugins/line-numbers/prism-line-numbers.js";
27 | import "prismjs/plugins/line-numbers/prism-line-numbers.css";
28 | import { useEffect } from "react";
29 |
30 | interface Props {
31 | data: string
32 | }
33 |
34 | export default function ParseHTML({data}: Props) {
35 | useEffect(() => {
36 | Prism.highlightAll()
37 | }, []);
38 |
39 | return
40 | {parse(data)}
41 |
;
42 | }
43 |
--------------------------------------------------------------------------------
/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
--------------------------------------------------------------------------------
/public/assets/icons/account.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/components/shared/EditDeleteAction.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { deleteAnswer } from "@/lib/actions/answer.action";
4 | import { deleteQuestion } from "@/lib/actions/question.action";
5 | import Image from "next/image";
6 | import { usePathname, useRouter } from "next/navigation";
7 |
8 | interface Props {
9 | type: string;
10 | itemId: string;
11 | }
12 |
13 | export default function EditDeleteAction({ type, itemId }: Props) {
14 | const pathname = usePathname();
15 | const router = useRouter();
16 |
17 | function handleEdit() {
18 | router.push(`/question/edit/${JSON.parse(itemId)}`);
19 | }
20 |
21 | async function handleDelete() {
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 |
29 | return (
30 |
31 | {type === "Question" && (
32 |
40 | )}
41 |
42 |
50 |
51 | );
52 | }
53 |
--------------------------------------------------------------------------------
/constants/filter.ts:
--------------------------------------------------------------------------------
1 | import { IFilterOptions } from "@/types";
2 |
3 | export const AnswerFilters: IFilterOptions[] = [
4 | { name: "Highest Upvotes", value: "highestUpvotes" },
5 | { name: "Lowest Upvotes", value: "lowestUpvotes" },
6 | { name: "Most Recent", value: "recent" },
7 | { name: "Oldest", value: "old" },
8 | ];
9 |
10 | export const UserFilters: IFilterOptions[] = [
11 | { name: "New Users", value: "new_users" },
12 | { name: "Old Users", value: "old_users" },
13 | { name: "Top Contributors", value: "top_contributors" },
14 | ];
15 |
16 | export const QuestionFilters: IFilterOptions[] = [
17 | { name: "Most Recent", value: "most_recent" },
18 | { name: "Oldest", value: "oldest" },
19 | { name: "Most Voted", value: "most_voted" },
20 | { name: "Most Viewed", value: "most_viewed" },
21 | { name: "Most Answered", value: "most_answered" },
22 | ];
23 |
24 | export const TagFilters: IFilterOptions[] = [
25 | { name: "Popular", value: "popular" },
26 | { name: "Recent", value: "recent" },
27 | { name: "Name", value: "name" },
28 | { name: "Old", value: "old" },
29 | ];
30 |
31 | export const HomePageFilters: IFilterOptions[] = [
32 | { name: "Newest", value: "newest" },
33 | { name: "Recommended", value: "recommended" },
34 | { name: "Frequent", value: "frequent" },
35 | { name: "Unanswered", value: "unanswered" },
36 | ];
37 |
38 | export const GlobalSearchFilters: IFilterOptions[] = [
39 | { name: "Question", value: "question" },
40 | { name: "Answer", value: "answer" },
41 | { name: "User", value: "user" },
42 | { name: "Tag", value: "tag" },
43 | ];
--------------------------------------------------------------------------------
/public/assets/icons/user.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "dev-overflow",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "@clerk/nextjs": "^5.4.1",
13 | "@hookform/resolvers": "^3.9.0",
14 | "@radix-ui/react-dialog": "^1.1.1",
15 | "@radix-ui/react-icons": "^1.3.0",
16 | "@radix-ui/react-label": "^2.1.0",
17 | "@radix-ui/react-menubar": "^1.1.1",
18 | "@radix-ui/react-select": "^2.1.1",
19 | "@radix-ui/react-slot": "^1.1.0",
20 | "@radix-ui/react-tabs": "^1.1.0",
21 | "@radix-ui/react-toast": "^1.2.1",
22 | "@tailwindcss/typography": "^0.5.15",
23 | "@tinymce/tinymce-react": "^5.1.1",
24 | "class-variance-authority": "^0.7.0",
25 | "clsx": "^2.1.1",
26 | "html-react-parser": "^5.1.15",
27 | "lucide-react": "^0.439.0",
28 | "mongoose": "^8.6.1",
29 | "next": "14.2.8",
30 | "prismjs": "^1.29.0",
31 | "query-string": "^9.1.0",
32 | "react": "^18",
33 | "react-dom": "^18",
34 | "react-hook-form": "^7.53.0",
35 | "svix": "^1.32.0",
36 | "tailwind-merge": "^2.5.2",
37 | "tailwindcss-animate": "^1.0.7",
38 | "zod": "^3.23.8"
39 | },
40 | "devDependencies": {
41 | "@types/node": "^22",
42 | "@types/prismjs": "^1.26.4",
43 | "@types/react": "^18",
44 | "@types/react-dom": "^18",
45 | "eslint": "^8",
46 | "eslint-config-next": "14.2.8",
47 | "postcss": "^8",
48 | "tailwindcss": "^3.4.10",
49 | "typescript": "^5"
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/constants/index.ts:
--------------------------------------------------------------------------------
1 | import { SidebarLink } from "../types/index";
2 |
3 | export const themes = [
4 | { value: "light", label: "Light", icon: "/assets/icons/sun.svg" },
5 | { value: "dark", label: "Dark", icon: "/assets/icons/moon.svg" },
6 | { value: "system", label: "System", icon: "/assets/icons/computer.svg" },
7 | ];
8 |
9 | export const sidebarLinks: SidebarLink[] = [
10 | {
11 | imgURL: "/assets/icons/home.svg",
12 | route: "/",
13 | label: "Home",
14 | },
15 | {
16 | imgURL: "/assets/icons/users.svg",
17 | route: "/community",
18 | label: "Community",
19 | },
20 | // {
21 | // imgURL: "/assets/icons/star.svg",
22 | // route: "/collection",
23 | // label: "Collections",
24 | // },
25 | {
26 | imgURL: "/assets/icons/tag.svg",
27 | route: "/tags",
28 | label: "Tags",
29 | },
30 | {
31 | imgURL: "/assets/icons/user.svg",
32 | route: "/profile",
33 | label: "Profile",
34 | },
35 | {
36 | imgURL: "/assets/icons/question.svg",
37 | route: "/ask-question",
38 | label: "Ask a question",
39 | },
40 | ];
41 |
42 | export const BADGE_CRITERIA = {
43 | QUESTION_COUNT: {
44 | BRONZE: 10,
45 | SILVER: 50,
46 | GOLD: 100,
47 | },
48 | ANSWER_COUNT: {
49 | BRONZE: 10,
50 | SILVER: 50,
51 | GOLD: 100,
52 | },
53 | QUESTION_UPVOTES: {
54 | BRONZE: 10,
55 | SILVER: 50,
56 | GOLD: 100,
57 | },
58 | ANSWER_UPVOTES: {
59 | BRONZE: 10,
60 | SILVER: 50,
61 | GOLD: 100,
62 | },
63 | TOTAL_VIEWS: {
64 | BRONZE: 1000,
65 | SILVER: 10000,
66 | GOLD: 100000,
67 | },
68 | };
--------------------------------------------------------------------------------
/public/assets/icons/eye.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import { ClerkProvider, UserButton } from "@clerk/nextjs";
2 | import { Inter, Space_Grotesk } from "next/font/google";
3 | import type { Metadata } from "next";
4 | import { ThemeProvider } from "@/context/ThemeProvider";
5 | import "./globals.css";
6 | import "../styles/prism.css"
7 |
8 | export const metadata: Metadata = {
9 | title: "DevForum",
10 | description:
11 | "DevForum is the ultimate online forum for developers of all levels to connect, collaborate, and grow. Whether you're a seasoned professional or just starting out, DevForum provides a supportive and engaging environment where you can share knowledge, seek advice, and find inspiration.",
12 | icons: {
13 | icon: "/assets/images/site-logo.svg",
14 | },
15 | };
16 |
17 | const inter = Inter({
18 | subsets: ["latin"],
19 | weight: ["100", "200", "300", "400", "500", "600", "700", "800", "900"],
20 | variable: "--font-inter",
21 | });
22 |
23 | const spaceGrotesk = Space_Grotesk({
24 | subsets: ["latin"],
25 | weight: ["300", "400", "500", "600", "700"],
26 | variable: "--font-spaceGrotesk",
27 | });
28 |
29 | export default function RootLayout({
30 | children,
31 | }: {
32 | children: React.ReactNode;
33 | }) {
34 | return (
35 |
36 |
37 |
45 | {children}
46 |
47 |
48 |
49 | );
50 | }
51 |
--------------------------------------------------------------------------------
/components/shared/Pagination.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { formUrlQuery } from '@/lib/utils';
4 | import { Button } from '../ui/button'
5 | import { useRouter, useSearchParams } from 'next/navigation';
6 |
7 | interface Props {
8 | pageNumber: number;
9 | isNext: boolean;
10 | }
11 |
12 | const Pagination = ({ pageNumber, isNext }: Props) => {
13 | const router = useRouter();
14 | const searchParams = useSearchParams();
15 |
16 | const handleNavigation = (direction: string) => {
17 | const nextPageNumber = direction === 'prev'
18 | ? pageNumber - 1
19 | : pageNumber + 1;
20 |
21 | const newUrl = formUrlQuery({
22 | params: searchParams.toString(),
23 | key: 'page',
24 | value: nextPageNumber.toString(),
25 | })
26 |
27 | router.push(newUrl)
28 | }
29 |
30 | if(!isNext && pageNumber === 1) return null;
31 |
32 | return (
33 |
34 |
41 |
42 |
{pageNumber}
43 |
44 |
45 |
52 |
53 | )
54 | }
55 |
56 | export default Pagination
--------------------------------------------------------------------------------
/components/home/HomeFilters.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { HomePageFilters } from "@/constants/filter";
4 | import { Button } from "../ui/button";
5 | import { useState } from "react";
6 | import { useRouter, useSearchParams } from "next/navigation";
7 | import { formUrlQuery } from "@/lib/utils";
8 |
9 | export default function HomeFilters() {
10 | const searchParams = useSearchParams();
11 | const router = useRouter();
12 |
13 | const [active, setActive] = useState("newest");
14 |
15 | function handleTypeClick(item: string) {
16 | if (active === item) {
17 | setActive("");
18 | const newUrl = formUrlQuery({
19 | params: searchParams.toString(),
20 | key: "filter",
21 | value: null,
22 | });
23 |
24 | router.push(newUrl, { scroll: false });
25 | } else {
26 | setActive(item);
27 | const newUrl = formUrlQuery({
28 | params: searchParams.toString(),
29 | key: "filter",
30 | value: item.toLowerCase(),
31 | });
32 | router.push(newUrl, { scroll: false });
33 | }
34 | }
35 |
36 | return (
37 |
38 | {HomePageFilters.map((item) => (
39 |
51 | ))}
52 |
53 | );
54 | }
55 |
--------------------------------------------------------------------------------
/context/ThemeProvider.tsx:
--------------------------------------------------------------------------------
1 | "use client"; // since this is a provider we will make it client component
2 |
3 | import React, { createContext, useContext, useState, useEffect } from "react";
4 |
5 | export interface ThemeContextType {
6 | mode: string;
7 | // a function which accepts the mode as string and returns void
8 | // cause localstorage doesnt know what is the current active theme
9 | setMode: (mode: string) => void;
10 | }
11 |
12 | const ThemeContext = createContext(undefined);
13 |
14 | export function ThemeProvider({ children }: { children: React.ReactNode }) {
15 | const [mode, setMode] = useState("");
16 |
17 | const handleThemeChange = () => {
18 | // (!("theme" in localStorage) && window.matchMedia("(prefers-color-scheme: dark)").matches)) checks if the user's system is in dark mode or not
19 | if (
20 | localStorage.theme === "dark" ||
21 | (!("theme" in localStorage) &&
22 | window.matchMedia("(prefers-color-scheme: dark)").matches)
23 | ) {
24 | // Commentd out the setMode because useEffect was triggering handleThemeChange which repeatedly changed light->dark and dark->light mode causing an infinte loop
25 |
26 | setMode("dark");
27 | document.documentElement.classList.add("dark");
28 | } else {
29 | setMode("light");
30 | document.documentElement.classList.remove("dark");
31 | }
32 | };
33 | useEffect(() => {
34 | handleThemeChange();
35 | }, [mode]);
36 |
37 | return (
38 |
39 | {children}
40 |
41 | );
42 | }
43 |
44 | export function useTheme() {
45 | const context = useContext(ThemeContext);
46 |
47 | if (context === undefined) {
48 | throw new Error("useTheme must be used within a ThemeProvider");
49 | }
50 |
51 | return context;
52 | }
--------------------------------------------------------------------------------
/public/assets/icons/stars.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/assets/icons/carbon-location.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/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 { useState } from "react";
7 |
8 | export default function 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 | function 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 |
54 | ))}
55 |
56 |
57 | );
58 | }
59 |
--------------------------------------------------------------------------------
/public/assets/icons/sun.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/components/shared/Filter.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import {
4 | Select,
5 | SelectContent,
6 | SelectGroup,
7 | SelectItem,
8 | SelectTrigger,
9 | SelectValue,
10 | } from "@/components/ui/select";
11 | import { formUrlQuery } from "@/lib/utils";
12 | import { useRouter, useSearchParams } from "next/navigation";
13 |
14 | interface Props {
15 | filters: {
16 | name: string;
17 | value: string;
18 | }[];
19 | otherClasses?: string;
20 | containerClasses?: string;
21 | }
22 |
23 | export default function Filter({
24 | filters,
25 | otherClasses,
26 | containerClasses,
27 | }: Props) {
28 | const searchParams = useSearchParams();
29 | const router = useRouter();
30 |
31 | const paramFilter = searchParams.get("filter");
32 |
33 | const handleUpdateParams = (value: string) => {
34 | const newUrl = formUrlQuery({ params: searchParams.toString(), key: "filter", value });
35 | router.push(newUrl, { scroll: false });
36 | };
37 |
38 | return (
39 |
40 |
61 |
62 | );
63 | }
64 |
--------------------------------------------------------------------------------
/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { Slot } from "@radix-ui/react-slot"
3 | import { cva, type VariantProps } from "class-variance-authority"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const buttonVariants = cva(
8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
9 | {
10 | variants: {
11 | variant: {
12 | default: "bg-primary text-primary-foreground hover:bg-primary/90",
13 | destructive:
14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90",
15 | outline:
16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
17 | secondary:
18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80",
19 | ghost: "hover:bg-accent hover:text-accent-foreground",
20 | link: "text-primary underline-offset-4 hover:underline",
21 | },
22 | size: {
23 | default: "h-10 px-4 py-2",
24 | sm: "h-9 rounded-md px-3",
25 | lg: "h-11 rounded-md px-8",
26 | icon: "h-10 w-10",
27 | },
28 | },
29 | defaultVariants: {
30 | variant: "default",
31 | size: "default",
32 | },
33 | }
34 | )
35 |
36 | export interface ButtonProps
37 | extends React.ButtonHTMLAttributes,
38 | VariantProps {
39 | asChild?: boolean
40 | }
41 |
42 | const Button = React.forwardRef(
43 | ({ className, variant, size, asChild = false, ...props }, ref) => {
44 | const Comp = asChild ? Slot : "button"
45 | return (
46 |
51 | )
52 | }
53 | )
54 | Button.displayName = "Button"
55 |
56 | export { Button, buttonVariants }
57 |
--------------------------------------------------------------------------------
/components/shared/RightSidebar.tsx:
--------------------------------------------------------------------------------
1 | import Image from "next/image";
2 | import Link from "next/link";
3 | import RenderTag from "./RenderTag";
4 | import { getHotQuestions } from "@/lib/actions/question.action";
5 | import { getTopPopularTags } from "@/lib/actions/tag.action";
6 |
7 | export default async function RightSidebar() {
8 | const hotQuestions = await getHotQuestions();
9 | const popularTags = await getTopPopularTags();
10 |
11 | return (
12 |
13 |
14 |
Top Questions
15 |
16 | {hotQuestions.map((question) => (
17 |
22 |
23 | {question.title}
24 |
25 |
32 |
33 | ))}
34 |
35 |
36 |
37 |
Popular Tags
38 |
39 | {popularTags.map((tag) => (
40 |
47 | ))}
48 |
49 |
50 |
51 | );
52 | }
53 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/public/assets/images/site-logo.svg:
--------------------------------------------------------------------------------
1 |
17 |
--------------------------------------------------------------------------------
/public/assets/icons/sign-up.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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;
--------------------------------------------------------------------------------
/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 | export default async function Page({ params, searchParams }: URLProps) {
9 | const result = await getQuestionsByTagId({
10 | tagId: params.id,
11 | searchQuery: searchParams.q,
12 | page: searchParams.page ? +searchParams.page : 1
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 |
60 | >
61 | );
62 | }
63 |
--------------------------------------------------------------------------------
/public/assets/icons/bronze-medal.svg:
--------------------------------------------------------------------------------
1 |
35 |
--------------------------------------------------------------------------------
/components/cards/UserCard.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 | import { getTopInteractedTags } from "@/lib/actions/tag.action";
3 | import Image from "next/image";
4 | import Link from "next/link";
5 | import { Badge } from "../ui/badge";
6 | import RenderTag from "../shared/RenderTag";
7 | import { useEffect, useState } from "react";
8 |
9 | interface Props {
10 | user: {
11 | _id: string;
12 | clerkId: string;
13 | picture: string;
14 | name: string;
15 | username: string;
16 | };
17 | }
18 |
19 | export default function UserCard({ user }: Props) {
20 | const [interactedTags, setInteractedTags] = useState<{ _id: string; name: string }[]>([]);
21 |
22 |
23 | useEffect(() => {
24 | async function fetchTags() {
25 | const tags = await getTopInteractedTags({ userId: user._id });
26 | setInteractedTags(tags);
27 | }
28 | fetchTags();
29 | }, [user._id]);
30 |
31 | return (
32 |
33 |
34 |
41 |
42 |
43 |
44 | {user.name}
45 |
46 |
@{user.username}
47 |
48 |
49 |
50 | {interactedTags ? (
51 | interactedTags.length > 0 ? (
52 |
53 | {interactedTags.map((tag) => (
54 |
59 | ))}
60 |
61 | ) : (
62 |
63 | No tags yet
64 |
65 | )
66 | ) : (
67 |
Loading...
68 | )}
69 |
70 |
71 |
72 | );
73 | }
74 |
--------------------------------------------------------------------------------
/public/assets/icons/like.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/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 |
37 |
38 |
39 |
40 | {getTimestamp(createdAt)}
41 |
42 |
43 |
44 | {question.title}
45 |
46 |
47 |
48 |
49 |
50 | {showActionButtons && (
51 |
52 | )}
53 |
54 |
55 |
56 |
57 |
66 |
67 |
68 |
75 |
76 |
77 |
78 | );
79 | };
80 |
81 | export default AnswerCard;
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "tailwindcss";
2 |
3 | const config: Config = {
4 | darkMode: ["class"],
5 | content: [
6 | "./pages/**/*.{js,ts,jsx,tsx,mdx}",
7 | "./components/**/*.{js,ts,jsx,tsx,mdx}",
8 | "./app/**/*.{js,ts,jsx,tsx,mdx}",
9 | ],
10 | theme: {
11 | container: {
12 | center: true,
13 | padding: "2rem",
14 | screens: {
15 | "2xl": "1400px",
16 | },
17 | },
18 | extend: {
19 | colors: {
20 | primary: {
21 | 500: "#FF7000",
22 | 100: "#FFF1E6",
23 | },
24 | dark: {
25 | 100: "#000000",
26 | 200: "#0F1117",
27 | 300: "#151821",
28 | 400: "#212734",
29 | 500: "#101012",
30 | },
31 | light: {
32 | 900: "#FFFFFF",
33 | 800: "#F4F6F8",
34 | 850: "#FDFDFD",
35 | 700: "#DCE3F1",
36 | 500: "#7B8EC8",
37 | 400: "#858EAD",
38 | },
39 | "accent-blue": "#1DA1F2",
40 | },
41 | fontFamily: {
42 | inter: ["var(--font-inter)"],
43 | spaceGrotesk: ["var(--font-spaceGrotesk)"],
44 | },
45 | boxShadow: {
46 | "light-100":
47 | "0px 12px 20px 0px rgba(184, 184, 184, 0.03), 0px 6px 12px 0px rgba(184, 184, 184, 0.02), 0px 2px 4px 0px rgba(184, 184, 184, 0.03)",
48 | "light-200": "10px 10px 20px 0px rgba(218, 213, 213, 0.10)",
49 | "light-300": "-10px 10px 20px 0px rgba(218, 213, 213, 0.10)",
50 | "dark-100": "0px 2px 10px 0px rgba(46, 52, 56, 0.10)",
51 | "dark-200": "2px 0px 20px 0px rgba(39, 36, 36, 0.04)",
52 | },
53 | backgroundImage: {
54 | "auth-dark": "url('/assets/images/auth-dark.png')",
55 | "auth-light": "url('/assets/images/auth-light.png')",
56 | },
57 | screens: {
58 | xs: "420px",
59 | },
60 | keyframes: {
61 | "accordion-down": {
62 | from: { height: "0" },
63 | to: { height: "var(--radix-accordion-content-height)" },
64 | },
65 | "accordion-up": {
66 | from: { height: "var(--radix-accordion-content-height)" },
67 | to: { height: "0" },
68 | },
69 | },
70 | animation: {
71 | "accordion-down": "accordion-down 0.2s ease-out",
72 | "accordion-up": "accordion-up 0.2s ease-out",
73 | },
74 | },
75 | },
76 | plugins: [require("tailwindcss-animate"), require("@tailwindcss/typography")],
77 | };
78 | export default config;
--------------------------------------------------------------------------------
/components/shared/Stats.tsx:
--------------------------------------------------------------------------------
1 | import { formatAndDivideNumber } from "@/lib/utils";
2 | import { BadgeCounts } from "@/types";
3 | import Image from "next/image";
4 |
5 | interface Props {
6 | totalAnswers: number;
7 | totalQuestions: number;
8 | badges: BadgeCounts,
9 | reputation: number,
10 | }
11 |
12 | interface StatsCardProps {
13 | title: string;
14 | imgUrl: string;
15 | value: number;
16 | }
17 |
18 | const StatsCard = ({ imgUrl, value, title }: StatsCardProps) => {
19 | return (
20 |
21 |
22 |
23 |
24 |
25 | {value}
26 |
27 |
{title}
28 |
29 |
30 | );
31 | };
32 |
33 | export default function Stats({ totalAnswers, totalQuestions, badges, reputation }: Props) {
34 | return (
35 |
36 |
Stats - {reputation}
37 |
38 |
39 |
40 |
41 |
42 | {formatAndDivideNumber(totalQuestions)}
43 |
44 |
Questions
45 |
46 |
47 |
48 | {formatAndDivideNumber(totalAnswers)}
49 |
50 |
Answers
51 |
52 |
53 |
54 |
59 |
64 |
69 |
70 |
71 | );
72 | }
73 |
--------------------------------------------------------------------------------
/public/assets/icons/home.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/assets/icons/silver-medal.svg:
--------------------------------------------------------------------------------
1 |
39 |
--------------------------------------------------------------------------------
/components/shared/navbar/Theme.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useTheme } from "@/context/ThemeProvider";
4 | import {
5 | Menubar,
6 | MenubarContent,
7 | MenubarItem,
8 | MenubarMenu,
9 | MenubarTrigger,
10 | } from "@/components/ui/menubar";
11 | import Image from "next/image";
12 | import { themes } from "@/constants";
13 |
14 | export default function Theme() {
15 | const { mode, setMode } = useTheme();
16 |
17 | return (
18 |
19 |
20 |
21 | {mode === "light" ? (
22 |
29 | ) : (
30 |
37 | )}
38 |
39 |
40 | {themes.map((item) => (
41 | {
44 | setMode(item.value);
45 | if (item.value !== "system") {
46 | localStorage.theme = item.value;
47 | } else {
48 | localStorage.removeItem("theme");
49 | }
50 | }}
51 | className="flex cursor-pointer items-center gap-4 px-2.5 py-2 focus:bg-light-800 dark:focus:bg-dark-400"
52 | >
53 |
60 |
67 | {item.label}
68 |
69 |
70 | ))}
71 |
72 |
73 |
74 | );
75 | }
76 |
--------------------------------------------------------------------------------
/components/shared/search/LocalSearchBar.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Input } from "@/components/ui/input";
4 | import { formUrlQuery, removeKeysFromQuery } from "@/lib/utils";
5 | import Image from "next/image";
6 | import { usePathname, useRouter, useSearchParams } from "next/navigation";
7 | import { useEffect, useState } from "react";
8 |
9 | interface CustomInputProps {
10 | route: string;
11 | iconPosition: string;
12 | imgSrc: string;
13 | placeholder: string;
14 | otherClasses?: string;
15 | }
16 |
17 | export default function 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 | const [search, setSearch] = useState(query || "");
30 |
31 | useEffect(() => {
32 | const delayDebounceFn = setTimeout(() => {
33 | if (search) {
34 | const newUrl = formUrlQuery({
35 | params: searchParams.toString(),
36 | key: "q",
37 | value: search,
38 | });
39 |
40 | router.push(newUrl, { scroll: false });
41 | } else {
42 | if(pathname === route){
43 | const newUrl = removeKeysFromQuery({
44 | params: searchParams.toString(),
45 | keysToRemove: ["q"],
46 | })
47 |
48 | router.push(newUrl, { scroll: false });
49 | }
50 | }
51 | }, 500);
52 |
53 | return () => clearTimeout(delayDebounceFn)
54 | }, [search, route, pathname, router, searchParams, query]);
55 |
56 | return (
57 |
60 | {iconPosition === "left" && (
61 |
68 | )}
69 |
70 | setSearch(e.target.value)}
75 | className="paragraph-regular no-focus placeholder text-dark400_light700 bg-transparent border-none shadow-none outline-none"
76 | />
77 |
78 | {iconPosition === "right" && (
79 |
86 | )}
87 |
88 | );
89 | }
90 |
--------------------------------------------------------------------------------
/public/assets/icons/question.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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/filter";
6 | import { getAllUsers } from "@/lib/actions/user.action";
7 | import { SearchParamsProps } from "@/types";
8 | import Link from "next/link";
9 |
10 | export default async function Page({ searchParams }: SearchParamsProps) {
11 | let result;
12 |
13 | try {
14 | result = await getAllUsers({
15 | searchQuery: searchParams.q,
16 | filter: searchParams.filter,
17 | page: searchParams.page ? +searchParams.page : 1,
18 | });
19 |
20 | // Convert users to plain objects if necessary
21 | result.users = result.users.map(user => ({
22 | _id: user._id.toString(),
23 | clerkId: user.clerkId,
24 | name: user.name,
25 | username: user.username,
26 | picture: user.picture,
27 | // Add other fields as necessary
28 | }));
29 | } catch (error) {
30 | console.error("Error fetching users:", error);
31 | result = { users: [], isNext: false };
32 | }
33 |
34 | const users = result?.users || [];
35 |
36 | return (
37 | <>
38 | All Users
39 |
40 |
41 |
48 |
49 |
53 |
54 |
55 |
56 | {users.length > 0 ? (
57 | users.map(user => (
58 |
59 | ))
60 | ) : (
61 |
62 |
No users yet
63 |
64 | Join to be the first!
65 |
66 |
67 | )}
68 |
69 |
70 |
76 | >
77 | );
78 | }
79 |
--------------------------------------------------------------------------------
/lib/actions/general.action.ts:
--------------------------------------------------------------------------------
1 | "use server";
2 |
3 | import Question from "@/database/question.model";
4 | import { connectToDatabase } from "../mongoose";
5 | import { SearchParams } from "./shared.types";
6 | import User from "@/database/user.model";
7 | import Answer from "@/database/answer.model";
8 | import Tag from "@/database/tag.model";
9 |
10 | const searchableTypes = ["question", "answer", "user", "tag"];
11 |
12 | export async function globalSearch(params: SearchParams) {
13 | try {
14 | connectToDatabase();
15 |
16 | const { query, type } = params;
17 | const regexQuery = { $regex: query, $options: "i" };
18 |
19 | let results = [];
20 |
21 | const modelsAndTypes = [
22 | { model: Question, searchField: "title", type: "question" },
23 | { model: User, searchField: "name", type: "user" },
24 | { model: Answer, searchField: "content", type: "answer" },
25 | { model: Tag, searchField: "name", type: "tag" },
26 | ];
27 |
28 | const typeLower = type?.toLowerCase();
29 | if (!typeLower || !searchableTypes.includes(typeLower)) {
30 | // Search Across everthing
31 | for (const { model, searchField, type } of modelsAndTypes) {
32 | const queryResults = await model
33 | .find({ [searchField]: regexQuery })
34 | .limit(2);
35 |
36 | results.push(
37 | ...queryResults.map((item) => ({
38 | title:
39 | type === "answer"
40 | ? `Answers containing ${query}`
41 | : item[searchField],
42 | type,
43 | id:
44 | type === "user"
45 | ? item.clerkId
46 | : type === "answer"
47 | ? item.question
48 | : item._id,
49 | }))
50 | );
51 | }
52 | } else {
53 | const modelInfo = modelsAndTypes.find((item) => item.type === type);
54 |
55 | if (!modelInfo) {
56 | throw new Error("Invalid search type");
57 | }
58 |
59 | const queryResults = await modelInfo.model
60 | .find({ [modelInfo.searchField]: regexQuery })
61 | .limit(8);
62 |
63 | results = queryResults.map((item) => ({
64 | title:
65 | type === "answer"
66 | ? `Answers containing ${query}`
67 | : item[modelInfo.searchField],
68 | type,
69 | id:
70 | type === "user"
71 | ? item.clerkId
72 | : type === "answer"
73 | ? item.question
74 | : item._id,
75 | }));
76 | }
77 | return JSON.stringify(results);
78 | } catch (error) {
79 | console.log(`Error fetching global results, ${error}`);
80 | throw error;
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/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/filter";
6 | import { getAllTags } from "@/lib/actions/tag.action";
7 | import { SearchParamsProps } from "@/types";
8 | import Link from "next/link";
9 |
10 | export default async function page({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 | {tag.name}
43 |
44 |
45 |
46 |
47 | {tag.questions.length}+ Questions
48 |
49 |
50 | )
51 | ) : (
52 |
58 | )}
59 |
60 |
61 |
67 | >
68 | );
69 | }
70 |
--------------------------------------------------------------------------------
/public/assets/icons/au.svg:
--------------------------------------------------------------------------------
1 |
21 |
--------------------------------------------------------------------------------
/public/assets/images/default-logo.svg:
--------------------------------------------------------------------------------
1 |
12 |
--------------------------------------------------------------------------------
/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/server";
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 | ) : (
58 |
64 | )}
65 |
66 |
67 |
73 | >
74 | );
75 | }
76 |
--------------------------------------------------------------------------------
/styles/prism.css:
--------------------------------------------------------------------------------
1 | /**
2 | * MIT License
3 | * Copyright (c) 2021 Ayush Saini
4 | * Holi Theme for prism.js
5 | * @author Ayush Saini <@AyushCodes on Twitter>
6 | */
7 |
8 | code[class*="language-"],
9 | pre[class*="language-"] {
10 | color: #d6e7ff;
11 | background: #030314;
12 | text-shadow: none;
13 | font-family: "Fira Code", "Fira Mono", Menlo, Consolas, "DejaVu Sans Mono",
14 | monospace;
15 | font-size: 1em;
16 | line-height: 1.5;
17 | letter-spacing: 0.2px;
18 | white-space: pre;
19 | word-spacing: normal;
20 | word-break: normal;
21 | word-wrap: normal;
22 | text-align: left;
23 |
24 | -moz-tab-size: 4;
25 | -o-tab-size: 4;
26 | tab-size: 4;
27 |
28 | -webkit-hyphens: none;
29 | -moz-hyphens: none;
30 | -ms-hyphens: none;
31 | hyphens: none;
32 | }
33 |
34 | pre[class*="language-"]::-moz-selection,
35 | pre[class*="language-"] ::-moz-selection,
36 | code[class*="language-"]::-moz-selection,
37 | code[class*="language-"] ::-moz-selection,
38 | pre[class*="language-"]::selection,
39 | pre[class*="language-"] ::selection,
40 | code[class*="language-"]::selection,
41 | code[class*="language-"] ::selection {
42 | color: inherit;
43 | background: #1d3b54;
44 | text-shadow: none;
45 | }
46 |
47 | pre[class*="language-"] {
48 | border: 1px solid #2a4555;
49 | border-radius: 5px;
50 | padding: 1.5em 1em;
51 | margin: 1em 0;
52 | overflow: auto;
53 | }
54 |
55 | :not(pre) > code[class*="language-"] {
56 | color: #f0f6f6;
57 | background: #2a4555;
58 | padding: 0.2em 0.3em;
59 | border-radius: 0.2em;
60 | box-decoration-break: clone;
61 | }
62 |
63 | .token.comment,
64 | .token.prolog,
65 | .token.doctype,
66 | .token.cdata {
67 | color: #446e69;
68 | }
69 |
70 | .token.punctuation {
71 | color: #d6b007;
72 | }
73 |
74 | .token.property,
75 | .token.tag,
76 | .token.boolean,
77 | .token.number,
78 | .token.constant,
79 | .token.symbol,
80 | .token.deleted {
81 | color: #d6e7ff;
82 | }
83 |
84 | .token.selector,
85 | .token.attr-name,
86 | .token.builtin,
87 | .token.inserted {
88 | color: #e60067;
89 | }
90 |
91 | .token.string,
92 | .token.char {
93 | color: #49c6ec;
94 | }
95 |
96 | .token.operator,
97 | .token.entity,
98 | .token.url,
99 | .language-css .token.string,
100 | .style .token.string {
101 | color: #ec8e01;
102 | background: transparent;
103 | }
104 |
105 | .token.atrule,
106 | .token.attr-value,
107 | .token.keyword {
108 | color: #0fe468;
109 | }
110 |
111 | .token.function,
112 | .token.class-name {
113 | color: #78f3e9;
114 | }
115 |
116 | .token.regex,
117 | .token.important,
118 | .token.variable {
119 | color: #d6e7ff;
120 | }
--------------------------------------------------------------------------------
/public/assets/icons/gold-medal.svg:
--------------------------------------------------------------------------------
1 |
39 |
--------------------------------------------------------------------------------
/components/shared/search/GlobalSearch.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Input } from "@/components/ui/input";
4 | import { formUrlQuery, removeKeysFromQuery } from "@/lib/utils";
5 | import Image from "next/image";
6 | import { usePathname, useRouter, useSearchParams } from "next/navigation";
7 | import { useEffect, useRef, useState } from "react";
8 | import GlobalResult from "./GlobalResult";
9 |
10 | export default function GlobalSearch() {
11 | const router = useRouter();
12 | const pathname = usePathname();
13 | const searchParams = useSearchParams();
14 | const searchContainerRef = useRef(null);
15 |
16 | const query = searchParams.get("q");
17 | const [search, setSearch] = useState(query || "");
18 | const [isOpen, setIsOpen] = useState(false);
19 |
20 | useEffect(() => {
21 | const handleOutsideClick = (event: any) => {
22 |
23 | if(searchContainerRef.current &&
24 | // @ts-ignore
25 | !searchContainerRef.current.contains(event.target)
26 | ) {
27 | setIsOpen(false);
28 | setSearch("")
29 | }
30 | }
31 |
32 | setIsOpen(false);
33 |
34 | document.addEventListener("click", handleOutsideClick);
35 |
36 | return () => {
37 | document.removeEventListener("click", handleOutsideClick)
38 | }
39 | }, [pathname])
40 |
41 | useEffect(() => {
42 | const delayDebounceFn = setTimeout(() => {
43 | if (search) {
44 | const newUrl = formUrlQuery({
45 | params: searchParams.toString(),
46 | key: "global",
47 | value: search,
48 | });
49 |
50 | router.push(newUrl, { scroll: false });
51 | } else {
52 | if (query) {
53 | const newUrl = removeKeysFromQuery({
54 | params: searchParams.toString(),
55 | keysToRemove: ["global", "type"],
56 | });
57 |
58 | router.push(newUrl, { scroll: false });
59 | }
60 | }
61 | }, 500);
62 |
63 | return () => clearTimeout(delayDebounceFn);
64 | }, [search, router, pathname, searchParams, query]);
65 |
66 | return (
67 |
68 |
69 |
76 |
77 | {
82 | setSearch(e.target.value);
83 | if (!isOpen) setIsOpen(true);
84 | if (e.target.value === "" && isOpen) setIsOpen(false);
85 | }}
86 | className="paragraph-regular no-focus placeholder text-dark400_light700 border-none shadow-none outline-none bg-transparent"
87 | />
88 |
89 | {isOpen &&
}
90 |
91 | );
92 | }
93 |
--------------------------------------------------------------------------------
/components/shared/AllAnswers.tsx:
--------------------------------------------------------------------------------
1 | import { AnswerFilters } from "@/constants/filter";
2 | import Filter from "./Filter";
3 | import { getAnswers } from "@/lib/actions/answer.action";
4 | import Link from "next/link";
5 | import Image from "next/image";
6 | import { getTimestamp } from "@/lib/utils";
7 | import ParseHTML from "./ParseHTML";
8 | import Votes from "./Votes";
9 | import Pagination from "./Pagination";
10 |
11 | interface Props {
12 | questionId: string;
13 | userId: string;
14 | totalAnswers: number;
15 | page?: number;
16 | filter?: string;
17 | }
18 |
19 | export default async function AllAnswers({
20 | questionId,
21 | userId,
22 | totalAnswers,
23 | page,
24 | filter,
25 | }: Props) {
26 | const result = await getAnswers({
27 | questionId,
28 | page: page ? +page : 1,
29 | sortBy: filter,
30 | });
31 |
32 | return (
33 |
34 |
35 |
{totalAnswers} Answers
36 |
37 |
38 |
39 |
40 |
41 | {result.answers.map((answer) => (
42 |
43 |
44 |
48 |
55 |
56 |
57 | {answer.author.name}
58 |
59 |
60 |
61 | answered {getTimestamp(answer.createdAt)}
62 |
63 |
64 |
65 |
66 |
75 |
76 |
77 |
78 |
79 |
80 | ))}
81 |
82 |
83 |
89 |
90 | );
91 | }
92 |
--------------------------------------------------------------------------------
/components/cards/QuestionCard.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import RenderTag from "../shared/RenderTag";
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 | _id: string;
10 | title: string;
11 | tags: {
12 | _id: string;
13 | name: string;
14 | }[];
15 | author: {
16 | _id: string;
17 | name: string;
18 | picture: string;
19 | clerkId: string;
20 | };
21 | upvotes: string[];
22 | views: number;
23 | answers: Array