femto-webapp/src/components/NewPostWidget.tsx
2025-05-18 13:52:15 +02:00

146 lines
4 KiB
TypeScript

import { useState } from 'react'
import FancyTextEditor, { TextInputKeyDownEvent } from './inputs/FancyTextEditor.tsx'
import Button from './buttons/Button.tsx'
import { openFileDialog } from '../utils/openFileDialog.ts'
interface NewPostWidgetProps {
onSubmit: (
content: string,
media: { file: File; width: number; height: number }[],
isPublic: boolean,
) => void
isSubmitting?: boolean
}
interface Attachment {
id: string
file: File
objectUrl: string
width: number
height: number
}
export default function NewPostWidget({ onSubmit, isSubmitting = false }: NewPostWidgetProps) {
const [content, setContent] = useState('')
const [attachments, setAttachments] = useState<Attachment[]>([])
const [isPublic, setIsPublic] = useState(false)
const onContentInput = (value: string) => {
setContent(value)
}
async function onAddMediaClicked() {
const files = await openFileDialog('image/*', true)
if (files == null || files.length === 0) {
return
}
const newAttachments = await Promise.all(Array.from(files).map(createAttachment))
setAttachments((attachments) => [...attachments, ...newAttachments])
}
const handleSubmit = () => {
if (!content.trim() && attachments.length === 0) {
return
}
onSubmit(content, attachments, isPublic)
attachments.forEach(({ objectUrl }) => URL.revokeObjectURL(objectUrl))
setContent('')
setAttachments([])
}
const handleRemoveMedia = (id: string) => {
const attachment = attachments.find((attachment) => attachment.id === id)!
setAttachments(attachments.filter((attachment) => attachment.id !== id))
URL.revokeObjectURL(attachment.objectUrl)
}
const onInputKeyDown = (e: TextInputKeyDownEvent) => {
if (e.key === 'Enter' && e.ctrlKey) {
e.preventDefault()
handleSubmit()
}
}
return (
<div className="w-full p-4 border-b border-gray-200">
<FancyTextEditor
value={content}
onInput={onContentInput}
onKeyDown={onInputKeyDown}
className="mb-3"
placeholder="write something..."
/>
{attachments.length > 0 && (
<div className="flex flex-row flex-wrap gap-2 mb-3">
{attachments.map(({ objectUrl, id }) => (
<button
key={id}
className="relative cursor-pointer"
onClick={() => handleRemoveMedia(id)}
disabled={isSubmitting}
>
<img src={objectUrl} alt="" className="w-24 h-24 object-cover rounded-md" />
</button>
))}
</div>
)}
<div className="flex justify-between items-center pt-2">
<div className="flex items-center gap-2">
<Button secondary onClick={onAddMediaClicked}>
+ add media
</Button>
<label className="flex items-center gap-1 cursor-pointer">
<input
type="checkbox"
checked={isPublic}
onChange={(e) => setIsPublic(e.target.checked)}
disabled={isSubmitting}
className="form-checkbox h-4 w-4 text-blue-600"
/>
<span className="text-primary-500">public</span>
</label>
</div>
<Button
onClick={handleSubmit}
disabled={isSubmitting || (content.trim() === '' && attachments.length === 0)}
>
post
</Button>
</div>
</div>
)
}
async function createAttachment(file: File): Promise<Attachment> {
if (!file.type.startsWith('image/')) {
throw new Error('not an image')
}
const objectUrl = URL.createObjectURL(file)
const { width, height } = await getImageFileDimensions(objectUrl)
return {
id: crypto.randomUUID(),
file,
objectUrl,
width,
height,
}
}
function getImageFileDimensions(objectURL: string): Promise<{ width: number; height: number }> {
return new Promise((resolve) => {
const img = document.createElement('img')
img.addEventListener('load', () => {
resolve({ width: img.width, height: img.height })
})
img.src = objectURL
})
}