From eea3a1f8d8c962d00209898450205ce5478e0d5f Mon Sep 17 00:00:00 2001 From: dwindown Date: Thu, 25 Dec 2025 13:41:51 +0700 Subject: [PATCH] Add Tiptap enhancements and webinar date/time fields MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add text alignment controls to Tiptap editor (left, center, right, justify) - Add horizontal rule/spacer button to Tiptap toolbar - Add event_start and duration_minutes fields to webinar products - Add webinar status badges (Recording Available, Coming Soon, Ended) - Install @tiptap/extension-text-align package 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- package-lock.json | 14 ++++++ package.json | 1 + src/components/RichTextEditor.tsx | 72 +++++++++++++++++++++++++++++-- src/pages/ProductDetail.tsx | 11 ++++- src/pages/admin/AdminProducts.tsx | 31 +++++++++++++ 5 files changed, 125 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index bcce7f4..dc64872 100644 --- a/package-lock.json +++ b/package-lock.json @@ -45,6 +45,7 @@ "@tiptap/extension-table-cell": "^3.14.0", "@tiptap/extension-table-header": "^3.14.0", "@tiptap/extension-table-row": "^3.14.0", + "@tiptap/extension-text-align": "^3.14.0", "@tiptap/react": "^3.13.0", "@tiptap/starter-kit": "^3.13.0", "class-variance-authority": "^0.7.1", @@ -3267,6 +3268,19 @@ "@tiptap/core": "^3.13.0" } }, + "node_modules/@tiptap/extension-text-align": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-text-align/-/extension-text-align-3.14.0.tgz", + "integrity": "sha512-CaxxlbAvfofZZ7KPL28Kg8xuMv8t4rvt5GPwZAqE+jd3rwrucpovpX/SdgclYDc75xs0t8qeoxDFe9HQmG5XZA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.14.0" + } + }, "node_modules/@tiptap/extension-underline": { "version": "3.13.0", "resolved": "https://registry.npmjs.org/@tiptap/extension-underline/-/extension-underline-3.13.0.tgz", diff --git a/package.json b/package.json index e990cc3..3ae6c54 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "@tiptap/extension-table-cell": "^3.14.0", "@tiptap/extension-table-header": "^3.14.0", "@tiptap/extension-table-row": "^3.14.0", + "@tiptap/extension-text-align": "^3.14.0", "@tiptap/react": "^3.13.0", "@tiptap/starter-kit": "^3.13.0", "class-variance-authority": "^0.7.1", diff --git a/src/components/RichTextEditor.tsx b/src/components/RichTextEditor.tsx index 57a504c..4b2d118 100644 --- a/src/components/RichTextEditor.tsx +++ b/src/components/RichTextEditor.tsx @@ -3,12 +3,13 @@ import StarterKit from '@tiptap/starter-kit'; import Link from '@tiptap/extension-link'; import Image from '@tiptap/extension-image'; import Placeholder from '@tiptap/extension-placeholder'; +import TextAlign from '@tiptap/extension-text-align'; import { Node } from '@tiptap/core'; import { Button } from '@/components/ui/button'; import { Bold, Italic, List, ListOrdered, Quote, Link as LinkIcon, Image as ImageIcon, Heading1, Heading2, Undo, Redo, - Maximize2, Minimize2, MousePointer, Square + Maximize2, Minimize2, MousePointer, Square, AlignLeft, AlignCenter, AlignRight, AlignJustify, MoreVertical, Minus } from 'lucide-react'; import { cn } from '@/lib/utils'; import { useCallback, useEffect, useState } from 'react'; @@ -243,7 +244,15 @@ export function RichTextEditor({ content, onChange, placeholder = 'Tulis konten. const editor = useEditor({ extensions: [ - StarterKit, + StarterKit.configure({ + heading: { + levels: [1, 2, 3], + }, + horizontalRule: true, + }), + TextAlign.configure({ + types: ['heading', 'paragraph'], + }), Link.configure({ openOnClick: false, HTMLAttributes: { @@ -517,6 +526,63 @@ export function RichTextEditor({ content, onChange, placeholder = 'Tulis konten. + {/* Text Align Separator */} +
+ + {/* Text Align Buttons */} + + + + + + {/* Spacer/Separator */} +
+ + {/* Email Components Separator */}
@@ -628,7 +694,7 @@ export function RichTextEditor({ content, onChange, placeholder = 'Tulis konten.
{uploading && ( diff --git a/src/pages/ProductDetail.tsx b/src/pages/ProductDetail.tsx index a0306e7..e9158cd 100644 --- a/src/pages/ProductDetail.tsx +++ b/src/pages/ProductDetail.tsx @@ -345,8 +345,17 @@ export default function ProductDetail() {

{product.title}

-
+
{product.type} + {product.type === 'webinar' && product.recording_url && ( + Rekaman Tersedia + )} + {product.type === 'webinar' && !product.recording_url && product.event_start && new Date(product.event_start) > new Date() && ( + Segera Hadir + )} + {product.type === 'webinar' && !product.recording_url && product.event_start && new Date(product.event_start) <= new Date() && ( + Telah Selesai + )} {hasAccess && Anda memiliki akses}
diff --git a/src/pages/admin/AdminProducts.tsx b/src/pages/admin/AdminProducts.tsx index 77201ed..afbd7c9 100644 --- a/src/pages/admin/AdminProducts.tsx +++ b/src/pages/admin/AdminProducts.tsx @@ -29,6 +29,8 @@ interface Product { content: string; meeting_link: string | null; recording_url: string | null; + event_start: string | null; + duration_minutes: number | null; price: number; sale_price: number | null; is_active: boolean; @@ -42,6 +44,8 @@ const emptyProduct = { content: '', meeting_link: '', recording_url: '', + event_start: null as string | null, + duration_minutes: null as number | null, price: 0, sale_price: null as number | null, is_active: true, @@ -84,6 +88,8 @@ export default function AdminProducts() { content: product.content || '', meeting_link: product.meeting_link || '', recording_url: product.recording_url || '', + event_start: product.event_start, + duration_minutes: product.duration_minutes, price: product.price, sale_price: product.sale_price, is_active: product.is_active, @@ -113,6 +119,8 @@ export default function AdminProducts() { content: form.content, meeting_link: form.meeting_link || null, recording_url: form.recording_url || null, + event_start: form.event_start || null, + duration_minutes: form.duration_minutes || null, price: form.price, sale_price: form.sale_price || null, is_active: form.is_active, @@ -322,6 +330,29 @@ export default function AdminProducts() { setForm({ ...form, recording_url: e.target.value })} placeholder="https://youtube.com/..." className="border-2" />
+ {form.type === 'webinar' && ( +
+
+ + setForm({ ...form, event_start: e.target.value || null })} + className="border-2" + /> +
+
+ + setForm({ ...form, duration_minutes: e.target.value ? parseInt(e.target.value) : null })} + placeholder="60" + className="border-2" + /> +
+
+ )}