some imagely stuff

This commit is contained in:
john 2025-05-04 23:39:59 +02:00
parent f30829a651
commit d35619308d
3 changed files with 59 additions and 41 deletions

View file

@ -13,6 +13,7 @@
"dependencies": { "dependencies": {
"@js-temporal/polyfill": "^0.5.1", "@js-temporal/polyfill": "^0.5.1",
"@tailwindcss/vite": "^4.1.5", "@tailwindcss/vite": "^4.1.5",
"immer": "^10.1.1",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-router": "^7.5.3", "react-router": "^7.5.3",

View file

@ -5,48 +5,62 @@ interface NewPostWidgetProps {
isSubmitting?: boolean isSubmitting?: boolean
} }
interface Thumbnail {
id: string
objectUrl: string
}
export default function NewPostWidget({ onSubmit, isSubmitting = false }: NewPostWidgetProps) { export default function NewPostWidget({ onSubmit, isSubmitting = false }: NewPostWidgetProps) {
const [content, setContent] = useState('') const [content, setContent] = useState('')
const [media, setMedia] = useState<File[]>([]) const [files, setFiles] = useState<{ id: string; file: File }[]>([])
const [mediaPreviewUrls, setMediaPreviewUrls] = useState<string[]>([]) const [thumbnails, setThumbnails] = useState<Thumbnail[]>([])
const handleContentChange = (e: ChangeEvent<HTMLTextAreaElement>) => { const handleContentChange = (e: ChangeEvent<HTMLTextAreaElement>) => {
setContent(e.target.value) setContent(e.target.value)
} }
const handleMediaChange = (e: ChangeEvent<HTMLInputElement>) => { const handleMediaChange = (e: ChangeEvent<HTMLInputElement>) => {
if (e.target.files && e.target.files.length > 0) { if (!(e.target.files && e.target.files.length > 0)) {
const newFiles = Array.from(e.target.files) return
setMedia([...media, ...newFiles])
// Create preview URLs for the new files
const newPreviewUrls = newFiles.map((file) => URL.createObjectURL(file))
setMediaPreviewUrls([...mediaPreviewUrls, ...newPreviewUrls])
} }
const newFiles = Array.from(e.target.files).map((file) => ({
id: crypto.randomUUID(),
file,
}))
setFiles([...files, ...newFiles])
const newThumbnails = newFiles.map((file) => {
return {
id: file.id,
objectUrl: URL.createObjectURL(file.file),
}
})
setThumbnails([...thumbnails, ...newThumbnails])
;(e.target as HTMLInputElement).value = ''
} }
const handleSubmit = () => { const handleSubmit = () => {
if (content.trim() || media.length > 0) { if (!content.trim() && files.length === 0) {
onSubmit(content, media) return
setContent('')
setMedia([])
// Revoke object URLs to avoid memory leaks
mediaPreviewUrls.forEach((url) => URL.revokeObjectURL(url))
setMediaPreviewUrls([])
} }
onSubmit(
content,
files.map((file) => file.file),
)
setContent('')
setFiles([])
thumbnails.forEach(({ objectUrl }) => URL.revokeObjectURL(objectUrl))
setThumbnails([])
} }
const handleRemoveMedia = (index: number) => { const handleRemoveMedia = (id: string) => {
const newMedia = [...media] setFiles(files.filter((file) => file.id !== id))
newMedia.splice(index, 1) const { objectUrl } = thumbnails.find((thumbnail) => thumbnail.id === id)!
setMedia(newMedia) URL.revokeObjectURL(objectUrl)
setThumbnails(thumbnails.filter((thumbnail) => thumbnail.id !== id))
// Revoke the URL of the removed media
URL.revokeObjectURL(mediaPreviewUrls[index])
const newPreviewUrls = [...mediaPreviewUrls]
newPreviewUrls.splice(index, 1)
setMediaPreviewUrls(newPreviewUrls)
} }
return ( return (
@ -60,19 +74,17 @@ export default function NewPostWidget({ onSubmit, isSubmitting = false }: NewPos
disabled={isSubmitting} disabled={isSubmitting}
/> />
{mediaPreviewUrls.length > 0 && ( {thumbnails.length > 0 && (
<div className="grid gap-2 grid-cols-3 mb-3"> <div className="flex flex-row flex-wrap gap-2 mb-3">
{mediaPreviewUrls.map((url, index) => ( {thumbnails.map(({ objectUrl, id }) => (
<div key={index} className="relative"> <button
<img src={url} alt="" className="w-24 h-24 object-cover rounded-md" /> key={id}
<button className="relative cursor-pointer"
className="absolute top-1 right-1 bg-gray-800 bg-opacity-50 text-white rounded-full p-1 text-xs" onClick={() => handleRemoveMedia(id)}
onClick={() => handleRemoveMedia(index)} disabled={isSubmitting}
disabled={isSubmitting} >
> <img src={objectUrl} alt="" className="w-24 h-24 object-cover rounded-md" />
</button>
</button>
</div>
))} ))}
</div> </div>
)} )}
@ -92,7 +104,7 @@ export default function NewPostWidget({ onSubmit, isSubmitting = false }: NewPos
<button <button
className="px-4 py-2 bg-gray-800 text-white rounded-md hover:bg-gray-700 disabled:opacity-50" className="px-4 py-2 bg-gray-800 text-white rounded-md hover:bg-gray-700 disabled:opacity-50"
onClick={handleSubmit} onClick={handleSubmit}
disabled={isSubmitting || (content.trim() === '' && media.length === 0)} disabled={isSubmitting || (content.trim() === '' && files.length === 0)}
> >
Post Post
</button> </button>

View file

@ -1601,6 +1601,11 @@ ignore@^5.2.0, ignore@^5.3.1:
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5"
integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g== integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==
immer@^10.1.1:
version "10.1.1"
resolved "https://registry.yarnpkg.com/immer/-/immer-10.1.1.tgz#206f344ea372d8ea176891545ee53ccc062db7bc"
integrity sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==
import-fresh@^3.2.1: import-fresh@^3.2.1:
version "3.3.1" version "3.3.1"
resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.1.tgz#9cecb56503c0ada1f2741dbbd6546e4b13b57ccf" resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.1.tgz#9cecb56503c0ada1f2741dbbd6546e4b13b57ccf"