Initialized repository for chat Esports platform development

Co-authored-by: null <4804959+fnvtk@users.noreply.github.com>
This commit is contained in:
v0
2025-11-20 03:13:10 +00:00
commit 8702d6bff3
51 changed files with 6968 additions and 0 deletions

30
README.md Normal file
View File

@@ -0,0 +1,30 @@
# Esports platform development
*Automatically synced with your [v0.app](https://v0.app) deployments*
[![Deployed on Vercel](https://img.shields.io/badge/Deployed%20on-Vercel-black?style=for-the-badge&logo=vercel)](https://vercel.com/fnvtks-projects/v0--aa)
[![Built with v0](https://img.shields.io/badge/Built%20with-v0.app-black?style=for-the-badge)](https://v0.app/chat/pDE9S8I1cvU)
## Overview
This repository will stay in sync with your deployed chats on [v0.app](https://v0.app).
Any changes you make to your deployed app will be automatically pushed to this repository from [v0.app](https://v0.app).
## Deployment
Your project is live at:
**[https://vercel.com/fnvtks-projects/v0--aa](https://vercel.com/fnvtks-projects/v0--aa)**
## Build your app
Continue building your app on:
**[https://v0.app/chat/pDE9S8I1cvU](https://v0.app/chat/pDE9S8I1cvU)**
## How It Works
1. Create and modify your project using [v0.app](https://v0.app)
2. Deploy your chats from the v0 interface
3. Changes are automatically pushed to this repository
4. Vercel deploys the latest version from this repository

277
app/booking/page.tsx Normal file
View File

@@ -0,0 +1,277 @@
"use client";
import { useState, Suspense } from "react";
import { useSearchParams, useRouter } from 'next/navigation';
import { ArrowLeft, Calendar, Clock, CreditCard, Shield, CheckCircle2 } from 'lucide-react';
import Link from "next/link";
import Image from "next/image";
// Mock data - matches star detail page
const bookingData: Record<string, any> = {
"1-1": {
starName: "GM-远洋",
starAvatar: "/gamer-girl-headphones.jpg",
serviceName: "单局陪玩",
price: 50,
duration: "1局",
type: "陪玩"
},
"1-2": {
starName: "GM-远洋",
starAvatar: "/gamer-girl-headphones.jpg",
serviceName: "5局套餐",
price: 220,
duration: "5局",
type: "陪玩"
},
"1-3": {
starName: "GM-远洋",
starAvatar: "/gamer-girl-headphones.jpg",
serviceName: "打野进阶课",
price: 299,
duration: "8节课",
type: "课程"
},
"1-4": {
starName: "GM-远洋",
starAvatar: "/gamer-girl-headphones.jpg",
serviceName: "拜师学艺",
price: 1980,
duration: "30天",
type: "拜师"
},
"2-1": {
starName: "卡若",
starAvatar: "/gamer-boy-esports-jersey.jpg",
serviceName: "单局陪玩",
price: 80,
duration: "1局",
type: "陪玩"
}
};
function BookingContent() {
const searchParams = useSearchParams();
const router = useRouter();
const starId = searchParams.get("starId") || "1";
const serviceId = searchParams.get("serviceId") || "1";
const bookingKey = `${starId}-${serviceId}`;
const booking = bookingData[bookingKey] || bookingData["1-1"];
const [selectedDate, setSelectedDate] = useState("");
const [selectedTime, setSelectedTime] = useState("");
const [note, setNote] = useState("");
const [isSubmitting, setIsSubmitting] = useState(false);
const [isSuccess, setIsSuccess] = useState(false);
// Generate available dates (next 7 days)
const availableDates = Array.from({ length: 7 }, (_, i) => {
const date = new Date();
date.setDate(date.getDate() + i);
return {
value: date.toISOString().split('T')[0],
label: i === 0 ? "今天" : i === 1 ? "明天" : `${date.getMonth() + 1}/${date.getDate()}`
};
});
// Available time slots
const timeSlots = [
"09:00", "10:00", "11:00", "14:00", "15:00", "16:00",
"17:00", "18:00", "19:00", "20:00", "21:00", "22:00"
];
const handleSubmit = async () => {
if (!selectedDate || !selectedTime) {
alert("请选择预约时间");
return;
}
setIsSubmitting(true);
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 1500));
setIsSubmitting(false);
setIsSuccess(true);
// Redirect after success
setTimeout(() => {
router.push("/profile");
}, 2000);
};
if (isSuccess) {
return (
<div className="min-h-screen flex items-center justify-center bg-background p-4">
<div className="text-center space-y-6 max-w-sm">
<div className="w-20 h-20 rounded-full bg-primary/20 flex items-center justify-center mx-auto">
<CheckCircle2 size={48} className="text-primary" />
</div>
<div>
<h2 className="text-2xl font-bold mb-2">!</h2>
<p className="text-white/60">
{booking.starName}
<br />
</p>
</div>
<Link
href="/profile"
className="inline-block px-6 py-3 rounded-full bg-primary text-black font-bold hover:bg-primary/90 transition-colors"
>
</Link>
</div>
</div>
);
}
return (
<div className="min-h-screen pb-24 bg-background">
{/* Header */}
<header className="sticky top-0 z-40 glass px-4 py-3 flex items-center gap-3 border-b border-white/5">
<Link href={`/star/${starId}`} className="p-2 -ml-2 rounded-full hover:bg-white/5">
<ArrowLeft size={20} />
</Link>
<h1 className="font-bold text-lg"></h1>
</header>
{/* Service Info */}
<div className="p-4">
<div className="glass-card p-4 rounded-xl flex items-center gap-4">
<div className="w-16 h-16 rounded-xl overflow-hidden shrink-0">
<Image
src={booking.starAvatar || "/placeholder.svg"}
alt={booking.starName}
width={64}
height={64}
className="object-cover"
/>
</div>
<div className="flex-1">
<h3 className="font-bold mb-1">{booking.starName}</h3>
<p className="text-sm text-white/60 mb-2">{booking.serviceName}</p>
<div className="flex items-center gap-3 text-xs text-white/50">
<span>{booking.duration}</span>
<span></span>
<span className="text-primary font-bold">{booking.price} </span>
</div>
</div>
</div>
</div>
{/* Date Selection */}
<div className="px-4 mb-6">
<h3 className="font-bold mb-3 flex items-center gap-2">
<Calendar size={18} className="text-primary" />
</h3>
<div className="grid grid-cols-4 gap-2">
{availableDates.map((date) => (
<button
key={date.value}
onClick={() => setSelectedDate(date.value)}
className={`p-3 rounded-xl text-sm font-medium transition-all ${
selectedDate === date.value
? "bg-primary text-black"
: "glass-card hover:bg-white/10"
}`}
>
{date.label}
</button>
))}
</div>
</div>
{/* Time Selection */}
<div className="px-4 mb-6">
<h3 className="font-bold mb-3 flex items-center gap-2">
<Clock size={18} className="text-secondary" />
</h3>
<div className="grid grid-cols-4 gap-2">
{timeSlots.map((time) => (
<button
key={time}
onClick={() => setSelectedTime(time)}
className={`p-3 rounded-xl text-sm font-medium transition-all ${
selectedTime === time
? "bg-secondary text-white"
: "glass-card hover:bg-white/10"
}`}
>
{time}
</button>
))}
</div>
</div>
{/* Note */}
<div className="px-4 mb-6">
<h3 className="font-bold mb-3"></h3>
<textarea
value={note}
onChange={(e) => setNote(e.target.value)}
placeholder="告诉大神你的需求,比如想学习的英雄、想提升的技巧等..."
className="w-full h-24 p-3 rounded-xl glass-card resize-none text-sm placeholder:text-white/30 focus:outline-none focus:ring-2 focus:ring-primary/50"
/>
</div>
{/* Payment Info */}
<div className="px-4 mb-6">
<h3 className="font-bold mb-3 flex items-center gap-2">
<CreditCard size={18} className="text-accent" />
</h3>
<div className="glass-card p-4 rounded-xl flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-primary/20 flex items-center justify-center">
<CreditCard size={20} className="text-primary" />
</div>
<div>
<div className="font-medium text-sm"></div>
<div className="text-xs text-white/50">当前余额: 1,205 </div>
</div>
</div>
<div className="text-primary font-bold">-{booking.price}</div>
</div>
</div>
{/* Trust Badge */}
<div className="mx-4 mb-6 p-4 rounded-xl bg-gradient-to-r from-primary/10 to-transparent border-l-4 border-primary">
<div className="flex items-center gap-3">
<Shield size={24} className="text-primary" />
<div>
<h4 className="font-bold text-sm"></h4>
<p className="text-xs text-white/60">,退</p>
</div>
</div>
</div>
{/* Submit Button */}
<div className="fixed bottom-0 left-0 right-0 p-4 glass border-t border-white/10 flex justify-center">
<div className="w-full max-w-md">
<button
onClick={handleSubmit}
disabled={isSubmitting || !selectedDate || !selectedTime}
className="w-full py-4 rounded-full bg-primary text-black font-bold text-lg hover:bg-primary/90 disabled:opacity-50 disabled:cursor-not-allowed transition-all active:scale-95"
>
{isSubmitting ? "提交中..." : `确认预约 (${booking.price} 玩值币)`}
</button>
</div>
</div>
</div>
);
}
export default function BookingPage() {
return (
<Suspense fallback={
<div className="min-h-screen flex items-center justify-center bg-background">
<div className="text-white/50">...</div>
</div>
}>
<BookingContent />
</Suspense>
);
}

122
app/course/[id]/page.tsx Normal file
View File

@@ -0,0 +1,122 @@
"use client"
import { ArrowLeft, Star, Clock, Users, PlayCircle, CheckCircle, Lock, Share2, MessageCircle } from 'lucide-react';
import Image from "next/image";
import Link from "next/link";
import { Button } from "@/components/ui/button";
import { useAppContext } from "@/components/providers/app-provider"; // Import context
export default function CourseDetailPage({ params }: { params: { id: string } }) {
const { pay } = useAppContext(); // Use context
const handlePurchase = () => {
pay(299, "diamonds", "打野进阶课:从入门到王者", "product");
};
return (
<div className="min-h-screen bg-background pb-24">
{/* Header Image */}
<div className="relative aspect-video w-full">
<Image
src="/esports-tournament-stadium-neon-lights-crowd.jpg"
alt="Course Cover"
fill
className="object-cover"
/>
<div className="absolute inset-0 bg-black/30" />
<Link href="/moments" className="absolute top-4 left-4 w-10 h-10 rounded-full bg-black/50 backdrop-blur-md flex items-center justify-center text-white hover:bg-black/70 transition-colors">
<ArrowLeft size={20} />
</Link>
<button className="absolute top-4 right-4 w-10 h-10 rounded-full bg-black/50 backdrop-blur-md flex items-center justify-center text-white hover:bg-black/70 transition-colors">
<Share2 size={20} />
</button>
<div className="absolute inset-0 flex items-center justify-center">
<div className="w-16 h-16 rounded-full bg-white/20 backdrop-blur-sm flex items-center justify-center cursor-pointer hover:scale-110 transition-transform">
<PlayCircle size={48} className="text-white fill-white/20" />
</div>
</div>
</div>
<div className="p-4">
{/* Title & Price */}
<div className="flex items-start justify-between mb-4">
<div>
<h1 className="text-xl font-bold mb-2"></h1>
<div className="flex items-center gap-4 text-xs text-white/60">
<span className="flex items-center gap-1"><Clock size={14} /> 8 (120)</span>
<span className="flex items-center gap-1"><Users size={14} /> 1.2k人已学</span>
</div>
</div>
<div className="text-right">
<div className="text-2xl font-bold text-primary">299</div>
<div className="text-xs text-white/50"></div>
</div>
</div>
{/* Instructor */}
<div className="flex items-center gap-3 p-3 rounded-xl bg-white/5 mb-6">
<div className="w-12 h-12 rounded-full overflow-hidden border border-white/10">
<Image src="/gamer-girl-headphones.jpg" alt="Instructor" width={48} height={48} className="object-cover" />
</div>
<div className="flex-1">
<h3 className="font-bold text-sm">GM-</h3>
<p className="text-xs text-white/50"></p>
</div>
<button className="px-4 py-1.5 rounded-full border border-primary text-primary text-xs font-bold hover:bg-primary hover:text-black transition-colors">
</button>
</div>
{/* Course Outline */}
<div className="mb-20">
<h3 className="font-bold text-lg mb-4"></h3>
<div className="space-y-3">
{[
{ title: "01. 打野基础思路与路线规划", duration: "15:00", isFree: true },
{ title: "02. 如何高效刷野与反野", duration: "18:30", isFree: false },
{ title: "03. Gank时机与路线选择", duration: "20:00", isFree: false },
{ title: "04. 控龙与峡谷先锋运营", duration: "16:45", isFree: false },
{ title: "05. 团战切入时机详解", duration: "22:10", isFree: false },
{ title: "06. 逆风局如何翻盘", duration: "19:20", isFree: false },
].map((lesson, i) => (
<div key={i} className="flex items-center gap-4 p-3 rounded-xl hover:bg-white/5 transition-colors cursor-pointer group">
<div className="w-8 h-8 rounded-full bg-white/5 flex items-center justify-center text-xs font-bold text-white/50 group-hover:bg-primary group-hover:text-black transition-colors">
{i + 1}
</div>
<div className="flex-1">
<h4 className="text-sm font-medium mb-1 group-hover:text-primary transition-colors">{lesson.title}</h4>
<span className="text-xs text-white/40">{lesson.duration}</span>
</div>
{lesson.isFree ? (
<div className="px-2 py-0.5 rounded bg-green-500/20 text-green-500 text-[10px] font-bold">
</div>
) : (
<Lock size={16} className="text-white/30" />
)}
</div>
))}
</div>
</div>
</div>
{/* Bottom Action Bar */}
<div className="fixed bottom-0 left-0 right-0 p-4 bg-background/80 backdrop-blur-lg border-t border-white/10 flex items-center gap-4 z-50">
<div className="flex flex-col items-center gap-1 text-white/60 cursor-pointer hover:text-primary transition-colors">
<MessageCircle size={20} />
<span className="text-[10px]"></span>
</div>
<div className="flex flex-col items-center gap-1 text-white/60 cursor-pointer hover:text-primary transition-colors">
<Star size={20} />
<span className="text-[10px]"></span>
</div>
<Button
className="flex-1 bg-primary text-black font-bold hover:bg-primary/90"
onClick={handlePurchase} // Add click handler
>
(299)
</Button>
</div>
</div>
);
}

67
app/globals.css Normal file
View File

@@ -0,0 +1,67 @@
@import "tailwindcss";
@theme {
--color-background: #050508;
--color-foreground: #ffffff;
--color-primary: #00f0ff;
--color-primary-foreground: #000000;
--color-secondary: #ff0099;
--color-secondary-foreground: #ffffff;
--color-accent: #bd00ff;
--color-accent-foreground: #ffffff;
--color-card: #12121a;
--color-card-foreground: #ffffff;
--color-muted: #1f1f2e;
--color-muted-foreground: #a1a1aa;
--color-border: #2a2a35;
--radius-lg: 0.75rem;
--radius-md: 0.5rem;
--radius-sm: 0.25rem;
--animate-pulse-slow: pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
@layer base {
body {
background-color: var(--color-background);
color: var(--color-foreground);
font-family: var(--font-sans);
-webkit-font-smoothing: antialiased;
}
}
@layer utilities {
.glass {
background-color: rgb(255 255 255 / 0.05);
backdrop-filter: blur(12px);
border: 1px solid rgb(255 255 255 / 0.1);
}
.glass-card {
background-color: rgb(26 26 36 / 0.8);
backdrop-filter: blur(4px);
border: 1px solid rgb(255 255 255 / 0.05);
}
.text-gradient {
background-clip: text;
-webkit-background-clip: text;
color: transparent;
background-image: linear-gradient(to right, var(--color-primary), var(--color-accent), var(--color-secondary));
}
.neon-shadow {
box-shadow: 0 0 10px rgba(0, 240, 255, 0.3), 0 0 20px rgba(0, 240, 255, 0.1);
}
.neon-text {
text-shadow: 0 0 5px rgba(0, 240, 255, 0.5);
}
}

34
app/layout.tsx Normal file
View File

@@ -0,0 +1,34 @@
import type { Metadata } from "next";
import { Inter } from 'next/font/google';
import "./globals.css";
import { MobileNav } from "@/components/layout/mobile-nav";
import { AppProvider } from "@/components/providers/app-provider";
import { Toaster } from "@/components/ui/toaster";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "玩值电竞 - WanZhi Esports",
description: "专注打造比体育明星价值高10倍的电竞俱乐部",
generator: 'v0.app'
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="zh-CN" className="dark">
<body className={`${inter.className} min-h-screen bg-background text-foreground antialiased overflow-x-hidden pb-20`}>
<AppProvider>
<main className="mx-auto max-w-md min-h-screen bg-background relative shadow-2xl overflow-hidden">
{children}
<MobileNav />
<Toaster />
</main>
</AppProvider>
</body>
</html>
);
}

3
app/loading.tsx Normal file
View File

@@ -0,0 +1,3 @@
export default function Loading() {
return null
}

235
app/mall/page.tsx Normal file
View File

@@ -0,0 +1,235 @@
"use client"
import {
ShoppingBag,
Diamond,
Crown,
Sparkles,
Zap,
Gift,
Coins,
BookOpen,
GraduationCap,
Monitor,
Hotel,
Gamepad2,
MapPin,
Coffee,
} from "lucide-react"
import { Button } from "@/components/ui/button"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { useAppContext } from "@/components/providers/app-provider"
import { toast } from "@/hooks/use-toast"
export default function MallPage() {
const { wallet, pay, recharge } = useAppContext()
const handleBuy = (price: number, currency: "diamonds" | "coins", name: string, type: "product" | "service") => {
pay(price, currency, name, type)
}
return (
<div className="min-h-screen pb-24 bg-background">
{/* Header */}
<header className="sticky top-0 z-40 glass px-4 py-3 flex items-center justify-between">
<h1 className="font-bold text-lg"></h1>
<div className="flex items-center gap-3">
<div className="flex items-center gap-2 px-3 py-1 rounded-full bg-white/5 border border-white/10">
<Diamond size={14} className="text-primary" />
<span className="text-xs font-bold">{wallet.diamonds}</span>
</div>
<div className="flex items-center gap-2 px-3 py-1 rounded-full bg-white/5 border border-white/10">
<Coins size={14} className="text-yellow-500" />
<span className="text-xs font-bold">{wallet.coins}</span>
</div>
</div>
</header>
{/* Banner */}
<div className="px-4 mt-4">
<div className="w-full h-32 rounded-2xl bg-gradient-to-r from-purple-600 to-blue-600 relative overflow-hidden flex items-center px-6">
<div className="relative z-10">
<h2 className="text-xl font-bold text-white mb-1"></h2>
<p className="text-xs text-white/80 mb-3"> </p>
<Button
size="sm"
className="bg-white text-purple-600 hover:bg-white/90 h-7 text-xs font-bold"
onClick={() => recharge(100)}
>
</Button>
</div>
<div className="absolute right-0 bottom-0 w-32 h-32 opacity-50">
<Diamond className="w-full h-full text-white/20 rotate-12 translate-x-8 translate-y-8" />
</div>
</div>
</div>
{/* Top Categories - Expanded as requested */}
<div className="px-4 mt-6">
<h3 className="text-sm font-bold mb-3 flex items-center gap-2">
<Sparkles size={16} className="text-yellow-400" />
</h3>
<div className="grid grid-cols-4 gap-3">
{[
{ name: "知识付费", icon: BookOpen, color: "text-blue-400", bg: "bg-blue-500/10", desc: "大神课程" },
{ name: "陪玩课程", icon: GraduationCap, color: "text-pink-400", bg: "bg-pink-500/10", desc: "专业指导" },
{ name: "电竞外设", icon: Monitor, color: "text-purple-400", bg: "bg-purple-500/10", desc: "职业装备" },
{ name: "电竞酒店", icon: Hotel, color: "text-orange-400", bg: "bg-orange-500/10", desc: "订阅特权" },
{ name: "PK电竞", icon: Gamepad2, color: "text-red-400", bg: "bg-red-500/10", desc: "竞技对战" },
{ name: "欢乐之路", icon: MapPin, color: "text-green-400", bg: "bg-green-500/10", desc: "趣味闯关" },
{ name: "网咖特权", icon: Coffee, color: "text-cyan-400", bg: "bg-cyan-500/10", desc: "上网优惠" },
{ name: "更多服务", icon: Sparkles, color: "text-yellow-400", bg: "bg-yellow-500/10", desc: "敬请期待" },
].map((item, i) => (
<div
key={i}
className="glass-card p-2 rounded-xl flex flex-col items-center gap-2 cursor-pointer hover:bg-white/10 transition-colors"
onClick={() => toast({ title: "即将开放", description: `${item.name}板块正在筹备中` })}
>
<div className={`w-10 h-10 rounded-full ${item.bg} flex items-center justify-center`}>
<item.icon className={item.color} size={20} />
</div>
<div className="text-center">
<h3 className="text-[10px] font-bold whitespace-nowrap">{item.name}</h3>
</div>
</div>
))}
</div>
</div>
<Tabs defaultValue="dress" className="mt-6">
<div className="px-4">
<TabsList className="w-full bg-white/5 p-1 rounded-xl">
<TabsTrigger value="dress" className="flex-1 text-xs">
</TabsTrigger>
<TabsTrigger value="gift" className="flex-1 text-xs">
</TabsTrigger>
<TabsTrigger value="exchange" className="flex-1 text-xs">
</TabsTrigger>
</TabsList>
</div>
<TabsContent value="dress" className="mt-4">
{/* Categories */}
<div className="flex items-center gap-4 px-4 overflow-x-auto no-scrollbar mb-4">
{["推荐", "头像框", "聊天气泡", "入场特效", "座驾"].map((cat, i) => (
<button
key={i}
className={`whitespace-nowrap text-xs font-medium px-3 py-1.5 rounded-full transition-colors ${i === 0 ? "bg-primary text-white" : "bg-white/5 text-white/50"}`}
>
{cat}
</button>
))}
</div>
{/* Grid */}
<div className="px-4 grid grid-cols-2 gap-4">
{[
{ name: "巫瞻猫影", price: 99, type: "frame", color: "from-purple-500/20 to-blue-500/20" },
{ name: "椰树风情", price: 128, type: "frame", color: "from-green-500/20 to-yellow-500/20" },
{ name: "黑白天使", price: 299, type: "wing", color: "from-gray-500/20 to-white/20" },
{ name: "敦煌飞天", price: 520, type: "ring", color: "from-orange-500/20 to-red-500/20" },
{ name: "恶魔之眼", price: 199, type: "frame", color: "from-red-900/20 to-black/20" },
{ name: "小熊猫", price: 68, type: "frame", color: "from-orange-300/20 to-yellow-300/20" },
].map((item, i) => (
<div
key={i}
className="glass-card rounded-xl p-4 flex flex-col items-center gap-3 group relative overflow-hidden"
>
<div
className={`absolute inset-0 bg-gradient-to-br ${item.color} opacity-0 group-hover:opacity-100 transition-opacity duration-500`}
/>
<div className="relative w-20 h-20 flex items-center justify-center">
<div className="w-16 h-16 rounded-full border-2 border-dashed border-white/20 flex items-center justify-center relative z-10">
<Sparkles className="text-white/20" />
</div>
<div className="absolute inset-0 rounded-full border-4 border-primary/30 blur-md animate-pulse-slow" />
</div>
<div className="text-center relative z-10">
<h3 className="font-bold text-sm mb-1">{item.name}</h3>
<div className="flex items-center justify-center gap-1 text-primary">
<Diamond size={12} />
<span className="text-xs font-bold">{item.price}</span>
</div>
</div>
<button
className="w-full py-2 mt-2 rounded-lg bg-white/5 hover:bg-primary hover:text-black transition-all text-xs font-bold relative z-10"
onClick={() => handleBuy(item.price, "diamonds", item.name, "product")}
>
</button>
</div>
))}
</div>
</TabsContent>
<TabsContent value="gift" className="mt-4 px-4">
<div className="grid grid-cols-3 gap-3">
{[
{ name: "棒棒糖", price: 10, icon: "🍭" },
{ name: "告白气球", price: 520, icon: "🎈" },
{ name: "跑车", price: 5000, icon: "🏎️" },
{ name: "火箭", price: 10000, icon: "🚀" },
{ name: "城堡", price: 50000, icon: "🏰" },
{ name: "爱心", price: 1, icon: "❤️" },
].map((item, i) => (
<div
key={i}
className="glass-card rounded-xl p-3 flex flex-col items-center gap-2 hover:bg-white/10 cursor-pointer transition-colors"
onClick={() => handleBuy(item.price, "diamonds", item.name, "product")}
>
<div className="text-3xl">{item.icon}</div>
<div className="text-center">
<h3 className="text-xs font-bold">{item.name}</h3>
<div className="flex items-center justify-center gap-1 text-primary mt-1">
<Diamond size={10} />
<span className="text-[10px]">{item.price}</span>
</div>
</div>
</div>
))}
</div>
</TabsContent>
<TabsContent value="exchange" className="mt-4 px-4">
<div className="grid grid-cols-2 gap-4">
{[
{ name: "10元话费券", price: 1000, icon: Gift },
{ name: "随机皮肤宝箱", price: 5000, icon: ShoppingBag },
{ name: "改名卡", price: 2000, icon: Crown },
{ name: "双倍经验卡", price: 500, icon: Zap },
].map((item, i) => (
<div key={i} className="glass-card rounded-xl p-4 flex flex-col items-center gap-3">
<div className="w-12 h-12 rounded-full bg-yellow-500/10 flex items-center justify-center">
<item.icon className="text-yellow-500" size={24} />
</div>
<div className="text-center">
<h3 className="font-bold text-sm mb-1">{item.name}</h3>
<div className="flex items-center justify-center gap-1 text-yellow-500">
<Coins size={12} />
<span className="text-xs font-bold">{item.price}</span>
</div>
</div>
<Button
size="sm"
variant="outline"
className="w-full h-7 text-xs border-yellow-500/20 text-yellow-500 hover:bg-yellow-500/10 bg-transparent"
onClick={() => handleBuy(item.price, "coins", item.name, "product")}
>
</Button>
</div>
))}
</div>
</TabsContent>
</Tabs>
</div>
)
}

130
app/moments/[id]/page.tsx Normal file
View File

@@ -0,0 +1,130 @@
"use client" // Convert to client component
import { Heart, MessageCircle, Share2, Gift, ArrowLeft, Send, BookOpen } from 'lucide-react';
import Image from "next/image";
import Link from "next/link";
import { toast } from "@/hooks/use-toast"; // Import toast
// Mock data - in a real app this would come from an API based on params.id
const momentData = {
id: 1,
user: "GM-远洋",
avatar: "/gamer-girl-headphones.jpg",
desc: "可惜不是你喜欢的甜妹音 😷 #电竞少女 #英雄联盟 #日常",
video: "/esports-gamer-girl-streaming-vertical.jpg",
likes: "1.2w",
comments: 458,
isLive: true,
hasCourse: true,
courseId: "101",
courseTitle: "打野进阶课:从入门到王者",
commentsList: [
{ user: "User1", text: "声音好听!", time: "1m ago" },
{ user: "User2", text: "求BGM", time: "5m ago" },
{ user: "User3", text: "这波操作666", time: "10m ago" },
]
};
export default function MomentDetailPage({ params }: { params: { id: string } }) {
const handleLike = () => toast({ title: "点赞成功", description: "已添加到我喜欢的视频" });
const handleShare = () => toast({ title: "分享成功", description: "链接已复制到剪贴板" });
const handleGift = () => toast({ title: "礼物发送成功", description: "主播已收到您的心意" });
return (
<div className="h-screen bg-black text-white relative flex flex-col">
{/* Header */}
<div className="absolute top-0 left-0 right-0 z-30 pt-safe px-4 py-3 flex items-center gap-4 bg-gradient-to-b from-black/60 to-transparent">
<Link href="/moments" className="p-2 -ml-2 rounded-full hover:bg-white/10 transition-colors">
<ArrowLeft size={24} />
</Link>
<div className="flex items-center gap-2">
<div className="w-8 h-8 rounded-full overflow-hidden border border-white/20">
<Image src={momentData.avatar || "/placeholder.svg"} alt={momentData.user} width={32} height={32} className="object-cover" />
</div>
<span className="font-bold text-sm">{momentData.user}</span>
<button className="px-3 py-1 rounded-full bg-primary text-black text-xs font-bold"></button>
</div>
</div>
{/* Video Area */}
<div className="flex-1 relative bg-zinc-900">
<Image
src={momentData.video || "/placeholder.svg"}
alt="Video"
fill
className="object-cover opacity-90"
/>
{/* Right Sidebar Actions */}
<div className="absolute right-2 bottom-32 flex flex-col items-center gap-6 z-20">
<button
className="flex flex-col items-center gap-1 group"
onClick={handleLike} // Add click handler
>
<Heart size={32} className="text-white drop-shadow-lg group-hover:text-red-500 transition-colors" />
<span className="text-xs font-medium drop-shadow-md">{momentData.likes}</span>
</button>
<button className="flex flex-col items-center gap-1 group">
<MessageCircle size={32} className="text-white drop-shadow-lg group-hover:text-primary transition-colors" />
<span className="text-xs font-medium drop-shadow-md">{momentData.comments}</span>
</button>
<button
className="flex flex-col items-center gap-1 group"
onClick={handleShare} // Add click handler
>
<Share2 size={32} className="text-white drop-shadow-lg group-hover:text-green-400 transition-colors" />
<span className="text-xs font-medium drop-shadow-md"></span>
</button>
<button
className="flex flex-col items-center gap-1 group"
onClick={handleGift} // Add click handler
>
<div className="w-10 h-10 rounded-full bg-gradient-to-br from-secondary to-accent flex items-center justify-center animate-spin-slow group-hover:scale-110 transition-transform">
<Gift size={20} />
</div>
<span className="text-xs font-medium text-secondary drop-shadow-md"></span>
</button>
</div>
{/* Bottom Info */}
<div className="absolute left-0 bottom-0 w-full p-4 z-20 bg-gradient-to-t from-black via-black/60 to-transparent pt-20">
{/* Course Link */}
{momentData.hasCourse && (
<Link href={`/course/${momentData.courseId}`} className="flex items-center justify-between w-full p-3 rounded-xl bg-white/10 backdrop-blur-md border border-white/10 mb-4 hover:bg-white/20 transition-colors group">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-primary/20 flex items-center justify-center text-primary">
<BookOpen size={20} />
</div>
<div>
<div className="text-xs text-primary font-bold mb-0.5"></div>
<div className="text-sm font-bold text-white group-hover:text-primary transition-colors">{momentData.courseTitle}</div>
</div>
</div>
<div className="px-3 py-1.5 rounded-lg bg-primary text-black text-xs font-bold">
</div>
</Link>
)}
<h3 className="font-bold text-lg mb-2">@{momentData.user}</h3>
<p className="text-sm text-white/90 leading-relaxed mb-4">
{momentData.desc}
</p>
{/* Comment Input Mock */}
<div className="flex items-center gap-3">
<div className="flex-1 h-10 bg-white/10 rounded-full px-4 flex items-center text-white/50 text-sm">
...
</div>
<button className="p-2 rounded-full bg-white/10 hover:bg-primary hover:text-black transition-colors">
<Send size={20} />
</button>
</div>
</div>
</div>
</div>
);
}

171
app/moments/page.tsx Normal file
View File

@@ -0,0 +1,171 @@
"use client" // Convert to client component
import { Heart, MessageCircle, Share2, Gift, Camera, Play, BookOpen } from "lucide-react"
import Image from "next/image"
import Link from "next/link"
import { toast } from "@/hooks/use-toast" // Import toast
const moments = [
{
id: 1,
user: "GM-远洋",
avatar: "/gamer-girl-headphones.jpg",
desc: "可惜不是你喜欢的甜妹音 😷 #电竞少女 #英雄联盟 #日常",
video: "/esports-gamer-girl-streaming-vertical.jpg",
likes: "1.2w",
comments: 458,
isLive: true,
hasCourse: true,
courseTitle: "打野进阶课",
},
{
id: 2,
user: "卡若",
avatar: "/gamer-boy-esports-jersey.jpg",
desc: "中单刺客教学,这波操作你学会了吗? #英雄联盟 #教学",
video: "/esports-tournament-stadium-neon-lights-crowd.jpg",
likes: "8.5k",
comments: 230,
isLive: false,
hasCourse: true,
courseTitle: "中单刺客通关指南",
},
{
id: 3,
user: "魔兽老张",
avatar: "/placeholder.svg?key=wow",
desc: "ICC 25人H 教授打法细节讲解 #魔兽世界 #WLK",
video: "/wow-bg.jpg",
likes: "3.2k",
comments: 120,
isLive: false,
hasCourse: false,
},
]
export default function MomentsPage() {
const handleLike = () => toast({ title: "点赞成功", description: "已添加到我喜欢的视频" })
const handleShare = () => toast({ title: "分享成功", description: "链接已复制到剪贴板" })
const handleGift = () => toast({ title: "礼物发送成功", description: "主播已收到您的心意" })
return (
<div className="min-h-screen bg-black text-white pb-20">
{/* Top Overlay */}
<div className="sticky top-0 z-30 pt-safe px-4 py-3 flex justify-between items-center bg-black/80 backdrop-blur-md">
<div className="flex gap-4 text-lg font-medium">
<span
className="text-white/60 cursor-pointer hover:text-white"
onClick={() => toast({ title: "关注列表", description: "暂无关注的主播" })}
>
</span>
<span className="text-white font-bold border-b-2 border-primary pb-1 cursor-pointer"></span>{" "}
{/* Renamed from 推荐 to 视频推荐 */}
<span
className="text-white/60 cursor-pointer hover:text-white"
onClick={() => toast({ title: "最新视频", description: "已为您刷新最新内容" })}
>
</span>
</div>
<Camera className="text-white/80 cursor-pointer hover:text-primary transition-colors" />
</div>
<div className="flex flex-col gap-1">
{moments.map((moment) => (
<div key={moment.id} className="relative w-full aspect-[9/16] bg-zinc-900 shrink-0 snap-start">
<Image src={moment.video || "/placeholder.svg"} alt="Stream" fill className="object-cover opacity-80" />
{/* Play Button Overlay (Mock) */}
<div className="absolute inset-0 flex items-center justify-center pointer-events-none">
<Play size={48} className="text-white/30 fill-white/30" />
</div>
{/* Right Sidebar Actions */}
<div className="absolute right-2 bottom-24 flex flex-col items-center gap-6 z-20">
<Link href={`/star/${moment.id}`} className="relative cursor-pointer">
<div className="w-10 h-10 rounded-full border-2 border-white overflow-hidden">
<Image
src={moment.avatar || "/placeholder.svg"}
alt={moment.user}
width={40}
height={40}
className="object-cover"
/>
</div>
<div className="absolute -bottom-2 left-1/2 -translate-x-1/2 w-4 h-4 bg-secondary rounded-full flex items-center justify-center text-[10px] font-bold">
+
</div>
</Link>
<button
className="flex flex-col items-center gap-1 group"
onClick={handleLike} // Add click handler
>
<Heart size={28} className="text-white drop-shadow-lg group-hover:text-red-500 transition-colors" />
<span className="text-xs font-medium drop-shadow-md">{moment.likes}</span>
</button>
<Link href={`/moments/${moment.id}`} className="flex flex-col items-center gap-1 group">
<MessageCircle
size={28}
className="text-white drop-shadow-lg group-hover:text-primary transition-colors"
/>
<span className="text-xs font-medium drop-shadow-md">{moment.comments}</span>
</Link>
<button
className="flex flex-col items-center gap-1 group"
onClick={handleShare} // Add click handler
>
<Share2 size={28} className="text-white drop-shadow-lg group-hover:text-green-400 transition-colors" />
<span className="text-xs font-medium drop-shadow-md"></span>
</button>
<button
className="flex flex-col items-center gap-1 group"
onClick={handleGift} // Add click handler
>
<div className="w-10 h-10 rounded-full bg-gradient-to-br from-secondary to-accent flex items-center justify-center animate-spin-slow group-hover:scale-110 transition-transform">
<Gift size={20} />
</div>
<span className="text-xs font-medium text-secondary drop-shadow-md"></span>
</button>
{moment.hasCourse && (
<Link href={`/course/${moment.id}`} className="flex flex-col items-center gap-1 group">
<div className="w-10 h-10 rounded-full bg-gradient-to-br from-blue-500 to-cyan-500 flex items-center justify-center group-hover:scale-110 transition-transform">
<BookOpen size={20} className="text-white" />
</div>
<span className="text-xs font-medium text-blue-400 drop-shadow-md"></span>
</Link>
)}
</div>
{/* Bottom Info */}
<div className="absolute left-0 bottom-10 w-3/4 p-4 z-20">
<div className="flex items-center gap-2 mb-2">
<span className="bg-white/20 backdrop-blur-md px-2 py-0.5 rounded text-[10px]"></span>
<Link href={`/star/${moment.id}`} className="font-bold text-shadow-sm hover:underline">
@{moment.user}
</Link>
</div>
<p className="text-sm text-white/90 leading-relaxed text-shadow-sm line-clamp-2">{moment.desc}</p>
{moment.isLive && (
<div className="mt-2 flex items-center gap-2 text-xs text-white/60">
<span className="w-3 h-3 bg-primary/50 rounded-full animate-pulse" />
<span></span>
</div>
)}
</div>
{/* Gradient Overlay */}
<div className="absolute inset-x-0 bottom-0 h-64 bg-gradient-to-t from-black/90 via-black/40 to-transparent pointer-events-none" />
</div>
))}
<div className="py-8 text-center text-xs text-white/30"></div>
</div>
</div>
)
}

70
app/my-courses/page.tsx Normal file
View File

@@ -0,0 +1,70 @@
"use client"
import { ArrowLeft, PlayCircle, Clock, BookOpen } from "lucide-react"
import { useRouter } from "next/navigation"
import Image from "next/image"
import { Button } from "@/components/ui/button"
import { useAppContext } from "@/components/providers/app-provider"
export default function MyCoursesPage() {
const router = useRouter()
const { orders } = useAppContext()
// Filter orders for courses (assuming type 'product' or specific title match for now as mock)
// In a real app, we'd have a specific 'course' type
const courses = orders.filter((o) => o.title.includes("课程") || o.title.includes("教学"))
return (
<div className="min-h-screen bg-background pb-24">
<header className="sticky top-0 z-40 glass px-4 py-3 flex items-center gap-4">
<button onClick={() => router.back()} className="p-2 hover:bg-white/10 rounded-full transition-colors">
<ArrowLeft size={20} />
</button>
<h1 className="font-bold text-lg"></h1>
</header>
<div className="p-4 space-y-4">
{courses.length > 0 ? (
courses.map((course, i) => (
<div key={i} className="glass-card p-3 rounded-xl flex gap-3">
<div className="relative w-24 h-16 rounded-lg overflow-hidden flex-shrink-0 bg-muted">
<Image src={course.image || "/placeholder.svg"} alt={course.title} fill className="object-cover" />
<div className="absolute inset-0 flex items-center justify-center bg-black/30">
<PlayCircle size={24} className="text-white/80" />
</div>
</div>
<div className="flex-1 flex flex-col justify-between">
<h3 className="font-bold text-sm line-clamp-2">{course.title}</h3>
<div className="flex items-center justify-between mt-2">
<div className="flex items-center gap-2 text-[10px] text-white/50">
<span className="flex items-center gap-1">
<Clock size={10} /> 45
</span>
<span className="flex items-center gap-1">
<BookOpen size={10} /> 3/12
</span>
</div>
<Button size="sm" className="h-6 text-xs px-3">
</Button>
</div>
</div>
</div>
))
) : (
<div className="flex flex-col items-center justify-center py-20 text-white/50">
<BookOpen size={48} className="mb-4 opacity-20" />
<p></p>
<Button
variant="outline"
className="mt-4 border-white/10 hover:bg-white/5 bg-transparent"
onClick={() => router.push("/mall")}
>
</Button>
</div>
)}
</div>
</div>
)
}

146
app/orders/page.tsx Normal file
View File

@@ -0,0 +1,146 @@
"use client"
import { Button } from "@/components/ui/button"
import { ArrowLeft, ShoppingBag, Clock, CheckCircle2, XCircle } from "lucide-react"
import { useRouter } from "next/navigation"
import Image from "next/image"
import { useAppContext } from "@/components/providers/app-provider"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
export default function OrdersPage() {
const router = useRouter()
const { orders } = useAppContext()
const getStatusColor = (status: string) => {
switch (status) {
case "completed":
return "text-green-500"
case "pending":
return "text-yellow-500"
case "cancelled":
return "text-red-500"
default:
return "text-white/50"
}
}
const getStatusIcon = (status: string) => {
switch (status) {
case "completed":
return <CheckCircle2 size={14} />
case "pending":
return <Clock size={14} />
case "cancelled":
return <XCircle size={14} />
default:
return null
}
}
const getStatusText = (status: string) => {
switch (status) {
case "completed":
return "已完成"
case "pending":
return "进行中"
case "cancelled":
return "已取消"
default:
return status
}
}
return (
<div className="min-h-screen bg-background pb-24">
<header className="sticky top-0 z-40 glass px-4 py-3 flex items-center gap-4">
<button onClick={() => router.back()} className="p-2 hover:bg-white/10 rounded-full transition-colors">
<ArrowLeft size={20} />
</button>
<h1 className="font-bold text-lg"></h1>
</header>
<Tabs defaultValue="all" className="w-full mt-2">
<div className="px-4">
<TabsList className="w-full bg-white/5 p-1 rounded-xl">
<TabsTrigger value="all" className="flex-1 text-xs">
</TabsTrigger>
<TabsTrigger value="service" className="flex-1 text-xs">
</TabsTrigger>
<TabsTrigger value="product" className="flex-1 text-xs">
</TabsTrigger>
</TabsList>
</div>
{["all", "service", "product"].map((tab) => (
<TabsContent key={tab} value={tab} className="px-4 mt-4 space-y-3">
{orders.filter((o) => tab === "all" || o.type === tab).length > 0 ? (
orders
.filter((o) => tab === "all" || o.type === tab)
.map((order, i) => (
<div key={i} className="glass-card p-4 rounded-xl">
<div className="flex justify-between items-start mb-3">
<div className="flex items-center gap-2">
<span className="text-xs text-white/50">{order.date}</span>
<span className="text-[10px] px-1.5 py-0.5 rounded bg-white/5 text-white/70 border border-white/10">
{order.type === "service" ? "陪玩服务" : "商城商品"}
</span>
</div>
<div className={`flex items-center gap-1 text-xs font-medium ${getStatusColor(order.status)}`}>
{getStatusIcon(order.status)}
{getStatusText(order.status)}
</div>
</div>
<div className="flex gap-3">
<div className="relative w-16 h-16 rounded-lg overflow-hidden bg-muted flex-shrink-0">
<Image
src={order.image || "/placeholder.svg"}
alt={order.title}
fill
className="object-cover"
/>
</div>
<div className="flex-1">
<h3 className="font-bold text-sm mb-1">{order.title}</h3>
<div className="flex items-center justify-between mt-2">
<span className="text-xs text-white/50">数量: 1</span>
<div className="flex items-center gap-1">
<span className="text-xs font-bold">{order.price}</span>
<span className="text-[10px] text-white/50">
{order.currency === "diamonds" ? "钻石" : "丸子币"}
</span>
</div>
</div>
</div>
</div>
<div className="mt-3 pt-3 border-t border-white/5 flex justify-end gap-2">
<Button
variant="outline"
size="sm"
className="h-7 text-xs border-white/10 hover:bg-white/5 bg-transparent"
>
</Button>
<Button size="sm" className="h-7 text-xs bg-white/10 hover:bg-white/20 border border-white/10">
</Button>
</div>
</div>
))
) : (
<div className="flex flex-col items-center justify-center py-20 text-white/50">
<ShoppingBag size={48} className="mb-4 opacity-20" />
<p></p>
</div>
)}
</TabsContent>
))}
</Tabs>
</div>
)
}

260
app/page.tsx Normal file
View File

@@ -0,0 +1,260 @@
"use client" // Convert to client component
import { Search, Bell, Flame, Trophy, Users, Star, Sword } from "lucide-react"
import Image from "next/image"
import Link from "next/link"
import { toast } from "@/hooks/use-toast" // Import toast
export default function HomePage() {
const handleJoinTeam = () => {
toast({
title: "申请已发送",
description: "车队队长稍后会联系您",
})
}
return (
<div className="flex flex-col min-h-screen pb-24">
{/* Header */}
<header className="sticky top-0 z-40 glass px-4 py-3 flex items-center justify-between">
<div className="flex items-center gap-2">
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-primary to-accent flex items-center justify-center">
<span className="font-bold text-black text-xs">WZ</span>
</div>
<h1 className="font-bold text-lg tracking-tight"></h1>
</div>
<div className="flex items-center gap-3">
<button className="p-2 rounded-full bg-white/5 hover:bg-white/10 transition-colors">
<Search size={20} className="text-white/80" />
</button>
<button className="p-2 rounded-full bg-white/5 hover:bg-white/10 transition-colors relative">
<Bell size={20} className="text-white/80" />
<span className="absolute top-1.5 right-1.5 w-2 h-2 bg-secondary rounded-full border border-background" />
</button>
</div>
</header>
<div className="p-4 space-y-6">
{/* Hero Banner */}
<div className="relative w-full aspect-[16/9] rounded-2xl overflow-hidden group">
<Image
src="/esports-tournament-stadium-neon-lights-crowd.jpg"
alt="Tournament"
fill
className="object-cover transition-transform duration-700 group-hover:scale-105"
/>
<div className="absolute inset-0 bg-gradient-to-t from-background via-transparent to-transparent" />
<div className="absolute bottom-0 left-0 p-4 w-full">
<div className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full bg-secondary/80 backdrop-blur-md text-[10px] font-bold mb-2">
<span className="w-1.5 h-1.5 rounded-full bg-white animate-pulse" />
LIVE
</div>
<h2 className="text-xl font-bold leading-tight mb-1">2025 </h2>
<p className="text-sm text-white/70"></p>
</div>
</div>
{/* Game Icons */}
<div className="grid grid-cols-5 gap-3">
{[
{ name: "英雄联盟", icon: "LoL" },
{ name: "无畏契约", icon: "Val" },
{ name: "王者荣耀", icon: "HoK" },
{ name: "CS:GO", icon: "CS" },
{ name: "魔兽世界", icon: "WoW" },
].map((game, i) => (
<div key={i} className="flex flex-col items-center gap-2 group cursor-pointer">
<div className="w-12 h-12 rounded-xl glass-card flex items-center justify-center border border-white/5 group-hover:border-primary/50 transition-colors relative overflow-hidden">
<div className="absolute inset-0 bg-gradient-to-br from-primary/10 to-transparent opacity-0 group-hover:opacity-100 transition-opacity" />
<span className="text-xs font-bold text-white/80 group-hover:text-primary transition-colors">
{game.icon}
</span>
</div>
<span className="text-[10px] text-white/60 group-hover:text-white transition-colors">{game.name}</span>
</div>
))}
</div>
{/* Industry Recommendations */}
<section className="space-y-3">
<div className="flex items-center justify-between">
<h3 className="font-bold text-lg flex items-center gap-2">
<Flame size={18} className="text-red-500" />
</h3>
</div>
<div className="grid grid-cols-2 gap-3">
<div
className="glass-card p-3 rounded-xl border-l-2 border-purple-500/50 relative overflow-hidden group cursor-pointer"
onClick={() => toast({ title: "即将开放", description: "电竞酒店预订功能即将上线" })}
>
<div className="absolute right-0 top-0 p-2 opacity-10 group-hover:opacity-20 transition-opacity">
<Trophy size={40} />
</div>
<h4 className="font-bold text-sm text-purple-100"></h4>
<p className="text-[10px] text-white/60 mt-1"> </p>
</div>
<div
className="glass-card p-3 rounded-xl border-l-2 border-green-500/50 relative overflow-hidden group cursor-pointer"
onClick={() => toast({ title: "即将开放", description: "网咖预订功能即将上线" })}
>
<div className="absolute right-0 top-0 p-2 opacity-10 group-hover:opacity-20 transition-opacity">
<Users size={40} />
</div>
<h4 className="font-bold text-sm text-green-100"></h4>
<p className="text-[10px] text-white/60 mt-1"> </p>
</div>
</div>
</section>
{/* Club Recommendations */}
<section>
<div className="flex items-center justify-between mb-3">
<h3 className="font-bold text-lg flex items-center gap-2">
<Trophy size={18} className="text-primary" />
</h3>
<Link href="#" className="text-xs text-white/50 hover:text-primary transition-colors">
</Link>
</div>
<div className="space-y-3">
{[
{ name: "洲越电竞", status: "即将开业", id: "50310", members: 26, tags: ["三角洲", "无畏契约"] },
{ name: "机枪电竞", status: "火热招募", id: "50130", members: 60, tags: ["LOL", "云顶"] },
{ name: "东东电竞", status: "官方自营", id: "88888", members: 1205, tags: ["全能", "陪玩"] },
].map((club, i) => (
<div
key={i}
className="glass-card p-3 rounded-xl flex items-center gap-3 hover:bg-white/5 transition-colors cursor-pointer"
>
<div className="w-14 h-14 rounded-lg bg-muted relative overflow-hidden shrink-0">
<Image
src={`/esports-logo.png?height=100&width=100&query=esports logo ${i}`}
alt={club.name}
fill
className="object-cover"
/>
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between mb-1">
<h4 className="font-bold truncate">{club.name}</h4>
<div className="flex -space-x-1">
{club.tags.map((tag, j) => (
<div
key={j}
className="w-5 h-5 rounded-full bg-muted border border-card flex items-center justify-center text-[8px] overflow-hidden"
>
{tag[0]}
</div>
))}
</div>
</div>
<p className="text-xs text-white/50 mb-2">{club.status}</p>
<div className="flex items-center gap-3 text-[10px] text-white/40">
<span className="flex items-center gap-1">
<span className="font-mono">ID:</span> {club.id}
</span>
<span className="flex items-center gap-1">
<Users size={10} /> {club.members}
</span>
</div>
</div>
</div>
))}
</div>
</section>
{/* Star Players / Streamers */}
<section>
<div className="flex items-center justify-between mb-3">
<h3 className="font-bold text-lg flex items-center gap-2">
<Star size={18} className="text-secondary" />
</h3>
<Link href="#" className="text-xs text-white/50 hover:text-secondary transition-colors">
</Link>
</div>
<div className="grid grid-cols-2 gap-3">
{[
{ id: "1", name: "GM-远洋", title: "国服第一盲僧", price: "50币/局", image: "gamer girl headphones" },
{ id: "2", name: "卡若", title: "职业战队退役", price: "80币/局", image: "gamer boy esports jersey" },
{ id: "3", name: "小柠檬", title: "人皮话多", price: "30币/局", image: "cute gamer girl" },
{ id: "4", name: "阿伟", title: "绝地求生刚枪王", price: "60币/局", image: "fps gamer focus" },
{ id: "5", name: "七七", title: "甜美声优", price: "40币/局", image: "anime style gamer" },
{ id: "6", name: "大魔王", title: "中单法王", price: "100币/局", image: "pro esports player" },
].map((star, i) => (
<Link
key={i}
href={`/star/${star.id}`}
className="glass-card rounded-xl overflow-hidden group cursor-pointer"
>
<div className="relative aspect-[3/4]">
<Image
src={`/.jpg?key=r1ncc&height=400&width=300&query=${star.image}`}
alt={star.name}
fill
className="object-cover transition-transform duration-500 group-hover:scale-110"
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/90 via-transparent to-transparent" />
<div className="absolute bottom-0 left-0 w-full p-3">
<h4 className="font-bold text-sm">{star.name}</h4>
<p className="text-[10px] text-white/70 mb-1">{star.title}</p>
<div className="flex items-center justify-between">
<span className="text-xs font-bold text-primary">{star.price}</span>
<button className="w-6 h-6 rounded-full bg-secondary flex items-center justify-center">
<Flame size={12} className="text-white" />
</button>
</div>
</div>
</div>
</Link>
))}
</div>
</section>
<section className="relative overflow-hidden rounded-2xl border border-yellow-500/20">
<div className="absolute inset-0 bg-[url('/wow-bg.jpg')] bg-cover bg-center opacity-40" />
<div className="absolute inset-0 bg-gradient-to-r from-black/90 to-transparent" />
<div className="relative p-4">
<div className="flex items-center justify-between mb-3">
<h3 className="font-bold text-lg flex items-center gap-2 text-yellow-500">
<Sword size={18} />
</h3>
<Link href="/wow" className="text-xs text-yellow-500/70 hover:text-yellow-500">
&gt;
</Link>
</div>
<div className="grid grid-cols-2 gap-3">
<div className="glass-card p-3 rounded-xl border-l-2 border-yellow-500/50">
<h4 className="font-bold text-sm text-yellow-100"></h4>
<p className="text-[10px] text-white/60 mt-1">ICC 25H全通团DPS</p>
<button
className="mt-2 w-full py-1 rounded bg-yellow-600/20 text-yellow-500 text-xs font-bold hover:bg-yellow-600/30"
onClick={() => toast({ title: "即将开放", description: "金团招募功能正在内测中" })} // Add click handler
>
</button>
</div>
<div className="glass-card p-3 rounded-xl border-l-2 border-blue-500/50">
<h4 className="font-bold text-sm text-blue-100"></h4>
<p className="text-[10px] text-white/60 mt-1">20</p>
<button
className="mt-2 w-full py-1 rounded bg-blue-600/20 text-blue-500 text-xs font-bold hover:bg-blue-600/30"
onClick={handleJoinTeam} // Add click handler
>
</button>
</div>
</div>
</div>
</section>
</div>
</div>
)
}

253
app/pitch-deck/page.tsx Normal file
View File

@@ -0,0 +1,253 @@
"use client";
import { useState } from "react";
import { ChevronLeft, ChevronRight, TrendingUp, Users, Target, Zap, Award, DollarSign } from 'lucide-react';
import { motion, AnimatePresence } from "framer-motion";
import { cn } from "@/lib/utils";
// Slide Data based on the Business Plan
const slides = [
{
id: 1,
title: "玩值电竞",
subtitle: "专注打造比体育明星价值高10倍的电竞俱乐部",
content: (
<div className="flex flex-col items-center justify-center h-full text-center space-y-6">
<div className="w-32 h-32 rounded-2xl bg-gradient-to-br from-primary to-accent flex items-center justify-center shadow-[0_0_50px_rgba(0,240,255,0.3)]">
<span className="text-4xl font-bold text-black">WZ</span>
</div>
<h1 className="text-4xl font-black tracking-tighter text-transparent bg-clip-text bg-gradient-to-r from-primary via-white to-secondary">
</h1>
<p className="text-xl text-white/80 font-light"><br/><span className="font-bold text-primary"></span></p>
<div className="flex gap-4 mt-8">
<Badge text="14个签约IP" />
<Badge text="46万铁粉" />
<Badge text="10倍价值" />
</div>
</div>
),
bg: "bg-[radial-gradient(ellipse_at_center,_var(--tw-gradient-stops))] from-blue-900/20 via-background to-background"
},
{
id: 2,
title: "市场痛点与机会",
subtitle: "电竞IP价值飞轮",
content: (
<div className="space-y-6">
<div className="grid grid-cols-2 gap-4">
<Card icon={Users} title="粘性更强" desc="比体育明星强10倍的用户粘性" />
<Card icon={Zap} title="变现更多" desc="粉丝价值跃迁,多重变现渠道" />
<Card icon={Target} title="流量更大" desc="线上线下全域流量分发" />
<Card icon={TrendingUp} title="增长更快" desc="C2B2S 裂变增长模型" />
</div>
<div className="p-4 rounded-xl bg-white/5 border border-white/10 mt-4">
<h3 className="font-bold text-primary mb-2"></h3>
<p className="text-sm text-white/70"></p>
</div>
</div>
),
bg: "bg-[radial-gradient(ellipse_at_top_right,_var(--tw-gradient-stops))] from-purple-900/20 via-background to-background"
},
{
id: 3,
title: "商业模式",
subtitle: "全方位价值变现体系",
content: (
<div className="space-y-4">
<ListItem number="01" title="游戏明星经纪" desc="选拔、签约、培养、包装打造顶级电竞IP。" />
<ListItem number="02" title="多元化变现" desc="陪玩服务、知识付费课程、拜师学艺、商业代言。" />
<ListItem number="03" title="电竞赛事运营" desc="举办线上线下赛事,赞助、门票、周边全链条收入。" />
<ListItem number="04" title="私域流量运营" desc="会员体系、粉丝社群、精准营销,沉淀私域资产。" />
</div>
),
bg: "bg-[radial-gradient(ellipse_at_bottom_left,_var(--tw-gradient-stops))] from-pink-900/20 via-background to-background"
},
{
id: 4,
title: "核心优势",
subtitle: "为什么选择玩值电竞?",
content: (
<div className="flex flex-col gap-4">
<div className="flex items-center gap-4 p-4 rounded-xl bg-gradient-to-r from-primary/10 to-transparent border-l-4 border-primary">
<div className="text-3xl font-black text-primary">3</div>
<div>
<h4 className="font-bold"></h4>
<p className="text-xs text-white/60">3AI员工</p>
</div>
</div>
<div className="flex items-center gap-4 p-4 rounded-xl bg-gradient-to-r from-secondary/10 to-transparent border-l-4 border-secondary">
<div className="text-3xl font-black text-secondary">18</div>
<div>
<h4 className="font-bold"></h4>
<p className="text-xs text-white/60"></p>
</div>
</div>
<div className="flex items-center gap-4 p-4 rounded-xl bg-gradient-to-r from-accent/10 to-transparent border-l-4 border-accent">
<div className="text-3xl font-black text-accent">3</div>
<div>
<h4 className="font-bold"></h4>
<p className="text-xs text-white/60"> + AI + </p>
</div>
</div>
</div>
),
bg: "bg-[radial-gradient(ellipse_at_center,_var(--tw-gradient-stops))] from-blue-900/20 via-background to-background"
},
{
id: 5,
title: "未来愿景",
subtitle: "构建全球领先的电竞生态平台",
content: (
<div className="text-center space-y-8 mt-8">
<div className="relative">
<div className="absolute inset-0 bg-gradient-to-r from-primary via-accent to-secondary blur-3xl opacity-20" />
<Award size={80} className="mx-auto text-white relative z-10" />
</div>
<div className="space-y-4">
<h3 className="text-2xl font-bold"></h3>
<p className="text-white/70 px-8">
<br/><br/>
</p>
</div>
<button className="px-8 py-3 rounded-full bg-white text-black font-bold hover:scale-105 transition-transform">
</button>
</div>
),
bg: "bg-[radial-gradient(ellipse_at_top,_var(--tw-gradient-stops))] from-green-900/20 via-background to-background"
}
];
export default function PitchDeckPage() {
const [currentSlide, setCurrentSlide] = useState(0);
const nextSlide = () => {
setCurrentSlide((prev) => (prev + 1) % slides.length);
};
const prevSlide = () => {
setCurrentSlide((prev) => (prev - 1 + slides.length) % slides.length);
};
return (
<div className="h-screen bg-background flex flex-col relative overflow-hidden">
{/* Background Effects */}
<div className={cn("absolute inset-0 transition-colors duration-1000", slides[currentSlide].bg)} />
{/* Header */}
<div className="relative z-10 p-4 flex justify-between items-center glass border-b border-white/5">
<h1 className="font-bold text-sm tracking-widest uppercase text-white/50">Business Plan 2025</h1>
<div className="text-xs font-mono text-primary">
{currentSlide + 1} / {slides.length}
</div>
</div>
{/* Slide Content */}
<div className="flex-1 relative z-10 flex flex-col p-6 overflow-hidden">
<AnimatePresence mode="wait">
<motion.div
key={currentSlide}
initial={{ opacity: 0, x: 50 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -50 }}
transition={{ duration: 0.4, ease: "easeOut" }}
className="flex-1 flex flex-col"
>
<div className="mb-8">
<motion.h2
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
className="text-3xl font-bold mb-2"
>
{slides[currentSlide].title}
</motion.h2>
<motion.p
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.3 }}
className="text-primary font-medium"
>
{slides[currentSlide].subtitle}
</motion.p>
</div>
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ delay: 0.4 }}
className="flex-1"
>
{slides[currentSlide].content}
</motion.div>
</motion.div>
</AnimatePresence>
</div>
{/* Controls */}
<div className="relative z-10 p-6 pb-24 flex items-center justify-between gap-4">
<button
onClick={prevSlide}
className="w-12 h-12 rounded-full glass flex items-center justify-center hover:bg-white/10 active:scale-95 transition-all"
>
<ChevronLeft />
</button>
<div className="flex gap-2">
{slides.map((_, i) => (
<div
key={i}
className={cn(
"w-2 h-2 rounded-full transition-all duration-300",
i === currentSlide ? "w-6 bg-primary" : "bg-white/20"
)}
/>
))}
</div>
<button
onClick={nextSlide}
className="w-12 h-12 rounded-full bg-primary text-black flex items-center justify-center hover:bg-primary/90 active:scale-95 transition-all shadow-[0_0_20px_rgba(0,240,255,0.4)]"
>
<ChevronRight />
</button>
</div>
</div>
);
}
// Helper Components for Slides
function Badge({ text }: { text: string }) {
return (
<span className="px-3 py-1 rounded-full bg-white/10 border border-white/10 text-xs font-medium backdrop-blur-md">
{text}
</span>
);
}
function Card({ icon: Icon, title, desc }: { icon: any, title: string, desc: string }) {
return (
<div className="p-4 rounded-xl bg-white/5 border border-white/5 hover:border-primary/30 transition-colors">
<Icon className="text-primary mb-2" size={24} />
<h4 className="font-bold text-sm mb-1">{title}</h4>
<p className="text-[10px] text-white/60 leading-tight">{desc}</p>
</div>
);
}
function ListItem({ number, title, desc }: { number: string, title: string, desc: string }) {
return (
<div className="flex gap-4 items-start p-3 rounded-xl hover:bg-white/5 transition-colors">
<span className="text-2xl font-black text-white/10 font-mono">{number}</span>
<div>
<h4 className="font-bold text-white mb-1">{title}</h4>
<p className="text-xs text-white/60">{desc}</p>
</div>
</div>
);
}

233
app/planet/page.tsx Normal file
View File

@@ -0,0 +1,233 @@
"use client"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { Card } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Mic, Users, Gamepad2, Headphones, Music, Heart, Sparkles } from "lucide-react"
import Image from "next/image"
import { useState } from "react"
import { toast } from "@/hooks/use-toast"
export default function PlanetPage() {
const [isMatching, setIsMatching] = useState(false)
const handleMatch = () => {
setIsMatching(true)
setTimeout(() => {
setIsMatching(false)
toast({
title: "匹配成功",
description: "已为您找到一位灵魂伴侣",
})
}, 3000)
}
const handleJoinParty = (title: string) => {
toast({
title: "正在加入派对",
description: `即将进入房间:${title}`,
})
}
return (
<div className="pb-24 pt-6 px-4">
<div className="flex items-center justify-between mb-6">
<h1 className="text-2xl font-bold text-white"></h1>
<div className="flex gap-2">
<Button variant="ghost" size="icon" className="rounded-full bg-white/5">
<Users className="w-5 h-5" />
</Button>
</div>
</div>
<Tabs defaultValue="voice" className="w-full">
<TabsList className="grid w-full grid-cols-3 bg-white/5 p-1 rounded-xl mb-6">
<TabsTrigger
value="voice"
className="rounded-lg data-[state=active]:bg-primary data-[state=active]:text-white"
>
</TabsTrigger>
<TabsTrigger
value="party"
className="rounded-lg data-[state=active]:bg-primary data-[state=active]:text-white"
>
</TabsTrigger>
<TabsTrigger
value="team"
className="rounded-lg data-[state=active]:bg-primary data-[state=active]:text-white"
>
</TabsTrigger>
</TabsList>
<TabsContent value="voice" className="mt-0">
<div className="flex flex-col items-center justify-center py-12 space-y-8">
<div className="relative w-64 h-64">
<div
className={`absolute inset-0 bg-primary/20 rounded-full ${isMatching ? "animate-ping duration-1000" : "animate-ping duration-3000"}`}
/>
<div
className={`absolute inset-4 bg-primary/30 rounded-full ${isMatching ? "animate-pulse duration-500" : "animate-pulse duration-2000"}`}
/>
{isMatching && (
<>
<div className="absolute inset-0 border-2 border-primary/50 rounded-full animate-spin-slow" />
<div className="absolute inset-8 border-2 border-purple-500/50 rounded-full animate-reverse-spin" />
</>
)}
<div className="absolute inset-0 flex items-center justify-center">
<div
className="w-48 h-48 bg-gradient-to-br from-primary to-purple-600 rounded-full flex items-center justify-center shadow-2xl shadow-primary/50 border-4 border-white/10 relative overflow-hidden group cursor-pointer transition-transform hover:scale-105 active:scale-95"
onClick={handleMatch}
>
<div className="absolute inset-0 bg-[url('https://images.unsplash.com/photo-1534438327276-14e5300c3a48?w=800&q=80')] opacity-20 bg-cover mix-blend-overlay" />
<div className="text-center z-10">
{isMatching ? (
<>
<Sparkles className="w-12 h-12 text-white mx-auto mb-2 animate-spin" />
<span className="text-xl font-bold text-white">...</span>
</>
) : (
<>
<Mic className="w-12 h-12 text-white mx-auto mb-2" />
<span className="text-xl font-bold text-white"></span>
<p className="text-xs text-white/70 mt-1"></p>
</>
)}
</div>
</div>
</div>
{/* Floating Avatars - Hide avatars when matching to reduce clutter */}
{!isMatching && (
<>
<div className="absolute top-0 left-1/2 -translate-x-1/2 -translate-y-4 animate-bounce duration-3000">
<div className="w-10 h-10 rounded-full border-2 border-primary bg-black overflow-hidden">
<Image src="/diverse-group-avatars.png" alt="User" width={40} height={40} />
</div>
</div>
<div className="absolute bottom-4 right-4 animate-bounce duration-4000 delay-500">
<div className="w-8 h-8 rounded-full border-2 border-purple-500 bg-black overflow-hidden">
<Image src="/gamer-girl-headphones.jpg" alt="User" width={32} height={32} />
</div>
</div>
<div className="absolute bottom-8 left-4 animate-bounce duration-5000 delay-1000">
<div className="w-12 h-12 rounded-full border-2 border-pink-500 bg-black overflow-hidden">
<Image src="/gamer-boy-esports-jersey.jpg" alt="User" width={48} height={48} />
</div>
</div>
</>
)}
</div>
<div className="grid grid-cols-3 gap-4 w-full">
<Button
variant="outline"
className="flex flex-col h-auto py-4 border-white/10 bg-white/5 hover:bg-white/10 hover:border-primary/50"
>
<Heart className="w-6 h-6 text-pink-500 mb-2" />
<span className="text-xs"></span>
</Button>
<Button
variant="outline"
className="flex flex-col h-auto py-4 border-white/10 bg-white/5 hover:bg-white/10 hover:border-primary/50"
>
<Music className="w-6 h-6 text-blue-500 mb-2" />
<span className="text-xs"></span>
</Button>
<Button
variant="outline"
className="flex flex-col h-auto py-4 border-white/10 bg-white/5 hover:bg-white/10 hover:border-primary/50"
>
<Gamepad2 className="w-6 h-6 text-green-500 mb-2" />
<span className="text-xs"></span>
</Button>
</div>
</div>
</TabsContent>
<TabsContent value="party" className="space-y-4">
{[
{
title: "王者荣耀五排车队",
tag: "游戏",
count: 125,
image: "https://images.unsplash.com/photo-1542751371-adc38448a05e?w=800&q=80",
},
{
title: "深夜电台 治愈系",
tag: "聊天",
count: 892,
image: "https://images.unsplash.com/photo-1478737270239-2f02b77ac6d5?w=800&q=80",
},
{
title: "KTV 麦霸进",
tag: "K歌",
count: 45,
image: "https://images.unsplash.com/photo-1516280440614-6697288d5d38?w=800&q=80",
},
{
title: "和平精英 刚枪",
tag: "游戏",
count: 230,
image: "https://images.unsplash.com/photo-1538481199705-c710c4e965fc?w=800&q=80",
},
].map((room, i) => (
<Card
key={i}
className="bg-white/5 border-white/10 overflow-hidden hover:bg-white/10 transition-colors cursor-pointer"
onClick={() => handleJoinParty(room.title)} // Added click handler
>
<div className="flex p-3 gap-4">
<div className="relative w-20 h-20 rounded-lg overflow-hidden flex-shrink-0">
<Image src={room.image || "/placeholder.svg"} alt={room.title} fill className="object-cover" />
<div className="absolute top-1 left-1 bg-black/60 text-[10px] px-1.5 py-0.5 rounded text-white backdrop-blur-sm">
{room.tag}
</div>
</div>
<div className="flex-1 flex flex-col justify-between py-1">
<div>
<h3 className="font-bold text-white line-clamp-1">{room.title}</h3>
<p className="text-xs text-white/60 mt-1 line-clamp-2"></p>
</div>
<div className="flex items-center justify-between mt-2">
<div className="flex -space-x-2">
{[1, 2, 3].map((avatar) => (
<div
key={avatar}
className="w-6 h-6 rounded-full border border-black bg-gray-800 overflow-hidden"
>
<Image
src={`/abstract-geometric-shapes.png?height=24&width=24&query=user${avatar}`}
alt="User"
width={24}
height={24}
/>
</div>
))}
</div>
<div className="flex items-center text-xs text-primary">
<Headphones className="w-3 h-3 mr-1" />
{room.count}
</div>
</div>
</div>
</div>
</Card>
))}
</TabsContent>
<TabsContent value="team" className="space-y-4">
<div className="text-center py-12 text-white/50">
<Gamepad2 className="w-12 h-12 mx-auto mb-4 opacity-50" />
<p>...</p>
</div>
</TabsContent>
</Tabs>
</div>
)
}

223
app/profile/page.tsx Normal file
View File

@@ -0,0 +1,223 @@
"use client" // Convert to client component
import {
Settings,
ChevronRight,
LayoutGrid,
FileText,
ShoppingBag,
MessageSquare,
Flag,
Eye,
Footprints,
Presentation,
Star,
Crown,
BookOpen,
Diamond,
} from "lucide-react"
import Image from "next/image"
import Link from "next/link"
import { Button } from "@/components/ui/button"
import { useAppContext } from "@/components/providers/app-provider" // Import context
import { toast } from "@/hooks/use-toast" // Import toast
export default function ProfilePage() {
const { user, wallet, recharge } = useAppContext() // Use context
const handleComingSoon = (feature: string) => {
toast({
title: "功能开发中",
description: `${feature}即将上线,敬请期待`,
})
}
return (
<div className="min-h-screen pb-24 bg-background">
{/* Header */}
<div className="relative h-48 bg-gradient-to-b from-primary/10 to-background">
<div className="absolute top-4 right-4 flex gap-4">
<Link href="/pitch-deck">
<Presentation className="text-white/80 hover:text-primary transition-colors" />
</Link>
<button onClick={() => handleComingSoon("设置")}>
<Settings className="text-white/80 hover:text-primary transition-colors" />
</button>
</div>
<div className="absolute -bottom-10 left-4 flex items-end gap-4">
<div className="w-20 h-20 rounded-full border-4 border-background bg-muted relative overflow-hidden">
<Image src={user.avatar || "/placeholder.svg"} alt="Profile" fill className="object-cover" />
</div>
<div className="mb-2">
<h1 className="text-xl font-bold flex items-center gap-2">
{user.name}
<span className="px-1.5 py-0.5 rounded bg-primary text-[10px] text-white font-bold">LV.{user.level}</span>
</h1>
<p className="text-xs text-white/50">ID: {user.id || "888888"}</p>
</div>
<button
className="mb-2 px-3 py-1 rounded-full bg-white/10 text-xs font-medium ml-auto flex items-center gap-1 hover:bg-white/20 transition-colors"
onClick={() => handleComingSoon("个人主页")}
>
<ChevronRight size={12} />
</button>
</div>
</div>
{/* Stats */}
<div className="mt-12 px-6 flex justify-between text-center">
{[
{ label: "动态", value: user.activity || 0 },
{ label: "关注", value: user.following || 0 },
{ label: "粉丝", value: user.followers || 0 },
{ label: "访客", value: user.visitors || 0 },
].map((stat, i) => (
<div key={i} className="flex flex-col gap-1">
<span className="font-bold text-lg">{stat.value}</span>
<span className="text-xs text-white/50">{stat.label}</span>
</div>
))}
</div>
{/* VIP Banner */}
<div
className="mx-4 mt-6 p-3 rounded-xl bg-gradient-to-r from-yellow-600/20 to-yellow-900/20 border border-yellow-500/20 flex items-center justify-between cursor-pointer hover:bg-yellow-600/30 transition-colors"
onClick={() => handleComingSoon("会员中心")}
>
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-full bg-yellow-500/20 flex items-center justify-center">
<Crown size={16} className="text-yellow-500" />
</div>
<div>
<h3 className="text-sm font-bold text-yellow-500"></h3>
<p className="text-[10px] text-yellow-500/60">12</p>
</div>
</div>
<Button
size="sm"
variant="outline"
className="h-7 text-xs border-yellow-500/30 text-yellow-500 hover:bg-yellow-500/10 bg-transparent"
>
</Button>
</div>
{/* Wallet Card */}
<div className="mx-4 mt-4 p-4 rounded-2xl bg-gradient-to-r from-[#1a1a24] to-[#0f0f16] border border-white/5 relative overflow-hidden">
<div className="absolute top-0 right-0 w-32 h-32 bg-primary/5 rounded-full blur-3xl" />
<div className="flex items-center justify-between mb-4">
<span className="text-sm font-medium text-white/80"></span>
<Button
size="sm"
className="h-6 text-xs bg-primary/20 hover:bg-primary/30 text-primary border-none"
onClick={() => recharge(100)} // Add recharge handler
>
</Button>
</div>
<div className="flex justify-between items-center">
<div>
<div className="flex items-center gap-1 text-primary mb-1">
<Diamond size={16} />
<span className="text-xs"></span>
</div>
<span className="text-2xl font-bold font-mono">{wallet.diamonds}</span> {/* Use wallet data */}
</div>
<div className="w-px h-8 bg-white/10" />
<div>
<div className="flex items-center gap-1 text-secondary mb-1">
<div className="w-4 h-4 rounded-full bg-secondary/20 flex items-center justify-center text-[10px]">
🐱
</div>
<span className="text-xs"></span> {/* Updated label to Wanzi Coin */}
</div>
<span className="text-2xl font-bold font-mono">{wallet.coins}</span>
</div>
</div>
</div>
{/* Creator Center */}
<div className="mx-4 mt-4 glass-card rounded-2xl p-4">
<h3 className="text-sm font-bold mb-4 flex items-center gap-2">
<Star size={16} className="text-yellow-400" />
</h3>
<div className="grid grid-cols-4 gap-4">
<div
className="col-span-2 bg-gradient-to-br from-primary/20 to-purple-500/20 rounded-xl p-3 border border-white/5 relative overflow-hidden cursor-pointer hover:opacity-80 transition-opacity"
onClick={() => handleComingSoon("大神申请")}
>
<div className="relative z-10">
<h4 className="font-bold text-sm"></h4>
<p className="text-[10px] text-white/60 mt-1"> </p>
</div>
<Star className="absolute bottom-0 right-0 text-primary/20 w-12 h-12 -mb-2 -mr-2" />
</div>
<div
className="col-span-2 bg-gradient-to-br from-blue-500/20 to-cyan-500/20 rounded-xl p-3 border border-white/5 relative overflow-hidden cursor-pointer hover:opacity-80 transition-opacity"
onClick={() => handleComingSoon("公会中心")}
>
<div className="relative z-10">
<h4 className="font-bold text-sm"></h4>
<p className="text-[10px] text-white/60 mt-1"> </p>
</div>
<Flag className="absolute bottom-0 right-0 text-blue-500/20 w-12 h-12 -mb-2 -mr-2" />
</div>
</div>
</div>
{/* Menu Grid */}
<div className="mx-4 mt-4 glass-card rounded-2xl p-4">
<h3 className="text-sm font-bold mb-4 flex items-center gap-2">
<LayoutGrid size={16} className="text-accent" />
</h3>
<div className="grid grid-cols-4 gap-y-6">
{[
{ icon: FileText, label: "订单中心", color: "text-green-400", href: "/orders" },
{ icon: BookOpen, label: "我的课程", color: "text-red-400", href: "/my-courses" }, // Changed to my-courses
{ icon: Presentation, label: "商业计划", color: "text-orange-400", href: "/pitch-deck" },
{
icon: MessageSquare,
label: "联系客服",
color: "text-purple-400",
action: () => handleComingSoon("客服系统"),
},
{ icon: ShoppingBag, label: "背包", color: "text-blue-400", action: () => handleComingSoon("背包系统") },
{ icon: Footprints, label: "足迹", color: "text-teal-400", action: () => handleComingSoon("足迹功能") },
{ icon: Eye, label: "访客", color: "text-yellow-400", action: () => handleComingSoon("访客记录") },
{ icon: Settings, label: "设置", color: "text-pink-400", action: () => handleComingSoon("设置") },
].map((item, i) =>
item.href ? (
<Link
key={i}
href={item.href}
className="flex flex-col items-center gap-2 hover:opacity-80 transition-opacity"
>
<div className={`w-10 h-10 rounded-xl bg-white/5 flex items-center justify-center ${item.color}`}>
<item.icon size={20} />
</div>
<span className="text-[10px] text-white/60">{item.label}</span>
</Link>
) : (
<button
key={i}
onClick={item.action}
className="flex flex-col items-center gap-2 hover:opacity-80 transition-opacity"
>
<div className={`w-10 h-10 rounded-xl bg-white/5 flex items-center justify-center ${item.color}`}>
<item.icon size={20} />
</div>
<span className="text-[10px] text-white/60">{item.label}</span>
</button>
),
)}
</div>
</div>
</div>
)
}

337
app/star/[id]/page.tsx Normal file
View File

@@ -0,0 +1,337 @@
import { ArrowLeft, Star, Trophy, Users, Clock, Shield, Heart, MessageCircle, Share2, Sparkles } from 'lucide-react';
import Image from "next/image";
import Link from "next/link";
// Mock data - in production this would come from a database
const starData: Record<string, any> = {
"1": {
name: "GM-远洋",
title: "国服第一盲僧",
avatar: "/gamer-girl-headphones.jpg",
cover: "/esports-tournament-stadium-neon-lights-crowd.jpg",
rating: 4.9,
orders: 1205,
fans: 46000,
games: ["英雄联盟", "无畏契约"],
tags: ["职业选手", "国服第一", "耐心教学"],
intro: "前职业战队打野选手,擅长盲僧、千珏等打野英雄。曾获得LPL春季赛冠军,现专注于游戏教学和陪玩服务。",
achievements: [
{ icon: Trophy, text: "LPL春季赛冠军" },
{ icon: Star, text: "国服第一盲僧" },
{ icon: Users, text: "46K+粉丝" },
],
services: [
{
id: "1",
type: "陪玩",
name: "单局陪玩",
price: 50,
duration: "1局",
desc: "一起开黑,边玩边教,轻松上分",
popular: true
},
{
id: "2",
type: "陪玩",
name: "5局套餐",
price: 220,
duration: "5局",
desc: "连续5局陪玩,享受9折优惠",
discount: "9折"
},
{
id: "3",
type: "课程",
name: "打野进阶课",
price: 299,
duration: "8节课",
desc: "系统学习打野思路、刷野路线、Gank时机",
},
{
id: "4",
type: "拜师",
name: "拜师学艺",
price: 1980,
duration: "30天",
desc: "一对一长期指导,包含10次实战陪练+课程",
hot: true
}
],
reviews: [
{
user: "玩家***23",
avatar: "/diverse-avatars.png",
rating: 5,
content: "远洋老师教的很好,盲僧R闪学会了!",
time: "2天前"
},
{
user: "电竞***爱好者",
avatar: "/diverse-avatars.png",
rating: 5,
content: "非常耐心,讲解很详细,值得!",
time: "5天前"
}
]
},
"2": {
name: "卡若",
title: "职业战队退役选手",
avatar: "/gamer-boy-esports-jersey.jpg",
cover: "/esports-tournament-stadium-neon-lights-crowd.jpg",
rating: 4.8,
orders: 856,
fans: 32000,
games: ["英雄联盟", "云顶之弈"],
tags: ["前职业选手", "中单大神", "幽默风趣"],
intro: "前LPL职业中单选手,擅长刺客英雄。退役后专注于游戏教学,帮助数千玩家提升段位。",
achievements: [
{ icon: Trophy, text: "LPL职业选手" },
{ icon: Star, text: "最强王者2000分" },
{ icon: Users, text: "32K+粉丝" },
],
services: [
{
id: "1",
type: "陪玩",
name: "单局陪玩",
price: 80,
duration: "1局",
desc: "一起开黑,边玩边教,轻松上分",
popular: true
},
{
id: "2",
type: "课程",
name: "中单进阶课",
price: 399,
duration: "10节课",
desc: "系统学习中单对线、游走、团战技巧",
},
{
id: "3",
type: "拜师",
name: "拜师学艺",
price: 2980,
duration: "30天",
desc: "一对一长期指导,包含15次实战陪练+课程",
hot: true
}
],
reviews: [
{
user: "召唤师***88",
avatar: "/diverse-avatars.png",
rating: 5,
content: "卡若老师很专业,从白银打到钻石了!",
time: "1天前"
}
]
}
};
export default function StarDetailPage({ params }: { params: { id: string } }) {
const star = starData[params.id] || starData["1"];
return (
<div className="min-h-screen pb-24 bg-background">
{/* Header with Cover */}
<div className="relative h-64">
<Image
src={star.cover || "/placeholder.svg"}
alt={star.name}
fill
className="object-cover"
/>
<div className="absolute inset-0 bg-gradient-to-b from-transparent via-background/50 to-background" />
{/* Back Button */}
<Link href="/" className="absolute top-4 left-4 z-10 w-10 h-10 rounded-full glass flex items-center justify-center">
<ArrowLeft size={20} />
</Link>
{/* Share Button */}
<button className="absolute top-4 right-4 z-10 w-10 h-10 rounded-full glass flex items-center justify-center">
<Share2 size={20} />
</button>
{/* Avatar */}
<div className="absolute -bottom-12 left-4 w-24 h-24 rounded-2xl border-4 border-background overflow-hidden">
<Image
src={star.avatar || "/placeholder.svg"}
alt={star.name}
fill
className="object-cover"
/>
</div>
</div>
{/* Profile Info */}
<div className="mt-14 px-4">
<div className="flex items-start justify-between mb-3">
<div>
<h1 className="text-2xl font-bold mb-1">{star.name}</h1>
<p className="text-sm text-white/60">{star.title}</p>
</div>
<button className="px-4 py-2 rounded-full bg-secondary text-white font-bold flex items-center gap-2 hover:bg-secondary/90 transition-colors">
<Heart size={16} />
</button>
</div>
{/* Stats */}
<div className="flex items-center gap-6 mb-4">
<div className="flex items-center gap-1">
<Star size={16} className="text-primary fill-primary" />
<span className="font-bold">{star.rating}</span>
<span className="text-xs text-white/50"></span>
</div>
<div className="flex items-center gap-1">
<Trophy size={16} className="text-accent" />
<span className="font-bold">{star.orders}</span>
<span className="text-xs text-white/50"></span>
</div>
<div className="flex items-center gap-1">
<Users size={16} className="text-secondary" />
<span className="font-bold">{(star.fans / 1000).toFixed(1)}K</span>
<span className="text-xs text-white/50"></span>
</div>
</div>
{/* Tags */}
<div className="flex flex-wrap gap-2 mb-6">
{star.tags.map((tag: string, i: number) => (
<span key={i} className="px-3 py-1 rounded-full bg-white/5 border border-white/10 text-xs">
{tag}
</span>
))}
</div>
{/* Games */}
<div className="flex gap-2 mb-6">
{star.games.map((game: string, i: number) => (
<div key={i} className="px-3 py-1.5 rounded-lg glass-card text-xs font-medium">
{game}
</div>
))}
</div>
</div>
{/* Achievements */}
<div className="px-4 mb-6">
<h3 className="font-bold mb-3 flex items-center gap-2">
<Trophy size={18} className="text-primary" />
</h3>
<div className="grid grid-cols-3 gap-3">
{star.achievements.map((achievement: any, i: number) => (
<div key={i} className="glass-card p-3 rounded-xl flex flex-col items-center gap-2 text-center">
<achievement.icon size={24} className="text-primary" />
<span className="text-[10px] text-white/70 leading-tight">{achievement.text}</span>
</div>
))}
</div>
</div>
{/* Introduction */}
<div className="px-4 mb-6">
<h3 className="font-bold mb-2"></h3>
<p className="text-sm text-white/70 leading-relaxed">{star.intro}</p>
</div>
{/* Services */}
<div className="px-4 mb-6">
<h3 className="font-bold mb-3 flex items-center gap-2">
<Sparkles size={18} className="text-secondary" />
</h3>
<div className="space-y-3">
{star.services.map((service: any) => (
<div key={service.id} className="glass-card p-4 rounded-xl relative overflow-hidden group hover:border-primary/30 transition-colors">
{service.popular && (
<div className="absolute top-2 right-2 px-2 py-0.5 rounded-full bg-primary/20 text-primary text-[10px] font-bold">
</div>
)}
{service.hot && (
<div className="absolute top-2 right-2 px-2 py-0.5 rounded-full bg-secondary/20 text-secondary text-[10px] font-bold animate-pulse">
🔥
</div>
)}
<div className="flex items-start justify-between mb-2">
<div>
<div className="flex items-center gap-2 mb-1">
<span className="px-2 py-0.5 rounded bg-white/5 text-[10px] font-medium">{service.type}</span>
<h4 className="font-bold">{service.name}</h4>
</div>
<p className="text-xs text-white/60 mb-2">{service.desc}</p>
<div className="flex items-center gap-2 text-xs text-white/50">
<Clock size={12} />
<span>{service.duration}</span>
</div>
</div>
<div className="text-right">
<div className="text-2xl font-bold text-primary">{service.price}</div>
<div className="text-[10px] text-white/50"></div>
</div>
</div>
<Link
href={`/booking?starId=${params.id}&serviceId=${service.id}`}
className="w-full mt-3 py-2.5 rounded-lg bg-primary text-black font-bold text-center block hover:bg-primary/90 transition-colors"
>
</Link>
</div>
))}
</div>
</div>
{/* Reviews */}
<div className="px-4 mb-6">
<div className="flex items-center justify-between mb-3">
<h3 className="font-bold flex items-center gap-2">
<MessageCircle size={18} className="text-accent" />
</h3>
<Link href="#" className="text-xs text-white/50"></Link>
</div>
<div className="space-y-3">
{star.reviews.map((review: any, i: number) => (
<div key={i} className="glass-card p-4 rounded-xl">
<div className="flex items-center gap-3 mb-2">
<div className="w-8 h-8 rounded-full bg-muted relative overflow-hidden">
<Image src={review.avatar || "/placeholder.svg"} alt={review.user} fill className="object-cover" />
</div>
<div className="flex-1">
<div className="flex items-center justify-between">
<span className="text-sm font-medium">{review.user}</span>
<span className="text-xs text-white/50">{review.time}</span>
</div>
<div className="flex gap-0.5">
{Array.from({ length: review.rating }).map((_, j) => (
<Star key={j} size={12} className="text-primary fill-primary" />
))}
</div>
</div>
</div>
<p className="text-sm text-white/70">{review.content}</p>
</div>
))}
</div>
</div>
{/* Trust Badge */}
<div className="mx-4 mb-6 p-4 rounded-xl bg-gradient-to-r from-primary/10 to-transparent border-l-4 border-primary">
<div className="flex items-center gap-3">
<Shield size={24} className="text-primary" />
<div>
<h4 className="font-bold text-sm"></h4>
<p className="text-xs text-white/60">,退</p>
</div>
</div>
</div>
</div>
);
}

21
components.json Normal file
View File

@@ -0,0 +1,21 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "",
"css": "app/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}

View File

@@ -0,0 +1,48 @@
"use client";
import Link from "next/link";
import { usePathname } from 'next/navigation';
import { Home, Play, ShoppingBag, User, Globe } from 'lucide-react';
import { cn } from "@/lib/utils";
export function MobileNav() {
const pathname = usePathname();
const navItems = [
{ href: "/", icon: Home, label: "首页" },
{ href: "/moments", icon: Play, label: "动态" },
{ href: "/planet", icon: Globe, label: "星球" },
{ href: "/mall", icon: ShoppingBag, label: "商城" },
{ href: "/profile", icon: User, label: "我的" },
];
return (
<div className="fixed bottom-0 left-0 right-0 z-50 flex justify-center">
<div className="w-full max-w-md glass border-t border-white/10 pb-safe">
<nav className="flex items-center justify-around h-16 px-2">
{navItems.map((item) => {
const isActive = pathname === item.href;
return (
<Link
key={item.href}
href={item.href}
className={cn(
"flex flex-col items-center justify-center w-full h-full space-y-1 transition-all duration-300",
isActive ? "text-primary" : "text-muted-foreground hover:text-foreground"
)}
>
<div className={cn("relative p-1 rounded-xl transition-all", isActive && "bg-primary/10")}>
<item.icon size={22} strokeWidth={isActive ? 2.5 : 2} />
{isActive && (
<span className="absolute inset-0 rounded-xl bg-primary/20 blur-lg animate-pulse" />
)}
</div>
<span className="text-[10px] font-medium">{item.label}</span>
</Link>
);
})}
</nav>
</div>
</div>
);
}

View File

@@ -0,0 +1,164 @@
"use client"
import type React from "react"
import { createContext, useContext, useState, useEffect } from "react"
import { toast } from "@/hooks/use-toast"
type User = {
name: string
avatar: string
level: number
isVip: boolean
vipExpiry?: string
id?: string
activity?: number
following?: number
followers?: number
visitors?: number
}
type Wallet = {
diamonds: number // For booking stars (Paid currency)
coins: number // For mall items (Earned/Free currency) - Renamed to Wanzi Coin in UI
}
type Order = {
id: string
type: "service" | "product"
title: string
price: number
currency: "diamonds" | "coins"
status: "pending" | "completed" | "cancelled"
date: string
image?: string
}
type AppContextType = {
user: User
wallet: Wallet
orders: Order[]
recharge: (amount: number) => void
pay: (
amount: number,
currency: "diamonds" | "coins",
title: string,
type: "service" | "product",
image?: string,
) => boolean
updateUser: (updates: Partial<User>) => void
}
const AppContext = createContext<AppContextType | undefined>(undefined)
export function AppProvider({ children }: { children: React.ReactNode }) {
// Initial mock state
const [user, setUser] = useState<User>({
name: "卡若", // Updated name to 卡若
avatar: "/gamer-boy-esports-jersey.jpg", // Updated avatar
level: 12,
isVip: false,
id: "888888",
activity: 128,
following: 45,
followers: 892,
visitors: 1205,
})
const [wallet, setWallet] = useState<Wallet>({
diamonds: 1280,
coins: 5600,
})
const [orders, setOrders] = useState<Order[]>([
{
id: "1",
type: "service",
title: "LOL陪玩 - 1小时",
price: 50,
currency: "diamonds",
status: "completed",
date: "2023-11-15",
image: "/lol.jpg",
},
])
// Persist to localStorage (optional, for demo feel)
useEffect(() => {
const savedWallet = localStorage.getItem("wanzhi_wallet")
if (savedWallet) setWallet(JSON.parse(savedWallet))
const savedOrders = localStorage.getItem("wanzhi_orders")
if (savedOrders) setOrders(JSON.parse(savedOrders))
}, [])
useEffect(() => {
localStorage.setItem("wanzhi_wallet", JSON.stringify(wallet))
}, [wallet])
useEffect(() => {
localStorage.setItem("wanzhi_orders", JSON.stringify(orders))
}, [orders])
const recharge = (amount: number) => {
setWallet((prev) => ({ ...prev, diamonds: prev.diamonds + amount }))
toast({
title: "充值成功",
description: `成功充值 ${amount} 钻石`,
})
}
const pay = (
amount: number,
currency: "diamonds" | "coins",
title: string,
type: "service" | "product",
image?: string,
): boolean => {
if (wallet[currency] < amount) {
toast({
title: "余额不足",
description: `您的${currency === "diamonds" ? "钻石" : "丸子币"}不足,请充值`, // Updated currency name to Wanzi Coin
variant: "destructive",
})
return false
}
setWallet((prev) => ({ ...prev, [currency]: prev[currency] - amount }))
const newOrder: Order = {
id: Math.random().toString(36).substr(2, 9),
type,
title,
price: amount,
currency,
status: "completed", // Auto-complete for demo
date: new Date().toISOString().split("T")[0],
image,
}
setOrders((prev) => [newOrder, ...prev])
toast({
title: "支付成功",
description: `成功支付 ${amount} ${currency === "diamonds" ? "钻石" : "丸子币"}`, // Updated currency name to Wanzi Coin
})
return true
}
const updateUser = (updates: Partial<User>) => {
setUser((prev) => ({ ...prev, ...updates }))
}
return (
<AppContext.Provider value={{ user, wallet, orders, recharge, pay, updateUser }}>{children}</AppContext.Provider>
)
}
export function useAppContext() {
const context = useContext(AppContext)
if (context === undefined) {
throw new Error("useAppContext must be used within an AppProvider")
}
return context
}

View File

@@ -0,0 +1,11 @@
'use client'
import * as React from 'react'
import {
ThemeProvider as NextThemesProvider,
type ThemeProviderProps,
} from 'next-themes'
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
}

60
components/ui/button.tsx Normal file
View File

@@ -0,0 +1,60 @@
import * as React from 'react'
import { Slot } from '@radix-ui/react-slot'
import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from '@/lib/utils'
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{
variants: {
variant: {
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
destructive:
'bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',
outline:
'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50',
secondary:
'bg-secondary text-secondary-foreground hover:bg-secondary/80',
ghost:
'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
link: 'text-primary underline-offset-4 hover:underline',
},
size: {
default: 'h-9 px-4 py-2 has-[>svg]:px-3',
sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5',
lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
icon: 'size-9',
'icon-sm': 'size-8',
'icon-lg': 'size-10',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
},
)
function Button({
className,
variant,
size,
asChild = false,
...props
}: React.ComponentProps<'button'> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean
}) {
const Comp = asChild ? Slot : 'button'
return (
<Comp
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
}
export { Button, buttonVariants }

92
components/ui/card.tsx Normal file
View File

@@ -0,0 +1,92 @@
import * as React from 'react'
import { cn } from '@/lib/utils'
function Card({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="card"
className={cn(
'bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm',
className,
)}
{...props}
/>
)
}
function CardHeader({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="card-header"
className={cn(
'@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6',
className,
)}
{...props}
/>
)
}
function CardTitle({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="card-title"
className={cn('leading-none font-semibold', className)}
{...props}
/>
)
}
function CardDescription({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="card-description"
className={cn('text-muted-foreground text-sm', className)}
{...props}
/>
)
}
function CardAction({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="card-action"
className={cn(
'col-start-2 row-span-2 row-start-1 self-start justify-self-end',
className,
)}
{...props}
/>
)
}
function CardContent({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="card-content"
className={cn('px-6', className)}
{...props}
/>
)
}
function CardFooter({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="card-footer"
className={cn('flex items-center px-6 [.border-t]:pt-6', className)}
{...props}
/>
)
}
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardAction,
CardDescription,
CardContent,
}

66
components/ui/tabs.tsx Normal file
View File

@@ -0,0 +1,66 @@
'use client'
import * as React from 'react'
import * as TabsPrimitive from '@radix-ui/react-tabs'
import { cn } from '@/lib/utils'
function Tabs({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.Root>) {
return (
<TabsPrimitive.Root
data-slot="tabs"
className={cn('flex flex-col gap-2', className)}
{...props}
/>
)
}
function TabsList({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.List>) {
return (
<TabsPrimitive.List
data-slot="tabs-list"
className={cn(
'bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]',
className,
)}
{...props}
/>
)
}
function TabsTrigger({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
return (
<TabsPrimitive.Trigger
data-slot="tabs-trigger"
className={cn(
"data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
/>
)
}
function TabsContent({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.Content>) {
return (
<TabsPrimitive.Content
data-slot="tabs-content"
className={cn('flex-1 outline-none', className)}
{...props}
/>
)
}
export { Tabs, TabsList, TabsTrigger, TabsContent }

129
components/ui/toast.tsx Normal file
View File

@@ -0,0 +1,129 @@
'use client'
import * as React from 'react'
import * as ToastPrimitives from '@radix-ui/react-toast'
import { cva, type VariantProps } from 'class-variance-authority'
import { X } from 'lucide-react'
import { cn } from '@/lib/utils'
const ToastProvider = ToastPrimitives.Provider
const ToastViewport = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Viewport>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Viewport
ref={ref}
className={cn(
'fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]',
className,
)}
{...props}
/>
))
ToastViewport.displayName = ToastPrimitives.Viewport.displayName
const toastVariants = cva(
'group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full',
{
variants: {
variant: {
default: 'border bg-background text-foreground',
destructive:
'destructive group border-destructive bg-destructive text-destructive-foreground',
},
},
defaultVariants: {
variant: 'default',
},
},
)
const Toast = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
VariantProps<typeof toastVariants>
>(({ className, variant, ...props }, ref) => {
return (
<ToastPrimitives.Root
ref={ref}
className={cn(toastVariants({ variant }), className)}
{...props}
/>
)
})
Toast.displayName = ToastPrimitives.Root.displayName
const ToastAction = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Action>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Action
ref={ref}
className={cn(
'inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive',
className,
)}
{...props}
/>
))
ToastAction.displayName = ToastPrimitives.Action.displayName
const ToastClose = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Close>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Close
ref={ref}
className={cn(
'absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600',
className,
)}
toast-close=""
{...props}
>
<X className="h-4 w-4" />
</ToastPrimitives.Close>
))
ToastClose.displayName = ToastPrimitives.Close.displayName
const ToastTitle = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Title>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Title
ref={ref}
className={cn('text-sm font-semibold', className)}
{...props}
/>
))
ToastTitle.displayName = ToastPrimitives.Title.displayName
const ToastDescription = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Description>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Description
ref={ref}
className={cn('text-sm opacity-90', className)}
{...props}
/>
))
ToastDescription.displayName = ToastPrimitives.Description.displayName
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>
type ToastActionElement = React.ReactElement<typeof ToastAction>
export {
type ToastProps,
type ToastActionElement,
ToastProvider,
ToastViewport,
Toast,
ToastTitle,
ToastDescription,
ToastClose,
ToastAction,
}

