@burdenoff/microfe-hookmovies 2026.705.2 → 2026.706.2
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.
- package/dist/hookmovies/HookMoviesRoutes.js +26 -6
- package/dist/hookmovies/HookMoviesRoutes.js.map +1 -1
- package/dist/hookmovies/components/index.js +7 -0
- package/dist/hookmovies/hooks/index.js +1 -0
- package/dist/hookmovies/hooks/useFiles.js +97 -0
- package/dist/hookmovies/hooks/useFiles.js.map +1 -0
- package/dist/hookmovies/hooks/useHookMoviesApi.js +204 -52
- package/dist/hookmovies/hooks/useHookMoviesApi.js.map +1 -1
- package/dist/hookmovies/pages/HomePage.js +2 -2
- package/dist/hookmovies/pages/HomePage.js.map +1 -1
- package/dist/hookmovies/pages/auditions/AuditionDetailPage.js +100 -45
- package/dist/hookmovies/pages/auditions/AuditionDetailPage.js.map +1 -1
- package/dist/hookmovies/pages/catalog/TitleDetailPage.js +1340 -266
- package/dist/hookmovies/pages/catalog/TitleDetailPage.js.map +1 -1
- package/dist/hookmovies/pages/catalog/TitlesPage.js +95 -74
- package/dist/hookmovies/pages/catalog/TitlesPage.js.map +1 -1
- package/dist/hookmovies/pages/consumer/DiscoveryPage.js +422 -0
- package/dist/hookmovies/pages/consumer/DiscoveryPage.js.map +1 -0
- package/dist/hookmovies/pages/consumer/HooksPage.js +252 -0
- package/dist/hookmovies/pages/consumer/HooksPage.js.map +1 -0
- package/dist/hookmovies/pages/consumer/PlayerPage.js +542 -0
- package/dist/hookmovies/pages/consumer/PlayerPage.js.map +1 -0
- package/dist/hookmovies/pages/consumer/TitleViewPage.js +424 -0
- package/dist/hookmovies/pages/consumer/TitleViewPage.js.map +1 -0
- package/dist/hookmovies/pages/distribution/RightsWindowsPage.js +1 -1
- package/dist/hookmovies/pages/distribution/RightsWindowsPage.js.map +1 -1
- package/dist/hookmovies/pages/library/EntitlementsPage.js +273 -75
- package/dist/hookmovies/pages/library/EntitlementsPage.js.map +1 -1
- package/dist/hookmovies/pages/library/ListsPage.js +186 -106
- package/dist/hookmovies/pages/library/ListsPage.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TitleViewPage.js","names":[],"sources":["../../../../src/hookmovies/pages/consumer/TitleViewPage.tsx"],"sourcesContent":["/**\n * Title view (Consumer)\n * @module hookmovies/pages/consumer\n *\n * Audience-facing detail page for a single title (route `/hookmovies/title/:id`).\n * This is the discovery → playback bridge: a cinematic, dark, full-bleed hero\n * built from the title's backdrop / poster file references, an entitlement-gated\n * Play/Resume call-to-action, an optional trailer, cast & crew, the season /\n * episode structure, and a reviews rail.\n *\n * Data comes exclusively from the live `hookmovies` subgraph via the shared\n * hand-written hooks — this page reads, it never mutates:\n * - {@link useTitle} — the title (with credits, seasons, episodes).\n * - {@link useCheckEntitlement} — whether the caller may watch, and how.\n * - {@link useMyWatchProgress} — resume position for the Play/Resume CTA.\n * - {@link useReviews} — the audience reviews rail.\n * - {@link useFileUrl} — resolves backdrop / poster / trailer file ids\n * to presigned URLs (no media is stored here).\n *\n * Entitlement branches:\n * - Entitled → Play/Resume navigates to the player (`watch/:id`), showing the\n * resume timecode when watch progress exists.\n * - Not entitled → a \"Where to watch\" CTA links to the (separate) checkout route\n * (`checkout/:id`) — this page only links there, it never builds\n * or invokes checkout.\n */\n\nimport { type FC, useCallback, useEffect, useMemo, useState } from 'react';\nimport { useParams } from 'react-router-dom';\nimport {\n ArrowLeft,\n BadgeCheck,\n Calendar,\n ChevronRight,\n Clapperboard,\n Clock,\n Film,\n Globe,\n Languages,\n Layers,\n Lock,\n Play,\n ShieldCheck,\n Sparkles,\n Star,\n Ticket,\n Users,\n X,\n} from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { EmptyState } from '../../components/EmptyState';\nimport { LoadingState, ErrorState } from '../../components/StateViews';\nimport { RatingDisplay } from '../../components/RatingDisplay';\nimport { useHookMovies } from '../../context/HookMoviesContext';\nimport {\n useTitle,\n useCheckEntitlement,\n useMyWatchProgress,\n useReviews,\n type HokCredit,\n type HokEpisodeSummary,\n} from '../../hooks/useHookMoviesApi';\nimport { useFileUrl } from '../../hooks/useFiles';\nimport { formatEnumLabel } from '../../utils/formatters';\nimport { cn } from '../../utils/cn';\n\n// ── Small display helpers ────────────────────────────────────────────────────\n\n/** Format a runtime in minutes as \"1h 58m\" / \"45m\". */\nfunction formatRuntime(minutes: number): string {\n if (minutes <= 0) return '';\n const hours = Math.floor(minutes / 60);\n const rest = minutes % 60;\n if (hours === 0) return `${rest}m`;\n if (rest === 0) return `${hours}h`;\n return `${hours}h ${rest}m`;\n}\n\n/** Format a seconds offset as a \"h:mm:ss\" / \"m:ss\" timecode for the resume CTA. */\nfunction formatTimecode(totalSeconds: number): string {\n const s = Math.max(0, Math.floor(totalSeconds));\n const hours = Math.floor(s / 3600);\n const minutes = Math.floor((s % 3600) / 60);\n const seconds = s % 60;\n const mm = String(minutes).padStart(2, '0');\n const ss = String(seconds).padStart(2, '0');\n return hours > 0 ? `${hours}:${mm}:${ss}` : `${minutes}:${ss}`;\n}\n\n/** Two-letter initials for a person avatar fallback (people carry no image ref). */\nfunction initials(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\n// ── Cast & crew ──────────────────────────────────────────────────────────────\n\nconst CreditCard: FC<{ credit: HokCredit }> = ({ credit }) => {\n const name = credit.person?.name ?? 'Unknown';\n const secondary = credit.characterName\n ? `as ${credit.characterName}`\n : formatEnumLabel(credit.role);\n return (\n <div className=\"flex items-center gap-3 rounded-xl border border-border-seam bg-bg-surface/60 p-3\">\n <div\n className=\"flex size-11 shrink-0 items-center justify-center rounded-full bg-bg-sunken text-sm font-semibold text-text-secondary\"\n aria-hidden=\"true\"\n >\n {initials(name)}\n </div>\n <div className=\"min-w-0\">\n <p className=\"truncate text-sm font-medium text-text-primary\">{name}</p>\n <p className=\"truncate text-xs text-text-secondary\">{secondary}</p>\n </div>\n </div>\n );\n};\n\n// ── Episodes ─────────────────────────────────────────────────────────────────\n\nconst EpisodeRow: FC<{ episode: HokEpisodeSummary; onPlay: () => void }> = ({\n episode,\n onPlay,\n}) => (\n <button\n type=\"button\"\n onClick={onPlay}\n className=\"group flex w-full items-center gap-4 rounded-xl border border-border-seam bg-bg-surface/60 p-3 text-left transition-colors hover:bg-bg-sunken focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]\"\n >\n <div className=\"flex size-10 shrink-0 items-center justify-center rounded-lg bg-bg-sunken text-sm font-semibold text-text-secondary\">\n {episode.number}\n </div>\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate text-sm font-medium text-text-primary\">{episode.name}</p>\n {episode.synopsis && (\n <p className=\"line-clamp-1 text-xs text-text-secondary\">{episode.synopsis}</p>\n )}\n </div>\n {episode.runtimeMinutes ? (\n <span className=\"shrink-0 text-xs text-text-secondary\">\n {formatRuntime(episode.runtimeMinutes)}\n </span>\n ) : null}\n <span className=\"flex size-8 shrink-0 items-center justify-center rounded-full bg-action-primary-bg/0 text-text-secondary transition-colors group-hover:bg-action-primary-bg group-hover:text-action-primary-text\">\n <Play className=\"h-4 w-4\" />\n </span>\n </button>\n);\n\n// ── Trailer modal ────────────────────────────────────────────────────────────\n\nconst TrailerModal: FC<{ url: string | null; isLoading: boolean; onClose: () => void }> = ({\n url,\n isLoading,\n onClose,\n}) => (\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-label=\"Trailer\"\n onClick={onClose}\n >\n <div\n className=\"relative w-full max-w-4xl overflow-hidden rounded-2xl border border-border-seam bg-bg-surface shadow-2xl\"\n onClick={(e) => e.stopPropagation()}\n >\n <button\n type=\"button\"\n onClick={onClose}\n className=\"absolute right-3 top-3 z-10 flex size-9 items-center justify-center rounded-full bg-bg-canvas/70 text-text-primary transition-colors hover:bg-bg-canvas focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]\"\n aria-label=\"Close trailer\"\n >\n <X className=\"h-5 w-5\" />\n </button>\n <div className=\"flex aspect-video w-full items-center justify-center bg-bg-canvas\">\n {isLoading ? (\n <LoadingState label=\"Loading trailer…\" />\n ) : url ? (\n <video src={url} controls autoPlay className=\"h-full w-full\" aria-label=\"Title trailer\">\n <track kind=\"captions\" />\n </video>\n ) : (\n <p className=\"p-8 text-sm text-text-secondary\">Trailer is unavailable right now.</p>\n )}\n </div>\n </div>\n </div>\n);\n\n// ── Page ─────────────────────────────────────────────────────────────────────\n\nexport const TitleViewPage: FC = () => {\n const { id } = useParams<{ id: string }>();\n const { navigateTo } = useHookMovies();\n\n const { data: title, isLoading, isError, refetch } = useTitle(id);\n const entitlement = useCheckEntitlement(id);\n const watchProgress = useMyWatchProgress(id);\n const reviews = useReviews({ titleId: id }, { limit: 12, offset: 0 });\n\n const backdrop = useFileUrl(title?.backdropFileId);\n const poster = useFileUrl(title?.posterFileId);\n\n const [showTrailer, setShowTrailer] = useState(false);\n const trailer = useFileUrl(showTrailer ? title?.trailerFileId : undefined);\n\n // Close the trailer modal on Escape.\n useEffect(() => {\n if (!showTrailer) return;\n const onKey = (e: KeyboardEvent) => {\n if (e.key === 'Escape') setShowTrailer(false);\n };\n window.addEventListener('keydown', onKey);\n return () => window.removeEventListener('keydown', onKey);\n }, [showTrailer]);\n\n // Resume position: the title-level (episodeId === null) progress row, if any.\n const resumeSeconds = useMemo(() => {\n const rows = watchProgress.data ?? [];\n const row = rows.find((r) => r.titleId === id && !r.episodeId && !r.completed);\n return row && row.positionSeconds > 0 ? row.positionSeconds : 0;\n }, [watchProgress.data, id]);\n\n const goBack = useCallback(() => navigateTo('browse'), [navigateTo]);\n\n if (isLoading) {\n return (\n <div className=\"flex h-full flex-col bg-bg-canvas\">\n <LoadingState label=\"Loading title…\" />\n </div>\n );\n }\n\n if (isError || !title) {\n return (\n <div className=\"flex h-full flex-col bg-bg-canvas\">\n <ErrorState\n title=\"Title not found\"\n message=\"This title may have been unpublished, or you may not have access to it.\"\n onRetry={() => void refetch()}\n />\n </div>\n );\n }\n\n const entitled = entitlement.data?.entitled ?? false;\n const reviewItems = reviews.data?.items ?? [];\n const avgScore =\n reviewItems.length > 0\n ? reviewItems.reduce((sum, r) => sum + r.score, 0) / reviewItems.length\n : null;\n\n const credits = [...title.credits].sort((a, b) => a.billingOrder - b.billingOrder);\n const episodes = [...title.episodes].sort((a, b) => a.number - b.number);\n const seasons = [...title.seasons].sort((a, b) => a.number - b.number);\n const isSeries = title.type === 'SERIES';\n\n // Group episodes by season (falling back to a single \"ungrouped\" bucket).\n const episodesBySeason = seasons.map((season) => ({\n season,\n items: episodes.filter((ep) => ep.seasonId === season.id),\n }));\n const ungroupedEpisodes = episodes.filter(\n (ep) => !ep.seasonId || !seasons.some((s) => s.id === ep.seasonId)\n );\n\n // Compact meta chips under the title.\n const metaItems: Array<{ icon: typeof Calendar; label: string }> = [];\n if (title.releaseYear) metaItems.push({ icon: Calendar, label: String(title.releaseYear) });\n if (title.runtimeMinutes)\n metaItems.push({ icon: Clock, label: formatRuntime(title.runtimeMinutes) });\n if (title.certification) metaItems.push({ icon: ShieldCheck, label: title.certification });\n if (title.originalLanguage)\n metaItems.push({ icon: Languages, label: title.originalLanguage.toUpperCase() });\n if (title.originCountry) metaItems.push({ icon: Globe, label: title.originCountry });\n\n const playEpisode = (episodeId: string) => navigateTo(`watch/${title.id}/episode/${episodeId}`);\n\n return (\n <div className=\"flex h-full flex-col overflow-y-auto bg-bg-canvas\">\n {/* ── Cinematic backdrop hero ─────────────────────────────────────────── */}\n <section className=\"relative isolate\">\n {/* Backdrop image / fallback */}\n <div className=\"absolute inset-0 -z-10 overflow-hidden bg-bg-sunken\">\n {backdrop.data ? (\n <img\n src={backdrop.data}\n alt=\"\"\n aria-hidden=\"true\"\n className=\"h-full w-full object-cover object-center\"\n />\n ) : (\n <div className=\"flex h-full w-full items-center justify-center bg-bg-sunken\">\n <Clapperboard className=\"h-16 w-16 text-text-muted\" aria-hidden=\"true\" />\n </div>\n )}\n {/* Legibility scrims (bottom + left), token-only gradients */}\n <div className=\"absolute inset-0 bg-gradient-to-t from-bg-canvas via-bg-canvas/70 to-bg-canvas/20\" />\n <div className=\"absolute inset-0 bg-gradient-to-r from-bg-canvas/90 via-bg-canvas/40 to-transparent\" />\n </div>\n\n {/* Back control */}\n <div className=\"px-4 pt-4 sm:px-8\">\n <button\n type=\"button\"\n onClick={goBack}\n className=\"inline-flex items-center gap-2 rounded-full border border-border-seam bg-bg-surface/60 px-3 py-1.5 text-sm font-medium text-text-primary backdrop-blur transition-colors hover:bg-bg-surface focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]\"\n >\n <ArrowLeft className=\"h-4 w-4\" />\n Browse\n </button>\n </div>\n\n {/* Hero content */}\n <div className=\"mx-auto flex max-w-6xl flex-col gap-6 px-4 pb-10 pt-24 sm:px-8 sm:pt-36 md:flex-row md:items-end lg:pt-48\">\n {/* Poster */}\n <div className=\"w-32 shrink-0 overflow-hidden rounded-xl border border-border-seam bg-bg-sunken shadow-2xl sm:w-40 md:w-48\">\n <div className=\"aspect-[2/3] w-full\">\n {poster.data ? (\n <img\n src={poster.data}\n alt={`${title.title} poster`}\n className=\"h-full w-full object-cover\"\n />\n ) : (\n <div className=\"flex h-full w-full items-center justify-center\">\n <Film className=\"h-10 w-10 text-text-muted\" aria-hidden=\"true\" />\n </div>\n )}\n </div>\n </div>\n\n {/* Title + meta + CTAs */}\n <div className=\"min-w-0 flex-1\">\n <p className=\"mb-2 text-xs font-semibold uppercase tracking-widest text-text-secondary\">\n {formatEnumLabel(title.type)}\n {title.studio ? ` · ${title.studio.name}` : ''}\n </p>\n <h1 className=\"text-3xl font-bold leading-tight tracking-tight text-text-primary sm:text-4xl lg:text-5xl\">\n {title.title}\n </h1>\n\n {metaItems.length > 0 && (\n <div className=\"mt-3 flex flex-wrap items-center gap-x-4 gap-y-2 text-sm text-text-secondary\">\n {metaItems.map((m) => {\n const Icon = m.icon;\n return (\n <span key={m.label} className=\"inline-flex items-center gap-1.5\">\n <Icon className=\"h-4 w-4\" aria-hidden=\"true\" />\n {m.label}\n </span>\n );\n })}\n {avgScore !== null && (\n <span className=\"inline-flex items-center gap-1.5\">\n <Star\n className=\"h-4 w-4 fill-status-warning-text text-status-warning-text\"\n aria-hidden=\"true\"\n />\n {(avgScore / 2).toFixed(1)}\n </span>\n )}\n </div>\n )}\n\n {title.genres.length > 0 && (\n <div className=\"mt-3 flex flex-wrap gap-1.5\">\n {title.genres.map((g) => (\n <span\n key={g}\n className=\"rounded-full border border-border-seam bg-bg-surface/60 px-2.5 py-0.5 text-xs text-text-primary backdrop-blur\"\n >\n {g}\n </span>\n ))}\n </div>\n )}\n\n {title.aiDisclosure && (\n <p className=\"mt-3 inline-flex items-center gap-1.5 text-xs text-text-secondary\">\n <Sparkles className=\"h-3.5 w-3.5\" aria-hidden=\"true\" />\n Contains AI-generated or AI-altered content\n </p>\n )}\n\n {/* Call to action row */}\n <div className=\"mt-6 flex flex-wrap items-center gap-3\">\n {entitlement.isLoading ? (\n <span className=\"inline-flex items-center gap-2 rounded-xl bg-bg-sunken px-5 py-2.5 text-sm font-medium text-text-secondary\">\n <span className=\"size-4 animate-spin rounded-full border-2 border-border-strong border-t-transparent\" />\n Checking access…\n </span>\n ) : entitled ? (\n <button\n type=\"button\"\n onClick={() => navigateTo(`watch/${title.id}`)}\n className=\"inline-flex items-center gap-2 rounded-xl bg-action-primary-bg px-6 py-2.5 text-sm font-semibold text-action-primary-text shadow-lg transition-opacity hover:opacity-90 focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]\"\n >\n <Play className=\"h-5 w-5 fill-current\" />\n {resumeSeconds > 0 ? `Resume from ${formatTimecode(resumeSeconds)}` : 'Play'}\n </button>\n ) : (\n <button\n type=\"button\"\n onClick={() => navigateTo(`checkout/${title.id}`)}\n className=\"inline-flex items-center gap-2 rounded-xl bg-action-primary-bg px-6 py-2.5 text-sm font-semibold text-action-primary-text shadow-lg transition-opacity hover:opacity-90 focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]\"\n >\n <Ticket className=\"h-5 w-5\" />\n Where to watch\n </button>\n )}\n\n {title.trailerFileId && (\n <button\n type=\"button\"\n onClick={() => setShowTrailer(true)}\n className=\"inline-flex items-center gap-2 rounded-xl border border-border-seam bg-bg-surface/60 px-5 py-2.5 text-sm font-semibold text-text-primary backdrop-blur transition-colors hover:bg-bg-surface focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]\"\n >\n <Film className=\"h-5 w-5\" />\n Watch Trailer\n </button>\n )}\n </div>\n\n {/* Access hint when not entitled */}\n {!entitlement.isLoading && !entitled && (\n <p className=\"mt-3 inline-flex items-center gap-1.5 text-xs text-text-secondary\">\n <Lock className=\"h-3.5 w-3.5\" aria-hidden=\"true\" />\n You don't have access yet — see your viewing options.\n </p>\n )}\n {entitled && entitlement.data && entitlement.data.modes.length > 0 && (\n <div className=\"mt-3 flex flex-wrap gap-1.5\">\n {entitlement.data.modes.map((mode) => (\n <span\n key={mode}\n className=\"inline-flex items-center gap-1 rounded-full bg-status-success-bg-subtle px-2.5 py-0.5 text-xs text-status-success-text\"\n >\n <BadgeCheck className=\"h-3.5 w-3.5\" aria-hidden=\"true\" />\n {formatEnumLabel(mode)}\n </span>\n ))}\n </div>\n )}\n </div>\n </div>\n </section>\n\n {/* ── Body ────────────────────────────────────────────────────────────── */}\n <div className=\"mx-auto w-full max-w-6xl space-y-10 px-4 pb-16 sm:px-8\">\n {/* Synopsis */}\n {title.synopsis && (\n <section>\n <h2 className=\"mb-3 text-lg font-semibold text-text-primary\">Synopsis</h2>\n <p className=\"max-w-3xl text-sm leading-relaxed text-text-secondary sm:text-base\">\n {title.synopsis}\n </p>\n </section>\n )}\n\n {/* Cast & Crew */}\n {credits.length > 0 && (\n <section>\n <h2 className=\"mb-4 flex items-center gap-2 text-lg font-semibold text-text-primary\">\n <Users className=\"h-5 w-5\" aria-hidden=\"true\" />\n Cast & Crew\n </h2>\n <div className=\"grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3\">\n {credits.map((credit) => (\n <CreditCard key={credit.id} credit={credit} />\n ))}\n </div>\n </section>\n )}\n\n {/* Seasons & Episodes */}\n {episodes.length > 0 && (\n <section>\n <h2 className=\"mb-4 flex items-center gap-2 text-lg font-semibold text-text-primary\">\n <Layers className=\"h-5 w-5\" aria-hidden=\"true\" />\n {isSeries ? 'Seasons & Episodes' : 'Episodes'}\n </h2>\n <div className=\"space-y-6\">\n {episodesBySeason\n .filter((group) => group.items.length > 0)\n .map((group) => (\n <div key={group.season.id}>\n <h3 className=\"mb-2 text-sm font-semibold text-text-secondary\">\n {group.season.name\n ? `${group.season.number}. ${group.season.name}`\n : `Season ${group.season.number}`}\n </h3>\n <div className=\"space-y-2\">\n {group.items.map((ep) => (\n <EpisodeRow key={ep.id} episode={ep} onPlay={() => playEpisode(ep.id)} />\n ))}\n </div>\n </div>\n ))}\n {ungroupedEpisodes.length > 0 && (\n <div>\n {episodesBySeason.some((g) => g.items.length > 0) && (\n <h3 className=\"mb-2 text-sm font-semibold text-text-secondary\">\n Other episodes\n </h3>\n )}\n <div className=\"space-y-2\">\n {ungroupedEpisodes.map((ep) => (\n <EpisodeRow key={ep.id} episode={ep} onPlay={() => playEpisode(ep.id)} />\n ))}\n </div>\n </div>\n )}\n </div>\n </section>\n )}\n\n {/* Reviews rail */}\n <section>\n <div className=\"mb-4 flex items-center justify-between gap-3\">\n <h2 className=\"flex items-center gap-2 text-lg font-semibold text-text-primary\">\n <Star className=\"h-5 w-5\" aria-hidden=\"true\" />\n Reviews\n </h2>\n {avgScore !== null && <RatingDisplay value={avgScore / 2} count={reviewItems.length} />}\n </div>\n\n {reviews.isLoading ? (\n <LoadingState label=\"Loading reviews…\" />\n ) : reviews.isError ? (\n <ErrorState title=\"Couldn't load reviews\" onRetry={() => void reviews.refetch()} />\n ) : reviewItems.length === 0 ? (\n <EmptyState\n title=\"No reviews yet\"\n description=\"Be the first to share what you thought once you've watched.\"\n illustration=\"empty-data\"\n />\n ) : (\n <div\n className={cn(\n 'flex snap-x snap-mandatory gap-4 overflow-x-auto pb-2',\n '[scrollbar-width:thin]'\n )}\n >\n {reviewItems.map((review) => (\n <GlassCard\n key={review.id}\n treatment=\"metric\"\n className=\"w-72 shrink-0 snap-start border-border-seam p-4\"\n >\n <div className=\"mb-2 flex items-center justify-between gap-2\">\n <span className=\"inline-flex items-center gap-1 text-sm font-semibold text-text-primary\">\n <Star\n className=\"h-4 w-4 fill-status-warning-text text-status-warning-text\"\n aria-hidden=\"true\"\n />\n {review.score}/10\n </span>\n {review.verifiedWatch && (\n <span className=\"inline-flex items-center gap-1 rounded-full bg-status-success-bg-subtle px-2 py-0.5 text-[10px] font-medium text-status-success-text\">\n <BadgeCheck className=\"h-3 w-3\" aria-hidden=\"true\" />\n Verified\n </span>\n )}\n </div>\n {review.hasSpoilers && (\n <span className=\"mb-1.5 inline-block rounded-full bg-status-warning-bg-subtle px-2 py-0.5 text-[10px] font-medium text-status-warning-text\">\n Spoilers\n </span>\n )}\n {review.body ? (\n <p className=\"line-clamp-5 text-sm leading-relaxed text-text-secondary\">\n {review.body}\n </p>\n ) : (\n <p className=\"text-sm italic text-text-muted\">No written review.</p>\n )}\n {review.helpfulCount > 0 && (\n <p className=\"mt-3 text-xs text-text-secondary\">\n {review.helpfulCount} found this helpful\n </p>\n )}\n </GlassCard>\n ))}\n </div>\n )}\n </section>\n\n {/* Nudge back into the catalog */}\n <div>\n <button\n type=\"button\"\n onClick={goBack}\n className=\"inline-flex items-center gap-1.5 text-sm font-medium text-text-secondary transition-colors hover:text-text-primary focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)] rounded\"\n >\n Explore more titles\n <ChevronRight className=\"h-4 w-4\" />\n </button>\n </div>\n </div>\n\n {/* Trailer modal */}\n {showTrailer && (\n <TrailerModal\n url={trailer.data ?? null}\n isLoading={trailer.isLoading}\n onClose={() => setShowTrailer(false)}\n />\n )}\n </div>\n );\n};\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RightsWindowsPage.js","names":[],"sources":["../../../../src/hookmovies/pages/distribution/RightsWindowsPage.tsx"],"sourcesContent":["/**\n * Rights Windows (Rights & distribution)\n * @module hookmovies/pages/distribution\n *\n * The availability source of truth. Lists rights windows via\n * `hokListRightsWindows`, creates them with `hokCreateRightsWindow`, and moves\n * them through their lifecycle with `hokTransitionRightsWindow`.\n */\n\nimport { type FC, type FormEvent, useState } from 'react';\nimport { ScrollText, Plus, X, Globe, ShieldCheck } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { StatusBadge } from '../../components/StatusBadge';\nimport { EmptyState } from '../../components/EmptyState';\nimport { LoadingState, ErrorState } from '../../components/StateViews';\nimport { Pagination } from '../../components/Pagination';\nimport {\n useRightsWindows,\n useCreateRightsWindow,\n useTransitionRightsWindow,\n useTitles,\n type HokRightsWindowStatus,\n type HokMonetizationMode,\n type CreateRightsWindowInput,\n} from '../../hooks/useHookMoviesApi';\nimport { formatDate, formatEnumLabel } from '../../utils/formatters';\n\nconst STATUS_OPTIONS: HokRightsWindowStatus[] = [\n 'DRAFT',\n 'PENDING',\n 'ACTIVE',\n 'EXPIRED',\n 'REVOKED',\n];\n\nconst MODE_OPTIONS: HokMonetizationMode[] = [\n 'FREE_AVOD',\n 'SVOD',\n 'TVOD_RENT',\n 'TVOD_BUY',\n 'PPV',\n 'FAST',\n];\n\nconst NEXT_STATUSES: Record<HokRightsWindowStatus, HokRightsWindowStatus[]> = {\n DRAFT: ['PENDING', 'ACTIVE'],\n PENDING: ['ACTIVE', 'REVOKED'],\n ACTIVE: ['EXPIRED', 'REVOKED'],\n EXPIRED: [],\n REVOKED: [],\n};\n\nconst PAGE_LIMIT = 20;\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\nexport const RightsWindowsPage: FC = () => {\n const [statusFilter, setStatusFilter] = useState<HokRightsWindowStatus | ''>('');\n const [page, setPage] = useState(0);\n const [showCreate, setShowCreate] = useState(false);\n\n const { data, isLoading, isError, refetch } = useRightsWindows(\n { status: statusFilter || undefined },\n { limit: PAGE_LIMIT, offset: page * PAGE_LIMIT }\n );\n const titles = useTitles({}, { limit: 200, offset: 0 });\n const createWindow = useCreateRightsWindow();\n const transition = useTransitionRightsWindow();\n\n const [titleId, setTitleId] = useState('');\n const [modes, setModes] = useState<HokMonetizationMode[]>(['SVOD']);\n const [territories, setTerritories] = useState('');\n const [exclusive, setExclusive] = useState(false);\n const [currency, setCurrency] = useState('USD');\n const [startsAt, setStartsAt] = useState('');\n const [endsAt, setEndsAt] = useState('');\n const [error, setError] = useState<string | null>(null);\n const [actionError, setActionError] = useState<string | null>(null);\n\n const toggleMode = (mode: HokMonetizationMode) => {\n setModes((prev) => (prev.includes(mode) ? prev.filter((m) => m !== mode) : [...prev, mode]));\n };\n\n const handleSubmit = async (e: FormEvent) => {\n e.preventDefault();\n setError(null);\n if (!titleId) {\n setError('Select a title.');\n return;\n }\n if (modes.length === 0) {\n setError('Select at least one monetization mode.');\n return;\n }\n const input: CreateRightsWindowInput = {\n titleId,\n modes,\n territories: territories.trim()\n ? territories\n .split(',')\n .map((t) => t.trim().toUpperCase())\n .filter(Boolean)\n : undefined,\n exclusive,\n currency: currency.trim() || undefined,\n startsAt: startsAt ? new Date(startsAt).toISOString() : undefined,\n endsAt: endsAt ? new Date(endsAt).toISOString() : undefined,\n };\n try {\n await createWindow.mutateAsync(input);\n setTitleId('');\n setModes(['SVOD']);\n setTerritories('');\n setExclusive(false);\n setStartsAt('');\n setEndsAt('');\n setShowCreate(false);\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to create rights window');\n }\n };\n\n const handleTransition = async (id: string, status: HokRightsWindowStatus) => {\n setActionError(null);\n try {\n await transition.mutateAsync({ id, status });\n } catch (err) {\n setActionError(err instanceof Error ? err.message : 'Transition failed');\n }\n };\n\n const items = data?.items ?? [];\n\n return (\n <PageLayout\n title=\"Rights & Distribution\"\n description=\"Licensing windows by territory, mode & exclusivity\"\n purpose=\"Rights windows are the source of truth for availability. Define what is licensed where, when, and how it can be monetized — entitlements derive from these windows.\"\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 New Rights Window\n </button>\n }\n >\n {actionError && (\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 {actionError}\n </div>\n )}\n\n {showCreate && (\n <GlassCard treatment=\"glass\" className=\"mb-6 border-border-seam p-5\">\n <div className=\"mb-3 flex items-center justify-between\">\n <h2 className=\"text-sm font-semibold text-text-primary\">Create Rights Window</h2>\n <button\n type=\"button\"\n onClick={() => setShowCreate(false)}\n className=\"rounded-md p-1 text-text-secondary hover:text-text-primary\"\n >\n <X className=\"h-4 w-4\" />\n </button>\n </div>\n <form onSubmit={handleSubmit} className=\"space-y-4\">\n {error && (\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 {error}\n </div>\n )}\n <div>\n <label htmlFor=\"rights-title\" className={labelClass}>\n Title *\n </label>\n <select\n id=\"rights-title\"\n value={titleId}\n onChange={(e) => setTitleId(e.target.value)}\n className={fieldClass}\n required\n >\n <option value=\"\">Select a title…</option>\n {(titles.data?.items ?? []).map((t) => (\n <option key={t.id} value={t.id}>\n {t.title}\n </option>\n ))}\n </select>\n </div>\n\n <div>\n <span className={labelClass}>Monetization Modes *</span>\n <div className=\"flex flex-wrap gap-2\">\n {MODE_OPTIONS.map((mode) => (\n <button\n key={mode}\n type=\"button\"\n onClick={() => toggleMode(mode)}\n className={\n modes.includes(mode)\n ? 'rounded-full bg-action-primary-bg px-3 py-1 text-xs font-medium text-action-primary-text'\n : 'rounded-full border border-border-seam px-3 py-1 text-xs text-text-secondary hover:bg-bg-sunken transition-colors'\n }\n >\n {formatEnumLabel(mode)}\n </button>\n ))}\n </div>\n </div>\n\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <div>\n <label htmlFor=\"rights-territories\" className={labelClass}>\n Territories (ISO codes, comma-separated)\n </label>\n <input\n id=\"rights-territories\"\n type=\"text\"\n value={territories}\n onChange={(e) => setTerritories(e.target.value)}\n placeholder=\"US, IN, GB\"\n className={fieldClass}\n />\n </div>\n <div>\n <label htmlFor=\"rights-currency\" className={labelClass}>\n Currency\n </label>\n <input\n id=\"rights-currency\"\n type=\"text\"\n value={currency}\n onChange={(e) => setCurrency(e.target.value)}\n placeholder=\"USD\"\n className={fieldClass}\n />\n </div>\n </div>\n\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <div>\n <label htmlFor=\"rights-start\" className={labelClass}>\n Starts At\n </label>\n <input\n id=\"rights-start\"\n type=\"date\"\n value={startsAt}\n onChange={(e) => setStartsAt(e.target.value)}\n className={fieldClass}\n />\n </div>\n <div>\n <label htmlFor=\"rights-end\" className={labelClass}>\n Ends At\n </label>\n <input\n id=\"rights-end\"\n type=\"date\"\n value={endsAt}\n onChange={(e) => setEndsAt(e.target.value)}\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={exclusive}\n onChange={(e) => setExclusive(e.target.checked)}\n className=\"h-4 w-4 rounded border-border-subtle\"\n />\n Exclusive license\n </label>\n\n <button\n type=\"submit\"\n disabled={createWindow.isPending}\n className=\"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 {createWindow.isPending ? 'Creating…' : 'Create Window'}\n </button>\n </form>\n </GlassCard>\n )}\n\n <div className=\"mb-4 flex flex-wrap items-center gap-3\">\n <select\n value={statusFilter}\n onChange={(e) => {\n setStatusFilter(e.target.value as HokRightsWindowStatus | '');\n setPage(0);\n }}\n className=\"rounded-lg border border-border-subtle bg-bg-surface px-3 py-2 text-xs text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n >\n <option value=\"\">Any Status</option>\n {STATUS_OPTIONS.map((s) => (\n <option key={s} value={s}>\n {formatEnumLabel(s)}\n </option>\n ))}\n </select>\n </div>\n\n {isLoading ? (\n <LoadingState label=\"Loading rights windows…\" />\n ) : isError ? (\n <ErrorState onRetry={() => void refetch()} />\n ) : items.length === 0 ? (\n <EmptyState\n icon={<ScrollText className=\"h-12 w-12\" />}\n title=\"No rights windows\"\n description=\"Create a rights window to make titles available.\"\n />\n ) : (\n <div className=\"space-y-3\">\n {items.map((win) => (\n <GlassCard key={win.id} treatment=\"metric\" className=\"border-border-seam p-4\">\n <div className=\"flex items-start justify-between gap-3\">\n <div className=\"min-w-0\">\n <h3 className=\"truncate text-sm font-semibold text-text-primary\">\n {win.title?.title ?? 'Title'}\n </h3>\n <div className=\"mt-1 flex flex-wrap items-center gap-1.5\">\n {win.modes.map((mode) => (\n <span\n key={mode}\n className=\"rounded-full bg-bg-sunken px-2 py-0.5 text-[10px] text-text-secondary\"\n >\n {formatEnumLabel(mode)}\n </span>\n ))}\n {win.exclusive && (\n <span className=\"inline-flex items-center gap-1 rounded-full bg-status-info-bg-subtle px-2 py-0.5 text-[10px] text-status-info-text\">\n <ShieldCheck className=\"h-3 w-3\" />\n Exclusive\n </span>\n )}\n </div>\n </div>\n <StatusBadge status={win.status} />\n </div>\n\n <div className=\"mt-3 flex flex-wrap items-center gap-x-4 gap-y-1 text-xs text-text-secondary\">\n {win.territories.length > 0 && (\n <span className=\"inline-flex items-center gap-1\">\n <Globe className=\"h-3 w-3\" />\n {win.territories.join(', ')}\n </span>\n )}\n {win.startsAt && (\n <span>\n {formatDate(win.startsAt, { hour: undefined, minute: undefined })}\n {win.endsAt\n ? ` → ${formatDate(win.endsAt, { hour: undefined, minute: undefined })}`\n : ' onward'}\n </span>\n )}\n </div>\n\n {NEXT_STATUSES[win.status].length > 0 && (\n <div className=\"mt-3 flex flex-wrap gap-2\">\n {NEXT_STATUSES[win.status].map((status) => (\n <button\n key={status}\n type=\"button\"\n disabled={transition.isPending}\n onClick={() => void handleTransition(win.id, status)}\n className=\"rounded-lg border border-border-seam px-2.5 py-1 text-xs font-medium text-text-primary hover:bg-bg-sunken disabled:opacity-50 transition-colors\"\n >\n Mark {formatEnumLabel(status)}\n </button>\n ))}\n </div>\n )}\n </GlassCard>\n ))}\n </div>\n )}\n\n {!isLoading && !isError && (\n <Pagination\n page={page}\n hasMore={data?.pageInfo.hasMore ?? false}\n onPageChange={setPage}\n shownCount={items.length}\n />\n )}\n </PageLayout>\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"RightsWindowsPage.js","names":[],"sources":["../../../../src/hookmovies/pages/distribution/RightsWindowsPage.tsx"],"sourcesContent":["/**\n * Rights Windows (Rights & distribution)\n * @module hookmovies/pages/distribution\n *\n * The availability source of truth. Lists rights windows via\n * `hokListRightsWindows`, creates them with `hokCreateRightsWindow`, and moves\n * them through their lifecycle with `hokTransitionRightsWindow`.\n */\n\nimport { type FC, type FormEvent, useState } from 'react';\nimport { ScrollText, Plus, X, Globe, ShieldCheck } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { StatusBadge } from '../../components/StatusBadge';\nimport { EmptyState } from '../../components/EmptyState';\nimport { LoadingState, ErrorState } from '../../components/StateViews';\nimport { Pagination } from '../../components/Pagination';\nimport {\n useRightsWindows,\n useCreateRightsWindow,\n useTransitionRightsWindow,\n useTitles,\n type HokRightsWindowStatus,\n type HokMonetizationMode,\n type CreateRightsWindowInput,\n} from '../../hooks/useHookMoviesApi';\nimport { formatDate, formatEnumLabel } from '../../utils/formatters';\n\nconst STATUS_OPTIONS: HokRightsWindowStatus[] = [\n 'DRAFT',\n 'PENDING',\n 'ACTIVE',\n 'EXPIRED',\n 'REVOKED',\n];\n\nconst MODE_OPTIONS: HokMonetizationMode[] = [\n 'FREE_AVOD',\n 'SVOD',\n 'TVOD_RENT',\n 'TVOD_BUY',\n 'PPV',\n 'FAST',\n];\n\n// Mirrors the svc RIGHTS_WINDOW_TRANSITIONS state machine\n// (wspace-hookmovies-svc monetization.service.ts). Only valid next statuses\n// for the window's current status are offered as transition actions.\nconst NEXT_STATUSES: Record<HokRightsWindowStatus, HokRightsWindowStatus[]> = {\n DRAFT: ['PENDING', 'REVOKED'],\n PENDING: ['ACTIVE', 'REVOKED'],\n ACTIVE: ['EXPIRED', 'REVOKED'],\n EXPIRED: [],\n REVOKED: [],\n};\n\nconst PAGE_LIMIT = 20;\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\nexport const RightsWindowsPage: FC = () => {\n const [statusFilter, setStatusFilter] = useState<HokRightsWindowStatus | ''>('');\n const [page, setPage] = useState(0);\n const [showCreate, setShowCreate] = useState(false);\n\n const { data, isLoading, isError, refetch } = useRightsWindows(\n { status: statusFilter || undefined },\n { limit: PAGE_LIMIT, offset: page * PAGE_LIMIT }\n );\n const titles = useTitles({}, { limit: 200, offset: 0 });\n const createWindow = useCreateRightsWindow();\n const transition = useTransitionRightsWindow();\n\n const [titleId, setTitleId] = useState('');\n const [modes, setModes] = useState<HokMonetizationMode[]>(['SVOD']);\n const [territories, setTerritories] = useState('');\n const [exclusive, setExclusive] = useState(false);\n const [currency, setCurrency] = useState('USD');\n const [startsAt, setStartsAt] = useState('');\n const [endsAt, setEndsAt] = useState('');\n const [error, setError] = useState<string | null>(null);\n const [actionError, setActionError] = useState<string | null>(null);\n\n const toggleMode = (mode: HokMonetizationMode) => {\n setModes((prev) => (prev.includes(mode) ? prev.filter((m) => m !== mode) : [...prev, mode]));\n };\n\n const handleSubmit = async (e: FormEvent) => {\n e.preventDefault();\n setError(null);\n if (!titleId) {\n setError('Select a title.');\n return;\n }\n if (modes.length === 0) {\n setError('Select at least one monetization mode.');\n return;\n }\n const input: CreateRightsWindowInput = {\n titleId,\n modes,\n territories: territories.trim()\n ? territories\n .split(',')\n .map((t) => t.trim().toUpperCase())\n .filter(Boolean)\n : undefined,\n exclusive,\n currency: currency.trim() || undefined,\n startsAt: startsAt ? new Date(startsAt).toISOString() : undefined,\n endsAt: endsAt ? new Date(endsAt).toISOString() : undefined,\n };\n try {\n await createWindow.mutateAsync(input);\n setTitleId('');\n setModes(['SVOD']);\n setTerritories('');\n setExclusive(false);\n setStartsAt('');\n setEndsAt('');\n setShowCreate(false);\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to create rights window');\n }\n };\n\n const handleTransition = async (id: string, status: HokRightsWindowStatus) => {\n setActionError(null);\n try {\n await transition.mutateAsync({ id, status });\n } catch (err) {\n setActionError(err instanceof Error ? err.message : 'Transition failed');\n }\n };\n\n const items = data?.items ?? [];\n\n return (\n <PageLayout\n title=\"Rights & Distribution\"\n description=\"Licensing windows by territory, mode & exclusivity\"\n purpose=\"Rights windows are the source of truth for availability. Define what is licensed where, when, and how it can be monetized — entitlements derive from these windows.\"\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 New Rights Window\n </button>\n }\n >\n {actionError && (\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 {actionError}\n </div>\n )}\n\n {showCreate && (\n <GlassCard treatment=\"glass\" className=\"mb-6 border-border-seam p-5\">\n <div className=\"mb-3 flex items-center justify-between\">\n <h2 className=\"text-sm font-semibold text-text-primary\">Create Rights Window</h2>\n <button\n type=\"button\"\n onClick={() => setShowCreate(false)}\n className=\"rounded-md p-1 text-text-secondary hover:text-text-primary\"\n >\n <X className=\"h-4 w-4\" />\n </button>\n </div>\n <form onSubmit={handleSubmit} className=\"space-y-4\">\n {error && (\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 {error}\n </div>\n )}\n <div>\n <label htmlFor=\"rights-title\" className={labelClass}>\n Title *\n </label>\n <select\n id=\"rights-title\"\n value={titleId}\n onChange={(e) => setTitleId(e.target.value)}\n className={fieldClass}\n required\n >\n <option value=\"\">Select a title…</option>\n {(titles.data?.items ?? []).map((t) => (\n <option key={t.id} value={t.id}>\n {t.title}\n </option>\n ))}\n </select>\n </div>\n\n <div>\n <span className={labelClass}>Monetization Modes *</span>\n <div className=\"flex flex-wrap gap-2\">\n {MODE_OPTIONS.map((mode) => (\n <button\n key={mode}\n type=\"button\"\n onClick={() => toggleMode(mode)}\n className={\n modes.includes(mode)\n ? 'rounded-full bg-action-primary-bg px-3 py-1 text-xs font-medium text-action-primary-text'\n : 'rounded-full border border-border-seam px-3 py-1 text-xs text-text-secondary hover:bg-bg-sunken transition-colors'\n }\n >\n {formatEnumLabel(mode)}\n </button>\n ))}\n </div>\n </div>\n\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <div>\n <label htmlFor=\"rights-territories\" className={labelClass}>\n Territories (ISO codes, comma-separated)\n </label>\n <input\n id=\"rights-territories\"\n type=\"text\"\n value={territories}\n onChange={(e) => setTerritories(e.target.value)}\n placeholder=\"US, IN, GB\"\n className={fieldClass}\n />\n </div>\n <div>\n <label htmlFor=\"rights-currency\" className={labelClass}>\n Currency\n </label>\n <input\n id=\"rights-currency\"\n type=\"text\"\n value={currency}\n onChange={(e) => setCurrency(e.target.value)}\n placeholder=\"USD\"\n className={fieldClass}\n />\n </div>\n </div>\n\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <div>\n <label htmlFor=\"rights-start\" className={labelClass}>\n Starts At\n </label>\n <input\n id=\"rights-start\"\n type=\"date\"\n value={startsAt}\n onChange={(e) => setStartsAt(e.target.value)}\n className={fieldClass}\n />\n </div>\n <div>\n <label htmlFor=\"rights-end\" className={labelClass}>\n Ends At\n </label>\n <input\n id=\"rights-end\"\n type=\"date\"\n value={endsAt}\n onChange={(e) => setEndsAt(e.target.value)}\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={exclusive}\n onChange={(e) => setExclusive(e.target.checked)}\n className=\"h-4 w-4 rounded border-border-subtle\"\n />\n Exclusive license\n </label>\n\n <button\n type=\"submit\"\n disabled={createWindow.isPending}\n className=\"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 {createWindow.isPending ? 'Creating…' : 'Create Window'}\n </button>\n </form>\n </GlassCard>\n )}\n\n <div className=\"mb-4 flex flex-wrap items-center gap-3\">\n <select\n value={statusFilter}\n onChange={(e) => {\n setStatusFilter(e.target.value as HokRightsWindowStatus | '');\n setPage(0);\n }}\n className=\"rounded-lg border border-border-subtle bg-bg-surface px-3 py-2 text-xs text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n >\n <option value=\"\">Any Status</option>\n {STATUS_OPTIONS.map((s) => (\n <option key={s} value={s}>\n {formatEnumLabel(s)}\n </option>\n ))}\n </select>\n </div>\n\n {isLoading ? (\n <LoadingState label=\"Loading rights windows…\" />\n ) : isError ? (\n <ErrorState onRetry={() => void refetch()} />\n ) : items.length === 0 ? (\n <EmptyState\n icon={<ScrollText className=\"h-12 w-12\" />}\n title=\"No rights windows\"\n description=\"Create a rights window to make titles available.\"\n />\n ) : (\n <div className=\"space-y-3\">\n {items.map((win) => (\n <GlassCard key={win.id} treatment=\"metric\" className=\"border-border-seam p-4\">\n <div className=\"flex items-start justify-between gap-3\">\n <div className=\"min-w-0\">\n <h3 className=\"truncate text-sm font-semibold text-text-primary\">\n {win.title?.title ?? 'Title'}\n </h3>\n <div className=\"mt-1 flex flex-wrap items-center gap-1.5\">\n {win.modes.map((mode) => (\n <span\n key={mode}\n className=\"rounded-full bg-bg-sunken px-2 py-0.5 text-[10px] text-text-secondary\"\n >\n {formatEnumLabel(mode)}\n </span>\n ))}\n {win.exclusive && (\n <span className=\"inline-flex items-center gap-1 rounded-full bg-status-info-bg-subtle px-2 py-0.5 text-[10px] text-status-info-text\">\n <ShieldCheck className=\"h-3 w-3\" />\n Exclusive\n </span>\n )}\n </div>\n </div>\n <StatusBadge status={win.status} />\n </div>\n\n <div className=\"mt-3 flex flex-wrap items-center gap-x-4 gap-y-1 text-xs text-text-secondary\">\n {win.territories.length > 0 && (\n <span className=\"inline-flex items-center gap-1\">\n <Globe className=\"h-3 w-3\" />\n {win.territories.join(', ')}\n </span>\n )}\n {win.startsAt && (\n <span>\n {formatDate(win.startsAt, { hour: undefined, minute: undefined })}\n {win.endsAt\n ? ` → ${formatDate(win.endsAt, { hour: undefined, minute: undefined })}`\n : ' onward'}\n </span>\n )}\n </div>\n\n {NEXT_STATUSES[win.status].length > 0 && (\n <div className=\"mt-3 flex flex-wrap gap-2\">\n {NEXT_STATUSES[win.status].map((status) => (\n <button\n key={status}\n type=\"button\"\n disabled={transition.isPending}\n onClick={() => void handleTransition(win.id, status)}\n className=\"rounded-lg border border-border-seam px-2.5 py-1 text-xs font-medium text-text-primary hover:bg-bg-sunken disabled:opacity-50 transition-colors\"\n >\n Mark {formatEnumLabel(status)}\n </button>\n ))}\n </div>\n )}\n </GlassCard>\n ))}\n </div>\n )}\n\n {!isLoading && !isError && (\n <Pagination\n page={page}\n hasMore={data?.pageInfo.hasMore ?? false}\n onPageChange={setPage}\n shownCount={items.length}\n />\n )}\n </PageLayout>\n );\n};\n"],"mappings":"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"}
|
|
@@ -1,90 +1,288 @@
|
|
|
1
1
|
import { useHookMovies as e } from "../../context/HookMoviesContext.js";
|
|
2
2
|
import { PageLayout as t } from "../../components/PageLayout.js";
|
|
3
3
|
import { StatusBadge as n } from "../../components/StatusBadge.js";
|
|
4
|
-
import {
|
|
5
|
-
import { formatDate as
|
|
6
|
-
import { ErrorState as
|
|
7
|
-
import { EmptyState as
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
4
|
+
import { useGrantEntitlement as r, useMyEntitlements as i, useRevokeEntitlement as a, useTitles as o } from "../../hooks/useHookMoviesApi.js";
|
|
5
|
+
import { formatDate as s, formatEnumLabel as c } from "../../utils/formatters.js";
|
|
6
|
+
import { ErrorState as l, LoadingState as ee } from "../../components/StateViews.js";
|
|
7
|
+
import { EmptyState as u } from "../../components/EmptyState.js";
|
|
8
|
+
import { useState as d } from "react";
|
|
9
|
+
import { PlayCircle as f, Plus as p, Ticket as m, X as h, XCircle as g } from "lucide-react";
|
|
10
|
+
import { jsx as _, jsxs as v } from "react/jsx-runtime";
|
|
11
|
+
import { GlassCard as y } from "@burdenoff/fe-libs/ui";
|
|
11
12
|
//#region src/hookmovies/pages/library/EntitlementsPage.tsx
|
|
12
|
-
var
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
var b = [
|
|
14
|
+
"FREE_AVOD",
|
|
15
|
+
"SVOD",
|
|
16
|
+
"TVOD_RENT",
|
|
17
|
+
"TVOD_BUY",
|
|
18
|
+
"PPV",
|
|
19
|
+
"FAST"
|
|
20
|
+
], x = [
|
|
21
|
+
"SUBSCRIPTION",
|
|
22
|
+
"PURCHASE",
|
|
23
|
+
"RENTAL",
|
|
24
|
+
"PROMO",
|
|
25
|
+
"FREE"
|
|
26
|
+
], S = "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)]", C = "mb-1.5 block text-xs font-medium text-text-secondary", w = () => {
|
|
27
|
+
let { navigateTo: w, currentUser: T } = e(), { data: E, isLoading: D, isError: O, refetch: k } = i(), A = a(), j = r(), [M, N] = d(!1), [P, F] = d(""), I = o({}, {
|
|
28
|
+
limit: 200,
|
|
29
|
+
offset: 0
|
|
30
|
+
}, P), [L, R] = d(""), [z, B] = d(T?.id ?? ""), [V, H] = d("SVOD"), [U, W] = d("SUBSCRIPTION"), [G, K] = d(""), [q, J] = d(""), [Y, X] = d(null), [Z, Q] = d(null), $ = E ?? [], te = () => {
|
|
31
|
+
R(""), B(T?.id ?? ""), H("SVOD"), W("SUBSCRIPTION"), K(""), J(""), F(""), X(null);
|
|
32
|
+
}, ne = async (e) => {
|
|
33
|
+
if (e.preventDefault(), X(null), !L) {
|
|
34
|
+
X("Select a title.");
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
if (!z.trim()) {
|
|
38
|
+
X("A user id is required.");
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
let t = {
|
|
42
|
+
titleId: L,
|
|
43
|
+
userId: z.trim(),
|
|
44
|
+
mode: V,
|
|
45
|
+
source: U,
|
|
46
|
+
territory: G.trim() ? G.trim().toUpperCase() : void 0,
|
|
47
|
+
expiresAt: q ? new Date(q).toISOString() : void 0
|
|
48
|
+
};
|
|
49
|
+
try {
|
|
50
|
+
await j.mutateAsync(t), te(), N(!1);
|
|
51
|
+
} catch (e) {
|
|
52
|
+
X(e instanceof Error ? e.message : "Failed to grant entitlement");
|
|
53
|
+
}
|
|
54
|
+
}, re = async (e) => {
|
|
55
|
+
Q(null);
|
|
56
|
+
try {
|
|
57
|
+
await A.mutateAsync(e);
|
|
58
|
+
} catch (e) {
|
|
59
|
+
Q(e instanceof Error ? e.message : "Failed to revoke entitlement");
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
return /* @__PURE__ */ v(t, {
|
|
15
63
|
title: "My Entitlements",
|
|
16
64
|
description: "What you can watch right now",
|
|
17
65
|
purpose: "Your computed right to watch each title — from subscriptions, purchases, rentals, or promos. Entitlements derive from plan and rights windows.",
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
/* @__PURE__ */
|
|
35
|
-
className: "
|
|
36
|
-
children:
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
66
|
+
actions: /* @__PURE__ */ v("button", {
|
|
67
|
+
type: "button",
|
|
68
|
+
onClick: () => N((e) => !e),
|
|
69
|
+
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",
|
|
70
|
+
children: [/* @__PURE__ */ _(p, { className: "h-4 w-4" }), "Grant Entitlement"]
|
|
71
|
+
}),
|
|
72
|
+
children: [
|
|
73
|
+
Z && /* @__PURE__ */ _("div", {
|
|
74
|
+
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",
|
|
75
|
+
children: Z
|
|
76
|
+
}),
|
|
77
|
+
M && /* @__PURE__ */ v(y, {
|
|
78
|
+
treatment: "glass",
|
|
79
|
+
className: "mb-6 border-border-seam p-5",
|
|
80
|
+
children: [/* @__PURE__ */ v("div", {
|
|
81
|
+
className: "mb-3 flex items-center justify-between",
|
|
82
|
+
children: [/* @__PURE__ */ _("h2", {
|
|
83
|
+
className: "text-sm font-semibold text-text-primary",
|
|
84
|
+
children: "Grant Entitlement"
|
|
85
|
+
}), /* @__PURE__ */ _("button", {
|
|
86
|
+
type: "button",
|
|
87
|
+
onClick: () => N(!1),
|
|
88
|
+
"aria-label": "Close grant form",
|
|
89
|
+
className: "rounded-md p-1 text-text-secondary hover:text-text-primary",
|
|
90
|
+
children: /* @__PURE__ */ _(h, { className: "h-4 w-4" })
|
|
91
|
+
})]
|
|
92
|
+
}), /* @__PURE__ */ v("form", {
|
|
93
|
+
onSubmit: ne,
|
|
94
|
+
className: "space-y-4",
|
|
95
|
+
children: [
|
|
96
|
+
Y && /* @__PURE__ */ _("div", {
|
|
97
|
+
className: "rounded-lg border border-status-error-border bg-status-error-bg-subtle px-3 py-2 text-sm text-status-error-text",
|
|
98
|
+
children: Y
|
|
99
|
+
}),
|
|
100
|
+
/* @__PURE__ */ v("div", { children: [/* @__PURE__ */ _("label", {
|
|
101
|
+
htmlFor: "grant-title-search",
|
|
102
|
+
className: C,
|
|
103
|
+
children: "Search titles"
|
|
104
|
+
}), /* @__PURE__ */ _("input", {
|
|
105
|
+
id: "grant-title-search",
|
|
106
|
+
type: "text",
|
|
107
|
+
value: P,
|
|
108
|
+
onChange: (e) => F(e.target.value),
|
|
109
|
+
placeholder: "Filter titles by name…",
|
|
110
|
+
className: S
|
|
111
|
+
})] }),
|
|
112
|
+
/* @__PURE__ */ v("div", { children: [/* @__PURE__ */ _("label", {
|
|
113
|
+
htmlFor: "grant-title",
|
|
114
|
+
className: C,
|
|
115
|
+
children: "Title *"
|
|
116
|
+
}), /* @__PURE__ */ v("select", {
|
|
117
|
+
id: "grant-title",
|
|
118
|
+
value: L,
|
|
119
|
+
onChange: (e) => R(e.target.value),
|
|
120
|
+
className: S,
|
|
121
|
+
required: !0,
|
|
122
|
+
children: [/* @__PURE__ */ _("option", {
|
|
123
|
+
value: "",
|
|
124
|
+
children: I.isLoading ? "Loading titles…" : "Select a title…"
|
|
125
|
+
}), (I.data?.items ?? []).map((e) => /* @__PURE__ */ _("option", {
|
|
126
|
+
value: e.id,
|
|
127
|
+
children: e.title
|
|
128
|
+
}, e.id))]
|
|
129
|
+
})] }),
|
|
130
|
+
/* @__PURE__ */ v("div", { children: [
|
|
131
|
+
/* @__PURE__ */ _("label", {
|
|
132
|
+
htmlFor: "grant-user",
|
|
133
|
+
className: C,
|
|
134
|
+
children: "User ID *"
|
|
49
135
|
}),
|
|
50
|
-
/* @__PURE__ */
|
|
51
|
-
|
|
52
|
-
|
|
136
|
+
/* @__PURE__ */ _("input", {
|
|
137
|
+
id: "grant-user",
|
|
138
|
+
type: "text",
|
|
139
|
+
value: z,
|
|
140
|
+
onChange: (e) => B(e.target.value),
|
|
141
|
+
placeholder: "User id to grant access to",
|
|
142
|
+
className: S,
|
|
143
|
+
required: !0
|
|
53
144
|
}),
|
|
54
|
-
|
|
55
|
-
className: "
|
|
56
|
-
children:
|
|
145
|
+
/* @__PURE__ */ _("p", {
|
|
146
|
+
className: "mt-1 text-[11px] text-text-secondary",
|
|
147
|
+
children: "Defaults to your own id. Change it to grant access to another user."
|
|
57
148
|
})
|
|
58
|
-
]
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
149
|
+
] }),
|
|
150
|
+
/* @__PURE__ */ v("div", {
|
|
151
|
+
className: "grid grid-cols-1 gap-4 sm:grid-cols-2",
|
|
152
|
+
children: [/* @__PURE__ */ v("div", { children: [/* @__PURE__ */ _("label", {
|
|
153
|
+
htmlFor: "grant-mode",
|
|
154
|
+
className: C,
|
|
155
|
+
children: "Monetization Mode *"
|
|
156
|
+
}), /* @__PURE__ */ _("select", {
|
|
157
|
+
id: "grant-mode",
|
|
158
|
+
value: V,
|
|
159
|
+
onChange: (e) => H(e.target.value),
|
|
160
|
+
className: S,
|
|
161
|
+
required: !0,
|
|
162
|
+
children: b.map((e) => /* @__PURE__ */ _("option", {
|
|
163
|
+
value: e,
|
|
164
|
+
children: c(e)
|
|
165
|
+
}, e))
|
|
166
|
+
})] }), /* @__PURE__ */ v("div", { children: [/* @__PURE__ */ _("label", {
|
|
167
|
+
htmlFor: "grant-source",
|
|
168
|
+
className: C,
|
|
169
|
+
children: "Source"
|
|
170
|
+
}), /* @__PURE__ */ _("select", {
|
|
171
|
+
id: "grant-source",
|
|
172
|
+
value: U,
|
|
173
|
+
onChange: (e) => W(e.target.value),
|
|
174
|
+
className: S,
|
|
175
|
+
children: x.map((e) => /* @__PURE__ */ _("option", {
|
|
176
|
+
value: e,
|
|
177
|
+
children: c(e)
|
|
178
|
+
}, e))
|
|
179
|
+
})] })]
|
|
180
|
+
}),
|
|
181
|
+
/* @__PURE__ */ v("div", {
|
|
182
|
+
className: "grid grid-cols-1 gap-4 sm:grid-cols-2",
|
|
183
|
+
children: [/* @__PURE__ */ v("div", { children: [/* @__PURE__ */ _("label", {
|
|
184
|
+
htmlFor: "grant-territory",
|
|
185
|
+
className: C,
|
|
186
|
+
children: "Territory (ISO code, optional)"
|
|
187
|
+
}), /* @__PURE__ */ _("input", {
|
|
188
|
+
id: "grant-territory",
|
|
189
|
+
type: "text",
|
|
190
|
+
value: G,
|
|
191
|
+
onChange: (e) => K(e.target.value),
|
|
192
|
+
placeholder: "US",
|
|
193
|
+
className: S
|
|
194
|
+
})] }), /* @__PURE__ */ v("div", { children: [/* @__PURE__ */ _("label", {
|
|
195
|
+
htmlFor: "grant-expires",
|
|
196
|
+
className: C,
|
|
197
|
+
children: "Expires At (optional)"
|
|
198
|
+
}), /* @__PURE__ */ _("input", {
|
|
199
|
+
id: "grant-expires",
|
|
200
|
+
type: "date",
|
|
201
|
+
value: q,
|
|
202
|
+
onChange: (e) => J(e.target.value),
|
|
203
|
+
className: S
|
|
204
|
+
})] })]
|
|
205
|
+
}),
|
|
206
|
+
/* @__PURE__ */ _("button", {
|
|
207
|
+
type: "submit",
|
|
208
|
+
disabled: j.isPending,
|
|
209
|
+
className: "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",
|
|
210
|
+
children: j.isPending ? "Granting…" : "Grant Entitlement"
|
|
211
|
+
})
|
|
212
|
+
]
|
|
213
|
+
})]
|
|
214
|
+
}),
|
|
215
|
+
D ? /* @__PURE__ */ _(ee, { label: "Loading entitlements…" }) : O ? /* @__PURE__ */ _(l, { onRetry: () => void k() }) : $.length === 0 ? /* @__PURE__ */ _(u, {
|
|
216
|
+
icon: /* @__PURE__ */ _(m, { className: "h-12 w-12" }),
|
|
217
|
+
title: "No entitlements yet",
|
|
218
|
+
description: "You don't have any active watch rights. Grant an entitlement above, or browse the catalog to find something to watch.",
|
|
219
|
+
action: /* @__PURE__ */ _("button", {
|
|
220
|
+
type: "button",
|
|
221
|
+
onClick: () => w("catalog/titles"),
|
|
222
|
+
className: "rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:opacity-90 transition-opacity",
|
|
223
|
+
children: "Browse Catalog"
|
|
224
|
+
})
|
|
225
|
+
}) : /* @__PURE__ */ _("div", {
|
|
226
|
+
className: "grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3",
|
|
227
|
+
children: $.map((e) => /* @__PURE__ */ v(y, {
|
|
228
|
+
treatment: "metric",
|
|
229
|
+
className: "flex flex-col border-border-seam p-4",
|
|
230
|
+
children: [
|
|
231
|
+
/* @__PURE__ */ v("div", {
|
|
232
|
+
className: "flex items-start justify-between gap-2",
|
|
233
|
+
children: [/* @__PURE__ */ _("button", {
|
|
234
|
+
type: "button",
|
|
235
|
+
onClick: () => w(`catalog/titles/${e.titleId}`),
|
|
236
|
+
className: "line-clamp-2 text-left text-sm font-semibold text-text-primary hover:text-action-primary-bg transition-colors",
|
|
237
|
+
children: e.title?.title ?? "Title"
|
|
238
|
+
}), /* @__PURE__ */ _(n, { status: e.status })]
|
|
239
|
+
}),
|
|
240
|
+
/* @__PURE__ */ v("div", {
|
|
241
|
+
className: "mt-2 flex flex-wrap items-center gap-1.5 text-xs text-text-secondary",
|
|
242
|
+
children: [
|
|
243
|
+
/* @__PURE__ */ _("span", {
|
|
244
|
+
className: "rounded-full bg-bg-sunken px-2 py-0.5",
|
|
245
|
+
children: c(e.mode)
|
|
246
|
+
}),
|
|
247
|
+
/* @__PURE__ */ _("span", {
|
|
248
|
+
className: "rounded-full bg-bg-sunken px-2 py-0.5",
|
|
249
|
+
children: c(e.source)
|
|
250
|
+
}),
|
|
251
|
+
e.territory && /* @__PURE__ */ _("span", {
|
|
252
|
+
className: "rounded-full bg-bg-sunken px-2 py-0.5",
|
|
253
|
+
children: e.territory
|
|
254
|
+
})
|
|
255
|
+
]
|
|
256
|
+
}),
|
|
257
|
+
e.expiresAt && /* @__PURE__ */ v("p", {
|
|
258
|
+
className: "mt-2 text-xs text-text-secondary",
|
|
259
|
+
children: ["Expires ", s(e.expiresAt, {
|
|
260
|
+
hour: void 0,
|
|
261
|
+
minute: void 0
|
|
262
|
+
})]
|
|
263
|
+
}),
|
|
264
|
+
/* @__PURE__ */ v("div", {
|
|
265
|
+
className: "mt-auto flex items-center gap-2 pt-3",
|
|
266
|
+
children: [/* @__PURE__ */ v("button", {
|
|
267
|
+
type: "button",
|
|
268
|
+
onClick: () => w(`catalog/titles/${e.titleId}`),
|
|
269
|
+
className: "inline-flex flex-1 items-center justify-center gap-1 rounded-lg bg-action-primary-bg px-3 py-1.5 text-xs font-medium text-action-primary-text hover:opacity-90 transition-opacity",
|
|
270
|
+
children: [/* @__PURE__ */ _(f, { className: "h-3.5 w-3.5" }), "Watch"]
|
|
271
|
+
}), e.status === "ACTIVE" && /* @__PURE__ */ v("button", {
|
|
272
|
+
type: "button",
|
|
273
|
+
disabled: A.isPending,
|
|
274
|
+
onClick: () => void re(e.id),
|
|
275
|
+
className: "inline-flex items-center gap-1 rounded-lg border border-border-seam px-3 py-1.5 text-xs font-medium text-text-secondary hover:text-status-error-text disabled:opacity-50 transition-colors",
|
|
276
|
+
children: [/* @__PURE__ */ _(g, { className: "h-3.5 w-3.5" }), "Revoke"]
|
|
277
|
+
})]
|
|
278
|
+
})
|
|
279
|
+
]
|
|
280
|
+
}, e.id))
|
|
281
|
+
})
|
|
282
|
+
]
|
|
85
283
|
});
|
|
86
284
|
};
|
|
87
285
|
//#endregion
|
|
88
|
-
export {
|
|
286
|
+
export { w as EntitlementsPage };
|
|
89
287
|
|
|
90
288
|
//# sourceMappingURL=EntitlementsPage.js.map
|