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,32 @@
import Link from 'next/link'
interface GameCardProps {
appId: number
name: string
headerImage?: string | null
}
export function GameCard({ appId, name, headerImage }: GameCardProps) {
return (
<Link href={`/game/${appId}`}>
<div className="bg-[#16213e] rounded-xl overflow-hidden hover:ring-2 hover:ring-[#00d4ff] transition-all group">
<div className="aspect-video relative overflow-hidden">
{headerImage ? (
<img
src={headerImage}
alt={name}
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
/>
) : (
<div className="w-full h-full bg-[#1a1a2e] flex items-center justify-center">
<span className="text-4xl">🎮</span>
</div>
)}
</div>
<div className="p-4">
<h3 className="text-white font-medium truncate">{name}</h3>
</div>
</div>
</Link>
)
}

56
src/components/Header.tsx Normal file
View File

@@ -0,0 +1,56 @@
'use client'
import Link from 'next/link'
import { useSession, signOut } from 'next-auth/react'
export function Header() {
const { data: session } = useSession()
return (
<header className="bg-[#1a1a2e] border-b border-[#16213e]">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex items-center justify-between h-16">
<Link href="/" className="flex items-center space-x-2">
<div className="w-8 h-8 bg-gradient-to-br from-[#00d4ff] to-[#7c3aed] rounded-lg flex items-center justify-center">
<span className="text-white font-bold text-sm">FR</span>
</div>
<span className="text-white font-bold text-xl">FairReview</span>
</Link>
<nav className="flex items-center space-x-4">
<Link
href="/"
className="text-gray-300 hover:text-white transition-colors"
>
Home
</Link>
{session ? (
<>
<Link
href="/profile"
className="text-gray-300 hover:text-white transition-colors"
>
Profile
</Link>
<button
onClick={() => signOut()}
className="text-gray-300 hover:text-white transition-colors"
>
Sign Out
</button>
</>
) : (
<Link
href="/login"
className="bg-[#00d4ff] hover:bg-[#00b8e6] text-[#1a1a2e] px-4 py-2 rounded-lg font-medium transition-colors"
>
Sign In
</Link>
)}
</nav>
</div>
</div>
</header>
)
}

View File

@@ -0,0 +1,15 @@
'use client'
import { SessionProvider } from 'next-auth/react'
import { Header } from '@/components/Header'
export function Providers({ children }: { children: React.ReactNode }) {
return (
<SessionProvider>
<div className="min-h-screen bg-[#1a1a2e]">
<Header />
<main>{children}</main>
</div>
</SessionProvider>
)
}

View File

@@ -0,0 +1,119 @@
'use client'
import { useState } from 'react'
import { useSession } from 'next-auth/react'
interface ReviewCardProps {
id: string
username: string
avatarUrl?: string | null
content: string
rating: number
playtimeHours?: number | null
upvotes: number
downvotes: number
userVote?: number | null
createdAt: string
onVote: (reviewId: string, voteType: 1 | -1) => Promise<void>
}
export function ReviewCard({
id,
username,
avatarUrl,
content,
rating,
playtimeHours,
upvotes,
downvotes,
userVote,
createdAt,
onVote
}: ReviewCardProps) {
const { data: session } = useSession()
const [voting, setVoting] = useState(false)
const handleVote = async (voteType: 1 | -1) => {
if (!session) return
setVoting(true)
try {
await onVote(id, voteType)
} finally {
setVoting(false)
}
}
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric'
})
}
return (
<div className="bg-[#16213e] rounded-xl p-6">
<div className="flex items-start gap-4">
<div className="w-12 h-12 rounded-full bg-[#1a1a2e] flex items-center justify-center overflow-hidden">
{avatarUrl ? (
<img src={avatarUrl} alt={username} className="w-full h-full object-cover" />
) : (
<span className="text-2xl">👤</span>
)}
</div>
<div className="flex-1">
<div className="flex items-center gap-3 mb-2">
<span className="text-white font-medium">{username}</span>
<span className="text-[#00d4ff] font-bold">{rating}/10</span>
{playtimeHours !== null && playtimeHours !== undefined && (
<span className="text-gray-400 text-sm">{playtimeHours}h played</span>
)}
</div>
<p className="text-gray-300 mb-4">{content}</p>
<div className="flex items-center justify-between">
<span className="text-gray-500 text-sm">{formatDate(createdAt)}</span>
{session && (
<div className="flex items-center gap-2">
<button
onClick={() => handleVote(1)}
disabled={voting}
className={`flex items-center gap-1 px-3 py-1 rounded-lg transition-colors ${
userVote === 1
? 'bg-green-600 text-white'
: 'bg-[#1a1a2e] text-gray-400 hover:text-green-400'
}`}
>
<span></span>
<span>{upvotes}</span>
</button>
<button
onClick={() => handleVote(-1)}
disabled={voting}
className={`flex items-center gap-1 px-3 py-1 rounded-lg transition-colors ${
userVote === -1
? 'bg-red-600 text-white'
: 'bg-[#1a1a2e] text-gray-400 hover:text-red-400'
}`}
>
<span></span>
<span>{downvotes}</span>
</button>
</div>
)}
{!session && (
<span className="text-gray-500 text-sm">
Sign in to vote
</span>
)}
</div>
</div>
</div>
</div>
)
}