35
components/ui/toaster.tsx Normal file
View File

@@ -0,0 +1,35 @@
'use client'
import { useToast } from '@/hooks/use-toast'
import {
Toast,
ToastClose,
ToastDescription,
ToastProvider,
ToastTitle,
ToastViewport,
} from '@/components/ui/toast'
export function Toaster() {
const { toasts } = useToast()
return (
<ToastProvider>
{toasts.map(function ({ id, title, description, action, ...props }) {
return (
<Toast key={id} {...props}>
<div className="grid gap-1">
{title && <ToastTitle>{title}</ToastTitle>}
{description && (
<ToastDescription>{description}</ToastDescription>
)}
</div>
{action}
<ToastClose />
</Toast>
)
})}
<ToastViewport />
</ToastProvider>
)
}

191
hooks/use-toast.ts Normal file
View File

@@ -0,0 +1,191 @@
'use client'
// Inspired by react-hot-toast library
import * as React from 'react'
import type { ToastActionElement, ToastProps } from '@/components/ui/toast'
const TOAST_LIMIT = 1
const TOAST_REMOVE_DELAY = 1000000
type ToasterToast = ToastProps & {
id: string
title?: React.ReactNode
description?: React.ReactNode
action?: ToastActionElement
}
const actionTypes = {
ADD_TOAST: 'ADD_TOAST',
UPDATE_TOAST: 'UPDATE_TOAST',
DISMISS_TOAST: 'DISMISS_TOAST',
REMOVE_TOAST: 'REMOVE_TOAST',
} as const
let count = 0
function genId() {
count = (count + 1) % Number.MAX_SAFE_INTEGER
return count.toString()
}
type ActionType = typeof actionTypes
type Action =
| {
type: ActionType['ADD_TOAST']
toast: ToasterToast
}
| {
type: ActionType['UPDATE_TOAST']
toast: Partial<ToasterToast>
}
| {
type: ActionType['DISMISS_TOAST']
toastId?: ToasterToast['id']
}
| {
type: ActionType['REMOVE_TOAST']
toastId?: ToasterToast['id']
}
interface State {
toasts: ToasterToast[]
}
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
const addToRemoveQueue = (toastId: string) => {
if (toastTimeouts.has(toastId)) {
return
}
const timeout = setTimeout(() => {
toastTimeouts.delete(toastId)
dispatch({
type: 'REMOVE_TOAST',
toastId: toastId,
})
}, TOAST_REMOVE_DELAY)
toastTimeouts.set(toastId, timeout)
}
export const reducer = (state: State, action: Action): State => {
switch (action.type) {
case 'ADD_TOAST':
return {
...state,
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
}
case 'UPDATE_TOAST':
return {
...state,
toasts: state.toasts.map((t) =>
t.id === action.toast.id ? { ...t, ...action.toast } : t,
),
}
case 'DISMISS_TOAST': {
const { toastId } = action
// ! Side effects ! - This could be extracted into a dismissToast() action,
// but I'll keep it here for simplicity
if (toastId) {
addToRemoveQueue(toastId)
} else {
state.toasts.forEach((toast) => {
addToRemoveQueue(toast.id)
})
}
return {
...state,
toasts: state.toasts.map((t) =>
t.id === toastId || toastId === undefined
? {
...t,
open: false,
}
: t,
),
}
}
case 'REMOVE_TOAST':
if (action.toastId === undefined) {
return {
...state,
toasts: [],
}
}
return {
...state,
toasts: state.toasts.filter((t) => t.id !== action.toastId),
}
}
}
const listeners: Array<(state: State) => void> = []
let memoryState: State = { toasts: [] }
function dispatch(action: Action) {
memoryState = reducer(memoryState, action)
listeners.forEach((listener) => {
listener(memoryState)
})
}
type Toast = Omit<ToasterToast, 'id'>
function toast({ ...props }: Toast) {
const id = genId()
const update = (props: ToasterToast) =>
dispatch({
type: 'UPDATE_TOAST',
toast: { ...props, id },
})
const dismiss = () => dispatch({ type: 'DISMISS_TOAST', toastId: id })
dispatch({
type: 'ADD_TOAST',
toast: {
...props,
id,
open: true,
onOpenChange: (open) => {
if (!open) dismiss()
},
},
})
return {
id: id,
dismiss,
update,
}
}
function useToast() {
const [state, setState] = React.useState<State>(memoryState)
React.useEffect(() => {
listeners.push(setState)
return () => {
const index = listeners.indexOf(setState)
if (index > -1) {
listeners.splice(index, 1)
}
}
}, [state])
return {
...state,
toast,
dismiss: (toastId?: string) => dispatch({ type: 'DISMISS_TOAST', toastId }),
}
}
export { useToast, toast }

