feat: setup frontend project with bun and dynamic data fetching

- Create new frontend project directory with Next.js 15 + React 19
- Migrate from pnpm to bun for faster package management
- Add TanStack Query + Axios for dynamic data fetching
- Create comprehensive Makefile with development commands
- Setup API layer with query hooks and error handling
- Configure environment variables and bun settings
- Add TypeScript type checking and project documentation
- Update CLAUDE.md with bun-specific development workflow
This commit is contained in:
xujiang
2025-07-08 15:28:26 +08:00
parent d06caee35a
commit 3d197eb7e3
87 changed files with 8329 additions and 0 deletions

63
frontend/app/globals.css Normal file
View File

@ -0,0 +1,63 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96%;
--secondary-foreground: 222.2 84% 4.9%;
--muted: 210 40% 96%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96%;
--accent-foreground: 222.2 84% 4.9%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 222.2 84% 4.9%;
--radius: 0.5rem;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}
/* Custom scrollbar */
::-webkit-scrollbar {
width: 6px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
}
::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: #a8a8a8;
}
/* Smooth transitions */
* {
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow,
transform, filter, backdrop-filter;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
}

39
frontend/app/layout.tsx Normal file
View File

@ -0,0 +1,39 @@
import type React from "react"
import type { Metadata } from "next"
import { Inter } from "next/font/google"
import "./globals.css"
import { QueryProvider } from "@/components/providers/query-provider"
import { ThemeProvider } from "@/components/theme-provider"
import { Toaster } from "@/components/ui/toaster"
const inter = Inter({ subsets: ["latin"] })
export const metadata: Metadata = {
title: "摄影作品集 - PhotoStudio",
description: "专业摄影师作品展示平台,记录世界的美好瞬间",
generator: 'v0.dev'
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="zh-CN">
<body className={inter.className}>
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
<QueryProvider>
{children}
<Toaster />
</QueryProvider>
</ThemeProvider>
</body>
</html>
)
}

153
frontend/app/page.tsx Normal file
View File

@ -0,0 +1,153 @@
"use client"
import { useState, useEffect, useMemo } from "react"
import { PhotoGallery } from "@/components/photo-gallery"
import { PhotoModal } from "@/components/photo-modal"
import { Navigation } from "@/components/navigation"
import { FilterBar } from "@/components/filter-bar"
import { LoadingSpinner } from "@/components/loading-spinner"
import { TimelineView } from "@/components/timeline-view"
import { AboutView } from "@/components/about-view"
import { ContactView } from "@/components/contact-view"
import { usePhotos, type Photo } from "@/lib/queries"
import { useToast } from "@/components/ui/use-toast"
export default function HomePage() {
const { data: photos = [], isLoading, error } = usePhotos()
const { toast } = useToast()
const [filteredPhotos, setFilteredPhotos] = useState<Photo[]>([])
const [selectedPhoto, setSelectedPhoto] = useState<Photo | null>(null)
const [activeCategory, setActiveCategory] = useState("all")
const [activeTab, setActiveTab] = useState("gallery")
useEffect(() => {
if (error) {
toast({
title: "数据加载失败",
description: "无法获取照片数据,请稍后重试",
variant: "destructive",
})
}
}, [error, toast])
useEffect(() => {
setFilteredPhotos(photos)
}, [photos])
const handleFilter = (category: string) => {
setActiveCategory(category)
if (category === "all") {
setFilteredPhotos(photos)
} else {
setFilteredPhotos(photos.filter((photo) => photo.category === category))
}
}
const handlePhotoClick = (photo: any) => {
setSelectedPhoto(photo)
}
const handleCloseModal = () => {
setSelectedPhoto(null)
}
const handlePrevPhoto = () => {
if (!selectedPhoto) return
const currentPhotos = activeTab === "gallery" ? filteredPhotos : photos
const currentIndex = currentPhotos.findIndex((p) => p.id === selectedPhoto.id)
const prevIndex = currentIndex > 0 ? currentIndex - 1 : currentPhotos.length - 1
setSelectedPhoto(currentPhotos[prevIndex])
}
const handleNextPhoto = () => {
if (!selectedPhoto) return
const currentPhotos = activeTab === "gallery" ? filteredPhotos : photos
const currentIndex = currentPhotos.findIndex((p) => p.id === selectedPhoto.id)
const nextIndex = currentIndex < currentPhotos.length - 1 ? currentIndex + 1 : 0
setSelectedPhoto(currentPhotos[nextIndex])
}
const handleTabChange = (tab: string) => {
setActiveTab(tab)
// Reset filters when switching tabs
if (tab === "timeline") {
setActiveCategory("all")
setFilteredPhotos(photos)
}
}
const handleContactClick = () => {
setActiveTab("contact")
}
const getPageTitle = () => {
switch (activeTab) {
case "gallery":
return "摄影作品集"
case "timeline":
return "创作时间线"
case "about":
return "关于我"
case "contact":
return "联系合作"
default:
return "摄影作品集"
}
}
const getPageDescription = () => {
switch (activeTab) {
case "gallery":
return "用镜头记录世界的美好瞬间,每一张照片都是时光的诗篇"
case "timeline":
return "按时间顺序回顾摄影创作历程,见证技艺与视角的成长轨迹"
case "about":
return "分享我的摄影故事,探索光影背后的创作理念与人生感悟"
case "contact":
return "期待与您合作,共同创造独特而珍贵的视觉记忆"
default:
return "用镜头记录世界的美好瞬间,每一张照片都是时光的诗篇"
}
}
if (isLoading) {
return <LoadingSpinner />
}
return (
<div className="min-h-screen bg-white">
<Navigation activeTab={activeTab} onTabChange={handleTabChange} />
<main className="container mx-auto px-4 py-8">
{(activeTab === "gallery" || activeTab === "timeline") && (
<div className="text-center mb-12">
<h1 className="text-4xl md:text-6xl font-light text-gray-900 mb-4">{getPageTitle()}</h1>
<p className="text-lg text-gray-600 max-w-2xl mx-auto">{getPageDescription()}</p>
</div>
)}
{activeTab === "gallery" && (
<>
<FilterBar activeCategory={activeCategory} onFilter={handleFilter} />
<PhotoGallery photos={filteredPhotos} onPhotoClick={handlePhotoClick} />
</>
)}
{activeTab === "timeline" && <TimelineView photos={photos} onPhotoClick={handlePhotoClick} />}
{activeTab === "about" && <AboutView onContactClick={handleContactClick} />}
{activeTab === "contact" && <ContactView />}
</main>
{selectedPhoto && (
<PhotoModal
photo={selectedPhoto}
onClose={handleCloseModal}
onPrev={handlePrevPhoto}
onNext={handleNextPhoto}
/>
)}
</div>
)
}