Barazo default frontend
barazo.forum
1/**
2 * TopicForm - Complete topic creation/edit form.
3 * Title, category, tags, markdown editor with preview, cross-post options.
4 * Client-side validation matching API Zod schemas.
5 * @see specs/prd-web.md Section 4 (Editor Components)
6 */
7
8'use client'
9
10import { useState, useCallback } from 'react'
11import type { CreateTopicInput, CategoryTreeNode } from '@/lib/api/types'
12import { cn } from '@/lib/utils'
13import { TopicMetaFields } from '@/components/topic-meta-fields'
14import { TopicContentEditor } from '@/components/topic-content-editor'
15import { TopicCrossPostSection } from '@/components/topic-cross-post-section'
16import { CrossPostAuthDialog } from '@/components/crosspost-auth-dialog'
17import { validateTopicForm } from '@/components/topic-form-validation'
18import type { TopicFormValues, FormErrors } from '@/components/topic-form-validation'
19import { useAuth } from '@/hooks/use-auth'
20
21interface TopicFormProps {
22 onSubmit: (values: CreateTopicInput) => void | Promise<void>
23 initialValues?: Partial<TopicFormValues>
24 mode?: 'create' | 'edit'
25 categories?: CategoryTreeNode[]
26 submitting?: boolean
27 className?: string
28}
29
30const CATEGORIES_FALLBACK: CategoryTreeNode[] = [
31 {
32 id: 'fallback-general',
33 slug: 'general',
34 name: 'General Discussion',
35 description: null,
36 parentId: null,
37 sortOrder: 0,
38 communityDid: '',
39 maturityRating: 'safe',
40 createdAt: '',
41 updatedAt: '',
42 children: [],
43 },
44]
45
46export function TopicForm({
47 onSubmit,
48 initialValues,
49 mode = 'create',
50 categories = CATEGORIES_FALLBACK,
51 submitting = false,
52 className,
53}: TopicFormProps) {
54 const [title, setTitle] = useState(initialValues?.title ?? '')
55 const [content, setContent] = useState(initialValues?.content ?? '')
56 const [category, setCategory] = useState(initialValues?.category ?? '')
57 const [tagInput, setTagInput] = useState(initialValues?.tags?.join(', ') ?? '')
58 const [crossPostBluesky, setCrossPostBluesky] = useState(initialValues?.crossPostBluesky ?? true)
59 const [crossPostFrontpage, setCrossPostFrontpage] = useState(
60 initialValues?.crossPostFrontpage ?? false
61 )
62 const [errors, setErrors] = useState<FormErrors>({})
63 const [showCrossPostAuthDialog, setShowCrossPostAuthDialog] = useState(false)
64 const { crossPostScopesGranted, requestCrossPostAuth } = useAuth()
65
66 const handleSubmit = useCallback(
67 (e: React.FormEvent) => {
68 e.preventDefault()
69 const values: TopicFormValues = {
70 title,
71 content,
72 category,
73 tags: tagInput
74 .split(',')
75 .map((t) => t.trim())
76 .filter(Boolean),
77 crossPostBluesky,
78 crossPostFrontpage,
79 }
80 const validationErrors = validateTopicForm(values)
81 setErrors(validationErrors)
82 if (Object.keys(validationErrors).length > 0) return
83 onSubmit({
84 title: values.title.trim(),
85 content: values.content.trim(),
86 category: values.category,
87 tags: values.tags?.length ? values.tags : undefined,
88 crossPostBluesky: values.crossPostBluesky,
89 crossPostFrontpage: values.crossPostFrontpage,
90 })
91 },
92 [title, content, category, tagInput, crossPostBluesky, crossPostFrontpage, onSubmit]
93 )
94
95 return (
96 <form
97 onSubmit={handleSubmit}
98 className={cn('space-y-6', className)}
99 noValidate
100 aria-label={mode === 'create' ? 'Create new topic' : 'Edit topic'}
101 >
102 <TopicMetaFields
103 title={title}
104 category={category}
105 tagInput={tagInput}
106 categories={categories}
107 errors={errors}
108 onTitleChange={setTitle}
109 onCategoryChange={setCategory}
110 onTagInputChange={setTagInput}
111 />
112
113 <TopicContentEditor content={content} onChange={setContent} error={errors.content} required />
114
115 {mode === 'create' && (
116 <TopicCrossPostSection
117 crossPostScopesGranted={crossPostScopesGranted}
118 crossPostBluesky={crossPostBluesky}
119 crossPostFrontpage={crossPostFrontpage}
120 onCrossPostBlueskyChange={setCrossPostBluesky}
121 onCrossPostFrontpageChange={setCrossPostFrontpage}
122 onAuthorizeClick={() => setShowCrossPostAuthDialog(true)}
123 />
124 )}
125
126 <CrossPostAuthDialog
127 open={showCrossPostAuthDialog}
128 onAuthorize={() => {
129 setShowCrossPostAuthDialog(false)
130 void requestCrossPostAuth()
131 }}
132 onCancel={() => setShowCrossPostAuthDialog(false)}
133 />
134
135 <div className="flex justify-end">
136 <button
137 type="submit"
138 disabled={submitting}
139 className={cn(
140 'rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground transition-colors',
141 'hover:bg-primary-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
142 'disabled:cursor-not-allowed disabled:opacity-50'
143 )}
144 >
145 {submitting
146 ? mode === 'create'
147 ? 'Creating...'
148 : 'Saving...'
149 : mode === 'create'
150 ? 'Create Topic'
151 : 'Save Changes'}
152 </button>
153 </div>
154 </form>
155 )
156}