146 lines
4 KiB
TypeScript
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
|
|
})
|
|
}
|