6
lib/utils.ts Normal file
View File

@@ -0,0 +1,6 @@
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}

12
next.config.mjs Normal file
View File

@@ -0,0 +1,12 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
typescript: {
ignoreBuildErrors: true,
},
images: {
unoptimized: true,
},
}
export default nextConfig

75
package.json Normal file
View File

@@ -0,0 +1,75 @@
{
"name": "my-v0-project",
"version": "0.1.0",
"private": true,
"scripts": {
"build": "next build",
"dev": "next dev",
"lint": "eslint .",
"start": "next start"
},
"dependencies": {
"@emotion/is-prop-valid": "latest",
"@hookform/resolvers": "^3.10.0",
"@radix-ui/react-accordion": "1.2.2",
"@radix-ui/react-alert-dialog": "1.1.4",
"@radix-ui/react-aspect-ratio": "1.1.1",
"@radix-ui/react-avatar": "1.1.2",
"@radix-ui/react-checkbox": "1.1.3",
"@radix-ui/react-collapsible": "1.1.2",
"@radix-ui/react-context-menu": "2.2.4",
"@radix-ui/react-dialog": "1.1.4",
"@radix-ui/react-dropdown-menu": "2.1.4",
"@radix-ui/react-hover-card": "1.1.4",
"@radix-ui/react-label": "2.1.1",
"@radix-ui/react-menubar": "1.1.4",
"@radix-ui/react-navigation-menu": "1.2.3",
"@radix-ui/react-popover": "1.1.4",
"@radix-ui/react-progress": "1.1.1",
"@radix-ui/react-radio-group": "1.2.2",
"@radix-ui/react-scroll-area": "1.2.2",
"@radix-ui/react-select": "2.1.4",
"@radix-ui/react-separator": "1.1.1",
"@radix-ui/react-slider": "1.2.2",
"@radix-ui/react-slot": "1.1.1",
"@radix-ui/react-switch": "1.1.2",
"@radix-ui/react-tabs": "1.1.2",
"@radix-ui/react-toast": "1.2.4",
"@radix-ui/react-toggle": "1.1.1",
"@radix-ui/react-toggle-group": "1.1.1",
"@radix-ui/react-tooltip": "1.1.6",
"@vercel/analytics": "1.3.1",
"autoprefixer": "^10.4.20",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "1.0.4",
"date-fns": "4.1.0",
"embla-carousel-react": "8.5.1",
"framer-motion": "latest",
"input-otp": "1.4.1",
"lucide-react": "^0.454.0",
"next": "16.0.3",
"next-themes": "^0.4.6",
"react": "19.2.0",
"react-day-picker": "9.8.0",
"react-dom": "19.2.0",
"react-hook-form": "^7.60.0",
"react-resizable-panels": "^2.1.7",
"recharts": "2.15.4",
"sonner": "^1.7.4",
"tailwind-merge": "^2.5.5",
"tailwindcss-animate": "^1.0.7",
"vaul": "^0.9.9",
"zod": "3.25.76"
},
"devDependencies": {
"@tailwindcss/postcss": "^4.1.9",
"@types/node": "^22",
"@types/react": "^19",
"@types/react-dom": "^19",
"postcss": "^8.5",
"tailwindcss": "^4.1.9",
"tw-animate-css": "1.3.3",
"typescript": "^5"
}
}

