feat: 首页支持宣传语
This commit is contained in:
@@ -7,6 +7,7 @@ import { Globe, Map } from "lucide-react";
|
|||||||
import { Navbar } from "@/components/layout/navbar";
|
import { Navbar } from "@/components/layout/navbar";
|
||||||
import { InstallBanner } from "@/components/layout/install-banner";
|
import { InstallBanner } from "@/components/layout/install-banner";
|
||||||
import { ParticleBg } from "@/components/layout/particle-bg";
|
import { ParticleBg } from "@/components/layout/particle-bg";
|
||||||
|
import { Hero } from "@/components/layout/hero";
|
||||||
import { GlobeView } from "@/components/globe/globe-view";
|
import { GlobeView } from "@/components/globe/globe-view";
|
||||||
import { StatsPanel } from "@/components/dashboard/stats-panel";
|
import { StatsPanel } from "@/components/dashboard/stats-panel";
|
||||||
import { ActivityTimeline } from "@/components/dashboard/activity-timeline";
|
import { ActivityTimeline } from "@/components/dashboard/activity-timeline";
|
||||||
@@ -45,6 +46,9 @@ export default function HomePage() {
|
|||||||
<Navbar />
|
<Navbar />
|
||||||
|
|
||||||
<main className="relative z-10 mx-auto max-w-[1800px] px-4 pt-20 pb-8">
|
<main className="relative z-10 mx-auto max-w-[1800px] px-4 pt-20 pb-8">
|
||||||
|
{/* Hero Section */}
|
||||||
|
<Hero />
|
||||||
|
|
||||||
{/* Section 1: Main Map View + Dashboard */}
|
{/* Section 1: Main Map View + Dashboard */}
|
||||||
<section className="min-h-[calc(100vh-5rem)]">
|
<section className="min-h-[calc(100vh-5rem)]">
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
incrementHourlyActivity,
|
incrementHourlyActivity,
|
||||||
publishEvent,
|
publishEvent,
|
||||||
} from "@/lib/redis";
|
} from "@/lib/redis";
|
||||||
import { validateApiKey } from "@/lib/auth/api-key";
|
import { authenticateRequest } from "@/lib/auth/request";
|
||||||
import { getGeoLocation } from "@/lib/geo/ip-location";
|
import { getGeoLocation } from "@/lib/geo/ip-location";
|
||||||
import { heartbeatSchema } from "@/lib/validators/schemas";
|
import { heartbeatSchema } from "@/lib/validators/schemas";
|
||||||
|
|
||||||
@@ -22,19 +22,11 @@ function getClientIp(req: NextRequest): string {
|
|||||||
|
|
||||||
export async function POST(req: NextRequest) {
|
export async function POST(req: NextRequest) {
|
||||||
try {
|
try {
|
||||||
const authHeader = req.headers.get("authorization");
|
const auth = await authenticateRequest(req);
|
||||||
if (!authHeader?.startsWith("Bearer ")) {
|
if (auth instanceof NextResponse) {
|
||||||
return NextResponse.json(
|
return auth;
|
||||||
{ error: "Missing or invalid authorization header" },
|
|
||||||
{ status: 401 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const apiKey = authHeader.slice(7);
|
|
||||||
const claw = await validateApiKey(apiKey);
|
|
||||||
if (!claw) {
|
|
||||||
return NextResponse.json({ error: "Invalid API key" }, { status: 401 });
|
|
||||||
}
|
}
|
||||||
|
const { claw } = auth;
|
||||||
|
|
||||||
let bodyData = {};
|
let bodyData = {};
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -7,24 +7,16 @@ import {
|
|||||||
incrementHourlyActivity,
|
incrementHourlyActivity,
|
||||||
publishEvent,
|
publishEvent,
|
||||||
} from "@/lib/redis";
|
} from "@/lib/redis";
|
||||||
import { validateApiKey } from "@/lib/auth/api-key";
|
import { authenticateRequest } from "@/lib/auth/request";
|
||||||
import { taskSchema } from "@/lib/validators/schemas";
|
import { taskSchema } from "@/lib/validators/schemas";
|
||||||
|
|
||||||
export async function POST(req: NextRequest) {
|
export async function POST(req: NextRequest) {
|
||||||
try {
|
try {
|
||||||
const authHeader = req.headers.get("authorization");
|
const auth = await authenticateRequest(req);
|
||||||
if (!authHeader?.startsWith("Bearer ")) {
|
if (auth instanceof NextResponse) {
|
||||||
return NextResponse.json(
|
return auth;
|
||||||
{ error: "Missing or invalid authorization header" },
|
|
||||||
{ status: 401 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const apiKey = authHeader.slice(7);
|
|
||||||
const claw = await validateApiKey(apiKey);
|
|
||||||
if (!claw) {
|
|
||||||
return NextResponse.json({ error: "Invalid API key" }, { status: 401 });
|
|
||||||
}
|
}
|
||||||
|
const { claw } = auth;
|
||||||
|
|
||||||
const body = await req.json();
|
const body = await req.json();
|
||||||
const parsed = taskSchema.safeParse(body);
|
const parsed = taskSchema.safeParse(body);
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ import { NextRequest, NextResponse } from "next/server";
|
|||||||
import { sql } from "drizzle-orm";
|
import { sql } from "drizzle-orm";
|
||||||
import { db } from "@/lib/db";
|
import { db } from "@/lib/db";
|
||||||
import { invalidateTokenLeaderboardCache } from "@/lib/redis";
|
import { invalidateTokenLeaderboardCache } from "@/lib/redis";
|
||||||
import { authenticateRequest, getTodayDateString } from "@/lib/utils";
|
import { authenticateRequest } from "@/lib/auth/request";
|
||||||
|
import { getTodayDateString } from "@/lib/utils";
|
||||||
import { tokenSchema } from "@/lib/validators/schemas";
|
import { tokenSchema } from "@/lib/validators/schemas";
|
||||||
|
|
||||||
export async function POST(req: NextRequest) {
|
export async function POST(req: NextRequest) {
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ import { NextRequest, NextResponse } from "next/server";
|
|||||||
import { eq, and, gte, sql } from "drizzle-orm";
|
import { eq, and, gte, sql } from "drizzle-orm";
|
||||||
import { db } from "@/lib/db";
|
import { db } from "@/lib/db";
|
||||||
import { tokenUsage } from "@/lib/db/schema";
|
import { tokenUsage } from "@/lib/db/schema";
|
||||||
import { authenticateRequest, getTodayDateString } from "@/lib/utils";
|
import { authenticateRequest } from "@/lib/auth/request";
|
||||||
|
import { getTodayDateString } from "@/lib/utils";
|
||||||
|
|
||||||
export async function GET(req: NextRequest) {
|
export async function GET(req: NextRequest) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -171,3 +171,80 @@ body {
|
|||||||
.maplibre-dark-popup .maplibregl-popup-close-button {
|
.maplibre-dark-popup .maplibregl-popup-close-button {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* === Hero Animations === */
|
||||||
|
|
||||||
|
/* Fade in animation */
|
||||||
|
@keyframes fade-in {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fade in and slide up animation */
|
||||||
|
@keyframes fade-in-up {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Glowing text pulse animation */
|
||||||
|
@keyframes glow-pulse {
|
||||||
|
0%, 100% {
|
||||||
|
text-shadow:
|
||||||
|
0 0 10px rgba(0, 240, 255, 0.5),
|
||||||
|
0 0 20px rgba(0, 240, 255, 0.3),
|
||||||
|
0 0 40px rgba(0, 240, 255, 0.1);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
text-shadow:
|
||||||
|
0 0 15px rgba(0, 240, 255, 0.6),
|
||||||
|
0 0 30px rgba(0, 240, 255, 0.4),
|
||||||
|
0 0 60px rgba(0, 240, 255, 0.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animation utility classes */
|
||||||
|
.animate-fade-in {
|
||||||
|
animation: fade-in 0.6s ease-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-fade-in-up {
|
||||||
|
animation: fade-in-up 0.6s ease-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Glowing text with pulse effect */
|
||||||
|
.glow-text-pulse {
|
||||||
|
color: var(--text-primary);
|
||||||
|
animation: glow-pulse 3s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animation delays */
|
||||||
|
.animation-delay-200 {
|
||||||
|
animation-delay: 0.2s;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animation-delay-400 {
|
||||||
|
animation-delay: 0.4s;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Accessibility: Respect reduced motion preferences */
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.animate-fade-in,
|
||||||
|
.animate-fade-in-up,
|
||||||
|
.glow-text-pulse {
|
||||||
|
animation: none;
|
||||||
|
opacity: 1;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
48
components/layout/hero.tsx
Normal file
48
components/layout/hero.tsx
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
import { ArrowRight, Sparkles } from "lucide-react";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
|
||||||
|
export function Hero() {
|
||||||
|
const t = useTranslations("hero");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="relative py-16 md:py-24">
|
||||||
|
<div className="mx-auto max-w-4xl px-4 text-center">
|
||||||
|
{/* Badge */}
|
||||||
|
<Badge className="mb-6 px-4 py-1.5 text-sm animate-fade-in">
|
||||||
|
{t("badge")}
|
||||||
|
</Badge>
|
||||||
|
|
||||||
|
{/* Main Title */}
|
||||||
|
<h1 className="mb-6 text-4xl font-bold leading-tight tracking-tight md:text-5xl lg:text-6xl glow-text-pulse animate-fade-in-up">
|
||||||
|
{t("title")}
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
{/* Subtitle */}
|
||||||
|
<p className="mb-8 text-lg md:text-xl text-[var(--text-secondary)] max-w-2xl mx-auto animate-fade-in-up animation-delay-200">
|
||||||
|
{t("subtitle")}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* CTA Buttons */}
|
||||||
|
<div className="flex flex-col sm:flex-row items-center justify-center gap-4 animate-fade-in-up animation-delay-400">
|
||||||
|
<a
|
||||||
|
href="#get-started"
|
||||||
|
className="group inline-flex items-center gap-2 rounded-lg bg-[var(--accent-cyan)] px-6 py-3 font-semibold text-[var(--bg-primary)] transition-all hover:shadow-[0_0_20px_rgba(0,240,255,0.4)] hover:scale-105"
|
||||||
|
>
|
||||||
|
<Sparkles className="h-4 w-4" />
|
||||||
|
{t("cta")}
|
||||||
|
<ArrowRight className="h-4 w-4 transition-transform group-hover:translate-x-1" />
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="#learn-more"
|
||||||
|
className="inline-flex items-center gap-2 rounded-lg border border-[var(--accent-cyan)]/30 bg-transparent px-6 py-3 font-semibold text-[var(--text-primary)] transition-all hover:border-[var(--accent-cyan)]/60 hover:bg-[var(--accent-cyan)]/5"
|
||||||
|
>
|
||||||
|
{t("secondary")}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
24
lib/auth/request.ts
Normal file
24
lib/auth/request.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import { validateApiKey } from "@/lib/auth/api-key";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authenticate an API request with Bearer token
|
||||||
|
* Returns the claw object if authenticated, or a 401 NextResponse if not
|
||||||
|
*/
|
||||||
|
export async function authenticateRequest(req: NextRequest) {
|
||||||
|
const authHeader = req.headers.get("authorization");
|
||||||
|
if (!authHeader?.startsWith("Bearer ")) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Missing or invalid authorization header" },
|
||||||
|
{ status: 401 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiKey = authHeader.slice(7);
|
||||||
|
const claw = await validateApiKey(apiKey);
|
||||||
|
if (!claw) {
|
||||||
|
return NextResponse.json({ error: "Invalid API key" }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
return { claw };
|
||||||
|
}
|
||||||
24
lib/utils.ts
24
lib/utils.ts
@@ -1,7 +1,5 @@
|
|||||||
import { type ClassValue, clsx } from "clsx";
|
import { type ClassValue, clsx } from "clsx";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
import { NextRequest, NextResponse } from "next/server";
|
|
||||||
import { validateApiKey } from "@/lib/auth/api-key";
|
|
||||||
|
|
||||||
export function cn(...inputs: ClassValue[]) {
|
export function cn(...inputs: ClassValue[]) {
|
||||||
return twMerge(clsx(inputs));
|
return twMerge(clsx(inputs));
|
||||||
@@ -13,25 +11,3 @@ export function cn(...inputs: ClassValue[]) {
|
|||||||
export function getTodayDateString(): string {
|
export function getTodayDateString(): string {
|
||||||
return new Date().toISOString().split("T")[0];
|
return new Date().toISOString().split("T")[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Authenticate an API request with Bearer token
|
|
||||||
* Returns the claw object if authenticated, or a 401 NextResponse if not
|
|
||||||
*/
|
|
||||||
export async function authenticateRequest(req: NextRequest) {
|
|
||||||
const authHeader = req.headers.get("authorization");
|
|
||||||
if (!authHeader?.startsWith("Bearer ")) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: "Missing or invalid authorization header" },
|
|
||||||
{ status: 401 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const apiKey = authHeader.slice(7);
|
|
||||||
const claw = await validateApiKey(apiKey);
|
|
||||||
if (!claw) {
|
|
||||||
return NextResponse.json({ error: "Invalid API key" }, { status: 401 });
|
|
||||||
}
|
|
||||||
|
|
||||||
return { claw };
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -3,6 +3,13 @@
|
|||||||
"title": "OpenClaw Market - Global Claw Activity",
|
"title": "OpenClaw Market - Global Claw Activity",
|
||||||
"description": "Real-time visualization of AI agent activity worldwide"
|
"description": "Real-time visualization of AI agent activity worldwide"
|
||||||
},
|
},
|
||||||
|
"hero": {
|
||||||
|
"badge": "🦞 OpenClaw Market",
|
||||||
|
"title": "A Community Built for Claw Agents",
|
||||||
|
"subtitle": "Share your Agent prompts, skills, and tuning tips. Learn from the community and level up your AI workflow.",
|
||||||
|
"cta": "Get Started",
|
||||||
|
"secondary": "Learn More"
|
||||||
|
},
|
||||||
"navbar": {
|
"navbar": {
|
||||||
"brand": "OpenClaw Market",
|
"brand": "OpenClaw Market",
|
||||||
"globe": "3D Globe",
|
"globe": "3D Globe",
|
||||||
|
|||||||
@@ -3,6 +3,13 @@
|
|||||||
"title": "OpenClaw Market - 全球龙虾活动",
|
"title": "OpenClaw Market - 全球龙虾活动",
|
||||||
"description": "全球 AI 代理活动实时可视化"
|
"description": "全球 AI 代理活动实时可视化"
|
||||||
},
|
},
|
||||||
|
"hero": {
|
||||||
|
"badge": "🦞 OpenClaw Market",
|
||||||
|
"title": "专为 Claw Agent 打造的社区",
|
||||||
|
"subtitle": "分享你的 Agent 提示词、技能和调教技巧。从社区学习,提升你的 AI 工作流。",
|
||||||
|
"cta": "立即开始",
|
||||||
|
"secondary": "了解更多"
|
||||||
|
},
|
||||||
"navbar": {
|
"navbar": {
|
||||||
"brand": "OpenClaw Market",
|
"brand": "OpenClaw Market",
|
||||||
"globe": "3D 地球",
|
"globe": "3D 地球",
|
||||||
|
|||||||
Reference in New Issue
Block a user