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 b06e44ffd8..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,3 +1,12 @@ +import { + Button, + Callout, + Flex, + Heading, + Select, + Text, + TextField, +} from "@radix-ui/themes"; import { useState } from "react"; import type { Room } from "../../../types"; @@ -5,44 +14,57 @@ import { useCurrentConference } from "../../utils/conference"; import { useAddItemModal } from "./context"; import { useCreateScheduleItemMutation } from "./create-schedule-item.generated"; +type CreateArgs = { title: string; type: string; rooms: Room[] }; + export const AddCustomEvent = () => { const { data, close } = useAddItemModal(); const { conferenceId } = useCurrentConference(); const [createScheduleItem] = useCreateScheduleItemMutation(); + const [error, setError] = useState(null); - const onCreate = async ({ - title, - type, - rooms, - }: { - title: string; - type: string; - rooms: Room[]; - }) => { - await createScheduleItem({ - variables: { - input: { - conferenceId, - type: type, - title: title, - slotId: data.slot.id, - rooms: rooms.map((room) => room.id), - languageId: null, + const onCreate = async ({ title, type, rooms }: CreateArgs) => { + 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} + + )} -
+ ); }; -const CustomDefinedOptions = ({ onCreate }) => { +const CustomDefinedOptions = ({ + onCreate, +}: { + onCreate: (args: CreateArgs) => void; +}) => { const { data: { day: { rooms }, @@ -52,9 +74,9 @@ const CustomDefinedOptions = ({ onCreate }) => { const allRooms = rooms; return ( - <> - Add Custom from list - - + + ); }; -const Option = ({ children, type, rooms, onClick }) => { +const Option = ({ + children, + type, + rooms, + onClick, +}: { + children: string; + type: string; + rooms: Room[]; + onClick: (args: CreateArgs) => void; +}) => { return ( -
  • { - onClick({ - title: children, - type, - rooms, - }); - }} - className="p-2 bg-slate-300 odd:bg-slate-200 cursor-pointer hover:bg-slate-400" +
  • + ); }; -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 +162,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..54ced6560a 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,28 @@ -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 + + Search an existing proposal or keynote, or create a custom event for + this slot. + + {/* 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..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,3 +1,5 @@ +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"; @@ -12,25 +14,37 @@ 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 ( -
  • - {keynote.title} + + + {keynote.title} + - -
  • + + {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 a3835583e8..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,27 +1,17 @@ +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_STYLES: Record< - AvailabilityValue, - { label: string; className: string } -> = { - 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", - }, -}; - type Props = { proposal: SubmissionFragmentFragment; }; @@ -42,22 +32,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_META[slotAvailability].label} + )} -
    + { ]} /> -
  • + ); }; @@ -75,36 +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/add-item-modal/search-event.tsx b/backend/custom_admin/src/components/schedule-builder/add-item-modal/search-event.tsx index 918f9c2c0a..a322992842 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,45 @@ 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 + + Search proposal / keynote + + {loading && Searching events} + {!loading && + debouncedSearch && + data?.searchEvents.results.length === 0 && ( + No events found )} - {debouncedSearch && data?.searchEvents.results.length > 0 && ( -
      - {data.searchEvents.results.map((event) => { - if (event.__typename === "Submission") { - return ; - } + {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 ; + } + + return null; + })} + + )} + ); }; diff --git a/backend/custom_admin/src/components/schedule-builder/calendar.tsx b/backend/custom_admin/src/components/schedule-builder/calendar.tsx index 9b32cb5500..738c52c50d 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}

    - +
    = { - 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} ); } @@ -157,7 +151,7 @@ export const Item = ({ .slice(currentSlotIndex, endingSlotIndex) .reduce((acc, s) => 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/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. 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%;