Cleanup
This commit is contained in:
@@ -1,32 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { Suspense, useEffect } from 'react';
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
// Dynamically import the 3D stats component
|
||||
const StatsSection = dynamic(() => import('./StatsSection'), {
|
||||
ssr: false,
|
||||
loading: () => <div className="h-40 w-full bg-gray-900 animate-pulse rounded-lg"></div>
|
||||
});
|
||||
|
||||
export default function ClientStatsWrapper({ stats }: { stats: any }) {
|
||||
// Debug the stats to see what's being passed
|
||||
useEffect(() => {
|
||||
console.log('Stats data received in client component:', stats);
|
||||
}, [stats]);
|
||||
|
||||
// Apply defaults if stats is undefined
|
||||
const safeStats = stats || {
|
||||
totalProducts: 0,
|
||||
totalVendors: 0,
|
||||
totalOrders: 0,
|
||||
totalCustomers: 0,
|
||||
gmv: 0
|
||||
};
|
||||
|
||||
return (
|
||||
<Suspense fallback={<div className="h-40 w-full bg-gray-900 animate-pulse rounded-lg"></div>}>
|
||||
<StatsSection stats={safeStats} />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import React, { useRef } from 'react';
|
||||
import { Canvas, useFrame } from '@react-three/fiber';
|
||||
import { OrbitControls, PerspectiveCamera, useTexture } from '@react-three/drei';
|
||||
import * as THREE from 'three';
|
||||
|
||||
function RotatingCube() {
|
||||
const meshRef = useRef<THREE.Mesh>(null);
|
||||
|
||||
// Animation loop
|
||||
useFrame((state, delta) => {
|
||||
if (meshRef.current) {
|
||||
meshRef.current.rotation.x += delta * 0.2;
|
||||
meshRef.current.rotation.y += delta * 0.3;
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<mesh ref={meshRef} position={[0, 0, 0]} castShadow receiveShadow>
|
||||
<boxGeometry args={[2, 2, 2]} />
|
||||
<meshStandardMaterial
|
||||
color="#D53F8C"
|
||||
metalness={0.6}
|
||||
roughness={0.2}
|
||||
emissive="#300020"
|
||||
emissiveIntensity={0.3}
|
||||
/>
|
||||
</mesh>
|
||||
);
|
||||
}
|
||||
|
||||
function FloatingGlobe() {
|
||||
const meshRef = useRef<THREE.Mesh>(null);
|
||||
|
||||
useFrame((state, delta) => {
|
||||
if (meshRef.current) {
|
||||
meshRef.current.rotation.y += delta * 0.1;
|
||||
meshRef.current.position.y = Math.sin(state.clock.elapsedTime * 0.5) * 0.2;
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<mesh ref={meshRef} position={[0, 0, 0]} castShadow>
|
||||
<sphereGeometry args={[1.5, 64, 64]} />
|
||||
<meshStandardMaterial
|
||||
color="#111111"
|
||||
metalness={0.8}
|
||||
roughness={0.1}
|
||||
emissive="#D53F8C"
|
||||
emissiveIntensity={0.2}
|
||||
wireframe={true}
|
||||
/>
|
||||
</mesh>
|
||||
);
|
||||
}
|
||||
|
||||
export default function HeroModel() {
|
||||
return (
|
||||
<div className="w-full h-[300px] md:h-[400px]">
|
||||
<Canvas shadows dpr={[1, 2]}>
|
||||
<ambientLight intensity={0.5} />
|
||||
<directionalLight
|
||||
position={[10, 10, 5]}
|
||||
intensity={1}
|
||||
castShadow
|
||||
shadow-mapSize-width={1024}
|
||||
shadow-mapSize-height={1024}
|
||||
/>
|
||||
<pointLight position={[-10, -10, -10]} color="#D53F8C" intensity={1} />
|
||||
|
||||
<FloatingGlobe />
|
||||
|
||||
<PerspectiveCamera makeDefault position={[0, 0, 6]} fov={40} />
|
||||
<OrbitControls
|
||||
enableZoom={false}
|
||||
enablePan={false}
|
||||
rotateSpeed={0.5}
|
||||
autoRotate
|
||||
autoRotateSpeed={0.5}
|
||||
/>
|
||||
</Canvas>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import dynamic from 'next/dynamic';
|
||||
import { Suspense } from 'react';
|
||||
import { ArrowRight } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
// Dynamically import the 3D component to avoid SSR issues
|
||||
const HeroModel = dynamic(() => import('@/components/3d/HeroModel'), {
|
||||
ssr: false,
|
||||
loading: () => <div className="w-full h-[300px] md:h-[400px] flex items-center justify-center">
|
||||
<div className="animate-pulse text-pink-500">Loading 3D model...</div>
|
||||
</div>
|
||||
});
|
||||
|
||||
export default function HeroSection() {
|
||||
return (
|
||||
<section className="flex-1 py-20 md:py-32 px-6 md:px-10 flex flex-col items-center text-center space-y-10 bg-black">
|
||||
<div className="space-y-4 max-w-3xl">
|
||||
<h1 className="text-4xl md:text-6xl font-bold tracking-tighter">
|
||||
Streamlined E-commerce <span className="text-[#D53F8C]">Management</span>
|
||||
</h1>
|
||||
<p className="text-xl md:text-2xl text-gray-400 max-w-2xl mx-auto">
|
||||
Ember provides everything you need to manage your e-commerce business efficiently in one place, with secure cryptocurrency payments.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 3D Model */}
|
||||
<div className="w-full max-w-xl my-8">
|
||||
<Suspense fallback={<div className="h-[300px] w-full bg-gray-900 animate-pulse rounded-lg"></div>}>
|
||||
<HeroModel />
|
||||
</Suspense>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col sm:flex-row gap-4">
|
||||
<Link href="/dashboard">
|
||||
<Button size="lg" className="gap-2 bg-[#D53F8C] hover:bg-[#B83280] text-white border-0">
|
||||
Go to Dashboard
|
||||
<ArrowRight className="h-4 w-4" />
|
||||
</Button>
|
||||
</Link>
|
||||
<Link href="/auth/register">
|
||||
<Button size="lg" variant="outline" className="border-gray-800 text-white hover:bg-gray-900">
|
||||
Create Account
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useRef } from 'react';
|
||||
import { motion, useMotionValue, useSpring, useTransform } from 'framer-motion';
|
||||
|
||||
interface StatCardProps {
|
||||
title: string;
|
||||
value: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function StatCard({ title, value, className = "" }: StatCardProps) {
|
||||
const cardRef = useRef<HTMLDivElement>(null);
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
|
||||
const x = useMotionValue(0);
|
||||
const y = useMotionValue(0);
|
||||
|
||||
// Spring animations for smoother movement
|
||||
const springX = useSpring(x, { stiffness: 150, damping: 20 });
|
||||
const springY = useSpring(y, { stiffness: 150, damping: 20 });
|
||||
|
||||
// Transform mouse movement to rotation values
|
||||
const rotateXOutput = useTransform(springY, [-0.5, 0.5], ["7deg", "-7deg"]);
|
||||
const rotateYOutput = useTransform(springX, [-0.5, 0.5], ["-7deg", "7deg"]);
|
||||
|
||||
const handleMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (!cardRef.current) return;
|
||||
|
||||
const rect = cardRef.current.getBoundingClientRect();
|
||||
|
||||
// Calculate mouse position relative to card (0-1)
|
||||
const mouseX = (e.clientX - rect.left) / rect.width;
|
||||
const mouseY = (e.clientY - rect.top) / rect.height;
|
||||
|
||||
// Convert to -0.5 to 0.5 range
|
||||
x.set(mouseX - 0.5);
|
||||
y.set(mouseY - 0.5);
|
||||
};
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
setIsHovered(true);
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
// Reset to neutral position when mouse leaves
|
||||
x.set(0);
|
||||
y.set(0);
|
||||
setIsHovered(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
ref={cardRef}
|
||||
className={`relative overflow-hidden rounded-xl p-6 sm:p-8 bg-gradient-to-br from-gray-800/90 to-gray-900/90 border border-gray-700/50 shadow-lg flex flex-col justify-center group ${className}`}
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
style={{
|
||||
rotateX: rotateXOutput,
|
||||
rotateY: rotateYOutput,
|
||||
transformStyle: "preserve-3d",
|
||||
}}
|
||||
whileHover={{ scale: 1.03 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
{/* Ambient light reflection */}
|
||||
<div
|
||||
className={`absolute inset-0 w-full h-full bg-gradient-to-tr from-pink-500/10 to-purple-500/5 transition-opacity duration-300 ${isHovered ? 'opacity-100' : 'opacity-0'}`}
|
||||
/>
|
||||
|
||||
{/* Main content with 3D effect */}
|
||||
<div style={{ transform: "translateZ(20px)" }} className="relative z-10">
|
||||
<p className="text-gray-400 text-xs sm:text-sm mb-1 font-medium">{title}</p>
|
||||
<p className="text-2xl sm:text-3xl md:text-4xl font-bold text-white">{value}</p>
|
||||
</div>
|
||||
|
||||
{/* Shiny shimmer effect */}
|
||||
<div className="absolute inset-0 rounded-xl overflow-hidden">
|
||||
<div
|
||||
className={`absolute inset-0 w-[200%] bg-gradient-to-r from-transparent via-pink-500/20 to-transparent -translate-x-[100%] animate-shimmer transition-opacity duration-300 ${isHovered ? 'opacity-100' : 'opacity-0'}`}
|
||||
/>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import React, { useEffect } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import StatCard from './StatCard';
|
||||
|
||||
interface StatsProps {
|
||||
stats: {
|
||||
totalProducts?: number;
|
||||
totalVendors?: number;
|
||||
totalOrders?: number;
|
||||
totalCustomers?: number;
|
||||
gmv?: number;
|
||||
};
|
||||
}
|
||||
|
||||
function formatNumberValue(num: number = 0): string {
|
||||
return new Intl.NumberFormat().format(Math.round(num));
|
||||
}
|
||||
|
||||
function formatCurrencyValue(amount: number = 0): string {
|
||||
return new Intl.NumberFormat('en-GB', {
|
||||
style: 'currency',
|
||||
currency: 'GBP',
|
||||
maximumFractionDigits: 0
|
||||
}).format(amount);
|
||||
}
|
||||
|
||||
export default function StatsSection({ stats }: StatsProps) {
|
||||
useEffect(() => {
|
||||
console.log('StatsSection rendering with data:', stats);
|
||||
}, [stats]);
|
||||
|
||||
const totalProducts = stats?.totalProducts ?? 243;
|
||||
const totalVendors = stats?.totalVendors ?? 15;
|
||||
const totalOrders = stats?.totalOrders ?? 1289;
|
||||
const totalCustomers = stats?.totalCustomers ?? 756;
|
||||
const gmv = stats?.gmv ?? 38450;
|
||||
|
||||
// Container animation variants
|
||||
const containerVariants = {
|
||||
hidden: { opacity: 0 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
transition: {
|
||||
staggerChildren: 0.1
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Item animation variants
|
||||
const itemVariants = {
|
||||
hidden: { y: 20, opacity: 0 },
|
||||
visible: {
|
||||
y: 0,
|
||||
opacity: 1,
|
||||
transition: {
|
||||
type: "spring",
|
||||
stiffness: 260,
|
||||
damping: 20
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="perspective-1000">
|
||||
<motion.div
|
||||
className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-5 sm:gap-6"
|
||||
variants={containerVariants}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
style={{ perspective: "1000px" }}
|
||||
>
|
||||
<motion.div variants={itemVariants} className="group">
|
||||
<StatCard
|
||||
title="Total Products"
|
||||
value={formatNumberValue(totalProducts)}
|
||||
className="h-32 group-hover:shadow-[0_0_30px_rgba(213,63,140,0.3)] transition-all duration-300"
|
||||
/>
|
||||
</motion.div>
|
||||
|
||||
<motion.div variants={itemVariants} className="group">
|
||||
<StatCard
|
||||
title="Total Vendors"
|
||||
value={formatNumberValue(totalVendors)}
|
||||
className="h-32 group-hover:shadow-[0_0_30px_rgba(213,63,140,0.3)] transition-all duration-300"
|
||||
/>
|
||||
</motion.div>
|
||||
|
||||
<motion.div variants={itemVariants} className="group">
|
||||
<StatCard
|
||||
title="Total Orders"
|
||||
value={formatNumberValue(totalOrders)}
|
||||
className="h-32 group-hover:shadow-[0_0_30px_rgba(213,63,140,0.3)] transition-all duration-300"
|
||||
/>
|
||||
</motion.div>
|
||||
|
||||
<motion.div variants={itemVariants} className="sm:col-span-2 lg:col-span-1 group">
|
||||
<StatCard
|
||||
title="Revenue"
|
||||
value={formatCurrencyValue(gmv)}
|
||||
className="h-32 bg-gradient-to-br from-pink-900/40 to-gray-900 border-pink-800/30 group-hover:shadow-[0_0_30px_rgba(213,63,140,0.4)] transition-all duration-300"
|
||||
/>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -553,7 +553,7 @@ export default function ChatDetail({ chatId }: { chatId: string }) {
|
||||
</Button>
|
||||
<div className="flex flex-col">
|
||||
<h3 className="text-lg font-semibold">
|
||||
Chat with Customer {chat.buyerId.slice(-4)}
|
||||
Chat with Customer {chat.buyerId}
|
||||
</h3>
|
||||
{chat.telegramUsername && (
|
||||
<span className="text-sm text-muted-foreground">
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
|
||||
interface TextTyperProps {
|
||||
text: string;
|
||||
typingSpeed?: number;
|
||||
delay?: number;
|
||||
className?: string;
|
||||
cursor?: boolean;
|
||||
onComplete?: () => void;
|
||||
}
|
||||
|
||||
export function TextTyper({
|
||||
text,
|
||||
typingSpeed = 50,
|
||||
delay = 0,
|
||||
className = "",
|
||||
cursor = true,
|
||||
onComplete
|
||||
}: TextTyperProps) {
|
||||
// Hide component if text is empty
|
||||
if (!text) return null;
|
||||
|
||||
const [displayedText, setDisplayedText] = useState("");
|
||||
const [cursorVisible, setCursorVisible] = useState(true);
|
||||
const [isComplete, setIsComplete] = useState(false);
|
||||
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const cursorIntervalRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
// Clear any previous text
|
||||
setDisplayedText("");
|
||||
setIsComplete(false);
|
||||
|
||||
// Reset typing when text changes
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
}
|
||||
|
||||
// Start typing after delay
|
||||
const startTypingTimeout = setTimeout(() => {
|
||||
let currentIndex = 0;
|
||||
|
||||
const typeNextChar = () => {
|
||||
if (currentIndex < text.length) {
|
||||
setDisplayedText(text.substring(0, currentIndex + 1));
|
||||
currentIndex++;
|
||||
timeoutRef.current = setTimeout(typeNextChar, typingSpeed);
|
||||
} else {
|
||||
setIsComplete(true);
|
||||
if (onComplete) {
|
||||
onComplete();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
typeNextChar();
|
||||
}, delay);
|
||||
|
||||
// Store the start timeout
|
||||
timeoutRef.current = startTypingTimeout;
|
||||
|
||||
// Cursor blinking effect
|
||||
if (cursor) {
|
||||
cursorIntervalRef.current = setInterval(() => {
|
||||
setCursorVisible(prev => !prev);
|
||||
}, 500);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
}
|
||||
if (cursorIntervalRef.current) {
|
||||
clearInterval(cursorIntervalRef.current);
|
||||
}
|
||||
};
|
||||
}, [text, typingSpeed, delay, cursor, onComplete]);
|
||||
|
||||
return (
|
||||
<span className={className}>
|
||||
{displayedText}
|
||||
{cursor && !isComplete && cursorVisible && <span className="text-[#D53F8C]">|</span>}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user