Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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