@burdenoff/microfe-hookmovies 2026.706.2 → 2026.706.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/dist/hookmovies/HookMoviesRoutes.js +54 -6
  2. package/dist/hookmovies/HookMoviesRoutes.js.map +1 -1
  3. package/dist/hookmovies/components/index.js +1 -1
  4. package/dist/hookmovies/context/HookMoviesContext.js +64 -40
  5. package/dist/hookmovies/context/HookMoviesContext.js.map +1 -1
  6. package/dist/hookmovies/hooks/useHookMoviesApi.js +592 -134
  7. package/dist/hookmovies/hooks/useHookMoviesApi.js.map +1 -1
  8. package/dist/hookmovies/pages/HomePage.js +211 -139
  9. package/dist/hookmovies/pages/HomePage.js.map +1 -1
  10. package/dist/hookmovies/pages/ai/ConciergePage.js +330 -0
  11. package/dist/hookmovies/pages/ai/ConciergePage.js.map +1 -0
  12. package/dist/hookmovies/pages/analytics/AnalyticsPage.js +450 -0
  13. package/dist/hookmovies/pages/analytics/AnalyticsPage.js.map +1 -0
  14. package/dist/hookmovies/pages/analytics/PayoutsPage.js +499 -0
  15. package/dist/hookmovies/pages/analytics/PayoutsPage.js.map +1 -0
  16. package/dist/hookmovies/pages/auditions/AuditionDetailPage.js +223 -140
  17. package/dist/hookmovies/pages/auditions/AuditionDetailPage.js.map +1 -1
  18. package/dist/hookmovies/pages/auditions/AuditionsPage.js +161 -83
  19. package/dist/hookmovies/pages/auditions/AuditionsPage.js.map +1 -1
  20. package/dist/hookmovies/pages/catalog/PeoplePage.js +177 -103
  21. package/dist/hookmovies/pages/catalog/PeoplePage.js.map +1 -1
  22. package/dist/hookmovies/pages/catalog/StudiosPage.js +191 -108
  23. package/dist/hookmovies/pages/catalog/StudiosPage.js.map +1 -1
  24. package/dist/hookmovies/pages/catalog/TitleDetailPage.js +744 -622
  25. package/dist/hookmovies/pages/catalog/TitleDetailPage.js.map +1 -1
  26. package/dist/hookmovies/pages/catalog/TitlesPage.js +218 -138
  27. package/dist/hookmovies/pages/catalog/TitlesPage.js.map +1 -1
  28. package/dist/hookmovies/pages/commerce/CheckoutPage.js +430 -0
  29. package/dist/hookmovies/pages/commerce/CheckoutPage.js.map +1 -0
  30. package/dist/hookmovies/pages/consumer/DiscoveryPage.js +21 -21
  31. package/dist/hookmovies/pages/consumer/HooksPage.js +5 -5
  32. package/dist/hookmovies/pages/consumer/TitleViewPage.js +32 -32
  33. package/dist/hookmovies/pages/consumer/TitleViewPage.js.map +1 -1
  34. package/dist/hookmovies/pages/developer/DevPortalPage.js +815 -0
  35. package/dist/hookmovies/pages/developer/DevPortalPage.js.map +1 -0
  36. package/dist/hookmovies/pages/distribution/RightsWindowsPage.js +235 -162
  37. package/dist/hookmovies/pages/distribution/RightsWindowsPage.js.map +1 -1
  38. package/dist/hookmovies/pages/engagement/ReviewsPage.js +201 -143
  39. package/dist/hookmovies/pages/engagement/ReviewsPage.js.map +1 -1
  40. package/dist/hookmovies/pages/library/EntitlementsPage.js +447 -221
  41. package/dist/hookmovies/pages/library/EntitlementsPage.js.map +1 -1
  42. package/dist/hookmovies/pages/library/ListsPage.js +243 -167
  43. package/dist/hookmovies/pages/library/ListsPage.js.map +1 -1
  44. package/dist/hookmovies/pages/live/LivePage.js +608 -0
  45. package/dist/hookmovies/pages/live/LivePage.js.map +1 -0
  46. package/dist/hookmovies/pages/localization/LocalizationPage.js +656 -0
  47. package/dist/hookmovies/pages/localization/LocalizationPage.js.map +1 -0
  48. package/dist/hookmovies/pages/onboarding/OnboardingPage.js +366 -0
  49. package/dist/hookmovies/pages/onboarding/OnboardingPage.js.map +1 -0
  50. package/dist/hookmovies/pages/productions/ProductionDetailPage.js +265 -179
  51. package/dist/hookmovies/pages/productions/ProductionDetailPage.js.map +1 -1
  52. package/dist/hookmovies/pages/productions/ProductionsPage.js +264 -71
  53. package/dist/hookmovies/pages/productions/ProductionsPage.js.map +1 -1
  54. package/dist/hookmovies/pages/social/ProfilesPage.js +701 -0
  55. package/dist/hookmovies/pages/social/ProfilesPage.js.map +1 -0
  56. package/dist/hookmovies/pages/social/WatchPartyListPage.js +281 -0
  57. package/dist/hookmovies/pages/social/WatchPartyListPage.js.map +1 -0
  58. package/dist/hookmovies/pages/social/WatchPartyRoomPage.js +567 -0
  59. package/dist/hookmovies/pages/social/WatchPartyRoomPage.js.map +1 -0
  60. package/dist/hookmovies/pages/trust/ModerationPage.js +320 -0
  61. package/dist/hookmovies/pages/trust/ModerationPage.js.map +1 -0
  62. package/package.json +1 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"LocalizationPage.js","names":[],"sources":["../../../../src/hookmovies/pages/localization/LocalizationPage.tsx"],"sourcesContent":["/**\n * Publishing & Localization (studio)\n * @module hookmovies/pages/localization\n *\n * The publishing control room for a single title. Two responsibilities:\n *\n * 1. Release scheduler — pick the `releaseDate` (persisted via `hokUpdateTitle`)\n * and walk the publication lifecycle with a status stepper wired to\n * `hokTransitionTitle` (the stepper mirrors the svc `TITLE_TRANSITIONS`\n * state machine so only legal moves are offered).\n *\n * 2. Localization tracks — per-language Subtitle / Caption / Dub rows. Track\n * files are uploaded to wspace-files-svc (`uploadFileToFiles`, this MFE\n * stores no blobs) and referenced by id via `hokCreateLocalization`. Tracks\n * can be marked default (`hokSetDefaultLocalization`) or removed\n * (`hokDeleteLocalization`). These rows become the player's `<track>`s.\n *\n * The title id comes from the route (`title/:id/localization`). All hooks are\n * consumed from the data layer — this page adds no new operations.\n */\n\nimport { type FC, type FormEvent, type ChangeEvent, useMemo, useRef, useState } from 'react';\nimport { useParams } from 'react-router-dom';\nimport {\n ArrowLeft,\n CalendarClock,\n Captions,\n CheckCircle2,\n Download,\n Languages,\n Loader2,\n Mic,\n Plus,\n Rocket,\n Star,\n Trash2,\n Upload,\n X,\n type LucideIcon,\n} from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { StatusBadge } from '../../components/StatusBadge';\nimport { LoadingState, ErrorState } from '../../components/StateViews';\nimport { EmptyState } from '../../components/EmptyState';\nimport { useHookMovies } from '../../context/HookMoviesContext';\nimport {\n useTitle,\n useUpdateTitle,\n useTransitionTitle,\n useLocalizations,\n useCreateLocalization,\n useSetDefaultLocalization,\n useDeleteLocalization,\n useLanguages,\n type HokTitleStatus,\n type HokLocalization,\n type HokLocalizationKind,\n type UpdateTitleInput,\n} from '../../hooks/useHookMoviesApi';\nimport { uploadFileToFiles, useFileUrl } from '../../hooks/useFiles';\nimport { formatDate, formatEnumLabel } from '../../utils/formatters';\nimport { cn } from '../../utils/cn';\n\n// ── Publication lifecycle ─────────────────────────────────────────────────────\n// The linear \"happy path\" the stepper visualizes. UNPUBLISHED / ARCHIVED are\n// off-pipeline terminal-ish states surfaced only as transition actions.\nconst PIPELINE: readonly HokTitleStatus[] = ['DRAFT', 'IN_REVIEW', 'SCHEDULED', 'PUBLISHED'];\n\n// Mirrors wspace-hookmovies-svc `TITLE_TRANSITIONS` (catalog.service.ts) exactly\n// so the stepper never offers an illegal move the backend would reject.\nconst NEXT_STATUSES: Record<HokTitleStatus, HokTitleStatus[]> = {\n DRAFT: ['IN_REVIEW', 'ARCHIVED'],\n IN_REVIEW: ['SCHEDULED', 'PUBLISHED', 'DRAFT', 'ARCHIVED'],\n SCHEDULED: ['PUBLISHED', 'IN_REVIEW', 'ARCHIVED'],\n PUBLISHED: ['UNPUBLISHED', 'ARCHIVED'],\n UNPUBLISHED: ['PUBLISHED', 'ARCHIVED'],\n ARCHIVED: [],\n};\n\n// ── Localization kinds ────────────────────────────────────────────────────────\n// Folder + accepted file types per track kind. Subtitles/Captions are timed\n// text (.vtt/.srt); Dubs are audio. Upload folders match the task contract.\ninterface KindMeta {\n label: string;\n icon: LucideIcon;\n folder: string;\n accept: string;\n hint: string;\n}\n\nconst KIND_META: Record<HokLocalizationKind, KindMeta> = {\n SUBTITLE: {\n label: 'Subtitle',\n icon: Captions,\n folder: '/HookMovies/Subtitles',\n accept: '.vtt,.srt,text/vtt,application/x-subrip',\n hint: 'Timed-text subtitles (.vtt or .srt).',\n },\n CAPTION: {\n label: 'Caption (SDH)',\n icon: Captions,\n folder: '/HookMovies/Subtitles',\n accept: '.vtt,.srt,text/vtt,application/x-subrip',\n hint: 'Captions for deaf / hard-of-hearing viewers (.vtt or .srt).',\n },\n DUB: {\n label: 'Dub',\n icon: Mic,\n folder: '/HookMovies/Dubs',\n accept: 'audio/*',\n hint: 'Localized audio track (dubbed).',\n },\n};\n\nconst KIND_OPTIONS: HokLocalizationKind[] = ['SUBTITLE', 'CAPTION', 'DUB'];\n\nconst fieldClass =\n 'w-full rounded-lg border border-border-subtle bg-bg-surface px-3 py-2 text-sm text-text-primary placeholder:text-text-secondary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]';\nconst labelClass = 'mb-1.5 block text-xs font-medium text-text-secondary';\n\n/** ISO string → `<input type=\"datetime-local\">` value in the viewer's timezone. */\nfunction isoToLocalInput(iso: string | null | undefined): string {\n if (!iso) return '';\n const d = new Date(iso);\n if (Number.isNaN(d.getTime())) return '';\n const pad = (n: number) => String(n).padStart(2, '0');\n return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`;\n}\n\n// ── Localization row ──────────────────────────────────────────────────────────\ninterface TrackRowProps {\n track: HokLocalization;\n onSetDefault: (track: HokLocalization) => void;\n onDelete: (track: HokLocalization) => void;\n settingDefault: boolean;\n deleting: boolean;\n}\n\n/** One localization track row — resolves its file id to a download URL for QA. */\nconst TrackRow: FC<TrackRowProps> = ({\n track,\n onSetDefault,\n onDelete,\n settingDefault,\n deleting,\n}) => {\n const fileUrl = useFileUrl(track.fileId);\n const meta = KIND_META[track.kind];\n const KindIcon = meta.icon;\n\n return (\n <tr className=\"border-t border-border-seam\">\n <td className=\"whitespace-nowrap px-4 py-3\">\n <span className=\"inline-flex items-center gap-2 text-sm font-medium text-text-primary\">\n <span className=\"rounded-md bg-bg-sunken px-2 py-0.5 text-xs uppercase tracking-wide text-text-secondary\">\n {track.language}\n </span>\n {track.localizedTitle && (\n <span className=\"truncate text-xs text-text-secondary\">{track.localizedTitle}</span>\n )}\n </span>\n </td>\n <td className=\"whitespace-nowrap px-4 py-3\">\n <span className=\"inline-flex items-center gap-1.5 text-sm text-text-primary\">\n <KindIcon className=\"h-3.5 w-3.5 text-text-secondary\" aria-hidden=\"true\" />\n {meta.label}\n </span>\n </td>\n <td className=\"whitespace-nowrap px-4 py-3\">\n <StatusBadge status={track.status} />\n </td>\n <td className=\"whitespace-nowrap px-4 py-3\">\n {track.isDefault ? (\n <span className=\"inline-flex items-center gap-1 rounded-full bg-status-success-bg-subtle px-2.5 py-0.5 text-xs font-medium text-status-success-text\">\n <Star className=\"h-3 w-3 fill-current\" aria-hidden=\"true\" />\n Default\n </span>\n ) : (\n <button\n type=\"button\"\n disabled={settingDefault}\n onClick={() => onSetDefault(track)}\n className=\"inline-flex items-center gap-1.5 rounded-lg border border-border-seam px-2.5 py-1 text-xs font-medium text-text-primary transition-colors hover:bg-bg-sunken disabled:opacity-50\"\n >\n <Star className=\"h-3.5 w-3.5\" aria-hidden=\"true\" />\n Make default\n </button>\n )}\n </td>\n <td className=\"whitespace-nowrap px-4 py-3\">\n {fileUrl.isLoading ? (\n <span className=\"inline-flex items-center gap-1.5 text-xs text-text-secondary\">\n <Loader2 className=\"h-3.5 w-3.5 animate-spin\" aria-hidden=\"true\" />\n Resolving…\n </span>\n ) : fileUrl.data ? (\n <a\n href={fileUrl.data}\n target=\"_blank\"\n rel=\"noreferrer\"\n download\n className=\"inline-flex items-center gap-1.5 rounded-lg border border-border-seam px-2.5 py-1 text-xs font-medium text-text-primary transition-colors hover:bg-bg-sunken\"\n >\n <Download className=\"h-3.5 w-3.5\" aria-hidden=\"true\" />\n File\n </a>\n ) : (\n <span className=\"text-xs text-text-muted\">Unavailable</span>\n )}\n </td>\n <td className=\"whitespace-nowrap px-4 py-3 text-right\">\n <button\n type=\"button\"\n disabled={deleting}\n onClick={() => onDelete(track)}\n aria-label={`Delete ${meta.label} track (${track.language})`}\n className=\"inline-flex items-center gap-1.5 rounded-lg border border-status-error-border px-2.5 py-1 text-xs font-medium text-status-error-text transition-colors hover:bg-status-error-bg-subtle disabled:opacity-50\"\n >\n {deleting ? (\n <Loader2 className=\"h-3.5 w-3.5 animate-spin\" aria-hidden=\"true\" />\n ) : (\n <Trash2 className=\"h-3.5 w-3.5\" aria-hidden=\"true\" />\n )}\n Delete\n </button>\n </td>\n </tr>\n );\n};\n\nexport const LocalizationPage: FC = () => {\n const { id } = useParams<{ id: string }>();\n const { navigateTo } = useHookMovies();\n\n const { data: title, isLoading, isError, refetch } = useTitle(id);\n const updateTitle = useUpdateTitle();\n const transition = useTransitionTitle();\n\n const localizations = useLocalizations(id);\n const createLocalization = useCreateLocalization();\n const setDefault = useSetDefaultLocalization();\n const deleteLocalization = useDeleteLocalization();\n\n const languages = useLanguages({ status: 'ACTIVE' }, 200, 0);\n\n // ── Release scheduler state ─────────────────────────────────────────────────\n const [releaseInput, setReleaseInput] = useState('');\n const [releaseTouched, setReleaseTouched] = useState(false);\n const [releaseSaved, setReleaseSaved] = useState(false);\n const [scheduleError, setScheduleError] = useState<string | null>(null);\n\n // ── Add-localization form state ─────────────────────────────────────────────\n const [showAdd, setShowAdd] = useState(false);\n const [formLanguage, setFormLanguage] = useState('');\n const [formKind, setFormKind] = useState<HokLocalizationKind>('SUBTITLE');\n const [formFile, setFormFile] = useState<File | null>(null);\n const [formLocalizedTitle, setFormLocalizedTitle] = useState('');\n const [formLocalizedSynopsis, setFormLocalizedSynopsis] = useState('');\n const [formIsDefault, setFormIsDefault] = useState(false);\n const [uploading, setUploading] = useState(false);\n const [formError, setFormError] = useState<string | null>(null);\n const fileInputRef = useRef<HTMLInputElement>(null);\n\n // ── Per-row action tracking ────────────────────────────────────────────────\n const [defaultingId, setDefaultingId] = useState<string | null>(null);\n const [deletingId, setDeletingId] = useState<string | null>(null);\n const [rowError, setRowError] = useState<string | null>(null);\n\n const languageOptions = languages.data?.items ?? [];\n\n const tracks = useMemo(() => {\n const rows = localizations.data ?? [];\n return [...rows].sort((a, b) => {\n const kindDelta = KIND_OPTIONS.indexOf(a.kind) - KIND_OPTIONS.indexOf(b.kind);\n if (kindDelta !== 0) return kindDelta;\n return a.language.localeCompare(b.language);\n });\n }, [localizations.data]);\n\n if (isLoading) {\n return (\n <PageLayout title=\"Publishing & Localization\">\n <LoadingState label=\"Loading title…\" />\n </PageLayout>\n );\n }\n\n if (isError || !title) {\n return (\n <PageLayout title=\"Publishing & Localization\">\n <ErrorState\n title=\"Title not found\"\n message=\"This title may have been archived, or you may not have access to it.\"\n onRetry={() => void refetch()}\n />\n </PageLayout>\n );\n }\n\n // Effective datetime-local value: the in-progress edit, else the persisted date.\n const releaseValue = releaseTouched ? releaseInput : isoToLocalInput(title.releaseDate);\n const currentIndex = PIPELINE.indexOf(title.status);\n const isOffPipeline = currentIndex === -1;\n\n const handleSaveRelease = async (e: FormEvent) => {\n e.preventDefault();\n setScheduleError(null);\n setReleaseSaved(false);\n // releaseDate accepts null to clear; HokUpdateTitleInput.releaseDate (schema)\n // is a valid field though the shared UpdateTitleInput type predates it — an\n // intersection keeps this strictly typed without touching the data layer.\n const releaseDate = releaseValue.trim() ? new Date(releaseValue).toISOString() : null;\n const input: UpdateTitleInput & { releaseDate: string | null } = { releaseDate };\n try {\n await updateTitle.mutateAsync({ id: title.id, input });\n setReleaseTouched(false);\n setReleaseSaved(true);\n } catch (err) {\n setScheduleError(err instanceof Error ? err.message : 'Could not save the release date.');\n }\n };\n\n const handleTransition = async (status: HokTitleStatus) => {\n setScheduleError(null);\n try {\n await transition.mutateAsync({ id: title.id, status });\n } catch (err) {\n setScheduleError(err instanceof Error ? err.message : 'Transition failed.');\n }\n };\n\n const resetAddForm = () => {\n setFormLanguage('');\n setFormKind('SUBTITLE');\n setFormFile(null);\n setFormLocalizedTitle('');\n setFormLocalizedSynopsis('');\n setFormIsDefault(false);\n setFormError(null);\n if (fileInputRef.current) fileInputRef.current.value = '';\n };\n\n const handleFileSelect = (e: ChangeEvent<HTMLInputElement>) => {\n setFormFile(e.target.files?.[0] ?? null);\n };\n\n const handleAddSubmit = async (e: FormEvent) => {\n e.preventDefault();\n setFormError(null);\n if (!formLanguage.trim()) {\n setFormError('Select a language.');\n return;\n }\n if (!formFile) {\n setFormError('Choose a track file to upload.');\n return;\n }\n setUploading(true);\n try {\n const uploaded = await uploadFileToFiles(formFile, KIND_META[formKind].folder);\n await createLocalization.mutateAsync({\n titleId: title.id,\n language: formLanguage.trim(),\n kind: formKind,\n fileId: uploaded.id,\n isDefault: formIsDefault,\n localizedTitle: formLocalizedTitle.trim() || undefined,\n localizedSynopsis: formLocalizedSynopsis.trim() || undefined,\n });\n resetAddForm();\n setShowAdd(false);\n } catch (err) {\n setFormError(err instanceof Error ? err.message : 'Failed to add localization track.');\n } finally {\n setUploading(false);\n }\n };\n\n const handleSetDefault = async (track: HokLocalization) => {\n setRowError(null);\n setDefaultingId(track.id);\n try {\n await setDefault.mutateAsync(track.id);\n } catch (err) {\n setRowError(err instanceof Error ? err.message : 'Could not set default track.');\n } finally {\n setDefaultingId(null);\n }\n };\n\n const handleDelete = async (track: HokLocalization) => {\n setRowError(null);\n setDeletingId(track.id);\n try {\n await deleteLocalization.mutateAsync(track.id);\n } catch (err) {\n setRowError(err instanceof Error ? err.message : 'Could not delete track.');\n } finally {\n setDeletingId(null);\n }\n };\n\n const activeKindMeta = KIND_META[formKind];\n\n return (\n <PageLayout\n title={\n <span className=\"flex items-center gap-3\">\n <button\n type=\"button\"\n onClick={() => navigateTo(`catalog/titles/${title.id}`)}\n className=\"rounded-md p-1 text-text-secondary transition-colors hover:text-text-primary\"\n aria-label=\"Back to title\"\n >\n <ArrowLeft className=\"h-5 w-5\" />\n </button>\n Publishing &amp; Localization\n </span>\n }\n description={title.title}\n purpose=\"Schedule the release, walk the title through its publication lifecycle, and manage the per-language subtitle, caption and dub tracks the player offers viewers.\"\n actions={<StatusBadge status={title.status} />}\n >\n {scheduleError && (\n <div className=\"mb-4 rounded-lg border border-status-error-border bg-status-error-bg-subtle px-4 py-2 text-sm text-status-error-text\">\n {scheduleError}\n </div>\n )}\n\n {/* Release scheduler + status stepper */}\n <GlassCard treatment=\"metric\" className=\"mb-6 border-border-seam p-5\">\n <div className=\"mb-5 flex items-center gap-2\">\n <Rocket className=\"h-4 w-4 text-text-secondary\" aria-hidden=\"true\" />\n <h2 className=\"text-sm font-semibold text-text-primary\">Release scheduler</h2>\n </div>\n\n {/* Status stepper */}\n <ol className=\"mb-6 flex flex-col gap-3 sm:flex-row sm:items-center sm:gap-0\">\n {PIPELINE.map((step, index) => {\n const isDone = !isOffPipeline && index < currentIndex;\n const isCurrent = step === title.status;\n const stateClass = isCurrent\n ? 'bg-action-primary-bg text-action-primary-text border-transparent'\n : isDone\n ? 'bg-status-success-bg-subtle text-status-success-text border-transparent'\n : 'bg-bg-sunken text-text-secondary border-border-seam';\n return (\n <li key={step} className=\"flex items-center sm:flex-1\">\n <div className=\"flex items-center gap-2\">\n <span\n className={cn(\n 'flex h-7 w-7 shrink-0 items-center justify-center rounded-full border text-xs font-semibold',\n stateClass\n )}\n aria-current={isCurrent ? 'step' : undefined}\n >\n {isDone ? <CheckCircle2 className=\"h-4 w-4\" aria-hidden=\"true\" /> : index + 1}\n </span>\n <span\n className={cn(\n 'text-xs font-medium',\n isCurrent ? 'text-text-primary' : 'text-text-secondary'\n )}\n >\n {formatEnumLabel(step)}\n </span>\n </div>\n {index < PIPELINE.length - 1 && (\n <span\n aria-hidden=\"true\"\n className={cn(\n 'mx-3 hidden h-px flex-1 sm:block',\n isDone ? 'bg-status-success-border' : 'bg-border-seam'\n )}\n />\n )}\n </li>\n );\n })}\n </ol>\n\n {isOffPipeline && (\n <p className=\"mb-4 rounded-lg border border-border-seam bg-bg-sunken px-3 py-2 text-xs text-text-secondary\">\n This title is currently{' '}\n <span className=\"font-medium\">{formatEnumLabel(title.status)}</span> — outside the\n standard release pipeline. Use the actions below to move it back.\n </p>\n )}\n\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-2\">\n {/* Release date */}\n <form onSubmit={handleSaveRelease} className=\"space-y-3\">\n <div>\n <label htmlFor=\"release-date\" className={labelClass}>\n <span className=\"inline-flex items-center gap-1.5\">\n <CalendarClock className=\"h-3.5 w-3.5\" aria-hidden=\"true\" />\n Release date &amp; time\n </span>\n </label>\n <input\n id=\"release-date\"\n type=\"datetime-local\"\n value={releaseValue}\n onChange={(e) => {\n setReleaseInput(e.target.value);\n setReleaseTouched(true);\n setReleaseSaved(false);\n }}\n className={fieldClass}\n />\n <p className=\"mt-1 text-[11px] text-text-secondary\">\n {title.releaseDate\n ? `Currently scheduled for ${formatDate(title.releaseDate)}.`\n : 'No release date set yet. Leave empty and save to clear.'}\n </p>\n </div>\n <div className=\"flex items-center gap-3\">\n <button\n type=\"submit\"\n disabled={updateTitle.isPending}\n className=\"inline-flex items-center gap-2 rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text transition-opacity hover:opacity-90 disabled:opacity-50\"\n >\n {updateTitle.isPending ? (\n <>\n <Loader2 className=\"h-4 w-4 animate-spin\" aria-hidden=\"true\" />\n Saving…\n </>\n ) : (\n 'Save release date'\n )}\n </button>\n {releaseSaved && (\n <span className=\"inline-flex items-center gap-1.5 text-sm text-status-success-text\">\n <CheckCircle2 className=\"h-4 w-4\" aria-hidden=\"true\" />\n Saved\n </span>\n )}\n </div>\n </form>\n\n {/* Lifecycle transitions */}\n <div>\n <span className={labelClass}>Publication actions</span>\n {NEXT_STATUSES[title.status].length === 0 ? (\n <p className=\"text-sm text-text-secondary\">\n No further transitions available from {formatEnumLabel(title.status)}.\n </p>\n ) : (\n <div className=\"flex flex-wrap gap-2\">\n {NEXT_STATUSES[title.status].map((status) => {\n const isArchive = status === 'ARCHIVED';\n return (\n <button\n key={status}\n type=\"button\"\n disabled={transition.isPending}\n onClick={() => void handleTransition(status)}\n className={cn(\n 'rounded-lg px-3 py-1.5 text-xs font-medium transition-opacity disabled:opacity-50',\n isArchive\n ? 'border border-status-error-border text-status-error-text hover:bg-status-error-bg-subtle'\n : 'bg-action-primary-bg text-action-primary-text hover:opacity-90'\n )}\n >\n Move to {formatEnumLabel(status)}\n </button>\n );\n })}\n </div>\n )}\n <p className=\"mt-2 text-[11px] text-text-secondary\">\n Set a release date, then move the title to{' '}\n <span className=\"font-medium\">Scheduled</span> and finally{' '}\n <span className=\"font-medium\">Published</span> to make it live.\n </p>\n </div>\n </div>\n </GlassCard>\n\n {/* Localization tracks */}\n <GlassCard treatment=\"metric\" className=\"border-border-seam p-5\">\n <div className=\"mb-4 flex flex-wrap items-center justify-between gap-3\">\n <div className=\"flex items-center gap-2\">\n <Languages className=\"h-4 w-4 text-text-secondary\" aria-hidden=\"true\" />\n <h2 className=\"text-sm font-semibold text-text-primary\">\n Localization tracks ({tracks.length})\n </h2>\n </div>\n <button\n type=\"button\"\n onClick={() => {\n setShowAdd((v) => !v);\n setFormError(null);\n }}\n className=\"inline-flex items-center gap-2 rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text transition-opacity hover:opacity-90\"\n >\n {showAdd ? <X className=\"h-4 w-4\" /> : <Plus className=\"h-4 w-4\" />}\n {showAdd ? 'Close' : 'Add track'}\n </button>\n </div>\n\n {showAdd && (\n <form\n onSubmit={handleAddSubmit}\n className=\"mb-6 space-y-4 rounded-xl border border-border-seam bg-bg-surface p-4\"\n >\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <div>\n <label htmlFor=\"loc-language\" className={labelClass}>\n Language *\n </label>\n {languageOptions.length > 0 ? (\n <select\n id=\"loc-language\"\n value={formLanguage}\n onChange={(e) => setFormLanguage(e.target.value)}\n className={fieldClass}\n required\n >\n <option value=\"\">\n {languages.isLoading ? 'Loading languages…' : 'Select a language…'}\n </option>\n {languageOptions.map((lang) => (\n <option key={lang.id} value={lang.code}>\n {lang.name}\n {lang.nativeName && lang.nativeName !== lang.name\n ? ` — ${lang.nativeName}`\n : ''}{' '}\n ({lang.code})\n </option>\n ))}\n </select>\n ) : (\n // Graceful fallback if the global i18n catalog is unavailable —\n // a raw language code still lets studios add tracks.\n <input\n id=\"loc-language\"\n type=\"text\"\n value={formLanguage}\n onChange={(e) => setFormLanguage(e.target.value)}\n placeholder={languages.isLoading ? 'Loading languages…' : 'e.g. en, hi, ta'}\n className={fieldClass}\n required\n />\n )}\n </div>\n <div>\n <label htmlFor=\"loc-kind\" className={labelClass}>\n Track type *\n </label>\n <select\n id=\"loc-kind\"\n value={formKind}\n onChange={(e) => {\n setFormKind(e.target.value as HokLocalizationKind);\n setFormFile(null);\n if (fileInputRef.current) fileInputRef.current.value = '';\n }}\n className={fieldClass}\n >\n {KIND_OPTIONS.map((k) => (\n <option key={k} value={k}>\n {KIND_META[k].label}\n </option>\n ))}\n </select>\n </div>\n </div>\n\n <div>\n <label htmlFor=\"loc-file\" className={labelClass}>\n Track file *\n </label>\n <input\n id=\"loc-file\"\n ref={fileInputRef}\n type=\"file\"\n accept={activeKindMeta.accept}\n onChange={handleFileSelect}\n className={fieldClass}\n />\n <p className=\"mt-1 text-[11px] text-text-secondary\">{activeKindMeta.hint}</p>\n {formFile && (\n <p className=\"mt-1 truncate text-xs text-text-secondary\">\n Selected: {formFile.name}\n </p>\n )}\n </div>\n\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <div>\n <label htmlFor=\"loc-title\" className={labelClass}>\n Localized title (optional)\n </label>\n <input\n id=\"loc-title\"\n type=\"text\"\n value={formLocalizedTitle}\n onChange={(e) => setFormLocalizedTitle(e.target.value)}\n placeholder=\"Title in this language\"\n className={fieldClass}\n />\n </div>\n <div>\n <label htmlFor=\"loc-synopsis\" className={labelClass}>\n Localized synopsis (optional)\n </label>\n <input\n id=\"loc-synopsis\"\n type=\"text\"\n value={formLocalizedSynopsis}\n onChange={(e) => setFormLocalizedSynopsis(e.target.value)}\n placeholder=\"Short synopsis in this language\"\n className={fieldClass}\n />\n </div>\n </div>\n\n <label className=\"flex items-center gap-2 text-sm text-text-primary\">\n <input\n type=\"checkbox\"\n checked={formIsDefault}\n onChange={(e) => setFormIsDefault(e.target.checked)}\n className=\"h-4 w-4 rounded border-border-subtle\"\n />\n Make this the default {activeKindMeta.label.toLowerCase()} track\n </label>\n\n {formError && <p className=\"text-xs text-status-error-text\">{formError}</p>}\n\n <div className=\"flex items-center gap-3\">\n <button\n type=\"submit\"\n disabled={uploading}\n className=\"inline-flex items-center gap-2 rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text transition-opacity hover:opacity-90 disabled:opacity-50\"\n >\n {uploading ? (\n <>\n <Loader2 className=\"h-4 w-4 animate-spin\" aria-hidden=\"true\" />\n Uploading…\n </>\n ) : (\n <>\n <Upload className=\"h-4 w-4\" aria-hidden=\"true\" />\n Add track\n </>\n )}\n </button>\n <button\n type=\"button\"\n onClick={() => {\n resetAddForm();\n setShowAdd(false);\n }}\n className=\"rounded-lg border border-border-seam px-4 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken\"\n >\n Cancel\n </button>\n </div>\n </form>\n )}\n\n {rowError && (\n <div className=\"mb-4 rounded-lg border border-status-error-border bg-status-error-bg-subtle px-4 py-2 text-sm text-status-error-text\">\n {rowError}\n </div>\n )}\n\n {localizations.isLoading ? (\n <LoadingState label=\"Loading tracks…\" className=\"min-h-[24vh]\" />\n ) : localizations.isError ? (\n <ErrorState\n title=\"Could not load tracks\"\n message=\"Something went wrong fetching this title's localization tracks.\"\n onRetry={() => void localizations.refetch()}\n className=\"min-h-[24vh]\"\n />\n ) : tracks.length === 0 ? (\n <EmptyState\n icon={<Captions className=\"h-12 w-12\" />}\n title=\"No localization tracks yet\"\n description=\"Add a subtitle, caption or dub track to make this title accessible in more languages.\"\n />\n ) : (\n <div className=\"overflow-x-auto\">\n <table className=\"w-full min-w-[640px] border-collapse text-left\">\n <thead>\n <tr className=\"text-xs uppercase tracking-wide text-text-secondary\">\n <th className=\"px-4 py-2 font-medium\">Language</th>\n <th className=\"px-4 py-2 font-medium\">Type</th>\n <th className=\"px-4 py-2 font-medium\">Status</th>\n <th className=\"px-4 py-2 font-medium\">Default</th>\n <th className=\"px-4 py-2 font-medium\">File</th>\n <th className=\"px-4 py-2 text-right font-medium\">Actions</th>\n </tr>\n </thead>\n <tbody>\n {tracks.map((track) => (\n <TrackRow\n key={track.id}\n track={track}\n onSetDefault={(t) => void handleSetDefault(t)}\n onDelete={(t) => void handleDelete(t)}\n settingDefault={defaultingId === track.id}\n deleting={deletingId === track.id}\n />\n ))}\n </tbody>\n </table>\n </div>\n )}\n </GlassCard>\n </PageLayout>\n );\n};\n"],"mappings":"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"}
