Skip to content
Merged
126 changes: 126 additions & 0 deletions client/src/components/admin/ProgramFormModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,9 @@ export function ProgramFormModal({
const [eventStartsAt, setEventStartsAt] = useState("");
const [eventEndsAt, setEventEndsAt] = useState("");
const [prizeTierRows, setPrizeTierRows] = useState<PrizeTierRow[]>([]);
const [galleryUrl, setGalleryUrl] = useState("");
type HonoraryMentionRow = { name: string; videoUrl: string; githubUrl: string };
const [honoraryMentionRows, setHonoraryMentionRows] = useState<HonoraryMentionRow[]>([]);
const [errors, setErrors] = useState<Record<string, string>>({});
const [submitting, setSubmitting] = useState(false);
const [uploadingCover, setUploadingCover] = useState(false);
Expand All @@ -129,6 +132,14 @@ export function ProgramFormModal({
setEventStartsAt(isoToLocal(program.eventStartsAt));
setEventEndsAt(isoToLocal(program.eventEndsAt));
setPrizeTierRows(tierRowsFromProgram(program));
setGalleryUrl(program.galleryUrl || "");
setHonoraryMentionRows(
(program.honoraryMentions ?? []).map((m) => ({
name: m.name,
videoUrl: m.videoUrl ?? "",
githubUrl: m.githubUrl ?? "",
})),
);
} else {
setName("");
setSlug("");
Expand All @@ -145,6 +156,8 @@ export function ProgramFormModal({
setEventStartsAt("");
setEventEndsAt("");
setPrizeTierRows(tierRowsFromProgram(null));
setGalleryUrl("");
setHonoraryMentionRows([]);
}
setErrors({});
}, [open, program]);
Expand Down Expand Up @@ -174,6 +187,9 @@ export function ProgramFormModal({
if (coverImageUrl.trim() && !/^https?:\/\//i.test(coverImageUrl.trim())) {
e.coverImageUrl = "Must start with http:// or https://";
}
if (galleryUrl.trim() && !/^https?:\/\//i.test(galleryUrl.trim())) {
e.galleryUrl = "Must start with http:// or https://";
}
if (applicationsOpenAt && applicationsCloseAt) {
if (new Date(applicationsOpenAt).getTime() >= new Date(applicationsCloseAt).getTime()) {
e.applicationsCloseAt = "Close date must be after open date.";
Expand Down Expand Up @@ -254,6 +270,14 @@ export function ProgramFormModal({
prizeTiers: prizeTierRows
.map((r) => ({ amount: Number(r.amount), currency: r.currency.trim(), label: r.label.trim() }))
.filter((t) => Number.isInteger(t.amount) && t.amount > 0 && t.currency),
galleryUrl: galleryUrl.trim() || null,
honoraryMentions: honoraryMentionRows
.filter((m) => m.name.trim())
.map((m) => ({
name: m.name.trim(),
videoUrl: m.videoUrl.trim() || null,
githubUrl: m.githubUrl.trim() || null,
})),
};

const res = editing
Expand Down Expand Up @@ -354,6 +378,29 @@ export function ProgramFormModal({
</Select>
</div>

{status === "completed" && (
<div className="sm:col-span-2 space-y-1">
{!galleryUrl.trim() && (
<p className="label-hw text-[10px]">
<span className="led led-sm mr-1" style={{ background: "var(--color-amber, #f59e0b)" }} aria-hidden="true" />
·GALLERY URL IS MISSING: add it below so the results panel can link to event photos.
</p>
)}
{honoraryMentionRows.length === 0 && (
<p className="label-hw text-[10px]">
<span className="led led-sm mr-1" style={{ background: "var(--color-amber, #f59e0b)" }} aria-hidden="true" />
·HONORARY MENTIONS ARE EMPTY: add them below if applicable.
</p>
)}
{(!eventStartsAt || !eventEndsAt) && (
<p className="label-hw text-[10px]">
<span className="led led-sm mr-1" style={{ background: "var(--color-amber, #f59e0b)" }} aria-hidden="true" />
·EVENT DATES MISSING: hours of hacking will not be shown in the results panel.
</p>
)}
</div>
)}

<div className="sm:col-span-2 space-y-1.5">
<Label htmlFor="pf-description" className="label-hw-dim">·DESCRIPTION</Label>
<Textarea
Expand Down Expand Up @@ -508,6 +555,85 @@ export function ProgramFormModal({
)}
</div>

<div className="sm:col-span-2 space-y-1.5">
<Label htmlFor="pf-gallery-url" className="label-hw-dim">·GALLERY URL (EVENT PHOTOS)</Label>
<Input
id="pf-gallery-url"
type="url"
placeholder="https://drive.google.com/drive/folders/…"
value={galleryUrl}
onChange={(e) => setGalleryUrl(e.target.value)}
aria-invalid={errors.galleryUrl ? true : undefined}
className="font-mono text-sm"
/>
{errors.galleryUrl && (
<p className="label-hw text-destructive">·{errors.galleryUrl.toUpperCase()}</p>
)}
</div>

<div className="sm:col-span-2 space-y-2">
<div className="flex items-center justify-between">
<Label className="label-hw-dim">·HONORARY MENTIONS</Label>
<button
type="button"
onClick={() =>
setHonoraryMentionRows((rows) => [...rows, { name: "", videoUrl: "", githubUrl: "" }])
}
className="font-mono text-[10px] tracking-[0.14em] border border-hairline text-display hover:bg-panel-deep px-2 py-1 inline-flex items-center gap-1"
>
<Plus className="h-3 w-3" aria-hidden="true" /> ADD
</button>
</div>
<p className="label-hw-dim">Projects that deserve recognition beyond the official prizes.</p>
{honoraryMentionRows.map((row, i) => (
<div key={i} className="flex items-center gap-2">
<Input
placeholder="Project name"
aria-label={`Mention ${i + 1} name`}
value={row.name}
onChange={(e) =>
setHonoraryMentionRows((rows) =>
rows.map((r, j) => (j === i ? { ...r, name: e.target.value } : r)),
)
}
className="font-mono text-sm flex-1"
/>
<Input
placeholder="Video URL"
aria-label={`Mention ${i + 1} video URL`}
value={row.videoUrl}
onChange={(e) =>
setHonoraryMentionRows((rows) =>
rows.map((r, j) => (j === i ? { ...r, videoUrl: e.target.value } : r)),
)
}
className="font-mono text-sm w-36"
/>
<Input
placeholder="GitHub URL"
aria-label={`Mention ${i + 1} GitHub URL`}
value={row.githubUrl}
onChange={(e) =>
setHonoraryMentionRows((rows) =>
rows.map((r, j) => (j === i ? { ...r, githubUrl: e.target.value } : r)),
)
}
className="font-mono text-sm w-36"
/>
<button
type="button"
aria-label={`Remove mention ${i + 1}`}
onClick={() =>
setHonoraryMentionRows((rows) => rows.filter((_, j) => j !== i))
}
className="border border-hairline text-display hover:bg-panel-deep p-1.5"
>
<X className="h-3 w-3" aria-hidden="true" />
</button>
</div>
))}
</div>

{programType === "hackathon" && (
<div className="sm:col-span-2 space-y-2">
<div className="flex items-center justify-between">
Expand Down
Loading