refactor(private-zone): use canonical product name
Docker Image / Build and Push Docker Image (push) Successful in 2m7s
Docker Image / Build and Push Docker Image (push) Successful in 2m7s
This commit is contained in:
@@ -0,0 +1,334 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useMemo, useRef } from "react";
|
||||
import Image from "next/image";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
|
||||
import { CharacterAvatar } from "@/app/_components";
|
||||
import {
|
||||
AppBottomNav,
|
||||
FavoriteEntryButton,
|
||||
MobileShell,
|
||||
} from "@/app/_components/core";
|
||||
import { buildGlobalPageUrl } from "@/router/global-route-context";
|
||||
import { ROUTES } from "@/router/routes";
|
||||
import { useAppNavigator } from "@/router/use-app-navigator";
|
||||
import {
|
||||
useActiveCharacter,
|
||||
useActiveCharacterRoutes,
|
||||
} from "@/providers/character-provider";
|
||||
import { isPrivateAlbumLocked } from "@/lib/private-zone/private_album";
|
||||
import { useAuthDispatch, useAuthState } from "@/stores/auth/auth-context";
|
||||
import {
|
||||
usePrivateZoneDispatch,
|
||||
usePrivateZoneState,
|
||||
} from "@/stores/private-zone";
|
||||
|
||||
import {
|
||||
PrivateAlbumCard,
|
||||
PrivateAlbumGallery,
|
||||
StatusCard,
|
||||
UnlockConfirmDialog,
|
||||
} from "./components";
|
||||
import styles from "./private-zone-screen.module.css";
|
||||
import {
|
||||
isPrivateZoneAuthRequired,
|
||||
usePrivateZoneBootstrapFlow,
|
||||
usePrivateZoneUnlockPaywallNavigation,
|
||||
usePrivateZoneUnlockSuccessRefresh,
|
||||
} from "./use-private-zone-flow";
|
||||
import {
|
||||
buildPrivateAlbumGalleryUrl,
|
||||
buildPrivateZoneWithoutGalleryUrl,
|
||||
getPrivateAlbumGalleryState,
|
||||
} from "./private-album-gallery-url";
|
||||
import { findPrivateAlbumDisplayImage } from "./private-album-images";
|
||||
|
||||
export function PrivateZoneScreen() {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const navigator = useAppNavigator();
|
||||
const character = useActiveCharacter();
|
||||
const characterRoutes = useActiveCharacterRoutes();
|
||||
const authState = useAuthState();
|
||||
const authDispatch = useAuthDispatch();
|
||||
const state = usePrivateZoneState();
|
||||
const dispatch = usePrivateZoneDispatch();
|
||||
const openedGalleryInPageRef = useRef(false);
|
||||
const galleryState = useMemo(
|
||||
() => getPrivateAlbumGalleryState(searchParams),
|
||||
[searchParams],
|
||||
);
|
||||
|
||||
usePrivateZoneBootstrapFlow({
|
||||
authDispatch,
|
||||
hasInitialized: authState.hasInitialized,
|
||||
isAuthLoading: authState.isLoading,
|
||||
loginStatus: authState.loginStatus,
|
||||
roomDispatch: dispatch,
|
||||
roomStatus: state.status,
|
||||
});
|
||||
usePrivateZoneUnlockPaywallNavigation({
|
||||
loginStatus: authState.loginStatus,
|
||||
roomDispatch: dispatch,
|
||||
unlockPaywallRequest: state.unlockPaywallRequest,
|
||||
});
|
||||
usePrivateZoneUnlockSuccessRefresh(state.unlockSuccessNonce);
|
||||
|
||||
const displayName = character.displayName;
|
||||
const avatarUrl = character.assets.avatar;
|
||||
const title = character.copy.privateZoneTitle;
|
||||
const subtitle = character.copy.privateZoneSubtitle;
|
||||
const pendingAlbum = useMemo(
|
||||
() =>
|
||||
state.items.find(
|
||||
(album) => album.albumId === state.pendingConfirmAlbumId,
|
||||
) ?? null,
|
||||
[state.items, state.pendingConfirmAlbumId],
|
||||
);
|
||||
const galleryAlbum = useMemo(
|
||||
() =>
|
||||
galleryState
|
||||
? state.items.find((album) => album.albumId === galleryState.albumId) ??
|
||||
null
|
||||
: null,
|
||||
[galleryState, state.items],
|
||||
);
|
||||
const galleryImage = useMemo(
|
||||
() =>
|
||||
galleryAlbum && galleryState
|
||||
? findPrivateAlbumDisplayImage(
|
||||
galleryAlbum,
|
||||
galleryState.imageIndex,
|
||||
)
|
||||
: null,
|
||||
[galleryAlbum, galleryState],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!galleryState || state.isLoading) return;
|
||||
if (
|
||||
!galleryAlbum ||
|
||||
isPrivateAlbumLocked(galleryAlbum) ||
|
||||
!galleryImage
|
||||
) {
|
||||
router.replace(
|
||||
buildPrivateZoneWithoutGalleryUrl(
|
||||
searchParams,
|
||||
characterRoutes.privateZone,
|
||||
),
|
||||
{ scroll: false },
|
||||
);
|
||||
}
|
||||
}, [
|
||||
characterRoutes.privateZone,
|
||||
galleryAlbum,
|
||||
galleryImage,
|
||||
galleryState,
|
||||
router,
|
||||
searchParams,
|
||||
state.isLoading,
|
||||
]);
|
||||
|
||||
const handleTopUpClick = () => {
|
||||
if (isPrivateZoneAuthRequired(authState.loginStatus)) {
|
||||
navigator.openAuth(characterRoutes.privateZone);
|
||||
return;
|
||||
}
|
||||
navigator.openSubscription({
|
||||
type: "topup",
|
||||
returnTo: "private-zone",
|
||||
analytics: {
|
||||
entryPoint: "private_zone",
|
||||
triggerReason: "vip_cta",
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleOpenGallery = (albumId: string, imageIndex: number) => {
|
||||
openedGalleryInPageRef.current = true;
|
||||
router.push(
|
||||
buildPrivateAlbumGalleryUrl(
|
||||
albumId,
|
||||
imageIndex,
|
||||
characterRoutes.privateZone,
|
||||
),
|
||||
{ scroll: false },
|
||||
);
|
||||
};
|
||||
|
||||
const handleCloseGallery = () => {
|
||||
if (openedGalleryInPageRef.current) {
|
||||
openedGalleryInPageRef.current = false;
|
||||
router.back();
|
||||
return;
|
||||
}
|
||||
router.replace(
|
||||
buildPrivateZoneWithoutGalleryUrl(
|
||||
searchParams,
|
||||
characterRoutes.privateZone,
|
||||
),
|
||||
{ scroll: false },
|
||||
);
|
||||
};
|
||||
|
||||
const handleGalleryIndexChange = (imageIndex: number) => {
|
||||
if (!galleryAlbum) return;
|
||||
router.replace(
|
||||
buildPrivateAlbumGalleryUrl(
|
||||
galleryAlbum.albumId,
|
||||
imageIndex,
|
||||
characterRoutes.privateZone,
|
||||
),
|
||||
{ scroll: false },
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<MobileShell background="#f3f4f2">
|
||||
<main className={styles.shell}>
|
||||
<div className={styles.backgroundGlowOne} />
|
||||
<div className={styles.backgroundGlowTwo} />
|
||||
<div className={styles.favoriteAction}>
|
||||
<FavoriteEntryButton
|
||||
characterSlug={character.slug}
|
||||
tone="dark"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<section className={styles.hero} aria-label={title}>
|
||||
<div className={styles.heroCover}>
|
||||
<Image
|
||||
src={character.assets.privateZoneBanner}
|
||||
alt=""
|
||||
width={2048}
|
||||
height={1152}
|
||||
className={styles.heroBanner}
|
||||
priority
|
||||
sizes="(max-width: 540px) 100vw, 540px"
|
||||
/>
|
||||
|
||||
<div className={styles.identity}>
|
||||
<div className={styles.avatarFrame}>
|
||||
<CharacterAvatar
|
||||
src={avatarUrl}
|
||||
alt={displayName}
|
||||
size="100%"
|
||||
imageSize={32}
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
|
||||
<p className={styles.identityName}>{displayName}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
data-analytics-key="private_zone.primary_cta"
|
||||
data-analytics-label="Open private zone top up"
|
||||
className={styles.primaryCta}
|
||||
onClick={handleTopUpClick}
|
||||
>
|
||||
<span>{subtitle}</span>
|
||||
</button>
|
||||
</section>
|
||||
|
||||
<section className={styles.postsSection} aria-labelledby="posts-title">
|
||||
<div className={styles.sectionHeader}>
|
||||
<div>
|
||||
<p className={styles.kicker}>Albums</p>
|
||||
<h2 id="posts-title" className={styles.sectionTitle}>
|
||||
Private albums
|
||||
</h2>
|
||||
</div>
|
||||
<span className={styles.postCount}>{state.items.length}</span>
|
||||
</div>
|
||||
|
||||
{state.errorMessage ? (
|
||||
<StatusCard
|
||||
message={state.errorMessage}
|
||||
actionLabel="Retry"
|
||||
onAction={() => dispatch({ type: "PrivateZoneRefresh" })}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{state.isLoading && state.items.length === 0 ? (
|
||||
<StatusCard message="Loading private albums..." />
|
||||
) : null}
|
||||
|
||||
{!state.isLoading && state.items.length === 0 && !state.errorMessage ? (
|
||||
<StatusCard
|
||||
message="No private albums yet."
|
||||
actionLabel="Refresh"
|
||||
onAction={() => dispatch({ type: "PrivateZoneRefresh" })}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<div className={styles.timeline}>
|
||||
{state.items.map((album, index) => (
|
||||
<PrivateAlbumCard
|
||||
key={album.albumId}
|
||||
album={album}
|
||||
displayName={displayName}
|
||||
avatarUrl={avatarUrl}
|
||||
index={index}
|
||||
isUnlocking={state.unlockingAlbumId === album.albumId}
|
||||
onOpenGallery={(imageIndex) =>
|
||||
handleOpenGallery(album.albumId, imageIndex)
|
||||
}
|
||||
onUnlock={() =>
|
||||
dispatch({
|
||||
type: "PrivateZoneUnlockRequested",
|
||||
albumId: album.albumId,
|
||||
})
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<AppBottomNav
|
||||
activeItem="privateZone"
|
||||
privateZoneLabel={character.copy.privateZoneTitle}
|
||||
onChatClick={() =>
|
||||
navigator.push(characterRoutes.splash, { scroll: false })
|
||||
}
|
||||
onPrivateZoneClick={() => undefined}
|
||||
onMenuClick={() =>
|
||||
navigator.push(
|
||||
buildGlobalPageUrl(ROUTES.profile, characterRoutes.chat),
|
||||
{ scroll: false },
|
||||
)
|
||||
}
|
||||
/>
|
||||
|
||||
{pendingAlbum ? (
|
||||
<UnlockConfirmDialog
|
||||
album={pendingAlbum}
|
||||
isUnlocking={state.isUnlocking}
|
||||
errorMessage={state.unlockErrorMessage}
|
||||
onCancel={() => dispatch({ type: "PrivateZoneUnlockCancelled" })}
|
||||
onConfirm={() => dispatch({ type: "PrivateZoneUnlockConfirmed" })}
|
||||
/>
|
||||
) : state.unlockErrorMessage ? (
|
||||
<div className={styles.toast} role="status">
|
||||
{state.unlockErrorMessage}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{galleryAlbum &&
|
||||
galleryState &&
|
||||
!isPrivateAlbumLocked(galleryAlbum) &&
|
||||
galleryImage ? (
|
||||
<PrivateAlbumGallery
|
||||
album={galleryAlbum}
|
||||
imageIndex={galleryState.imageIndex}
|
||||
onClose={handleCloseGallery}
|
||||
onImageIndexChange={handleGalleryIndexChange}
|
||||
/>
|
||||
) : null}
|
||||
</main>
|
||||
</MobileShell>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user