From 85ef7247ea330006d809ba9567495e792a172737 Mon Sep 17 00:00:00 2001 From: mr-shortman Date: Sat, 22 Mar 2025 09:20:12 +0100 Subject: [PATCH] implimentet comments ui and server side part --- .gitea/workflows/demo.yml | 11 -- src/app/(PAGES)/artikel/[slug]/page.tsx | 104 ++++++---------- src/components/comment/comment-form.tsx | 111 ++++++++++++++++++ src/components/comment/comment-list.tsx | 27 +++++ src/components/comment/comment-section.tsx | 46 ++++++++ .../comment/comment-vote-button.tsx | 23 ++++ src/components/comment/comment.tsx | 62 ++++++++++ src/components/editor/comment-editor.tsx | 87 ++++++++++++++ src/components/editor/index.tsx | 6 +- src/components/editor/menu/bubble-menu.tsx | 14 ++- src/components/editor/menu/menu-bar.tsx | 37 ++++-- .../editor/selector/selection-items.tsx | 6 +- src/components/editor/styles/editor.css | 49 ++++++++ .../editor/{styles.css => styles/shared.css} | 48 -------- src/components/ui/alert-dialog.tsx | 60 +++++----- src/components/ui/alert.tsx | 24 ++-- src/components/ui/avatar.tsx | 26 ++-- src/components/ui/badge.tsx | 14 +-- src/components/ui/breadcrumb.tsx | 50 ++++---- src/components/ui/button.tsx | 26 ++-- src/components/ui/checkbox.tsx | 18 +-- src/components/ui/command.tsx | 66 +++++------ src/components/ui/dialog.tsx | 56 ++++----- src/components/ui/dropdown-menu.tsx | 82 ++++++------- src/components/ui/form.tsx | 103 ++++++++-------- src/components/ui/input.tsx | 16 +-- src/components/ui/label.tsx | 20 ++-- src/components/ui/popover.tsx | 22 ++-- src/components/ui/select.tsx | 60 +++++----- src/components/ui/separator.tsx | 20 ++-- src/components/ui/sheet.tsx | 58 ++++----- src/components/ui/skeleton.tsx | 6 +- src/components/ui/table.tsx | 46 ++++---- src/components/ui/textarea.tsx | 14 +-- src/components/ui/tooltip.tsx | 22 ++-- src/lib/utils/comments.ts | 46 ++++++++ src/lib/{utils.ts => utils/index.ts} | 0 src/server/api/routers/article.ts | 4 +- src/server/api/trpc.ts | 12 +- src/server/db/schema/article.ts | 3 +- src/server/db/schema/comments.ts | 20 +++- tailwind.config.ts | 3 + 42 files changed, 974 insertions(+), 554 deletions(-) delete mode 100644 .gitea/workflows/demo.yml create mode 100644 src/components/comment/comment-form.tsx create mode 100644 src/components/comment/comment-list.tsx create mode 100644 src/components/comment/comment-section.tsx create mode 100644 src/components/comment/comment-vote-button.tsx create mode 100644 src/components/comment/comment.tsx create mode 100644 src/components/editor/comment-editor.tsx create mode 100644 src/components/editor/styles/editor.css rename src/components/editor/{styles.css => styles/shared.css} (77%) create mode 100644 src/lib/utils/comments.ts rename src/lib/{utils.ts => utils/index.ts} (100%) diff --git a/.gitea/workflows/demo.yml b/.gitea/workflows/demo.yml deleted file mode 100644 index d50fbe7..0000000 --- a/.gitea/workflows/demo.yml +++ /dev/null @@ -1,11 +0,0 @@ -name: Gitea Actions Demo -run-name: ${{ gitea.actor }} is testing out Gitea Actions 🚀 -on: [push] - -jobs: - Explore-Gitea-Actions: - runs-on: linux - steps: - - run: echo "🎉 The job was automatically triggered by a ${{ gitea.event_name }} event." - - run: ssh - - run: echo "🍏 This job's status is ${{ gitea.job_status }}." diff --git a/src/app/(PAGES)/artikel/[slug]/page.tsx b/src/app/(PAGES)/artikel/[slug]/page.tsx index 5a64450..784b021 100644 --- a/src/app/(PAGES)/artikel/[slug]/page.tsx +++ b/src/app/(PAGES)/artikel/[slug]/page.tsx @@ -1,88 +1,60 @@ -import BreadNavigator from "@/components/bread-navigator"; +import React from "react"; +import { notFound } from "next/navigation"; +import { auth } from "@/server/auth"; +import { api } from "@/trpc/server"; +import { hasPermission, Role } from "@/lib/validation/permissions"; +import { appRoutes } from "@/config"; + +import Link from "next/link"; import RenderContent from "@/components/editor/render-content"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; -import { Separator } from "@/components/ui/separator"; -import { appRoutes } from "@/config"; -import { hasPermission, Role } from "@/lib/validation/permissions"; -import { auth } from "@/server/auth"; -import { api } from "@/trpc/server"; -import { Edit, Edit2Icon, Edit3, MoreVertical, Trash } from "lucide-react"; -import Link from "next/link"; -import { notFound } from "next/navigation"; -import React from "react"; +import { Edit, MoreVertical } from "lucide-react"; +import CommentSection from "@/components/comment/comment-section"; async function Page({ params }: { params: Promise<{ slug: string }> }) { const { slug } = await params; - const article = await api.article.get({ slug: slug }); + const article = await api.article.get({ + slug: slug, + with: { category: true, comments: { with: { author: true } } }, + }); if (!article) return notFound(); const session = await auth(); const isEditor = session?.user ? hasPermission(session.user.role, Role.EDITOR) : false; return ( -
- {/*
-
- +
+ {isEditor && ( +
+ + {article.published ? "Veröffentlicht" : "Draft"} + + +
+ + +
-
*/} -
+ )} +

