diff --git a/src/components/PhotoSourcePicker.tsx b/src/components/PhotoSourcePicker.tsx new file mode 100644 index 0000000..015488c --- /dev/null +++ b/src/components/PhotoSourcePicker.tsx @@ -0,0 +1,76 @@ +import { useMemo, useRef } from "react"; +import { Camera, Images } from "lucide-react"; +import { isMobileUa } from "../lib/photoWallShare"; + +type PhotoSourcePickerProps = { + disabled?: boolean; + picking?: boolean; + hasFile?: boolean; + onPick: (file: File | null) => void; +}; + +const btnClass = + "inline-flex h-11 w-full items-center justify-center gap-2 rounded-lg border border-zinc-300 bg-zinc-100 px-4 text-sm font-semibold text-gray-600 disabled:opacity-55"; + +/** + * Mobile: gallery + camera (capture forces camera-only on iOS/Android). + * Desktop: one “Upload Photo” control that uses the gallery/file input. + */ +export function PhotoSourcePicker({ disabled, picking, hasFile, onPick }: PhotoSourcePickerProps) { + const galleryRef = useRef(null); + const cameraRef = useRef(null); + const isMobile = useMemo(() => isMobileUa(), []); + + const emitFile = (input: HTMLInputElement | null, selected: File | null) => { + onPick(selected); + if (input) input.value = ""; + }; + + const galleryLabel = picking + ? "Processing…" + : isMobile + ? "Gallery" + : hasFile + ? "Change photo" + : "Upload Photo"; + + return ( +
+ emitFile(galleryRef.current, e.target.files?.[0] || null)} + /> + emitFile(cameraRef.current, e.target.files?.[0] || null)} + /> + + {isMobile ? ( + + ) : null} +
+ ); +} diff --git a/src/components/PhotoWallPresentChrome.tsx b/src/components/PhotoWallPresentChrome.tsx new file mode 100644 index 0000000..5f177c0 --- /dev/null +++ b/src/components/PhotoWallPresentChrome.tsx @@ -0,0 +1,137 @@ +import { Calendar, MapPin } from "lucide-react"; +import type { PhotoWallEventHeaderDisplay, PhotoWallPublicEvent } from "../lib/api"; +import { formatEventScheduleRange } from "../lib/sessionDisplay"; +import { EventBanner } from "./EventBanner"; +import { EventBuzzLogo } from "./EventBuzzLogo"; + +const EVENT_TYPE_LABEL: Record = { + conference: "Conference", + workshop: "Workshop", + summit: "Summit", + cme: "CME", + webinar: "Webinar", +}; + +function resolveEventHeader(display?: PhotoWallEventHeaderDisplay | null) { + return { + showLogo: display?.showLogo !== false, + showBanner: display?.showBanner === true, + showTitle: display?.showTitle !== false, + showDateRange: display?.showDateRange !== false, + showEventType: display?.showEventType !== false, + showLocationType: display?.showLocationType !== false, + showVenue: display?.showVenue !== false, + }; +} + +function formatEventType(t?: string): string { + if (!t) return ""; + return EVENT_TYPE_LABEL[t] || t.replace(/^\w/, (c) => c.toUpperCase()); +} + +function formatLocationKind(locationType?: string): string { + if (locationType === "virtual") return "Virtual"; + if (locationType === "hybrid") return "Hybrid"; + if (locationType === "physical") return "In person"; + return locationType?.trim() || ""; +} + +export function PhotoWallPresentChrome({ + event, + eventHeader, + eventNameFallback = "", +}: { + event?: PhotoWallPublicEvent | null; + eventHeader?: PhotoWallEventHeaderDisplay | null; + eventNameFallback?: string; +}) { + const flags = resolveEventHeader(eventHeader); + const eventName = event?.name?.trim() || eventNameFallback || "Event"; + const bannerSrc = event?.imageUrl?.trim() || ""; + const logoSrc = event?.logoUrl?.trim() || ""; + const schedule = event?.startDate + ? formatEventScheduleRange( + typeof event.startDate === "string" ? event.startDate : String(event.startDate), + event.endDate + ? typeof event.endDate === "string" + ? event.endDate + : String(event.endDate) + : String(event.startDate), + ) + : ""; + const eventTypeLabel = formatEventType(event?.eventType); + const locationKind = formatLocationKind(event?.locationType); + const eventLocation = event?.location?.trim() || ""; + const showDate = flags.showDateRange && Boolean(schedule); + const showType = flags.showEventType && Boolean(eventTypeLabel); + const showLocationKind = flags.showLocationType && Boolean(locationKind); + const showVenue = flags.showVenue && Boolean(eventLocation); + const showTitle = flags.showTitle && Boolean(eventName); + const showEventInfo = showTitle || showDate || showType || showLocationKind || showVenue; + const showHeader = flags.showLogo || showEventInfo; + + if (!showHeader && !flags.showBanner) return null; + + return ( + <> + {showHeader ? ( +
+
+ {flags.showLogo ? ( + logoSrc ? ( + {eventName} + ) : ( + + ) + ) : ( + + )} +
+ {showEventInfo ? ( +
+
+ {showType ? ( +
+ {eventTypeLabel} +
+ ) : null} + {showTitle ? ( +

{eventName}

+ ) : null} +
+
+ {showDate ? ( +
+ + {schedule} +
+ ) : null} + {showLocationKind || showVenue ? ( +
+ {showLocationKind ? {locationKind} : null} + {showVenue ? ( + + + {eventLocation} + + ) : null} +
+ ) : null} +
+ +
+ ) : null} +
+ ) : null} + {flags.showBanner ? ( + + ) : null} + + ); +} diff --git a/src/components/present/PresentFooterNav.tsx b/src/components/present/PresentFooterNav.tsx index 7a9266b..49dbc94 100644 --- a/src/components/present/PresentFooterNav.tsx +++ b/src/components/present/PresentFooterNav.tsx @@ -12,13 +12,13 @@ export function PresentFooterNav({ nextDisabled: boolean; }) { return ( -