3279
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

8
postcss.config.mjs Normal file
View File

@@ -0,0 +1,8 @@
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: {
'@tailwindcss/postcss': {},
},
}
export default config

Binary file not shown.

After

Width:  |  Height:  |  Size: 649 KiB

BIN
public/apple-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

BIN
public/avatar-3d-style.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

BIN
public/diverse-avatars.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 665 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 581 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

BIN
public/esports-logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1013 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 217 KiB

BIN
public/icon-dark-32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 585 B

BIN
public/icon-light-32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 566 B

26
public/icon.svg Normal file
View File

@@ -0,0 +1,26 @@
<svg width="180" height="180" viewBox="0 0 180 180" fill="none" xmlns="http://www.w3.org/2000/svg">
<style>
@media (prefers-color-scheme: light) {
.background { fill: black; }
.foreground { fill: white; }
}
@media (prefers-color-scheme: dark) {
.background { fill: white; }
.foreground { fill: black; }
}
</style>
<g clip-path="url(#clip0_7960_43945)">
<rect class="background" width="180" height="180" rx="37" />
<g style="transform: scale(95%); transform-origin: center">
<path class="foreground"
d="M101.141 53H136.632C151.023 53 162.689 64.6662 162.689 79.0573V112.904H148.112V79.0573C148.112 78.7105 148.098 78.3662 148.072 78.0251L112.581 112.898C112.701 112.902 112.821 112.904 112.941 112.904H148.112V126.672H112.941C98.5504 126.672 86.5638 114.891 86.5638 100.5V66.7434H101.141V100.5C101.141 101.15 101.191 101.792 101.289 102.422L137.56 66.7816C137.255 66.7563 136.945 66.7434 136.632 66.7434H101.141V53Z" />
<path class="foreground"
d="M65.2926 124.136L14 66.7372H34.6355L64.7495 100.436V66.7372H80.1365V118.47C80.1365 126.278 70.4953 129.958 65.2926 124.136Z" />
</g>
</g>
<defs>
<clipPath id="clip0_7960_43945">
<rect width="180" height="180" fill="white" />
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
public/lol.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

