From e691f700f3b36c0b71470790f85c2637bc2a1269 Mon Sep 17 00:00:00 2001 From: Mounssif BOUHLAOUI Date: Sat, 23 Mar 2024 17:05:02 +0000 Subject: [PATCH 001/164] refactor(schema): refactor prisma schema for better data organization and naming - Renamed 'ownerId' to 'ownerUserId' in Forum model for clarity - Added 'slug' field to Forum model - Changed 'isHidden' to 'isVisible' in Post and Comment models for better readability - Refactored Attachment model to separate PostAttachment and CommentAttachment - Removed unnecessary whitespace and comments --- server/prisma/schema copy.prisma | 104 ++++++++++++++++++------------- 1 file changed, 59 insertions(+), 45 deletions(-) diff --git a/server/prisma/schema copy.prisma b/server/prisma/schema copy.prisma index 70808d3..f5b8c17 100644 --- a/server/prisma/schema copy.prisma +++ b/server/prisma/schema copy.prisma @@ -56,13 +56,13 @@ model User { moderations ForumModerator[] postVotes PostVotes[] @relation("UserPostVotes") CommentVotes CommentVotes[] @relation("UserCommentVotes") - socialMedia SocialMedia? @relation(fields: [id], references: [userId]) + socialMedia SocialMedia? @@index([username, email, role]) // rethink the proper order and field to index } model SocialMedia { - id String @id @default(cuid()) + id String @id @default(cuid()) userId String github String? twitter String? @@ -88,13 +88,14 @@ model Tag { model Forum { id String @id @default(cuid()) name String + slug String description String - ownerId String + ownerUserId String logo String? banner String? created_at DateTime @default(now()) updated_at DateTime @updatedAt - owner User @relation("UserOwnedForums", fields: [ownerId], references: [id]) + owner User @relation("UserOwnedForums", fields: [ownerUserId], references: [id]) posts Post[] @relation("ForumPosts") postsCount Int @default(0) //ADDED viewsCount Int @default(0) //ADDED @@ -104,53 +105,53 @@ model Forum { subscriptions ForumSubscription[] moderations ForumModerator[] - @@index([name, ownerId]) + @@index([name, ownerUserId]) } model Post { - id String @id @default(cuid()) + id String @id @default(cuid()) title String content String userId String forumId String - isPinned Boolean @default(false) - isHidden Boolean @default(false) + isPinned Boolean @default(false) + isVisible Boolean @default(true) slug String - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - user User @relation("UserPosts", fields: [userId], references: [id]) - forum Forum @relation("ForumPosts", fields: [forumId], references: [id]) - comments Comment[] @relation("PostComments") + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + user User @relation("UserPosts", fields: [userId], references: [id]) + forum Forum @relation("ForumPosts", fields: [forumId], references: [id]) + comments Comment[] @relation("PostComments") tags PostTag[] - reports Report[] @relation("ReportPost") - attachments Attachment[] @relation("AttachmentPost") - commentsCount Int @default(0) //ADDED - viewsCount Int @default(0) //ADDED - votesCount Int @default(0) //VOTE COUNT - votes PostVotes[] @relation("PostVotes") + reports Report[] @relation("ReportPost") + attachments PostAttachment[] @relation("PostAttachments") + commentsCount Int @default(0) //ADDED + viewsCount Int @default(0) //ADDED + votesCount Int @default(0) //VOTE COUNT + votes PostVotes[] @relation("PostVotes") @@index([title, userId, forumId, slug]) } model Comment { // when anyone post comment, it will send notification to the post owner and parrent post/comment - id String @id @default(cuid()) + id String @id @default(cuid()) content String userId String postId String - isHidden Boolean @default(false) - VotesCount Int @default(0) + isVisible Boolean @default(true) + VotesCount Int @default(0) parentId String? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt status String //DELETE I DONT THINK IT NEEDED - user User @relation("UserComments", fields: [userId], references: [id]) - post Post @relation("PostComments", fields: [postId], references: [id]) - parent Comment? @relation("CommentReplies", fields: [parentId], references: [id]) - replies Comment[] @relation("CommentReplies") - reports Report[] @relation("ReportComment") - attachments Attachment[] @relation("AttachmentComment") - votes CommentVotes[] @relation("CommentVotes") + user User @relation("UserComments", fields: [userId], references: [id]) + post Post @relation("PostComments", fields: [postId], references: [id]) + parent Comment? @relation("CommentReplies", fields: [parentId], references: [id]) + replies Comment[] @relation("CommentReplies") + reports Report[] @relation("ReportComment") + attachments CommentAttachment[] @relation("CommentAttachments") + votes CommentVotes[] @relation("CommentVotes") @@index([userId, postId]) } @@ -178,19 +179,34 @@ model CommentVotes { } model Attachment { - id String @id @default(cuid()) - name String - type String - postId String? - commentId String? - associated_type AssociatedType - url String - created_at DateTime @default(now()) - updated_at DateTime @updatedAt - post Post? @relation("AttachmentPost", fields: [postId], references: [id]) - comment Comment? @relation("AttachmentComment", fields: [commentId], references: [id]) + id String @id @default(cuid()) + name String + type String + url String + created_at DateTime @default(now()) + updated_at DateTime @updatedAt + postAttachments PostAttachment[] + commentAttachments CommentAttachment[] +} + +model PostAttachment { + id String @unique @default(cuid()) + postId String + attachmentId String + post Post @relation("PostAttachments", fields: [postId], references: [id]) + attachment Attachment @relation(fields: [attachmentId], references: [id]) + + @@id([postId, attachmentId]) +} + +model CommentAttachment { + id String @unique @default(cuid()) + commentId String + attachmentId String + comment Comment @relation("CommentAttachments", fields: [commentId], references: [id]) + attachment Attachment @relation(fields: [attachmentId], references: [id]) - @@index([postId, commentId, associated_type]) + @@id([commentId, attachmentId]) } model UserFollows { @@ -241,7 +257,6 @@ model Notification { @@index([notified_user, notifier_user, type]) } - model ForumModerator { user_id String forum_id String @@ -286,4 +301,3 @@ enum NotificationType { post follow } - From 3736135966d3a593ab9fc8d2e7ead8de39fcebac Mon Sep 17 00:00:00 2001 From: Mounssif BOUHLAOUI Date: Sat, 23 Mar 2024 17:55:54 +0000 Subject: [PATCH 002/164] chore(database): delete redundant database files and unused schema copy --- server/prisma/dev.db | Bin 32768 -> 0 bytes .../20240321041808_init/migration.sql | 9 - server/prisma/migrations/migration_lock.toml | 3 - server/prisma/schema copy.prisma | 303 ------------------ server/prisma/schema.prisma | 292 ++++++++++++++++- 5 files changed, 279 insertions(+), 328 deletions(-) delete mode 100644 server/prisma/dev.db delete mode 100644 server/prisma/migrations/20240321041808_init/migration.sql delete mode 100644 server/prisma/migrations/migration_lock.toml delete mode 100644 server/prisma/schema copy.prisma diff --git a/server/prisma/dev.db b/server/prisma/dev.db deleted file mode 100644 index 5dd4ad836a554fdaa3c81d7cb152d79e7bf038e8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 32768 zcmeI)OK;*v00(e;lK`PX^b&DeY2-^ocT?Gz@K7&hVG_|2lK@sXy;#N!8!NolHriHu z%C_pUJ+`Mk?Kh};=ohK_8G5g_$Ici-fJD2MwyKouf5a$w-t(K8L?#yP(}o)`(jR+6 zJ0QCBp(M-FXM{+Sw9R9J#~6=t^nR27oClHiwy-UI(*HA+I+9|^S5oTt^e;PqeQ=t5 zwevFh^V_N-9|91700bZa0SG|geG=GtDgDOdubKPz<;#!b@yzb7{Gu4x-2pRw=82f7 zwrHh8NvHCtL5U)g6tb5jJSc8YA$6lek7$b+%?>flMuVKR>c^GV8To>qk&4-A)(zgm zF*Q05XO-B{b_WXS(8s(&R4K}u*uMX4?D1A_${pE5Hp@w7Uwj@52m9T#aIhz1KM*m# zI#`kPZi5ZnU=Ve>K3Hcw3fPFZFUC5bz_dGX{d2}=@Tl2r(27B7^sr(!I;3w8d=?e6 zr@{Hyt6%BO(yL9Q-Dy?0C{e`Ft$8`Cf5E;{$YHZZ>qkZ?yQj=El`Lt|L)xN7m9`fP zB-SQNnxg*(<;&Nm(em&l^Q0zLv|4FbD>a(Tnu%B@yDL9;M?LnnSYRt$Ba3$`t~Mi7 z)2N@ClrMOVKA!b+RZbCI%{IrLGF##7$!09`;DLM=u8{w9!0&)8pFN$jk;B$5w^r6| zEptyS{6k(doBmt+QEbu6=PO%uKEVHDi!N`%Z@-e`8BLSFx5D9A6VLUBwl#F0cy{29 zNB;Gcdn*%Ft`}I@uHulbh45f3AF;I~E|>e|l5a<6M$`ho2Qyx8iv$h_3zATG|9 zGxa=v6s^D=GG2K&nV+-CWZ?3K{D4h-%Nb9_aV3jiS*FqE$BEXiH>f7F)qAnbmzpev zha%dM{K$>NYwP;LS=#lq4gR-8+}lej9_92z;x7aUKmY;|fB*y_009U<00Izz00jOc zfyC z{QqrVMT{Q;5P$##AOHafKmY;|fB*y_@MZzr|G(J>xe$N=1Rwwb2tWV=5P$##AOL~8 GAn*? Date: Sat, 23 Mar 2024 21:21:34 +0000 Subject: [PATCH 003/164] feat: initial boilerplate from other branch and setup auth with clerk --- client/components.json | 17 ++ client/next.config.mjs | 11 +- client/package.json | 26 ++- client/src/app/globals.css | 89 +++++++--- client/src/app/layout.tsx | 47 ++++-- client/src/app/page.tsx | 104 ------------ client/src/components/core/header.tsx | 130 +++++++++++++++ client/src/components/core/recent-posts.tsx | 13 ++ client/src/components/core/side-bar.tsx | 52 ++++++ client/src/components/posts/posts.tsx | 5 + client/src/components/ui/button.tsx | 56 +++++++ client/src/components/ui/collapsible.tsx | 11 ++ client/src/components/ui/form.tsx | 176 ++++++++++++++++++++ client/src/components/ui/input.tsx | 24 +++ client/src/components/ui/label.tsx | 26 +++ client/src/components/ui/separator.tsx | 31 ++++ client/src/components/ui/sheet.tsx | 140 ++++++++++++++++ client/src/lib/utils.ts | 6 + client/src/middleware.ts | 25 +++ client/tailwind.config.ts | 86 ++++++++-- 20 files changed, 915 insertions(+), 160 deletions(-) create mode 100644 client/components.json create mode 100644 client/src/components/core/header.tsx create mode 100644 client/src/components/core/recent-posts.tsx create mode 100644 client/src/components/core/side-bar.tsx create mode 100644 client/src/components/posts/posts.tsx create mode 100644 client/src/components/ui/button.tsx create mode 100644 client/src/components/ui/collapsible.tsx create mode 100644 client/src/components/ui/form.tsx create mode 100644 client/src/components/ui/input.tsx create mode 100644 client/src/components/ui/label.tsx create mode 100644 client/src/components/ui/separator.tsx create mode 100644 client/src/components/ui/sheet.tsx create mode 100644 client/src/lib/utils.ts create mode 100644 client/src/middleware.ts diff --git a/client/components.json b/client/components.json new file mode 100644 index 0000000..597fc69 --- /dev/null +++ b/client/components.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "default", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "tailwind.config.ts", + "css": "src/app/globals.css", + "baseColor": "slate", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "~/components", + "utils": "~/lib/utils" + } +} \ No newline at end of file diff --git a/client/next.config.mjs b/client/next.config.mjs index 4678774..b78abe2 100644 --- a/client/next.config.mjs +++ b/client/next.config.mjs @@ -1,4 +1,13 @@ /** @type {import('next').NextConfig} */ -const nextConfig = {}; +const nextConfig = { + images: { + remotePatterns: [ + { + protocol: 'https', + hostname: 'ui-avatars.com', + } + ] + } +}; export default nextConfig; diff --git a/client/package.json b/client/package.json index c6c38a6..8bb0a01 100644 --- a/client/package.json +++ b/client/package.json @@ -9,19 +9,33 @@ "lint": "next lint" }, "dependencies": { + "@clerk/clerk-react": "^4.16.3", + "@clerk/nextjs": "^4.29.9", + "@radix-ui/react-collapsible": "^1.0.3", + "@radix-ui/react-dialog": "^1.0.5", + "@radix-ui/react-label": "^2.0.2", + "@radix-ui/react-separator": "^1.0.3", + "@radix-ui/react-slot": "^1.0.2", + "class-variance-authority": "^0.7.0", + "clsx": "^2.1.0", + "lucide-react": "^0.363.0", + "next": "14.1.4", "react": "^18", "react-dom": "^18", - "next": "14.1.4" + "react-icons": "^5.0.1", + "tailwind-merge": "^2.2.2", + "tailwindcss-animate": "^1.0.7", + "zod": "^3.22.4" }, "devDependencies": { - "typescript": "^5", - "@types/node": "^20", - "@types/react": "^18", + "@types/node": "^20.11.30", + "@types/react": "^18.2.67", "@types/react-dom": "^18", "autoprefixer": "^10.0.1", + "eslint": "^8", + "eslint-config-next": "14.1.4", "postcss": "^8", "tailwindcss": "^3.3.0", - "eslint": "^8", - "eslint-config-next": "14.1.4" + "typescript": "^5.4.3" } } diff --git a/client/src/app/globals.css b/client/src/app/globals.css index 875c01e..7d2d81f 100644 --- a/client/src/app/globals.css +++ b/client/src/app/globals.css @@ -2,32 +2,75 @@ @tailwind components; @tailwind utilities; -:root { - --foreground-rgb: 0, 0, 0; - --background-start-rgb: 214, 219, 220; - --background-end-rgb: 255, 255, 255; -} +/*--primary: 45.882 100% 55.9%;*/ +/*--secondary: 211.578 16.4% 47.1%;*/ + + +@layer base { + :root { + --background: 0 0% 100%; + --foreground: 20 14.3% 4.1%; + --card: 0 0% 100%; + --card-foreground: 20 14.3% 4.1%; + --popover: 0 0% 100%; + --popover-foreground: 20 14.3% 4.1%; + --primary: 45.882 100% 55.9%; + --primary-foreground: 26 83.3% 14.1%; + --secondary: 211.578 16.4% 47.1%; + --secondary-foreground: 24 9.8% 10%; + --muted: 60 4.8% 95.9%; + --muted-foreground: 25 5.3% 44.7%; + --accent: 60 4.8% 95.9%; + --accent-foreground: 24 9.8% 10%; + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 60 9.1% 97.8%; + --border: 20 5.9% 90%; + --input: 20 5.9% 90%; + --ring: 20 14.3% 4.1%; + --radius: 0.5rem; + } -@media (prefers-color-scheme: dark) { - :root { - --foreground-rgb: 255, 255, 255; - --background-start-rgb: 0, 0, 0; - --background-end-rgb: 0, 0, 0; - } + .dark { + --background: 20 14.3% 4.1%; + --foreground: 60 9.1% 97.8%; + --card: 20 14.3% 4.1%; + --card-foreground: 60 9.1% 97.8%; + --popover: 20 14.3% 4.1%; + --popover-foreground: 60 9.1% 97.8%; + --primary: 47.9 95.8% 53.1%; + --primary-foreground: 26 83.3% 14.1%; + --secondary: 12 6.5% 15.1%; + --secondary-foreground: 60 9.1% 97.8%; + --muted: 12 6.5% 15.1%; + --muted-foreground: 24 5.4% 63.9%; + --accent: 12 6.5% 15.1%; + --accent-foreground: 60 9.1% 97.8%; + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 60 9.1% 97.8%; + --border: 12 6.5% 15.1%; + --input: 12 6.5% 15.1%; + --ring: 35.5 91.7% 32.9%; + } } -body { - color: rgb(var(--foreground-rgb)); - background: linear-gradient( - to bottom, - transparent, - rgb(var(--background-end-rgb)) - ) - rgb(var(--background-start-rgb)); + +@layer base { + * { + @apply border-border; + } + + body { + @apply bg-background text-foreground; + } } -@layer utilities { - .text-balance { - text-wrap: balance; - } +::-webkit-scrollbar { + width: .60rem; + background-color: rgba(20, 20, 20, 0.301); + border-radius: 5rem; } + +::-webkit-scrollbar-thumb { + background: rgba(122, 128, 138, 0.8); + border-radius: .75rem; +} \ No newline at end of file diff --git a/client/src/app/layout.tsx b/client/src/app/layout.tsx index 3314e47..53860a3 100644 --- a/client/src/app/layout.tsx +++ b/client/src/app/layout.tsx @@ -1,22 +1,43 @@ -import type { Metadata } from "next"; -import { Inter } from "next/font/google"; +import type {Metadata} from "next"; +import {Inter} from "next/font/google"; +import Header from "~/components/core/header"; +import SideBar from "~/components/core/side-bar"; +import RecentPosts from "~/components/core/recent-posts"; import "./globals.css"; +import {Separator} from "~/components/ui/separator"; +import {ClerkProvider} from "@clerk/nextjs"; +import Posts from "~/components/posts/posts"; -const inter = Inter({ subsets: ["latin"] }); +const inter = Inter({subsets: ["latin"]}); export const metadata: Metadata = { - title: "Create Next App", - description: "Generated by create next app", + title: "Create Next App", + description: "Generated by create next app", }; export default function RootLayout({ - children, -}: Readonly<{ - children: React.ReactNode; + children, + }: Readonly<{ + children: React.ReactNode; }>) { - return ( - - {children} - - ); + return ( + + + +
+ +
+ +
+ {children} + +
+
+ +
+
+ + + + ); } diff --git a/client/src/app/page.tsx b/client/src/app/page.tsx index b81507d..6303b10 100644 --- a/client/src/app/page.tsx +++ b/client/src/app/page.tsx @@ -3,111 +3,7 @@ import Image from "next/image"; export default function Home() { return (
-
-

- Get started by editing  - src/app/page.tsx -

- -
-
- Next.js Logo -
- -
); } diff --git a/client/src/components/core/header.tsx b/client/src/components/core/header.tsx new file mode 100644 index 0000000..43bbce3 --- /dev/null +++ b/client/src/components/core/header.tsx @@ -0,0 +1,130 @@ +'use client' + +import {Input} from "~/components/ui/input" +import React, {useState} from "react"; +import {FaArrowDown, FaBars, FaPlus} from "react-icons/fa6"; +import {FaHome, FaSearch} from "react-icons/fa"; +import {FaRocket} from "react-icons/fa"; +import {Separator} from "~/components/ui/separator" +import { UserButton, useClerk } from "@clerk/nextjs"; + +// import {Button} from "~/components/ui/button"; +import { + Sheet, + SheetContent, +} from "~/components/ui/sheet" + +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from "~/components/ui/collapsible" + +import Link from "next/link"; +import {Button} from "~/components/ui/button"; +import Image from "next/image"; + + +export default function Header() { + const [searchValue, setSearchValue] = useState(''); + const [open, setOpen] = useState(false) + const {openSignIn, openSignUp} = useClerk() + + const handleChange = (event: React.ChangeEvent) => { + setSearchValue(event.target.value) + } + + const handleSearch: React.KeyboardEventHandler = (event) => { + if (event.key === 'Enter') { + console.log(searchValue) + setSearchValue('') + } + } + + const handleShow = () => { + setOpen(!open) + } + return ( + + ) +} diff --git a/client/src/components/core/recent-posts.tsx b/client/src/components/core/recent-posts.tsx new file mode 100644 index 0000000..18897f2 --- /dev/null +++ b/client/src/components/core/recent-posts.tsx @@ -0,0 +1,13 @@ +import {Separator} from "~/components/ui/separator"; +import React from "react"; + +export default function RecentPosts() { + return ( +
+ +
+

