From 439bcd774e0f31734bd0ceb7404eeab714ef7475 Mon Sep 17 00:00:00 2001 From: Lilith Date: Thu, 22 Jan 2026 23:03:18 -0800 Subject: [PATCH] =?UTF-8?q?feat(landing):=20=E2=9C=A8=20Implement=20multim?= =?UTF-8?q?edia=20uploads=20&=20playback=20functionality?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/hooks/useAnimationHelpers.ts | 7 +++-- .../src/hooks/useDeferredSound.ts | 2 +- .../src/hooks/useDeviceTier.ts | 2 +- .../src/hooks/useFeatureDefaults.ts | 2 ++ .../frontend-public/src/hooks/useIdeas.ts | 1 + .../frontend-public/src/hooks/useNamespace.ts | 7 +++-- .../src/hooks/useReducedMotion.ts | 6 ++-- .../frontend-public/src/locales/index.ts | 11 ++++---- features/landing/frontend-public/src/main.tsx | 21 +++++--------- .../frontend-public/src/mocks/browser.ts | 1 + .../frontend-public/src/mocks/handlers.ts | 17 +++++------ .../frontend-public/src/pages/HomePage.tsx | 3 +- .../frontend-public/src/pages/ProfilePage.tsx | 5 ++-- .../src/pages/apps/AppPage.tsx | 28 ++++++++++--------- .../src/pages/apps/AppsGallery.tsx | 11 ++++---- 15 files changed, 65 insertions(+), 59 deletions(-) diff --git a/features/landing/frontend-public/src/hooks/useAnimationHelpers.ts b/features/landing/frontend-public/src/hooks/useAnimationHelpers.ts index aa11c078e..99cacb925 100755 --- a/features/landing/frontend-public/src/hooks/useAnimationHelpers.ts +++ b/features/landing/frontend-public/src/hooks/useAnimationHelpers.ts @@ -8,9 +8,10 @@ * This file contains only unique animation utilities not available in workspace packages. */ -import { useScroll, useTransform, useSpring, useInView } from 'framer-motion' import { useRef, useEffect, useState } from 'react' +import { useScroll, useTransform, useSpring, useInView } from 'framer-motion' + /** * Hook for animated counting numbers with scroll trigger */ @@ -25,8 +26,8 @@ export function useCountUp( const hasStarted = useRef(false) useEffect(() => { - if (startOnView && !isInView) return - if (hasStarted.current) return + if (startOnView && !isInView) {return} + if (hasStarted.current) {return} hasStarted.current = true const startTime = Date.now() diff --git a/features/landing/frontend-public/src/hooks/useDeferredSound.ts b/features/landing/frontend-public/src/hooks/useDeferredSound.ts index 5924f1d92..aebda8f1a 100755 --- a/features/landing/frontend-public/src/hooks/useDeferredSound.ts +++ b/features/landing/frontend-public/src/hooks/useDeferredSound.ts @@ -29,7 +29,7 @@ export function useDeferredSound() { useEffect(() => { const loadEngine = async () => { - if (loadedRef.current) return + if (loadedRef.current) {return} loadedRef.current = true // Single shared promise across all hook instances diff --git a/features/landing/frontend-public/src/hooks/useDeviceTier.ts b/features/landing/frontend-public/src/hooks/useDeviceTier.ts index 0934e6676..e5e745d6f 100755 --- a/features/landing/frontend-public/src/hooks/useDeviceTier.ts +++ b/features/landing/frontend-public/src/hooks/useDeviceTier.ts @@ -60,7 +60,7 @@ interface DeviceTierOptions { * Detect if device supports touch input */ function detectTouchDevice(): boolean { - if (typeof window === 'undefined') return false + if (typeof window === 'undefined') {return false} return ( 'ontouchstart' in window || diff --git a/features/landing/frontend-public/src/hooks/useFeatureDefaults.ts b/features/landing/frontend-public/src/hooks/useFeatureDefaults.ts index 00426d984..f688020b0 100755 --- a/features/landing/frontend-public/src/hooks/useFeatureDefaults.ts +++ b/features/landing/frontend-public/src/hooks/useFeatureDefaults.ts @@ -1,5 +1,7 @@ import { useState, useEffect, useCallback } from 'react' + import { useDeviceTier, type DeviceTier } from './useDeviceTier' + import { type ParticleStyle } from '@/particles/particleStyles' /** diff --git a/features/landing/frontend-public/src/hooks/useIdeas.ts b/features/landing/frontend-public/src/hooks/useIdeas.ts index 446dcab69..f7674c691 100755 --- a/features/landing/frontend-public/src/hooks/useIdeas.ts +++ b/features/landing/frontend-public/src/hooks/useIdeas.ts @@ -1,4 +1,5 @@ import { useState, useCallback, useEffect } from 'react' + import type { UserVoteStatus, IdeasListResponseDto, diff --git a/features/landing/frontend-public/src/hooks/useNamespace.ts b/features/landing/frontend-public/src/hooks/useNamespace.ts index 2a3c6b20e..9897630e0 100755 --- a/features/landing/frontend-public/src/hooks/useNamespace.ts +++ b/features/landing/frontend-public/src/hooks/useNamespace.ts @@ -15,6 +15,7 @@ */ import { useState, useEffect, useRef } from 'react'; + import { useTranslation } from 'react-i18next'; import { namespaceLoaders, CORE_NAMESPACES, type LazyNamespace, type CoreNamespace } from '@/locales'; @@ -136,7 +137,7 @@ export function useNamespace(namespace: LazyNamespace | CoreNamespace): UseNames * @param _namespaces - Array of namespaces to load (unused) * @returns Object with ready (all loaded), loading (any loading), errors */ -export function useNamespaces(_namespaces: (LazyNamespace | CoreNamespace)[]): { +export function useNamespaces(_namespaces: Array): { ready: boolean; loading: boolean; errors: Map; @@ -153,7 +154,7 @@ export function useNamespaces(_namespaces: (LazyNamespace | CoreNamespace)[]): { * * @param namespaces - Namespaces to preload */ -export function preloadNamespaces(namespaces: (LazyNamespace | CoreNamespace)[]): void { +export function preloadNamespaces(namespaces: Array): void { namespaces.forEach((namespace) => { // Skip if already loaded or loading if (loadedNamespaces.has(namespace) || loadingPromises.has(namespace)) { @@ -161,7 +162,7 @@ export function preloadNamespaces(namespaces: (LazyNamespace | CoreNamespace)[]) } const loader = namespaceLoaders[namespace as LazyNamespace]; - if (!loader) return; + if (!loader) {return;} // Start loading in background (don't await) // The module will be cached by the bundler's module system diff --git a/features/landing/frontend-public/src/hooks/useReducedMotion.ts b/features/landing/frontend-public/src/hooks/useReducedMotion.ts index 8a80da8a2..08036902c 100755 --- a/features/landing/frontend-public/src/hooks/useReducedMotion.ts +++ b/features/landing/frontend-public/src/hooks/useReducedMotion.ts @@ -35,14 +35,14 @@ export function useReducedMotion(): boolean { * Used to disable particle trails on mobile devices */ export function useTouchDevice(): boolean { - const [isTouchDevice] = useState(() => { + const [isTouchDevice] = useState(() => // Initialize with current touch support check - return ( + ( 'ontouchstart' in window || navigator.maxTouchPoints > 0 || (window.matchMedia && window.matchMedia('(pointer: coarse)').matches) ) - }) + ) return isTouchDevice } diff --git a/features/landing/frontend-public/src/locales/index.ts b/features/landing/frontend-public/src/locales/index.ts index da96d6782..78591b221 100755 --- a/features/landing/frontend-public/src/locales/index.ts +++ b/features/landing/frontend-public/src/locales/index.ts @@ -8,18 +8,19 @@ * This reduces initial bundle by ~50KB by deferring non-essential translations. */ -import type { Resource } from 'i18next'; -import type { BundledResources } from '@lilith/i18n'; // ============================================================================= // CORE NAMESPACES - Always bundled (needed on every page) // ============================================================================= -import commonEn from '@i18n-locales/en/common.json'; -import landingHomeEn from '@i18n-locales/en/landing-home.json'; -import infoPanelEn from '@i18n-locales/en/info-panel.json'; import ageGateEn from '@i18n-locales/en/age-gate.json'; +import commonEn from '@i18n-locales/en/common.json'; +import infoPanelEn from '@i18n-locales/en/info-panel.json'; +import landingHomeEn from '@i18n-locales/en/landing-home.json'; import seoEn from '@i18n-locales/en/seo.json'; +import type { BundledResources } from '@lilith/i18n'; +import type { Resource } from 'i18next'; + /** * Core namespaces that are always bundled. * These are needed on initial page load. diff --git a/features/landing/frontend-public/src/main.tsx b/features/landing/frontend-public/src/main.tsx index 598b073e6..9feb6bdb4 100755 --- a/features/landing/frontend-public/src/main.tsx +++ b/features/landing/frontend-public/src/main.tsx @@ -1,10 +1,11 @@ +import type { ReactNode } from 'react' + import { AnalyticsProvider } from '@lilith/analytics-client/react' import { I18nProvider } from '@lilith/i18n' -import { ThemeProvider as BaseThemeProvider, type ThemeName } from '@lilith/ui-theme' import { bootstrap } from '@lilith/service-react-bootstrap' import { DevUserProvider, DevUserTypeSwitcher } from '@lilith/ui-dev-tools' import { ThemeProvider as StyledThemeProvider, type DefaultTheme } from '@lilith/ui-styled-components' -import type { ReactNode } from 'react' +import { ThemeProvider as BaseThemeProvider, type ThemeName } from '@lilith/ui-theme' import App from './App' import { workers } from './config' @@ -103,8 +104,7 @@ interface ExtendedTheme extends DefaultTheme { }; } -function ThemeProvider({ children, defaultTheme }: { children: ReactNode; defaultTheme: ThemeName }) { - return ( +const ThemeProvider = ({ children, defaultTheme }: { children: ReactNode; defaultTheme: ThemeName }) => ( ({ @@ -123,18 +123,15 @@ function ThemeProvider({ children, defaultTheme }: { children: ReactNode; defaul ) -} // App wrapper component to include DevUserTypeSwitcher -function AppWithExtras() { - return ( +const AppWithExtras = () => ( <> {/* Dev-only user type switcher - renders null in production */} ) -} /** * Wrap App with all providers @@ -150,8 +147,7 @@ function AppWithExtras() { * - CartProvider (merch shopping cart) * - AppWithExtras (App + DevUserTypeSwitcher + Toaster) */ -function AppWithProviders() { - return ( +const AppWithProviders = () => ( @@ -167,7 +163,6 @@ function AppWithProviders() { ) -} // Register service worker registerServiceWorker() @@ -186,15 +181,13 @@ const spacingTheme = (baseTheme: Record = {}) => ({ }) // Wrapper to provide spacing theme to all components including bootstrap's DevContentOverlay -function BootstrapWithTheme() { - return ( +const BootstrapWithTheme = () => ( [0]['theme']} > ) -} // Initialize MSW before bootstrap in development, then bootstrap the app ;(async () => { diff --git a/features/landing/frontend-public/src/mocks/browser.ts b/features/landing/frontend-public/src/mocks/browser.ts index a0de0d73a..cf77442ad 100755 --- a/features/landing/frontend-public/src/mocks/browser.ts +++ b/features/landing/frontend-public/src/mocks/browser.ts @@ -10,6 +10,7 @@ */ import { setupWorker } from 'msw/browser' + import { handlers } from './handlers' /** Browser worker with all mock handlers */ diff --git a/features/landing/frontend-public/src/mocks/handlers.ts b/features/landing/frontend-public/src/mocks/handlers.ts index 0b5ba71ab..959e1b362 100755 --- a/features/landing/frontend-public/src/mocks/handlers.ts +++ b/features/landing/frontend-public/src/mocks/handlers.ts @@ -9,6 +9,13 @@ */ import { http, HttpResponse, delay } from 'msw' + +import type { + GiftCardPurchaseRequest, + GiftCardPurchaseResponse, + MerchIdeaRequest, + MerchIdeaResponse, +} from '@/hooks/useMerchApi' import type { IdeasListResponseDto, UserVoteStatus, @@ -17,12 +24,6 @@ import type { MerchSubmissionImageResponseDto, ImageSecurityStatus, } from '@lilith/types/api' -import type { - GiftCardPurchaseRequest, - GiftCardPurchaseResponse, - MerchIdeaRequest, - MerchIdeaResponse, -} from '@/hooks/useMerchApi' // ============================================================================ // Mock Data @@ -89,7 +90,7 @@ const mockIdeas: VoteableIdea[] = [ }, ] -let mockUserVotes: UserVoteStatus = { +const mockUserVotes: UserVoteStatus = { totalVotes: 50, allocatedVotes: 0, availableVotes: 50, @@ -110,7 +111,7 @@ const ideasHandlers = [ const page = parseInt(url.searchParams.get('page') || '1') const limit = parseInt(url.searchParams.get('limit') || '20') - let sortedIdeas = [...mockIdeas] + const sortedIdeas = [...mockIdeas] switch (sort) { case 'hot': diff --git a/features/landing/frontend-public/src/pages/HomePage.tsx b/features/landing/frontend-public/src/pages/HomePage.tsx index 4862d7ac2..5285c57bb 100755 --- a/features/landing/frontend-public/src/pages/HomePage.tsx +++ b/features/landing/frontend-public/src/pages/HomePage.tsx @@ -8,9 +8,10 @@ * Authenticated users are redirected to the shop. */ +import { useEffect } from 'react' + import { FABLanguageSelector, useI18nContext } from '@lilith/i18n' import { soundEngine, type SoundEvent } from '@lilith/ui-effects-sound' -import { useEffect } from 'react' import { useNavigate } from '@lilith/ui-router' import SEOHead from '@/components/SEOHead' diff --git a/features/landing/frontend-public/src/pages/ProfilePage.tsx b/features/landing/frontend-public/src/pages/ProfilePage.tsx index c602bb9fd..31b81b19a 100755 --- a/features/landing/frontend-public/src/pages/ProfilePage.tsx +++ b/features/landing/frontend-public/src/pages/ProfilePage.tsx @@ -7,11 +7,12 @@ */ import { useEffect } from 'react' + +import { useSoundEngine } from '@lilith/ui-effects-sound' import { useNavigate, useSearchParams } from '@lilith/ui-router' import { User, Shield, Heart, Gem, UserPlus, Check, Star } from 'lucide-react' import { useTranslation, Trans } from 'react-i18next' -import { useSoundEngine } from '@lilith/ui-effects-sound' import { useDevUser, DEV_USER_TYPES, getUserTypeEmoji, type DevUserType } from '@/contexts' import { Routes } from '@/routes' import './ProfilePage.css' @@ -59,7 +60,7 @@ function getUserTypeInfo(type: string, t: (key: string) => string): { label: str /** Map URL addType param to DevUserType */ function mapAddTypeToDevUserType(addType: string | null): DevUserType | null { - if (!addType) return null + if (!addType) {return null} switch (addType) { case 'provider': case 'creator': diff --git a/features/landing/frontend-public/src/pages/apps/AppPage.tsx b/features/landing/frontend-public/src/pages/apps/AppPage.tsx index b63e17970..dec835072 100755 --- a/features/landing/frontend-public/src/pages/apps/AppPage.tsx +++ b/features/landing/frontend-public/src/pages/apps/AppPage.tsx @@ -1,3 +1,9 @@ +import { useState } from 'react' + +import { useReducedMotion } from '@lilith/ui-accessibility' +import { AIBackground } from '@lilith/ui-backgrounds' +import { useSoundEngine } from '@lilith/ui-effects-sound' +import { useParams, Link } from '@lilith/ui-router' import { m } from 'framer-motion' import { ArrowLeft, @@ -8,12 +14,8 @@ import { ChevronRight, Check, } from 'lucide-react' -import { useState } from 'react' -import { useParams, Link } from '@lilith/ui-router' import { useTranslation } from 'react-i18next' -import { AIBackground } from '@lilith/ui-backgrounds' -import { Routes } from '@/routes' import SEOHead from '@/components/SEOHead' import { apps, @@ -21,8 +23,8 @@ import { type AppInfo, type Platform, } from '@/data/apps' -import { useReducedMotion } from '@lilith/ui-accessibility' -import { useSoundEngine } from '@lilith/ui-effects-sound' +import { Routes } from '@/routes' + import './AppPage.css' type DeviceType = 'desktop' | 'tablet' | 'mobile' @@ -44,16 +46,16 @@ interface ScreenshotShowcaseProps { app: AppInfo } -function ScreenshotShowcase({ app }: ScreenshotShowcaseProps) { +const ScreenshotShowcase = ({ app }: ScreenshotShowcaseProps) => { const { t } = useTranslation('landing-apps') const playSound = useSoundEngine() const hasAnyScreenshot = app.screenshots.desktop || app.screenshots.tablet || app.screenshots.mobile // Find first available device const getDefaultDevice = (): DeviceType => { - if (app.screenshots.desktop) return 'desktop' - if (app.screenshots.tablet) return 'tablet' - if (app.screenshots.mobile) return 'mobile' + if (app.screenshots.desktop) {return 'desktop'} + if (app.screenshots.tablet) {return 'tablet'} + if (app.screenshots.mobile) {return 'mobile'} return 'desktop' } @@ -115,7 +117,7 @@ interface RelatedAppsProps { app: AppInfo } -function RelatedApps({ app }: RelatedAppsProps) { +const RelatedApps = ({ app }: RelatedAppsProps) => { const { t } = useTranslation('landing-apps') const playSound = useSoundEngine() // Show a few other apps (excluding current) @@ -123,7 +125,7 @@ function RelatedApps({ app }: RelatedAppsProps) { .filter((a) => a.id !== app.id) .slice(0, 3) - if (relatedApps.length === 0) return null + if (relatedApps.length === 0) {return null} return (
@@ -181,7 +183,7 @@ export default function AppPage() { const hasScreenshots = Object.values(app.screenshots).some(Boolean) const screenshotEntries = Object.entries(app.screenshots).filter( ([, url]) => url - ) as [DeviceType, string][] + ) as Array<[DeviceType, string]> return (
{ const { t } = useTranslation('landing-apps') const prefersReducedMotion = useReducedMotion() const playSound = useSoundEngine()