Initialized repository for chat Esports platform development
Co-authored-by: null <4804959+fnvtk@users.noreply.github.com>
30
README.md
Normal file
@@ -0,0 +1,30 @@
|
||||
# Esports platform development
|
||||
|
||||
*Automatically synced with your [v0.app](https://v0.app) deployments*
|
||||
|
||||
[](https://vercel.com/fnvtks-projects/v0--aa)
|
||||
[](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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,3 @@
|
||||
export default function Loading() {
|
||||
return null
|
||||
}
|
||||
235
app/mall/page.tsx
Normal 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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">
|
||||
进入艾泽拉斯 >
|
||||
</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 25人H全通团,来强力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
@@ -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">让你拥有3个AI员工:客服、企划、财务</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
@@ -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
@@ -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
@@ -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
@@ -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"
|
||||
}
|
||||
48
components/layout/mobile-nav.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
164
components/providers/app-provider.tsx
Normal 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
|
||||
}
|
||||
11
components/theme-provider.tsx
Normal 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,12 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
typescript: {
|
||||
ignoreBuildErrors: true,
|
||||
},
|
||||
images: {
|
||||
unoptimized: true,
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
export default nextConfig
|
||||
75
package.json
Normal 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
8
postcss.config.mjs
Normal file
@@ -0,0 +1,8 @@
|
||||
/** @type {import('postcss-load-config').Config} */
|
||||
const config = {
|
||||
plugins: {
|
||||
'@tailwindcss/postcss': {},
|
||||
},
|
||||
}
|
||||
|
||||
export default config
|
||||
BIN
public/abstract-geometric-shapes.png
Normal file
|
After Width: | Height: | Size: 649 KiB |
BIN
public/apple-icon.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
public/avatar-3d-style.jpg
Normal file
|
After Width: | Height: | Size: 69 KiB |
BIN
public/diverse-avatars.png
Normal file
|
After Width: | Height: | Size: 665 KiB |
BIN
public/diverse-group-avatars.png
Normal file
|
After Width: | Height: | Size: 581 KiB |
BIN
public/esports-gamer-girl-streaming-vertical.jpg
Normal file
|
After Width: | Height: | Size: 106 KiB |
BIN
public/esports-logo.png
Normal file
|
After Width: | Height: | Size: 1013 KiB |
BIN
public/esports-tournament-stadium-neon-lights-crowd.jpg
Normal file
|
After Width: | Height: | Size: 217 KiB |
BIN
public/icon-dark-32x32.png
Normal file
|
After Width: | Height: | Size: 585 B |
BIN
public/icon-light-32x32.png
Normal file
|
After Width: | Height: | Size: 566 B |
26
public/icon.svg
Normal 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
|
After Width: | Height: | Size: 29 KiB |
BIN
public/placeholder-logo.png
Normal file
|
After Width: | Height: | Size: 568 B |
1
public/placeholder-logo.svg
Normal 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
|
After Width: | Height: | Size: 1.6 KiB |
BIN
public/placeholder.jpg
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
1
public/placeholder.svg
Normal 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
|
After Width: | Height: | Size: 111 KiB |
125
styles/globals.css
Normal 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
@@ -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"]
|
||||
}
|
||||