├── .eslintrc.json ├── app ├── icon.ico ├── (auth) │ ├── sign-in │ │ └── [[...sign-in]] │ │ │ └── page.tsx │ ├── sign-up │ │ └── [[...sign-up]] │ │ │ └── page.tsx │ └── layout.tsx ├── (root) │ ├── collection │ │ ├── loading.tsx │ │ └── page.tsx │ ├── tags │ │ ├── [id] │ │ │ ├── loading.tsx │ │ │ └── page.tsx │ │ ├── loading.tsx │ │ └── page.tsx │ ├── ask-question │ │ └── page.tsx │ ├── community │ │ ├── loading.tsx │ │ └── page.tsx │ ├── profile │ │ ├── edit │ │ │ └── page.tsx │ │ └── [id] │ │ │ ├── loading.tsx │ │ │ └── page.tsx │ ├── layout.tsx │ ├── question │ │ ├── edit │ │ │ └── [id] │ │ │ │ └── page.tsx │ │ └── [id] │ │ │ └── page.tsx │ └── (home) │ │ ├── loading.tsx │ │ └── page.tsx ├── layout.tsx ├── api │ └── webhooks │ │ └── clerk │ │ └── route.ts └── globals.css ├── public ├── assets │ ├── images │ │ ├── logo.png │ │ ├── auth-dark.png │ │ ├── auth-light.png │ │ ├── dark-illustration.png │ │ ├── light-illustration.png │ │ ├── site-logo.svg │ │ └── default-logo.svg │ └── icons │ │ ├── arrow-right.svg │ │ ├── arrow-left.svg │ │ ├── star.svg │ │ ├── tag.svg │ │ ├── avatar.svg │ │ ├── computer.svg │ │ ├── moon.svg │ │ ├── arrow-up-right.svg │ │ ├── clock-2.svg │ │ ├── clock.svg │ │ ├── hamburger.svg │ │ ├── message.svg │ │ ├── search.svg │ │ ├── trash.svg │ │ ├── star-red.svg │ │ ├── star-filled.svg │ │ ├── downvote.svg │ │ ├── upvote.svg │ │ ├── currency-dollar-circle.svg │ │ ├── edit.svg │ │ ├── downvoted.svg │ │ ├── upvoted.svg │ │ ├── job-search.svg │ │ ├── chevron-down.svg │ │ ├── mingcute-down-line.svg │ │ ├── link.svg │ │ ├── chevron-right.svg │ │ ├── sign-out.svg │ │ ├── location.svg │ │ ├── close.svg │ │ ├── suitcase.svg │ │ ├── account.svg │ │ ├── user.svg │ │ ├── eye.svg │ │ ├── stars.svg │ │ ├── carbon-location.svg │ │ ├── sun.svg │ │ ├── sign-up.svg │ │ ├── bronze-medal.svg │ │ ├── like.svg │ │ ├── home.svg │ │ ├── silver-medal.svg │ │ ├── question.svg │ │ ├── au.svg │ │ ├── gold-medal.svg │ │ └── calendar.svg ├── vercel.svg └── next.svg ├── postcss.config.mjs ├── .env.example ├── components.json ├── middleware.ts ├── components ├── ui │ ├── skeleton.tsx │ ├── label.tsx │ ├── textarea.tsx │ ├── input.tsx │ ├── toaster.tsx │ ├── badge.tsx │ ├── button.tsx │ └── tabs.tsx ├── shared │ ├── ProfileLink.tsx │ ├── RenderTag.tsx │ ├── AnswerTab.tsx │ ├── Metric.tsx │ ├── QuestionTab.tsx │ ├── navbar │ │ ├── Navbar.tsx │ │ ├── Theme.tsx │ │ └── MobileNav.tsx │ ├── NoResult.tsx │ ├── ParseHTML.tsx │ ├── EditDeleteAction.tsx │ ├── Pagination.tsx │ ├── search │ │ ├── GlobalFilters.tsx │ │ ├── LocalSearchBar.tsx │ │ ├── GlobalSearch.tsx │ │ └── GlobalResult.tsx │ ├── Filter.tsx │ ├── RightSidebar.tsx │ ├── Stats.tsx │ ├── AllAnswers.tsx │ └── LeftSidebar.tsx ├── home │ └── HomeFilters.tsx └── cards │ ├── UserCard.tsx │ ├── AnswerCard.tsx │ └── QuestionCard.tsx ├── next.config.mjs ├── .gitignore ├── lib ├── validations.ts ├── mongoose.ts ├── actions │ ├── interaction.action.ts │ ├── general.action.ts │ ├── shared.types.d.ts │ └── tag.action.ts └── utils.ts ├── tsconfig.json ├── database ├── tag.model.ts ├── answer.model.ts ├── interaction.model.ts ├── question.model.ts └── user.model.ts ├── types └── index.d.ts ├── constants ├── filter.ts └── index.ts ├── package.json ├── context └── ThemeProvider.tsx ├── tailwind.config.ts └── styles └── prism.css /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /app/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rupesh-dev30/DevForum/HEAD/app/icon.ico -------------------------------------------------------------------------------- /public/assets/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rupesh-dev30/DevForum/HEAD/public/assets/images/logo.png -------------------------------------------------------------------------------- /public/assets/images/auth-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rupesh-dev30/DevForum/HEAD/public/assets/images/auth-dark.png -------------------------------------------------------------------------------- /public/assets/images/auth-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rupesh-dev30/DevForum/HEAD/public/assets/images/auth-light.png -------------------------------------------------------------------------------- /public/assets/images/dark-illustration.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rupesh-dev30/DevForum/HEAD/public/assets/images/dark-illustration.png -------------------------------------------------------------------------------- /public/assets/images/light-illustration.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rupesh-dev30/DevForum/HEAD/public/assets/images/light-illustration.png -------------------------------------------------------------------------------- /app/(auth)/sign-in/[[...sign-in]]/page.tsx: -------------------------------------------------------------------------------- 1 | import { SignIn } from "@clerk/nextjs"; 2 | 3 | export default function Page() { 4 | return ; 5 | } -------------------------------------------------------------------------------- /app/(auth)/sign-up/[[...sign-up]]/page.tsx: -------------------------------------------------------------------------------- 1 | import { SignUp } from "@clerk/nextjs"; 2 | 3 | export default function Page() { 4 | return ; 5 | } -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /app/(auth)/layout.tsx: -------------------------------------------------------------------------------- 1 | export default function AuthLayout({ 2 | children, 3 | }: Readonly<{ 4 | children: React.ReactNode; 5 | }>) { 6 | return ( 7 |
8 | {children} 9 |
10 | ); 11 | } -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | NEXT_CLERK_WEBHOOK_SECRET= 2 | NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY= 3 | CLERK_SECRET_KEY= 4 | NEXT_PUBLIC_CLERK_SIGN_IN_URL= 5 | NEXT_PUBLIC_CLERK_SIGN_UP_URL= 6 | NEXT_PUBLIC_TINY_EDITOR_API_KEY= 7 | MONGODB_URL= 8 | NEXT_PUBLIC_SERVER_URL=http://localhost:3000 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /public/assets/icons/arrow-right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /public/assets/icons/arrow-left.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /public/assets/icons/star.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /public/assets/icons/tag.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /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.ts", 8 | "css": "app/globals.css", 9 | "baseColor": "slate", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } -------------------------------------------------------------------------------- /middleware.ts: -------------------------------------------------------------------------------- 1 | import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server"; 2 | 3 | const isProtectedRoute = createRouteMatcher([ 4 | '/ask-question(.*)', 5 | '/collection(.*)' 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 | }; -------------------------------------------------------------------------------- /components/ui/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils"; 2 | import { HTMLAttributes } from "react"; 3 | 4 | function Skeleton({ className, ...props }: HTMLAttributes) { 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 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /public/assets/icons/computer.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /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 | 2 | 3 | 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 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/assets/icons/clock-2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /public/assets/icons/clock.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /public/assets/icons/hamburger.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /public/assets/icons/message.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/assets/icons/search.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /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 |
17 | 21 |
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 | icon 14 | 15 | {href ? ( 16 | 21 | {title} 22 | 23 | ) : ( 24 |

{title}

25 | )} 26 |
27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /public/assets/icons/trash.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 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 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /public/assets/icons/star-filled.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /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 |