Prepare production and Coolify deployment with original exports and datetime pickers
This commit is contained in:
@@ -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, unchanged—including 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
|
||||
|
||||
Reference in New Issue
Block a user