@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":"ProfilesPage.js","names":[],"sources":["../../../../src/hookmovies/pages/social/ProfilesPage.tsx"],"sourcesContent":["/**\n * Profiles — \"Who's watching?\" (Netflix-style per-account viewing profiles)\n * @module hookmovies/pages/social\n *\n * The consumer entry surface for a shared HookMovies account. It renders the\n * calling account's viewing profiles ({@link useMyProfiles}) as a centered\n * \"Who's watching?\" grid: pick a profile to make it active (persisted in the\n * MFE context via `setActiveProfile`, which the player / discovery surfaces read\n * to apply that profile's language + maturity preferences), then drop into the\n * catalogue.\n *\n * Account holders can add, edit and delete profiles here, including parental\n * controls — a Kids flag, a maturity ceiling, preferred languages, an avatar\n * (uploaded to wspace-files-svc, referenced by id — HookMovies stores no media)\n * and an optional 4-digit PIN.\n *\n * Data layer: `useMyProfiles`, `useCreateProfile`, `useUpdateProfile`,\n * `useDeleteProfile` (hooks/useHookMoviesApi) + `uploadFileToFiles` / `useFileUrl`\n * (hooks/useFiles). Styling stays on fe-libs semantic tokens only.\n */\n\nimport { type FC, type ReactNode, useCallback, useEffect, useMemo, useState } from 'react';\nimport {\n AlertCircle,\n Baby,\n Camera,\n Check,\n Loader2,\n Lock,\n Pencil,\n Plus,\n ShieldCheck,\n Trash2,\n X,\n} from 'lucide-react';\n\nimport { GlassCard, Switch } from '@burdenoff/fe-libs/ui';\n\nimport { EmptyState, ErrorState, LoadingState } from '../../components';\nimport { useHookMovies } from '../../context/HookMoviesContext';\nimport {\n useCreateProfile,\n useDeleteProfile,\n useMyProfiles,\n useUpdateProfile,\n type CreateProfileInput,\n type HokProfile,\n type UpdateProfileInput,\n} from '../../hooks/useHookMoviesApi';\nimport { uploadFileToFiles, useFileUrl } from '../../hooks/useFiles';\nimport { cn } from '../../utils/cn';\n\n// ============================================================================\n// Constants\n// ============================================================================\n\n/** wspace-files folder for uploaded profile avatars. */\nconst AVATAR_FOLDER = '/HookMovies/Avatars';\n\n/** Curated language options (BCP-47-ish codes) for the multi-language catalogue. */\nconst LANGUAGE_OPTIONS: ReadonlyArray<{ code: string; label: string }> = [\n { code: 'en', label: 'English' },\n { code: 'es', label: 'Spanish' },\n { code: 'hi', label: 'Hindi' },\n { code: 'ta', label: 'Tamil' },\n { code: 'fr', label: 'French' },\n { code: 'de', label: 'German' },\n { code: 'pt', label: 'Portuguese' },\n { code: 'ja', label: 'Japanese' },\n { code: 'ko', label: 'Korean' },\n { code: 'zh', label: 'Chinese' },\n { code: 'ar', label: 'Arabic' },\n { code: 'ru', label: 'Russian' },\n];\n\n/** Maturity ceiling options. Empty string == no limit (adult / all ratings). */\nconst MATURITY_OPTIONS: ReadonlyArray<{ value: string; label: string }> = [\n { value: '', label: 'No limit — all ratings' },\n { value: 'G', label: 'G — General audiences' },\n { value: 'PG', label: 'PG — Parental guidance' },\n { value: 'PG-13', label: 'PG-13 — Teens' },\n { value: 'R', label: 'R — Restricted' },\n { value: 'NC-17', label: 'NC-17 — Adults only' },\n];\n\n/** Kid-safe default maturity ceiling applied when a profile is flagged as Kids. */\nconst KIDS_MATURITY_DEFAULT = 'PG';\n\nconst MAX_PROFILES = 5;\n\n// ============================================================================\n// Shared input styling (semantic tokens only)\n// ============================================================================\n\nconst FIELD_LABEL = 'block text-sm font-medium text-text-primary';\n\nconst TEXT_INPUT =\n 'w-full rounded-lg border border-border-seam bg-bg-surface px-3 py-2 text-sm text-text-primary ' +\n 'placeholder:text-text-muted transition-colors focus:border-border-strong focus:outline-none ' +\n 'focus-visible:ring-2 focus-visible:ring-border-focus';\n\nconst PRIMARY_BUTTON =\n 'inline-flex items-center justify-center gap-2 rounded-lg bg-action-primary-bg px-4 py-2 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-lg border border-border-seam bg-bg-surface ' +\n 'px-4 py-2 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// Avatar\n// ============================================================================\n\n/** Deterministic initials fallback when a profile has no uploaded avatar. */\nfunction initialsOf(name: string): string {\n const parts = name.trim().split(/\\s+/).filter(Boolean);\n if (parts.length === 0) return '?';\n if (parts.length === 1) return parts[0]!.slice(0, 2).toUpperCase();\n return (parts[0]![0]! + parts[parts.length - 1]![0]!).toUpperCase();\n}\n\ninterface ProfileAvatarProps {\n fileId?: string | null;\n /** A freshly-uploaded (or externally-resolved) URL that takes priority over `fileId`. */\n overrideUrl?: string | null;\n name: string;\n className?: string;\n}\n\n/**\n * Square, rounded avatar. Resolves a wspace-files id to a short-lived presigned\n * URL; falls back to token-tinted initials while loading or when unset.\n */\nconst ProfileAvatar: FC<ProfileAvatarProps> = ({ fileId, overrideUrl, name, className }) => {\n const { data: resolvedUrl, isLoading } = useFileUrl(overrideUrl ? undefined : fileId);\n const url = overrideUrl ?? resolvedUrl ?? null;\n\n return (\n <div\n className={cn(\n 'relative flex aspect-square w-full items-center justify-center overflow-hidden rounded-2xl ' +\n 'border border-border-seam bg-bg-sunken',\n className\n )}\n >\n {url ? (\n <img src={url} alt=\"\" className=\"h-full w-full object-cover\" />\n ) : isLoading && fileId ? (\n <Loader2 className=\"h-6 w-6 animate-spin text-text-muted\" aria-hidden=\"true\" />\n ) : (\n <span className=\"text-2xl font-semibold text-text-secondary sm:text-3xl\">\n {initialsOf(name)}\n </span>\n )}\n </div>\n );\n};\n\n// ============================================================================\n// Profile card\n// ============================================================================\n\ninterface ProfileCardProps {\n profile: HokProfile;\n isActive: boolean;\n manageMode: boolean;\n onSelect: () => void;\n onEdit: () => void;\n}\n\n/**\n * A single selectable profile tile. In normal mode the whole tile selects the\n * profile; in Manage mode the whole tile opens its editor (a pencil badge marks\n * the mode). Kept as a single button so there are never nested interactives.\n */\nconst ProfileCard: FC<ProfileCardProps> = ({ profile, isActive, manageMode, onSelect, onEdit }) => {\n const label = manageMode\n ? `Edit ${profile.name}'s profile`\n : `Switch to ${profile.name}'s profile`;\n\n return (\n <button\n type=\"button\"\n onClick={manageMode ? onEdit : onSelect}\n aria-label={label}\n aria-pressed={!manageMode && isActive}\n className={cn(\n 'group flex w-full flex-col items-center gap-3 rounded-2xl p-2 text-center transition-transform',\n 'focus:outline-none focus-visible:ring-2 focus-visible:ring-border-focus',\n 'hover:-translate-y-0.5 motion-reduce:transform-none'\n )}\n >\n <div className=\"relative w-full\">\n <GlassCard\n treatment={isActive ? 'insight' : 'glass'}\n glow={isActive}\n className={cn('overflow-hidden !p-0', isActive && 'ring-2 ring-action-primary-bg')}\n >\n <ProfileAvatar fileId={profile.avatarFileId} name={profile.name} />\n </GlassCard>\n\n {/* Kids marker */}\n {profile.isKids && (\n <span\n className=\"absolute left-2 top-2 inline-flex items-center gap-1 rounded-full border border-border-seam bg-bg-surface/80 px-2 py-0.5 text-[0.65rem] font-semibold text-status-info-text backdrop-blur\"\n title=\"Kids profile\"\n >\n <Baby className=\"h-3 w-3\" aria-hidden=\"true\" />\n Kids\n </span>\n )}\n\n {/* PIN marker */}\n {profile.hasPin && (\n <span\n className=\"absolute right-2 top-2 inline-flex h-6 w-6 items-center justify-center rounded-full border border-border-seam bg-bg-surface/80 text-text-secondary backdrop-blur\"\n title=\"PIN protected\"\n >\n <Lock className=\"h-3 w-3\" aria-hidden=\"true\" />\n </span>\n )}\n\n {/* Manage-mode edit affordance (decorative — whole tile is the trigger) */}\n {manageMode && (\n <span\n aria-hidden=\"true\"\n className=\"pointer-events-none absolute inset-0 flex items-center justify-center rounded-2xl bg-bg-overlay/50 opacity-0 transition-opacity group-hover:opacity-100 group-focus-visible:opacity-100\"\n >\n <span className=\"inline-flex h-10 w-10 items-center justify-center rounded-full border border-border-seam bg-bg-surface text-text-primary\">\n <Pencil className=\"h-4 w-4\" />\n </span>\n </span>\n )}\n </div>\n\n <div className=\"min-w-0\">\n <p\n className={cn(\n 'truncate text-sm font-medium',\n isActive ? 'text-text-primary' : 'text-text-secondary group-hover:text-text-primary'\n )}\n >\n {profile.name}\n </p>\n {isActive && !manageMode && (\n <p className=\"mt-0.5 text-[0.7rem] font-medium text-action-primary-bg\">Watching now</p>\n )}\n </div>\n </button>\n );\n};\n\n/** The \"Add profile\" tile shown when under the profile cap. */\nconst AddProfileTile: FC<{ onClick: () => void }> = ({ onClick }) => (\n <button\n type=\"button\"\n onClick={onClick}\n aria-label=\"Add a profile\"\n className={cn(\n 'group flex w-full flex-col items-center gap-3 rounded-2xl p-2 text-center transition-transform',\n 'focus:outline-none focus-visible:ring-2 focus-visible:ring-border-focus',\n 'hover:-translate-y-0.5 motion-reduce:transform-none'\n )}\n >\n <div className=\"flex aspect-square w-full items-center justify-center rounded-2xl border border-dashed border-border-strong bg-bg-surface/40 text-text-muted transition-colors group-hover:border-action-primary-bg group-hover:text-action-primary-bg\">\n <Plus className=\"h-8 w-8\" aria-hidden=\"true\" />\n </div>\n <p className=\"text-sm font-medium text-text-secondary group-hover:text-text-primary\">\n Add profile\n </p>\n </button>\n);\n\n// ============================================================================\n// Create / edit modal\n// ============================================================================\n\ninterface ProfileFormModalProps {\n /** The profile being edited, or `null` for a create flow. */\n profile: HokProfile | null;\n onClose: () => void;\n onDelete?: (profile: HokProfile) => void;\n isDeleting?: boolean;\n}\n\ninterface FormState {\n name: string;\n isKids: boolean;\n maturityLimit: string;\n languages: string[];\n pin: string;\n avatarFileId: string | null;\n}\n\nconst ProfileFormModal: FC<ProfileFormModalProps> = ({\n profile,\n onClose,\n onDelete,\n isDeleting = false,\n}) => {\n const isEdit = profile !== null;\n const createProfile = useCreateProfile();\n const updateProfile = useUpdateProfile();\n\n const [form, setForm] = useState<FormState>(() => ({\n name: profile?.name ?? '',\n isKids: profile?.isKids ?? false,\n maturityLimit: profile?.maturityLimit ?? '',\n languages: profile?.languages ?? [],\n pin: '',\n avatarFileId: profile?.avatarFileId ?? null,\n }));\n\n const [uploadedAvatarUrl, setUploadedAvatarUrl] = useState<string | null>(null);\n const [uploading, setUploading] = useState(false);\n const [error, setError] = useState<string | null>(null);\n\n const saving = createProfile.isPending || updateProfile.isPending;\n const busy = saving || uploading || isDeleting;\n\n // Close on Escape.\n useEffect(() => {\n const onKey = (e: KeyboardEvent) => {\n if (e.key === 'Escape' && !busy) onClose();\n };\n window.addEventListener('keydown', onKey);\n return () => window.removeEventListener('keydown', onKey);\n }, [busy, onClose]);\n\n const patch = useCallback(<K extends keyof FormState>(key: K, value: FormState[K]) => {\n setForm((prev) => ({ ...prev, [key]: value }));\n }, []);\n\n const toggleLanguage = useCallback((code: string) => {\n setForm((prev) => ({\n ...prev,\n languages: prev.languages.includes(code)\n ? prev.languages.filter((c) => c !== code)\n : [...prev.languages, code],\n }));\n }, []);\n\n const handleKidsToggle = useCallback((next: boolean) => {\n setForm((prev) => {\n // When flipping a profile to Kids, tighten an unset/adult ceiling to a\n // kid-safe default; never loosen an already-stricter choice.\n const tightenedMaturity =\n next &&\n (prev.maturityLimit === '' || prev.maturityLimit === 'R' || prev.maturityLimit === 'NC-17')\n ? KIDS_MATURITY_DEFAULT\n : prev.maturityLimit;\n return { ...prev, isKids: next, maturityLimit: tightenedMaturity };\n });\n }, []);\n\n const handleAvatarChange = useCallback(async (event: React.ChangeEvent<HTMLInputElement>) => {\n const file = event.target.files?.[0];\n // Allow re-selecting the same file later.\n event.target.value = '';\n if (!file) return;\n if (!file.type.startsWith('image/')) {\n setError('Please choose an image file for the avatar.');\n return;\n }\n setError(null);\n setUploading(true);\n try {\n const uploaded = await uploadFileToFiles(file, AVATAR_FOLDER);\n setForm((prev) => ({ ...prev, avatarFileId: uploaded.id }));\n setUploadedAvatarUrl(uploaded.url || null);\n } catch (err) {\n setError(err instanceof Error ? err.message : 'The avatar upload failed. Please try again.');\n } finally {\n setUploading(false);\n }\n }, []);\n\n const handleSubmit = useCallback(\n async (event: React.FormEvent) => {\n event.preventDefault();\n const name = form.name.trim();\n if (!name) {\n setError('A profile name is required.');\n return;\n }\n const pin = form.pin.trim();\n if (pin && !/^\\d{4}$/.test(pin)) {\n setError('The PIN must be exactly 4 digits.');\n return;\n }\n setError(null);\n\n const maturity = form.maturityLimit || undefined;\n try {\n if (isEdit && profile) {\n const input: UpdateProfileInput = {\n name,\n isKids: form.isKids,\n languages: form.languages,\n maturityLimit: maturity,\n avatarFileId: form.avatarFileId ?? undefined,\n };\n if (pin) input.pin = pin;\n await updateProfile.mutateAsync({ id: profile.id, input });\n } else {\n const input: CreateProfileInput = {\n name,\n isKids: form.isKids,\n languages: form.languages,\n maturityLimit: maturity,\n avatarFileId: form.avatarFileId ?? undefined,\n };\n if (pin) input.pin = pin;\n await createProfile.mutateAsync(input);\n }\n onClose();\n } catch (err) {\n setError(\n err instanceof Error ? err.message : 'Could not save the profile. Please try again.'\n );\n }\n },\n [form, isEdit, profile, updateProfile, createProfile, onClose]\n );\n\n const titleId = 'hok-profile-modal-title';\n\n return (\n <div\n className=\"fixed inset-0 z-50 flex items-center justify-center bg-bg-canvas/90 p-4 backdrop-blur-sm\"\n role=\"dialog\"\n aria-modal=\"true\"\n aria-labelledby={titleId}\n onClick={() => {\n if (!busy) onClose();\n }}\n >\n <div\n className=\"relative flex max-h-[90vh] w-full max-w-lg flex-col overflow-hidden rounded-2xl border border-border-seam bg-bg-surface shadow-2xl\"\n onClick={(e) => e.stopPropagation()}\n >\n {/* Header */}\n <div className=\"flex items-center justify-between gap-4 border-b border-border-seam px-5 py-4\">\n <h2 id={titleId} className=\"text-lg font-semibold text-text-primary\">\n {isEdit ? 'Edit profile' : 'Add a profile'}\n </h2>\n <button\n type=\"button\"\n onClick={onClose}\n disabled={busy}\n aria-label=\"Close\"\n className=\"flex size-9 items-center justify-center rounded-full text-text-secondary transition-colors hover:bg-bg-sunken hover:text-text-primary focus:outline-none focus-visible:ring-2 focus-visible:ring-border-focus disabled:opacity-50\"\n >\n <X className=\"h-5 w-5\" />\n </button>\n </div>\n\n <form onSubmit={handleSubmit} className=\"flex min-h-0 flex-1 flex-col\">\n <div className=\"flex-1 space-y-5 overflow-y-auto px-5 py-5\">\n {/* Avatar + name */}\n <div className=\"flex items-start gap-4\">\n <div className=\"relative w-24 shrink-0\">\n <ProfileAvatar\n fileId={form.avatarFileId}\n overrideUrl={uploadedAvatarUrl}\n name={form.name || 'New profile'}\n />\n <label\n className={cn(\n 'absolute -bottom-2 -right-2 flex size-9 cursor-pointer items-center justify-center rounded-full',\n 'border border-border-seam bg-bg-surface text-text-primary shadow-sm transition-colors',\n 'hover:border-border-strong hover:bg-bg-sunken focus-within:ring-2 focus-within:ring-border-focus',\n uploading && 'pointer-events-none opacity-70'\n )}\n title=\"Upload avatar\"\n >\n {uploading ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" aria-hidden=\"true\" />\n ) : (\n <Camera className=\"h-4 w-4\" aria-hidden=\"true\" />\n )}\n <span className=\"sr-only\">Upload avatar image</span>\n <input\n type=\"file\"\n accept=\"image/*\"\n className=\"sr-only\"\n onChange={handleAvatarChange}\n disabled={uploading}\n />\n </label>\n </div>\n\n <div className=\"min-w-0 flex-1 space-y-1.5\">\n <label htmlFor=\"hok-profile-name\" className={FIELD_LABEL}>\n Name\n </label>\n <input\n id=\"hok-profile-name\"\n type=\"text\"\n value={form.name}\n onChange={(e) => patch('name', e.target.value)}\n placeholder=\"e.g. Alex\"\n maxLength={60}\n autoFocus\n required\n className={TEXT_INPUT}\n />\n <p className=\"text-xs text-text-muted\">\n Shown on the “Who’s watching?” screen and personalises recommendations.\n </p>\n </div>\n </div>\n\n {/* Kids toggle */}\n <div className=\"flex items-center justify-between gap-4 rounded-lg border border-border-seam bg-bg-sunken px-4 py-3\">\n <div className=\"flex items-start gap-3\">\n <Baby className=\"mt-0.5 h-5 w-5 text-status-info-text\" aria-hidden=\"true\" />\n <div>\n <p className=\"text-sm font-medium text-text-primary\">Kids profile</p>\n <p className=\"text-xs text-text-muted\">\n A simpler experience limited to age-appropriate titles.\n </p>\n </div>\n </div>\n <Switch\n checked={form.isKids}\n onCheckedChange={handleKidsToggle}\n aria-label=\"Kids profile\"\n />\n </div>\n\n {/* Maturity ceiling */}\n <div className=\"space-y-1.5\">\n <label htmlFor=\"hok-profile-maturity\" className={FIELD_LABEL}>\n Maturity ceiling\n </label>\n <select\n id=\"hok-profile-maturity\"\n value={form.maturityLimit}\n onChange={(e) => patch('maturityLimit', e.target.value)}\n className={cn(TEXT_INPUT, 'appearance-none')}\n >\n {MATURITY_OPTIONS.map((option) => (\n <option key={option.value || 'none'} value={option.value}>\n {option.label}\n </option>\n ))}\n </select>\n <p className=\"text-xs text-text-muted\">\n Hides titles rated above this level from this profile.\n </p>\n </div>\n\n {/* Languages multiselect */}\n <fieldset className=\"space-y-2\">\n <legend className={FIELD_LABEL}>Preferred languages</legend>\n <div className=\"flex flex-wrap gap-2\">\n {LANGUAGE_OPTIONS.map((language) => {\n const selected = form.languages.includes(language.code);\n return (\n <button\n key={language.code}\n type=\"button\"\n aria-pressed={selected}\n onClick={() => toggleLanguage(language.code)}\n className={cn(\n 'inline-flex items-center gap-1.5 rounded-full border px-3 py-1.5 text-sm transition-colors',\n 'focus:outline-none focus-visible:ring-2 focus-visible:ring-border-focus',\n selected\n ? 'border-action-primary-bg bg-action-primary-bg/10 text-action-primary-bg'\n : 'border-border-seam bg-bg-surface text-text-secondary hover:border-border-strong hover:text-text-primary'\n )}\n >\n {selected && <Check className=\"h-3.5 w-3.5\" aria-hidden=\"true\" />}\n {language.label}\n </button>\n );\n })}\n </div>\n <p className=\"text-xs text-text-muted\">\n Prioritises audio &amp; subtitle tracks in these languages.\n </p>\n </fieldset>\n\n {/* PIN */}\n <div className=\"space-y-1.5\">\n <label htmlFor=\"hok-profile-pin\" className={FIELD_LABEL}>\n Profile PIN{' '}\n <span className=\"font-normal text-text-muted\">(optional, 4 digits)</span>\n </label>\n <div className=\"relative\">\n <Lock\n className=\"pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-text-muted\"\n aria-hidden=\"true\"\n />\n <input\n id=\"hok-profile-pin\"\n type=\"password\"\n inputMode=\"numeric\"\n autoComplete=\"off\"\n pattern=\"\\d{4}\"\n maxLength={4}\n value={form.pin}\n onChange={(e) => patch('pin', e.target.value.replace(/\\D/g, '').slice(0, 4))}\n placeholder={profile?.hasPin ? '•••• (set — leave blank to keep)' : 'Set a PIN'}\n className={cn(TEXT_INPUT, 'pl-9 tracking-[0.4em]')}\n />\n </div>\n {profile?.hasPin && (\n <p className=\"inline-flex items-center gap-1.5 text-xs text-status-success-text\">\n <ShieldCheck className=\"h-3.5 w-3.5\" aria-hidden=\"true\" />\n This profile is PIN protected. Leave blank to keep the current PIN.\n </p>\n )}\n </div>\n\n {/* Error */}\n {error && (\n <p\n role=\"alert\"\n className=\"flex items-start gap-2 rounded-lg border border-status-error-border bg-status-error-bg-subtle px-3 py-2 text-sm text-status-error-text\"\n >\n <AlertCircle className=\"mt-0.5 h-4 w-4 shrink-0\" aria-hidden=\"true\" />\n {error}\n </p>\n )}\n </div>\n\n {/* Footer */}\n <div className=\"flex items-center justify-between gap-3 border-t border-border-seam px-5 py-4\">\n {isEdit && profile && onDelete ? (\n <button\n type=\"button\"\n onClick={() => onDelete(profile)}\n disabled={busy}\n className=\"inline-flex items-center gap-2 rounded-lg px-3 py-2 text-sm font-medium text-status-error-text transition-colors hover:bg-status-error-bg-subtle focus:outline-none focus-visible:ring-2 focus-visible:ring-border-focus disabled:opacity-50\"\n >\n {isDeleting ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" aria-hidden=\"true\" />\n ) : (\n <Trash2 className=\"h-4 w-4\" aria-hidden=\"true\" />\n )}\n Delete\n </button>\n ) : (\n <span />\n )}\n\n <div className=\"flex items-center gap-3\">\n <button type=\"button\" onClick={onClose} disabled={busy} className={GHOST_BUTTON}>\n Cancel\n </button>\n <button type=\"submit\" disabled={busy} className={PRIMARY_BUTTON}>\n {saving && <Loader2 className=\"h-4 w-4 animate-spin\" aria-hidden=\"true\" />}\n {isEdit ? 'Save changes' : 'Create profile'}\n </button>\n </div>\n </div>\n </form>\n </div>\n </div>\n );\n};\n\n// ============================================================================\n// Delete confirmation\n// ============================================================================\n\nconst DeleteConfirm: FC<{\n profile: HokProfile;\n onCancel: () => void;\n onConfirm: () => void;\n isDeleting: boolean;\n}> = ({ profile, onCancel, onConfirm, isDeleting }) => {\n useEffect(() => {\n const onKey = (e: KeyboardEvent) => {\n if (e.key === 'Escape' && !isDeleting) onCancel();\n };\n window.addEventListener('keydown', onKey);\n return () => window.removeEventListener('keydown', onKey);\n }, [isDeleting, onCancel]);\n\n return (\n <div\n className=\"fixed inset-0 z-[60] flex items-center justify-center bg-bg-canvas/90 p-4 backdrop-blur-sm\"\n role=\"alertdialog\"\n aria-modal=\"true\"\n aria-label={`Delete ${profile.name}'s profile`}\n onClick={() => {\n if (!isDeleting) onCancel();\n }}\n >\n <div\n className=\"w-full max-w-sm rounded-2xl border border-border-seam bg-bg-surface p-6 shadow-2xl\"\n onClick={(e) => e.stopPropagation()}\n >\n <div className=\"flex items-start gap-3\">\n <span className=\"flex size-10 shrink-0 items-center justify-center rounded-full bg-status-error-bg-subtle text-status-error-text\">\n <Trash2 className=\"h-5 w-5\" aria-hidden=\"true\" />\n </span>\n <div className=\"min-w-0\">\n <h2 className=\"text-base font-semibold text-text-primary\">Delete this profile?</h2>\n <p className=\"mt-1 text-sm text-text-secondary\">\n <span className=\"font-medium text-text-primary\">{profile.name}</span>&apos;s viewing\n history and preferences will be permanently removed. This can&apos;t be undone.\n </p>\n </div>\n </div>\n <div className=\"mt-6 flex items-center justify-end gap-3\">\n <button type=\"button\" onClick={onCancel} disabled={isDeleting} className={GHOST_BUTTON}>\n Cancel\n </button>\n <button\n type=\"button\"\n onClick={onConfirm}\n disabled={isDeleting}\n className=\"inline-flex items-center justify-center gap-2 rounded-lg bg-status-error-text px-4 py-2 text-sm font-semibold text-bg-surface transition-opacity hover:opacity-90 focus:outline-none focus-visible:ring-2 focus-visible:ring-border-focus disabled:opacity-50\"\n >\n {isDeleting && <Loader2 className=\"h-4 w-4 animate-spin\" aria-hidden=\"true\" />}\n Delete profile\n </button>\n </div>\n </div>\n </div>\n );\n};\n\n// ============================================================================\n// Page\n// ============================================================================\n\ntype ModalState =\n | { kind: 'create' }\n | { kind: 'edit'; profile: HokProfile }\n | { kind: 'delete'; profile: HokProfile }\n | null;\n\nexport const ProfilesPage: FC = () => {\n const { activeProfileId, setActiveProfile, navigateTo } = useHookMovies();\n const { data: profiles, isLoading, isError, refetch } = useMyProfiles();\n const deleteProfile = useDeleteProfile();\n\n const [manageMode, setManageMode] = useState(false);\n const [modal, setModal] = useState<ModalState>(null);\n\n const list = useMemo(() => profiles ?? [], [profiles]);\n const canAddMore = list.length < MAX_PROFILES;\n\n const handleSelect = useCallback(\n (profile: HokProfile) => {\n setActiveProfile(profile.id);\n navigateTo('browse');\n },\n [setActiveProfile, navigateTo]\n );\n\n const handleConfirmDelete = useCallback(\n async (profile: HokProfile) => {\n try {\n await deleteProfile.mutateAsync(profile.id);\n if (activeProfileId === profile.id) setActiveProfile(null);\n setModal(null);\n } catch {\n // Surface a lightweight recovery: reopen the editor so the error banner\n // (from the mutation) is visible and the user can retry.\n setModal({ kind: 'edit', profile });\n }\n },\n [deleteProfile, activeProfileId, setActiveProfile]\n );\n\n let content: ReactNode;\n if (isLoading) {\n content = <LoadingState label=\"Loading profiles…\" className=\"min-h-[60vh]\" />;\n } else if (isError) {\n content = (\n <ErrorState\n title=\"Couldn't load profiles\"\n message=\"We couldn't reach your account profiles. Check your connection and try again.\"\n onRetry={() => void refetch()}\n className=\"min-h-[60vh]\"\n />\n );\n } else if (list.length === 0) {\n content = (\n <div className=\"flex min-h-[50vh] items-center justify-center\">\n <EmptyState\n illustration=\"empty-data\"\n title=\"No profiles yet\"\n description=\"Create a profile for each person who shares this account — each gets its own recommendations, languages and parental controls.\"\n action={\n <button\n type=\"button\"\n onClick={() => setModal({ kind: 'create' })}\n className={PRIMARY_BUTTON}\n >\n <Plus className=\"h-4 w-4\" aria-hidden=\"true\" />\n Add your first profile\n </button>\n }\n />\n </div>\n );\n } else {\n content = (\n <div className=\"mx-auto grid w-full max-w-4xl grid-cols-2 gap-4 sm:grid-cols-3 sm:gap-6 md:grid-cols-4 lg:grid-cols-5\">\n {list.map((profile) => (\n <ProfileCard\n key={profile.id}\n profile={profile}\n isActive={activeProfileId === profile.id}\n manageMode={manageMode}\n onSelect={() => handleSelect(profile)}\n onEdit={() => setModal({ kind: 'edit', profile })}\n />\n ))}\n {canAddMore && <AddProfileTile onClick={() => setModal({ kind: 'create' })} />}\n </div>\n );\n }\n\n return (\n <div className=\"flex min-h-full w-full flex-col items-center bg-bg-canvas px-4 py-10 sm:py-16\">\n {/* Header */}\n <div className=\"mb-8 text-center sm:mb-12\">\n <h1 className=\"text-2xl font-semibold tracking-tight text-text-primary sm:text-4xl\">\n {manageMode ? 'Manage profiles' : \"Who's watching?\"}\n </h1>\n <p className=\"mt-2 text-sm text-text-secondary\">\n {manageMode\n ? 'Select a profile to edit its details and parental controls.'\n : 'Pick a profile to personalise your HookMovies experience.'}\n </p>\n </div>\n\n <div className=\"w-full\">{content}</div>\n\n {/* Manage toggle — only meaningful once at least one profile exists */}\n {!isLoading && !isError && list.length > 0 && (\n <div className=\"mt-10 sm:mt-14\">\n <button\n type=\"button\"\n onClick={() => setManageMode((v) => !v)}\n className={manageMode ? PRIMARY_BUTTON : GHOST_BUTTON}\n >\n {manageMode ? (\n <>\n <Check className=\"h-4 w-4\" aria-hidden=\"true\" />\n Done\n </>\n ) : (\n <>\n <Pencil className=\"h-4 w-4\" aria-hidden=\"true\" />\n Manage profiles\n </>\n )}\n </button>\n </div>\n )}\n\n {/* Create / edit modal */}\n {modal?.kind === 'create' && (\n <ProfileFormModal profile={null} onClose={() => setModal(null)} />\n )}\n {modal?.kind === 'edit' && (\n <ProfileFormModal\n profile={modal.profile}\n onClose={() => setModal(null)}\n onDelete={(profile) => setModal({ kind: 'delete', profile })}\n isDeleting={deleteProfile.isPending}\n />\n )}\n\n {/* Delete confirmation */}\n {modal?.kind === 'delete' && (\n <DeleteConfirm\n profile={modal.profile}\n isDeleting={deleteProfile.isPending}\n onCancel={() => setModal({ kind: 'edit', profile: modal.profile })}\n onConfirm={() => void handleConfirmDelete(modal.profile)}\n />\n )}\n </div>\n );\n};\n"],"mappings":"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"}
@@ -0,0 +1,281 @@
1
+ import { useHookMovies as e } from "../../context/HookMoviesContext.js";
2
+ import { PageLayout as t } from "../../components/PageLayout.js";
3
+ import "../../context/index.js";
4
+ import { useCreateWatchParty as n, useJoinWatchParty as r, useTitles as i } from "../../hooks/useHookMoviesApi.js";
5
+ import { formatRelativeTime as a } from "../../utils/formatters.js";
6
+ import { EmptyState as o } from "../../components/EmptyState.js";
7
+ import { useCallback as s, useMemo as c, useState as l } from "react";
8
+ import { ArrowRight as u, Clapperboard as d, LogIn as f, PlayCircle as p, Plus as m, Users as h, X as g } from "lucide-react";
9
+ import { jsx as _, jsxs as v } from "react/jsx-runtime";
10
+ import { GlassCard as y } from "@burdenoff/fe-libs/ui";
11
+ //#region src/hookmovies/pages/social/WatchPartyListPage.tsx
12
+ var b = 24, x = (e) => `hookmovies:watchParties:${e ?? "default"}`, S = (e) => {
13
+ if (typeof window > "u") return [];
14
+ try {
15
+ let t = window.localStorage.getItem(x(e));
16
+ if (!t) return [];
17
+ let n = JSON.parse(t);
18
+ return Array.isArray(n) ? n.filter((e) => typeof e == "object" && !!e && typeof e.id == "string" && typeof e.titleId == "string") : [];
19
+ } catch {
20
+ return [];
21
+ }
22
+ }, C = (e, t) => {
23
+ if (!(typeof window > "u")) try {
24
+ window.localStorage.setItem(x(e), JSON.stringify(t));
25
+ } catch {}
26
+ }, w = (e, t) => ({
27
+ id: e.id,
28
+ titleId: e.titleId,
29
+ titleName: e.title?.title ?? "Untitled",
30
+ status: e.status,
31
+ memberCount: e.members.length,
32
+ updatedAt: e.updatedAt,
33
+ role: t
34
+ }), T = {
35
+ LIVE: {
36
+ pill: "bg-status-success-bg-subtle text-status-success-text",
37
+ dot: "bg-status-success-text",
38
+ live: !0
39
+ },
40
+ PAUSED: {
41
+ pill: "bg-status-warning-bg-subtle text-status-warning-text",
42
+ dot: "bg-status-warning-text",
43
+ live: !1
44
+ },
45
+ ENDED: {
46
+ pill: "bg-bg-sunken text-text-secondary",
47
+ dot: "bg-text-muted",
48
+ live: !1
49
+ }
50
+ }, E = {
51
+ LIVE: "Live",
52
+ PAUSED: "Paused",
53
+ ENDED: "Ended"
54
+ }, D = ({ status: e }) => {
55
+ let t = T[e];
56
+ return /* @__PURE__ */ v("span", {
57
+ className: `inline-flex items-center gap-1.5 rounded-full px-2.5 py-0.5 text-xs font-medium ${t.pill}`,
58
+ children: [/* @__PURE__ */ _("span", {
59
+ className: `size-1.5 rounded-full ${t.dot} ${t.live ? "motion-safe:animate-vc-glow-pulse" : ""}`,
60
+ "aria-hidden": !0
61
+ }), E[e]]
62
+ });
63
+ }, O = "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)]", k = "mb-1.5 block text-xs font-medium text-text-secondary", A = () => {
64
+ let { navigateTo: x, workspaceId: T } = e(), [E, A] = l(() => S(T)), [j, M] = l(!1), N = s((e, t) => {
65
+ A((n) => {
66
+ let r = [w(e, t), ...n.filter((t) => t.id !== e.id)].slice(0, b);
67
+ return C(T, r), r;
68
+ });
69
+ }, [T]), P = i({}, {
70
+ limit: 100,
71
+ offset: 0
72
+ }), F = c(() => P.data?.items ?? [], [P.data]), I = n(), L = r(), [R, z] = l(""), [B, V] = l(null), H = async (e) => {
73
+ if (e.preventDefault(), V(null), !R) {
74
+ V("Choose a title to watch together.");
75
+ return;
76
+ }
77
+ try {
78
+ let e = await I.mutateAsync({ titleId: R });
79
+ N(e, "host"), z(""), M(!1), x(`watch-party/${e.id}`);
80
+ } catch (e) {
81
+ V(e instanceof Error ? e.message : "Failed to start the watch party.");
82
+ }
83
+ }, [U, W] = l(""), [G, K] = l(null), q = async (e) => {
84
+ e.preventDefault(), K(null);
85
+ let t = U.trim();
86
+ if (!t) {
87
+ K("Enter a party invite code to join.");
88
+ return;
89
+ }
90
+ try {
91
+ let e = await L.mutateAsync(t);
92
+ N(e, "guest"), W(""), x(`watch-party/${e.id}`);
93
+ } catch (e) {
94
+ K(e instanceof Error ? e.message : "Could not join — check the invite code and try again.");
95
+ }
96
+ }, J = E.filter((e) => e.status !== "ENDED").length;
97
+ return /* @__PURE__ */ v(t, {
98
+ title: "Watch Parties",
99
+ description: "Synchronized co-viewing rooms",
100
+ purpose: "Start a room to watch a title together in sync, or join a friend's room with an invite code. Everyone shares the same playhead — play, pause, and seek stay locked across the party.",
101
+ actions: /* @__PURE__ */ v("button", {
102
+ type: "button",
103
+ onClick: () => M((e) => !e),
104
+ 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 hover:opacity-90 transition-opacity",
105
+ children: [/* @__PURE__ */ _(m, { className: "h-4 w-4" }), "Start Party"]
106
+ }),
107
+ children: [/* @__PURE__ */ v("div", {
108
+ className: "grid grid-cols-1 gap-4 lg:grid-cols-2",
109
+ children: [j && /* @__PURE__ */ v(y, {
110
+ treatment: "glass",
111
+ className: "border-border-seam p-5",
112
+ children: [/* @__PURE__ */ v("div", {
113
+ className: "mb-3 flex items-center justify-between",
114
+ children: [/* @__PURE__ */ v("h2", {
115
+ className: "inline-flex items-center gap-2 text-sm font-semibold text-text-primary",
116
+ children: [/* @__PURE__ */ _(d, { className: "h-4 w-4 text-text-secondary" }), "Start a Party"]
117
+ }), /* @__PURE__ */ _("button", {
118
+ type: "button",
119
+ onClick: () => M(!1),
120
+ className: "rounded-md p-1 text-text-secondary hover:text-text-primary transition-colors",
121
+ "aria-label": "Close",
122
+ children: /* @__PURE__ */ _(g, { className: "h-4 w-4" })
123
+ })]
124
+ }), /* @__PURE__ */ v("form", {
125
+ onSubmit: H,
126
+ className: "space-y-4",
127
+ children: [
128
+ B && /* @__PURE__ */ _("div", {
129
+ className: "rounded-lg border border-status-error-border bg-status-error-bg-subtle px-3 py-2 text-sm text-status-error-text",
130
+ children: B
131
+ }),
132
+ /* @__PURE__ */ v("div", { children: [
133
+ /* @__PURE__ */ _("label", {
134
+ htmlFor: "wp-title",
135
+ className: k,
136
+ children: "Title *"
137
+ }),
138
+ /* @__PURE__ */ v("select", {
139
+ id: "wp-title",
140
+ value: R,
141
+ onChange: (e) => z(e.target.value),
142
+ className: O,
143
+ disabled: P.isLoading,
144
+ required: !0,
145
+ children: [/* @__PURE__ */ _("option", {
146
+ value: "",
147
+ children: P.isLoading ? "Loading titles…" : F.length === 0 ? "No titles available" : "Select a title…"
148
+ }), F.map((e) => /* @__PURE__ */ _("option", {
149
+ value: e.id,
150
+ children: e.title
151
+ }, e.id))]
152
+ }),
153
+ P.isError && /* @__PURE__ */ _("p", {
154
+ className: "mt-1.5 text-xs text-status-error-text",
155
+ children: "Couldn't load titles. Try refreshing."
156
+ })
157
+ ] }),
158
+ /* @__PURE__ */ v("button", {
159
+ type: "submit",
160
+ disabled: I.isPending || !R,
161
+ 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 hover:opacity-90 disabled:opacity-50 transition-opacity",
162
+ children: [/* @__PURE__ */ _(p, { className: "h-4 w-4" }), I.isPending ? "Starting…" : "Start Watching Together"]
163
+ })
164
+ ]
165
+ })]
166
+ }), /* @__PURE__ */ v(y, {
167
+ treatment: "glass",
168
+ className: `border-border-seam p-5 ${j ? "" : "lg:col-span-2"}`,
169
+ children: [/* @__PURE__ */ v("h2", {
170
+ className: "mb-3 inline-flex items-center gap-2 text-sm font-semibold text-text-primary",
171
+ children: [/* @__PURE__ */ _(f, { className: "h-4 w-4 text-text-secondary" }), "Join with a Code"]
172
+ }), /* @__PURE__ */ v("form", {
173
+ onSubmit: q,
174
+ className: "space-y-4",
175
+ children: [G && /* @__PURE__ */ _("div", {
176
+ className: "rounded-lg border border-status-error-border bg-status-error-bg-subtle px-3 py-2 text-sm text-status-error-text",
177
+ children: G
178
+ }), /* @__PURE__ */ v("div", { children: [
179
+ /* @__PURE__ */ _("label", {
180
+ htmlFor: "wp-code",
181
+ className: k,
182
+ children: "Invite code"
183
+ }),
184
+ /* @__PURE__ */ v("div", {
185
+ className: "flex flex-col gap-2 sm:flex-row",
186
+ children: [/* @__PURE__ */ _("input", {
187
+ id: "wp-code",
188
+ type: "text",
189
+ value: U,
190
+ onChange: (e) => W(e.target.value),
191
+ placeholder: "Paste a party invite code",
192
+ className: O,
193
+ autoComplete: "off"
194
+ }), /* @__PURE__ */ v("button", {
195
+ type: "submit",
196
+ disabled: L.isPending || !U.trim(),
197
+ className: "inline-flex shrink-0 items-center justify-center gap-2 rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:opacity-90 disabled:opacity-50 transition-opacity",
198
+ children: [/* @__PURE__ */ _(f, { className: "h-4 w-4" }), L.isPending ? "Joining…" : "Join"]
199
+ })]
200
+ }),
201
+ /* @__PURE__ */ _("p", {
202
+ className: "mt-1.5 text-xs text-text-muted",
203
+ children: "Ask the host for their room's invite code to join."
204
+ })
205
+ ] })]
206
+ })]
207
+ })]
208
+ }), /* @__PURE__ */ v("div", {
209
+ className: "mt-8",
210
+ children: [/* @__PURE__ */ v("div", {
211
+ className: "mb-4 flex items-center justify-between gap-3",
212
+ children: [/* @__PURE__ */ _("h2", {
213
+ className: "text-sm font-semibold text-text-primary",
214
+ children: "Your Rooms"
215
+ }), E.length > 0 && /* @__PURE__ */ v("span", {
216
+ className: "text-xs text-text-muted",
217
+ children: [
218
+ J,
219
+ " active · ",
220
+ E.length,
221
+ " recent"
222
+ ]
223
+ })]
224
+ }), E.length === 0 ? /* @__PURE__ */ _(o, {
225
+ icon: /* @__PURE__ */ _(h, { className: "h-12 w-12" }),
226
+ title: "No watch parties yet",
227
+ description: "Start a room to watch a title in sync with friends, or join one with an invite code.",
228
+ action: /* @__PURE__ */ v("button", {
229
+ type: "button",
230
+ onClick: () => M(!0),
231
+ 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 hover:opacity-90 transition-opacity",
232
+ children: [/* @__PURE__ */ _(m, { className: "h-4 w-4" }), "Start Party"]
233
+ })
234
+ }) : /* @__PURE__ */ _("div", {
235
+ className: "grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3",
236
+ children: E.map((e) => /* @__PURE__ */ v(y, {
237
+ treatment: "metric",
238
+ className: "flex flex-col border-border-seam p-4",
239
+ children: [
240
+ /* @__PURE__ */ v("div", {
241
+ className: "flex items-start justify-between gap-2",
242
+ children: [/* @__PURE__ */ _("h3", {
243
+ className: "line-clamp-2 text-sm font-semibold text-text-primary",
244
+ children: e.titleName
245
+ }), /* @__PURE__ */ _(D, { status: e.status })]
246
+ }),
247
+ /* @__PURE__ */ v("div", {
248
+ className: "mt-3 flex items-center gap-3 text-xs text-text-secondary",
249
+ children: [/* @__PURE__ */ v("span", {
250
+ className: "inline-flex items-center gap-1",
251
+ children: [
252
+ /* @__PURE__ */ _(h, { className: "h-3.5 w-3.5" }),
253
+ e.memberCount,
254
+ " ",
255
+ e.memberCount === 1 ? "member" : "members"
256
+ ]
257
+ }), /* @__PURE__ */ _("span", {
258
+ className: "rounded-full bg-bg-sunken px-2 py-0.5 capitalize",
259
+ children: e.role
260
+ })]
261
+ }),
262
+ /* @__PURE__ */ v("p", {
263
+ className: "mt-2 text-[11px] text-text-muted",
264
+ children: ["Updated ", a(e.updatedAt)]
265
+ }),
266
+ /* @__PURE__ */ v("button", {
267
+ type: "button",
268
+ onClick: () => x(`watch-party/${e.id}`),
269
+ className: "mt-4 inline-flex items-center justify-center gap-2 rounded-lg border border-border-subtle px-3 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken transition-colors",
270
+ children: [e.status === "ENDED" ? "View room" : "Enter room", /* @__PURE__ */ _(u, { className: "h-4 w-4" })]
271
+ })
272
+ ]
273
+ }, e.id))
274
+ })]
275
+ })]
276
+ });
277
+ };
278
+ //#endregion
279
+ export { A as WatchPartyListPage };
280
+
281
+ //# sourceMappingURL=WatchPartyListPage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"WatchPartyListPage.js","names":[],"sources":["../../../../src/hookmovies/pages/social/WatchPartyListPage.tsx"],"sourcesContent":["/**\n * Watch Party Lobby (Social — self-scoped co-viewing)\n * @module hookmovies/pages/social\n *\n * The lobby for synchronized co-viewing rooms. Members can:\n * - Start a party for one of the catalog's titles (`hokCreateWatchParty`).\n * - Join an existing party by its invite code / id (`hokJoinWatchParty`).\n * - Re-enter the parties they have recently hosted or joined.\n *\n * The subgraph exposes no \"list all parties\" query (parties are ephemeral,\n * invite-only rooms), so the lobby surfaces the caller's own recently\n * created/joined rooms — tracked from the real backend responses and persisted\n * per workspace in localStorage. Every id shown is a real party id; entering a\n * room re-fetches its live state via `hokWatchParty`. No fabricated rooms.\n */\n\nimport { type FC, type FormEvent, useCallback, useMemo, useState } from 'react';\nimport { Users, Plus, X, LogIn, PlayCircle, Clapperboard, ArrowRight } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { EmptyState } from '../../components/EmptyState';\nimport { useHookMovies } from '../../context';\nimport {\n useTitles,\n useCreateWatchParty,\n useJoinWatchParty,\n type HokWatchParty,\n type HokWatchPartyStatus,\n} from '../../hooks/useHookMoviesApi';\nimport { formatRelativeTime } from '../../utils/formatters';\n\n// ============================================================================\n// Recent parties — persisted per workspace (real party ids only)\n// ============================================================================\n\ninterface RecentParty {\n id: string;\n titleId: string;\n titleName: string;\n status: HokWatchPartyStatus;\n memberCount: number;\n updatedAt: string;\n role: 'host' | 'guest';\n}\n\nconst RECENT_LIMIT = 24;\n\nconst recentStorageKey = (workspaceId?: string): string =>\n `hookmovies:watchParties:${workspaceId ?? 'default'}`;\n\nconst readRecentParties = (workspaceId?: string): RecentParty[] => {\n if (typeof window === 'undefined') return [];\n try {\n const raw = window.localStorage.getItem(recentStorageKey(workspaceId));\n if (!raw) return [];\n const parsed: unknown = JSON.parse(raw);\n if (!Array.isArray(parsed)) return [];\n return parsed.filter(\n (p): p is RecentParty =>\n typeof p === 'object' &&\n p !== null &&\n typeof (p as RecentParty).id === 'string' &&\n typeof (p as RecentParty).titleId === 'string'\n );\n } catch {\n return [];\n }\n};\n\nconst writeRecentParties = (workspaceId: string | undefined, parties: RecentParty[]): void => {\n if (typeof window === 'undefined') return;\n try {\n window.localStorage.setItem(recentStorageKey(workspaceId), JSON.stringify(parties));\n } catch {\n // Ignore storage failures (private mode / quota) — in-memory state still works.\n }\n};\n\nconst toRecent = (party: HokWatchParty, role: RecentParty['role']): RecentParty => ({\n id: party.id,\n titleId: party.titleId,\n titleName: party.title?.title ?? 'Untitled',\n status: party.status,\n memberCount: party.members.length,\n updatedAt: party.updatedAt,\n role,\n});\n\n// ============================================================================\n// Status pill (watch-party statuses are not covered by the generic StatusBadge)\n// ============================================================================\n\nconst STATUS_STYLES: Record<HokWatchPartyStatus, { pill: string; dot: string; live: boolean }> = {\n LIVE: {\n pill: 'bg-status-success-bg-subtle text-status-success-text',\n dot: 'bg-status-success-text',\n live: true,\n },\n PAUSED: {\n pill: 'bg-status-warning-bg-subtle text-status-warning-text',\n dot: 'bg-status-warning-text',\n live: false,\n },\n ENDED: {\n pill: 'bg-bg-sunken text-text-secondary',\n dot: 'bg-text-muted',\n live: false,\n },\n};\n\nconst STATUS_LABEL: Record<HokWatchPartyStatus, string> = {\n LIVE: 'Live',\n PAUSED: 'Paused',\n ENDED: 'Ended',\n};\n\nconst PartyStatusPill: FC<{ status: HokWatchPartyStatus }> = ({ status }) => {\n const s = STATUS_STYLES[status];\n return (\n <span\n className={`inline-flex items-center gap-1.5 rounded-full px-2.5 py-0.5 text-xs font-medium ${s.pill}`}\n >\n <span\n className={`size-1.5 rounded-full ${s.dot} ${s.live ? 'motion-safe:animate-vc-glow-pulse' : ''}`}\n aria-hidden\n />\n {STATUS_LABEL[status]}\n </span>\n );\n};\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// ============================================================================\n// Page\n// ============================================================================\n\nexport const WatchPartyListPage: FC = () => {\n const { navigateTo, workspaceId } = useHookMovies();\n\n const [recent, setRecent] = useState<RecentParty[]>(() => readRecentParties(workspaceId));\n const [showCreate, setShowCreate] = useState(false);\n\n const rememberParty = useCallback(\n (party: HokWatchParty, role: RecentParty['role']) => {\n setRecent((prev) => {\n const next = [toRecent(party, role), ...prev.filter((p) => p.id !== party.id)].slice(\n 0,\n RECENT_LIMIT\n );\n writeRecentParties(workspaceId, next);\n return next;\n });\n },\n [workspaceId]\n );\n\n // ---- Title picker (start a party) ----\n const titlesQuery = useTitles({}, { limit: 100, offset: 0 });\n const titles = useMemo(() => titlesQuery.data?.items ?? [], [titlesQuery.data]);\n\n const createParty = useCreateWatchParty();\n const joinParty = useJoinWatchParty();\n\n const [selectedTitleId, setSelectedTitleId] = useState('');\n const [createError, setCreateError] = useState<string | null>(null);\n\n const handleCreate = async (e: FormEvent) => {\n e.preventDefault();\n setCreateError(null);\n if (!selectedTitleId) {\n setCreateError('Choose a title to watch together.');\n return;\n }\n try {\n const party = await createParty.mutateAsync({ titleId: selectedTitleId });\n rememberParty(party, 'host');\n setSelectedTitleId('');\n setShowCreate(false);\n navigateTo(`watch-party/${party.id}`);\n } catch (err) {\n setCreateError(err instanceof Error ? err.message : 'Failed to start the watch party.');\n }\n };\n\n // ---- Join by code ----\n const [joinCode, setJoinCode] = useState('');\n const [joinError, setJoinError] = useState<string | null>(null);\n\n const handleJoin = async (e: FormEvent) => {\n e.preventDefault();\n setJoinError(null);\n const code = joinCode.trim();\n if (!code) {\n setJoinError('Enter a party invite code to join.');\n return;\n }\n try {\n const party = await joinParty.mutateAsync(code);\n rememberParty(party, 'guest');\n setJoinCode('');\n navigateTo(`watch-party/${party.id}`);\n } catch (err) {\n setJoinError(\n err instanceof Error ? err.message : 'Could not join — check the invite code and try again.'\n );\n }\n };\n\n const activeCount = recent.filter((p) => p.status !== 'ENDED').length;\n\n return (\n <PageLayout\n title=\"Watch Parties\"\n description=\"Synchronized co-viewing rooms\"\n purpose=\"Start a room to watch a title together in sync, or join a friend's room with an invite code. Everyone shares the same playhead — play, pause, and seek stay locked across the party.\"\n actions={\n <button\n type=\"button\"\n onClick={() => setShowCreate((v) => !v)}\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 hover:opacity-90 transition-opacity\"\n >\n <Plus className=\"h-4 w-4\" />\n Start Party\n </button>\n }\n >\n <div className=\"grid grid-cols-1 gap-4 lg:grid-cols-2\">\n {/* Start a party */}\n {showCreate && (\n <GlassCard treatment=\"glass\" className=\"border-border-seam p-5\">\n <div className=\"mb-3 flex items-center justify-between\">\n <h2 className=\"inline-flex items-center gap-2 text-sm font-semibold text-text-primary\">\n <Clapperboard className=\"h-4 w-4 text-text-secondary\" />\n Start a Party\n </h2>\n <button\n type=\"button\"\n onClick={() => setShowCreate(false)}\n className=\"rounded-md p-1 text-text-secondary hover:text-text-primary transition-colors\"\n aria-label=\"Close\"\n >\n <X className=\"h-4 w-4\" />\n </button>\n </div>\n <form onSubmit={handleCreate} className=\"space-y-4\">\n {createError && (\n <div className=\"rounded-lg border border-status-error-border bg-status-error-bg-subtle px-3 py-2 text-sm text-status-error-text\">\n {createError}\n </div>\n )}\n <div>\n <label htmlFor=\"wp-title\" className={labelClass}>\n Title *\n </label>\n <select\n id=\"wp-title\"\n value={selectedTitleId}\n onChange={(e) => setSelectedTitleId(e.target.value)}\n className={fieldClass}\n disabled={titlesQuery.isLoading}\n required\n >\n <option value=\"\">\n {titlesQuery.isLoading\n ? 'Loading titles…'\n : titles.length === 0\n ? 'No titles available'\n : 'Select a title…'}\n </option>\n {titles.map((t) => (\n <option key={t.id} value={t.id}>\n {t.title}\n </option>\n ))}\n </select>\n {titlesQuery.isError && (\n <p className=\"mt-1.5 text-xs text-status-error-text\">\n Couldn&apos;t load titles. Try refreshing.\n </p>\n )}\n </div>\n <button\n type=\"submit\"\n disabled={createParty.isPending || !selectedTitleId}\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 hover:opacity-90 disabled:opacity-50 transition-opacity\"\n >\n <PlayCircle className=\"h-4 w-4\" />\n {createParty.isPending ? 'Starting…' : 'Start Watching Together'}\n </button>\n </form>\n </GlassCard>\n )}\n\n {/* Join by code */}\n <GlassCard\n treatment=\"glass\"\n className={`border-border-seam p-5 ${showCreate ? '' : 'lg:col-span-2'}`}\n >\n <h2 className=\"mb-3 inline-flex items-center gap-2 text-sm font-semibold text-text-primary\">\n <LogIn className=\"h-4 w-4 text-text-secondary\" />\n Join with a Code\n </h2>\n <form onSubmit={handleJoin} className=\"space-y-4\">\n {joinError && (\n <div className=\"rounded-lg border border-status-error-border bg-status-error-bg-subtle px-3 py-2 text-sm text-status-error-text\">\n {joinError}\n </div>\n )}\n <div>\n <label htmlFor=\"wp-code\" className={labelClass}>\n Invite code\n </label>\n <div className=\"flex flex-col gap-2 sm:flex-row\">\n <input\n id=\"wp-code\"\n type=\"text\"\n value={joinCode}\n onChange={(e) => setJoinCode(e.target.value)}\n placeholder=\"Paste a party invite code\"\n className={fieldClass}\n autoComplete=\"off\"\n />\n <button\n type=\"submit\"\n disabled={joinParty.isPending || !joinCode.trim()}\n className=\"inline-flex shrink-0 items-center justify-center gap-2 rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:opacity-90 disabled:opacity-50 transition-opacity\"\n >\n <LogIn className=\"h-4 w-4\" />\n {joinParty.isPending ? 'Joining…' : 'Join'}\n </button>\n </div>\n <p className=\"mt-1.5 text-xs text-text-muted\">\n Ask the host for their room&apos;s invite code to join.\n </p>\n </div>\n </form>\n </GlassCard>\n </div>\n\n {/* Recent / active parties */}\n <div className=\"mt-8\">\n <div className=\"mb-4 flex items-center justify-between gap-3\">\n <h2 className=\"text-sm font-semibold text-text-primary\">Your Rooms</h2>\n {recent.length > 0 && (\n <span className=\"text-xs text-text-muted\">\n {activeCount} active · {recent.length} recent\n </span>\n )}\n </div>\n\n {recent.length === 0 ? (\n <EmptyState\n icon={<Users className=\"h-12 w-12\" />}\n title=\"No watch parties yet\"\n description=\"Start a room to watch a title in sync with friends, or join one with an invite code.\"\n action={\n <button\n type=\"button\"\n onClick={() => setShowCreate(true)}\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 hover:opacity-90 transition-opacity\"\n >\n <Plus className=\"h-4 w-4\" />\n Start Party\n </button>\n }\n />\n ) : (\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n {recent.map((party) => (\n <GlassCard\n key={party.id}\n treatment=\"metric\"\n className=\"flex flex-col border-border-seam p-4\"\n >\n <div className=\"flex items-start justify-between gap-2\">\n <h3 className=\"line-clamp-2 text-sm font-semibold text-text-primary\">\n {party.titleName}\n </h3>\n <PartyStatusPill status={party.status} />\n </div>\n <div className=\"mt-3 flex items-center gap-3 text-xs text-text-secondary\">\n <span className=\"inline-flex items-center gap-1\">\n <Users className=\"h-3.5 w-3.5\" />\n {party.memberCount} {party.memberCount === 1 ? 'member' : 'members'}\n </span>\n <span className=\"rounded-full bg-bg-sunken px-2 py-0.5 capitalize\">\n {party.role}\n </span>\n </div>\n <p className=\"mt-2 text-[11px] text-text-muted\">\n Updated {formatRelativeTime(party.updatedAt)}\n </p>\n <button\n type=\"button\"\n onClick={() => navigateTo(`watch-party/${party.id}`)}\n className=\"mt-4 inline-flex items-center justify-center gap-2 rounded-lg border border-border-subtle px-3 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken transition-colors\"\n >\n {party.status === 'ENDED' ? 'View room' : 'Enter room'}\n <ArrowRight className=\"h-4 w-4\" />\n </button>\n </GlassCard>\n ))}\n </div>\n )}\n </div>\n </PageLayout>\n );\n};\n"],"mappings":"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"}