From 260062f79c53a7391cbfcd6fb3d6ed8021328c2a Mon Sep 17 00:00:00 2001 From: mpsalunggg Date: Mon, 1 Sep 2025 22:22:52 +0700 Subject: [PATCH 1/9] fix(dob): update format request dob to iso string --- src/features/profile/components/ProfileForm.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/profile/components/ProfileForm.tsx b/src/features/profile/components/ProfileForm.tsx index ac001c5..8e6a90e 100644 --- a/src/features/profile/components/ProfileForm.tsx +++ b/src/features/profile/components/ProfileForm.tsx @@ -125,7 +125,7 @@ const ProfileForm = ({ activeTab }: ProfileFormProps) => { field.onChange(date ? format(date, "yyyy-MM-dd") : "")} + onSelect={(date) => field.onChange(date ? date.toISOString() : "")} disabled={(date) => date > new Date() || date < new Date("1900-01-01")} classNames={{ button_previous: "text-foreground hover:bg-accent p-2 rounded-lg hover:text-accent-foreground", From 4cd82d0955755f38b8f15610451ff19869c4e868 Mon Sep 17 00:00:00 2001 From: mpsalunggg Date: Mon, 1 Sep 2025 22:24:31 +0700 Subject: [PATCH 2/9] fix(toaster): update color text title to white --- src/components/ui/Toaster/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ui/Toaster/index.tsx b/src/components/ui/Toaster/index.tsx index a21e57e..6d42908 100644 --- a/src/components/ui/Toaster/index.tsx +++ b/src/components/ui/Toaster/index.tsx @@ -21,7 +21,7 @@ const Toaster = ({ ...props }: ToasterProps) => { classNames: { success: "!bg-green-500 !text-white", error: "!bg-destructive !text-white", - title: "!text-popover-foreground !font-semibold", + title: "!text-white !font-semibold", description: "!text-popover-foreground", }, }} From dd7647adf5b7ad8ee921917dd1f17cf81aea3c4b Mon Sep 17 00:00:00 2001 From: mpsalunggg Date: Thu, 4 Sep 2025 20:44:58 +0700 Subject: [PATCH 3/9] feat: create reusable ui for update and create events --- package.json | 3 + pnpm-lock.yaml | 61 ++ .../common/TextEditor/TextEditor.tsx | 41 +- .../common/TextEditor/ToolbarButton.tsx | 1 + src/components/common/TextEditor/utils.ts | 84 +++ src/components/ui/Switch/index.tsx | 28 + src/components/ui/TextArea/index.tsx | 18 + src/features/events/components/EventForm.tsx | 525 ++++++++++++++++++ .../events/pages/AdminEventsCreatePage.tsx | 48 +- src/lib/utils.ts | 9 + 10 files changed, 755 insertions(+), 63 deletions(-) create mode 100644 src/components/common/TextEditor/utils.ts create mode 100644 src/components/ui/Switch/index.tsx create mode 100644 src/components/ui/TextArea/index.tsx create mode 100644 src/features/events/components/EventForm.tsx diff --git a/package.json b/package.json index c2b12c7..ed8979c 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "@radix-ui/react-select": "^2.1.1", "@radix-ui/react-separator": "^1.1.7", "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-switch": "^1.2.6", "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-tooltip": "^1.2.7", "@tanstack/react-query": "^5.85.5", @@ -48,11 +49,13 @@ "clsx": "^2.1.1", "cookie": "^1.0.2", "date-fns": "^4.1.0", + "dompurify": "^3.2.6", "embla-carousel-autoplay": "^8.2.0", "embla-carousel-react": "^8.2.0", "js-cookie": "^3.0.5", "jwt-decode": "^4.0.0", "lucide-react": "^0.536.0", + "marked": "^16.2.1", "motion": "^12.7.4", "next": "15.3.2", "next-intl": "^4.1.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d654c07..fe51eff 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -41,6 +41,9 @@ importers: "@radix-ui/react-slot": specifier: ^1.2.3 version: 1.2.3(@types/react@19.1.5)(react@19.1.0) + "@radix-ui/react-switch": + specifier: ^1.2.6 + version: 1.2.6(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) "@radix-ui/react-tabs": specifier: ^1.1.13 version: 1.1.13(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -95,6 +98,9 @@ importers: date-fns: specifier: ^4.1.0 version: 4.1.0 + dompurify: + specifier: ^3.2.6 + version: 3.2.6 embla-carousel-autoplay: specifier: ^8.2.0 version: 8.6.0(embla-carousel@8.6.0) @@ -110,6 +116,9 @@ importers: lucide-react: specifier: ^0.536.0 version: 0.536.0(react@19.1.0) + marked: + specifier: ^16.2.1 + version: 16.2.1 motion: specifier: ^12.7.4 version: 12.23.12(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -1360,6 +1369,20 @@ packages: "@types/react": optional: true + "@radix-ui/react-switch@1.2.6": + resolution: + { integrity: sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ== } + peerDependencies: + "@types/react": 19.1.5 + "@types/react-dom": 19.1.5 + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + "@radix-ui/react-tabs@1.1.13": resolution: { integrity: sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A== } @@ -2136,6 +2159,10 @@ packages: resolution: { integrity: sha512-piErsCVVbpMMT2r7wbawdZsq4xMvIAhQuac2gedQHysu1TZYEigE6pnFfgZT+/jQnrRuF5r+SHzuehFjfRjr4g== } + "@types/trusted-types@2.0.7": + resolution: + { integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw== } + "@types/turndown@5.0.5": resolution: { integrity: sha512-TL2IgGgc7B5j78rIccBtlYAnkuv8nUQqhQc+DSYV5j9Be9XOcm/SKOVRuA47xAVI3680Tk9B1d8flK2GWT2+4w== } @@ -2890,6 +2917,10 @@ packages: resolution: { integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w== } + dompurify@3.2.6: + resolution: + { integrity: sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ== } + dunder-proto@1.0.1: resolution: { integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A== } @@ -4066,6 +4097,12 @@ packages: resolution: { integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw== } + marked@16.2.1: + resolution: + { integrity: sha512-r3UrXED9lMlHF97jJByry90cwrZBBvZmjG1L68oYfuPMW+uDTnuMbyJDymCWwbTE+f+3LhpNDKfpR3a3saFyjA== } + engines: { node: ">= 20" } + hasBin: true + math-intrinsics@1.1.0: resolution: { integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g== } @@ -6733,6 +6770,21 @@ snapshots: optionalDependencies: "@types/react": 19.1.5 + "@radix-ui/react-switch@1.2.6(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)": + dependencies: + "@radix-ui/primitive": 1.1.3 + "@radix-ui/react-compose-refs": 1.1.2(@types/react@19.1.5)(react@19.1.0) + "@radix-ui/react-context": 1.1.2(@types/react@19.1.5)(react@19.1.0) + "@radix-ui/react-primitive": 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + "@radix-ui/react-use-controllable-state": 1.2.2(@types/react@19.1.5)(react@19.1.0) + "@radix-ui/react-use-previous": 1.1.1(@types/react@19.1.5)(react@19.1.0) + "@radix-ui/react-use-size": 1.1.1(@types/react@19.1.5)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + "@types/react": 19.1.5 + "@types/react-dom": 19.1.5(@types/react@19.1.5) + "@radix-ui/react-tabs@1.1.13(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)": dependencies: "@radix-ui/primitive": 1.1.3 @@ -7352,6 +7404,9 @@ snapshots: dependencies: csstype: 3.1.3 + "@types/trusted-types@2.0.7": + optional: true + "@types/turndown@5.0.5": {} "@types/unist@2.0.11": {} @@ -7965,6 +8020,10 @@ snapshots: dom-accessibility-api@0.6.3: {} + dompurify@3.2.6: + optionalDependencies: + "@types/trusted-types": 2.0.7 + dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.2 @@ -9169,6 +9228,8 @@ snapshots: markdown-table@3.0.4: {} + marked@16.2.1: {} + math-intrinsics@1.1.0: {} mdast-util-find-and-replace@3.0.2: diff --git a/src/components/common/TextEditor/TextEditor.tsx b/src/components/common/TextEditor/TextEditor.tsx index ad54823..5c77e2c 100644 --- a/src/components/common/TextEditor/TextEditor.tsx +++ b/src/components/common/TextEditor/TextEditor.tsx @@ -7,8 +7,8 @@ import Underline from "@tiptap/extension-underline"; import Typography from "@tiptap/extension-typography"; import Link from "@tiptap/extension-link"; import Placeholder from "@tiptap/extension-placeholder"; -import { useState, useCallback, useMemo } from "react"; -import TurndownService from "turndown"; +import { useState, useCallback, useMemo, useEffect } from "react"; +import { markdownToHtml, htmlToMarkdown } from "./utils"; import { Button } from "@/components/ui/Button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/Card"; @@ -16,25 +16,14 @@ import { Toolbar } from "@/components/common/TextEditor"; interface TextEditorProps { markdownOutput?: boolean; + value?: string; + onChange?: (value: string) => void; } -const TextEditor = ({ markdownOutput = false }: TextEditorProps) => { +const TextEditor = ({ markdownOutput = false, value, onChange }: TextEditorProps) => { const [markdownContent, setMarkdownContent] = useState(""); - const turndownService = useMemo(() => { - const service = new TurndownService({ - headingStyle: "atx", - codeBlockStyle: "fenced", - }); - - // Custom rule for underline tags - service.addRule("underline", { - filter: "u", - replacement: (content) => `${content}`, - }); - - return service; - }, []); + const htmlContent = useMemo(() => markdownToHtml(value || ""), [value]); const editor = useEditor({ extensions: [ @@ -55,20 +44,24 @@ const TextEditor = ({ markdownOutput = false }: TextEditorProps) => { allowBase64: true, }), ], - content: "", + content: htmlContent, immediatelyRender: false, onCreate: ({ editor }) => { const html = editor.getHTML(); - setMarkdownContent(turndownService.turndown(html)); + setMarkdownContent(htmlToMarkdown(html)); }, onUpdate: ({ editor }) => { const html = editor.getHTML(); - setMarkdownContent(turndownService.turndown(html)); + const markdown = htmlToMarkdown(html); + setMarkdownContent(markdown); + onChange?.(html); + console.log("markdown: ", markdown); + console.log("html: ", html); }, editorProps: { attributes: { class: - "prose dark:prose-invert max-w-none mx-auto focus:outline-none min-h-[300px] p-3 prose-blockquote:border-primary prose-blockquote:bg-muted/50 prose-blockquote:pl-4 prose-blockquote:py-1 prose-blockquote:before:content-none prose-blockquote:not-italic prose-code:bg-muted prose-code:px-1.5 prose-code:py-0.5 prose-code:rounded prose-code:before:content-none prose-code:after:content-none prose-pre:bg-muted prose-pre:border prose-pre:text-foreground", + "prose dark:prose-invert max-w-none mx-auto focus:outline-none max-h-[300px] p-3 prose-blockquote:border-primary prose-blockquote:bg-muted/50 prose-blockquote:pl-4 prose-blockquote:py-1 prose-blockquote:before:content-none prose-blockquote:not-italic prose-code:bg-muted prose-code:rounded prose-code:before:content-none prose-code:after:content-none prose-pre:bg-muted prose-pre:border prose-pre:text-foreground prose-pre:p-3", }, }, }); @@ -132,6 +125,12 @@ const TextEditor = ({ markdownOutput = false }: TextEditorProps) => { editor.chain().focus().extendMarkRange("link").setLink({ href: url }).run(); }, [editor]); + useEffect(() => { + if (editor && htmlContent !== undefined && editor.getHTML() !== htmlContent) { + editor.commands.setContent(htmlContent); + } + }, [editor, htmlContent]); + if (!editor) { return (
diff --git a/src/components/common/TextEditor/ToolbarButton.tsx b/src/components/common/TextEditor/ToolbarButton.tsx index 1250766..6e34648 100644 --- a/src/components/common/TextEditor/ToolbarButton.tsx +++ b/src/components/common/TextEditor/ToolbarButton.tsx @@ -11,6 +11,7 @@ interface ToolbarButtonProps { const ToolbarButton = ({ onClick, isActive, disabled, children, title, variant = "outline" }: ToolbarButtonProps) => (