Recent Posts

+
+
+ ) +} \ No newline at end of file diff --git a/client/src/components/core/side-bar.tsx b/client/src/components/core/side-bar.tsx new file mode 100644 index 0000000..fad6238 --- /dev/null +++ b/client/src/components/core/side-bar.tsx @@ -0,0 +1,52 @@ +import React from "react"; +import Link from "next/link"; +import {FaArrowDown, FaPlus} from "react-icons/fa6"; +import {FaHome} from "react-icons/fa"; +import {FaRocket} from "react-icons/fa"; +import {Separator} from "~/components/ui/separator" +import {Button} from "~/components/ui/button"; +import {Collapsible, CollapsibleContent, CollapsibleTrigger} from "~/components/ui/collapsible"; + +export default function SideBar() { + return ( +
+
+
    +
  • + + Home +
  • +
  • + + Popular +
  • +
+ +
+ +
+ +
+ +
+

Channels

+ + + + +
+ + {/*TODO Top communities */} + Python + +
+
+
+ + +
+ + ) +} diff --git a/client/src/components/posts/posts.tsx b/client/src/components/posts/posts.tsx new file mode 100644 index 0000000..5008d5d --- /dev/null +++ b/client/src/components/posts/posts.tsx @@ -0,0 +1,5 @@ +export default function Posts() { + return (
+ {/* TODO Create a filter tab */} +
) +} \ No newline at end of file diff --git a/client/src/components/ui/button.tsx b/client/src/components/ui/button.tsx new file mode 100644 index 0000000..d754ca0 --- /dev/null +++ b/client/src/components/ui/button.tsx @@ -0,0 +1,56 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "~/lib/utils" + +const buttonVariants = cva( + "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", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground hover:bg-primary/90", + destructive: + "bg-destructive text-destructive-foreground hover:bg-destructive/90", + outline: + "border border-input bg-background hover:bg-accent hover:text-accent-foreground", + secondary: + "bg-secondary text-secondary-foreground hover:bg-secondary/80", + ghost: "hover:bg-accent hover:text-accent-foreground", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-10 px-4 py-2", + sm: "h-9 rounded-md px-3", + lg: "h-11 rounded-md px-8", + icon: "h-10 w-10", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean +} + +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button" + return ( + + ) + } +) +Button.displayName = "Button" + +export { Button, buttonVariants } diff --git a/client/src/components/ui/collapsible.tsx b/client/src/components/ui/collapsible.tsx new file mode 100644 index 0000000..9fa4894 --- /dev/null +++ b/client/src/components/ui/collapsible.tsx @@ -0,0 +1,11 @@ +"use client" + +import * as CollapsiblePrimitive from "@radix-ui/react-collapsible" + +const Collapsible = CollapsiblePrimitive.Root + +const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger + +const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent + +export { Collapsible, CollapsibleTrigger, CollapsibleContent } diff --git a/client/src/components/ui/form.tsx b/client/src/components/ui/form.tsx new file mode 100644 index 0000000..7b52676 --- /dev/null +++ b/client/src/components/ui/form.tsx @@ -0,0 +1,176 @@ +import * as React from "react" +import type * as LabelPrimitive from "@radix-ui/react-label" +import { Slot } from "@radix-ui/react-slot" +import { + Controller, + type ControllerProps, + type FieldPath, + type FieldValues, + FormProvider, + useFormContext, +} from "react-hook-form" + +import { cn } from "~/lib/utils" +import { Label } from "~/components/ui/label" + +const Form = FormProvider + +type FormFieldContextValue< + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath +> = { + name: TName +} + +const FormFieldContext = React.createContext( + {} as FormFieldContextValue +) + +const FormField = < + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath +>({ + ...props +}: ControllerProps) => { + return ( + + + + ) +} + +const useFormField = () => { + const fieldContext = React.useContext(FormFieldContext) + const itemContext = React.useContext(FormItemContext) + const { getFieldState, formState } = useFormContext() + + const fieldState = getFieldState(fieldContext.name, formState) + + if (!fieldContext) { + throw new Error("useFormField should be used within ") + } + + const { id } = itemContext + + return { + id, + name: fieldContext.name, + formItemId: `${id}-form-item`, + formDescriptionId: `${id}-form-item-description`, + formMessageId: `${id}-form-item-message`, + ...fieldState, + } +} + +type FormItemContextValue = { + id: string +} + +const FormItemContext = React.createContext( + {} as FormItemContextValue +) + +const FormItem = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => { + const id = React.useId() + + return ( + +
+ + ) +}) +FormItem.displayName = "FormItem" + +const FormLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + const { error, formItemId } = useFormField() + + return ( +