From af31ee8d1cd48e5d59eb3b6175ba8ea220e4c22b Mon Sep 17 00:00:00 2001 From: Marco Acierno Date: Tue, 2 Jun 2026 15:45:33 +0200 Subject: [PATCH 1/3] Migrate custom_admin Schedule Builder to Radix UI Replace the schedule builder's ad-hoc UI with @radix-ui/themes, matching the invitation-letter document builder pattern. - Custom div Modal -> Radix Dialog (guard content on data so the close animation never dereferences null after the data is cleared) - Native /`/` → TextField; remove console.log + empty else +│ ├── add-custom-event.tsx →TextField, list→Cards, .btn→Button +│ ├── proposal-preview.tsx .btn → Button;
  • / → Card/Text/Badge +│ ├── keynote-preview.tsx same as proposal-preview +│ └── info-recap.tsx grid+strong/span → Text (or DataList) +├── shared/ +│ ├── modal.tsx DELETE (only schedule-builder imports it) +│ ├── spacer.tsx reuse +│ └── base.tsx wrapper — leave +└── ... +src/custom-styles.css remove `.btn` rule once no longer referenced +``` + +## Code Style + +Follow the invitation-letter editor (`src/components/invitation-letter-document-builder/`). +Components from `@radix-ui/themes`, semantic props over Tailwind, `lucide-react` icons, +compound Dialog/AlertDialog pattern. Example (the reference modal pattern from +`editor-section.tsx`): + +```tsx +import { Dialog, Button, Flex, Text, TextField } from "@radix-ui/themes"; +import { Pencil } from "lucide-react"; + + !o && close()}> + + Add event to schedule + + + + + + +``` + +Select pattern (replacing the native ``/``→`TextField` (autofocus kept); `console.log` + empty `else` removed. + - Verify: `pnpm build`; typing searches. + - Files: `add-item-modal/search-event.tsx` + +- [ ] **T5 — add-custom-event**. + - Acceptance: native ``→`TextField`; `.btn`→`Button`; + quick-option list → Radix Cards/Buttons; create-by-hand validation unchanged. + - Verify: `pnpm build`; both create paths work; Select layers above Dialog. + - Files: `add-item-modal/add-custom-event.tsx` + +- [ ] **T6 — modal → Dialog + delete custom Modal**. + - Acceptance: `index.tsx` uses `Dialog.Root/Content/Title`; `shared/modal.tsx` deleted; + no manual `body.overflow` code remains; open/close still driven by `useAddItemModal`. + - Verify: `pnpm build`; `grep -rn shared/modal src` empty; dialog opens/closes/scroll-locks. + - Files: `add-item-modal/index.tsx`, **delete** `shared/modal.tsx` + +- [ ] **T7 — calendar header**. + - Acceptance: `

    `→`Heading`; "Edit day in admin" `

  • + ); }; -const CustomByHand = ({ onCreate }) => { +const TYPE_OPTIONS = [ + { value: "talk", label: "Talk" }, + { value: "training", label: "Training" }, + { value: "keynote", label: "Keynote" }, + { value: "panel", label: "Panel" }, + { value: "registration", label: "Registration" }, + { value: "announcements", label: "Announcements" }, + { value: "break", label: "Break" }, + { value: "social", label: "Social" }, + { value: "recruiting", label: "Recruiting" }, + { value: "custom", label: "Custom" }, +]; + +const CustomByHand = ({ + onCreate, +}: { + onCreate: (args: CreateArgs) => void; +}) => { const { data: { room: selectedRoom }, } = useAddItemModal(); @@ -117,51 +146,41 @@ const CustomByHand = ({ onCreate }) => { }; return ( - <> - Create custom by hand -
    - - + Create custom by hand + + + Title + + setTitle(e.target.value)} /> - - -
    - - + + ); }; diff --git a/backend/custom_admin/src/components/schedule-builder/add-item-modal/index.tsx b/backend/custom_admin/src/components/schedule-builder/add-item-modal/index.tsx index 2f98821cea..eba5c4b630 100644 --- a/backend/custom_admin/src/components/schedule-builder/add-item-modal/index.tsx +++ b/backend/custom_admin/src/components/schedule-builder/add-item-modal/index.tsx @@ -1,32 +1,24 @@ -import { Modal } from "../../shared/modal"; +import { Dialog, Flex } from "@radix-ui/themes"; import { AddCustomEvent } from "./add-custom-event"; import { useAddItemModal } from "./context"; import { SearchEvent } from "./search-event"; export const AddItemModal = () => { - const { isOpen, close } = useAddItemModal(); - - if (!isOpen) { - return null; - } + const { isOpen, close, data } = useAddItemModal(); return ( - -
    -

    Add event to schedule

    -
      -
    • + !open && close()}> + + Add event to schedule + {/* data is null while the dialog animates closed; guard so children + (which read data.day/slot/room) never dereference null. */} + {data && ( + -
    • -
    • -
    • -
    -
    -
    + + )} + + ); }; diff --git a/backend/custom_admin/src/components/schedule-builder/add-item-modal/info-recap.tsx b/backend/custom_admin/src/components/schedule-builder/add-item-modal/info-recap.tsx index 94e286b7e6..59bb824d59 100644 --- a/backend/custom_admin/src/components/schedule-builder/add-item-modal/info-recap.tsx +++ b/backend/custom_admin/src/components/schedule-builder/add-item-modal/info-recap.tsx @@ -1,4 +1,4 @@ -import { Fragment } from "react"; +import { DataList } from "@radix-ui/themes"; type Props = { info: { @@ -9,13 +9,13 @@ type Props = { export const InfoRecap = ({ info }: Props) => { return ( -
    + {info.map(({ label, value }) => ( - - {label} - {value} - + + {label} + {value} + ))} -
    + ); }; diff --git a/backend/custom_admin/src/components/schedule-builder/add-item-modal/keynote-preview.tsx b/backend/custom_admin/src/components/schedule-builder/add-item-modal/keynote-preview.tsx index 07acdb358a..5222f4852d 100644 --- a/backend/custom_admin/src/components/schedule-builder/add-item-modal/keynote-preview.tsx +++ b/backend/custom_admin/src/components/schedule-builder/add-item-modal/keynote-preview.tsx @@ -1,3 +1,4 @@ +import { Button, Card, Text } from "@radix-ui/themes"; import type { KeynoteFragmentFragment } from "../../fragments/keynote.generated"; import { useCurrentConference } from "../../utils/conference"; import { useAddItemModal } from "./context"; @@ -29,8 +30,10 @@ export const KeynotePreview = ({ }; return ( -
  • - {keynote.title} + + + {keynote.title} + - -
  • + + ); }; diff --git a/backend/custom_admin/src/components/schedule-builder/add-item-modal/proposal-preview.tsx b/backend/custom_admin/src/components/schedule-builder/add-item-modal/proposal-preview.tsx index a3835583e8..11017ff47e 100644 --- a/backend/custom_admin/src/components/schedule-builder/add-item-modal/proposal-preview.tsx +++ b/backend/custom_admin/src/components/schedule-builder/add-item-modal/proposal-preview.tsx @@ -1,3 +1,4 @@ +import { Badge, Button, Card, Flex, Text } from "@radix-ui/themes"; import type { Language } from "../../../types"; import type { SubmissionFragmentFragment } from "../../fragments/submission.generated"; import type { AvailabilityValue } from "../../utils/availability"; @@ -7,19 +8,13 @@ import { useAddItemModal } from "./context"; import { useCreateScheduleItemMutation } from "./create-schedule-item.generated"; import { InfoRecap } from "./info-recap"; -const AVAILABILITY_STYLES: Record< +const AVAILABILITY_BADGE: Record< AvailabilityValue, - { label: string; className: string } + { label: string; color: "green" | "blue" | "red" } > = { - preferred: { - label: "Preferred", - className: "bg-green-200 text-green-900 font-semibold", - }, - available: { label: "Available", className: "bg-blue-100 text-blue-900" }, - unavailable: { - label: "Unavailable", - className: "bg-red-200 text-red-900 font-semibold", - }, + preferred: { label: "Preferred", color: "green" }, + available: { label: "Available", color: "blue" }, + unavailable: { label: "Unavailable", color: "red" }, }; type Props = { @@ -42,22 +37,24 @@ export const ProposalPreview = ({ proposal }: Props) => { : undefined; return ( -
  • -
    + +
    - {proposal.title} + + {proposal.title} + {proposal.italianTitle !== proposal.title && ( -
    {proposal.italianTitle}
    + + {proposal.italianTitle} + )}
    {slotAvailability && ( - - {AVAILABILITY_STYLES[slotAvailability].label} - + + {AVAILABILITY_BADGE[slotAvailability].label} + )} -
    + { ]} /> -
  • + ); }; @@ -94,17 +91,16 @@ const AddActions = ({ proposal }: { proposal: SubmissionFragmentFragment }) => { }; return ( -
    + {languages.map((language) => ( - + ))} -
    + ); }; diff --git a/backend/custom_admin/src/components/schedule-builder/add-item-modal/search-event.tsx b/backend/custom_admin/src/components/schedule-builder/add-item-modal/search-event.tsx index 918f9c2c0a..79989512cf 100644 --- a/backend/custom_admin/src/components/schedule-builder/add-item-modal/search-event.tsx +++ b/backend/custom_admin/src/components/schedule-builder/add-item-modal/search-event.tsx @@ -1,3 +1,4 @@ +import { Flex, Heading, Text, TextField } from "@radix-ui/themes"; import { useEffect, useRef, useState } from "react"; import { useCurrentConference } from "../../utils/conference"; @@ -29,52 +30,41 @@ export const SearchEvent = () => { useEffect(() => { if (debouncedSearch) { - console.log("running search", debouncedSearch); - runSearch({ variables: { conferenceId, query: debouncedSearch, }, }); - } else { } }, [debouncedSearch]); return ( - <> -
    - Search proposal / keynote -
    -
    - -
    -
    - {loading && Searching events} - {!loading && data?.searchEvents.results.length === 0 && ( - No events found - )} - {debouncedSearch && data?.searchEvents.results.length > 0 && ( -
      - {data.searchEvents.results.map((event) => { - if (event.__typename === "Submission") { - return ; - } + + Search proposal / keynote + + {loading && Searching events} + {!loading && data?.searchEvents.results.length === 0 && ( + No events found + )} + {debouncedSearch && data?.searchEvents.results.length > 0 && ( + + {data.searchEvents.results.map((event) => { + if (event.__typename === "Submission") { + return ; + } - if (event.__typename === "Keynote") { - return ; - } - })} -
    - )} -
    - + if (event.__typename === "Keynote") { + return ; + } + })} + + )} + ); }; diff --git a/backend/custom_admin/src/components/schedule-builder/calendar.tsx b/backend/custom_admin/src/components/schedule-builder/calendar.tsx index 9b32cb5500..078bd0b65d 100644 --- a/backend/custom_admin/src/components/schedule-builder/calendar.tsx +++ b/backend/custom_admin/src/components/schedule-builder/calendar.tsx @@ -1,4 +1,5 @@ -import React, { Fragment } from "react"; +import { Button, Heading } from "@radix-ui/themes"; +import { Fragment } from "react"; import { useDjangoAdminEditor } from "../shared/django-admin-editor-modal/context"; import { formatHour } from "../utils/time"; @@ -26,10 +27,12 @@ export const Calendar = ({ day }: Props) => { return (
    -

    {date}

    - +
    acc + 1, 0), }} - className="z-50 bg-slate-200" + className="z-50" > - {availability === "unavailable" && ( -
  • - ⚠ Speaker unavailable - - } - > - + + {availability === "unavailable" && ( + + + ⚠ Speaker unavailable + + + } > - i - - -
  • - )} -
  • - [{item.type} - {duration || "??"} mins] -
  • -
  • {item.status}
  • -
  • - {item.title} -
  • - {item.speakers.length > 0 && ( -
  • - -
  • - )} -
  • - [TM: {item.talkManager?.fullname}] -
  • -
  • - -
  • - + + i + + + + )} + + {item.type} + + {duration || "??"} mins + + + + {item.status} + + + {item.title} + + {item.speakers.length > 0 && ( + + + + )} + + TM: {item.talkManager?.fullname} + + + + ); }; diff --git a/backend/custom_admin/src/components/schedule-builder/pending-items-basket/index.tsx b/backend/custom_admin/src/components/schedule-builder/pending-items-basket/index.tsx index 6b4c999ee5..f4ae6bb5cb 100644 --- a/backend/custom_admin/src/components/schedule-builder/pending-items-basket/index.tsx +++ b/backend/custom_admin/src/components/schedule-builder/pending-items-basket/index.tsx @@ -1,4 +1,6 @@ +import { IconButton, Text } from "@radix-ui/themes"; import clsx from "clsx"; +import { ChevronLeft, ChevronRight } from "lucide-react"; import { useEffect, useRef, useState } from "react"; import { useDrop } from "react-dnd"; @@ -76,9 +78,9 @@ export const PendingItemsBasket = () => { }, )} > - + Drop here to unassign from slot - +
    - {direction === "backwards" ? "👈" : "👉"} -
    + {direction === "backwards" ? : } + ); }; diff --git a/backend/custom_admin/src/components/schedule-builder/placeholder.tsx b/backend/custom_admin/src/components/schedule-builder/placeholder.tsx index 44262058f6..98f6f8d2bf 100644 --- a/backend/custom_admin/src/components/schedule-builder/placeholder.tsx +++ b/backend/custom_admin/src/components/schedule-builder/placeholder.tsx @@ -1,3 +1,4 @@ +import { Text } from "@radix-ui/themes"; import clsx from "clsx"; import { useDrop } from "react-dnd"; @@ -84,8 +85,9 @@ export const Placeholder = ({ gridRowEnd: rowEnd, }} > - {isMovingItemLoading && Please wait} - {!isMovingItemLoading && Drop / Add} + + {isMovingItemLoading ? "Please wait" : "Drop / Add"} +
    ); diff --git a/backend/custom_admin/src/components/schedule-builder/slot-creation.tsx b/backend/custom_admin/src/components/schedule-builder/slot-creation.tsx index da30c22c6b..0ae039c332 100644 --- a/backend/custom_admin/src/components/schedule-builder/slot-creation.tsx +++ b/backend/custom_admin/src/components/schedule-builder/slot-creation.tsx @@ -1,32 +1,35 @@ -import { Button } from "@radix-ui/themes"; +import { Button, Flex, Grid, Heading } from "@radix-ui/themes"; import { useCurrentConference } from "../utils/conference"; import { useCreateScheduleSlotMutation } from "./create-schedule-slot.generated"; export const SlotCreation = ({ dayId }) => { return ( -
    - - Add 15 mins slot - - - Add 30 mins slot - - - Add 45 mins slot - - - Add 60 mins slot - - - Break slot: Add 10 mins slot - - - Lunch slot: 2h 5 mins - - - Break slot: 30 mins - -
    + + Add slot + + + Add 15 mins slot + + + Add 30 mins slot + + + Add 45 mins slot + + + Add 60 mins slot + + + Break slot: Add 10 mins slot + + + Lunch slot: 2h 5 mins + + + Break slot: 30 mins + + + ); }; diff --git a/backend/custom_admin/src/components/shared/modal.tsx b/backend/custom_admin/src/components/shared/modal.tsx deleted file mode 100644 index f5fb63741e..0000000000 --- a/backend/custom_admin/src/components/shared/modal.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import clsx from "clsx"; -import { useEffect } from "react"; - -type Props = { - onClose: () => void; - children: React.ReactNode; - className?: string; - isOpen: boolean; -}; - -export const Modal = ({ isOpen, onClose, children, className }: Props) => { - useEffect(() => { - if (isOpen) { - document.body.style.overflow = "hidden"; - } else { - document.body.style.overflow = "auto"; - } - - return () => { - document.body.style.overflow = "auto"; - }; - }, [isOpen]); - - return ( -
    -
    -
    - {children} -
    -
    - ); -}; diff --git a/backend/custom_admin/src/custom-styles.css b/backend/custom_admin/src/custom-styles.css index 6d39885108..da6cf32626 100644 --- a/backend/custom_admin/src/custom-styles.css +++ b/backend/custom_admin/src/custom-styles.css @@ -25,14 +25,6 @@ } } -.btn { - @apply font-bold py-2 px-4 bg-slate-500 text-white hover:bg-slate-600; -} - -.btn:disabled { - @apply opacity-50 cursor-not-allowed; -} - .radix-themes.is-widget-theme { min-height: auto; width: 100%; From 58073a9a3d550bb1130170178ebe5649d66787ea Mon Sep 17 00:00:00 2001 From: Marco Acierno Date: Tue, 2 Jun 2026 15:55:56 +0200 Subject: [PATCH 2/3] Address PR review feedback - Delete the committed AI planning spec (no value as repo docs) - Add Dialog.Description to add-item modal (a11y; silences Radix warning) - Gate "No events found" on debouncedSearch so it doesn't show pre-search - Add type="button" to the calendar "Edit day in admin" Button - Add a return null fallback for unknown event __typename in search results Co-Authored-By: Claude Opus 4.8 (1M context) --- .../SPEC-schedule-builder-radix.md | 272 ------------------ .../schedule-builder/add-item-modal/index.tsx | 4 + .../add-item-modal/search-event.tsx | 10 +- .../components/schedule-builder/calendar.tsx | 2 +- 4 files changed, 12 insertions(+), 276 deletions(-) delete mode 100644 backend/custom_admin/SPEC-schedule-builder-radix.md diff --git a/backend/custom_admin/SPEC-schedule-builder-radix.md b/backend/custom_admin/SPEC-schedule-builder-radix.md deleted file mode 100644 index 873370e12c..0000000000 --- a/backend/custom_admin/SPEC-schedule-builder-radix.md +++ /dev/null @@ -1,272 +0,0 @@ -# Spec: Schedule Builder — Radix UI Migration - -## Objective - -Migrate the **custom_admin Schedule Builder** from its current ad-hoc UI (custom -`div` modal, native ``/` → TextField; remove console.log + empty else -│ ├── add-custom-event.tsx →TextField, list→Cards, .btn→Button -│ ├── proposal-preview.tsx .btn → Button;
  • / → Card/Text/Badge -│ ├── keynote-preview.tsx same as proposal-preview -│ └── info-recap.tsx grid+strong/span → Text (or DataList) -├── shared/ -│ ├── modal.tsx DELETE (only schedule-builder imports it) -│ ├── spacer.tsx reuse -│ └── base.tsx wrapper — leave -└── ... -src/custom-styles.css remove `.btn` rule once no longer referenced -``` - -## Code Style - -Follow the invitation-letter editor (`src/components/invitation-letter-document-builder/`). -Components from `@radix-ui/themes`, semantic props over Tailwind, `lucide-react` icons, -compound Dialog/AlertDialog pattern. Example (the reference modal pattern from -`editor-section.tsx`): - -```tsx -import { Dialog, Button, Flex, Text, TextField } from "@radix-ui/themes"; -import { Pencil } from "lucide-react"; - - !o && close()}> - - Add event to schedule - - - - - - -``` - -Select pattern (replacing the native ``/``→`TextField` (autofocus kept); `console.log` + empty `else` removed. - - Verify: `pnpm build`; typing searches. - - Files: `add-item-modal/search-event.tsx` - -- [ ] **T5 — add-custom-event**. - - Acceptance: native ``→`TextField`; `.btn`→`Button`; - quick-option list → Radix Cards/Buttons; create-by-hand validation unchanged. - - Verify: `pnpm build`; both create paths work; Select layers above Dialog. - - Files: `add-item-modal/add-custom-event.tsx` - -- [ ] **T6 — modal → Dialog + delete custom Modal**. - - Acceptance: `index.tsx` uses `Dialog.Root/Content/Title`; `shared/modal.tsx` deleted; - no manual `body.overflow` code remains; open/close still driven by `useAddItemModal`. - - Verify: `pnpm build`; `grep -rn shared/modal src` empty; dialog opens/closes/scroll-locks. - - Files: `add-item-modal/index.tsx`, **delete** `shared/modal.tsx` - -- [ ] **T7 — calendar header**. - - Acceptance: `

    `→`Heading`; "Edit day in admin" ` From e5a34e95913b106ed30219e8e9623b69c67b7ffb Mon Sep 17 00:00:00 2001 From: Marco Acierno Date: Tue, 2 Jun 2026 16:05:02 +0200 Subject: [PATCH 3/3] Address second round of PR review feedback - Only close the add-item modal on mutation success; surface a red Callout on graphql/network errors instead of closing silently (add-custom-event, proposal-preview, keynote-preview) - Consolidate the two divergent AVAILABILITY_BADGE maps into a single AVAILABILITY_META source of truth in utils/availability.ts, consumed by both the inline tooltip badge and the Radix Badge - Render the event-type Select with position="popper" to sidestep portal stacking against the Dialog overlay Modal form/search state already resets on reopen: closing sets data to null, which unmounts the search/custom-event subtree via the existing `{data && ...}` guard, so each open remounts with fresh useState. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../add-item-modal/add-custom-event.tsx | 42 +++++++--- .../add-item-modal/keynote-preview.tsx | 40 ++++++--- .../add-item-modal/proposal-preview.tsx | 84 +++++++++++-------- .../src/components/schedule-builder/item.tsx | 18 ++-- .../src/components/utils/availability.ts | 37 ++++++++ 5 files changed, 148 insertions(+), 73 deletions(-) diff --git a/backend/custom_admin/src/components/schedule-builder/add-item-modal/add-custom-event.tsx b/backend/custom_admin/src/components/schedule-builder/add-item-modal/add-custom-event.tsx index 6e7e303cbc..7cdfb20d89 100644 --- a/backend/custom_admin/src/components/schedule-builder/add-item-modal/add-custom-event.tsx +++ b/backend/custom_admin/src/components/schedule-builder/add-item-modal/add-custom-event.tsx @@ -1,5 +1,6 @@ import { Button, + Callout, Flex, Heading, Select, @@ -19,25 +20,40 @@ export const AddCustomEvent = () => { const { data, close } = useAddItemModal(); const { conferenceId } = useCurrentConference(); const [createScheduleItem] = useCreateScheduleItemMutation(); + const [error, setError] = useState(null); const onCreate = async ({ title, type, rooms }: CreateArgs) => { - await createScheduleItem({ - variables: { - input: { - conferenceId, - type: type, - title: title, - slotId: data.slot.id, - rooms: rooms.map((room) => room.id), - languageId: null, + setError(null); + try { + const { errors } = await createScheduleItem({ + variables: { + input: { + conferenceId, + type: type, + title: title, + slotId: data.slot.id, + rooms: rooms.map((room) => room.id), + languageId: null, + }, }, - }, - }); - close(); + }); + if (errors?.length) { + setError("Could not add to the schedule. Please try again."); + return; + } + close(); + } catch { + setError("Could not add to the schedule. Please try again."); + } }; return ( + {error && ( + + {error} + + )} @@ -164,7 +180,7 @@ const CustomByHand = ({ - + {TYPE_OPTIONS.map((option) => ( {option.label} diff --git a/backend/custom_admin/src/components/schedule-builder/add-item-modal/keynote-preview.tsx b/backend/custom_admin/src/components/schedule-builder/add-item-modal/keynote-preview.tsx index 5222f4852d..0be146b2a9 100644 --- a/backend/custom_admin/src/components/schedule-builder/add-item-modal/keynote-preview.tsx +++ b/backend/custom_admin/src/components/schedule-builder/add-item-modal/keynote-preview.tsx @@ -1,4 +1,5 @@ -import { Button, Card, Text } from "@radix-ui/themes"; +import { Button, Callout, Card, Text } from "@radix-ui/themes"; +import { useState } from "react"; import type { KeynoteFragmentFragment } from "../../fragments/keynote.generated"; import { useCurrentConference } from "../../utils/conference"; import { useAddItemModal } from "./context"; @@ -13,20 +14,30 @@ export const KeynotePreview = ({ const { conferenceId } = useCurrentConference(); const { data, close } = useAddItemModal(); const [createScheduleItem] = useCreateScheduleItemMutation(); + const [error, setError] = useState(null); const onAddToSchedule = async () => { - await createScheduleItem({ - variables: { - input: { - conferenceId, - type: "keynote", - keynoteId: keynote.id, - slotId: data.slot.id, - rooms: [data.room.id], + setError(null); + try { + const { errors } = await createScheduleItem({ + variables: { + input: { + conferenceId, + type: "keynote", + keynoteId: keynote.id, + slotId: data.slot.id, + rooms: [data.room.id], + }, }, - }, - }); - close(); + }); + if (errors?.length) { + setError("Could not add to the schedule. Please try again."); + return; + } + close(); + } catch { + setError("Could not add to the schedule. Please try again."); + } }; return ( @@ -46,6 +57,11 @@ export const KeynotePreview = ({ + {error && ( + + {error} + + )} ); }; diff --git a/backend/custom_admin/src/components/schedule-builder/add-item-modal/proposal-preview.tsx b/backend/custom_admin/src/components/schedule-builder/add-item-modal/proposal-preview.tsx index 11017ff47e..e39c029b1f 100644 --- a/backend/custom_admin/src/components/schedule-builder/add-item-modal/proposal-preview.tsx +++ b/backend/custom_admin/src/components/schedule-builder/add-item-modal/proposal-preview.tsx @@ -1,22 +1,17 @@ -import { Badge, Button, Card, Flex, Text } from "@radix-ui/themes"; +import { Badge, Button, Callout, Card, Flex, Text } from "@radix-ui/themes"; +import { useState } from "react"; import type { Language } from "../../../types"; import type { SubmissionFragmentFragment } from "../../fragments/submission.generated"; import type { AvailabilityValue } from "../../utils/availability"; -import { getSlotAvailabilityKey } from "../../utils/availability"; +import { + AVAILABILITY_META, + getSlotAvailabilityKey, +} from "../../utils/availability"; import { useCurrentConference } from "../../utils/conference"; import { useAddItemModal } from "./context"; import { useCreateScheduleItemMutation } from "./create-schedule-item.generated"; import { InfoRecap } from "./info-recap"; -const AVAILABILITY_BADGE: Record< - AvailabilityValue, - { label: string; color: "green" | "blue" | "red" } -> = { - preferred: { label: "Preferred", color: "green" }, - available: { label: "Available", color: "blue" }, - unavailable: { label: "Unavailable", color: "red" }, -}; - type Props = { proposal: SubmissionFragmentFragment; }; @@ -50,8 +45,8 @@ export const ProposalPreview = ({ proposal }: Props) => { )}

  • {slotAvailability && ( - - {AVAILABILITY_BADGE[slotAvailability].label} + + {AVAILABILITY_META[slotAvailability].label} )} @@ -72,35 +67,52 @@ const AddActions = ({ proposal }: { proposal: SubmissionFragmentFragment }) => { const { conferenceId } = useCurrentConference(); const { data, close } = useAddItemModal(); const [createScheduleItem] = useCreateScheduleItemMutation(); + const [error, setError] = useState(null); const languages = proposal.languages; const onCreate = async (language: Language) => { - await createScheduleItem({ - variables: { - input: { - conferenceId, - type: - proposal.type.name.toLowerCase() === "talk" ? "talk" : "training", - proposalId: proposal.id, - languageId: language.id, - slotId: data.slot.id, - rooms: [data.room.id], + setError(null); + try { + const { errors } = await createScheduleItem({ + variables: { + input: { + conferenceId, + type: + proposal.type.name.toLowerCase() === "talk" ? "talk" : "training", + proposalId: proposal.id, + languageId: language.id, + slotId: data.slot.id, + rooms: [data.room.id], + }, }, - }, - }); - close(); + }); + if (errors?.length) { + setError("Could not add to the schedule. Please try again."); + return; + } + close(); + } catch { + setError("Could not add to the schedule. Please try again."); + } }; return ( - - {languages.map((language) => ( - - ))} + + + {languages.map((language) => ( + + ))} + + {error && ( + + {error} + + )} ); }; diff --git a/backend/custom_admin/src/components/schedule-builder/item.tsx b/backend/custom_admin/src/components/schedule-builder/item.tsx index 4454506919..274b30bfa0 100644 --- a/backend/custom_admin/src/components/schedule-builder/item.tsx +++ b/backend/custom_admin/src/components/schedule-builder/item.tsx @@ -4,7 +4,10 @@ import { Badge, Button, Card, Flex, Text, Tooltip } from "@radix-ui/themes"; import type { ScheduleItemFragmentFragment } from "../fragments/schedule-item.generated"; import { useDjangoAdminEditor } from "../shared/django-admin-editor-modal/context"; import type { AvailabilityValue } from "../utils/availability"; -import { getSlotAvailabilityKey } from "../utils/availability"; +import { + AVAILABILITY_META, + getSlotAvailabilityKey, +} from "../utils/availability"; import { convertHoursToMinutes } from "../utils/time"; // Only the primary speaker's availability is checked. Co-speakers are not asked @@ -20,20 +23,11 @@ function getSpeakerAvailability( return availabilities[getSlotAvailabilityKey(date, slotHour)] ?? null; } -const AVAILABILITY_BADGE: Record< - AvailabilityValue, - { bg: string; text: string; label: string } -> = { - preferred: { bg: "#dcfce7", text: "#15803d", label: "★ Preferred" }, - available: { bg: "#dbeafe", text: "#1d4ed8", label: "✓ Available" }, - unavailable: { bg: "#fee2e2", text: "#b91c1c", label: "✗ Unavailable" }, -}; - function AvailabilityBadge({ value, }: { value: AvailabilityValue | undefined }) { if (!value) return ; - const { bg, text, label } = AVAILABILITY_BADGE[value]; + const { bg, text, glyph, label } = AVAILABILITY_META[value]; return ( - {label} + {glyph} {label} ); } diff --git a/backend/custom_admin/src/components/utils/availability.ts b/backend/custom_admin/src/components/utils/availability.ts index 75781346de..176059eeb9 100644 --- a/backend/custom_admin/src/components/utils/availability.ts +++ b/backend/custom_admin/src/components/utils/availability.ts @@ -1,5 +1,42 @@ export type AvailabilityValue = "preferred" | "available" | "unavailable"; +// Single source of truth for how each availability value is presented. +// Both the inline-styled tooltip badge (item.tsx) and the Radix Badge +// (proposal-preview.tsx) read from here, so adding a new AvailabilityValue +// forces updating one map and the Record type stays exhaustive. +export const AVAILABILITY_META: Record< + AvailabilityValue, + { + label: string; + glyph: string; + color: "green" | "blue" | "red"; + bg: string; + text: string; + } +> = { + preferred: { + label: "Preferred", + glyph: "★", + color: "green", + bg: "#dcfce7", + text: "#15803d", + }, + available: { + label: "Available", + glyph: "✓", + color: "blue", + bg: "#dbeafe", + text: "#1d4ed8", + }, + unavailable: { + label: "Unavailable", + glyph: "✗", + color: "red", + bg: "#fee2e2", + text: "#b91c1c", + }, +}; + // Availability is stored at half-day granularity: "am" (before 12:00) or "pm" (12:00 and after). // A slot at 09:00 and one at 11:30 map to the same "am" bucket. The badge reflects the // half-day preference, not the exact start time of the slot.