feat(characters): use local character catalog
This commit is contained in:
@@ -3,7 +3,7 @@ import { createRoot, type Root } from "react-dom/client";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import type { LoginStatus } from "@/data/schemas/auth";
|
||||
import { ROUTES } from "@/router/routes";
|
||||
import { getCharacterRoutes } from "@/router/routes";
|
||||
import type { PrivateRoomEvent } from "@/stores/private-room";
|
||||
import type { PrivateRoomUnlockPaywallRequest } from "@/stores/private-room/private-room-state";
|
||||
|
||||
@@ -79,7 +79,9 @@ describe("Private Room paywall navigation", () => {
|
||||
|
||||
renderHarness(root, "guest", roomDispatch);
|
||||
|
||||
expect(mocks.openAuth).toHaveBeenCalledWith(ROUTES.privateRoom);
|
||||
expect(mocks.openAuth).toHaveBeenCalledWith(
|
||||
getCharacterRoutes("elio").privateRoom,
|
||||
);
|
||||
expect(mocks.openSubscription).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
import { PrivateRoomRouteProvider } from "@/providers/private-room-route-provider";
|
||||
|
||||
export default function PrivateRoomLayout({
|
||||
export default function LegacyPrivateRoomLayout({
|
||||
children,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
}) {
|
||||
return <PrivateRoomRouteProvider>{children}</PrivateRoomRouteProvider>;
|
||||
return children;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
import { PrivateRoomScreen } from "./private-room-screen";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export default function PrivateRoomPage() {
|
||||
return <PrivateRoomScreen />;
|
||||
import { DEFAULT_CHARACTER_SLUG } from "@/data/constants/character";
|
||||
import {
|
||||
appendRouteSearchParams,
|
||||
getCharacterRoutes,
|
||||
type RouteSearchParams,
|
||||
} from "@/router/routes";
|
||||
|
||||
export default async function PrivateRoomPage({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: Promise<RouteSearchParams>;
|
||||
}) {
|
||||
redirect(
|
||||
appendRouteSearchParams(
|
||||
getCharacterRoutes(DEFAULT_CHARACTER_SLUG).privateRoom,
|
||||
await searchParams,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -15,20 +15,22 @@ export function getPrivateAlbumGalleryState(input: {
|
||||
export function buildPrivateAlbumGalleryUrl(
|
||||
albumId: string,
|
||||
imageIndex: number,
|
||||
): `/private-room?${string}` {
|
||||
basePath: string = ROUTES.privateRoom,
|
||||
): string {
|
||||
const params = new URLSearchParams({
|
||||
[PRIVATE_ALBUM_QUERY_PARAM]: albumId,
|
||||
[PRIVATE_ALBUM_IMAGE_QUERY_PARAM]: String(imageIndex),
|
||||
});
|
||||
return `${ROUTES.privateRoom}?${params.toString()}` as const;
|
||||
return `${basePath}?${params.toString()}`;
|
||||
}
|
||||
|
||||
export function buildPrivateRoomWithoutGalleryUrl(input: {
|
||||
toString: () => string;
|
||||
}): string {
|
||||
export function buildPrivateRoomWithoutGalleryUrl(
|
||||
input: { toString: () => string },
|
||||
basePath: string = ROUTES.privateRoom,
|
||||
): string {
|
||||
const params = new URLSearchParams(input.toString());
|
||||
params.delete(PRIVATE_ALBUM_QUERY_PARAM);
|
||||
params.delete(PRIVATE_ALBUM_IMAGE_QUERY_PARAM);
|
||||
const query = params.toString();
|
||||
return query ? `${ROUTES.privateRoom}?${query}` : ROUTES.privateRoom;
|
||||
return query ? `${basePath}?${query}` : basePath;
|
||||
}
|
||||
|
||||
@@ -6,8 +6,11 @@ import { useRouter, useSearchParams } from "next/navigation";
|
||||
|
||||
import { CharacterAvatar } from "@/app/_components";
|
||||
import { AppBottomNav, MobileShell } from "@/app/_components/core";
|
||||
import { ROUTES } from "@/router/routes";
|
||||
import { useAppNavigator } from "@/router/use-app-navigator";
|
||||
import {
|
||||
useActiveCharacter,
|
||||
useActiveCharacterRoutes,
|
||||
} from "@/providers/character-provider";
|
||||
import { isPrivateAlbumLocked } from "@/lib/private-room/private_album";
|
||||
import { useAuthDispatch, useAuthState } from "@/stores/auth/auth-context";
|
||||
import {
|
||||
@@ -34,17 +37,12 @@ import {
|
||||
getPrivateAlbumGalleryState,
|
||||
} from "./private-album-gallery-url";
|
||||
|
||||
const FALLBACK_PROFILE = {
|
||||
name: "Elio Silvestri",
|
||||
avatar: "/images/avatar/elio.png",
|
||||
title: "Elio Private room",
|
||||
subtitle: "Join me, unlock my private room",
|
||||
} as const;
|
||||
|
||||
export function PrivateRoomScreen() {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const navigator = useAppNavigator();
|
||||
const character = useActiveCharacter();
|
||||
const characterRoutes = useActiveCharacterRoutes();
|
||||
const authState = useAuthState();
|
||||
const authDispatch = useAuthDispatch();
|
||||
const state = usePrivateRoomState();
|
||||
@@ -70,10 +68,10 @@ export function PrivateRoomScreen() {
|
||||
});
|
||||
usePrivateRoomUnlockSuccessRefresh(state.unlockSuccessNonce);
|
||||
|
||||
const displayName = FALLBACK_PROFILE.name;
|
||||
const avatarUrl = FALLBACK_PROFILE.avatar;
|
||||
const title = FALLBACK_PROFILE.title;
|
||||
const subtitle = FALLBACK_PROFILE.subtitle;
|
||||
const displayName = character.displayName;
|
||||
const avatarUrl = character.assets.avatar;
|
||||
const title = character.copy.privateRoomTitle;
|
||||
const subtitle = character.copy.privateRoomSubtitle;
|
||||
const pendingAlbum = useMemo(
|
||||
() =>
|
||||
state.items.find(
|
||||
@@ -98,15 +96,26 @@ export function PrivateRoomScreen() {
|
||||
isPrivateAlbumLocked(galleryAlbum) ||
|
||||
!image?.url
|
||||
) {
|
||||
router.replace(buildPrivateRoomWithoutGalleryUrl(searchParams), {
|
||||
scroll: false,
|
||||
});
|
||||
router.replace(
|
||||
buildPrivateRoomWithoutGalleryUrl(
|
||||
searchParams,
|
||||
characterRoutes.privateRoom,
|
||||
),
|
||||
{ scroll: false },
|
||||
);
|
||||
}
|
||||
}, [galleryAlbum, galleryState, router, searchParams, state.isLoading]);
|
||||
}, [
|
||||
characterRoutes.privateRoom,
|
||||
galleryAlbum,
|
||||
galleryState,
|
||||
router,
|
||||
searchParams,
|
||||
state.isLoading,
|
||||
]);
|
||||
|
||||
const handleTopUpClick = () => {
|
||||
if (isPrivateRoomAuthRequired(authState.loginStatus)) {
|
||||
navigator.openAuth(ROUTES.privateRoom);
|
||||
navigator.openAuth(characterRoutes.privateRoom);
|
||||
return;
|
||||
}
|
||||
navigator.openSubscription({
|
||||
@@ -121,7 +130,10 @@ export function PrivateRoomScreen() {
|
||||
|
||||
const handleOpenGallery = (albumId: string) => {
|
||||
openedGalleryInPageRef.current = true;
|
||||
router.push(buildPrivateAlbumGalleryUrl(albumId, 0), { scroll: false });
|
||||
router.push(
|
||||
buildPrivateAlbumGalleryUrl(albumId, 0, characterRoutes.privateRoom),
|
||||
{ scroll: false },
|
||||
);
|
||||
};
|
||||
|
||||
const handleCloseGallery = () => {
|
||||
@@ -130,15 +142,23 @@ export function PrivateRoomScreen() {
|
||||
router.back();
|
||||
return;
|
||||
}
|
||||
router.replace(buildPrivateRoomWithoutGalleryUrl(searchParams), {
|
||||
scroll: false,
|
||||
});
|
||||
router.replace(
|
||||
buildPrivateRoomWithoutGalleryUrl(
|
||||
searchParams,
|
||||
characterRoutes.privateRoom,
|
||||
),
|
||||
{ scroll: false },
|
||||
);
|
||||
};
|
||||
|
||||
const handleGalleryIndexChange = (imageIndex: number) => {
|
||||
if (!galleryAlbum) return;
|
||||
router.replace(
|
||||
buildPrivateAlbumGalleryUrl(galleryAlbum.albumId, imageIndex),
|
||||
buildPrivateAlbumGalleryUrl(
|
||||
galleryAlbum.albumId,
|
||||
imageIndex,
|
||||
characterRoutes.privateRoom,
|
||||
),
|
||||
{ scroll: false },
|
||||
);
|
||||
};
|
||||
@@ -152,7 +172,7 @@ export function PrivateRoomScreen() {
|
||||
<section className={styles.hero} aria-label={title}>
|
||||
<div className={styles.heroCover}>
|
||||
<Image
|
||||
src="/images/private-room/banner/elio.png"
|
||||
src={character.assets.privateRoomBanner}
|
||||
alt=""
|
||||
width={2048}
|
||||
height={1152}
|
||||
@@ -241,7 +261,10 @@ export function PrivateRoomScreen() {
|
||||
|
||||
<AppBottomNav
|
||||
activeItem="privateRoom"
|
||||
onChatClick={() => navigator.push(ROUTES.splash, { scroll: false })}
|
||||
privateRoomLabel={character.copy.privateRoomTitle}
|
||||
onChatClick={() =>
|
||||
navigator.push(characterRoutes.splash, { scroll: false })
|
||||
}
|
||||
onPrivateRoomClick={() => undefined}
|
||||
/>
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { type Dispatch, useEffect, useRef } from "react";
|
||||
import type { LoginStatus } from "@/data/schemas/auth";
|
||||
import { useGuestLoginBootstrap } from "@/hooks/use-guest-login-bootstrap";
|
||||
import { behaviorAnalytics } from "@/lib/analytics";
|
||||
import { ROUTES } from "@/router/routes";
|
||||
import { useActiveCharacterRoutes } from "@/providers/character-provider";
|
||||
import { useAppNavigator } from "@/router/use-app-navigator";
|
||||
import type { AuthEvent } from "@/stores/auth/auth-events";
|
||||
import type { PrivateRoomEvent } from "@/stores/private-room";
|
||||
@@ -78,12 +78,13 @@ export function usePrivateRoomUnlockPaywallNavigation({
|
||||
unlockPaywallRequest,
|
||||
}: UsePrivateRoomUnlockPaywallNavigationInput): void {
|
||||
const navigator = useAppNavigator();
|
||||
const characterRoutes = useActiveCharacterRoutes();
|
||||
|
||||
useEffect(() => {
|
||||
if (!unlockPaywallRequest) return;
|
||||
|
||||
if (isPrivateRoomAuthRequired(loginStatus)) {
|
||||
navigator.openAuth(ROUTES.privateRoom);
|
||||
navigator.openAuth(characterRoutes.privateRoom);
|
||||
} else {
|
||||
behaviorAnalytics.paywallShown({
|
||||
entryPoint: "private_album_unlock",
|
||||
@@ -99,7 +100,13 @@ export function usePrivateRoomUnlockPaywallNavigation({
|
||||
});
|
||||
}
|
||||
roomDispatch({ type: "PrivateRoomUnlockPaywallConsumed" });
|
||||
}, [loginStatus, navigator, roomDispatch, unlockPaywallRequest]);
|
||||
}, [
|
||||
characterRoutes.privateRoom,
|
||||
loginStatus,
|
||||
navigator,
|
||||
roomDispatch,
|
||||
unlockPaywallRequest,
|
||||
]);
|
||||
}
|
||||
|
||||
export function usePrivateRoomUnlockSuccessRefresh(
|
||||
|
||||
Reference in New Issue
Block a user