feat(marketplace/frontend-public): Add React hooks for profile auto-selection, deployment configurations, and i18n support for niche categories

This commit is contained in:
Lilith 2026-01-22 23:03:39 -08:00
parent 34cc8f7a81
commit 8752ba2c39
15 changed files with 89 additions and 73 deletions

View file

@ -12,7 +12,9 @@
*/
import { useEffect, useRef } from 'react';
import { useParams, useLocation } from '@lilith/ui-router';
import { useActiveProfile } from '@/contexts/ActiveProfileContext';
import { useCooperativeMembers } from '@/features/coop/hooks';
@ -28,7 +30,7 @@ interface RouteParams {
interface AutoSelectContext {
profiles: ReturnType<typeof useActiveProfile>['profiles'];
coopMembers?: { profileId: string }[];
coopMembers?: Array<{ profileId: string }>;
}
// ============================================
@ -68,16 +70,16 @@ export function useAutoSelectProfile(options: UseAutoSelectProfileOptions = {})
useEffect(() => {
// Skip if disabled or profiles still loading
if (!enabled || profilesLoading) return;
if (!enabled || profilesLoading) {return;}
// Skip if no profiles
if (profiles.length === 0) return;
if (profiles.length === 0) {return;}
// Create route key to track if we've already auto-selected
const routeKey = `${location.pathname}:${coopId || ''}`;
// Skip if we've already auto-selected for this exact route
if (lastAutoSelectRoute.current === routeKey) return;
if (lastAutoSelectRoute.current === routeKey) {return;}
// Auto-select based on route
const newProfileId = determineProfileForRoute(location.pathname, params, {
@ -107,12 +109,10 @@ export function useAutoSelectProfile(options: UseAutoSelectProfileOptions = {})
]);
// Reset tracking when navigating away
useEffect(() => {
return () => {
useEffect(() => () => {
lastAutoSelectRoute.current = null;
lastAutoSelectProfileId.current = null;
};
}, []);
}, []);
}
// ============================================
@ -199,7 +199,7 @@ export function useAutoSelectEditProfile() {
const { profiles, setActiveProfile, activeProfileId, profilesLoading } = useActiveProfile();
useEffect(() => {
if (profilesLoading || !params.slug) return;
if (profilesLoading || !params.slug) {return;}
const profile = profiles.find((p) => p.slug === params.slug);
if (profile && profile.id !== activeProfileId) {

View file

@ -1,6 +1,9 @@
import { useMemo } from 'react';
import type { DeploymentConfig, VerticalConfig } from '@lilith/types';
import { getVerticalConfigByDomain, VERTICAL_CONFIGS } from '@lilith/marketplace-shared';
import type { DeploymentConfig, VerticalConfig } from '@lilith/types';
import { getCurrentDeployment } from '@/deployments';
declare global {

View file

@ -15,8 +15,10 @@
*/
import { useEffect, useCallback } from 'react';
import { useLocation } from '@lilith/ui-router';
import { useAuth } from '@lilith/auth-provider';
import { useLocation } from '@lilith/ui-router';
import { useAudience } from '@/contexts/AudienceContext';
type FunnelEvent =

View file

@ -23,9 +23,11 @@
*/
import { useMemo } from 'react';
import { getPluginNavItems } from '@/plugins/registry';
import type { NavItem } from '@/plugins/types';
import type { DeploymentId } from '@/deployments';
import type { NavItem } from '@/plugins/types';
import { getPluginNavItems } from '@/plugins/registry';
// Type declaration for build-time constant
declare const __DEPLOYMENT__: string;

View file

@ -23,9 +23,11 @@
*/
import { useMemo } from 'react';
import type { RouteObject } from '@lilith/ui-router';
import { getPluginRoutes } from '@/plugins/registry';
import type { DeploymentId } from '@/deployments';
import type { RouteObject } from '@lilith/ui-router';
import { getPluginRoutes } from '@/plugins/registry';
// Build-time deployment constant (injected by Vite)
declare const __DEPLOYMENT__: string;

View file

@ -12,6 +12,7 @@
*/
import { useCallback, useRef, useEffect } from 'react'
import { useAuth } from '@lilith/auth-provider'
// ─────────────────────────────────────────────────────────────────────────────
@ -146,11 +147,11 @@ function clearAttribution(profileId: string): void {
// ─────────────────────────────────────────────────────────────────────────────
function getDeviceType(): ProfileDeviceType {
if (typeof window === 'undefined') return 'DESKTOP'
if (typeof window === 'undefined') {return 'DESKTOP'}
const width = window.innerWidth
if (width < 768) return 'MOBILE'
if (width < 1024) return 'TABLET'
if (width < 768) {return 'MOBILE'}
if (width < 1024) {return 'TABLET'}
return 'DESKTOP'
}
@ -221,7 +222,7 @@ export function useProfileTracking(
const flushDiscoveryBatch = useCallback(() => {
const batch = discoveryBatchRef.current
if (batch.length === 0) return
if (batch.length === 0) {return}
discoveryBatchRef.current = []
@ -256,8 +257,7 @@ export function useProfileTracking(
}, [user?.id])
// Cleanup on unmount
useEffect(() => {
return () => {
useEffect(() => () => {
if (batchTimeoutRef.current) {
clearTimeout(batchTimeoutRef.current)
}
@ -265,8 +265,7 @@ export function useProfileTracking(
if (discoveryBatchRef.current.length > 0) {
flushDiscoveryBatch()
}
}
}, [flushDiscoveryBatch])
}, [flushDiscoveryBatch])
const trackDiscovery = useCallback(
(params: TrackDiscoveryParams) => {
@ -359,9 +358,7 @@ export function useProfileTracking(
[user?.id]
)
const getProfileAttribution = useCallback((profileId: string) => {
return getAttribution(profileId)
}, [])
const getProfileAttribution = useCallback((profileId: string) => getAttribution(profileId), [])
const setProfileAttribution = useCallback(
(profileId: string, attribution: Omit<ProfileAttribution, 'timestamp'>) => {

View file

@ -1,16 +1,17 @@
import React, { useCallback } from 'react';
import styled, { type DefaultTheme } from '@lilith/ui-styled-components';
import { useNavigate, useLocation } from '@lilith/ui-router';
import { FABLanguageSelector, useI18nContext, type SoundEngine } from '@lilith/i18n';
import { soundEngine, type SoundEvent } from '@lilith/ui-effects-sound';
import { DeveloperFab, type DevUserContextForFAB } from '@lilith/ui-developer-fab';
import { useDevUser } from '@lilith/ui-dev-tools';
import { DeveloperFab, type DevUserContextForFAB } from '@lilith/ui-developer-fab';
import { soundEngine, type SoundEvent } from '@lilith/ui-effects-sound';
import { MultiFAB } from '@lilith/ui-fab';
import { useNavigate, useLocation } from '@lilith/ui-router';
import styled, { type DefaultTheme } from '@lilith/ui-styled-components';
import { BrowseModeStatusBar } from '@/components/BrowseModeStatusBar';
import { FloatingSettings } from '@/components/FloatingSettings';
import { MarketplaceHeader } from '@/components/MarketplaceHeader';
import { BrowseModeStatusBar } from '@/components/BrowseModeStatusBar';
const HEADER_HEIGHT = 72;
@ -42,7 +43,7 @@ const soundEngineAdapter: SoundEngine = {
play: (sound: string) => soundEngine.play(sound as SoundEvent),
};
export function MarketplaceLayout({ children }: MarketplaceLayoutProps) {
export const MarketplaceLayout = ({ children }: MarketplaceLayoutProps) => {
const { changeLanguage } = useI18nContext();
const devUser = useDevUser();
const navigate = useNavigate();
@ -68,7 +69,7 @@ export function MarketplaceLayout({ children }: MarketplaceLayoutProps) {
} else if (level === 'admin') {
// Admin = admin type only
context.userTypes.forEach((type) => {
if (type !== 'admin') context.removeType(type);
if (type !== 'admin') {context.removeType(type);}
});
if (!context.userTypes.includes('admin')) {
context.addType('admin');
@ -77,7 +78,7 @@ export function MarketplaceLayout({ children }: MarketplaceLayoutProps) {
} else if (level === 'user') {
// User = provider (default user type for marketplace)
context.userTypes.forEach((type) => {
if (type === 'admin') context.removeType(type);
if (type === 'admin') {context.removeType(type);}
});
if (!context.userTypes.includes('provider')) {
context.addType('provider');
@ -142,7 +143,7 @@ export function MarketplaceLayout({ children }: MarketplaceLayoutProps) {
{ id: 'provider', name: 'Provider' },
{ id: 'escort', name: 'Escort' },
]}
showContentEditor={true}
showContentEditor
devUserContext={devUserContext}
onAccessLevelChange={handleAccessLevelChange}
onProfileChange={handleProfileChange}

View file

@ -4,12 +4,13 @@
* Content for professional Dominants, Submissives, and kink specialists.
*/
import landingChoiceEn from './en/landing-choice.json';
import landingClientEn from './en/landing-client.json';
import landingWorkerEn from './en/landing-worker.json';
import type { Resource } from 'i18next';
// Import landing page translations
import landingWorkerEn from './en/landing-worker.json';
import landingClientEn from './en/landing-client.json';
import landingChoiceEn from './en/landing-choice.json';
/**
* Bundled translation resources for BDSM deployment

View file

@ -4,12 +4,13 @@
* Content for cam performers and content creators.
*/
import landingChoiceEn from './en/landing-choice.json';
import landingClientEn from './en/landing-client.json';
import landingWorkerEn from './en/landing-worker.json';
import type { Resource } from 'i18next';
// Import landing page translations
import landingWorkerEn from './en/landing-worker.json';
import landingClientEn from './en/landing-client.json';
import landingChoiceEn from './en/landing-choice.json';
/**
* Bundled translation resources for cam deployment

View file

@ -5,12 +5,13 @@
* All content is consolidated in the shared locales/en/ directory for unified editing.
*/
import type { Resource } from 'i18next';
// Import landing page translations from consolidated shared location
import landingWorkerEn from '../en/marketplace-landing-worker.json';
import landingClientEn from '../en/marketplace-landing-client.json';
import landingChoiceEn from '../en/marketplace-landing-choice.json';
import landingChoiceEn from '@/en/marketplace-landing-choice.json';
import landingClientEn from '@/en/marketplace-landing-client.json';
import landingWorkerEn from '@/en/marketplace-landing-worker.json';
/**
* Bundled translation resources for escorts deployment

View file

@ -5,21 +5,31 @@
* All TrustedMeet content is consolidated in locales/en/ for unified editing.
*/
import marketplaceLandingClientBdsmEn from '@i18n-locales/en/marketplace-landing-client-bdsm.json';
import marketplaceLandingClientCamEn from '@i18n-locales/en/marketplace-landing-client-cam.json';
import marketplaceLandingClientEscortsEn from '@i18n-locales/en/marketplace-landing-client-escorts.json';
import marketplaceLandingClientMassageEn from '@i18n-locales/en/marketplace-landing-client-massage.json';
import marketplaceLandingWorkerBdsmEn from '@i18n-locales/en/marketplace-landing-worker-bdsm.json';
import marketplaceLandingWorkerCamEn from '@i18n-locales/en/marketplace-landing-worker-cam.json';
import marketplaceLandingWorkerEscortsEn from '@i18n-locales/en/marketplace-landing-worker-escorts.json';
import marketplaceLandingWorkerMassageEn from '@i18n-locales/en/marketplace-landing-worker-massage.json';
import verticalEscortsEn from '@i18n-locales/en/vertical-escorts.json';
import type { Resource } from 'i18next';
// Import vertical landing page translations
import verticalEscortsEn from '@i18n-locales/en/vertical-escorts.json';
// Import audience-specific landing page translations (TrustedMeet-specific, consolidated in locales/en/)
import marketplaceLandingWorkerEn from '@/locales/en/marketplace-landing-worker.json';
import marketplaceLandingClientEn from '@/locales/en/marketplace-landing-client.json';
import marketplaceLandingChoiceEn from '@/locales/en/marketplace-landing-choice.json';
import marketplaceLandingClientEn from '@/locales/en/marketplace-landing-client.json';
import marketplaceLandingWorkerEn from '@/locales/en/marketplace-landing-worker.json';
// Import worker content pages (from root locales/ directory)
import marketplaceSubscribeClientEn from '@/locales/en/marketplace-subscribe-client.json';
import marketplaceWorkerAboutEn from '@/locales/en/marketplace-worker-about.json';
import marketplaceWorkerFeaturesEn from '@/locales/en/marketplace-worker-features.json';
import marketplaceWorkerSafetyEn from '@/locales/en/marketplace-worker-safety.json';
import marketplaceWorkerPricingEn from '@/locales/en/marketplace-worker-pricing.json';
import marketplaceWorkerSafetyEn from '@/locales/en/marketplace-worker-safety.json';
// Import client content pages
import marketplaceClientAboutEn from '@/locales/en/marketplace-client-about.json';
@ -31,19 +41,10 @@ import marketplaceAboutEn from '@/locales/en/marketplace-about.json';
import marketplaceAboutLilithEn from '@/locales/en/marketplace-about-lilith.json';
// Import subscription pages
import marketplaceSubscribeClientEn from '@/locales/en/marketplace-subscribe-client.json';
// Import vertical-specific worker landing pages
import marketplaceLandingWorkerEscortsEn from '@i18n-locales/en/marketplace-landing-worker-escorts.json';
import marketplaceLandingWorkerCamEn from '@i18n-locales/en/marketplace-landing-worker-cam.json';
import marketplaceLandingWorkerMassageEn from '@i18n-locales/en/marketplace-landing-worker-massage.json';
import marketplaceLandingWorkerBdsmEn from '@i18n-locales/en/marketplace-landing-worker-bdsm.json';
// Import vertical-specific client landing pages
import marketplaceLandingClientEscortsEn from '@i18n-locales/en/marketplace-landing-client-escorts.json';
import marketplaceLandingClientCamEn from '@i18n-locales/en/marketplace-landing-client-cam.json';
import marketplaceLandingClientMassageEn from '@i18n-locales/en/marketplace-landing-client-massage.json';
import marketplaceLandingClientBdsmEn from '@i18n-locales/en/marketplace-landing-client-bdsm.json';
/**
* Bundled translation resources in i18next format

View file

@ -4,12 +4,13 @@
* Content for massage therapists and bodyworkers.
*/
import landingChoiceEn from './en/landing-choice.json';
import landingClientEn from './en/landing-client.json';
import landingWorkerEn from './en/landing-worker.json';
import type { Resource } from 'i18next';
// Import landing page translations
import landingWorkerEn from './en/landing-worker.json';
import landingClientEn from './en/landing-client.json';
import landingChoiceEn from './en/landing-choice.json';
/**
* Bundled translation resources for massage deployment

View file

@ -1,6 +1,8 @@
import type { ComponentType, ReactNode } from 'react';
import { bootstrap } from '@lilith/service-react-bootstrap';
import { I18nProvider } from '@lilith/i18n';
import { bootstrap } from '@lilith/service-react-bootstrap';
import { App } from './app/App';
import { ErrorFallback } from './components/ErrorFallback';
import './index.css';

View file

@ -10,23 +10,24 @@
* - Centralized plugin configuration
*/
import type { DeploymentId } from '@/deployments';
import type { MarketplacePlugin } from './types';
// Import all plugin implementations
import { bookingPlugin } from './booking.plugin';
import { reviewsPlugin } from './reviews.plugin';
import { availabilityPlugin } from './availability.plugin';
import { touringSupportPlugin } from './touring.plugin';
import { bodyworkTypesPlugin } from './bodywork-types.plugin';
import { bookingPlugin } from './booking.plugin';
import { incallOutcallPlugin } from './incall-outcall.plugin';
import { kinkSpecializationsPlugin } from './kink-specializations.plugin';
import { privateShowsPlugin } from './private-shows.plugin';
import { protocolPlugin } from './protocol.plugin';
import { reviewsPlugin } from './reviews.plugin';
import { streamingPlugin } from './streaming.plugin';
import { tipsPlugin } from './tips.plugin';
import { privateShowsPlugin } from './private-shows.plugin';
import { virtualGiftsPlugin } from './virtual-gifts.plugin';
import { kinkSpecializationsPlugin } from './kink-specializations.plugin';
import { protocolPlugin } from './protocol.plugin';
import { touringSupportPlugin } from './touring.plugin';
import { tributePlugin } from './tribute.plugin';
import { incallOutcallPlugin } from './incall-outcall.plugin';
import { bodyworkTypesPlugin } from './bodywork-types.plugin';
import { virtualGiftsPlugin } from './virtual-gifts.plugin';
import type { MarketplacePlugin } from './types';
import type { DeploymentId } from '@/deployments';
/**
* Deployment Plugin Mapping

View file

@ -12,9 +12,10 @@
* - Feature flag overrides
*/
import type { RouteObject } from '@lilith/ui-router';
import type { ComponentType, ReactNode } from 'react';
import type { RouteObject } from '@lilith/ui-router';
/**
* Navigation item for plugin-provided routes
*