feat(frontend): Add smooth page transitions and animations
Some checks are pending
Deploy Elmeg / deploy (push) Waiting to run
Some checks are pending
Deploy Elmeg / deploy (push) Waiting to run
This commit is contained in:
parent
cb91b5ad6d
commit
1ece5a037e
2 changed files with 165 additions and 88 deletions
|
|
@ -8,6 +8,7 @@ import Link from "next/link"
|
||||||
import { Star, MapPin, Music, User, Trophy, Calendar, Sparkles } from "lucide-react"
|
import { Star, MapPin, Music, User, Trophy, Calendar, Sparkles } from "lucide-react"
|
||||||
import { Badge } from "@/components/ui/badge"
|
import { Badge } from "@/components/ui/badge"
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
|
||||||
|
import { motion, AnimatePresence } from "framer-motion"
|
||||||
|
|
||||||
interface TopShow {
|
interface TopShow {
|
||||||
show: { id: number; date: string; venue_id: number }
|
show: { id: number; date: string; venue_id: number }
|
||||||
|
|
@ -87,14 +88,19 @@ export default function LeaderboardsPage() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container py-10 space-y-8 max-w-5xl">
|
<div className="container py-10 space-y-8 max-w-5xl">
|
||||||
<div className="flex flex-col gap-2">
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: -20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.5 }}
|
||||||
|
className="flex flex-col gap-2"
|
||||||
|
>
|
||||||
<h1 className="text-4xl font-bold tracking-tight bg-gradient-to-r from-primary to-primary/60 bg-clip-text text-transparent">
|
<h1 className="text-4xl font-bold tracking-tight bg-gradient-to-r from-primary to-primary/60 bg-clip-text text-transparent">
|
||||||
Leaderboards
|
Leaderboards
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-xl text-muted-foreground">
|
<p className="text-xl text-muted-foreground">
|
||||||
Discover the highest rated shows, legendary jams, and top contributors.
|
Discover the highest rated shows, legendary jams, and top contributors.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</motion.div>
|
||||||
|
|
||||||
<Tabs defaultValue="jams" className="w-full">
|
<Tabs defaultValue="jams" className="w-full">
|
||||||
<TabsList className="grid w-full grid-cols-4 mb-8 h-12">
|
<TabsList className="grid w-full grid-cols-4 mb-8 h-12">
|
||||||
|
|
@ -106,11 +112,19 @@ export default function LeaderboardsPage() {
|
||||||
|
|
||||||
{/* HEADY JAMS CONTENT */}
|
{/* HEADY JAMS CONTENT */}
|
||||||
<TabsContent value="jams">
|
<TabsContent value="jams">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, x: -10 }}
|
||||||
|
animate={{ opacity: 1, x: 0 }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
>
|
||||||
<Card className="border-none shadow-none bg-transparent">
|
<Card className="border-none shadow-none bg-transparent">
|
||||||
<div className="grid gap-4">
|
<div className="grid gap-4">
|
||||||
{topPerformances.map((item, i) => (
|
{topPerformances.map((item, i) => (
|
||||||
<div
|
<motion.div
|
||||||
key={item.performance.id}
|
key={item.performance.id}
|
||||||
|
initial={{ opacity: 0, y: 10 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: i * 0.05 }}
|
||||||
className="group relative flex items-center gap-4 rounded-xl border bg-card p-4 shadow-sm transition-all hover:shadow-md hover:border-primary/50"
|
className="group relative flex items-center gap-4 rounded-xl border bg-card p-4 shadow-sm transition-all hover:shadow-md hover:border-primary/50"
|
||||||
>
|
>
|
||||||
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-secondary/50 font-bold text-xl">
|
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-secondary/50 font-bold text-xl">
|
||||||
|
|
@ -145,7 +159,7 @@ export default function LeaderboardsPage() {
|
||||||
</div>
|
</div>
|
||||||
<span className="text-xs text-muted-foreground">{item.rating_count} votes</span>
|
<span className="text-xs text-muted-foreground">{item.rating_count} votes</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</motion.div>
|
||||||
))}
|
))}
|
||||||
{topPerformances.length === 0 && (
|
{topPerformances.length === 0 && (
|
||||||
<div className="text-center py-12 text-muted-foreground">
|
<div className="text-center py-12 text-muted-foreground">
|
||||||
|
|
@ -154,13 +168,25 @@ export default function LeaderboardsPage() {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
</motion.div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
{/* TOP SHOWS CONTENT */}
|
{/* TOP SHOWS CONTENT */}
|
||||||
<TabsContent value="shows">
|
<TabsContent value="shows">
|
||||||
<div className="grid gap-4">
|
<motion.div
|
||||||
|
initial={{ opacity: 0, x: -10 }}
|
||||||
|
animate={{ opacity: 1, x: 0 }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
className="grid gap-4"
|
||||||
|
>
|
||||||
{topShows.map((item, i) => (
|
{topShows.map((item, i) => (
|
||||||
<div key={item.show.id} className="flex items-center justify-between p-4 rounded-lg border bg-card hover:bg-accent/50 transition-colors">
|
<motion.div
|
||||||
|
key={item.show.id}
|
||||||
|
initial={{ opacity: 0, y: 10 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: i * 0.05 }}
|
||||||
|
className="flex items-center justify-between p-4 rounded-lg border bg-card hover:bg-accent/50 transition-colors"
|
||||||
|
>
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<div className="font-mono text-muted-foreground w-6 text-center">{i + 1}</div>
|
<div className="font-mono text-muted-foreground w-6 text-center">{i + 1}</div>
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -181,16 +207,27 @@ export default function LeaderboardsPage() {
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-muted-foreground">{item.review_count} ratings</div>
|
<div className="text-xs text-muted-foreground">{item.review_count} ratings</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</motion.div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</motion.div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
{/* VENUES CONTENT */}
|
{/* VENUES CONTENT */}
|
||||||
<TabsContent value="venues">
|
<TabsContent value="venues">
|
||||||
<div className="grid gap-4 md:grid-cols-2">
|
<motion.div
|
||||||
|
initial={{ opacity: 0, x: -10 }}
|
||||||
|
animate={{ opacity: 1, x: 0 }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
className="grid gap-4 md:grid-cols-2"
|
||||||
|
>
|
||||||
{topVenues.map((item, i) => (
|
{topVenues.map((item, i) => (
|
||||||
<div key={item.venue.id} className="flex items-center justify-between p-4 rounded-lg border bg-card/50">
|
<motion.div
|
||||||
|
key={item.venue.id}
|
||||||
|
initial={{ opacity: 0, scale: 0.95 }}
|
||||||
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
|
transition={{ delay: i * 0.05 }}
|
||||||
|
className="flex items-center justify-between p-4 rounded-lg border bg-card/50"
|
||||||
|
>
|
||||||
<div className="flex items-center gap-3 overflow-hidden">
|
<div className="flex items-center gap-3 overflow-hidden">
|
||||||
<div className={`
|
<div className={`
|
||||||
flex h-8 w-8 items-center justify-center rounded-full text-sm font-bold shrink-0
|
flex h-8 w-8 items-center justify-center rounded-full text-sm font-bold shrink-0
|
||||||
|
|
@ -211,16 +248,27 @@ export default function LeaderboardsPage() {
|
||||||
<Star className="h-3 w-3 fill-current" />
|
<Star className="h-3 w-3 fill-current" />
|
||||||
{item.avg_score.toFixed(2)}
|
{item.avg_score.toFixed(2)}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</motion.div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</motion.div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
{/* USERS CONTENT */}
|
{/* USERS CONTENT */}
|
||||||
<TabsContent value="users">
|
<TabsContent value="users">
|
||||||
<div className="grid gap-4 md:grid-cols-3">
|
<motion.div
|
||||||
|
initial={{ opacity: 0, x: -10 }}
|
||||||
|
animate={{ opacity: 1, x: 0 }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
className="grid gap-4 md:grid-cols-3"
|
||||||
|
>
|
||||||
{topUsers.map((item, i) => (
|
{topUsers.map((item, i) => (
|
||||||
<Card key={item.profile.id} className="flex flex-col items-center justify-center p-6 text-center hover:border-primary/50 transition-colors">
|
<motion.div
|
||||||
|
key={item.profile.id}
|
||||||
|
initial={{ opacity: 0, y: 10 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: i * 0.05 }}
|
||||||
|
>
|
||||||
|
<Card className="flex flex-col items-center justify-center p-6 text-center hover:border-primary/50 transition-colors h-full">
|
||||||
<Avatar className="h-16 w-16 mb-4">
|
<Avatar className="h-16 w-16 mb-4">
|
||||||
<AvatarImage src={`https://api.dicebear.com/7.x/notionists/svg?seed=${item.profile.user_id}`} />
|
<AvatarImage src={`https://api.dicebear.com/7.x/notionists/svg?seed=${item.profile.user_id}`} />
|
||||||
<AvatarFallback>U</AvatarFallback>
|
<AvatarFallback>U</AvatarFallback>
|
||||||
|
|
@ -231,9 +279,10 @@ export default function LeaderboardsPage() {
|
||||||
{item.review_count} Reviews
|
{item.review_count} Reviews
|
||||||
</Badge>
|
</Badge>
|
||||||
</Card>
|
</Card>
|
||||||
|
</motion.div>
|
||||||
))}
|
))}
|
||||||
{topUsers.length === 0 && <p className="col-span-3 text-center text-muted-foreground py-10">No active users yet.</p>}
|
{topUsers.length === 0 && <p className="col-span-3 text-center text-muted-foreground py-10">No active users yet.</p>}
|
||||||
</div>
|
</motion.div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import { UserAttendanceList } from "@/components/profile/user-attendance-list"
|
||||||
import { UserReviewsList } from "@/components/profile/user-reviews-list"
|
import { UserReviewsList } from "@/components/profile/user-reviews-list"
|
||||||
import { UserGroupsList } from "@/components/profile/user-groups-list"
|
import { UserGroupsList } from "@/components/profile/user-groups-list"
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
|
||||||
|
import { motion } from "framer-motion"
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
interface UserProfile {
|
interface UserProfile {
|
||||||
|
|
@ -105,7 +106,12 @@ export default function ProfilePage() {
|
||||||
return (
|
return (
|
||||||
<div className="container py-10 max-w-5xl space-y-8">
|
<div className="container py-10 max-w-5xl space-y-8">
|
||||||
{/* Header Section */}
|
{/* Header Section */}
|
||||||
<div className="relative">
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: -20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.5 }}
|
||||||
|
className="relative"
|
||||||
|
>
|
||||||
<div className="absolute right-0 top-0">
|
<div className="absolute right-0 top-0">
|
||||||
<Link href="/settings">
|
<Link href="/settings">
|
||||||
<Button variant="outline" size="sm" className="gap-2">
|
<Button variant="outline" size="sm" className="gap-2">
|
||||||
|
|
@ -159,7 +165,7 @@ export default function ProfilePage() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</motion.div>
|
||||||
|
|
||||||
<Tabs defaultValue="overview" className="w-full">
|
<Tabs defaultValue="overview" className="w-full">
|
||||||
<TabsList className="grid w-full grid-cols-4 mb-8 h-12">
|
<TabsList className="grid w-full grid-cols-4 mb-8 h-12">
|
||||||
|
|
@ -170,6 +176,11 @@ export default function ProfilePage() {
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
<TabsContent value="overview" className="space-y-6">
|
<TabsContent value="overview" className="space-y-6">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, x: -10 }}
|
||||||
|
animate={{ opacity: 1, x: 0 }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
>
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="flex items-center gap-2">
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
|
@ -184,20 +195,37 @@ export default function ProfilePage() {
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
</motion.div>
|
||||||
{/* Recent Activity or generic stats summary could go here later */}
|
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="attendance">
|
<TabsContent value="attendance">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, x: -10 }}
|
||||||
|
animate={{ opacity: 1, x: 0 }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
>
|
||||||
<UserAttendanceList userId={user.id} />
|
<UserAttendanceList userId={user.id} />
|
||||||
|
</motion.div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="reviews">
|
<TabsContent value="reviews">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, x: -10 }}
|
||||||
|
animate={{ opacity: 1, x: 0 }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
>
|
||||||
<UserReviewsList userId={user.id} />
|
<UserReviewsList userId={user.id} />
|
||||||
|
</motion.div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="groups">
|
<TabsContent value="groups">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, x: -10 }}
|
||||||
|
animate={{ opacity: 1, x: 0 }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
>
|
||||||
<UserGroupsList userId={user.id} />
|
<UserGroupsList userId={user.id} />
|
||||||
|
</motion.div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue