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:
32
src/components/GameCard.tsx
Normal file
32
src/components/GameCard.tsx
Normal 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
56
src/components/Header.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
15
src/components/Providers.tsx
Normal file
15
src/components/Providers.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
119
src/components/ReviewCard.tsx
Normal file
119
src/components/ReviewCard.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user