Prepare production and Coolify deployment with original exports and datetime pickers

This commit is contained in:
2026-09-09 17:59:20 -04:00
parent 574f29a68e
commit 64d7acc9c0
38 changed files with 1296 additions and 8 deletions
+3
View File
@@ -0,0 +1,3 @@
export function GET() {
return Response.json({ status: "ok" });
}
@@ -0,0 +1,30 @@
"use client";
import { useState } from "react";
import { Download } from "lucide-react";
import { api } from "@/trpc/react";
import { Button } from "@/components/ui/button";
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { toast } from "sonner";
export function EventExports({eventId}:{eventId:string}) {
const [filter,setFilter] = useState<"approved"|"all">("approved");
const history = api.manager.exports.useQuery({eventId},{refetchInterval:3000});
const request = api.manager.requestExport.useMutation({onSuccess:()=>{toast.success("Originals export queued");void history.refetch();},onError:e=>toast.error(e.message)});
const download = api.manager.downloadExport.useMutation({onSuccess:result=>{window.location.assign(result.url);},onError:e=>toast.error(e.message)});
return <section className="flex flex-col gap-3" aria-label="Export originals">
<h2 className="text-lg font-semibold">Export originals</h2>
<p className="text-sm text-muted-foreground">Full-quality files, unchangedincluding original metadata. ZIPs expire after 24 hours. Includes processed gallery photos, not standalone banners. Maximum 10,000 photos / 20 GB per export.</p>
<div className="flex flex-wrap items-center gap-2">
<Select value={filter} onValueChange={value=>setFilter(value as "approved"|"all")}>
<SelectTrigger aria-label="Photos to export"><SelectValue /></SelectTrigger>
<SelectContent><SelectGroup><SelectItem value="approved">Approved photos</SelectItem><SelectItem value="all">All photos, including private</SelectItem></SelectGroup></SelectContent>
</Select>
<Button variant="outline" disabled={request.isPending || history.data?.some(job=>job.status==="pending"||job.status==="processing")} onClick={()=>request.mutate({eventId,filter})}><Download data-icon="inline-start" />Export originals</Button>
</div>
{history.isError ? <p role="alert">Could not load exports.</p> : null}
<ul className="flex flex-col gap-2" aria-live="polite">{history.data?.map(job=><li key={job.id} className="flex flex-wrap items-center justify-between gap-2">
<span className="text-sm">{job.filter==="all"?"All photos":"Approved photos"} · {job.status.charAt(0).toUpperCase()+job.status.slice(1)}{job.total>0?` · ${job.processed}/${job.total}`:""}{job.status==="failed"?" — empty album, size limit, or processing error; try a new export.":""}</span>
{job.status==="ready" ? <Button variant="outline" disabled={download.isPending} onClick={()=>download.mutate({eventId,exportId:job.id})}><Download data-icon="inline-start" />Download ZIP</Button> : null}
</li>)}</ul>
</section>;
}
@@ -5,7 +5,7 @@ import { useRouter } from "next/navigation";
import { toast } from "sonner";
import { api } from "@/trpc/react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { DateTimePicker } from "@/components/ui/date-time-picker";
import { Field, FieldGroup, FieldLabel } from "@/components/ui/field";
import { Card, CardHeader, CardTitle, CardDescription, CardContent } from "@/components/ui/card";
@@ -36,7 +36,7 @@ export function EventSchedule({ eventId }: { eventId: string }) {
}}>
<FieldGroup className="grid gap-4 sm:grid-cols-2">
{dateFields.map(([key, label]) => <Field key={key}><FieldLabel htmlFor={`schedule-${key}`}>{label}</FieldLabel>
<Input id={`schedule-${key}`} type="datetime-local" value={dates[key] ?? localDate(event.data![key])} onChange={(e) => setDates({ ...dates, [key]: e.target.value })} />
<DateTimePicker id={`schedule-${key}`} ariaLabel={label} disabled={update.isPending} value={dates[key] ?? localDate(event.data![key])} onValueChange={(value) => setDates(previous => ({ ...previous, [key]: value }))} />
</Field>)}
</FieldGroup>
<p className="text-sm text-muted-foreground">Setting a future public date hides the guest page until then. Gallery and note schedules never approve existing private content. Submission closing ends new contributions; it does not hide the gallery. Dates alone do not send email.</p>
@@ -11,6 +11,7 @@ import { EventAudit } from "./event-audit";
import { EventSchedule } from "./event-schedule";
import { eventStatusLabel } from "@/lib/event-status";
import { effectiveEvent } from "@/lib/event-lifecycle";
import { EventExports } from "./event-exports";
export default async function EventDashboardPage({
params,
@@ -37,12 +38,15 @@ export default async function EventDashboardPage({
id: "photos",
label: "Photos",
content: (
<div className="flex flex-col gap-6">
{canSettings && event.permissions.includes("photos.private.read") ? <EventExports eventId={event.id} /> : null}
<ModerationGrid
eventId={event.id}
canModerate={canModerate}
canDelete={event.permissions.includes("photos.delete")}
canPrivate={event.permissions.includes("photos.private.read")}
/>
</div>
),
},
...(canSettings