BIN
public/placeholder-logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 568 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="215" height="48" fill="none"><path fill="#000" d="M57.588 9.6h6L73.828 38h-5.2l-2.36-6.88h-11.36L52.548 38h-5.2l10.24-28.4Zm7.16 17.16-4.16-12.16-4.16 12.16h8.32Zm23.694-2.24c-.186-1.307-.706-2.32-1.56-3.04-.853-.72-1.866-1.08-3.04-1.08-1.68 0-2.986.613-3.92 1.84-.906 1.227-1.36 2.947-1.36 5.16s.454 3.933 1.36 5.16c.934 1.227 2.24 1.84 3.92 1.84 1.254 0 2.307-.373 3.16-1.12.854-.773 1.387-1.867 1.6-3.28l5.12.24c-.186 1.68-.733 3.147-1.64 4.4-.906 1.227-2.08 2.173-3.52 2.84-1.413.667-2.986 1-4.72 1-2.08 0-3.906-.453-5.48-1.36-1.546-.907-2.76-2.2-3.64-3.88-.853-1.68-1.28-3.627-1.28-5.84 0-2.24.427-4.187 1.28-5.84.88-1.68 2.094-2.973 3.64-3.88 1.574-.907 3.4-1.36 5.48-1.36 1.68 0 3.227.32 4.64.96 1.414.64 2.56 1.56 3.44 2.76.907 1.2 1.454 2.6 1.64 4.2l-5.12.28Zm11.486-7.72.12 3.4c.534-1.227 1.307-2.173 2.32-2.84 1.04-.693 2.267-1.04 3.68-1.04 1.494 0 2.76.387 3.8 1.16 1.067.747 1.827 1.813 2.28 3.2.507-1.44 1.294-2.52 2.36-3.24 1.094-.747 2.414-1.12 3.96-1.12 1.414 0 2.64.307 3.68.92s1.84 1.52 2.4 2.72c.56 1.2.84 2.667.84 4.4V38h-4.96V25.92c0-1.813-.293-3.187-.88-4.12-.56-.96-1.413-1.44-2.56-1.44-.906 0-1.68.213-2.32.64-.64.427-1.133 1.053-1.48 1.88-.32.827-.48 1.84-.48 3.04V38h-4.56V25.92c0-1.2-.133-2.213-.4-3.04-.24-.827-.626-1.453-1.16-1.88-.506-.427-1.133-.64-1.88-.64-.906 0-1.68.227-2.32.68-.64.427-1.133 1.053-1.48 1.88-.32.827-.48 1.827-.48 3V38h-4.96V16.8h4.48Zm26.723 10.6c0-2.24.427-4.187 1.28-5.84.854-1.68 2.067-2.973 3.64-3.88 1.574-.907 3.4-1.36 5.48-1.36 1.84 0 3.494.413 4.96 1.24 1.467.827 2.64 2.08 3.52 3.76.88 1.653 1.347 3.693 1.4 6.12v1.32h-15.08c.107 1.813.614 3.227 1.52 4.24.907.987 2.134 1.48 3.68 1.48.987 0 1.88-.253 2.68-.76a4.803 4.803 0 0 0 1.84-2.2l5.08.36c-.64 2.027-1.84 3.64-3.6 4.84-1.733 1.173-3.733 1.76-6 1.76-2.08 0-3.906-.453-5.48-1.36-1.573-.907-2.786-2.2-3.64-3.88-.853-1.68-1.28-3.627-1.28-5.84Zm15.16-2.04c-.213-1.733-.76-3.013-1.64-3.84-.853-.827-1.893-1.24-3.12-1.24-1.44 0-2.6.453-3.48 1.36-.88.88-1.44 2.12-1.68 3.72h9.92ZM163.139 9.6V38h-5.04V9.6h5.04Zm8.322 7.2.24 5.88-.64-.36c.32-2.053 1.094-3.56 2.32-4.52 1.254-.987 2.787-1.48 4.6-1.48 2.32 0 4.107.733 5.36 2.2 1.254 1.44 1.88 3.387 1.88 5.84V38h-4.96V25.92c0-1.253-.12-2.28-.36-3.08-.24-.8-.64-1.413-1.2-1.84-.533-.427-1.253-.64-2.16-.64-1.44 0-2.573.48-3.4 1.44-.8.933-1.2 2.307-1.2 4.12V38h-4.96V16.8h4.48Zm30.003 7.72c-.186-1.307-.706-2.32-1.56-3.04-.853-.72-1.866-1.08-3.04-1.08-1.68 0-2.986.613-3.92 1.84-.906 1.227-1.36 2.947-1.36 5.16s.454 3.933 1.36 5.16c.934 1.227 2.24 1.84 3.92 1.84 1.254 0 2.307-.373 3.16-1.12.854-.773 1.387-1.867 1.6-3.28l5.12.24c-.186 1.68-.733 3.147-1.64 4.4-.906 1.227-2.08 2.173-3.52 2.84-1.413.667-2.986 1-4.72 1-2.08 0-3.906-.453-5.48-1.36-1.546-.907-2.76-2.2-3.64-3.88-.853-1.68-1.28-3.627-1.28-5.84 0-2.24.427-4.187 1.28-5.84.88-1.68 2.094-2.973 3.64-3.88 1.574-.907 3.4-1.36 5.48-1.36 1.68 0 3.227.32 4.64.96 1.414.64 2.56 1.56 3.44 2.76.907 1.2 1.454 2.6 1.64 4.2l-5.12.28Zm11.443 8.16V38h-5.6v-5.32h5.6Z"/><path fill="#171717" fill-rule="evenodd" d="m7.839 40.783 16.03-28.054L20 6 0 40.783h7.839Zm8.214 0H40L27.99 19.894l-4.02 7.032 3.976 6.914H20.02l-3.967 6.943Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