@@ -0,0 +1,366 @@
1
+ import { useHookMovies as e } from "../../context/HookMoviesContext.js";
2
+ import { cn as t } from "../../utils/cn.js";
3
+ import "../../context/index.js";
4
+ import { useLanguages as n, useMyPreferences as r, useSetPreferences as i, useSetUserLanguagePreference as a } from "../../hooks/useHookMoviesApi.js";
5
+ import { EmptyState as o } from "../../components/EmptyState.js";
6
+ import { ErrorState as s, LoadingState as c } from "../../components/StateViews.js";
7
+ import "../../components/index.js";
8
+ import { useCallback as l, useEffect as u, useState as d } from "react";
9
+ import { AlertCircle as f, ArrowRight as p, Check as m, Clapperboard as h, Film as g, Globe as _, Loader2 as v, ShieldCheck as y, Sparkles as b, Star as x, Tv as S, Zap as C } from "lucide-react";
10
+ import { Fragment as w, jsx as T, jsxs as E } from "react/jsx-runtime";
11
+ import { AuroraBackground as D, GlassCard as O } from "@burdenoff/fe-libs/ui";
12
+ //#region src/hookmovies/pages/onboarding/OnboardingPage.tsx
13
+ var k = [
14
+ {
15
+ code: "MOVIE",
16
+ label: "Movies",
17
+ hint: "Feature films",
18
+ icon: g
19
+ },
20
+ {
21
+ code: "SERIES",
22
+ label: "Series",
23
+ hint: "Multi-episode shows",
24
+ icon: S
25
+ },
26
+ {
27
+ code: "ANIME",
28
+ label: "Anime",
29
+ hint: "Animated series & films",
30
+ icon: b
31
+ },
32
+ {
33
+ code: "DOCUMENTARY",
34
+ label: "Documentaries",
35
+ hint: "Real stories",
36
+ icon: h
37
+ },
38
+ {
39
+ code: "SHORT_FORM",
40
+ label: "Shorts",
41
+ hint: "Quick, snackable clips",
42
+ icon: C
43
+ }
44
+ ], A = [
45
+ {
46
+ value: "",
47
+ label: "No limit — show me everything"
48
+ },
49
+ {
50
+ value: "G",
51
+ label: "G — General audiences"
52
+ },
53
+ {
54
+ value: "PG",
55
+ label: "PG — Parental guidance"
56
+ },
57
+ {
58
+ value: "PG-13",
59
+ label: "PG-13 — Teens and up"
60
+ },
61
+ {
62
+ value: "R",
63
+ label: "R — Restricted"
64
+ },
65
+ {
66
+ value: "NC-17",
67
+ label: "NC-17 — Adults only"
68
+ }
69
+ ], j = new Set(k.map((e) => e.code)), M = "text-base font-semibold text-text-primary", N = "mt-0.5 text-sm text-text-secondary", P = "inline-flex items-center justify-center gap-2 rounded-xl bg-action-primary-bg px-6 py-3 text-sm font-semibold text-action-primary-text transition-opacity hover:opacity-90 disabled:opacity-50 disabled:cursor-not-allowed focus:outline-none focus-visible:ring-2 focus-visible:ring-border-focus", ee = "inline-flex items-center justify-center gap-2 rounded-xl border border-border-seam bg-bg-surface px-6 py-3 text-sm font-medium text-text-primary transition-colors hover:border-border-strong hover:bg-bg-sunken focus:outline-none focus-visible:ring-2 focus-visible:ring-border-focus", F = 3, I = 2, L = () => /* @__PURE__ */ E("div", {
70
+ className: "flex flex-col items-center gap-3",
71
+ children: [/* @__PURE__ */ E("p", {
72
+ className: "text-xs font-semibold uppercase tracking-widest text-text-secondary",
73
+ children: [
74
+ "Step ",
75
+ I,
76
+ " of ",
77
+ F
78
+ ]
79
+ }), /* @__PURE__ */ T("div", {
80
+ className: "flex items-center gap-2",
81
+ role: "progressbar",
82
+ "aria-valuemin": 1,
83
+ "aria-valuemax": F,
84
+ "aria-valuenow": I,
85
+ "aria-label": `Onboarding step ${I} of ${F}`,
86
+ children: Array.from({ length: F }, (e, n) => {
87
+ let r = n + 1;
88
+ return /* @__PURE__ */ T("span", {
89
+ "aria-hidden": "true",
90
+ className: t("h-1.5 rounded-full transition-all", r === I ? "w-10" : "w-6", r <= I ? "bg-action-primary-bg" : "bg-border-seam")
91
+ }, r);
92
+ })
93
+ })]
94
+ }), R = ({ language: e, selected: n, isPrimary: r, onToggle: i, onSetPrimary: a }) => {
95
+ let o = e.direction === "RTL" ? "rtl" : "ltr";
96
+ return /* @__PURE__ */ E("div", {
97
+ className: "relative",
98
+ children: [/* @__PURE__ */ E("button", {
99
+ type: "button",
100
+ onClick: () => i(e.code),
101
+ "aria-pressed": n,
102
+ "aria-label": `${n ? "Remove" : "Add"} ${e.name}`,
103
+ className: t("group flex h-full w-full flex-col items-start gap-1 rounded-xl border px-4 py-3 text-left transition-all", "focus:outline-none focus-visible:ring-2 focus-visible:ring-border-focus", "motion-safe:hover:-translate-y-0.5", n ? "border-action-primary-bg bg-action-primary-bg/10 ring-1 ring-action-primary-bg" : "border-border-seam bg-bg-surface hover:border-border-strong hover:bg-bg-sunken"),
104
+ children: [/* @__PURE__ */ E("span", {
105
+ className: "flex w-full items-center justify-between gap-2",
106
+ children: [/* @__PURE__ */ T("span", {
107
+ dir: o,
108
+ className: "truncate text-sm font-semibold text-text-primary",
109
+ title: e.nativeName,
110
+ children: e.nativeName
111
+ }), /* @__PURE__ */ T("span", {
112
+ "aria-hidden": "true",
113
+ className: t("flex size-5 shrink-0 items-center justify-center rounded-full border transition-colors", n ? "border-action-primary-bg bg-action-primary-bg text-action-primary-text" : "border-border-strong text-transparent group-hover:border-action-primary-bg"),
114
+ children: /* @__PURE__ */ T(m, { className: "size-3" })
115
+ })]
116
+ }), /* @__PURE__ */ T("span", {
117
+ className: "truncate text-xs text-text-secondary",
118
+ title: e.name,
119
+ children: e.name
120
+ })]
121
+ }), n && /* @__PURE__ */ E("button", {
122
+ type: "button",
123
+ onClick: () => a(e.code),
124
+ "aria-pressed": r,
125
+ "aria-label": r ? `${e.name} is your app language` : `Set ${e.name} as app language`,
126
+ title: r ? "App language" : "Set as app language",
127
+ className: t("absolute -bottom-2 right-2 inline-flex items-center gap-1 rounded-full border px-2 py-0.5 text-[0.65rem] font-semibold shadow-sm transition-colors", "focus:outline-none focus-visible:ring-2 focus-visible:ring-border-focus", r ? "border-action-primary-bg bg-action-primary-bg text-action-primary-text" : "border-border-seam bg-bg-surface text-text-secondary hover:border-border-strong hover:text-text-primary"),
128
+ children: [/* @__PURE__ */ T(x, {
129
+ className: t("size-3", r && "fill-current"),
130
+ "aria-hidden": "true"
131
+ }), r ? "App language" : "Set as app"]
132
+ })]
133
+ });
134
+ }, z = () => {
135
+ let { currentUser: m, navigateTo: h } = e(), g = n({ status: "ACTIVE" }, 100, 0), b = r(), x = i(), S = a(), [C, F] = d([]), [I, z] = d(null), [B, V] = d([]), [H, U] = d(""), [W, G] = d(!1), [K, q] = d(null);
136
+ u(() => {
137
+ if (W || b.isLoading) return;
138
+ let e = b.data;
139
+ e && (F(e.languages ?? []), z(e.languages?.[0] ?? null), V((e.contentTypes ?? []).filter((e) => j.has(e))), U(e.maturityLimit ?? "")), G(!0);
140
+ }, [
141
+ W,
142
+ b.isLoading,
143
+ b.data
144
+ ]);
145
+ let J = l((e) => {
146
+ F((t) => {
147
+ if (t.includes(e)) {
148
+ let n = t.filter((t) => t !== e);
149
+ return z((t) => t === e ? n[0] ?? null : t), n;
150
+ }
151
+ let n = [...t, e];
152
+ return z((t) => t ?? e), n;
153
+ }), q(null);
154
+ }, []), Y = l((e) => {
155
+ z(e);
156
+ }, []), X = l((e) => {
157
+ V((t) => t.includes(e) ? t.filter((t) => t !== e) : [...t, e]);
158
+ }, []), Z = g.data?.items ?? [], Q = C.length > 0 && !x.isPending, te = l(async () => {
159
+ if (C.length === 0) {
160
+ q("Pick at least one language to continue.");
161
+ return;
162
+ }
163
+ q(null);
164
+ try {
165
+ await x.mutateAsync({
166
+ languages: C,
167
+ contentTypes: B,
168
+ maturityLimit: H || void 0,
169
+ onboarded: !0
170
+ });
171
+ let e = I ?? C[0];
172
+ if (m?.id && e) try {
173
+ await S.mutateAsync({
174
+ userId: m.id,
175
+ languageCode: e
176
+ });
177
+ } catch {}
178
+ h("profiles");
179
+ } catch (e) {
180
+ q(e instanceof Error ? e.message : "We couldn't save your preferences. Please try again.");
181
+ }
182
+ }, [
183
+ C,
184
+ B,
185
+ H,
186
+ I,
187
+ m?.id,
188
+ x,
189
+ S,
190
+ h
191
+ ]), ne = l(() => {
192
+ h("browse");
193
+ }, [h]), $;
194
+ return $ = g.isLoading ? /* @__PURE__ */ T(c, {
195
+ label: "Loading languages…",
196
+ className: "min-h-[30vh]"
197
+ }) : g.isError ? /* @__PURE__ */ T(s, {
198
+ title: "Couldn't load languages",
199
+ message: "Something went wrong fetching the language catalogue.",
200
+ onRetry: () => void g.refetch(),
201
+ className: "min-h-[30vh]"
202
+ }) : Z.length === 0 ? /* @__PURE__ */ T(o, {
203
+ title: "No languages available",
204
+ description: "The language catalogue is empty right now. You can continue and set this later."
205
+ }) : /* @__PURE__ */ T("ul", {
206
+ className: "grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-4",
207
+ children: Z.map((e) => /* @__PURE__ */ T("li", { children: /* @__PURE__ */ T(R, {
208
+ language: e,
209
+ selected: C.includes(e.code),
210
+ isPrimary: I === e.code,
211
+ onToggle: J,
212
+ onSetPrimary: Y
213
+ }) }, e.id))
214
+ }), /* @__PURE__ */ E("div", {
215
+ className: "relative min-h-full overflow-hidden bg-bg-canvas",
216
+ children: [/* @__PURE__ */ T(D, { intensity: "subtle" }), /* @__PURE__ */ E("div", {
217
+ className: "relative mx-auto flex w-full max-w-4xl flex-col gap-8 px-4 py-10 sm:px-6 sm:py-14",
218
+ children: [
219
+ /* @__PURE__ */ E("header", {
220
+ className: "flex flex-col items-center gap-4 text-center",
221
+ children: [
222
+ /* @__PURE__ */ T(L, {}),
223
+ /* @__PURE__ */ T("span", {
224
+ className: "inline-flex size-12 items-center justify-center rounded-2xl bg-action-primary-bg/10 text-action-primary-bg",
225
+ children: /* @__PURE__ */ T(_, {
226
+ className: "size-6",
227
+ "aria-hidden": "true"
228
+ })
229
+ }),
230
+ /* @__PURE__ */ E("div", { children: [/* @__PURE__ */ T("h1", {
231
+ className: "text-2xl font-bold tracking-tight text-text-primary sm:text-3xl",
232
+ children: "What do you want to watch?"
233
+ }), /* @__PURE__ */ T("p", {
234
+ className: "mx-auto mt-2 max-w-xl text-sm text-text-secondary sm:text-base",
235
+ children: "Pick your languages and the kinds of stories you love. We'll tailor your home feed — and set your app language — around them. You can change this anytime."
236
+ })] })
237
+ ]
238
+ }),
239
+ /* @__PURE__ */ E(O, {
240
+ treatment: "glass",
241
+ className: "flex flex-col gap-4 p-5 sm:p-6",
242
+ children: [/* @__PURE__ */ E("div", {
243
+ className: "flex items-start justify-between gap-3",
244
+ children: [/* @__PURE__ */ E("div", { children: [/* @__PURE__ */ T("h2", {
245
+ className: M,
246
+ children: "Languages"
247
+ }), /* @__PURE__ */ T("p", {
248
+ className: N,
249
+ children: "Choose every language you'd like to watch in. Star one to make it your app language."
250
+ })] }), C.length > 0 && /* @__PURE__ */ E("span", {
251
+ className: "shrink-0 rounded-full border border-border-seam bg-bg-surface px-2.5 py-1 text-xs font-medium text-text-secondary",
252
+ children: [C.length, " selected"]
253
+ })]
254
+ }), $]
255
+ }),
256
+ /* @__PURE__ */ E(O, {
257
+ treatment: "glass",
258
+ className: "flex flex-col gap-4 p-5 sm:p-6",
259
+ children: [/* @__PURE__ */ E("div", { children: [/* @__PURE__ */ T("h2", {
260
+ className: M,
261
+ children: "What are you into?"
262
+ }), /* @__PURE__ */ T("p", {
263
+ className: N,
264
+ children: "Select the content types you want to see more of."
265
+ })] }), /* @__PURE__ */ T("div", {
266
+ role: "group",
267
+ "aria-label": "Content types",
268
+ className: "grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-5",
269
+ children: k.map(({ code: e, label: n, hint: r, icon: i }) => {
270
+ let a = B.includes(e);
271
+ return /* @__PURE__ */ E("button", {
272
+ type: "button",
273
+ onClick: () => X(e),
274
+ "aria-pressed": a,
275
+ className: t("flex flex-col items-center gap-2 rounded-xl border px-3 py-4 text-center transition-all", "focus:outline-none focus-visible:ring-2 focus-visible:ring-border-focus", "motion-safe:hover:-translate-y-0.5", a ? "border-action-primary-bg bg-action-primary-bg/10 ring-1 ring-action-primary-bg" : "border-border-seam bg-bg-surface hover:border-border-strong hover:bg-bg-sunken"),
276
+ children: [
277
+ /* @__PURE__ */ T("span", {
278
+ className: t("flex size-10 items-center justify-center rounded-full transition-colors", a ? "bg-action-primary-bg text-action-primary-text" : "bg-bg-sunken text-text-secondary"),
279
+ children: /* @__PURE__ */ T(i, {
280
+ className: "size-5",
281
+ "aria-hidden": !0
282
+ })
283
+ }),
284
+ /* @__PURE__ */ T("span", {
285
+ className: "text-sm font-semibold text-text-primary",
286
+ children: n
287
+ }),
288
+ /* @__PURE__ */ T("span", {
289
+ className: "text-xs text-text-secondary",
290
+ children: r
291
+ })
292
+ ]
293
+ }, e);
294
+ })
295
+ })]
296
+ }),
297
+ /* @__PURE__ */ E(O, {
298
+ treatment: "glass",
299
+ className: "flex flex-col gap-4 p-5 sm:p-6",
300
+ children: [/* @__PURE__ */ E("div", {
301
+ className: "flex items-center gap-2",
302
+ children: [/* @__PURE__ */ T(y, {
303
+ className: "size-5 text-text-secondary",
304
+ "aria-hidden": "true"
305
+ }), /* @__PURE__ */ E("div", { children: [/* @__PURE__ */ T("h2", {
306
+ className: M,
307
+ children: "Maturity limit"
308
+ }), /* @__PURE__ */ T("p", {
309
+ className: N,
310
+ children: "Cap the ratings that appear across your feed and search."
311
+ })] })]
312
+ }), /* @__PURE__ */ E("div", {
313
+ className: "max-w-md",
314
+ children: [/* @__PURE__ */ T("label", {
315
+ htmlFor: "maturity-limit",
316
+ className: "sr-only",
317
+ children: "Maturity limit"
318
+ }), /* @__PURE__ */ T("select", {
319
+ id: "maturity-limit",
320
+ value: H,
321
+ onChange: (e) => U(e.target.value),
322
+ className: t("w-full rounded-lg border border-border-seam bg-bg-surface px-3 py-2.5 text-sm text-text-primary", "transition-colors focus:border-border-strong focus:outline-none focus-visible:ring-2 focus-visible:ring-border-focus"),
323
+ children: A.map((e) => /* @__PURE__ */ T("option", {
324
+ value: e.value,
325
+ children: e.label
326
+ }, e.value || "none"))
327
+ })]
328
+ })]
329
+ }),
330
+ K && /* @__PURE__ */ E("div", {
331
+ role: "alert",
332
+ className: "flex items-start gap-2 rounded-xl border border-status-error-border bg-status-error-bg-subtle px-4 py-3 text-sm text-status-error-text",
333
+ children: [/* @__PURE__ */ T(f, {
334
+ className: "mt-0.5 size-4 shrink-0",
335
+ "aria-hidden": "true"
336
+ }), /* @__PURE__ */ T("span", { children: K })]
337
+ }),
338
+ /* @__PURE__ */ E("div", {
339
+ className: "flex flex-col-reverse items-stretch gap-3 sm:flex-row sm:items-center sm:justify-between",
340
+ children: [/* @__PURE__ */ T("button", {
341
+ type: "button",
342
+ onClick: ne,
343
+ className: ee,
344
+ children: "Skip for now"
345
+ }), /* @__PURE__ */ T("button", {
346
+ type: "button",
347
+ onClick: () => void te(),
348
+ disabled: !Q,
349
+ className: P,
350
+ children: x.isPending ? /* @__PURE__ */ E(w, { children: [/* @__PURE__ */ T(v, {
351
+ className: "size-4 animate-spin",
352
+ "aria-hidden": "true"
353
+ }), "Saving…"] }) : /* @__PURE__ */ E(w, { children: ["Continue", /* @__PURE__ */ T(p, {
354
+ className: "size-4",
355
+ "aria-hidden": "true"
356
+ })] })
357
+ })]
358
+ })
359
+ ]
360
+ })]
361
+ });
362
+ };
363
+ //#endregion
364
+ export { z as OnboardingPage };
365
+
366
+ //# sourceMappingURL=OnboardingPage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"OnboardingPage.js","names":[],"sources":["../../../../src/hookmovies/pages/onboarding/OnboardingPage.tsx"],"sourcesContent":["/**\n * Onboarding — Language & content preferences (Step 2 of 3)\n * @module hookmovies/pages/onboarding\n *\n * The consumer-facing onboarding surface where a new HookMovies viewer picks the\n * languages they want to watch in, the kinds of content they care about, and a\n * maturity ceiling. It is deliberately audience-facing (a calm, centered hero —\n * NOT the admin PageLayout header) and reads like Step 2 of a 3-step welcome\n * flow (account → preferences → profiles).\n *\n * Data layer (all reused, nothing duplicated):\n * - `useLanguages` (global-i18n, GLOBAL gateway) — the language tile catalogue.\n * - `useMyPreferences` / `useSetPreferences` (hookmovies, WORKSPACE gateway) —\n * the viewer's stored language / content-type / maturity preferences.\n * - `useSetUserLanguagePreference` (global-i18n, GLOBAL gateway) — records the\n * chosen *UI locale* (the \"app language\") against the calling user. This is\n * the platform's single source of truth for the UI locale, so we reuse it\n * rather than standing up a second locale store.\n *\n * On \"Continue\" we persist the HookMovies content preferences AND sync the app\n * language to global-i18n, then drop the viewer into Step 3 (profiles). Styling\n * stays on fe-libs semantic tokens only; media (none here) would be wspace-files\n * refs. Fully responsive, with loading / error / empty states and a11y.\n */\n\nimport { type FC, type ReactNode, useCallback, useEffect, useState } from 'react';\nimport {\n AlertCircle,\n ArrowRight,\n Check,\n Clapperboard,\n Film,\n Globe,\n Loader2,\n ShieldCheck,\n Sparkles,\n Star,\n Tv,\n Zap,\n} from 'lucide-react';\n\nimport { AuroraBackground, GlassCard } from '@burdenoff/fe-libs/ui';\n\nimport { EmptyState, ErrorState, LoadingState } from '../../components';\nimport { useHookMovies } from '../../context';\nimport {\n useLanguages,\n useMyPreferences,\n useSetPreferences,\n useSetUserLanguagePreference,\n type Language,\n} from '../../hooks/useHookMoviesApi';\nimport { cn } from '../../utils/cn';\n\n// ============================================================================\n// Static option catalogues\n// ============================================================================\n\n/**\n * The content types a viewer can opt into. Codes are stored verbatim in\n * `hokSetPreferences.contentTypes` (a free-form string list on the subgraph);\n * they intentionally mirror the relevant `HokTitleType` members so downstream\n * discovery can filter on them directly.\n */\nconst CONTENT_TYPES: ReadonlyArray<{\n code: string;\n label: string;\n hint: string;\n icon: FC<{ className?: string; 'aria-hidden'?: boolean }>;\n}> = [\n { code: 'MOVIE', label: 'Movies', hint: 'Feature films', icon: Film },\n { code: 'SERIES', label: 'Series', hint: 'Multi-episode shows', icon: Tv },\n { code: 'ANIME', label: 'Anime', hint: 'Animated series & films', icon: Sparkles },\n { code: 'DOCUMENTARY', label: 'Documentaries', hint: 'Real stories', icon: Clapperboard },\n { code: 'SHORT_FORM', label: 'Shorts', hint: 'Quick, snackable clips', icon: Zap },\n];\n\n/** Maturity ceiling options. Empty value == no limit (all ratings). */\nconst MATURITY_OPTIONS: ReadonlyArray<{ value: string; label: string }> = [\n { value: '', label: 'No limit — show me everything' },\n { value: 'G', label: 'G — General audiences' },\n { value: 'PG', label: 'PG — Parental guidance' },\n { value: 'PG-13', label: 'PG-13 — Teens and up' },\n { value: 'R', label: 'R — Restricted' },\n { value: 'NC-17', label: 'NC-17 — Adults only' },\n];\n\nconst CONTENT_TYPE_CODES = new Set(CONTENT_TYPES.map((c) => c.code));\n\n// ============================================================================\n// Shared token-only styling\n// ============================================================================\n\nconst SECTION_TITLE = 'text-base font-semibold text-text-primary';\nconst SECTION_SUBTITLE = 'mt-0.5 text-sm text-text-secondary';\n\nconst PRIMARY_BUTTON =\n 'inline-flex items-center justify-center gap-2 rounded-xl bg-action-primary-bg px-6 py-3 text-sm ' +\n 'font-semibold text-action-primary-text transition-opacity hover:opacity-90 disabled:opacity-50 ' +\n 'disabled:cursor-not-allowed focus:outline-none focus-visible:ring-2 focus-visible:ring-border-focus';\n\nconst GHOST_BUTTON =\n 'inline-flex items-center justify-center gap-2 rounded-xl border border-border-seam bg-bg-surface ' +\n 'px-6 py-3 text-sm font-medium text-text-primary transition-colors hover:border-border-strong ' +\n 'hover:bg-bg-sunken focus:outline-none focus-visible:ring-2 focus-visible:ring-border-focus';\n\n// ============================================================================\n// Step header\n// ============================================================================\n\nconst TOTAL_STEPS = 3;\nconst CURRENT_STEP = 2;\n\nconst StepIndicator: FC = () => (\n <div className=\"flex flex-col items-center gap-3\">\n <p className=\"text-xs font-semibold uppercase tracking-widest text-text-secondary\">\n Step {CURRENT_STEP} of {TOTAL_STEPS}\n </p>\n <div\n className=\"flex items-center gap-2\"\n role=\"progressbar\"\n aria-valuemin={1}\n aria-valuemax={TOTAL_STEPS}\n aria-valuenow={CURRENT_STEP}\n aria-label={`Onboarding step ${CURRENT_STEP} of ${TOTAL_STEPS}`}\n >\n {Array.from({ length: TOTAL_STEPS }, (_, i) => {\n const stepNo = i + 1;\n const done = stepNo <= CURRENT_STEP;\n return (\n <span\n key={stepNo}\n aria-hidden=\"true\"\n className={cn(\n 'h-1.5 rounded-full transition-all',\n stepNo === CURRENT_STEP ? 'w-10' : 'w-6',\n done ? 'bg-action-primary-bg' : 'bg-border-seam'\n )}\n />\n );\n })}\n </div>\n </div>\n);\n\n// ============================================================================\n// Language tile\n// ============================================================================\n\ninterface LanguageTileProps {\n language: Language;\n selected: boolean;\n isPrimary: boolean;\n onToggle: (code: string) => void;\n onSetPrimary: (code: string) => void;\n}\n\nconst LanguageTile: FC<LanguageTileProps> = ({\n language,\n selected,\n isPrimary,\n onToggle,\n onSetPrimary,\n}) => {\n const dir = language.direction === 'RTL' ? 'rtl' : 'ltr';\n return (\n <div className=\"relative\">\n <button\n type=\"button\"\n onClick={() => onToggle(language.code)}\n aria-pressed={selected}\n aria-label={`${selected ? 'Remove' : 'Add'} ${language.name}`}\n className={cn(\n 'group flex h-full w-full flex-col items-start gap-1 rounded-xl border px-4 py-3 text-left transition-all',\n 'focus:outline-none focus-visible:ring-2 focus-visible:ring-border-focus',\n 'motion-safe:hover:-translate-y-0.5',\n selected\n ? 'border-action-primary-bg bg-action-primary-bg/10 ring-1 ring-action-primary-bg'\n : 'border-border-seam bg-bg-surface hover:border-border-strong hover:bg-bg-sunken'\n )}\n >\n <span className=\"flex w-full items-center justify-between gap-2\">\n <span\n dir={dir}\n className=\"truncate text-sm font-semibold text-text-primary\"\n title={language.nativeName}\n >\n {language.nativeName}\n </span>\n <span\n aria-hidden=\"true\"\n className={cn(\n 'flex size-5 shrink-0 items-center justify-center rounded-full border transition-colors',\n selected\n ? 'border-action-primary-bg bg-action-primary-bg text-action-primary-text'\n : 'border-border-strong text-transparent group-hover:border-action-primary-bg'\n )}\n >\n <Check className=\"size-3\" />\n </span>\n </span>\n <span className=\"truncate text-xs text-text-secondary\" title={language.name}>\n {language.name}\n </span>\n </button>\n\n {/* App-language (UI locale) selector — only meaningful once selected. */}\n {selected && (\n <button\n type=\"button\"\n onClick={() => onSetPrimary(language.code)}\n aria-pressed={isPrimary}\n aria-label={\n isPrimary\n ? `${language.name} is your app language`\n : `Set ${language.name} as app language`\n }\n title={isPrimary ? 'App language' : 'Set as app language'}\n className={cn(\n 'absolute -bottom-2 right-2 inline-flex items-center gap-1 rounded-full border px-2 py-0.5 text-[0.65rem] font-semibold shadow-sm transition-colors',\n 'focus:outline-none focus-visible:ring-2 focus-visible:ring-border-focus',\n isPrimary\n ? 'border-action-primary-bg bg-action-primary-bg text-action-primary-text'\n : 'border-border-seam bg-bg-surface text-text-secondary hover:border-border-strong hover:text-text-primary'\n )}\n >\n <Star className={cn('size-3', isPrimary && 'fill-current')} aria-hidden=\"true\" />\n {isPrimary ? 'App language' : 'Set as app'}\n </button>\n )}\n </div>\n );\n};\n\n// ============================================================================\n// Page\n// ============================================================================\n\nexport const OnboardingPage: FC = () => {\n const { currentUser, navigateTo } = useHookMovies();\n\n const languagesQuery = useLanguages({ status: 'ACTIVE' }, 100, 0);\n const preferencesQuery = useMyPreferences();\n const setPreferences = useSetPreferences();\n const setUserLanguagePreference = useSetUserLanguagePreference();\n\n // Selection is kept as an ordered list so the first-selected language is a\n // deterministic fallback for the primary (app) language.\n const [selectedLanguages, setSelectedLanguages] = useState<string[]>([]);\n const [primaryLanguage, setPrimaryLanguage] = useState<string | null>(null);\n const [selectedTypes, setSelectedTypes] = useState<string[]>([]);\n const [maturity, setMaturity] = useState<string>('');\n const [hydrated, setHydrated] = useState(false);\n const [error, setError] = useState<string | null>(null);\n\n // Hydrate the form once from any previously-saved preferences (returning\n // viewers editing their choices). Runs a single time when the query settles.\n useEffect(() => {\n if (hydrated || preferencesQuery.isLoading) return;\n const prefs = preferencesQuery.data;\n if (prefs) {\n setSelectedLanguages(prefs.languages ?? []);\n setPrimaryLanguage(prefs.languages?.[0] ?? null);\n setSelectedTypes((prefs.contentTypes ?? []).filter((c) => CONTENT_TYPE_CODES.has(c)));\n setMaturity(prefs.maturityLimit ?? '');\n }\n setHydrated(true);\n }, [hydrated, preferencesQuery.isLoading, preferencesQuery.data]);\n\n const toggleLanguage = useCallback((code: string) => {\n setSelectedLanguages((prev) => {\n if (prev.includes(code)) {\n const next = prev.filter((c) => c !== code);\n setPrimaryLanguage((current) => (current === code ? (next[0] ?? null) : current));\n return next;\n }\n const next = [...prev, code];\n setPrimaryLanguage((current) => current ?? code);\n return next;\n });\n setError(null);\n }, []);\n\n const setPrimary = useCallback((code: string) => {\n setPrimaryLanguage(code);\n }, []);\n\n const toggleType = useCallback((code: string) => {\n setSelectedTypes((prev) =>\n prev.includes(code) ? prev.filter((c) => c !== code) : [...prev, code]\n );\n }, []);\n\n const languageItems = languagesQuery.data?.items ?? [];\n const canContinue = selectedLanguages.length > 0 && !setPreferences.isPending;\n\n const handleContinue = useCallback(async () => {\n if (selectedLanguages.length === 0) {\n setError('Pick at least one language to continue.');\n return;\n }\n setError(null);\n try {\n await setPreferences.mutateAsync({\n languages: selectedLanguages,\n contentTypes: selectedTypes,\n maturityLimit: maturity || undefined,\n onboarded: true,\n });\n\n // Sync the chosen UI locale to the platform's single locale store\n // (global-i18n). Best-effort: the content preferences above are the\n // primary outcome and are already persisted, so a locale hiccup must not\n // block the viewer from finishing onboarding.\n const uiLocale = primaryLanguage ?? selectedLanguages[0];\n if (currentUser?.id && uiLocale) {\n try {\n await setUserLanguagePreference.mutateAsync({\n userId: currentUser.id,\n languageCode: uiLocale,\n });\n } catch {\n // Locale sync is non-blocking — preferences are saved regardless.\n }\n }\n\n navigateTo('profiles');\n } catch (err) {\n setError(\n err instanceof Error ? err.message : \"We couldn't save your preferences. Please try again.\"\n );\n }\n }, [\n selectedLanguages,\n selectedTypes,\n maturity,\n primaryLanguage,\n currentUser?.id,\n setPreferences,\n setUserLanguagePreference,\n navigateTo,\n ]);\n\n const handleSkip = useCallback(() => {\n navigateTo('browse');\n }, [navigateTo]);\n\n // ------------------------------------------------------------------\n // Language section body (loading / error / empty / data)\n // ------------------------------------------------------------------\n let languageBody: ReactNode;\n if (languagesQuery.isLoading) {\n languageBody = <LoadingState label=\"Loading languages…\" className=\"min-h-[30vh]\" />;\n } else if (languagesQuery.isError) {\n languageBody = (\n <ErrorState\n title=\"Couldn't load languages\"\n message=\"Something went wrong fetching the language catalogue.\"\n onRetry={() => void languagesQuery.refetch()}\n className=\"min-h-[30vh]\"\n />\n );\n } else if (languageItems.length === 0) {\n languageBody = (\n <EmptyState\n title=\"No languages available\"\n description=\"The language catalogue is empty right now. You can continue and set this later.\"\n />\n );\n } else {\n languageBody = (\n <ul className=\"grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-4\">\n {languageItems.map((language) => (\n <li key={language.id}>\n <LanguageTile\n language={language}\n selected={selectedLanguages.includes(language.code)}\n isPrimary={primaryLanguage === language.code}\n onToggle={toggleLanguage}\n onSetPrimary={setPrimary}\n />\n </li>\n ))}\n </ul>\n );\n }\n\n return (\n <div className=\"relative min-h-full overflow-hidden bg-bg-canvas\">\n <AuroraBackground intensity=\"subtle\" />\n\n <div className=\"relative mx-auto flex w-full max-w-4xl flex-col gap-8 px-4 py-10 sm:px-6 sm:py-14\">\n {/* Header */}\n <header className=\"flex flex-col items-center gap-4 text-center\">\n <StepIndicator />\n <span className=\"inline-flex size-12 items-center justify-center rounded-2xl bg-action-primary-bg/10 text-action-primary-bg\">\n <Globe className=\"size-6\" aria-hidden=\"true\" />\n </span>\n <div>\n <h1 className=\"text-2xl font-bold tracking-tight text-text-primary sm:text-3xl\">\n What do you want to watch?\n </h1>\n <p className=\"mx-auto mt-2 max-w-xl text-sm text-text-secondary sm:text-base\">\n Pick your languages and the kinds of stories you love. We&apos;ll tailor your home\n feed — and set your app language — around them. You can change this anytime.\n </p>\n </div>\n </header>\n\n {/* Languages */}\n <GlassCard treatment=\"glass\" className=\"flex flex-col gap-4 p-5 sm:p-6\">\n <div className=\"flex items-start justify-between gap-3\">\n <div>\n <h2 className={SECTION_TITLE}>Languages</h2>\n <p className={SECTION_SUBTITLE}>\n Choose every language you&apos;d like to watch in. Star one to make it your app\n language.\n </p>\n </div>\n {selectedLanguages.length > 0 && (\n <span className=\"shrink-0 rounded-full border border-border-seam bg-bg-surface px-2.5 py-1 text-xs font-medium text-text-secondary\">\n {selectedLanguages.length} selected\n </span>\n )}\n </div>\n {languageBody}\n </GlassCard>\n\n {/* Content types */}\n <GlassCard treatment=\"glass\" className=\"flex flex-col gap-4 p-5 sm:p-6\">\n <div>\n <h2 className={SECTION_TITLE}>What are you into?</h2>\n <p className={SECTION_SUBTITLE}>Select the content types you want to see more of.</p>\n </div>\n <div\n role=\"group\"\n aria-label=\"Content types\"\n className=\"grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-5\"\n >\n {CONTENT_TYPES.map(({ code, label, hint, icon: Icon }) => {\n const active = selectedTypes.includes(code);\n return (\n <button\n key={code}\n type=\"button\"\n onClick={() => toggleType(code)}\n aria-pressed={active}\n className={cn(\n 'flex flex-col items-center gap-2 rounded-xl border px-3 py-4 text-center transition-all',\n 'focus:outline-none focus-visible:ring-2 focus-visible:ring-border-focus',\n 'motion-safe:hover:-translate-y-0.5',\n active\n ? 'border-action-primary-bg bg-action-primary-bg/10 ring-1 ring-action-primary-bg'\n : 'border-border-seam bg-bg-surface hover:border-border-strong hover:bg-bg-sunken'\n )}\n >\n <span\n className={cn(\n 'flex size-10 items-center justify-center rounded-full transition-colors',\n active\n ? 'bg-action-primary-bg text-action-primary-text'\n : 'bg-bg-sunken text-text-secondary'\n )}\n >\n <Icon className=\"size-5\" aria-hidden={true} />\n </span>\n <span className=\"text-sm font-semibold text-text-primary\">{label}</span>\n <span className=\"text-xs text-text-secondary\">{hint}</span>\n </button>\n );\n })}\n </div>\n </GlassCard>\n\n {/* Maturity */}\n <GlassCard treatment=\"glass\" className=\"flex flex-col gap-4 p-5 sm:p-6\">\n <div className=\"flex items-center gap-2\">\n <ShieldCheck className=\"size-5 text-text-secondary\" aria-hidden=\"true\" />\n <div>\n <h2 className={SECTION_TITLE}>Maturity limit</h2>\n <p className={SECTION_SUBTITLE}>\n Cap the ratings that appear across your feed and search.\n </p>\n </div>\n </div>\n <div className=\"max-w-md\">\n <label htmlFor=\"maturity-limit\" className=\"sr-only\">\n Maturity limit\n </label>\n <select\n id=\"maturity-limit\"\n value={maturity}\n onChange={(e) => setMaturity(e.target.value)}\n className={cn(\n 'w-full rounded-lg border border-border-seam bg-bg-surface px-3 py-2.5 text-sm text-text-primary',\n 'transition-colors focus:border-border-strong focus:outline-none focus-visible:ring-2 focus-visible:ring-border-focus'\n )}\n >\n {MATURITY_OPTIONS.map((opt) => (\n <option key={opt.value || 'none'} value={opt.value}>\n {opt.label}\n </option>\n ))}\n </select>\n </div>\n </GlassCard>\n\n {/* Error banner */}\n {error && (\n <div\n role=\"alert\"\n className=\"flex items-start gap-2 rounded-xl border border-status-error-border bg-status-error-bg-subtle px-4 py-3 text-sm text-status-error-text\"\n >\n <AlertCircle className=\"mt-0.5 size-4 shrink-0\" aria-hidden=\"true\" />\n <span>{error}</span>\n </div>\n )}\n\n {/* Actions */}\n <div className=\"flex flex-col-reverse items-stretch gap-3 sm:flex-row sm:items-center sm:justify-between\">\n <button type=\"button\" onClick={handleSkip} className={GHOST_BUTTON}>\n Skip for now\n </button>\n <button\n type=\"button\"\n onClick={() => void handleContinue()}\n disabled={!canContinue}\n className={PRIMARY_BUTTON}\n >\n {setPreferences.isPending ? (\n <>\n <Loader2 className=\"size-4 animate-spin\" aria-hidden=\"true\" />\n Saving…\n </>\n ) : (\n <>\n Continue\n <ArrowRight className=\"size-4\" aria-hidden=\"true\" />\n </>\n )}\n </button>\n </div>\n </div>\n </div>\n );\n};\n"],"mappings":"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"}