- Implemented WriteReviewPage for users to submit reviews with ratings and playtime. - Created global CSS styles for consistent theming across the application. - Established RootLayout for metadata and global styles. - Developed LoginPage for user authentication with email/password and Steam login options. - Built Home page to display game search results and recent reviews. - Added LinkSteamPage for linking Steam accounts to user profiles. - Created ProfilePage to manage user information and display their reviews. - Developed GameCard and ReviewCard components for displaying games and reviews. - Implemented Header component for navigation and user session management. - Added Providers component to wrap the application with session context. - Integrated NextAuth for user authentication with Steam and credentials. - Set up Prisma client for database interactions. - Created Steam API utility functions for fetching game and user data. - Configured TypeScript settings for the project.
219 lines
6.6 KiB
TypeScript
219 lines
6.6 KiB
TypeScript
'use client'
|
|
|
|
import { useState, useEffect } from 'react'
|
|
import { useSession } from 'next-auth/react'
|
|
import { useRouter } from 'next/navigation'
|
|
import { ReviewCard } from '@/components/ReviewCard'
|
|
import Link from 'next/link'
|
|
|
|
interface GameDetails {
|
|
app_id: number
|
|
name: string
|
|
description: string | null
|
|
header_image: string | null
|
|
background_image: string | null
|
|
release_date: string | null
|
|
developers: string[]
|
|
publishers: string[]
|
|
genres: string[]
|
|
}
|
|
|
|
interface Review {
|
|
id: string
|
|
username: string
|
|
avatar_url: string | null
|
|
content: string
|
|
rating: number
|
|
playtime_hours: number | null
|
|
upvotes: number
|
|
downvotes: number
|
|
user_vote: number | null
|
|
created_at: string
|
|
}
|
|
|
|
export default function GamePage({ params }: { params: Promise<{ appId: string }> }) {
|
|
const { data: session } = useSession()
|
|
const router = useRouter()
|
|
const [game, setGame] = useState<GameDetails | null>(null)
|
|
const [reviews, setReviews] = useState<Review[]>([])
|
|
const [loading, setLoading] = useState(true)
|
|
const [resolvedParams, setResolvedParams] = useState<{ appId: string } | null>(null)
|
|
|
|
useEffect(() => {
|
|
params.then(setResolvedParams)
|
|
}, [params])
|
|
|
|
useEffect(() => {
|
|
if (!resolvedParams) return
|
|
|
|
const fetchData = async () => {
|
|
setLoading(true)
|
|
try {
|
|
const [gameRes, reviewsRes] = await Promise.all([
|
|
fetch(`/api/games/${resolvedParams.appId}`),
|
|
fetch(`/api/reviews?appId=${resolvedParams.appId}`)
|
|
])
|
|
|
|
const gameData = await gameRes.json()
|
|
const reviewsData = await reviewsRes.json()
|
|
|
|
setGame(gameData)
|
|
setReviews(reviewsData)
|
|
} catch (error) {
|
|
console.error('Failed to fetch data:', error)
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
fetchData()
|
|
}, [resolvedParams])
|
|
|
|
const handleVote = async (reviewId: string, voteType: 1 | -1) => {
|
|
await fetch(`/api/reviews/${reviewId}/vote`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ voteType })
|
|
})
|
|
|
|
const res = await fetch(`/api/reviews?appId=${resolvedParams?.appId}`)
|
|
const data = await res.json()
|
|
setReviews(data)
|
|
}
|
|
|
|
const canReview = session?.user?.steamId
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="flex items-center justify-center min-h-[50vh]">
|
|
<div className="text-white">Loading...</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
if (!game) {
|
|
return (
|
|
<div className="flex items-center justify-center min-h-[50vh]">
|
|
<div className="text-white">Game not found</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div>
|
|
<div
|
|
className="relative h-64 md:h-96 bg-cover bg-center"
|
|
style={{ backgroundImage: game.background_image ? `url(${game.background_image})` : undefined }}
|
|
>
|
|
<div className="absolute inset-0 bg-gradient-to-t from-[#1a1a2e] to-transparent" />
|
|
</div>
|
|
|
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 -mt-32 relative z-10">
|
|
<div className="flex flex-col md:flex-row gap-8">
|
|
<div className="w-full md:w-1/3">
|
|
<img
|
|
src={game.header_image || '/placeholder.png'}
|
|
alt={game.name}
|
|
className="w-full rounded-xl shadow-2xl"
|
|
/>
|
|
</div>
|
|
|
|
<div className="w-full md:w-2/3">
|
|
<h1 className="text-4xl font-bold text-white mb-4">{game.name}</h1>
|
|
|
|
<div className="flex flex-wrap gap-2 mb-4">
|
|
{game.genres.map((genre) => (
|
|
<span
|
|
key={genre}
|
|
className="px-3 py-1 bg-[#16213e] text-gray-300 rounded-full text-sm"
|
|
>
|
|
{genre}
|
|
</span>
|
|
))}
|
|
</div>
|
|
|
|
{game.developers.length > 0 && (
|
|
<p className="text-gray-400 mb-2">
|
|
<span className="text-white">Developer:</span> {game.developers.join(', ')}
|
|
</p>
|
|
)}
|
|
|
|
{game.publishers.length > 0 && (
|
|
<p className="text-gray-400 mb-2">
|
|
<span className="text-white">Publisher:</span> {game.publishers.join(', ')}
|
|
</p>
|
|
)}
|
|
|
|
{game.release_date && (
|
|
<p className="text-gray-400 mb-4">
|
|
<span className="text-white">Release Date:</span> {game.release_date}
|
|
</p>
|
|
)}
|
|
|
|
{canReview ? (
|
|
<Link
|
|
href={`/game/${game.app_id}/write`}
|
|
className="inline-block px-6 py-3 bg-[#00d4ff] hover:bg-[#00b8e6] text-[#1a1a2e] font-medium rounded-lg transition-colors"
|
|
>
|
|
Write a Review
|
|
</Link>
|
|
) : session ? (
|
|
<Link
|
|
href="/profile/link-steam"
|
|
className="inline-block px-6 py-3 bg-[#7c3aed] hover:bg-[#6d28d9] text-white font-medium rounded-lg transition-colors"
|
|
>
|
|
Link Steam to Write Review
|
|
</Link>
|
|
) : (
|
|
<Link
|
|
href="/login"
|
|
className="inline-block px-6 py-3 bg-[#00d4ff] hover:bg-[#00b8e6] text-[#1a1a2e] font-medium rounded-lg transition-colors"
|
|
>
|
|
Sign In to Write Review
|
|
</Link>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{game.description && (
|
|
<div className="mt-8 bg-[#16213e] rounded-xl p-6">
|
|
<h2 className="text-2xl font-bold text-white mb-4">About</h2>
|
|
<p className="text-gray-300 whitespace-pre-line">{game.description}</p>
|
|
</div>
|
|
)}
|
|
|
|
<div className="mt-8 mb-12">
|
|
<h2 className="text-2xl font-bold text-white mb-6">
|
|
Reviews ({reviews.length})
|
|
</h2>
|
|
|
|
{reviews.length === 0 ? (
|
|
<div className="text-center py-12 bg-[#16213e] rounded-xl">
|
|
<p className="text-gray-400">No reviews yet. Be the first to write one!</p>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-4">
|
|
{reviews.map((review) => (
|
|
<ReviewCard
|
|
key={review.id}
|
|
id={review.id}
|
|
username={review.username}
|
|
avatarUrl={review.avatar_url}
|
|
content={review.content}
|
|
rating={review.rating}
|
|
playtimeHours={review.playtime_hours}
|
|
upvotes={review.upvotes}
|
|
downvotes={review.downvotes}
|
|
userVote={review.user_vote}
|
|
createdAt={review.created_at}
|
|
onVote={handleVote}
|
|
/>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|