BIN
public/placeholder-user.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

BIN
public/placeholder.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

1
public/placeholder.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="1200" fill="none"><rect width="1200" height="1200" fill="#EAEAEA" rx="3"/><g opacity=".5"><g opacity=".5"><path fill="#FAFAFA" d="M600.709 736.5c-75.454 0-136.621-61.167-136.621-136.62 0-75.454 61.167-136.621 136.621-136.621 75.453 0 136.62 61.167 136.62 136.621 0 75.453-61.167 136.62-136.62 136.62Z"/><path stroke="#C9C9C9" stroke-width="2.418" d="M600.709 736.5c-75.454 0-136.621-61.167-136.621-136.62 0-75.454 61.167-136.621 136.621-136.621 75.453 0 136.62 61.167 136.62 136.621 0 75.453-61.167 136.62-136.62 136.62Z"/></g><path stroke="url(#a)" stroke-width="2.418" d="M0-1.209h553.581" transform="scale(1 -1) rotate(45 1163.11 91.165)"/><path stroke="url(#b)" stroke-width="2.418" d="M404.846 598.671h391.726"/><path stroke="url(#c)" stroke-width="2.418" d="M599.5 795.742V404.017"/><path stroke="url(#d)" stroke-width="2.418" d="m795.717 796.597-391.441-391.44"/><path fill="#fff" d="M600.709 656.704c-31.384 0-56.825-25.441-56.825-56.824 0-31.384 25.441-56.825 56.825-56.825 31.383 0 56.824 25.441 56.824 56.825 0 31.383-25.441 56.824-56.824 56.824Z"/><g clip-path="url(#e)"><path fill="#666" fill-rule="evenodd" d="M616.426 586.58h-31.434v16.176l3.553-3.554.531-.531h9.068l.074-.074 8.463-8.463h2.565l7.18 7.181V586.58Zm-15.715 14.654 3.698 3.699 1.283 1.282-2.565 2.565-1.282-1.283-5.2-5.199h-6.066l-5.514 5.514-.073.073v2.876a2.418 2.418 0 0 0 2.418 2.418h26.598a2.418 2.418 0 0 0 2.418-2.418v-8.317l-8.463-8.463-7.181 7.181-.071.072Zm-19.347 5.442v4.085a6.045 6.045 0 0 0 6.046 6.045h26.598a6.044 6.044 0 0 0 6.045-6.045v-7.108l1.356-1.355-1.282-1.283-.074-.073v-17.989h-38.689v23.43l-.146.146.146.147Z" clip-rule="evenodd"/></g><path stroke="#C9C9C9" stroke-width="2.418" d="M600.709 656.704c-31.384 0-56.825-25.441-56.825-56.824 0-31.384 25.441-56.825 56.825-56.825 31.383 0 56.824 25.441 56.824 56.825 0 31.383-25.441 56.824-56.824 56.824Z"/></g><defs><linearGradient id="a" x1="554.061" x2="-.48" y1=".083" y2=".087" gradientUnits="userSpaceOnUse"><stop stop-color="#C9C9C9" stop-opacity="0"/><stop offset=".208" stop-color="#C9C9C9"/><stop offset=".792" stop-color="#C9C9C9"/><stop offset="1" stop-color="#C9C9C9" stop-opacity="0"/></linearGradient><linearGradient id="b" x1="796.912" x2="404.507" y1="599.963" y2="599.965" gradientUnits="userSpaceOnUse"><stop stop-color="#C9C9C9" stop-opacity="0"/><stop offset=".208" stop-color="#C9C9C9"/><stop offset=".792" stop-color="#C9C9C9"/><stop offset="1" stop-color="#C9C9C9" stop-opacity="0"/></linearGradient><linearGradient id="c" x1="600.792" x2="600.794" y1="403.677" y2="796.082" gradientUnits="userSpaceOnUse"><stop stop-color="#C9C9C9" stop-opacity="0"/><stop offset=".208" stop-color="#C9C9C9"/><stop offset=".792" stop-color="#C9C9C9"/><stop offset="1" stop-color="#C9C9C9" stop-opacity="0"/></linearGradient><linearGradient id="d" x1="404.85" x2="796.972" y1="403.903" y2="796.02" gradientUnits="userSpaceOnUse"><stop stop-color="#C9C9C9" stop-opacity="0"/><stop offset=".208" stop-color="#C9C9C9"/><stop offset=".792" stop-color="#C9C9C9"/><stop offset="1" stop-color="#C9C9C9" stop-opacity="0"/></linearGradient><clipPath id="e"><path fill="#fff" d="M581.364 580.535h38.689v38.689h-38.689z"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

BIN
public/wow-bg.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

125
styles/globals.css Normal file
View File

@@ -0,0 +1,125 @@
@import 'tailwindcss';
@import 'tw-animate-css';
@custom-variant dark (&:is(.dark *));
:root {
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--destructive-foreground: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--radius: 0.625rem;
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.145 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.145 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.985 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.396 0.141 25.723);
--destructive-foreground: oklch(0.637 0.237 25.331);
--border: oklch(0.269 0 0);
--input: oklch(0.269 0 0);
--ring: oklch(0.439 0 0);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(0.269 0 0);
--sidebar-ring: oklch(0.439 0 0);
}
@theme inline {
--font-sans: 'Geist', 'Geist Fallback';
--font-mono: 'Geist Mono', 'Geist Mono Fallback';
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}

27
tsconfig.json Normal file
View File

@@ -0,0 +1,27 @@
{
"compilerOptions": {
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"target": "ES6",
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}