refactor(landing): migrate motion.* to m.* for LazyMotion
Run codemod to convert 40 files from motion.div to m.div components.
This enables proper deferred feature loading with LazyMotion.
- All motion.* JSX → m.* (div, button, section, header, footer, etc.)
- All imports: { motion } → { m }
- MotionProvider now uses LazyMotion with domAnimation features
Bundle impact: framer-motion vendor 338KB → 296KB (-42KB)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
5bb0e69fb7
commit
241cce2ea6
41 changed files with 437 additions and 440 deletions
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import { Grid } from '@ui/ui'
|
||||
import { motion } from 'framer-motion'
|
||||
import { m } from 'framer-motion'
|
||||
import { useRef } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
|
|
@ -30,7 +30,7 @@ export interface BenefitsSectionProps {
|
|||
pageType: string
|
||||
}
|
||||
|
||||
const Section = styled(motion.section)`
|
||||
const Section = styled(m.section)`
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
max-width: 1200px;
|
||||
|
|
@ -38,7 +38,7 @@ const Section = styled(motion.section)`
|
|||
padding: 2rem 2rem 4rem;
|
||||
`
|
||||
|
||||
const Header = styled(motion.div)`
|
||||
const Header = styled(m.div)`
|
||||
text-align: center;
|
||||
margin-bottom: 3rem;
|
||||
`
|
||||
|
|
@ -58,7 +58,7 @@ const Divider = styled.div<{ $gradientFrom: string; $gradientTo: string }>`
|
|||
border-radius: 2px;
|
||||
`
|
||||
|
||||
const Card = styled(motion.div)<{ $color: string }>`
|
||||
const Card = styled(m.div)<{ $color: string }>`
|
||||
position: relative;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
backdrop-filter: blur(12px);
|
||||
|
|
@ -120,7 +120,7 @@ const VersionBadgeWrapper = styled.div`
|
|||
z-index: 10;
|
||||
`
|
||||
|
||||
const IconWrapper = styled(motion.span)`
|
||||
const IconWrapper = styled(m.span)`
|
||||
font-size: 2.5rem;
|
||||
display: block;
|
||||
margin-bottom: 1rem;
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import { Grid } from '@ui/ui'
|
||||
import { ChevronRight } from 'lucide-react'
|
||||
import { motion } from 'framer-motion'
|
||||
import { m } from 'framer-motion'
|
||||
import { useRef } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
|
|
@ -29,7 +29,7 @@ export interface FeaturesSectionProps {
|
|||
pageType: string
|
||||
}
|
||||
|
||||
const Section = styled(motion.section)`
|
||||
const Section = styled(m.section)`
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
max-width: 1000px;
|
||||
|
|
@ -37,7 +37,7 @@ const Section = styled(motion.section)`
|
|||
padding: 2rem 2rem 4rem;
|
||||
`
|
||||
|
||||
const Header = styled(motion.div)`
|
||||
const Header = styled(m.div)`
|
||||
text-align: center;
|
||||
margin-bottom: 3rem;
|
||||
`
|
||||
|
|
@ -57,7 +57,7 @@ const Divider = styled.div<{ $color: string }>`
|
|||
border-radius: 2px;
|
||||
`
|
||||
|
||||
const Block = styled(motion.div)`
|
||||
const Block = styled(m.div)`
|
||||
position: relative;
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
backdrop-filter: blur(8px);
|
||||
|
|
@ -109,7 +109,7 @@ const List = styled.ul`
|
|||
gap: 0.75rem;
|
||||
`
|
||||
|
||||
const Item = styled(motion.li)<{ $color: string }>`
|
||||
const Item = styled(m.li)<{ $color: string }>`
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.75rem;
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
* Uses @ui/layout patterns for consistent styling.
|
||||
*/
|
||||
|
||||
import { motion } from 'framer-motion'
|
||||
import { m } from 'framer-motion'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { useReducedMotion } from '@ui/accessibility'
|
||||
|
|
@ -27,7 +27,7 @@ export interface StatCardProps {
|
|||
delay?: number
|
||||
}
|
||||
|
||||
const Card = styled(motion.div)`
|
||||
const Card = styled(m.div)`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import { Grid } from '@ui/ui'
|
||||
import { motion } from 'framer-motion'
|
||||
import { m } from 'framer-motion'
|
||||
import { useRef } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
|
|
@ -35,7 +35,7 @@ const Section = styled.section`
|
|||
padding: 2rem;
|
||||
`
|
||||
|
||||
const Container = styled(motion.div)<{ $gradientFrom: string; $gradientTo: string; $color: string }>`
|
||||
const Container = styled(m.div)<{ $gradientFrom: string; $gradientTo: string; $color: string }>`
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border-radius: 24px;
|
||||
|
|
@ -69,7 +69,7 @@ const Orbs = styled.div`
|
|||
overflow: hidden;
|
||||
`
|
||||
|
||||
const Orb = styled(motion.div)<{ $color: string; $size: number; $position: 'top-left' | 'bottom-right' }>`
|
||||
const Orb = styled(m.div)<{ $color: string; $size: number; $position: 'top-left' | 'bottom-right' }>`
|
||||
position: absolute;
|
||||
width: ${props => props.$size}px;
|
||||
height: ${props => props.$size}px;
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
* Uses dotted underline styling per accessibility conventions.
|
||||
*/
|
||||
|
||||
import { motion, AnimatePresence } from 'framer-motion'
|
||||
import { m, AnimatePresence } from 'framer-motion'
|
||||
import { useState, useRef, useEffect } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import './Acronym.css'
|
||||
|
|
@ -159,7 +159,7 @@ export default function Acronym({ children, definition, delay = 300 }: AcronymPr
|
|||
<AnimatePresence>
|
||||
{isVisible &&
|
||||
createPortal(
|
||||
<motion.div
|
||||
<m.div
|
||||
id={`acronym-tooltip-${acronymText}`}
|
||||
className="acronym-tooltip"
|
||||
role="tooltip"
|
||||
|
|
@ -175,7 +175,7 @@ export default function Acronym({ children, definition, delay = 300 }: AcronymPr
|
|||
<span className="acronym-tooltip-term">{acronymText}</span>
|
||||
<span className="acronym-tooltip-definition">{tooltipContent}</span>
|
||||
<div className="acronym-tooltip-arrow" />
|
||||
</motion.div>,
|
||||
</m.div>,
|
||||
document.body
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
* and newsletter signup. Renders as an overlay on any page.
|
||||
*/
|
||||
|
||||
import { motion, AnimatePresence } from 'framer-motion'
|
||||
import { m, AnimatePresence } from 'framer-motion'
|
||||
import { X, Check, MessageCircle, ArrowRight, Mail, Lock, User, Building2, MessageSquare } from 'lucide-react'
|
||||
import { useEffect, useRef, useState, useMemo } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
|
|
@ -206,7 +206,7 @@ function SuccessState({
|
|||
const playSound = useSoundEngine()
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
<m.div
|
||||
className="cta-success"
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
|
|
@ -249,7 +249,7 @@ function SuccessState({
|
|||
Close
|
||||
</button>
|
||||
)}
|
||||
</motion.div>
|
||||
</m.div>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -281,7 +281,7 @@ function FeatureWaitlistStep({ onComplete }: { onComplete: () => void }) {
|
|||
}
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
<m.div
|
||||
className="cta-features"
|
||||
initial={{ opacity: 0, x: 20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
|
|
@ -300,7 +300,7 @@ function FeatureWaitlistStep({ onComplete }: { onComplete: () => void }) {
|
|||
const isSelected = selectedFeatures.includes(feature.id)
|
||||
|
||||
return (
|
||||
<motion.button
|
||||
<m.button
|
||||
key={feature.id}
|
||||
type="button"
|
||||
className={`cta-feature-card ${isSelected ? 'selected' : ''}`}
|
||||
|
|
@ -321,7 +321,7 @@ function FeatureWaitlistStep({ onComplete }: { onComplete: () => void }) {
|
|||
<Check size={16} />
|
||||
</div>
|
||||
)}
|
||||
</motion.button>
|
||||
</m.button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
|
@ -345,7 +345,7 @@ function FeatureWaitlistStep({ onComplete }: { onComplete: () => void }) {
|
|||
Save Preferences ({selectedFeatures.length})
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
</m.div>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -515,14 +515,14 @@ export default function CTAModal({ context, onClose }: CTAModalProps) {
|
|||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
<motion.div
|
||||
<m.div
|
||||
className="cta-modal-overlay"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
onClick={handleOverlayClick}
|
||||
>
|
||||
<motion.div
|
||||
<m.div
|
||||
ref={modalRef}
|
||||
className="cta-modal-container"
|
||||
style={themeVars}
|
||||
|
|
@ -641,7 +641,7 @@ export default function CTAModal({ context, onClose }: CTAModalProps) {
|
|||
|
||||
{/* Final success after features */}
|
||||
{showFinalSuccess && (
|
||||
<motion.div
|
||||
<m.div
|
||||
className="cta-success"
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
|
|
@ -663,10 +663,10 @@ export default function CTAModal({ context, onClose }: CTAModalProps) {
|
|||
<MessageCircle size={20} />
|
||||
Join Our Discord Community
|
||||
</a>
|
||||
</motion.div>
|
||||
</m.div>
|
||||
)}
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</m.div>
|
||||
</m.div>
|
||||
</AnimatePresence>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { motion, AnimatePresence } from 'framer-motion'
|
||||
import { m, AnimatePresence } from 'framer-motion'
|
||||
import { X, ShoppingCart, Minus, Plus, Trash2, Sparkles, ArrowRight } from 'lucide-react'
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
|
|
@ -88,7 +88,7 @@ export default function CartDrawer() {
|
|||
{isOpen && (
|
||||
<>
|
||||
{/* Backdrop */}
|
||||
<motion.div
|
||||
<m.div
|
||||
className="cart-drawer-backdrop"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
|
|
@ -97,7 +97,7 @@ export default function CartDrawer() {
|
|||
/>
|
||||
|
||||
{/* Drawer */}
|
||||
<motion.div
|
||||
<m.div
|
||||
ref={drawerRef}
|
||||
className="cart-drawer"
|
||||
initial={{ x: '100%' }}
|
||||
|
|
@ -142,7 +142,7 @@ export default function CartDrawer() {
|
|||
) : (
|
||||
<div className="cart-items">
|
||||
{items.map((item) => (
|
||||
<motion.div
|
||||
<m.div
|
||||
key={item.cartKey}
|
||||
className="cart-item"
|
||||
initial={{ opacity: 0, x: 20 }}
|
||||
|
|
@ -214,7 +214,7 @@ export default function CartDrawer() {
|
|||
<Trash2 size={16} />
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
</m.div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -243,7 +243,7 @@ export default function CartDrawer() {
|
|||
>
|
||||
Clear Cart
|
||||
</button>
|
||||
<motion.button
|
||||
<m.button
|
||||
className="checkout-button"
|
||||
onClick={handleCheckout}
|
||||
onMouseEnter={() => playSound('button-hover')}
|
||||
|
|
@ -252,11 +252,11 @@ export default function CartDrawer() {
|
|||
>
|
||||
Checkout
|
||||
<ArrowRight size={18} />
|
||||
</motion.button>
|
||||
</m.button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
</m.div>
|
||||
</>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
* - Click-outside detection to collapse
|
||||
*/
|
||||
|
||||
import { motion, AnimatePresence } from 'framer-motion'
|
||||
import { m, AnimatePresence } from 'framer-motion'
|
||||
import { Globe, X } from 'lucide-react'
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
|
||||
|
|
@ -130,7 +130,7 @@ export default function FABLanguageSelector({ onLanguageChange }: FABLanguageSel
|
|||
{/* Backdrop blur when expanded */}
|
||||
<AnimatePresence>
|
||||
{isExpanded && (
|
||||
<motion.div
|
||||
<m.div
|
||||
className="fab-language-backdrop"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
|
|
@ -143,7 +143,7 @@ export default function FABLanguageSelector({ onLanguageChange }: FABLanguageSel
|
|||
{/* Language options (expand left when FAB clicked) */}
|
||||
<AnimatePresence>
|
||||
{isExpanded && (
|
||||
<motion.div
|
||||
<m.div
|
||||
className="fab-language-options"
|
||||
initial={{ opacity: 0, x: 20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
|
|
@ -157,7 +157,7 @@ export default function FABLanguageSelector({ onLanguageChange }: FABLanguageSel
|
|||
const isActive = currentLanguage === langCode
|
||||
|
||||
return (
|
||||
<motion.button
|
||||
<m.button
|
||||
key={langCode}
|
||||
className={`fab-language-option ${isActive ? 'active' : ''}`}
|
||||
onClick={() => handleLanguageSelect(langCode)}
|
||||
|
|
@ -178,15 +178,15 @@ export default function FABLanguageSelector({ onLanguageChange }: FABLanguageSel
|
|||
>
|
||||
<span className="flag-emoji">{getFlagEmoji(langCode)}</span>
|
||||
<span className="lang-code">{langCode.toUpperCase()}</span>
|
||||
</motion.button>
|
||||
</m.button>
|
||||
)
|
||||
})}
|
||||
</motion.div>
|
||||
</m.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Main FAB (always visible) */}
|
||||
<motion.button
|
||||
<m.button
|
||||
className="fab-language-button"
|
||||
onClick={toggleExpanded}
|
||||
whileHover={{ scale: 1.05 }}
|
||||
|
|
@ -196,7 +196,7 @@ export default function FABLanguageSelector({ onLanguageChange }: FABLanguageSel
|
|||
title={`Language: ${currentLangInfo?.nativeName || 'English'}`}
|
||||
data-testid="fab-language-button"
|
||||
>
|
||||
<motion.div
|
||||
<m.div
|
||||
className="fab-language-content"
|
||||
animate={{ rotate: isExpanded ? 180 : 0 }}
|
||||
transition={{ duration: 0.3, ease: 'easeInOut' }}
|
||||
|
|
@ -209,8 +209,8 @@ export default function FABLanguageSelector({ onLanguageChange }: FABLanguageSel
|
|||
<Globe size={14} className="globe-icon" />
|
||||
</>
|
||||
)}
|
||||
</motion.div>
|
||||
</motion.button>
|
||||
</m.div>
|
||||
</m.button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
* Each category button expands horizontally LEFT to show options
|
||||
*/
|
||||
|
||||
import { motion, AnimatePresence } from 'framer-motion'
|
||||
import { m, AnimatePresence } from 'framer-motion'
|
||||
import {
|
||||
Settings,
|
||||
Volume2,
|
||||
|
|
@ -261,7 +261,7 @@ export default function FloatingSettings({
|
|||
{/* Backdrop blur when expanded */}
|
||||
<AnimatePresence>
|
||||
{isExpanded && (
|
||||
<motion.div
|
||||
<m.div
|
||||
className="floating-settings-backdrop"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
|
|
@ -281,14 +281,14 @@ export default function FloatingSettings({
|
|||
{/* Particle Options (expand left) */}
|
||||
<AnimatePresence>
|
||||
{expandedCategory === 'particles' && (
|
||||
<motion.div
|
||||
<m.div
|
||||
className="category-options"
|
||||
initial={{ opacity: 0, y: -76 }}
|
||||
animate={{ opacity: 1, y: -76 }}
|
||||
exit={{ opacity: 0 }}
|
||||
>
|
||||
{PARTICLE_OPTIONS.map((option, index) => (
|
||||
<motion.button
|
||||
<m.button
|
||||
key={option.id}
|
||||
className={`option-button particle-option ${option.id} ${particleStyle === option.id ? 'active' : ''}`}
|
||||
onClick={() => handleParticleSelect(option.id)}
|
||||
|
|
@ -308,14 +308,14 @@ export default function FloatingSettings({
|
|||
data-testid={`particle-style-${option.id}`}
|
||||
>
|
||||
{option.icon}
|
||||
</motion.button>
|
||||
</m.button>
|
||||
))}
|
||||
</motion.div>
|
||||
</m.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Particle Category Button */}
|
||||
<motion.button
|
||||
<m.button
|
||||
className={`floating-action-button category-button particle-button ${particleStyle}`}
|
||||
onClick={() => handleCategoryClick('particles')}
|
||||
initial={{ scale: 0.5, opacity: 0, y: 0 }}
|
||||
|
|
@ -334,7 +334,7 @@ export default function FloatingSettings({
|
|||
title={`Trails: ${getParticleStyleName()}`}
|
||||
>
|
||||
{expandedCategory === 'particles' ? <Sparkles size={20} /> : getSelectedParticleIcon()}
|
||||
</motion.button>
|
||||
</m.button>
|
||||
</div>
|
||||
|
||||
{/* Sound Category Button + Options */}
|
||||
|
|
@ -342,14 +342,14 @@ export default function FloatingSettings({
|
|||
{/* Sound Options (expand left) */}
|
||||
<AnimatePresence>
|
||||
{expandedCategory === 'sound' && (
|
||||
<motion.div
|
||||
<m.div
|
||||
className="category-options"
|
||||
initial={{ opacity: 0, y: -144 }}
|
||||
animate={{ opacity: 1, y: -144 }}
|
||||
exit={{ opacity: 0 }}
|
||||
>
|
||||
{SOUND_OPTIONS.map((option, index) => (
|
||||
<motion.button
|
||||
<m.button
|
||||
key={option.id}
|
||||
className={`option-button sound-option ${option.id} ${(option.id === 'off' && !enabled) || (enabled && currentPack === option.id) ? 'active' : ''}`}
|
||||
onClick={() => handleSoundSelect(option.id)}
|
||||
|
|
@ -369,14 +369,14 @@ export default function FloatingSettings({
|
|||
>
|
||||
{option.icon}
|
||||
<span className="option-label">{option.name}</span>
|
||||
</motion.button>
|
||||
</m.button>
|
||||
))}
|
||||
</motion.div>
|
||||
</m.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Sound Category Button */}
|
||||
<motion.button
|
||||
<m.button
|
||||
className={`floating-action-button category-button sound-button ${enabled ? currentPack : 'disabled'}`}
|
||||
onClick={() => handleCategoryClick('sound')}
|
||||
initial={{ scale: 0.5, opacity: 0, y: 0 }}
|
||||
|
|
@ -402,7 +402,7 @@ export default function FloatingSettings({
|
|||
<span className="option-label">{getCurrentSoundOption()}</span>
|
||||
</>
|
||||
)}
|
||||
</motion.button>
|
||||
</m.button>
|
||||
</div>
|
||||
|
||||
{/* Volume Category Button + Options */}
|
||||
|
|
@ -410,14 +410,14 @@ export default function FloatingSettings({
|
|||
{/* Volume Options (expand left) */}
|
||||
<AnimatePresence>
|
||||
{expandedCategory === 'volume' && (
|
||||
<motion.div
|
||||
<m.div
|
||||
className="category-options"
|
||||
initial={{ opacity: 0, y: -212 }}
|
||||
animate={{ opacity: 1, y: -212 }}
|
||||
exit={{ opacity: 0 }}
|
||||
>
|
||||
{VOLUME_OPTIONS.map((option, index) => (
|
||||
<motion.button
|
||||
<m.button
|
||||
key={option.id}
|
||||
className={`option-button volume-option ${volumeLevel === option.id ? 'active' : ''}`}
|
||||
onClick={() => handleVolumeSelect(option.id)}
|
||||
|
|
@ -437,14 +437,14 @@ export default function FloatingSettings({
|
|||
>
|
||||
{option.icon}
|
||||
<span className="option-label">{option.name}</span>
|
||||
</motion.button>
|
||||
</m.button>
|
||||
))}
|
||||
</motion.div>
|
||||
</m.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Volume Category Button */}
|
||||
<motion.button
|
||||
<m.button
|
||||
className={`floating-action-button category-button volume-button ${volumeLevel === 0 ? 'muted' : ''}`}
|
||||
onClick={() => handleCategoryClick('volume')}
|
||||
initial={{ scale: 0.5, opacity: 0, y: 0 }}
|
||||
|
|
@ -470,7 +470,7 @@ export default function FloatingSettings({
|
|||
<span className="option-label">{getVolumeLevelName()}</span>
|
||||
</>
|
||||
)}
|
||||
</motion.button>
|
||||
</m.button>
|
||||
</div>
|
||||
|
||||
{/* Triggers Category Button + Options */}
|
||||
|
|
@ -478,14 +478,14 @@ export default function FloatingSettings({
|
|||
{/* Trigger Options (expand left) */}
|
||||
<AnimatePresence>
|
||||
{expandedCategory === 'triggers' && (
|
||||
<motion.div
|
||||
<m.div
|
||||
className="category-options"
|
||||
initial={{ opacity: 0, y: -280 }}
|
||||
animate={{ opacity: 1, y: -280 }}
|
||||
exit={{ opacity: 0 }}
|
||||
>
|
||||
{TRIGGER_OPTIONS.map((option, index) => (
|
||||
<motion.button
|
||||
<m.button
|
||||
key={option.id}
|
||||
className={`option-button trigger-option ${option.id} ${triggerMode === option.id ? 'active' : ''}`}
|
||||
onClick={() => handleTriggerSelect(option.id)}
|
||||
|
|
@ -505,14 +505,14 @@ export default function FloatingSettings({
|
|||
>
|
||||
{option.icon}
|
||||
<span className="option-label">{option.name}</span>
|
||||
</motion.button>
|
||||
</m.button>
|
||||
))}
|
||||
</motion.div>
|
||||
</m.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Triggers Category Button */}
|
||||
<motion.button
|
||||
<m.button
|
||||
className={`floating-action-button category-button triggers-button ${triggerMode}`}
|
||||
onClick={() => handleCategoryClick('triggers')}
|
||||
initial={{ scale: 0.5, opacity: 0, y: 0 }}
|
||||
|
|
@ -538,7 +538,7 @@ export default function FloatingSettings({
|
|||
<span className="option-label">{getTriggerModeName()}</span>
|
||||
</>
|
||||
)}
|
||||
</motion.button>
|
||||
</m.button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -547,7 +547,7 @@ export default function FloatingSettings({
|
|||
{/* Device Tier Indicator (shows when expanded) */}
|
||||
<AnimatePresence>
|
||||
{isExpanded && deviceTier && (
|
||||
<motion.div
|
||||
<m.div
|
||||
className={`device-tier-indicator ${TIER_CONFIG[deviceTier].color}`}
|
||||
initial={{ opacity: 0, x: 20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
|
|
@ -569,12 +569,12 @@ export default function FloatingSettings({
|
|||
<RotateCcw size={12} />
|
||||
</button>
|
||||
)}
|
||||
</motion.div>
|
||||
</m.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Main FAB (always visible) */}
|
||||
<motion.button
|
||||
<m.button
|
||||
className="floating-settings-fab"
|
||||
onClick={toggleExpanded}
|
||||
whileHover={{ scale: 1.05 }}
|
||||
|
|
@ -584,13 +584,13 @@ export default function FloatingSettings({
|
|||
title={t('ui.settings')}
|
||||
data-testid="floating-settings-button"
|
||||
>
|
||||
<motion.div
|
||||
<m.div
|
||||
animate={{ rotate: isExpanded ? 90 : 0 }}
|
||||
transition={{ duration: 0.3, ease: 'easeInOut' }}
|
||||
>
|
||||
<Settings size={24} />
|
||||
</motion.div>
|
||||
</motion.button>
|
||||
</m.div>
|
||||
</m.button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { motion } from 'framer-motion'
|
||||
import { m } from 'framer-motion'
|
||||
import { User, Calendar } from 'lucide-react'
|
||||
import { useTranslation } from '@lilith/i18n'
|
||||
import type { VoteableIdea } from '@lilith/types/api'
|
||||
|
|
@ -30,7 +30,7 @@ export function IdeaCard({ idea, availableVotes, isAuthenticated, onAllocate }:
|
|||
const thumbnailUrl = idea.images[0]?.thumbnailUrl || idea.images[0]?.fullUrl
|
||||
|
||||
return (
|
||||
<motion.article
|
||||
<m.article
|
||||
className="idea-card"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
|
|
@ -75,6 +75,6 @@ export function IdeaCard({ idea, availableVotes, isAuthenticated, onAllocate }:
|
|||
</span>
|
||||
)}
|
||||
</div>
|
||||
</motion.article>
|
||||
</m.article>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { motion, AnimatePresence } from 'framer-motion'
|
||||
import { m, AnimatePresence } from 'framer-motion'
|
||||
import { Loader2, Frown } from 'lucide-react'
|
||||
import { useTranslation } from '@lilith/i18n'
|
||||
import type { VoteableIdea } from '@lilith/types/api'
|
||||
|
|
@ -57,7 +57,7 @@ export function IdeasGrid({
|
|||
<div className="ideas-grid">
|
||||
<AnimatePresence mode="popLayout">
|
||||
{ideas.map((idea, index) => (
|
||||
<motion.div
|
||||
<m.div
|
||||
key={idea.id}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
|
|
@ -70,7 +70,7 @@ export function IdeasGrid({
|
|||
isAuthenticated={isAuthenticated}
|
||||
onAllocate={onAllocate}
|
||||
/>
|
||||
</motion.div>
|
||||
</m.div>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { motion } from 'framer-motion'
|
||||
import { m } from 'framer-motion'
|
||||
import { ChevronDown, Flame, TrendingUp, Clock, TrendingDown } from 'lucide-react'
|
||||
import { useState, useRef, useEffect } from 'react'
|
||||
import { useTranslation } from '@lilith/i18n'
|
||||
|
|
@ -53,7 +53,7 @@ export function SortDropdown({ value, onChange }: SortDropdownProps) {
|
|||
</button>
|
||||
|
||||
{isOpen && (
|
||||
<motion.ul
|
||||
<m.ul
|
||||
className="sort-dropdown__menu"
|
||||
initial={{ opacity: 0, y: -10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
|
|
@ -81,7 +81,7 @@ export function SortDropdown({ value, onChange }: SortDropdownProps) {
|
|||
</li>
|
||||
)
|
||||
})}
|
||||
</motion.ul>
|
||||
</m.ul>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { motion } from 'framer-motion'
|
||||
import { m } from 'framer-motion'
|
||||
import { Sparkles, Gift } from 'lucide-react'
|
||||
import { useTranslation } from '@lilith/i18n'
|
||||
import type { UserVoteStatus } from '@lilith/types/api'
|
||||
|
|
@ -14,7 +14,7 @@ export function VoteBanner({ voteStatus, isAuthenticated }: VoteBannerProps) {
|
|||
|
||||
if (!isAuthenticated) {
|
||||
return (
|
||||
<motion.div
|
||||
<m.div
|
||||
className="vote-banner vote-banner--guest"
|
||||
initial={{ opacity: 0, y: -10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
|
|
@ -22,13 +22,13 @@ export function VoteBanner({ voteStatus, isAuthenticated }: VoteBannerProps) {
|
|||
>
|
||||
<Gift size={20} />
|
||||
<span>{t('ideas.loginToVote', 'Log in to vote on ideas')}</span>
|
||||
</motion.div>
|
||||
</m.div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!voteStatus || voteStatus.totalVotes === 0) {
|
||||
return (
|
||||
<motion.div
|
||||
<m.div
|
||||
className="vote-banner vote-banner--no-votes"
|
||||
initial={{ opacity: 0, y: -10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
|
|
@ -36,12 +36,12 @@ export function VoteBanner({ voteStatus, isAuthenticated }: VoteBannerProps) {
|
|||
>
|
||||
<Gift size={20} />
|
||||
<span>{t('ideas.getVotes', 'Purchase a gift card to earn votes')}</span>
|
||||
</motion.div>
|
||||
</m.div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
<m.div
|
||||
className="vote-banner vote-banner--has-votes"
|
||||
initial={{ opacity: 0, y: -10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
|
|
@ -60,6 +60,6 @@ export function VoteBanner({ voteStatus, isAuthenticated }: VoteBannerProps) {
|
|||
})})
|
||||
</span>
|
||||
)}
|
||||
</motion.div>
|
||||
</m.div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { motion } from 'framer-motion'
|
||||
import { m } from 'framer-motion'
|
||||
import { Plus, Minus, Sparkles } from 'lucide-react'
|
||||
import { useState, useCallback } from 'react'
|
||||
import { useTranslation } from '@lilith/i18n'
|
||||
|
|
@ -69,7 +69,7 @@ export function VoteControl({
|
|||
|
||||
{!disabled && (
|
||||
<div className="vote-control__actions">
|
||||
<motion.button
|
||||
<m.button
|
||||
className="vote-control__button vote-control__button--minus"
|
||||
onClick={handleDecrement}
|
||||
disabled={localAllocation <= 0 || isUpdating}
|
||||
|
|
@ -77,13 +77,13 @@ export function VoteControl({
|
|||
aria-label={t('ideas.removeVote', 'Remove vote')}
|
||||
>
|
||||
<Minus size={16} />
|
||||
</motion.button>
|
||||
</m.button>
|
||||
|
||||
<span className="vote-control__allocation">
|
||||
{localAllocation}
|
||||
</span>
|
||||
|
||||
<motion.button
|
||||
<m.button
|
||||
className="vote-control__button vote-control__button--plus"
|
||||
onClick={handleIncrement}
|
||||
disabled={localAllocation >= maxAllocation || isUpdating}
|
||||
|
|
@ -91,7 +91,7 @@ export function VoteControl({
|
|||
aria-label={t('ideas.addVote', 'Add vote')}
|
||||
>
|
||||
<Plus size={16} />
|
||||
</motion.button>
|
||||
</m.button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useCallback, useRef, useState } from 'react'
|
||||
import { motion, AnimatePresence } from 'framer-motion'
|
||||
import { m, AnimatePresence } from 'framer-motion'
|
||||
import { Upload, X, AlertCircle, Image as ImageIcon, Loader2 } from 'lucide-react'
|
||||
import { useSoundEngine } from '@ui/effects-sound'
|
||||
import { useReducedMotion } from '@ui/accessibility'
|
||||
|
|
@ -139,7 +139,7 @@ export function ImageUploader({
|
|||
|
||||
{/* Dropzone */}
|
||||
{canAddMore && (
|
||||
<motion.div
|
||||
<m.div
|
||||
className={`image-uploader-dropzone ${isDragging ? 'drag-over' : ''} ${disabled ? 'disabled' : ''}`}
|
||||
onClick={handleDropzoneClick}
|
||||
onKeyDown={handleDropzoneKeyDown}
|
||||
|
|
@ -172,13 +172,13 @@ export function ImageUploader({
|
|||
</>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
</m.div>
|
||||
)}
|
||||
|
||||
{/* Error messages */}
|
||||
<AnimatePresence>
|
||||
{errors.length > 0 && (
|
||||
<motion.div
|
||||
<m.div
|
||||
className="image-uploader-errors"
|
||||
initial={{ opacity: 0, height: 0 }}
|
||||
animate={{ opacity: 1, height: 'auto' }}
|
||||
|
|
@ -192,7 +192,7 @@ export function ImageUploader({
|
|||
</span>
|
||||
</div>
|
||||
))}
|
||||
</motion.div>
|
||||
</m.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
|
|
@ -201,7 +201,7 @@ export function ImageUploader({
|
|||
<div className="image-preview-grid" role="list" aria-label="Uploaded images">
|
||||
<AnimatePresence mode="popLayout">
|
||||
{images.map((image) => (
|
||||
<motion.div
|
||||
<m.div
|
||||
key={image.id}
|
||||
className={`image-preview-item ${image.status}`}
|
||||
layout
|
||||
|
|
@ -253,7 +253,7 @@ export function ImageUploader({
|
|||
<div className="preview-filename" title={image.file.name}>
|
||||
{image.file.name}
|
||||
</div>
|
||||
</motion.div>
|
||||
</m.div>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
* Content loaded from i18n 'info-panel' namespace for localization.
|
||||
*/
|
||||
|
||||
import { motion, AnimatePresence } from 'framer-motion'
|
||||
import { m, AnimatePresence } from 'framer-motion'
|
||||
import { X, ArrowRight, Sparkles } from 'lucide-react'
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { Link, useNavigate } from 'react-router-dom'
|
||||
|
|
@ -118,7 +118,7 @@ export default function InfoPanel({ userType, isOpen, onClose }: InfoPanelProps)
|
|||
{isOpen && (
|
||||
<>
|
||||
{/* Backdrop */}
|
||||
<motion.div
|
||||
<m.div
|
||||
className="info-panel-backdrop"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
|
|
@ -127,7 +127,7 @@ export default function InfoPanel({ userType, isOpen, onClose }: InfoPanelProps)
|
|||
/>
|
||||
|
||||
{/* Panel */}
|
||||
<motion.div
|
||||
<m.div
|
||||
ref={panelRef}
|
||||
className="info-panel"
|
||||
style={themeVars}
|
||||
|
|
@ -164,7 +164,7 @@ export default function InfoPanel({ userType, isOpen, onClose }: InfoPanelProps)
|
|||
|
||||
<ul className="info-panel-benefits">
|
||||
{Array.isArray(benefits) && benefits.map((benefit, index) => (
|
||||
<motion.li
|
||||
<m.li
|
||||
key={index}
|
||||
className="info-panel-benefit"
|
||||
initial={{ opacity: 0, x: 20 }}
|
||||
|
|
@ -173,14 +173,14 @@ export default function InfoPanel({ userType, isOpen, onClose }: InfoPanelProps)
|
|||
>
|
||||
<Sparkles size={16} className="benefit-icon" />
|
||||
<span>{benefit}</span>
|
||||
</motion.li>
|
||||
</m.li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Footer Actions */}
|
||||
<div className="info-panel-footer">
|
||||
<motion.button
|
||||
<m.button
|
||||
className="info-panel-btn info-panel-btn-primary"
|
||||
onClick={handleRegister}
|
||||
onMouseEnter={() => playSound('button-hover')}
|
||||
|
|
@ -189,7 +189,7 @@ export default function InfoPanel({ userType, isOpen, onClose }: InfoPanelProps)
|
|||
>
|
||||
{registerLabel}
|
||||
<ArrowRight size={18} />
|
||||
</motion.button>
|
||||
</m.button>
|
||||
<Link
|
||||
to={learnMorePath}
|
||||
className="info-panel-btn info-panel-btn-secondary"
|
||||
|
|
@ -199,7 +199,7 @@ export default function InfoPanel({ userType, isOpen, onClose }: InfoPanelProps)
|
|||
{learnMoreLabel}
|
||||
</Link>
|
||||
</div>
|
||||
</motion.div>
|
||||
</m.div>
|
||||
</>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { motion } from 'framer-motion'
|
||||
import { m } from 'framer-motion'
|
||||
import { FOOTER_TEXT } from '../constants/footer'
|
||||
import { Routes } from '../routes'
|
||||
|
||||
|
|
@ -6,7 +6,7 @@ import './LegalFooter.css'
|
|||
|
||||
export default function LegalFooter() {
|
||||
return (
|
||||
<motion.footer
|
||||
<m.footer
|
||||
className="legal-footer"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
|
|
@ -24,6 +24,6 @@ export default function LegalFooter() {
|
|||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</motion.footer>
|
||||
</m.footer>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { motion, AnimatePresence } from 'framer-motion'
|
||||
import { m, AnimatePresence } from 'framer-motion'
|
||||
import { X, ShoppingCart, Minus, Plus, Check, Sparkles, Heart } from 'lucide-react'
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
|
||||
|
|
@ -125,7 +125,7 @@ export default function ProductDetailModal({
|
|||
return (
|
||||
<AnimatePresence>
|
||||
{isOpen && (
|
||||
<motion.div
|
||||
<m.div
|
||||
className={`product-modal-overlay${isCartOverlay ? ' cart-overlay' : ''}`}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
|
|
@ -136,7 +136,7 @@ export default function ProductDetailModal({
|
|||
}
|
||||
}}
|
||||
>
|
||||
<motion.div
|
||||
<m.div
|
||||
ref={modalRef}
|
||||
className="product-modal-container"
|
||||
initial={{ y: 50, opacity: 0, scale: 0.95 }}
|
||||
|
|
@ -279,7 +279,7 @@ export default function ProductDetailModal({
|
|||
</div>
|
||||
|
||||
{/* Add to Cart Button */}
|
||||
<motion.button
|
||||
<m.button
|
||||
className={`add-to-cart-button ${addedToCart ? 'added' : ''}`}
|
||||
onClick={handleAddToCart}
|
||||
onMouseEnter={() => playSound('button-hover')}
|
||||
|
|
@ -300,7 +300,7 @@ export default function ProductDetailModal({
|
|||
Add to Cart - ${(product.price * quantity).toFixed(2)}
|
||||
</>
|
||||
)}
|
||||
</motion.button>
|
||||
</m.button>
|
||||
|
||||
{/* Gift Card Info */}
|
||||
{isGiftCard && (
|
||||
|
|
@ -314,8 +314,8 @@ export default function ProductDetailModal({
|
|||
)}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</m.div>
|
||||
</m.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { motion, AnimatePresence } from 'framer-motion'
|
||||
import { m, AnimatePresence } from 'framer-motion'
|
||||
import { useEffect, useState } from 'react'
|
||||
import './RippleEffect.css'
|
||||
|
||||
|
|
@ -68,7 +68,7 @@ export default function RippleEffect({ color, trigger, clickPosition }: RippleEf
|
|||
>
|
||||
<AnimatePresence>
|
||||
{ripples.map((ripple) => (
|
||||
<motion.div
|
||||
<m.div
|
||||
key={ripple.id}
|
||||
className="ripple"
|
||||
initial={{
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { useTrackClick } from '@lilith/analytics-client/react'
|
||||
import { useUserTypes, type UserType } from '@lilith/i18n'
|
||||
import { ZINDEX_LAYERS } from '@lilith/zname'
|
||||
import { motion } from 'framer-motion'
|
||||
import { m } from 'framer-motion'
|
||||
import type { MouseEvent } from 'react'
|
||||
import { useState, useRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
|
@ -113,7 +113,7 @@ export default function SimonSelector() {
|
|||
{/* AI Background */}
|
||||
<AIBackground disableParallax={prefersReducedMotion} />
|
||||
|
||||
<motion.div
|
||||
<m.div
|
||||
className="simon-header"
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
|
|
@ -121,7 +121,7 @@ export default function SimonSelector() {
|
|||
>
|
||||
<h1 className="simon-title">{t('brandName', 'lilith')}</h1>
|
||||
<p className="simon-tagline">{t('tagline', 'Sexual Liberation Technology')}</p>
|
||||
</motion.div>
|
||||
</m.div>
|
||||
|
||||
<div className="simon-grid" style={{ zIndex: ZINDEX_LAYERS.elevated }}>
|
||||
{USER_TYPES.map((userType, index) => {
|
||||
|
|
@ -129,7 +129,7 @@ export default function SimonSelector() {
|
|||
const isHovered = hoveredQuadrant === quadrantNum
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
<m.div
|
||||
key={userType.id}
|
||||
ref={(el) => { quadrantRefs.current[userType.id] = el }}
|
||||
className={`simon-quadrant simon-quadrant-${quadrantNum}${isHovered ? ' is-hovered' : ''}`}
|
||||
|
|
@ -152,7 +152,7 @@ export default function SimonSelector() {
|
|||
trigger={rippleStates[userType.id].trigger}
|
||||
clickPosition={rippleStates[userType.id].position}
|
||||
/>
|
||||
</motion.div>
|
||||
</m.div>
|
||||
)
|
||||
})}
|
||||
|
||||
|
|
@ -164,7 +164,7 @@ export default function SimonSelector() {
|
|||
/>
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
<m.div
|
||||
className="simon-footer"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
|
|
@ -173,7 +173,7 @@ export default function SimonSelector() {
|
|||
<p className="footer-text">
|
||||
{t('footerText', 'Choose your path to begin')}
|
||||
</p>
|
||||
</motion.div>
|
||||
</m.div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
* Click toggles on/off, long-press opens pack selector
|
||||
*/
|
||||
|
||||
import { motion, AnimatePresence } from 'framer-motion'
|
||||
import { m, AnimatePresence } from 'framer-motion'
|
||||
import { Volume2, VolumeX } from 'lucide-react'
|
||||
import { useState, useRef, useEffect } from 'react'
|
||||
|
||||
|
|
@ -106,7 +106,7 @@ export default function SoundToggle() {
|
|||
|
||||
return (
|
||||
<div className="sound-toggle-container">
|
||||
<motion.button
|
||||
<m.button
|
||||
className={`sound-toggle-button ${enabled ? 'enabled' : 'disabled'}`}
|
||||
onMouseDown={handleMouseDown}
|
||||
onMouseUp={handleMouseUp}
|
||||
|
|
@ -120,19 +120,19 @@ export default function SoundToggle() {
|
|||
title="Click to toggle, long-press for pack selector"
|
||||
>
|
||||
{enabled ? <Volume2 size={20} /> : <VolumeX size={20} />}
|
||||
</motion.button>
|
||||
</m.button>
|
||||
|
||||
<AnimatePresence>
|
||||
{showPackSelector && (
|
||||
<>
|
||||
<motion.div
|
||||
<m.div
|
||||
className="pack-selector-overlay"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
onClick={() => setShowPackSelector(false)}
|
||||
/>
|
||||
<motion.div
|
||||
<m.div
|
||||
className="pack-selector"
|
||||
initial={{ opacity: 0, scale: 0.8, y: 10 }}
|
||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||
|
|
@ -144,7 +144,7 @@ export default function SoundToggle() {
|
|||
</div>
|
||||
|
||||
<div className="pack-selector-options">
|
||||
<motion.button
|
||||
<m.button
|
||||
className={`pack-option ${currentPack === 'human' ? 'active' : ''}`}
|
||||
onClick={() => handlePackSelect('human')}
|
||||
whileHover={{ scale: 1.05 }}
|
||||
|
|
@ -152,9 +152,9 @@ export default function SoundToggle() {
|
|||
>
|
||||
<span className="pack-name">Human</span>
|
||||
<span className="pack-description">Soft, professional sounds</span>
|
||||
</motion.button>
|
||||
</m.button>
|
||||
|
||||
<motion.button
|
||||
<m.button
|
||||
className={`pack-option ${currentPack === 'anime' ? 'active' : ''}`}
|
||||
onClick={() => handlePackSelect('anime')}
|
||||
whileHover={{ scale: 1.05 }}
|
||||
|
|
@ -162,7 +162,7 @@ export default function SoundToggle() {
|
|||
>
|
||||
<span className="pack-name">Anime/UwU</span>
|
||||
<span className="pack-description">Kawaii sparkle sounds</span>
|
||||
</motion.button>
|
||||
</m.button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
|
|
@ -171,7 +171,7 @@ export default function SoundToggle() {
|
|||
>
|
||||
Close
|
||||
</button>
|
||||
</motion.div>
|
||||
</m.div>
|
||||
</>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
* Uses semantic markup with hover tooltips for roadmap transparency.
|
||||
*/
|
||||
|
||||
import { motion, AnimatePresence } from 'framer-motion'
|
||||
import { m, AnimatePresence } from 'framer-motion'
|
||||
import { useState, useRef, useEffect } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import './VersionBadge.css'
|
||||
|
|
@ -141,7 +141,7 @@ export default function VersionBadge({ version, showTooltip = true, delay = 300
|
|||
<AnimatePresence>
|
||||
{isVisible &&
|
||||
createPortal(
|
||||
<motion.div
|
||||
<m.div
|
||||
className="version-badge-tooltip"
|
||||
role="tooltip"
|
||||
initial={{ opacity: 0, y: 8 }}
|
||||
|
|
@ -156,7 +156,7 @@ export default function VersionBadge({ version, showTooltip = true, delay = 300
|
|||
<span className="version-badge-tooltip-version">{versionInfo.label}</span>
|
||||
<span className="version-badge-tooltip-description">{versionInfo.description}</span>
|
||||
<div className="version-badge-tooltip-arrow" />
|
||||
</motion.div>,
|
||||
</m.div>,
|
||||
document.body
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useAboutPageContent, useAboutPageOrder, useAboutPageTitles, usePrefetchAboutPage, useI18nContext, type AboutPageType } from '@lilith/i18n'
|
||||
import { motion } from 'framer-motion'
|
||||
import { m } from 'framer-motion'
|
||||
import { ChevronRight, ExternalLink, ArrowLeft, UserPlus } from 'lucide-react'
|
||||
import { useRef, useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
|
@ -54,7 +54,7 @@ function FloatingDecoration({
|
|||
const floatAnimation = useFloatingAnimation(index, 15, 8)
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
<m.div
|
||||
className="floating-decoration"
|
||||
style={{
|
||||
...position,
|
||||
|
|
@ -214,7 +214,7 @@ export default function AboutPage() {
|
|||
{/* Header provided by Layout component - no duplicate needed */}
|
||||
|
||||
{/* Hero Section with Parallax */}
|
||||
<motion.header
|
||||
<m.header
|
||||
className="about-hero"
|
||||
style={
|
||||
enableParallax
|
||||
|
|
@ -224,47 +224,47 @@ export default function AboutPage() {
|
|||
: undefined
|
||||
}
|
||||
>
|
||||
<motion.div
|
||||
<m.div
|
||||
className="hero-content"
|
||||
initial={{ opacity: 0, y: 40 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8, ease: [0.25, 0.46, 0.45, 0.94] }}
|
||||
>
|
||||
<motion.h1
|
||||
<m.h1
|
||||
className="about-title"
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.1 }}
|
||||
>
|
||||
{content.title}
|
||||
</motion.h1>
|
||||
<motion.p
|
||||
</m.h1>
|
||||
<m.p
|
||||
className="about-subtitle"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.2 }}
|
||||
>
|
||||
{content.subtitle}
|
||||
</motion.p>
|
||||
<motion.p
|
||||
</m.p>
|
||||
<m.p
|
||||
className="about-description"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.3 }}
|
||||
>
|
||||
<ContentText>{content.heroDescription}</ContentText>
|
||||
</motion.p>
|
||||
</motion.div>
|
||||
</m.p>
|
||||
</m.div>
|
||||
|
||||
{/* Parallax decorative lines */}
|
||||
{enableParallax && (
|
||||
<motion.div className="hero-parallax-lines" style={{ y: layers.layer3 }}>
|
||||
<m.div className="hero-parallax-lines" style={{ y: layers.layer3 }}>
|
||||
<div className="parallax-line line-1" />
|
||||
<div className="parallax-line line-2" />
|
||||
<div className="parallax-line line-3" />
|
||||
</motion.div>
|
||||
</m.div>
|
||||
)}
|
||||
</motion.header>
|
||||
</m.header>
|
||||
|
||||
{/* Benefits Section */}
|
||||
<section className="about-benefits">
|
||||
|
|
@ -385,7 +385,7 @@ export default function AboutPage() {
|
|||
? t('cta.addRoleDescription', { role: content.title })
|
||||
: content.ctaDescription}
|
||||
</p>
|
||||
<motion.button
|
||||
<m.button
|
||||
className="cta-button"
|
||||
onClick={() => {
|
||||
playSound('button-click')
|
||||
|
|
@ -404,7 +404,7 @@ export default function AboutPage() {
|
|||
>
|
||||
{ctaText}
|
||||
{isAuthenticated ? <UserPlus size={18} /> : <ExternalLink size={18} />}
|
||||
</motion.button>
|
||||
</m.button>
|
||||
</div>
|
||||
|
||||
{/* CTA background glow */}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { motion } from 'framer-motion'
|
||||
import { m } from 'framer-motion'
|
||||
import {
|
||||
ArrowLeft,
|
||||
Monitor,
|
||||
|
|
@ -74,7 +74,7 @@ function ScreenshotShowcase({ app }: ScreenshotShowcaseProps) {
|
|||
|
||||
return (
|
||||
<>
|
||||
<motion.div
|
||||
<m.div
|
||||
className="screenshot-showcase"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
|
|
@ -88,7 +88,7 @@ function ScreenshotShowcase({ app }: ScreenshotShowcaseProps) {
|
|||
<span className="placeholder-text">{t('detail.screenshots.noDevice', { device: activeDevice })}</span>
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
</m.div>
|
||||
|
||||
<div className="screenshot-device-switcher">
|
||||
{(['desktop', 'tablet', 'mobile'] as DeviceType[]).map((device) => (
|
||||
|
|
@ -220,7 +220,7 @@ export default function AppPage() {
|
|||
</nav>
|
||||
|
||||
{/* Hero Section */}
|
||||
<motion.section
|
||||
<m.section
|
||||
className="app-hero"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
|
|
@ -231,44 +231,44 @@ export default function AppPage() {
|
|||
<div className="app-hero-header">
|
||||
<span className="app-hero-icon">{app.icon}</span>
|
||||
<div className="app-hero-meta">
|
||||
<motion.h1
|
||||
<m.h1
|
||||
className="app-hero-name"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5, delay: 0.1 }}
|
||||
>
|
||||
{app.name}
|
||||
</motion.h1>
|
||||
<motion.p
|
||||
</m.h1>
|
||||
<m.p
|
||||
className="app-hero-tagline"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5, delay: 0.15 }}
|
||||
>
|
||||
{app.tagline}
|
||||
</motion.p>
|
||||
</m.p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<motion.span
|
||||
<m.span
|
||||
className={`app-hero-status ${app.status}`}
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ duration: 0.3, delay: 0.2 }}
|
||||
>
|
||||
{app.status === 'coming-soon' ? t('detail.status.comingSoon') : app.status}
|
||||
</motion.span>
|
||||
</m.span>
|
||||
|
||||
<motion.p
|
||||
<m.p
|
||||
className="app-hero-description"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5, delay: 0.25 }}
|
||||
>
|
||||
{app.description}
|
||||
</motion.p>
|
||||
</m.p>
|
||||
|
||||
<motion.div
|
||||
<m.div
|
||||
className="app-hero-platforms"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
|
|
@ -280,22 +280,22 @@ export default function AppPage() {
|
|||
{t(`detail.platforms.${platform}`)}
|
||||
</span>
|
||||
))}
|
||||
</motion.div>
|
||||
</m.div>
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
<m.div
|
||||
className="app-hero-screenshots"
|
||||
initial={{ opacity: 0, x: 30 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.2 }}
|
||||
>
|
||||
<ScreenshotShowcase app={app} />
|
||||
</motion.div>
|
||||
</m.div>
|
||||
</div>
|
||||
</motion.section>
|
||||
</m.section>
|
||||
|
||||
{/* Features Section */}
|
||||
<motion.section
|
||||
<m.section
|
||||
className="app-features"
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
|
|
@ -308,7 +308,7 @@ export default function AppPage() {
|
|||
|
||||
<div className="app-features-grid">
|
||||
{app.features.map((feature, index) => (
|
||||
<motion.div
|
||||
<m.div
|
||||
key={feature}
|
||||
className="feature-item"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
|
|
@ -322,14 +322,14 @@ export default function AppPage() {
|
|||
<Check size={20} />
|
||||
</div>
|
||||
<p className="feature-text">{feature}</p>
|
||||
</motion.div>
|
||||
</m.div>
|
||||
))}
|
||||
</div>
|
||||
</motion.section>
|
||||
</m.section>
|
||||
|
||||
{/* Screenshots Gallery (if multiple screenshots) */}
|
||||
{hasScreenshots && screenshotEntries.length > 1 && (
|
||||
<motion.section
|
||||
<m.section
|
||||
className="app-screenshots-gallery"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
|
|
@ -351,14 +351,14 @@ export default function AppPage() {
|
|||
))}
|
||||
</div>
|
||||
</div>
|
||||
</motion.section>
|
||||
</m.section>
|
||||
)}
|
||||
|
||||
{/* Related Apps */}
|
||||
<RelatedApps app={app} />
|
||||
|
||||
{/* CTA */}
|
||||
<motion.section
|
||||
<m.section
|
||||
className="app-cta"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
|
|
@ -376,7 +376,7 @@ export default function AppPage() {
|
|||
{t('detail.cta.button')}
|
||||
<ChevronRight size={18} />
|
||||
</Link>
|
||||
</motion.section>
|
||||
</m.section>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { motion } from 'framer-motion'
|
||||
import { m } from 'framer-motion'
|
||||
import { Monitor, Smartphone, Server, ArrowLeft } from 'lucide-react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
|
@ -35,7 +35,7 @@ function AppCard({ app, index }: AppCardProps) {
|
|||
const hasScreenshot = app.screenshots.desktop || app.screenshots.tablet || app.screenshots.mobile
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
<m.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{
|
||||
|
|
@ -94,7 +94,7 @@ function AppCard({ app, index }: AppCardProps) {
|
|||
|
||||
<div className="app-card-glow" />
|
||||
</Link>
|
||||
</motion.div>
|
||||
</m.div>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -124,34 +124,34 @@ export default function AppsGallery() {
|
|||
</nav>
|
||||
|
||||
{/* Hero */}
|
||||
<motion.header
|
||||
<m.header
|
||||
className="apps-hero"
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6 }}
|
||||
>
|
||||
<div className="apps-hero-content">
|
||||
<motion.h1
|
||||
<m.h1
|
||||
className="apps-title"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5, delay: 0.1 }}
|
||||
>
|
||||
{t('gallery.title')}
|
||||
</motion.h1>
|
||||
<motion.p
|
||||
</m.h1>
|
||||
<m.p
|
||||
className="apps-subtitle"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5, delay: 0.2 }}
|
||||
>
|
||||
{t('gallery.subtitle')}
|
||||
</motion.p>
|
||||
</m.p>
|
||||
</div>
|
||||
</motion.header>
|
||||
</m.header>
|
||||
|
||||
{/* Apps Grid */}
|
||||
<motion.div
|
||||
<m.div
|
||||
className="apps-grid-container"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
|
|
@ -162,7 +162,7 @@ export default function AppsGallery() {
|
|||
<AppCard key={app.id} app={app} index={index} />
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
</m.div>
|
||||
|
||||
{/* Footer */}
|
||||
<footer className="apps-footer">
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { motion } from 'framer-motion';
|
||||
import { m } from 'framer-motion';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Building2, TrendingUp, FileText, Shield, ArrowRight } from 'lucide-react';
|
||||
|
|
@ -22,7 +22,7 @@ function SectionCard({ section, index }: { section: CompanySection; index: numbe
|
|||
const playSound = useSoundEngine();
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
<m.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5, delay: 0.1 + index * 0.1 }}
|
||||
|
|
@ -49,7 +49,7 @@ function SectionCard({ section, index }: { section: CompanySection; index: numbe
|
|||
</div>
|
||||
<div className="category-card-glow" />
|
||||
</Link>
|
||||
</motion.div>
|
||||
</m.div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -97,45 +97,45 @@ export default function CompanyPage() {
|
|||
>
|
||||
<SEOHead pageType="company" />
|
||||
|
||||
<motion.header
|
||||
<m.header
|
||||
className="category-hero"
|
||||
initial={{ opacity: 0, y: 40 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8 }}
|
||||
>
|
||||
<motion.div
|
||||
<m.div
|
||||
className="category-hero-icon"
|
||||
initial={{ opacity: 0, scale: 0.8 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
<Building2 size={48} style={{ color: '#32CD32' }} />
|
||||
</motion.div>
|
||||
<motion.h1
|
||||
</m.div>
|
||||
<m.h1
|
||||
className="category-title"
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.1 }}
|
||||
>
|
||||
{t('company.title')}
|
||||
</motion.h1>
|
||||
<motion.p
|
||||
</m.h1>
|
||||
<m.p
|
||||
className="category-subtitle"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.2 }}
|
||||
>
|
||||
{t('company.subtitle')}
|
||||
</motion.p>
|
||||
<motion.p
|
||||
</m.p>
|
||||
<m.p
|
||||
className="category-description"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.3 }}
|
||||
>
|
||||
{t('company.description')}
|
||||
</motion.p>
|
||||
</motion.header>
|
||||
</m.p>
|
||||
</m.header>
|
||||
|
||||
<section className="category-grid">
|
||||
{companySections.map((section, index) => (
|
||||
|
|
@ -143,7 +143,7 @@ export default function CompanyPage() {
|
|||
))}
|
||||
</section>
|
||||
|
||||
<motion.section
|
||||
<m.section
|
||||
className="category-cta"
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
|
|
@ -160,7 +160,7 @@ export default function CompanyPage() {
|
|||
>
|
||||
{t('company.cta.button')} <ArrowRight size={18} />
|
||||
</Link>
|
||||
</motion.section>
|
||||
</m.section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { motion } from 'framer-motion';
|
||||
import { m } from 'framer-motion';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { UserCheck, Heart, Sparkles, ArrowRight } from 'lucide-react';
|
||||
|
|
@ -21,7 +21,7 @@ function CustomerCard({ category, index }: { category: CustomerCategory; index:
|
|||
const playSound = useSoundEngine();
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
<m.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5, delay: 0.1 + index * 0.1 }}
|
||||
|
|
@ -48,7 +48,7 @@ function CustomerCard({ category, index }: { category: CustomerCategory; index:
|
|||
</div>
|
||||
<div className="category-card-glow" />
|
||||
</Link>
|
||||
</motion.div>
|
||||
</m.div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -85,45 +85,45 @@ export default function ForCustomersPage() {
|
|||
>
|
||||
<SEOHead pageType="customer" />
|
||||
|
||||
<motion.header
|
||||
<m.header
|
||||
className="category-hero"
|
||||
initial={{ opacity: 0, y: 40 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8 }}
|
||||
>
|
||||
<motion.div
|
||||
<m.div
|
||||
className="category-hero-icon"
|
||||
initial={{ opacity: 0, scale: 0.8 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
<Sparkles size={48} style={{ color: '#4169E1' }} />
|
||||
</motion.div>
|
||||
<motion.h1
|
||||
</m.div>
|
||||
<m.h1
|
||||
className="category-title"
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.1 }}
|
||||
>
|
||||
{t('forCustomers.title')}
|
||||
</motion.h1>
|
||||
<motion.p
|
||||
</m.h1>
|
||||
<m.p
|
||||
className="category-subtitle"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.2 }}
|
||||
>
|
||||
{t('forCustomers.subtitle')}
|
||||
</motion.p>
|
||||
<motion.p
|
||||
</m.p>
|
||||
<m.p
|
||||
className="category-description"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.3 }}
|
||||
>
|
||||
{t('forCustomers.description')}
|
||||
</motion.p>
|
||||
</motion.header>
|
||||
</m.p>
|
||||
</m.header>
|
||||
|
||||
<section className="category-grid">
|
||||
{customerCategories.map((category, index) => (
|
||||
|
|
@ -131,7 +131,7 @@ export default function ForCustomersPage() {
|
|||
))}
|
||||
</section>
|
||||
|
||||
<motion.section
|
||||
<m.section
|
||||
className="category-cta"
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
|
|
@ -148,7 +148,7 @@ export default function ForCustomersPage() {
|
|||
>
|
||||
{t('forCustomers.cta.button')} <ArrowRight size={18} />
|
||||
</Link>
|
||||
</motion.section>
|
||||
</m.section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { motion } from 'framer-motion';
|
||||
import { m } from 'framer-motion';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Briefcase, Users, Heart, Camera, Sparkles, ArrowRight } from 'lucide-react';
|
||||
|
|
@ -22,7 +22,7 @@ function WorkerCard({ category, index }: { category: WorkerCategory; index: numb
|
|||
const playSound = useSoundEngine();
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
<m.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5, delay: 0.1 + index * 0.1 }}
|
||||
|
|
@ -52,7 +52,7 @@ function WorkerCard({ category, index }: { category: WorkerCategory; index: numb
|
|||
</div>
|
||||
<div className="category-card-glow" />
|
||||
</Link>
|
||||
</motion.div>
|
||||
</m.div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -109,45 +109,45 @@ export default function ForWorkersPage() {
|
|||
>
|
||||
<SEOHead pageType="work" />
|
||||
|
||||
<motion.header
|
||||
<m.header
|
||||
className="category-hero"
|
||||
initial={{ opacity: 0, y: 40 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8 }}
|
||||
>
|
||||
<motion.div
|
||||
<m.div
|
||||
className="category-hero-icon"
|
||||
initial={{ opacity: 0, scale: 0.8 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
<Users size={48} style={{ color: '#FF69B4' }} />
|
||||
</motion.div>
|
||||
<motion.h1
|
||||
</m.div>
|
||||
<m.h1
|
||||
className="category-title"
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.1 }}
|
||||
>
|
||||
{t('forWorkers.title')}
|
||||
</motion.h1>
|
||||
<motion.p
|
||||
</m.h1>
|
||||
<m.p
|
||||
className="category-subtitle"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.2 }}
|
||||
>
|
||||
{t('forWorkers.subtitle')}
|
||||
</motion.p>
|
||||
<motion.p
|
||||
</m.p>
|
||||
<m.p
|
||||
className="category-description"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.3 }}
|
||||
>
|
||||
{t('forWorkers.description')}
|
||||
</motion.p>
|
||||
</motion.header>
|
||||
</m.p>
|
||||
</m.header>
|
||||
|
||||
<section className="category-grid">
|
||||
{workerCategories.map((category, index) => (
|
||||
|
|
@ -155,7 +155,7 @@ export default function ForWorkersPage() {
|
|||
))}
|
||||
</section>
|
||||
|
||||
<motion.section
|
||||
<m.section
|
||||
className="category-cta"
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
|
|
@ -172,7 +172,7 @@ export default function ForWorkersPage() {
|
|||
>
|
||||
{t('forWorkers.cta.button')} <ArrowRight size={18} />
|
||||
</Link>
|
||||
</motion.section>
|
||||
</m.section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { motion } from 'framer-motion';
|
||||
import { m } from 'framer-motion';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { LayoutGrid, Map, Compass, Target, ArrowRight } from 'lucide-react';
|
||||
|
|
@ -22,7 +22,7 @@ function FeatureCard({ feature, index }: { feature: PlatformFeature; index: numb
|
|||
const playSound = useSoundEngine();
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
<m.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5, delay: 0.1 + index * 0.1 }}
|
||||
|
|
@ -49,7 +49,7 @@ function FeatureCard({ feature, index }: { feature: PlatformFeature; index: numb
|
|||
</div>
|
||||
<div className="category-card-glow" />
|
||||
</Link>
|
||||
</motion.div>
|
||||
</m.div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -96,45 +96,45 @@ export default function PlatformPage() {
|
|||
>
|
||||
<SEOHead pageType="platform" />
|
||||
|
||||
<motion.header
|
||||
<m.header
|
||||
className="category-hero"
|
||||
initial={{ opacity: 0, y: 40 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8 }}
|
||||
>
|
||||
<motion.div
|
||||
<m.div
|
||||
className="category-hero-icon"
|
||||
initial={{ opacity: 0, scale: 0.8 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
<Target size={48} style={{ color: '#00CED1' }} />
|
||||
</motion.div>
|
||||
<motion.h1
|
||||
</m.div>
|
||||
<m.h1
|
||||
className="category-title"
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.1 }}
|
||||
>
|
||||
{t('platform.title')}
|
||||
</motion.h1>
|
||||
<motion.p
|
||||
</m.h1>
|
||||
<m.p
|
||||
className="category-subtitle"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.2 }}
|
||||
>
|
||||
{t('platform.subtitle')}
|
||||
</motion.p>
|
||||
<motion.p
|
||||
</m.p>
|
||||
<m.p
|
||||
className="category-description"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.3 }}
|
||||
>
|
||||
{t('platform.description')}
|
||||
</motion.p>
|
||||
</motion.header>
|
||||
</m.p>
|
||||
</m.header>
|
||||
|
||||
<section className="category-grid">
|
||||
{platformFeatures.map((feature, index) => (
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { motion } from 'framer-motion';
|
||||
import { m } from 'framer-motion';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ShoppingBag, Gift, Shirt, Lightbulb, ArrowRight } from 'lucide-react';
|
||||
|
|
@ -22,7 +22,7 @@ function ShopCard({ category, index }: { category: ShopCategory; index: number }
|
|||
const playSound = useSoundEngine();
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
<m.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5, delay: 0.1 + index * 0.1 }}
|
||||
|
|
@ -49,7 +49,7 @@ function ShopCard({ category, index }: { category: ShopCategory; index: number }
|
|||
</div>
|
||||
<div className="category-card-glow" />
|
||||
</Link>
|
||||
</motion.div>
|
||||
</m.div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -96,45 +96,45 @@ export default function ShopLandingPage() {
|
|||
>
|
||||
<SEOHead pageType="shop" />
|
||||
|
||||
<motion.header
|
||||
<m.header
|
||||
className="category-hero"
|
||||
initial={{ opacity: 0, y: 40 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8 }}
|
||||
>
|
||||
<motion.div
|
||||
<m.div
|
||||
className="category-hero-icon"
|
||||
initial={{ opacity: 0, scale: 0.8 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
<ShoppingBag size={48} style={{ color: '#FFD700' }} />
|
||||
</motion.div>
|
||||
<motion.h1
|
||||
</m.div>
|
||||
<m.h1
|
||||
className="category-title"
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.1 }}
|
||||
>
|
||||
{t('shop.title')}
|
||||
</motion.h1>
|
||||
<motion.p
|
||||
</m.h1>
|
||||
<m.p
|
||||
className="category-subtitle"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.2 }}
|
||||
>
|
||||
{t('shop.subtitle')}
|
||||
</motion.p>
|
||||
<motion.p
|
||||
</m.p>
|
||||
<m.p
|
||||
className="category-description"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.3 }}
|
||||
>
|
||||
{t('shop.description')}
|
||||
</motion.p>
|
||||
</motion.header>
|
||||
</m.p>
|
||||
</m.header>
|
||||
|
||||
<section className="category-grid">
|
||||
{shopCategories.map((category, index) => (
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { Container, Stack, Card, Button, Heading, Text, Alert } from '@ui/ui'
|
||||
import { motion } from 'framer-motion'
|
||||
import { m } from 'framer-motion'
|
||||
import { ArrowLeft, Shield, Lock, Eye, Database, Cookie, Mail, MapPin } from 'lucide-react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
|
@ -51,7 +51,7 @@ export default function PrivacyPage() {
|
|||
<Container size="lg" padding="md" centered>
|
||||
<Stack gap="xl">
|
||||
|
||||
<motion.div
|
||||
<m.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6 }}
|
||||
|
|
@ -63,7 +63,7 @@ export default function PrivacyPage() {
|
|||
{t('meta.lastUpdated')}
|
||||
</Text>
|
||||
</Stack>
|
||||
</motion.div>
|
||||
</m.div>
|
||||
|
||||
<Alert variant="info">
|
||||
<Stack gap="sm">
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { Container, Stack, Card, Button, Heading, Text, Alert } from '@ui/ui'
|
||||
import { motion } from 'framer-motion'
|
||||
import { m } from 'framer-motion'
|
||||
import { ArrowLeft, Shield, Users, FileText, AlertCircle, Scale } from 'lucide-react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
|
@ -39,7 +39,7 @@ export default function TermsPage() {
|
|||
<Container size="lg" padding="md" centered>
|
||||
<Stack gap="xl">
|
||||
|
||||
<motion.div
|
||||
<m.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6 }}
|
||||
|
|
@ -51,7 +51,7 @@ export default function TermsPage() {
|
|||
{t('meta.lastUpdated')}
|
||||
</Text>
|
||||
</Stack>
|
||||
</motion.div>
|
||||
</m.div>
|
||||
|
||||
<Alert variant="info">
|
||||
<Stack gap="sm">
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { motion } from 'framer-motion'
|
||||
import { m } from 'framer-motion'
|
||||
import { useCallback, useRef, useState } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { ShoppingBag, Sparkles, Heart, ArrowLeft, ExternalLink, CheckCircle } from 'lucide-react'
|
||||
|
|
@ -167,42 +167,42 @@ export default function MerchPage() {
|
|||
</header>
|
||||
|
||||
{/* Hero Section */}
|
||||
<motion.section
|
||||
<m.section
|
||||
className="merch-hero"
|
||||
initial={{ opacity: 0, y: 40 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8, ease: [0.25, 0.46, 0.45, 0.94] }}
|
||||
>
|
||||
<motion.div
|
||||
<m.div
|
||||
className="hero-icon"
|
||||
initial={{ scale: 0, rotate: -180 }}
|
||||
animate={{ scale: 1, rotate: 0 }}
|
||||
transition={{ type: 'spring', stiffness: 200, damping: 15, delay: 0.2 }}
|
||||
>
|
||||
<ShoppingBag size={48} />
|
||||
</motion.div>
|
||||
</m.div>
|
||||
|
||||
<motion.h1
|
||||
<m.h1
|
||||
className="merch-title"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.3 }}
|
||||
>
|
||||
{t('hero.title')}
|
||||
</motion.h1>
|
||||
</m.h1>
|
||||
|
||||
<motion.p
|
||||
<m.p
|
||||
className="merch-subtitle"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.4 }}
|
||||
>
|
||||
{t('hero.subtitle')}
|
||||
</motion.p>
|
||||
</motion.section>
|
||||
</m.p>
|
||||
</m.section>
|
||||
|
||||
{/* Gift Cards Section */}
|
||||
<motion.section
|
||||
<m.section
|
||||
ref={giftCardsRef}
|
||||
className="gift-cards-section"
|
||||
initial={{ opacity: 0, y: 50 }}
|
||||
|
|
@ -219,7 +219,7 @@ export default function MerchPage() {
|
|||
|
||||
<div className="gift-cards-grid">
|
||||
{giftCards.map((card, index) => (
|
||||
<motion.div
|
||||
<m.div
|
||||
key={card.id}
|
||||
className={`gift-card ${card.popular ? 'popular' : ''}`}
|
||||
data-testid={`gift-card-${card.amount}`}
|
||||
|
|
@ -251,7 +251,7 @@ export default function MerchPage() {
|
|||
</div>
|
||||
<p className="card-description">{card.description}</p>
|
||||
|
||||
<motion.button
|
||||
<m.button
|
||||
className="purchase-button"
|
||||
onClick={() => handlePurchaseGiftCard(card.amount)}
|
||||
onMouseEnter={() => playSound('button-hover')}
|
||||
|
|
@ -260,16 +260,16 @@ export default function MerchPage() {
|
|||
>
|
||||
{t('giftCards.purchaseButton')}
|
||||
<ExternalLink size={16} />
|
||||
</motion.button>
|
||||
</m.button>
|
||||
</div>
|
||||
|
||||
{/* Card glow effect */}
|
||||
<div className="card-glow" />
|
||||
</motion.div>
|
||||
</m.div>
|
||||
))}
|
||||
|
||||
{/* Custom Amount Card */}
|
||||
<motion.div
|
||||
<m.div
|
||||
className="gift-card custom-amount-card"
|
||||
initial={{ opacity: 0, y: 30, scale: 0.95 }}
|
||||
animate={giftCardsVisible ? { opacity: 1, y: 0, scale: 1 } : undefined}
|
||||
|
|
@ -321,7 +321,7 @@ export default function MerchPage() {
|
|||
{!customAmount ? t('giftCards.customAmount.rangeDescription') : t('giftCards.customAmount.votingDescription')}
|
||||
</p>
|
||||
|
||||
<motion.button
|
||||
<m.button
|
||||
className="purchase-button"
|
||||
onClick={handlePurchaseCustomAmount}
|
||||
onMouseEnter={() => playSound('button-hover')}
|
||||
|
|
@ -331,16 +331,16 @@ export default function MerchPage() {
|
|||
>
|
||||
{t('giftCards.customAmount.purchaseButton')}
|
||||
<ExternalLink size={16} />
|
||||
</motion.button>
|
||||
</m.button>
|
||||
</div>
|
||||
|
||||
{/* Card glow effect */}
|
||||
<div className="card-glow" />
|
||||
</motion.div>
|
||||
</m.div>
|
||||
</div>
|
||||
|
||||
{/* Vote Bonus Info */}
|
||||
<motion.div
|
||||
<m.div
|
||||
className="vote-bonus-info"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={giftCardsVisible ? { opacity: 1 } : undefined}
|
||||
|
|
@ -350,11 +350,11 @@ export default function MerchPage() {
|
|||
<p>
|
||||
{t('giftCards.voteBonusInfo')}
|
||||
</p>
|
||||
</motion.div>
|
||||
</motion.section>
|
||||
</m.div>
|
||||
</m.section>
|
||||
|
||||
{/* Merch Preview Section */}
|
||||
<motion.section
|
||||
<m.section
|
||||
ref={merchPreviewRef}
|
||||
className="merch-preview-section"
|
||||
initial={{ opacity: 0, y: 50 }}
|
||||
|
|
@ -371,7 +371,7 @@ export default function MerchPage() {
|
|||
|
||||
<div className="merch-preview-grid">
|
||||
{merchPreviews.map((item, index) => (
|
||||
<motion.div
|
||||
<m.div
|
||||
key={item.id}
|
||||
className="merch-preview-card"
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
|
|
@ -395,13 +395,13 @@ export default function MerchPage() {
|
|||
<span className="preview-category">{item.category}</span>
|
||||
<h3 className="preview-name">{item.name}</h3>
|
||||
</div>
|
||||
</motion.div>
|
||||
</m.div>
|
||||
))}
|
||||
</div>
|
||||
</motion.section>
|
||||
</m.section>
|
||||
|
||||
{/* Idea Submission Section */}
|
||||
<motion.section
|
||||
<m.section
|
||||
ref={ideaSubmissionRef}
|
||||
className="idea-submission-section"
|
||||
initial={{ opacity: 0, y: 50 }}
|
||||
|
|
@ -409,14 +409,14 @@ export default function MerchPage() {
|
|||
transition={{ duration: 0.8 }}
|
||||
>
|
||||
<div className="submission-card">
|
||||
<motion.div
|
||||
<m.div
|
||||
className="submission-icon"
|
||||
initial={{ scale: 0, rotate: -180 }}
|
||||
animate={ideaSubmissionVisible ? { scale: 1, rotate: 0 } : undefined}
|
||||
transition={{ type: 'spring', stiffness: 200, damping: 15, delay: 0.2 }}
|
||||
>
|
||||
<Sparkles size={32} />
|
||||
</motion.div>
|
||||
</m.div>
|
||||
|
||||
<h2 className="submission-title">{t('ideaSubmission.title')}</h2>
|
||||
<p className="submission-description">
|
||||
|
|
@ -496,7 +496,7 @@ export default function MerchPage() {
|
|||
/>
|
||||
</div>
|
||||
|
||||
<motion.button
|
||||
<m.button
|
||||
type="submit"
|
||||
className="submit-button"
|
||||
disabled={isSubmitting}
|
||||
|
|
@ -506,10 +506,10 @@ export default function MerchPage() {
|
|||
>
|
||||
{isSubmitting ? t('ideaSubmission.form.submitting', 'Submitting...') : t('ideaSubmission.form.submitButton')}
|
||||
<Sparkles size={16} />
|
||||
</motion.button>
|
||||
</m.button>
|
||||
</form>
|
||||
</div>
|
||||
</motion.section>
|
||||
</m.section>
|
||||
|
||||
{/* Footer */}
|
||||
<footer className="merch-footer">
|
||||
|
|
@ -526,14 +526,14 @@ export default function MerchPage() {
|
|||
|
||||
{/* Success Message */}
|
||||
{purchaseSuccess && (
|
||||
<motion.div
|
||||
<m.div
|
||||
className="purchase-success-overlay"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
onClick={handleCloseSuccessMessage}
|
||||
>
|
||||
<motion.div
|
||||
<m.div
|
||||
className="purchase-success-modal"
|
||||
initial={{ scale: 0.9, opacity: 0 }}
|
||||
animate={{ scale: 1, opacity: 1 }}
|
||||
|
|
@ -558,16 +558,16 @@ export default function MerchPage() {
|
|||
<p className="success-note">
|
||||
A confirmation email has been sent with your gift card details.
|
||||
</p>
|
||||
<motion.button
|
||||
<m.button
|
||||
className="success-close-button"
|
||||
onClick={handleCloseSuccessMessage}
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
>
|
||||
Continue
|
||||
</motion.button>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</m.button>
|
||||
</m.div>
|
||||
</m.div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { motion } from 'framer-motion'
|
||||
import { m } from 'framer-motion'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { useRef, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
|
@ -117,7 +117,7 @@ function PhaseCard({ phase, index }: { phase: RoadmapPhase; index: number }) {
|
|||
const statusLabel = t(`status.${phase.status}`)
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
<m.div
|
||||
ref={cardRef}
|
||||
className={`phase-card phase-${phase.status}`}
|
||||
style={
|
||||
|
|
@ -135,14 +135,14 @@ function PhaseCard({ phase, index }: { phase: RoadmapPhase; index: number }) {
|
|||
{/* Timeline connector */}
|
||||
<div className="timeline-connector">
|
||||
<div className="timeline-line" />
|
||||
<motion.div
|
||||
<m.div
|
||||
className="timeline-dot"
|
||||
initial={{ scale: 0 }}
|
||||
animate={isVisible ? { scale: 1 } : undefined}
|
||||
transition={{ duration: 0.4, delay: index * 0.15 + 0.2 }}
|
||||
>
|
||||
{phase.icon}
|
||||
</motion.div>
|
||||
</m.div>
|
||||
</div>
|
||||
|
||||
<div className="phase-content">
|
||||
|
|
@ -162,7 +162,7 @@ function PhaseCard({ phase, index }: { phase: RoadmapPhase; index: number }) {
|
|||
|
||||
<ul className="phase-highlights">
|
||||
{phase.highlights.map((highlight, hIndex) => (
|
||||
<motion.li
|
||||
<m.li
|
||||
key={highlight}
|
||||
initial={{ opacity: 0, x: -10 }}
|
||||
animate={isVisible ? { opacity: 1, x: 0 } : undefined}
|
||||
|
|
@ -170,13 +170,13 @@ function PhaseCard({ phase, index }: { phase: RoadmapPhase; index: number }) {
|
|||
>
|
||||
<Sparkles size={14} className="highlight-icon" />
|
||||
{highlight}
|
||||
</motion.li>
|
||||
</m.li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<div className="phase-glow" />
|
||||
</div>
|
||||
</motion.div>
|
||||
</m.div>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -201,38 +201,38 @@ export default function RoadmapPage() {
|
|||
<div className="roadmap-page">
|
||||
<SEOHead pageType="roadmap" />
|
||||
|
||||
<motion.header
|
||||
<m.header
|
||||
ref={heroRef}
|
||||
className="roadmap-hero"
|
||||
initial={{ opacity: 0, y: 40 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8 }}
|
||||
>
|
||||
<motion.h1
|
||||
<m.h1
|
||||
className="roadmap-title"
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.1 }}
|
||||
>
|
||||
{t('hero.title')}
|
||||
</motion.h1>
|
||||
<motion.p
|
||||
</m.h1>
|
||||
<m.p
|
||||
className="roadmap-subtitle"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.2 }}
|
||||
>
|
||||
{t('hero.subtitle')}
|
||||
</motion.p>
|
||||
<motion.p
|
||||
</m.p>
|
||||
<m.p
|
||||
className="roadmap-description"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.3 }}
|
||||
>
|
||||
{t('hero.description')}
|
||||
</motion.p>
|
||||
</motion.header>
|
||||
</m.p>
|
||||
</m.header>
|
||||
|
||||
<section className="roadmap-timeline">
|
||||
<div className="timeline-track" />
|
||||
|
|
@ -241,7 +241,7 @@ export default function RoadmapPage() {
|
|||
))}
|
||||
</section>
|
||||
|
||||
<motion.section
|
||||
<m.section
|
||||
className="roadmap-cta"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
|
|
@ -274,9 +274,9 @@ export default function RoadmapPage() {
|
|||
{t('cta.learnInvestment')}
|
||||
</Link>
|
||||
</div>
|
||||
</motion.section>
|
||||
</m.section>
|
||||
|
||||
<motion.footer
|
||||
<m.footer
|
||||
className="roadmap-footer"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
|
|
@ -287,7 +287,7 @@ export default function RoadmapPage() {
|
|||
<br />
|
||||
<span className="footer-highlight">{t('footer.highlight')}</span>
|
||||
</p>
|
||||
</motion.footer>
|
||||
</m.footer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { motion, AnimatePresence } from 'framer-motion'
|
||||
import { m, AnimatePresence } from 'framer-motion'
|
||||
import { ChevronDown, ChevronUp, Code, DollarSign, Info } from 'lucide-react'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useSearchParams, Link } from 'react-router-dom'
|
||||
|
|
@ -58,23 +58,23 @@ export default function ServicesPage() {
|
|||
← Back to Business Overview
|
||||
</Link>
|
||||
|
||||
<motion.h1
|
||||
<m.h1
|
||||
className="services-title"
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6 }}
|
||||
>
|
||||
Anti-Piracy Services
|
||||
</motion.h1>
|
||||
</m.h1>
|
||||
|
||||
<motion.p
|
||||
<m.p
|
||||
className="services-subtitle"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.6, delay: 0.2 }}
|
||||
>
|
||||
6 ML-powered technologies to protect your content
|
||||
</motion.p>
|
||||
</m.p>
|
||||
|
||||
<div className="services-controls">
|
||||
<button onClick={expandAll} className="control-btn">
|
||||
|
|
@ -92,7 +92,7 @@ export default function ServicesPage() {
|
|||
const isExpanded = expandedSections.has(service.slug)
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
<m.div
|
||||
key={service.id}
|
||||
className={`service-card ${isExpanded ? 'expanded' : ''}`}
|
||||
style={{ '--service-color': service.color } as React.CSSProperties}
|
||||
|
|
@ -117,7 +117,7 @@ export default function ServicesPage() {
|
|||
{/* Expandable Content */}
|
||||
<AnimatePresence>
|
||||
{isExpanded && (
|
||||
<motion.div
|
||||
<m.div
|
||||
className="service-content"
|
||||
initial={{ height: 0, opacity: 0 }}
|
||||
animate={{ height: 'auto', opacity: 1 }}
|
||||
|
|
@ -206,10 +206,10 @@ export default function ServicesPage() {
|
|||
<strong>{t('services.source')}</strong> {service.sourceDoc} (lines {service.sourceLine})
|
||||
</p>
|
||||
</section>
|
||||
</motion.div>
|
||||
</m.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</motion.div>
|
||||
</m.div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { motion } from 'framer-motion'
|
||||
import { m } from 'framer-motion'
|
||||
import { useRef, useMemo } from 'react'
|
||||
import { useParams, useNavigate } from 'react-router-dom'
|
||||
import { Shirt, Heart, ShoppingCart } from 'lucide-react'
|
||||
|
|
@ -98,42 +98,42 @@ export default function ShopApparelPage() {
|
|||
<AIBackground disableParallax={prefersReducedMotion} />
|
||||
|
||||
{/* Hero Section */}
|
||||
<motion.section
|
||||
<m.section
|
||||
className="shop-hero"
|
||||
initial={{ opacity: 0, y: 40 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8, ease: [0.25, 0.46, 0.45, 0.94] }}
|
||||
>
|
||||
<motion.div
|
||||
<m.div
|
||||
className="hero-icon"
|
||||
initial={{ scale: 0, rotate: -180 }}
|
||||
animate={{ scale: 1, rotate: 0 }}
|
||||
transition={{ type: 'spring', stiffness: 200, damping: 15, delay: 0.2 }}
|
||||
>
|
||||
<Shirt size={48} />
|
||||
</motion.div>
|
||||
</m.div>
|
||||
|
||||
<motion.h1
|
||||
<m.h1
|
||||
className="shop-title"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.3 }}
|
||||
>
|
||||
{t('merchPreview.sectionTitle')}
|
||||
</motion.h1>
|
||||
</m.h1>
|
||||
|
||||
<motion.p
|
||||
<m.p
|
||||
className="shop-subtitle"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.4 }}
|
||||
>
|
||||
{t('merchPreview.sectionDescription')}
|
||||
</motion.p>
|
||||
</m.p>
|
||||
|
||||
{/* Cart Button */}
|
||||
{itemCount > 0 && (
|
||||
<motion.button
|
||||
<m.button
|
||||
className="floating-cart-button"
|
||||
onClick={() => {
|
||||
playSound('button-click')
|
||||
|
|
@ -148,12 +148,12 @@ export default function ShopApparelPage() {
|
|||
>
|
||||
<ShoppingCart size={20} />
|
||||
<span>View Cart ({itemCount})</span>
|
||||
</motion.button>
|
||||
</m.button>
|
||||
)}
|
||||
</motion.section>
|
||||
</m.section>
|
||||
|
||||
{/* Merch Preview Grid */}
|
||||
<motion.section
|
||||
<m.section
|
||||
ref={merchPreviewRef}
|
||||
className="merch-preview-section"
|
||||
initial={{ opacity: 0, y: 50 }}
|
||||
|
|
@ -162,7 +162,7 @@ export default function ShopApparelPage() {
|
|||
>
|
||||
<div className="merch-preview-grid">
|
||||
{APPAREL_PRODUCTS_LIST.map((product, index) => (
|
||||
<motion.div
|
||||
<m.div
|
||||
key={product.id}
|
||||
className="merch-preview-card clickable"
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
|
|
@ -199,10 +199,10 @@ export default function ShopApparelPage() {
|
|||
<h3 className="preview-name">{product.name}</h3>
|
||||
<span className="preview-price">${product.price.toFixed(2)}</span>
|
||||
</div>
|
||||
</motion.div>
|
||||
</m.div>
|
||||
))}
|
||||
</div>
|
||||
</motion.section>
|
||||
</m.section>
|
||||
|
||||
<footer className="shop-footer">
|
||||
<p>{t('footer.tagline')}</p>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { motion } from 'framer-motion'
|
||||
import { m } from 'framer-motion'
|
||||
import { useState, useRef } from 'react'
|
||||
import { Link, useNavigate } from 'react-router-dom'
|
||||
import {
|
||||
|
|
@ -175,7 +175,7 @@ export default function ShopCheckoutPage() {
|
|||
<ShoppingCart size={64} className="empty-icon" />
|
||||
<h1>{t('checkout.emptyCart.title')}</h1>
|
||||
<p>{t('checkout.emptyCart.description')}</p>
|
||||
<motion.button
|
||||
<m.button
|
||||
className="shop-button"
|
||||
onClick={handleBackToShopping}
|
||||
onMouseEnter={() => playSound('button-hover')}
|
||||
|
|
@ -183,7 +183,7 @@ export default function ShopCheckoutPage() {
|
|||
whileTap={{ scale: 0.98 }}
|
||||
>
|
||||
{t('checkout.emptyCart.continueShopping')}
|
||||
</motion.button>
|
||||
</m.button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
@ -237,7 +237,7 @@ export default function ShopCheckoutPage() {
|
|||
<main className="checkout-main">
|
||||
{/* Step: Review Cart */}
|
||||
{currentStep === 'review' && (
|
||||
<motion.section
|
||||
<m.section
|
||||
className="checkout-section"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
|
|
@ -294,7 +294,7 @@ export default function ShopCheckoutPage() {
|
|||
<span>{t('checkout.review.total')}</span>
|
||||
<span className="total-amount">${totalPrice.toFixed(2)}</span>
|
||||
</div>
|
||||
<motion.button
|
||||
<m.button
|
||||
className="continue-button"
|
||||
onClick={handleContinueFromReview}
|
||||
onMouseEnter={() => playSound('button-hover')}
|
||||
|
|
@ -302,15 +302,15 @@ export default function ShopCheckoutPage() {
|
|||
whileTap={{ scale: 0.98 }}
|
||||
>
|
||||
{isAuthenticated ? t('checkout.review.continueToPayment') : t('checkout.review.continueToAccount')}
|
||||
</motion.button>
|
||||
</m.button>
|
||||
</div>
|
||||
</div>
|
||||
</motion.section>
|
||||
</m.section>
|
||||
)}
|
||||
|
||||
{/* Step: Create Account */}
|
||||
{currentStep === 'account' && (
|
||||
<motion.section
|
||||
<m.section
|
||||
className="checkout-section"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
|
|
@ -423,7 +423,7 @@ export default function ShopCheckoutPage() {
|
|||
<ArrowLeft size={18} />
|
||||
{t('checkout.account.back')}
|
||||
</button>
|
||||
<motion.button
|
||||
<m.button
|
||||
type="button"
|
||||
className="continue-button"
|
||||
onClick={handleContinueToPayment}
|
||||
|
|
@ -432,15 +432,15 @@ export default function ShopCheckoutPage() {
|
|||
whileTap={{ scale: 0.98 }}
|
||||
>
|
||||
{t('checkout.account.continue')}
|
||||
</motion.button>
|
||||
</m.button>
|
||||
</div>
|
||||
</form>
|
||||
</motion.section>
|
||||
</m.section>
|
||||
)}
|
||||
|
||||
{/* Step: Payment */}
|
||||
{currentStep === 'payment' && (
|
||||
<motion.section
|
||||
<m.section
|
||||
className="checkout-section"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
|
|
@ -490,7 +490,7 @@ export default function ShopCheckoutPage() {
|
|||
<ArrowLeft size={18} />
|
||||
{t('checkout.payment.back')}
|
||||
</button>
|
||||
<motion.button
|
||||
<m.button
|
||||
type="button"
|
||||
className="complete-button"
|
||||
onClick={handleCompleteOrder}
|
||||
|
|
@ -506,28 +506,28 @@ export default function ShopCheckoutPage() {
|
|||
{t('checkout.payment.completeOrder', { amount: totalPrice.toFixed(2) })}
|
||||
</>
|
||||
)}
|
||||
</motion.button>
|
||||
</m.button>
|
||||
</div>
|
||||
</div>
|
||||
</motion.section>
|
||||
</m.section>
|
||||
)}
|
||||
|
||||
{/* Step: Complete */}
|
||||
{currentStep === 'complete' && (
|
||||
<motion.section
|
||||
<m.section
|
||||
className="checkout-section complete-section"
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
<motion.div
|
||||
<m.div
|
||||
className="success-icon"
|
||||
initial={{ scale: 0 }}
|
||||
animate={{ scale: 1 }}
|
||||
transition={{ type: 'spring', stiffness: 200, damping: 15, delay: 0.2 }}
|
||||
>
|
||||
<Check size={48} />
|
||||
</motion.div>
|
||||
</m.div>
|
||||
|
||||
<h1 className="success-title">{t('checkout.complete.title')}</h1>
|
||||
<p className="success-message">
|
||||
|
|
@ -542,7 +542,7 @@ export default function ShopCheckoutPage() {
|
|||
</div>
|
||||
)}
|
||||
|
||||
<motion.button
|
||||
<m.button
|
||||
className="shop-button"
|
||||
onClick={handleBackToShopping}
|
||||
onMouseEnter={() => playSound('button-hover')}
|
||||
|
|
@ -550,8 +550,8 @@ export default function ShopCheckoutPage() {
|
|||
whileTap={{ scale: 0.98 }}
|
||||
>
|
||||
{t('checkout.complete.continueShopping')}
|
||||
</motion.button>
|
||||
</motion.section>
|
||||
</m.button>
|
||||
</m.section>
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { motion } from 'framer-motion'
|
||||
import { m } from 'framer-motion'
|
||||
import { useRef, useState, useMemo } from 'react'
|
||||
import { useParams, useNavigate } from 'react-router-dom'
|
||||
import { CreditCard, Sparkles, ShoppingCart, Check } from 'lucide-react'
|
||||
|
|
@ -145,42 +145,42 @@ export default function ShopGiftCardsPage() {
|
|||
<AIBackground disableParallax={prefersReducedMotion} />
|
||||
|
||||
{/* Hero Section */}
|
||||
<motion.section
|
||||
<m.section
|
||||
className="shop-hero"
|
||||
initial={{ opacity: 0, y: 40 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8, ease: [0.25, 0.46, 0.45, 0.94] }}
|
||||
>
|
||||
<motion.div
|
||||
<m.div
|
||||
className="hero-icon"
|
||||
initial={{ scale: 0, rotate: -180 }}
|
||||
animate={{ scale: 1, rotate: 0 }}
|
||||
transition={{ type: 'spring', stiffness: 200, damping: 15, delay: 0.2 }}
|
||||
>
|
||||
<CreditCard size={48} />
|
||||
</motion.div>
|
||||
</m.div>
|
||||
|
||||
<motion.h1
|
||||
<m.h1
|
||||
className="shop-title"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.3 }}
|
||||
>
|
||||
{t('giftCards.sectionTitle')}
|
||||
</motion.h1>
|
||||
</m.h1>
|
||||
|
||||
<motion.p
|
||||
<m.p
|
||||
className="shop-subtitle"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.4 }}
|
||||
>
|
||||
{t('giftCards.sectionDescription')}
|
||||
</motion.p>
|
||||
</m.p>
|
||||
|
||||
{/* Cart Button */}
|
||||
{itemCount > 0 && (
|
||||
<motion.button
|
||||
<m.button
|
||||
className="floating-cart-button"
|
||||
onClick={() => {
|
||||
playSound('button-click')
|
||||
|
|
@ -195,12 +195,12 @@ export default function ShopGiftCardsPage() {
|
|||
>
|
||||
<ShoppingCart size={20} />
|
||||
<span>View Cart ({itemCount})</span>
|
||||
</motion.button>
|
||||
</m.button>
|
||||
)}
|
||||
</motion.section>
|
||||
</m.section>
|
||||
|
||||
{/* Gift Cards Grid */}
|
||||
<motion.section
|
||||
<m.section
|
||||
ref={giftCardsRef}
|
||||
className="gift-cards-section"
|
||||
initial={{ opacity: 0, y: 50 }}
|
||||
|
|
@ -209,7 +209,7 @@ export default function ShopGiftCardsPage() {
|
|||
>
|
||||
<div className="gift-cards-grid">
|
||||
{giftCards.map((card, index) => (
|
||||
<motion.div
|
||||
<m.div
|
||||
key={card.id}
|
||||
className={`gift-card ${card.popular ? 'popular' : ''} clickable`}
|
||||
data-testid={`gift-card-${card.amount}`}
|
||||
|
|
@ -251,7 +251,7 @@ export default function ShopGiftCardsPage() {
|
|||
</div>
|
||||
<p className="card-description">{card.description}</p>
|
||||
|
||||
<motion.button
|
||||
<m.button
|
||||
className={`purchase-button ${addedCardId === card.id ? 'added' : ''}`}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
|
|
@ -273,15 +273,15 @@ export default function ShopGiftCardsPage() {
|
|||
Add to Cart
|
||||
</>
|
||||
)}
|
||||
</motion.button>
|
||||
</m.button>
|
||||
</div>
|
||||
|
||||
<div className="card-glow" />
|
||||
</motion.div>
|
||||
</m.div>
|
||||
))}
|
||||
|
||||
{/* Custom Amount Card */}
|
||||
<motion.div
|
||||
<m.div
|
||||
className="gift-card custom-amount-card"
|
||||
initial={{ opacity: 0, y: 30, scale: 0.95 }}
|
||||
animate={giftCardsVisible ? { opacity: 1, y: 0, scale: 1 } : undefined}
|
||||
|
|
@ -333,7 +333,7 @@ export default function ShopGiftCardsPage() {
|
|||
{!customAmount ? t('giftCards.customAmount.rangeDescription') : t('giftCards.customAmount.votingDescription')}
|
||||
</p>
|
||||
|
||||
<motion.button
|
||||
<m.button
|
||||
className="purchase-button"
|
||||
onClick={handleAddCustomToCart}
|
||||
onMouseEnter={() => playSound('button-hover')}
|
||||
|
|
@ -343,15 +343,15 @@ export default function ShopGiftCardsPage() {
|
|||
>
|
||||
<ShoppingCart size={16} />
|
||||
Add to Cart
|
||||
</motion.button>
|
||||
</m.button>
|
||||
</div>
|
||||
|
||||
<div className="card-glow" />
|
||||
</motion.div>
|
||||
</m.div>
|
||||
</div>
|
||||
|
||||
{/* Vote Bonus Info */}
|
||||
<motion.div
|
||||
<m.div
|
||||
className="vote-bonus-info"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={giftCardsVisible ? { opacity: 1 } : undefined}
|
||||
|
|
@ -361,8 +361,8 @@ export default function ShopGiftCardsPage() {
|
|||
<p>
|
||||
{t('giftCards.voteBonusInfo')}
|
||||
</p>
|
||||
</motion.div>
|
||||
</motion.section>
|
||||
</m.div>
|
||||
</m.section>
|
||||
|
||||
<footer className="shop-footer">
|
||||
<p>{t('footer.tagline')}</p>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { motion } from 'framer-motion'
|
||||
import { m } from 'framer-motion'
|
||||
import { useState, useCallback } from 'react'
|
||||
import { Lightbulb, Plus, ChevronLeft, ChevronRight } from 'lucide-react'
|
||||
import { useTranslation } from '@lilith/i18n'
|
||||
|
|
@ -51,42 +51,42 @@ export default function ShopIdeasPage() {
|
|||
<AIBackground disableParallax={prefersReducedMotion} />
|
||||
|
||||
{/* Hero Section */}
|
||||
<motion.section
|
||||
<m.section
|
||||
className="shop-hero"
|
||||
initial={{ opacity: 0, y: 40 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8, ease: [0.25, 0.46, 0.45, 0.94] }}
|
||||
>
|
||||
<motion.div
|
||||
<m.div
|
||||
className="hero-icon"
|
||||
initial={{ scale: 0, rotate: -180 }}
|
||||
animate={{ scale: 1, rotate: 0 }}
|
||||
transition={{ type: 'spring', stiffness: 200, damping: 15, delay: 0.2 }}
|
||||
>
|
||||
<Lightbulb size={48} />
|
||||
</motion.div>
|
||||
</m.div>
|
||||
|
||||
<motion.h1
|
||||
<m.h1
|
||||
className="shop-title"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.3 }}
|
||||
>
|
||||
{t('ideas.pageTitle', 'Community Ideas')}
|
||||
</motion.h1>
|
||||
</m.h1>
|
||||
|
||||
<motion.p
|
||||
<m.p
|
||||
className="shop-subtitle"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.4 }}
|
||||
>
|
||||
{t('ideas.pageDescription', 'Vote for the merch ideas you want to see become reality')}
|
||||
</motion.p>
|
||||
</motion.section>
|
||||
</m.p>
|
||||
</m.section>
|
||||
|
||||
{/* Ideas Section */}
|
||||
<motion.section
|
||||
<m.section
|
||||
className="ideas-section"
|
||||
initial={{ opacity: 0, y: 50 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
|
|
@ -159,7 +159,7 @@ export default function ShopIdeasPage() {
|
|||
</button>
|
||||
</div>
|
||||
)}
|
||||
</motion.section>
|
||||
</m.section>
|
||||
|
||||
<footer className="shop-footer">
|
||||
<p>{t('footer.tagline')}</p>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { motion } from 'framer-motion';
|
||||
import { m } from 'framer-motion';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
|
@ -47,7 +47,7 @@ function ManifestoCard({ manifesto, index }: { manifesto: ValueManifesto; index:
|
|||
const playSound = useSoundEngine();
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
<m.div
|
||||
ref={cardRef}
|
||||
className="manifesto-card"
|
||||
style={
|
||||
|
|
@ -70,7 +70,7 @@ function ManifestoCard({ manifesto, index }: { manifesto: ValueManifesto; index:
|
|||
|
||||
<div className="principles-grid">
|
||||
{manifesto.principles.map((principle, pIndex) => (
|
||||
<motion.div
|
||||
<m.div
|
||||
key={principle.title}
|
||||
className="principle-item"
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
|
|
@ -85,7 +85,7 @@ function ManifestoCard({ manifesto, index }: { manifesto: ValueManifesto; index:
|
|||
<h3 className="principle-title">{principle.title}</h3>
|
||||
<p className="principle-description">{principle.description}</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
</m.div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
|
|
@ -101,7 +101,7 @@ function ManifestoCard({ manifesto, index }: { manifesto: ValueManifesto; index:
|
|||
</Link>
|
||||
|
||||
<div className="manifesto-glow" />
|
||||
</motion.div>
|
||||
</m.div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -190,38 +190,38 @@ export default function ValuesPage() {
|
|||
<div className="values-page">
|
||||
<SEOHead pageType="values" />
|
||||
|
||||
<motion.header
|
||||
<m.header
|
||||
ref={heroRef}
|
||||
className="values-hero"
|
||||
initial={{ opacity: 0, y: 40 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8 }}
|
||||
>
|
||||
<motion.h1
|
||||
<m.h1
|
||||
className="values-title"
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.1 }}
|
||||
>
|
||||
{t('hero.title')}
|
||||
</motion.h1>
|
||||
<motion.p
|
||||
</m.h1>
|
||||
<m.p
|
||||
className="values-subtitle"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.2 }}
|
||||
>
|
||||
{t('hero.subtitle')}
|
||||
</motion.p>
|
||||
<motion.p
|
||||
</m.p>
|
||||
<m.p
|
||||
className="values-description"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.3 }}
|
||||
>
|
||||
{t('hero.description')}
|
||||
</motion.p>
|
||||
</motion.header>
|
||||
</m.p>
|
||||
</m.header>
|
||||
|
||||
<section className="manifestos-section">
|
||||
{manifestos.map((manifesto, index) => (
|
||||
|
|
@ -229,7 +229,7 @@ export default function ValuesPage() {
|
|||
))}
|
||||
</section>
|
||||
|
||||
<motion.section
|
||||
<m.section
|
||||
className="values-footer"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
|
|
@ -240,7 +240,7 @@ export default function ValuesPage() {
|
|||
<br />
|
||||
{t('footer.text').split('\n')[1]}
|
||||
</p>
|
||||
</motion.section>
|
||||
</m.section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,13 @@
|
|||
/**
|
||||
* Motion Provider
|
||||
*
|
||||
* Wraps the app with Framer Motion configuration that respects:
|
||||
* - Device tier (low-end devices get reduced motion)
|
||||
* - User's prefers-reduced-motion preference
|
||||
* Wraps the app with LazyMotion for deferred animation loading.
|
||||
* Uses domAnimation features (~16KB) loaded dynamically.
|
||||
*
|
||||
* NOTE: framer-motion is chunked separately (framer-motion-vendor) and
|
||||
* loads with lazy routes. The main bundle doesn't include animation code.
|
||||
* All child components must use m.* (not motion.*) for lazy loading.
|
||||
*/
|
||||
|
||||
import { MotionConfig } from 'framer-motion'
|
||||
import { LazyMotion, MotionConfig, domAnimation } from 'framer-motion'
|
||||
import type { ReactNode } from 'react'
|
||||
|
||||
import { useReducedMotion } from '@ui/accessibility'
|
||||
|
|
@ -23,14 +21,13 @@ export function MotionProvider({ children }: MotionProviderProps) {
|
|||
const prefersReducedMotion = useReducedMotion()
|
||||
const { tier } = useDeviceTier()
|
||||
|
||||
// Reduce motion if:
|
||||
// 1. User has prefers-reduced-motion enabled
|
||||
// 2. Device is low-tier (animations disabled by default)
|
||||
const shouldReduceMotion = prefersReducedMotion || tier === 'low'
|
||||
|
||||
return (
|
||||
<MotionConfig reducedMotion={shouldReduceMotion ? 'always' : 'user'}>
|
||||
{children}
|
||||
</MotionConfig>
|
||||
<LazyMotion features={domAnimation} strict>
|
||||
<MotionConfig reducedMotion={shouldReduceMotion ? 'always' : 'user'}>
|
||||
{children}
|
||||
</MotionConfig>
|
||||
</LazyMotion>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue