174 lines
6.1 KiB
TypeScript
174 lines
6.1 KiB
TypeScript
import { Button, Card, Textarea } from '../ui'
|
|
import { t } from '@/lib/i18n'
|
|
import type { Page } from '@/types/nostr'
|
|
|
|
export function PagesManager(params: {
|
|
pages: Page[]
|
|
onPageContentChange: (pageNumber: number, content: string) => void
|
|
onPageTypeChange: (pageNumber: number, type: 'markdown' | 'image') => void
|
|
onRemovePage: (pageNumber: number) => void
|
|
onImageUpload: (file: File, pageNumber: number) => Promise<void>
|
|
}): React.ReactElement {
|
|
if (params.pages.length === 0) {
|
|
return <div className="text-sm text-gray-500">{t('page.empty')}</div>
|
|
}
|
|
return (
|
|
<div className="space-y-4">
|
|
<h3 className="text-lg font-semibold">{t('page.title')}</h3>
|
|
{params.pages.map((page) => (
|
|
<PageEditor
|
|
key={page.number}
|
|
page={page}
|
|
onContentChange={(content) => params.onPageContentChange(page.number, content)}
|
|
onTypeChange={(type) => params.onPageTypeChange(page.number, type)}
|
|
onRemove={() => params.onRemovePage(page.number)}
|
|
onImageUpload={async (file) => {
|
|
await params.onImageUpload(file, page.number)
|
|
}}
|
|
/>
|
|
))}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function PageEditor(params: {
|
|
page: Page
|
|
onContentChange: (content: string) => void
|
|
onTypeChange: (type: 'markdown' | 'image') => void
|
|
onRemove: () => void
|
|
onImageUpload: (file: File) => Promise<void>
|
|
}): React.ReactElement {
|
|
return (
|
|
<Card variant="default" className="space-y-3">
|
|
<PageEditorHeader page={params.page} onTypeChange={params.onTypeChange} onRemove={params.onRemove} />
|
|
<PageEditorBody page={params.page} onContentChange={params.onContentChange} onImageUpload={params.onImageUpload} />
|
|
</Card>
|
|
)
|
|
}
|
|
|
|
function PageEditorHeader(params: { page: Page; onTypeChange: (type: 'markdown' | 'image') => void; onRemove: () => void }): React.ReactElement {
|
|
return (
|
|
<div className="flex items-center justify-between">
|
|
<h4 className="font-semibold">
|
|
{t('page.number', { number: params.page.number })} - {t(`page.type.${params.page.type}`)}
|
|
</h4>
|
|
<div className="flex items-center gap-2">
|
|
<select
|
|
value={params.page.type}
|
|
onChange={(e) => params.onTypeChange(e.target.value as 'markdown' | 'image')}
|
|
className="text-sm border rounded px-2 py-1"
|
|
>
|
|
<option value="markdown">{t('page.type.markdown')}</option>
|
|
<option value="image">{t('page.type.image')}</option>
|
|
</select>
|
|
<Button
|
|
type="button"
|
|
variant="danger"
|
|
size="small"
|
|
onClick={params.onRemove}
|
|
className="px-2 py-1 text-sm rounded bg-red-600 text-white hover:bg-red-700"
|
|
>
|
|
{t('page.remove')}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function PageEditorBody(params: { page: Page; onContentChange: (content: string) => void; onImageUpload: (file: File) => Promise<void> }): React.ReactElement {
|
|
if (params.page.type === 'markdown') {
|
|
return (
|
|
<Textarea
|
|
value={params.page.content}
|
|
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => params.onContentChange(e.target.value)}
|
|
placeholder={t('page.markdown.placeholder')}
|
|
className="w-full border rounded p-2 h-48 font-mono text-sm"
|
|
rows={12}
|
|
/>
|
|
)
|
|
}
|
|
return <PageEditorImageBody page={params.page} onContentChange={params.onContentChange} onImageUpload={params.onImageUpload} />
|
|
}
|
|
|
|
function PageEditorImageBody(params: { page: Page; onContentChange: (content: string) => void; onImageUpload: (file: File) => Promise<void> }): React.ReactElement {
|
|
if (params.page.content) {
|
|
return (
|
|
<div className="relative">
|
|
<img src={params.page.content} alt={t('page.image.alt', { number: params.page.number })} className="max-w-full h-auto rounded" />
|
|
<Button
|
|
type="button"
|
|
variant="danger"
|
|
size="small"
|
|
onClick={() => params.onContentChange('')}
|
|
className="absolute top-2 right-2 px-2 py-1 text-sm rounded bg-red-600 text-white hover:bg-red-700"
|
|
>
|
|
{t('page.image.remove')}
|
|
</Button>
|
|
</div>
|
|
)
|
|
}
|
|
return <PageImageUploadButton onFileSelected={params.onImageUpload} />
|
|
}
|
|
|
|
function PageImageUploadButton(params: { onFileSelected: (file: File) => Promise<void> }): React.ReactElement {
|
|
return (
|
|
<label className="block cursor-pointer text-center">
|
|
<span className="inline-flex items-center justify-center px-3 py-2 text-sm rounded bg-blue-600 text-white hover:bg-blue-700 transition-colors">
|
|
{t('page.image.upload')}
|
|
</span>
|
|
<input
|
|
type="file"
|
|
accept=".png,.jpg,.jpeg,.webp"
|
|
className="hidden"
|
|
onChange={(e) => {
|
|
const file = e.target.files?.[0]
|
|
if (file) {
|
|
void params.onFileSelected(file)
|
|
}
|
|
}}
|
|
/>
|
|
</label>
|
|
)
|
|
}
|
|
|
|
export function createPagesHandlers(params: {
|
|
pages: Page[]
|
|
onPagesChange: ((pages: Page[]) => void) | undefined
|
|
}): {
|
|
addPage: (type: 'markdown' | 'image') => void
|
|
setPageContent: (pageNumber: number, content: string) => void
|
|
setPageType: (pageNumber: number, type: 'markdown' | 'image') => void
|
|
removePage: (pageNumber: number) => void
|
|
} {
|
|
const update = (next: Page[]): void => {
|
|
params.onPagesChange?.(next)
|
|
}
|
|
return {
|
|
addPage: (type: 'markdown' | 'image'): void => {
|
|
if (!params.onPagesChange) {
|
|
return
|
|
}
|
|
const newPage: Page = { number: params.pages.length + 1, type, content: '' }
|
|
update([...params.pages, newPage])
|
|
},
|
|
setPageContent: (pageNumber: number, content: string): void => {
|
|
if (!params.onPagesChange) {
|
|
return
|
|
}
|
|
update(params.pages.map((p) => (p.number === pageNumber ? { ...p, content } : p)))
|
|
},
|
|
setPageType: (pageNumber: number, type: 'markdown' | 'image'): void => {
|
|
if (!params.onPagesChange) {
|
|
return
|
|
}
|
|
update(params.pages.map((p) => (p.number === pageNumber ? { ...p, type, content: '' } : p)))
|
|
},
|
|
removePage: (pageNumber: number): void => {
|
|
if (!params.onPagesChange) {
|
|
return
|
|
}
|
|
update(params.pages.filter((p) => p.number !== pageNumber).map((p, idx) => ({ ...p, number: idx + 1 })))
|
|
},
|
|
}
|
|
}
|