Files
wedding-website/components/invite/photo-upload.tsx
T

157 lines
4.5 KiB
TypeScript

"use client"
import { useRef, useState } from "react"
import { allowedImageContentTypes, maxUploadSizeBytes } from "@/lib/validation/upload"
interface PhotoUploadLabels {
title: string
description: string
current: string
choose: string
upload: string
replace: string
uploading: string
success: string
error: string
disabled: string
requirements: string
}
export function PhotoUpload({
token,
labels,
disabled,
existingFilename,
}: {
token: string
labels: PhotoUploadLabels
disabled: boolean
existingFilename: string | null
}) {
const inputRef = useRef<HTMLInputElement>(null)
const [filename, setFilename] = useState(existingFilename)
const [status, setStatus] = useState<"idle" | "uploading" | "success" | "error">("idle")
async function uploadSelectedFile() {
const file = inputRef.current?.files?.[0]
if (!file) return
if (
!allowedImageContentTypes.includes(
file.type as (typeof allowedImageContentTypes)[number],
) ||
file.size > maxUploadSizeBytes
) {
setStatus("error")
return
}
setStatus("uploading")
try {
const presignResponse = await fetch("/api/uploads/presign", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
token,
filename: file.name,
contentType: file.type,
sizeBytes: file.size,
}),
})
if (!presignResponse.ok) throw new Error("Could not presign upload")
const { uploadUrl, objectKey } = (await presignResponse.json()) as {
uploadUrl: string
objectKey: string
}
const uploadResponse = await fetch(uploadUrl, {
method: "PUT",
headers: { "content-type": file.type },
body: file,
})
if (!uploadResponse.ok) throw new Error("Could not upload file")
const completeResponse = await fetch("/api/uploads/complete", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
token,
filename: file.name,
contentType: file.type,
sizeBytes: file.size,
objectKey,
}),
})
if (!completeResponse.ok) throw new Error("Could not complete upload")
setFilename(file.name)
setStatus("success")
if (inputRef.current) inputRef.current.value = ""
} catch {
setStatus("error")
}
}
return (
<section className="border-t border-wedding-line pt-10">
<div className="grid gap-4 wedding-layout:grid-cols-3">
<p className="wedding-section-label">04</p>
<div>
<h2 className="font-wedding-display text-wedding-medium">{labels.title}</h2>
<p className="mt-2 font-wedding-ui text-sm leading-wedding-body">{labels.description}</p>
</div>
<p className="font-wedding-ui text-sm">{labels.requirements}</p>
</div>
<div className="mt-6 wedding-layout:ml-60">
{filename ? (
<p className="bg-wedding-cream px-4 py-3 font-wedding-ui text-sm text-wedding-ink">
{labels.current.replace("{filename}", filename)}
</p>
) : null}
{disabled ? (
<p className="border border-wedding-line bg-wedding-cream px-4 py-3 font-wedding-ui text-sm">
{labels.disabled}
</p>
) : (
<div className="mt-4 flex flex-col gap-4 wedding-phone:flex-row wedding-phone:items-end">
<label className="flex-1 font-wedding-ui text-sm">
<span>{labels.choose}</span>
<input
ref={inputRef}
type="file"
accept={allowedImageContentTypes.join(",")}
className="mt-2 w-full border border-wedding-line bg-wedding-ivory p-3"
/>
</label>
<button
type="button"
onClick={uploadSelectedFile}
disabled={status === "uploading"}
className="wedding-primary-cta disabled:cursor-not-allowed disabled:opacity-60"
>
{status === "uploading"
? labels.uploading
: filename
? labels.replace
: labels.upload}
</button>
</div>
)}
{status === "success" ? (
<p className="mt-4 font-wedding-ui text-sm text-wedding-olive">{labels.success}</p>
) : null}
{status === "error" ? <p className="mt-4 font-wedding-ui text-sm text-wedding-terracotta">{labels.error}</p> : null}
</div>
</section>
)
}