Add all-in-one desktop wallet with extensive feature set: Infrastructure: - Storage: IPFS-based decentralized file storage with upload/download - Hosting: Domain registration and static site hosting - Compute: GPU/CPU job marketplace for distributed computing - Database: Multi-model database services (KV, Document, Vector, etc.) Financial Features: - Privacy: Confidential transactions with Pedersen commitments - Bridge: Cross-chain transfers (Ethereum, Bitcoin, IBC/Cosmos) - Governance: DAO proposals, voting, and delegation - ZK-Rollup: L2 scaling with deposits, withdrawals, and transfers UI/UX Improvements: - Add ErrorBoundary component for graceful error handling - Add LoadingStates components (spinners, skeletons, overlays) - Add Animation components (FadeIn, SlideIn, CountUp, etc.) - Update navigation with new feature sections Testing: - Add Playwright E2E smoke tests - Test route accessibility and page rendering - Verify build process and asset loading Build: - Fix TypeScript compilation errors - Update Tauri plugin dependencies - Successfully build macOS app bundle and DMG
270 lines
5 KiB
TypeScript
270 lines
5 KiB
TypeScript
import { ReactNode, useEffect, useState } from 'react';
|
|
|
|
/**
|
|
* Fade in animation wrapper
|
|
*/
|
|
export function FadeIn({
|
|
children,
|
|
delay = 0,
|
|
duration = 300,
|
|
className = '',
|
|
}: {
|
|
children: ReactNode;
|
|
delay?: number;
|
|
duration?: number;
|
|
className?: string;
|
|
}) {
|
|
const [visible, setVisible] = useState(false);
|
|
|
|
useEffect(() => {
|
|
const timer = setTimeout(() => setVisible(true), delay);
|
|
return () => clearTimeout(timer);
|
|
}, [delay]);
|
|
|
|
return (
|
|
<div
|
|
className={className}
|
|
style={{
|
|
opacity: visible ? 1 : 0,
|
|
transition: `opacity ${duration}ms ease-in-out`,
|
|
}}
|
|
>
|
|
{children}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Slide in from direction
|
|
*/
|
|
export function SlideIn({
|
|
children,
|
|
direction = 'left',
|
|
delay = 0,
|
|
duration = 300,
|
|
distance = 20,
|
|
className = '',
|
|
}: {
|
|
children: ReactNode;
|
|
direction?: 'left' | 'right' | 'up' | 'down';
|
|
delay?: number;
|
|
duration?: number;
|
|
distance?: number;
|
|
className?: string;
|
|
}) {
|
|
const [visible, setVisible] = useState(false);
|
|
|
|
useEffect(() => {
|
|
const timer = setTimeout(() => setVisible(true), delay);
|
|
return () => clearTimeout(timer);
|
|
}, [delay]);
|
|
|
|
const transforms = {
|
|
left: `translateX(${visible ? 0 : -distance}px)`,
|
|
right: `translateX(${visible ? 0 : distance}px)`,
|
|
up: `translateY(${visible ? 0 : -distance}px)`,
|
|
down: `translateY(${visible ? 0 : distance}px)`,
|
|
};
|
|
|
|
return (
|
|
<div
|
|
className={className}
|
|
style={{
|
|
opacity: visible ? 1 : 0,
|
|
transform: transforms[direction],
|
|
transition: `all ${duration}ms ease-out`,
|
|
}}
|
|
>
|
|
{children}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Scale in animation
|
|
*/
|
|
export function ScaleIn({
|
|
children,
|
|
delay = 0,
|
|
duration = 200,
|
|
className = '',
|
|
}: {
|
|
children: ReactNode;
|
|
delay?: number;
|
|
duration?: number;
|
|
className?: string;
|
|
}) {
|
|
const [visible, setVisible] = useState(false);
|
|
|
|
useEffect(() => {
|
|
const timer = setTimeout(() => setVisible(true), delay);
|
|
return () => clearTimeout(timer);
|
|
}, [delay]);
|
|
|
|
return (
|
|
<div
|
|
className={className}
|
|
style={{
|
|
opacity: visible ? 1 : 0,
|
|
transform: `scale(${visible ? 1 : 0.95})`,
|
|
transition: `all ${duration}ms ease-out`,
|
|
}}
|
|
>
|
|
{children}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Stagger children animations
|
|
*/
|
|
export function StaggerChildren({
|
|
children,
|
|
staggerDelay = 50,
|
|
initialDelay = 0,
|
|
className = '',
|
|
}: {
|
|
children: ReactNode[];
|
|
staggerDelay?: number;
|
|
initialDelay?: number;
|
|
className?: string;
|
|
}) {
|
|
return (
|
|
<div className={className}>
|
|
{children.map((child, index) => (
|
|
<FadeIn key={index} delay={initialDelay + index * staggerDelay}>
|
|
{child}
|
|
</FadeIn>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Pulse animation (for attention)
|
|
*/
|
|
export function Pulse({
|
|
children,
|
|
className = '',
|
|
}: {
|
|
children: ReactNode;
|
|
className?: string;
|
|
}) {
|
|
return (
|
|
<div className={`animate-pulse ${className}`}>
|
|
{children}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Bounce animation
|
|
*/
|
|
export function Bounce({
|
|
children,
|
|
className = '',
|
|
}: {
|
|
children: ReactNode;
|
|
className?: string;
|
|
}) {
|
|
return (
|
|
<div className={`animate-bounce ${className}`}>
|
|
{children}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Number counter animation
|
|
*/
|
|
export function CountUp({
|
|
end,
|
|
start = 0,
|
|
duration = 1000,
|
|
decimals = 0,
|
|
prefix = '',
|
|
suffix = '',
|
|
className = '',
|
|
}: {
|
|
end: number;
|
|
start?: number;
|
|
duration?: number;
|
|
decimals?: number;
|
|
prefix?: string;
|
|
suffix?: string;
|
|
className?: string;
|
|
}) {
|
|
const [count, setCount] = useState(start);
|
|
|
|
useEffect(() => {
|
|
const startTime = Date.now();
|
|
const diff = end - start;
|
|
|
|
const animate = () => {
|
|
const elapsed = Date.now() - startTime;
|
|
const progress = Math.min(elapsed / duration, 1);
|
|
|
|
// Ease out cubic
|
|
const eased = 1 - Math.pow(1 - progress, 3);
|
|
const current = start + diff * eased;
|
|
|
|
setCount(current);
|
|
|
|
if (progress < 1) {
|
|
requestAnimationFrame(animate);
|
|
}
|
|
};
|
|
|
|
requestAnimationFrame(animate);
|
|
}, [end, start, duration]);
|
|
|
|
return (
|
|
<span className={className}>
|
|
{prefix}
|
|
{count.toFixed(decimals)}
|
|
{suffix}
|
|
</span>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Typing animation for text
|
|
*/
|
|
export function TypeWriter({
|
|
text,
|
|
speed = 50,
|
|
delay = 0,
|
|
className = '',
|
|
onComplete,
|
|
}: {
|
|
text: string;
|
|
speed?: number;
|
|
delay?: number;
|
|
className?: string;
|
|
onComplete?: () => void;
|
|
}) {
|
|
const [displayed, setDisplayed] = useState('');
|
|
|
|
useEffect(() => {
|
|
let index = 0;
|
|
const timer = setTimeout(() => {
|
|
const interval = setInterval(() => {
|
|
setDisplayed(text.slice(0, index + 1));
|
|
index++;
|
|
if (index >= text.length) {
|
|
clearInterval(interval);
|
|
onComplete?.();
|
|
}
|
|
}, speed);
|
|
return () => clearInterval(interval);
|
|
}, delay);
|
|
return () => clearTimeout(timer);
|
|
}, [text, speed, delay, onComplete]);
|
|
|
|
return (
|
|
<span className={className}>
|
|
{displayed}
|
|
<span className="animate-pulse">|</span>
|
|
</span>
|
|
);
|
|
}
|