{article.title}

{article.content && }
- -
-
-

- Kommentare bald verfügbar -

-
- {isEditor && ( -
- - {article.published ? "Veröffentlicht" : "Draft"} - - - - -
- )} -
+
); diff --git a/src/components/comment/comment-form.tsx b/src/components/comment/comment-form.tsx new file mode 100644 index 0000000..9f7593b --- /dev/null +++ b/src/components/comment/comment-form.tsx @@ -0,0 +1,111 @@ +"use client"; +import React from "react"; +import { Comment } from "@/server/db/schema"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; + +import { Button } from "@/components/ui/button"; +import { + Form, + FormControl, + FormField, + FormItem, + FormMessage, +} from "@/components/ui/form"; +import { commentSchema } from "@/lib/validation/zod/comment"; +import CommentEditor from "../editor/comment-editor"; +import { createComment } from "@/server/actions/comment"; +import { toast } from "sonner"; +import { EditorInstance } from "novel"; +import { XIcon } from "lucide-react"; + +function CommentForm({ + articleId, + parentComment, + removeReplyComment, +}: { + articleId: string; + parentComment?: Comment; + removeReplyComment: () => void; +}) { + const form = useForm>({ + resolver: zodResolver(commentSchema), + defaultValues: { + parentId: parentComment?.id ?? "", + content: undefined, + }, + }); + const editorRef = React.useRef(null); + const editor = editorRef.current; + async function onSubmit(values: z.infer) { + const { success } = await createComment( + { + ...values, + parentId: parentComment?.id?.length ? parentComment.id : undefined, + }, + articleId, + ); + if (!success) toast.error("Etwas ist schief gelaufen."); + else toast.success("Dein Kommentar wurde hinzugefügt."); + form.reset(); + editor?.commands?.clearContent(); + removeReplyComment(); + } + + React.useEffect(() => { + if (parentComment) editor?.commands?.focus(); + }, [parentComment]); + + return ( + <> + {parentComment && ( +
+ +

answer comment

+
+ )} +
+ + ( + + + { + editorRef.current = editor; + }} + initialContent={field.value} + onContentChange={field.onChange} + > + + + + + + )} + /> + + + + ); +} + +export default CommentForm; diff --git a/src/components/comment/comment-list.tsx b/src/components/comment/comment-list.tsx new file mode 100644 index 0000000..5297ce5 --- /dev/null +++ b/src/components/comment/comment-list.tsx @@ -0,0 +1,27 @@ +import { CommentNode, Comment as CommentType } from "@/server/db/schema"; +import React from "react"; +import { buildCommentTree } from "@/lib/utils/comments"; +import Comment from "./comment"; + +function CommentList({ + comments, + setReplyComment, +}: { + comments: Array; + setReplyComment: (comment: CommentNode) => void; +}) { + const commentTree = buildCommentTree(comments); + return ( +
    + {commentTree.map((comment) => ( + + ))} +
+ ); +} + +export default CommentList; diff --git a/src/components/comment/comment-section.tsx b/src/components/comment/comment-section.tsx new file mode 100644 index 0000000..dc9d977 --- /dev/null +++ b/src/components/comment/comment-section.tsx @@ -0,0 +1,46 @@ +"use client"; +import React from "react"; +import CommentForm from "@/components/comment/comment-form"; +import CommentList from "@/components/comment/comment-list"; +import { Comment, CommentNode } from "@/server/db/schema"; +import { Badge } from "../ui/badge"; +import { Button } from "../ui/button"; + +function CommentSection({ + articleId, + comments, +}: { + articleId: string; + comments?: Array; +}) { + const [replyComment, setReplyComment] = React.useState< + CommentNode | undefined + >(); + return ( +
+
+ setReplyComment(undefined)} + /> +
+ +
+
+

Kommentare

+ {comments?.length} +
+ +
+ + {comments && ( + + )} +
+ ); +} + +export default CommentSection; diff --git a/src/components/comment/comment-vote-button.tsx b/src/components/comment/comment-vote-button.tsx new file mode 100644 index 0000000..65140ae --- /dev/null +++ b/src/components/comment/comment-vote-button.tsx @@ -0,0 +1,23 @@ +import React from "react"; +import { Button } from "../ui/button"; +import { ThumbsDown, ThumbsUp } from "lucide-react"; + +function CommentVoteButton({ + vote, + commentId, +}: { + vote: boolean; + commentId: string; +}) { + return ( + + ); +} + +export default CommentVoteButton; diff --git a/src/components/comment/comment.tsx b/src/components/comment/comment.tsx new file mode 100644 index 0000000..fcdd973 --- /dev/null +++ b/src/components/comment/comment.tsx @@ -0,0 +1,62 @@ +import React from "react"; +import { CommentNode } from "@/server/db/schema"; +import Avatar from "../avatar"; +import { formatCommentDate } from "@/lib/utils/comments"; +import { ArrowDown, ArrowUp, ThumbsDown, ThumbsUp } from "lucide-react"; +import { Button } from "../ui/button"; +import CommentEditor from "../editor/comment-editor"; +import CommentVoteButton from "./comment-vote-button"; + +const Comment = ({ + comment, + setReplyComment, +}: { + comment: CommentNode; + setReplyComment: (comment: CommentNode) => void; +}) => { + return ( +
+
+
+ +
+
+
+ {comment.author?.name} +
+ + {formatCommentDate(comment.createdAt)} + +
+ +
+ + + +
+
+
+
+ {comment.children.map((child) => ( + + ))} +
+
+ ); +}; + +export default Comment; diff --git a/src/components/editor/comment-editor.tsx b/src/components/editor/comment-editor.tsx new file mode 100644 index 0000000..cfd381e --- /dev/null +++ b/src/components/editor/comment-editor.tsx @@ -0,0 +1,87 @@ +"use client"; +import "./styles/shared.css"; +import { + EditorContent, + EditorInstance, + EditorRoot, + JSONContent, + Placeholder, + StarterKit, + TiptapUnderline, +} from "novel"; + +import { MenuBar } from "./menu/menu-bar"; + +const extentions = [ + TiptapUnderline, + StarterKit.configure({ + blockquote: false, + bulletList: false, + orderedList: false, + code: false, + codeBlock: false, + horizontalRule: false, + listItem: false, + + heading: { + levels: [4, 5], + }, + }), + Placeholder.configure({ + placeholder: "Schreibe einen Kommentar …", + emptyNodeClass: + "first:before:absolute first:before:text-gray-400 first:before:float-left first:before:content-[attr(data-placeholder)] first:before:pointer-events-none", + }), +]; + +const CommentEditor = ({ + children, + initialContent, + readOnly, + onContentChange, + setEditor, +}: { + initialContent?: JSONContent; + onContentChange?: (content: JSONContent) => void; + readOnly?: boolean; + children?: React.ReactNode; + setEditor?: (editor: EditorInstance) => void; +}) => { + const slotAfter = ( +
+ {!readOnly && ( + + )} + {children} +
+ ); + + return ( + + { + setEditor?.(editor); + }} + onUpdate={({ editor }) => { + onContentChange?.(editor.getJSON()); + }} + /> + + ); +}; +export default CommentEditor; diff --git a/src/components/editor/index.tsx b/src/components/editor/index.tsx index 2777e51..e60bcaf 100644 --- a/src/components/editor/index.tsx +++ b/src/components/editor/index.tsx @@ -1,5 +1,5 @@ "use client"; -import "./styles.css"; +import "./styles/editor.css"; import { EditorContent, EditorRoot, @@ -23,9 +23,13 @@ const Editor = ({ return ( } extensions={defaultExtensions} editorProps={{ + attributes: { + class: "min-h-screen w-full", + }, handleDOMEvents: { keydown: (_view, event) => handleCommandNavigation(event), }, diff --git a/src/components/editor/menu/bubble-menu.tsx b/src/components/editor/menu/bubble-menu.tsx index 13b9abb..c3a6858 100644 --- a/src/components/editor/menu/bubble-menu.tsx +++ b/src/components/editor/menu/bubble-menu.tsx @@ -7,7 +7,11 @@ import { LinkSelector } from "../selector/link-selector"; import { TextButtons } from "../selector/text-buttont"; import { ColorSelector } from "../selector/color-selector"; -const BubbleMenu = () => { +const BubbleMenu = ({ + hideNodeSelector = false, +}: { + hideNodeSelector?: boolean; +}) => { const { editor } = useEditor(); const [openNode, setOpenNode] = useState(false); const [openColor, setOpenColor] = useState(false); @@ -27,8 +31,12 @@ const BubbleMenu = () => { > {!open && ( - - + {!hideNodeSelector && ( + <> + + + + )} diff --git a/src/components/editor/menu/menu-bar.tsx b/src/components/editor/menu/menu-bar.tsx index ec74b6b..8245b56 100644 --- a/src/components/editor/menu/menu-bar.tsx +++ b/src/components/editor/menu/menu-bar.tsx @@ -1,26 +1,47 @@ -import { Button } from "@/components/ui/button"; +import { Button, ButtonProps } from "@/components/ui/button"; import { useEditor } from "novel"; import { RedoIcon, UndoIcon } from "lucide-react"; -import { selectionItems } from "../selector/selection-items"; +import { + inlineSelectorItems, + selectionItems, +} from "../selector/selection-items"; +import { cn } from "@/lib/utils"; -export const MenuBar = () => { +export const MenuBar = ({ + onlyInline = false, + className, + buttonProps, +}: { + onlyInline?: boolean; + className?: string; + buttonProps?: ButtonProps; +}) => { const { editor } = useEditor(); if (!editor) { return null; } - + const items = onlyInline ? inlineSelectorItems : selectionItems; return ( -
+
- {selectionItems.map((item) => ( + {items.map((item) => ( @@ -32,6 +53,7 @@ export const MenuBar = () => { size={"icon"} onClick={() => editor.chain().focus().undo().run()} disabled={!editor.can().chain().focus().undo().run()} + {...buttonProps} > @@ -40,6 +62,7 @@ export const MenuBar = () => { size={"icon"} onClick={() => editor.chain().focus().redo().run()} disabled={!editor.can().chain().focus().redo().run()} + {...buttonProps} > diff --git a/src/components/editor/selector/selection-items.tsx b/src/components/editor/selector/selection-items.tsx index 004ff60..eee4a16 100644 --- a/src/components/editor/selector/selection-items.tsx +++ b/src/components/editor/selector/selection-items.tsx @@ -26,7 +26,7 @@ export type SelectorItem = { canRun?: (editor: EditorInstance) => boolean; }; -export const selectionItems: SelectorItem[] = [ +export const inlineSelectorItems: SelectorItem[] = [ { name: "bold", isActive: (editor) => editor.isActive("bold"), @@ -59,6 +59,10 @@ export const selectionItems: SelectorItem[] = [ inline: true, canRun: (editor) => !editor.can().chain().focus().toggleStrike().run(), }, +]; + +export const selectionItems: SelectorItem[] = [ + ...inlineSelectorItems, // blocks { diff --git a/src/components/editor/styles/editor.css b/src/components/editor/styles/editor.css new file mode 100644 index 0000000..6c1fecb --- /dev/null +++ b/src/components/editor/styles/editor.css @@ -0,0 +1,49 @@ +@import url("./shared.css"); + +.ProseMirror { + /* min-height: 100vh; + width: 100%; */ +} + +.tiptap ul li p, +.tiptap ol li p { + margin: 0.75rem 0; +} + +ul[data-type="taskList"] li > label input[type="checkbox"] { + @apply size-4 border border-border bg-muted; + -webkit-appearance: none; + appearance: none; + margin: 0; + margin-bottom: 0.75rem; + margin-top: 0.75rem; + cursor: pointer; + position: relative; + transform: translateY(0.17rem); + display: grid; + border-radius: 0.25rem; + place-content: center; +} + +ul[data-type="taskList"] li > label input[type="checkbox"]::before { + content: ""; + width: 0.65em; + height: 0.65em; + transform: scale(0); + transition: 120ms transform ease-in-out; + box-shadow: inset 1em 1em; + transform-origin: center; + clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%); +} +ul[data-type="taskList"] li > label input[type="checkbox"]:checked::before { + transform: scale(1); +} +ul[data-type="taskList"] li[data-checked="true"] > div > p { + @apply text-foreground/75; + text-decoration: line-through; + text-decoration-thickness: 2px; +} + +img { + @apply rounded-md; +} diff --git a/src/components/editor/styles.css b/src/components/editor/styles/shared.css similarity index 77% rename from src/components/editor/styles.css rename to src/components/editor/styles/shared.css index 1b998b4..eb44195 100644 --- a/src/components/editor/styles.css +++ b/src/components/editor/styles/shared.css @@ -3,54 +3,10 @@ margin-top: 0; } -.ProseMirror { - min-height: 100vh; - width: 100%; -} - .ProseMirror:focus { outline: none; } -.tiptap ul li p, -.tiptap ol li p { - margin: 0.75rem 0; -} - -ul[data-type="taskList"] li > label input[type="checkbox"] { - @apply size-4 border border-border bg-muted; - -webkit-appearance: none; - appearance: none; - margin: 0; - margin-bottom: 0.75rem; - margin-top: 0.75rem; - cursor: pointer; - position: relative; - transform: translateY(0.17rem); - display: grid; - border-radius: 0.25rem; - place-content: center; -} - -ul[data-type="taskList"] li > label input[type="checkbox"]::before { - content: ""; - width: 0.65em; - height: 0.65em; - transform: scale(0); - transition: 120ms transform ease-in-out; - box-shadow: inset 1em 1em; - transform-origin: center; - clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%); -} -ul[data-type="taskList"] li > label input[type="checkbox"]:checked::before { - transform: scale(1); -} -ul[data-type="taskList"] li[data-checked="true"] > div > p { - @apply text-foreground/75; - text-decoration: line-through; - text-decoration-thickness: 2px; -} - /* Heading styles */ .tiptap h1, .tiptap h2, @@ -97,10 +53,6 @@ ul[data-type="taskList"] li[data-checked="true"] > div > p { transition-duration: 0s; } -img { - @apply rounded-md; -} - .ProseMirror:not(.dragging) .ProseMirror-selectednode { outline: none !important; background-color: var(--novel-highlight-blue); diff --git a/src/components/ui/alert-dialog.tsx b/src/components/ui/alert-dialog.tsx index 57760f2..f69596f 100644 --- a/src/components/ui/alert-dialog.tsx +++ b/src/components/ui/alert-dialog.tsx @@ -1,16 +1,16 @@ -"use client" +"use client"; -import * as React from "react" -import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog" +import * as React from "react"; +import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"; -import { cn } from "@/lib/utils" -import { buttonVariants } from "@/components/ui/button" +import { cn } from "@/lib/utils"; +import { buttonVariants } from "@/components/ui/button"; -const AlertDialog = AlertDialogPrimitive.Root +const AlertDialog = AlertDialogPrimitive.Root; -const AlertDialogTrigger = AlertDialogPrimitive.Trigger +const AlertDialogTrigger = AlertDialogPrimitive.Trigger; -const AlertDialogPortal = AlertDialogPrimitive.Portal +const AlertDialogPortal = AlertDialogPrimitive.Portal; const AlertDialogOverlay = React.forwardRef< React.ElementRef, @@ -19,13 +19,13 @@ const AlertDialogOverlay = React.forwardRef< -)) -AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName +)); +AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName; const AlertDialogContent = React.forwardRef< React.ElementRef, @@ -37,13 +37,13 @@ const AlertDialogContent = React.forwardRef< ref={ref} className={cn( "fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg", - className + className, )} {...props} /> -)) -AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName +)); +AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName; const AlertDialogHeader = ({ className, @@ -52,12 +52,12 @@ const AlertDialogHeader = ({
-) -AlertDialogHeader.displayName = "AlertDialogHeader" +); +AlertDialogHeader.displayName = "AlertDialogHeader"; const AlertDialogFooter = ({ className, @@ -66,12 +66,12 @@ const AlertDialogFooter = ({
-) -AlertDialogFooter.displayName = "AlertDialogFooter" +); +AlertDialogFooter.displayName = "AlertDialogFooter"; const AlertDialogTitle = React.forwardRef< React.ElementRef, @@ -82,8 +82,8 @@ const AlertDialogTitle = React.forwardRef< className={cn("text-lg font-semibold", className)} {...props} /> -)) -AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName +)); +AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName; const AlertDialogDescription = React.forwardRef< React.ElementRef, @@ -94,9 +94,9 @@ const AlertDialogDescription = React.forwardRef< className={cn("text-sm text-muted-foreground", className)} {...props} /> -)) +)); AlertDialogDescription.displayName = - AlertDialogPrimitive.Description.displayName + AlertDialogPrimitive.Description.displayName; const AlertDialogAction = React.forwardRef< React.ElementRef, @@ -107,8 +107,8 @@ const AlertDialogAction = React.forwardRef< className={cn(buttonVariants(), className)} {...props} /> -)) -AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName +)); +AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName; const AlertDialogCancel = React.forwardRef< React.ElementRef, @@ -119,12 +119,12 @@ const AlertDialogCancel = React.forwardRef< className={cn( buttonVariants({ variant: "outline" }), "mt-2 sm:mt-0", - className + className, )} {...props} /> -)) -AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName +)); +AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName; export { AlertDialog, @@ -138,4 +138,4 @@ export { AlertDialogDescription, AlertDialogAction, AlertDialogCancel, -} +}; diff --git a/src/components/ui/alert.tsx b/src/components/ui/alert.tsx index 5afd41d..1df0436 100644 --- a/src/components/ui/alert.tsx +++ b/src/components/ui/alert.tsx @@ -1,7 +1,7 @@ -import * as React from "react" -import { cva, type VariantProps } from "class-variance-authority" +import * as React from "react"; +import { cva, type VariantProps } from "class-variance-authority"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; const alertVariants = cva( "relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7", @@ -16,8 +16,8 @@ const alertVariants = cva( defaultVariants: { variant: "default", }, - } -) + }, +); const Alert = React.forwardRef< HTMLDivElement, @@ -29,8 +29,8 @@ const Alert = React.forwardRef< className={cn(alertVariants({ variant }), className)} {...props} /> -)) -Alert.displayName = "Alert" +)); +Alert.displayName = "Alert"; const AlertTitle = React.forwardRef< HTMLParagraphElement, @@ -41,8 +41,8 @@ const AlertTitle = React.forwardRef< className={cn("mb-1 font-medium leading-none tracking-tight", className)} {...props} /> -)) -AlertTitle.displayName = "AlertTitle" +)); +AlertTitle.displayName = "AlertTitle"; const AlertDescription = React.forwardRef< HTMLParagraphElement, @@ -53,7 +53,7 @@ const AlertDescription = React.forwardRef< className={cn("text-sm [&_p]:leading-relaxed", className)} {...props} /> -)) -AlertDescription.displayName = "AlertDescription" +)); +AlertDescription.displayName = "AlertDescription"; -export { Alert, AlertTitle, AlertDescription } +export { Alert, AlertTitle, AlertDescription }; diff --git a/src/components/ui/avatar.tsx b/src/components/ui/avatar.tsx index 51e507b..09cd14d 100644 --- a/src/components/ui/avatar.tsx +++ b/src/components/ui/avatar.tsx @@ -1,9 +1,9 @@ -"use client" +"use client"; -import * as React from "react" -import * as AvatarPrimitive from "@radix-ui/react-avatar" +import * as React from "react"; +import * as AvatarPrimitive from "@radix-ui/react-avatar"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; const Avatar = React.forwardRef< React.ElementRef, @@ -13,12 +13,12 @@ const Avatar = React.forwardRef< ref={ref} className={cn( "relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full", - className + className, )} {...props} /> -)) -Avatar.displayName = AvatarPrimitive.Root.displayName +)); +Avatar.displayName = AvatarPrimitive.Root.displayName; const AvatarImage = React.forwardRef< React.ElementRef, @@ -29,8 +29,8 @@ const AvatarImage = React.forwardRef< className={cn("aspect-square h-full w-full", className)} {...props} /> -)) -AvatarImage.displayName = AvatarPrimitive.Image.displayName +)); +AvatarImage.displayName = AvatarPrimitive.Image.displayName; const AvatarFallback = React.forwardRef< React.ElementRef, @@ -40,11 +40,11 @@ const AvatarFallback = React.forwardRef< ref={ref} className={cn( "flex h-full w-full items-center justify-center rounded-full bg-muted", - className + className, )} {...props} /> -)) -AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName +)); +AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName; -export { Avatar, AvatarImage, AvatarFallback } +export { Avatar, AvatarImage, AvatarFallback }; diff --git a/src/components/ui/badge.tsx b/src/components/ui/badge.tsx index e87d62b..f795980 100644 --- a/src/components/ui/badge.tsx +++ b/src/components/ui/badge.tsx @@ -1,7 +1,7 @@ -import * as React from "react" -import { cva, type VariantProps } from "class-variance-authority" +import * as React from "react"; +import { cva, type VariantProps } from "class-variance-authority"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; const badgeVariants = cva( "inline-flex items-center rounded-md 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", @@ -20,8 +20,8 @@ const badgeVariants = cva( defaultVariants: { variant: "default", }, - } -) + }, +); export interface BadgeProps extends React.HTMLAttributes, @@ -30,7 +30,7 @@ export interface BadgeProps function Badge({ className, variant, ...props }: BadgeProps) { return (
- ) + ); } -export { Badge, badgeVariants } +export { Badge, badgeVariants }; diff --git a/src/components/ui/breadcrumb.tsx b/src/components/ui/breadcrumb.tsx index 60e6c96..8614cd7 100644 --- a/src/components/ui/breadcrumb.tsx +++ b/src/components/ui/breadcrumb.tsx @@ -1,16 +1,16 @@ -import * as React from "react" -import { Slot } from "@radix-ui/react-slot" -import { ChevronRight, MoreHorizontal } from "lucide-react" +import * as React from "react"; +import { Slot } from "@radix-ui/react-slot"; +import { ChevronRight, MoreHorizontal } from "lucide-react"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; const Breadcrumb = React.forwardRef< HTMLElement, React.ComponentPropsWithoutRef<"nav"> & { - separator?: React.ReactNode + separator?: React.ReactNode; } ->(({ ...props }, ref) =>
-)) +)); -CommandInput.displayName = CommandPrimitive.Input.displayName +CommandInput.displayName = CommandPrimitive.Input.displayName; const CommandList = React.forwardRef< React.ElementRef, @@ -63,9 +63,9 @@ const CommandList = React.forwardRef< className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)} {...props} /> -)) +)); -CommandList.displayName = CommandPrimitive.List.displayName +CommandList.displayName = CommandPrimitive.List.displayName; const CommandEmpty = React.forwardRef< React.ElementRef, @@ -76,9 +76,9 @@ const CommandEmpty = React.forwardRef< className="py-6 text-center text-sm" {...props} /> -)) +)); -CommandEmpty.displayName = CommandPrimitive.Empty.displayName +CommandEmpty.displayName = CommandPrimitive.Empty.displayName; const CommandGroup = React.forwardRef< React.ElementRef, @@ -88,13 +88,13 @@ const CommandGroup = React.forwardRef< ref={ref} className={cn( "overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground", - className + className, )} {...props} /> -)) +)); -CommandGroup.displayName = CommandPrimitive.Group.displayName +CommandGroup.displayName = CommandPrimitive.Group.displayName; const CommandSeparator = React.forwardRef< React.ElementRef, @@ -105,8 +105,8 @@ const CommandSeparator = React.forwardRef< className={cn("-mx-1 h-px bg-border", className)} {...props} /> -)) -CommandSeparator.displayName = CommandPrimitive.Separator.displayName +)); +CommandSeparator.displayName = CommandPrimitive.Separator.displayName; const CommandItem = React.forwardRef< React.ElementRef, @@ -115,14 +115,14 @@ const CommandItem = React.forwardRef< -)) +)); -CommandItem.displayName = CommandPrimitive.Item.displayName +CommandItem.displayName = CommandPrimitive.Item.displayName; const CommandShortcut = ({ className, @@ -132,13 +132,13 @@ const CommandShortcut = ({ - ) -} -CommandShortcut.displayName = "CommandShortcut" + ); +}; +CommandShortcut.displayName = "CommandShortcut"; export { Command, @@ -150,4 +150,4 @@ export { CommandItem, CommandShortcut, CommandSeparator, -} +}; diff --git a/src/components/ui/dialog.tsx b/src/components/ui/dialog.tsx index 1647513..9e25b54 100644 --- a/src/components/ui/dialog.tsx +++ b/src/components/ui/dialog.tsx @@ -1,18 +1,18 @@ -"use client" +"use client"; -import * as React from "react" -import * as DialogPrimitive from "@radix-ui/react-dialog" -import { X } from "lucide-react" +import * as React from "react"; +import * as DialogPrimitive from "@radix-ui/react-dialog"; +import { X } from "lucide-react"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; -const Dialog = DialogPrimitive.Root +const Dialog = DialogPrimitive.Root; -const DialogTrigger = DialogPrimitive.Trigger +const DialogTrigger = DialogPrimitive.Trigger; -const DialogPortal = DialogPrimitive.Portal +const DialogPortal = DialogPrimitive.Portal; -const DialogClose = DialogPrimitive.Close +const DialogClose = DialogPrimitive.Close; const DialogOverlay = React.forwardRef< React.ElementRef, @@ -21,13 +21,13 @@ const DialogOverlay = React.forwardRef< -)) -DialogOverlay.displayName = DialogPrimitive.Overlay.displayName +)); +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; const DialogContent = React.forwardRef< React.ElementRef, @@ -39,7 +39,7 @@ const DialogContent = React.forwardRef< ref={ref} className={cn( "fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg", - className + className, )} {...props} > @@ -50,8 +50,8 @@ const DialogContent = React.forwardRef< -)) -DialogContent.displayName = DialogPrimitive.Content.displayName +)); +DialogContent.displayName = DialogPrimitive.Content.displayName; const DialogHeader = ({ className, @@ -60,12 +60,12 @@ const DialogHeader = ({
-) -DialogHeader.displayName = "DialogHeader" +); +DialogHeader.displayName = "DialogHeader"; const DialogFooter = ({ className, @@ -74,12 +74,12 @@ const DialogFooter = ({
-) -DialogFooter.displayName = "DialogFooter" +); +DialogFooter.displayName = "DialogFooter"; const DialogTitle = React.forwardRef< React.ElementRef, @@ -89,12 +89,12 @@ const DialogTitle = React.forwardRef< ref={ref} className={cn( "text-lg font-semibold leading-none tracking-tight", - className + className, )} {...props} /> -)) -DialogTitle.displayName = DialogPrimitive.Title.displayName +)); +DialogTitle.displayName = DialogPrimitive.Title.displayName; const DialogDescription = React.forwardRef< React.ElementRef, @@ -105,8 +105,8 @@ const DialogDescription = React.forwardRef< className={cn("text-sm text-muted-foreground", className)} {...props} /> -)) -DialogDescription.displayName = DialogPrimitive.Description.displayName +)); +DialogDescription.displayName = DialogPrimitive.Description.displayName; export { Dialog, @@ -119,4 +119,4 @@ export { DialogFooter, DialogTitle, DialogDescription, -} +}; diff --git a/src/components/ui/dropdown-menu.tsx b/src/components/ui/dropdown-menu.tsx index 1154fb9..ec350fd 100644 --- a/src/components/ui/dropdown-menu.tsx +++ b/src/components/ui/dropdown-menu.tsx @@ -1,27 +1,27 @@ -"use client" +"use client"; -import * as React from "react" -import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" -import { Check, ChevronRight, Circle } from "lucide-react" +import * as React from "react"; +import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"; +import { Check, ChevronRight, Circle } from "lucide-react"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; -const DropdownMenu = DropdownMenuPrimitive.Root +const DropdownMenu = DropdownMenuPrimitive.Root; -const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger +const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger; -const DropdownMenuGroup = DropdownMenuPrimitive.Group +const DropdownMenuGroup = DropdownMenuPrimitive.Group; -const DropdownMenuPortal = DropdownMenuPrimitive.Portal +const DropdownMenuPortal = DropdownMenuPrimitive.Portal; -const DropdownMenuSub = DropdownMenuPrimitive.Sub +const DropdownMenuSub = DropdownMenuPrimitive.Sub; -const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup +const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup; const DropdownMenuSubTrigger = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef & { - inset?: boolean + inset?: boolean; } >(({ className, inset, children, ...props }, ref) => ( {children} -)) +)); DropdownMenuSubTrigger.displayName = - DropdownMenuPrimitive.SubTrigger.displayName + DropdownMenuPrimitive.SubTrigger.displayName; const DropdownMenuSubContent = React.forwardRef< React.ElementRef, @@ -48,13 +48,13 @@ const DropdownMenuSubContent = React.forwardRef< ref={ref} className={cn( "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", - className + className, )} {...props} /> -)) +)); DropdownMenuSubContent.displayName = - DropdownMenuPrimitive.SubContent.displayName + DropdownMenuPrimitive.SubContent.displayName; const DropdownMenuContent = React.forwardRef< React.ElementRef, @@ -67,18 +67,18 @@ const DropdownMenuContent = React.forwardRef< className={cn( "z-50 max-h-[var(--radix-dropdown-menu-content-available-height)] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md", "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", - className + className, )} {...props} /> -)) -DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName +)); +DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName; const DropdownMenuItem = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef & { - inset?: boolean + inset?: boolean; } >(({ className, inset, ...props }, ref) => ( svg]:size-4 [&>svg]:shrink-0", inset && "pl-8", - className + className, )} {...props} /> -)) -DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName +)); +DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName; const DropdownMenuCheckboxItem = React.forwardRef< React.ElementRef, @@ -101,7 +101,7 @@ const DropdownMenuCheckboxItem = React.forwardRef< ref={ref} className={cn( "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50", - className + className, )} checked={checked} {...props} @@ -113,9 +113,9 @@ const DropdownMenuCheckboxItem = React.forwardRef< {children} -)) +)); DropdownMenuCheckboxItem.displayName = - DropdownMenuPrimitive.CheckboxItem.displayName + DropdownMenuPrimitive.CheckboxItem.displayName; const DropdownMenuRadioItem = React.forwardRef< React.ElementRef, @@ -125,7 +125,7 @@ const DropdownMenuRadioItem = React.forwardRef< ref={ref} className={cn( "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50", - className + className, )} {...props} > @@ -136,13 +136,13 @@ const DropdownMenuRadioItem = React.forwardRef< {children} -)) -DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName +)); +DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName; const DropdownMenuLabel = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef & { - inset?: boolean + inset?: boolean; } >(({ className, inset, ...props }, ref) => ( -)) -DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName +)); +DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName; const DropdownMenuSeparator = React.forwardRef< React.ElementRef, @@ -166,8 +166,8 @@ const DropdownMenuSeparator = React.forwardRef< className={cn("-mx-1 my-1 h-px bg-muted", className)} {...props} /> -)) -DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName +)); +DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName; const DropdownMenuShortcut = ({ className, @@ -178,9 +178,9 @@ const DropdownMenuShortcut = ({ className={cn("ml-auto text-xs tracking-widest opacity-60", className)} {...props} /> - ) -} -DropdownMenuShortcut.displayName = "DropdownMenuShortcut" + ); +}; +DropdownMenuShortcut.displayName = "DropdownMenuShortcut"; export { DropdownMenu, @@ -198,4 +198,4 @@ export { DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuRadioGroup, -} +}; diff --git a/src/components/ui/form.tsx b/src/components/ui/form.tsx index ad1ae02..28bb2fe 100644 --- a/src/components/ui/form.tsx +++ b/src/components/ui/form.tsx @@ -1,8 +1,8 @@ -"use client" +"use client"; -import * as React from "react" -import * as LabelPrimitive from "@radix-ui/react-label" -import { Slot } from "@radix-ui/react-slot" +import * as React from "react"; +import * as LabelPrimitive from "@radix-ui/react-label"; +import { Slot } from "@radix-ui/react-slot"; import { Controller, FormProvider, @@ -10,27 +10,27 @@ import { type ControllerProps, type FieldPath, type FieldValues, -} from "react-hook-form" +} from "react-hook-form"; -import { cn } from "@/lib/utils" -import { Label } from "@/components/ui/label" +import { cn } from "@/lib/utils"; +import { Label } from "@/components/ui/label"; -const Form = FormProvider +const Form = FormProvider; type FormFieldContextValue< TFieldValues extends FieldValues = FieldValues, - TName extends FieldPath = FieldPath + TName extends FieldPath = FieldPath, > = { - name: TName -} + name: TName; +}; const FormFieldContext = React.createContext( - {} as FormFieldContextValue -) + {} as FormFieldContextValue, +); const FormField = < TFieldValues extends FieldValues = FieldValues, - TName extends FieldPath = FieldPath + TName extends FieldPath = FieldPath, >({ ...props }: ControllerProps) => { @@ -38,21 +38,21 @@ const FormField = < - ) -} + ); +}; const useFormField = () => { - const fieldContext = React.useContext(FormFieldContext) - const itemContext = React.useContext(FormItemContext) - const { getFieldState, formState } = useFormContext() + const fieldContext = React.useContext(FormFieldContext); + const itemContext = React.useContext(FormItemContext); + const { getFieldState, formState } = useFormContext(); - const fieldState = getFieldState(fieldContext.name, formState) + const fieldState = getFieldState(fieldContext.name, formState); if (!fieldContext) { - throw new Error("useFormField should be used within ") + throw new Error("useFormField should be used within "); } - const { id } = itemContext + const { id } = itemContext; return { id, @@ -61,36 +61,36 @@ const useFormField = () => { formDescriptionId: `${id}-form-item-description`, formMessageId: `${id}-form-item-message`, ...fieldState, - } -} + }; +}; type FormItemContextValue = { - id: string -} + id: string; +}; const FormItemContext = React.createContext( - {} as FormItemContextValue -) + {} as FormItemContextValue, +); const FormItem = React.forwardRef< HTMLDivElement, React.HTMLAttributes >(({ className, ...props }, ref) => { - const id = React.useId() + const id = React.useId(); return (
- ) -}) -FormItem.displayName = "FormItem" + ); +}); +FormItem.displayName = "FormItem"; const FormLabel = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => { - const { error, formItemId } = useFormField() + const { error, formItemId } = useFormField(); return (