feat: add review writing page and integrate Steam account linking

- 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.
This commit is contained in:
2026-02-22 02:53:23 +05:00
parent ce018da271
commit 1a6e754e4b
41 changed files with 2093 additions and 2 deletions

View File

@@ -0,0 +1,218 @@
'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>
)
}