feat: add auth pages, update navbar, fix leaderboards venue display, and fix api-config
This commit is contained in:
parent
2c9301ae14
commit
3873ddfe8f
9 changed files with 425 additions and 29 deletions
|
|
@ -21,11 +21,13 @@ def get_top_shows(limit: int = 10, session: Session = Depends(get_session)):
|
||||||
query = (
|
query = (
|
||||||
select(
|
select(
|
||||||
Show,
|
Show,
|
||||||
|
Venue,
|
||||||
func.avg(Review.score).label("avg_score"),
|
func.avg(Review.score).label("avg_score"),
|
||||||
func.count(Review.id).label("review_count")
|
func.count(Review.id).label("review_count")
|
||||||
)
|
)
|
||||||
.join(Review, Review.show_id == Show.id)
|
.join(Review, Review.show_id == Show.id)
|
||||||
.group_by(Show.id)
|
.join(Venue, Show.venue_id == Venue.id)
|
||||||
|
.group_by(Show.id, Venue.id)
|
||||||
.having(func.count(Review.id) >= 1)
|
.having(func.count(Review.id) >= 1)
|
||||||
.order_by(desc("avg_score"), desc("review_count"))
|
.order_by(desc("avg_score"), desc("review_count"))
|
||||||
.limit(limit)
|
.limit(limit)
|
||||||
|
|
@ -36,10 +38,11 @@ def get_top_shows(limit: int = 10, session: Session = Depends(get_session)):
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
"show": show,
|
"show": show,
|
||||||
|
"venue": venue,
|
||||||
"avg_score": round(score, 2),
|
"avg_score": round(score, 2),
|
||||||
"review_count": count
|
"review_count": count
|
||||||
}
|
}
|
||||||
for show, score, count in results
|
for show, venue, score, count in results
|
||||||
]
|
]
|
||||||
|
|
||||||
@router.get("/venues/top")
|
@router.get("/venues/top")
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import "./globals.css";
|
||||||
import { Navbar } from "@/components/layout/navbar";
|
import { Navbar } from "@/components/layout/navbar";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { PreferencesProvider } from "@/contexts/preferences-context";
|
import { PreferencesProvider } from "@/contexts/preferences-context";
|
||||||
|
import { AuthProvider } from "@/contexts/auth-context";
|
||||||
|
|
||||||
const inter = Inter({ subsets: ["latin"] });
|
const inter = Inter({ subsets: ["latin"] });
|
||||||
|
|
||||||
|
|
@ -20,12 +21,14 @@ export default function RootLayout({
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<body className={cn(inter.className, "min-h-screen bg-background font-sans antialiased")}>
|
<body className={cn(inter.className, "min-h-screen bg-background font-sans antialiased")}>
|
||||||
<PreferencesProvider>
|
<AuthProvider>
|
||||||
<Navbar />
|
<PreferencesProvider>
|
||||||
<main className="container py-6">
|
<Navbar />
|
||||||
{children}
|
<main className="container py-6">
|
||||||
</main>
|
{children}
|
||||||
</PreferencesProvider>
|
</main>
|
||||||
|
</PreferencesProvider>
|
||||||
|
</AuthProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,12 @@ interface TopShow {
|
||||||
date: string
|
date: string
|
||||||
venue_id: number
|
venue_id: number
|
||||||
}
|
}
|
||||||
|
venue: {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
city: string
|
||||||
|
state: string
|
||||||
|
}
|
||||||
avg_score: number
|
avg_score: number
|
||||||
review_count: number
|
review_count: number
|
||||||
}
|
}
|
||||||
|
|
@ -91,13 +97,16 @@ export default function LeaderboardsPage() {
|
||||||
<div key={item.show.id} className="flex items-center justify-between">
|
<div key={item.show.id} className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<span className={`flex h-6 w-6 items-center justify-center rounded-full text-xs font-bold ${i === 0 ? "bg-yellow-100 text-yellow-700" :
|
<span className={`flex h-6 w-6 items-center justify-center rounded-full text-xs font-bold ${i === 0 ? "bg-yellow-100 text-yellow-700" :
|
||||||
i === 1 ? "bg-gray-100 text-gray-700" :
|
i === 1 ? "bg-gray-100 text-gray-700" :
|
||||||
i === 2 ? "bg-orange-100 text-orange-700" : "text-muted-foreground"
|
i === 2 ? "bg-orange-100 text-orange-700" : "text-muted-foreground"
|
||||||
}`}>
|
}`}>
|
||||||
{i + 1}
|
{i + 1}
|
||||||
</span>
|
</span>
|
||||||
<Link href={`/shows/${item.show.id}`} className="font-medium hover:underline">
|
<Link href={`/shows/${item.show.id}`} className="font-medium hover:underline block">
|
||||||
{new Date(item.show.date).toLocaleDateString()}
|
{new Date(item.show.date).toLocaleDateString()}
|
||||||
|
<span className="text-sm font-normal text-muted-foreground ml-2">
|
||||||
|
{item.venue?.name ? `${item.venue.name} (${item.venue.city}, ${item.venue.state})` : ""}
|
||||||
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1 text-sm">
|
<div className="flex items-center gap-1 text-sm">
|
||||||
|
|
@ -125,8 +134,8 @@ export default function LeaderboardsPage() {
|
||||||
<div key={item.venue.id} className="flex items-center justify-between">
|
<div key={item.venue.id} className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-3 overflow-hidden">
|
<div className="flex items-center gap-3 overflow-hidden">
|
||||||
<span className={`flex h-6 w-6 shrink-0 items-center justify-center rounded-full text-xs font-bold ${i === 0 ? "bg-yellow-100 text-yellow-700" :
|
<span className={`flex h-6 w-6 shrink-0 items-center justify-center rounded-full text-xs font-bold ${i === 0 ? "bg-yellow-100 text-yellow-700" :
|
||||||
i === 1 ? "bg-gray-100 text-gray-700" :
|
i === 1 ? "bg-gray-100 text-gray-700" :
|
||||||
i === 2 ? "bg-orange-100 text-orange-700" : "text-muted-foreground"
|
i === 2 ? "bg-orange-100 text-orange-700" : "text-muted-foreground"
|
||||||
}`}>
|
}`}>
|
||||||
{i + 1}
|
{i + 1}
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -159,8 +168,8 @@ export default function LeaderboardsPage() {
|
||||||
<div key={item.profile.id} className="flex items-center justify-between">
|
<div key={item.profile.id} className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<span className={`flex h-6 w-6 items-center justify-center rounded-full text-xs font-bold ${i === 0 ? "bg-yellow-100 text-yellow-700" :
|
<span className={`flex h-6 w-6 items-center justify-center rounded-full text-xs font-bold ${i === 0 ? "bg-yellow-100 text-yellow-700" :
|
||||||
i === 1 ? "bg-gray-100 text-gray-700" :
|
i === 1 ? "bg-gray-100 text-gray-700" :
|
||||||
i === 2 ? "bg-orange-100 text-orange-700" : "text-muted-foreground"
|
i === 2 ? "bg-orange-100 text-orange-700" : "text-muted-foreground"
|
||||||
}`}>
|
}`}>
|
||||||
{i + 1}
|
{i + 1}
|
||||||
</span>
|
</span>
|
||||||
|
|
|
||||||
105
frontend/app/login/page.tsx
Normal file
105
frontend/app/login/page.tsx
Normal file
|
|
@ -0,0 +1,105 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useState } from "react"
|
||||||
|
import { useAuth } from "@/contexts/auth-context"
|
||||||
|
import { useRouter } from "next/navigation"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
import { Input } from "@/components/ui/input"
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle, CardDescription, CardFooter } from "@/components/ui/card"
|
||||||
|
import { Label } from "@/components/ui/label"
|
||||||
|
import { getApiUrl } from "@/lib/api-config"
|
||||||
|
import Link from "next/link"
|
||||||
|
|
||||||
|
export default function LoginPage() {
|
||||||
|
const [email, setEmail] = useState("")
|
||||||
|
const [password, setPassword] = useState("")
|
||||||
|
const [error, setError] = useState("")
|
||||||
|
const { login } = useAuth()
|
||||||
|
const router = useRouter()
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
setLoading(true)
|
||||||
|
setError("")
|
||||||
|
|
||||||
|
try {
|
||||||
|
const formData = new URLSearchParams()
|
||||||
|
formData.append('username', email)
|
||||||
|
formData.append('password', password)
|
||||||
|
|
||||||
|
const res = await fetch(`${getApiUrl()}/auth/token`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
|
},
|
||||||
|
body: formData,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const data = await res.json()
|
||||||
|
throw new Error(data.detail || "Login failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await res.json()
|
||||||
|
await login(data.access_token)
|
||||||
|
router.push("/")
|
||||||
|
} catch (err: any) {
|
||||||
|
setError(err.message)
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center min-h-[60vh]">
|
||||||
|
<Card className="w-full max-w-md">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Sign In</CardTitle>
|
||||||
|
<CardDescription>Enter your email and password to access your account</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
{error && (
|
||||||
|
<div className="bg-destructive/15 text-destructive text-sm p-3 rounded-md">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="email">Email</Label>
|
||||||
|
<Input
|
||||||
|
id="email"
|
||||||
|
type="email"
|
||||||
|
placeholder="name@example.com"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="password">Password</Label>
|
||||||
|
<Input
|
||||||
|
id="password"
|
||||||
|
type="password"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
<CardFooter className="flex flex-col gap-4">
|
||||||
|
<Button type="submit" className="w-full" disabled={loading}>
|
||||||
|
{loading ? "Signing in..." : "Sign In"}
|
||||||
|
</Button>
|
||||||
|
<p className="text-sm text-center text-muted-foreground">
|
||||||
|
Don't have an account?{" "}
|
||||||
|
<Link href="/register" className="text-primary hover:underline">
|
||||||
|
Sign up
|
||||||
|
</Link>
|
||||||
|
</p>
|
||||||
|
</CardFooter>
|
||||||
|
</form>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
130
frontend/app/register/page.tsx
Normal file
130
frontend/app/register/page.tsx
Normal file
|
|
@ -0,0 +1,130 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useState } from "react"
|
||||||
|
import { useRouter } from "next/navigation"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
import { Input } from "@/components/ui/input"
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle, CardDescription, CardFooter } from "@/components/ui/card"
|
||||||
|
import { Label } from "@/components/ui/label"
|
||||||
|
import { getApiUrl } from "@/lib/api-config"
|
||||||
|
import Link from "next/link"
|
||||||
|
import { useAuth } from "@/contexts/auth-context"
|
||||||
|
|
||||||
|
export default function RegisterPage() {
|
||||||
|
const [email, setEmail] = useState("")
|
||||||
|
const [username, setUsername] = useState("")
|
||||||
|
const [password, setPassword] = useState("")
|
||||||
|
const [error, setError] = useState("")
|
||||||
|
const router = useRouter()
|
||||||
|
const { login } = useAuth()
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
setLoading(true)
|
||||||
|
setError("")
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. Register
|
||||||
|
const res = await fetch(`${getApiUrl()}/auth/register`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ email, username, password }),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const data = await res.json()
|
||||||
|
throw new Error(data.detail || "Registration failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Login automatically
|
||||||
|
const formData = new URLSearchParams()
|
||||||
|
formData.append('username', email)
|
||||||
|
formData.append('password', password)
|
||||||
|
|
||||||
|
const loginRes = await fetch(`${getApiUrl()}/auth/token`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||||
|
body: formData,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (loginRes.ok) {
|
||||||
|
const loginData = await loginRes.json()
|
||||||
|
await login(loginData.access_token)
|
||||||
|
router.push("/")
|
||||||
|
} else {
|
||||||
|
router.push("/login")
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (err: any) {
|
||||||
|
setError(err.message)
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center min-h-[60vh]">
|
||||||
|
<Card className="w-full max-w-md">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Create Account</CardTitle>
|
||||||
|
<CardDescription>Join the community to track shows and rate sets</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
{error && (
|
||||||
|
<div className="bg-destructive/15 text-destructive text-sm p-3 rounded-md">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="email">Email</Label>
|
||||||
|
<Input
|
||||||
|
id="email"
|
||||||
|
type="email"
|
||||||
|
placeholder="name@example.com"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="username">Username</Label>
|
||||||
|
<Input
|
||||||
|
id="username"
|
||||||
|
type="text"
|
||||||
|
placeholder="GooseFan123"
|
||||||
|
value={username}
|
||||||
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="password">Password</Label>
|
||||||
|
<Input
|
||||||
|
id="password"
|
||||||
|
type="password"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
<CardFooter className="flex flex-col gap-4">
|
||||||
|
<Button type="submit" className="w-full" disabled={loading}>
|
||||||
|
{loading ? "Creating account..." : "Sign Up"}
|
||||||
|
</Button>
|
||||||
|
<p className="text-sm text-center text-muted-foreground">
|
||||||
|
Already have an account?{" "}
|
||||||
|
<Link href="/login" className="text-primary hover:underline">
|
||||||
|
Sign in
|
||||||
|
</Link>
|
||||||
|
</p>
|
||||||
|
</CardFooter>
|
||||||
|
</form>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
|
"use client"
|
||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
import { Music, Search, User, ChevronDown } from "lucide-react"
|
import { Music, User, ChevronDown } from "lucide-react"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { SearchDialog } from "@/components/ui/search-dialog"
|
import { SearchDialog } from "@/components/ui/search-dialog"
|
||||||
import { NotificationBell } from "@/components/layout/notification-bell"
|
import { NotificationBell } from "@/components/layout/notification-bell"
|
||||||
|
|
@ -10,8 +11,10 @@ import {
|
||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu"
|
} from "@/components/ui/dropdown-menu"
|
||||||
|
import { useAuth } from "@/contexts/auth-context"
|
||||||
|
|
||||||
export function Navbar() {
|
export function Navbar() {
|
||||||
|
const { user, logout } = useAuth()
|
||||||
return (
|
return (
|
||||||
<header className="sticky top-0 z-50 w-full border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
<header className="sticky top-0 z-50 w-full border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
||||||
<div className="container flex h-14 items-center">
|
<div className="container flex h-14 items-center">
|
||||||
|
|
@ -62,18 +65,55 @@ export function Navbar() {
|
||||||
<SearchDialog />
|
<SearchDialog />
|
||||||
</div>
|
</div>
|
||||||
<nav className="flex items-center gap-2">
|
<nav className="flex items-center gap-2">
|
||||||
<Link href="/mod">
|
{user ? (
|
||||||
<Button variant="ghost" size="sm">
|
<>
|
||||||
Mod
|
{(user.role === 'admin' || user.role === 'moderator') && (
|
||||||
</Button>
|
<Link href="/mod">
|
||||||
</Link>
|
<Button variant="ghost" size="sm">
|
||||||
<NotificationBell />
|
Mod
|
||||||
<Link href="/profile">
|
</Button>
|
||||||
<Button variant="ghost" size="icon">
|
</Link>
|
||||||
<User className="h-5 w-5" />
|
)}
|
||||||
<span className="sr-only">User</span>
|
<NotificationBell />
|
||||||
</Button>
|
<DropdownMenu>
|
||||||
</Link>
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button variant="ghost" size="icon">
|
||||||
|
<User className="h-5 w-5" />
|
||||||
|
<span className="sr-only">User</span>
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end">
|
||||||
|
<DropdownMenuItem disabled className="font-semibold">
|
||||||
|
{user.email}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<Link href="/profile">
|
||||||
|
<DropdownMenuItem>Profile</DropdownMenuItem>
|
||||||
|
</Link>
|
||||||
|
<Link href="/settings">
|
||||||
|
<DropdownMenuItem>Settings</DropdownMenuItem>
|
||||||
|
</Link>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem onClick={logout} className="text-red-500 focus:text-red-500">
|
||||||
|
Sign Out
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Link href="/login">
|
||||||
|
<Button variant="ghost" size="sm">
|
||||||
|
Sign In
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
<Link href="/register">
|
||||||
|
<Button size="sm">
|
||||||
|
Sign Up
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -51,4 +51,28 @@ const CardContent = React.forwardRef<
|
||||||
))
|
))
|
||||||
CardContent.displayName = "CardContent"
|
CardContent.displayName = "CardContent"
|
||||||
|
|
||||||
export { Card, CardHeader, CardTitle, CardContent }
|
const CardDescription = React.forwardRef<
|
||||||
|
HTMLParagraphElement,
|
||||||
|
React.HTMLAttributes<HTMLParagraphElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<p
|
||||||
|
ref={ref}
|
||||||
|
className={cn("text-sm text-muted-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
CardDescription.displayName = "CardDescription"
|
||||||
|
|
||||||
|
const CardFooter = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn("flex items-center p-6 pt-0", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
CardFooter.displayName = "CardFooter"
|
||||||
|
|
||||||
|
export { Card, CardHeader, CardTitle, CardContent, CardDescription, CardFooter }
|
||||||
|
|
|
||||||
79
frontend/contexts/auth-context.tsx
Normal file
79
frontend/contexts/auth-context.tsx
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import React, { createContext, useContext, useState, useEffect } from "react"
|
||||||
|
import { getApiUrl } from "@/lib/api-config"
|
||||||
|
|
||||||
|
interface User {
|
||||||
|
id: number
|
||||||
|
email: string
|
||||||
|
is_active: boolean
|
||||||
|
is_superuser: boolean
|
||||||
|
role: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AuthContextType {
|
||||||
|
user: User | null
|
||||||
|
loading: boolean
|
||||||
|
login: (token: string) => Promise<void>
|
||||||
|
logout: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const AuthContext = createContext<AuthContextType>({
|
||||||
|
user: null,
|
||||||
|
loading: true,
|
||||||
|
login: async () => { },
|
||||||
|
logout: () => { },
|
||||||
|
})
|
||||||
|
|
||||||
|
export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||||
|
const [user, setUser] = useState<User | null>(null)
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const initAuth = async () => {
|
||||||
|
const token = localStorage.getItem("token")
|
||||||
|
if (token) {
|
||||||
|
try {
|
||||||
|
await fetchUser(token)
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Auth init failed", err)
|
||||||
|
localStorage.removeItem("token")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
initAuth()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const fetchUser = async (token: string) => {
|
||||||
|
const res = await fetch(`${getApiUrl()}/auth/users/me`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (res.ok) {
|
||||||
|
const userData = await res.json()
|
||||||
|
setUser(userData)
|
||||||
|
} else {
|
||||||
|
throw new Error("Failed to fetch user")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const login = async (token: string) => {
|
||||||
|
localStorage.setItem("token", token)
|
||||||
|
await fetchUser(token)
|
||||||
|
}
|
||||||
|
|
||||||
|
const logout = () => {
|
||||||
|
localStorage.removeItem("token")
|
||||||
|
setUser(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AuthContext.Provider value={{ user, loading, login, logout }}>
|
||||||
|
{children}
|
||||||
|
</AuthContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useAuth = () => useContext(AuthContext)
|
||||||
|
|
@ -4,5 +4,8 @@ export function getApiUrl() {
|
||||||
return process.env.INTERNAL_API_URL || 'http://localhost:8000'
|
return process.env.INTERNAL_API_URL || 'http://localhost:8000'
|
||||||
}
|
}
|
||||||
// Client-side
|
// Client-side
|
||||||
|
if (window.location.hostname === 'elmeg.xyz') {
|
||||||
|
return 'https://elmeg.xyz/api'
|
||||||
|
}
|
||||||
return process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000'
|
return process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000'
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue