- 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.
120 lines
3.4 KiB
TypeScript
120 lines
3.4 KiB
TypeScript
'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>
|
|
)
|
|
}
|