feat(landing): ✨ Implement multimedia uploads & playback functionality
This commit is contained in:
parent
e16e19ea4e
commit
439bcd774e
15 changed files with 65 additions and 59 deletions
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 ||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import { useState, useEffect, useCallback } from 'react'
|
||||
|
||||
import { useDeviceTier, type DeviceTier } from './useDeviceTier'
|
||||
|
||||
import { type ParticleStyle } from '@/particles/particleStyles'
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { useState, useCallback, useEffect } from 'react'
|
||||
|
||||
import type {
|
||||
UserVoteStatus,
|
||||
IdeasListResponseDto,
|
||||
|
|
|
|||
|
|
@ -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<LazyNamespace | CoreNamespace>): {
|
||||
ready: boolean;
|
||||
loading: boolean;
|
||||
errors: Map<string, Error>;
|
||||
|
|
@ -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<LazyNamespace | CoreNamespace>): 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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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 }) => (
|
||||
<BaseThemeProvider defaultTheme={defaultTheme}>
|
||||
<StyledThemeProvider
|
||||
theme={(baseTheme?: DefaultTheme): ExtendedTheme => ({
|
||||
|
|
@ -123,18 +123,15 @@ function ThemeProvider({ children, defaultTheme }: { children: ReactNode; defaul
|
|||
</StyledThemeProvider>
|
||||
</BaseThemeProvider>
|
||||
)
|
||||
}
|
||||
|
||||
// App wrapper component to include DevUserTypeSwitcher
|
||||
function AppWithExtras() {
|
||||
return (
|
||||
const AppWithExtras = () => (
|
||||
<>
|
||||
<App />
|
||||
{/* Dev-only user type switcher - renders null in production */}
|
||||
<DevUserTypeSwitcher />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap App with all providers
|
||||
|
|
@ -150,8 +147,7 @@ function AppWithExtras() {
|
|||
* - CartProvider (merch shopping cart)
|
||||
* - AppWithExtras (App + DevUserTypeSwitcher + Toaster)
|
||||
*/
|
||||
function AppWithProviders() {
|
||||
return (
|
||||
const AppWithProviders = () => (
|
||||
<AnalyticsProvider config={analyticsConfig}>
|
||||
<I18nProvider config={i18nConfig}>
|
||||
<ThemeProvider defaultTheme="cyberpunk">
|
||||
|
|
@ -167,7 +163,6 @@ function AppWithProviders() {
|
|||
</I18nProvider>
|
||||
</AnalyticsProvider>
|
||||
)
|
||||
}
|
||||
|
||||
// Register service worker
|
||||
registerServiceWorker()
|
||||
|
|
@ -186,15 +181,13 @@ const spacingTheme = (baseTheme: Record<string, unknown> = {}) => ({
|
|||
})
|
||||
|
||||
// Wrapper to provide spacing theme to all components including bootstrap's DevContentOverlay
|
||||
function BootstrapWithTheme() {
|
||||
return (
|
||||
const BootstrapWithTheme = () => (
|
||||
<StyledThemeProvider
|
||||
theme={spacingTheme as Parameters<typeof StyledThemeProvider>[0]['theme']}
|
||||
>
|
||||
<AppWithProviders />
|
||||
</StyledThemeProvider>
|
||||
)
|
||||
}
|
||||
|
||||
// Initialize MSW before bootstrap in development, then bootstrap the app
|
||||
;(async () => {
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
*/
|
||||
|
||||
import { setupWorker } from 'msw/browser'
|
||||
|
||||
import { handlers } from './handlers'
|
||||
|
||||
/** Browser worker with all mock handlers */
|
||||
|
|
|
|||
|
|
@ -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':
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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':
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<section className="app-related">
|
||||
|
|
@ -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 (
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -1,17 +1,18 @@
|
|||
import { useReducedMotion } from '@lilith/ui-accessibility'
|
||||
import { useSoundEngine } from '@lilith/ui-effects-sound'
|
||||
import { Link } from '@lilith/ui-router'
|
||||
import { m } from 'framer-motion'
|
||||
import { Monitor, Smartphone, Server, ArrowLeft } from 'lucide-react'
|
||||
import { Link } from '@lilith/ui-router'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { Routes } from '@/routes'
|
||||
import SEOHead from '@/components/SEOHead'
|
||||
import {
|
||||
apps,
|
||||
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 './AppsGallery.css'
|
||||
|
||||
// Note: useReducedMotion and useSoundEngine are used by AppCard component
|
||||
|
|
@ -28,7 +29,7 @@ interface AppCardProps {
|
|||
index: number
|
||||
}
|
||||
|
||||
function AppCard({ app, index }: AppCardProps) {
|
||||
const AppCard = ({ app, index }: AppCardProps) => {
|
||||
const { t } = useTranslation('landing-apps')
|
||||
const prefersReducedMotion = useReducedMotion()
|
||||
const playSound = useSoundEngine()
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue