| [40ac7a9] | 1 | import React, { useEffect, useState } from 'react';
|
|---|
| 2 | import {
|
|---|
| 3 | Container, Box, Typography, Button, Grid, Card, CardMedia, CardContent,
|
|---|
| 4 | CardActions, Chip, Rating, Dialog, DialogTitle, DialogContent, DialogActions,
|
|---|
| 5 | IconButton, Divider
|
|---|
| 6 | } from '@mui/material';
|
|---|
| 7 | import CloseIcon from '@mui/icons-material/Close';
|
|---|
| 8 | import AutoFixHighIcon from '@mui/icons-material/AutoFixHigh';
|
|---|
| 9 | import CommentIcon from '@mui/icons-material/Comment';
|
|---|
| 10 | import StarIcon from '@mui/icons-material/Star';
|
|---|
| 11 | import HardwareIcon from '@mui/icons-material/Memory';
|
|---|
| 12 | import ListIcon from '@mui/icons-material/List'; // Icon for "Show All"
|
|---|
| 13 |
|
|---|
| 14 | // --- PLACEHOLDER ASSETS ---
|
|---|
| 15 | const PLACEHOLDER_IMG = "https://placehold.co/600x400?text=Gaming+PC";
|
|---|
| 16 |
|
|---|
| 17 | import { onGetHighestRankedBuilds, onCloneBuild } from './buildCards.telefunc';
|
|---|
| [67dfe57] | 18 | import {onGetApprovedBuilds, onGetAuthState} from '../+Layout.telefunc';
|
|---|
| [40ac7a9] | 19 | import {getAuthState} from "../../server/telefunc/ctx"; //
|
|---|
| 20 |
|
|---|
| 21 | export default function HomePage() {
|
|---|
| 22 | const [data, setData] = useState<any>(null);
|
|---|
| 23 | const [selectedBuild, setSelectedBuild] = useState<any>(null); // For Detail Popup
|
|---|
| 24 | const [openDetailPopup, setOpenDetailPopup] = useState(false);
|
|---|
| 25 |
|
|---|
| 26 | // NEW: Popup for "All Top Ranked Builds"
|
|---|
| 27 | const [openRankedPopup, setOpenRankedPopup] = useState(false);
|
|---|
| 28 |
|
|---|
| 29 | useEffect(() => {
|
|---|
| 30 | async function loadSite(){
|
|---|
| 31 | try{
|
|---|
| 32 | const [
|
|---|
| 33 | authData, highestRankedBuilds, commnityBuilds
|
|---|
| 34 | ] = await Promise.all([
|
|---|
| [67dfe57] | 35 | onGetAuthState(),
|
|---|
| [40ac7a9] | 36 | onGetHighestRankedBuilds({ limit: 3 }),
|
|---|
| 37 | onGetApprovedBuilds({ limit: 12 })
|
|---|
| 38 | ]);
|
|---|
| 39 |
|
|---|
| 40 | setData({
|
|---|
| 41 | isLoggedIn: authData.isLoggedIn,
|
|---|
| 42 | userId: authData.userId,
|
|---|
| 43 | prebuilts: highestRankedBuilds,
|
|---|
| 44 | communityBuilds: commnityBuilds
|
|---|
| 45 | });
|
|---|
| 46 | } catch (error) {
|
|---|
| 47 | console.error("Error loading homepage data:", error);
|
|---|
| 48 | }
|
|---|
| 49 | }
|
|---|
| 50 |
|
|---|
| 51 | loadSite();
|
|---|
| 52 | }, []);
|
|---|
| 53 |
|
|---|
| 54 | // Open Build Details
|
|---|
| 55 | const handleCardClick = (build: any) => {
|
|---|
| 56 | setSelectedBuild(build);
|
|---|
| 57 | setOpenDetailPopup(true);
|
|---|
| 58 | };
|
|---|
| 59 |
|
|---|
| 60 | const handleClone = async () => {
|
|---|
| 61 | if (!data?.isLoggedIn) return alert("Please login to clone builds!");
|
|---|
| 62 | if (confirm(`Clone "${selectedBuild.name}" to your dashboard?`)) {
|
|---|
| 63 | await onCloneBuild({ buildId: selectedBuild.id });
|
|---|
| 64 | alert("Build cloned! Check your dashboard.");
|
|---|
| 65 | setOpenDetailPopup(false);
|
|---|
| 66 | }
|
|---|
| 67 | };
|
|---|
| 68 |
|
|---|
| 69 | if (!data) return <Box sx={{ p: 10, textAlign: 'center' }}>Loading Forge...</Box>;
|
|---|
| 70 |
|
|---|
| 71 | return (
|
|---|
| 72 | <Box>
|
|---|
| 73 | <Box sx={{
|
|---|
| 74 | position: 'relative', height: '500px',
|
|---|
| 75 | display: 'flex', alignItems: 'center', justifyContent: 'center', color: 'white',
|
|---|
| 76 | '&::before': { content: '""', position: 'absolute', top: 0, left: 0, width: '100%', height: '100%', backgroundColor: 'rgba(0,0,0,0.6)' }
|
|---|
| 77 | }}>
|
|---|
| 78 | <Box sx={{ position: 'relative', textAlign: 'center', zIndex: 1, p: 2 }}>
|
|---|
| 79 | <Typography variant="h2" fontWeight="bold" gutterBottom>Forge Your Ultimate Machine</Typography>
|
|---|
| 80 | <Typography variant="h5" sx={{ maxWidth: '800px', mx: 'auto' }}>
|
|---|
| 81 | Build, share, discuss, and discover custom PC configurations.
|
|---|
| 82 | </Typography>
|
|---|
| 83 | <Typography variant="h5" sx={{ mb: 1, maxWidth: '800px', mx: 'auto' }}>
|
|---|
| 84 | Join the community today.
|
|---|
| 85 | </Typography>
|
|---|
| 86 |
|
|---|
| 87 | <Button variant="contained" size="large" href="/forger" startIcon={<AutoFixHighIcon />} sx={{ fontSize: '1.2rem', px: 4, py: 1.5 }}>
|
|---|
| 88 | Start Forging
|
|---|
| 89 | </Button>
|
|---|
| 90 | </Box>
|
|---|
| 91 | </Box>
|
|---|
| 92 |
|
|---|
| 93 | <Container maxWidth="xl" sx={{ mt: 6, mb: 10 }}>
|
|---|
| 94 | <Box sx={{ mb: 4, borderLeft: '5px solid #ff8201', pl: 2, display: 'flex', justifyContent: 'space-between', alignItems: 'end' }}>
|
|---|
| 95 | <Box>
|
|---|
| 96 | <Typography variant="h4" fontWeight="bold">Hall of Fame</Typography>
|
|---|
| 97 | <Typography variant="subtitle1" color="text.secondary">The highest rated configurations from across the community.</Typography>
|
|---|
| 98 | </Box>
|
|---|
| 99 | <Button
|
|---|
| 100 | variant="outlined"
|
|---|
| 101 | startIcon={<ListIcon />}
|
|---|
| 102 | onClick={() => setOpenRankedPopup(true)}
|
|---|
| 103 | >
|
|---|
| 104 | Show All Top Ranked
|
|---|
| 105 | </Button>
|
|---|
| 106 | </Box>
|
|---|
| 107 |
|
|---|
| 108 | <Grid container spacing={3} sx={{ mb: 8 }}>
|
|---|
| 109 | <Grid item xs={12} md={3}>
|
|---|
| 110 | <Box sx={{
|
|---|
| 111 | height: '100%', p: 3, bgcolor: '#ff8201', color: 'black', borderRadius: 2,
|
|---|
| 112 | display: 'flex', flexDirection: 'column', justifyContent: 'center'
|
|---|
| 113 | }}>
|
|---|
| 114 | <Typography color={'black'} variant="h4" fontWeight="bold" gutterBottom>The Elite</Typography>
|
|---|
| 115 | <Typography fontWeight={'bold'}>
|
|---|
| 116 | These 3 builds represent the pinnacle of performance and looks as voted by the PC Forge community.
|
|---|
| 117 | </Typography>
|
|---|
| 118 | </Box>
|
|---|
| 119 | </Grid>
|
|---|
| 120 |
|
|---|
| 121 | {data.prebuilts.map((build: any) => (
|
|---|
| 122 | <Grid item xs={12} sm={6} md={3} key={build.id}>
|
|---|
| 123 | <BuildCard build={build} onClick={() => handleCardClick(build)} />
|
|---|
| 124 | </Grid>
|
|---|
| 125 | ))}
|
|---|
| 126 | </Grid>
|
|---|
| 127 |
|
|---|
| 128 | <SectionHeader title="Community Forge" subtitle="Fresh builds from users around the world."/>
|
|---|
| 129 | <Grid container spacing={3}>
|
|---|
| 130 | {data.communityBuilds.map((build: any) => (
|
|---|
| 131 | <Grid item xs={12} sm={6} md={3} key={build.id}>
|
|---|
| 132 | <BuildCard build={build} onClick={() => handleCardClick(build)} />
|
|---|
| 133 | </Grid>
|
|---|
| 134 | ))}
|
|---|
| 135 | </Grid>
|
|---|
| 136 |
|
|---|
| 137 | <Box sx={{ display: 'flex', justifyContent: 'center', mt: 6 }}>
|
|---|
| 138 | <Button variant="outlined" size="large" href="/completed-builds">View All Community Builds</Button>
|
|---|
| 139 | </Box>
|
|---|
| 140 |
|
|---|
| 141 | </Container>
|
|---|
| 142 |
|
|---|
| 143 | <Dialog open={openDetailPopup} onClose={() => setOpenDetailPopup(false)} maxWidth="md" fullWidth>
|
|---|
| 144 | {selectedBuild && (
|
|---|
| 145 | <>
|
|---|
| 146 | <DialogTitle sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
|---|
| 147 | <Box>
|
|---|
| 148 | <Typography variant="h5" fontWeight="bold">{selectedBuild.name}</Typography>
|
|---|
| 149 | <Typography variant="subtitle2" color="text.secondary">By {selectedBuild.username || "Unknown"}</Typography>
|
|---|
| 150 | </Box>
|
|---|
| 151 | <IconButton onClick={() => setOpenDetailPopup(false)}><CloseIcon /></IconButton>
|
|---|
| 152 | </DialogTitle>
|
|---|
| 153 |
|
|---|
| 154 | <DialogContent dividers>
|
|---|
| 155 | <Grid container spacing={4}>
|
|---|
| 156 | <Grid item xs={12} md={6}>
|
|---|
| 157 | <Box component="img" src={selectedBuild.imageUrl || PLACEHOLDER_IMG} sx={{ width: '100%', borderRadius: 2, mb: 2 }} />
|
|---|
| 158 | <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
|---|
| 159 | <Rating value={Number(selectedBuild.avgRating) || 0} readOnly precision={0.5} />
|
|---|
| 160 | <Typography>({selectedBuild.ratingCount || 0} reviews)</Typography>
|
|---|
| 161 | </Box>
|
|---|
| 162 | </Grid>
|
|---|
| 163 | <Grid item xs={12} md={6}>
|
|---|
| 164 | <Typography variant="h6" gutterBottom>Build Specs</Typography>
|
|---|
| 165 | <ComponentRow name="CPU" value="Intel Core i9-13900K" /> {/* Placeholder */}
|
|---|
| 166 | <ComponentRow name="GPU" value="NVIDIA RTX 4090" /> {/* Placeholder */}
|
|---|
| 167 |
|
|---|
| 168 | <Box sx={{ mt: 4, bgcolor: '#f5f5f5', p: 2, borderRadius: 1 }}>
|
|---|
| 169 | <Typography variant="subtitle2" fontWeight="bold">Actions</Typography>
|
|---|
| 170 | <Box sx={{ display: 'flex', gap: 1, mt: 1 }}>
|
|---|
| 171 | <Button size="small" variant="outlined" startIcon={<StarIcon />}>Rate</Button>
|
|---|
| 172 | <Button size="small" variant="outlined" startIcon={<CommentIcon />}>Review</Button>
|
|---|
| 173 | </Box>
|
|---|
| 174 | </Box>
|
|---|
| 175 | </Grid>
|
|---|
| 176 | </Grid>
|
|---|
| 177 | </DialogContent>
|
|---|
| 178 |
|
|---|
| 179 | <DialogActions sx={{ p: 2 }}>
|
|---|
| 180 | {data.userId === selectedBuild.userId ? (
|
|---|
| 181 | <Button variant="contained" href={`/forger?edit=${selectedBuild.id}`}>Edit</Button>
|
|---|
| 182 | ) : (
|
|---|
| 183 | <Button variant="contained" color="secondary" startIcon={<AutoFixHighIcon />} onClick={null}>
|
|---|
| 184 | Clone & Edit
|
|---|
| 185 | </Button>
|
|---|
| 186 | )}
|
|---|
| 187 | <Button href={`/build/${selectedBuild.id}`}>View Full Details</Button>
|
|---|
| 188 | </DialogActions>
|
|---|
| 189 | </>
|
|---|
| 190 | )}
|
|---|
| 191 | </Dialog>
|
|---|
| 192 |
|
|---|
| 193 |
|
|---|
| 194 | <Dialog open={openRankedPopup} onClose={() => setOpenRankedPopup(false)} maxWidth="lg" fullWidth>
|
|---|
| 195 | <DialogTitle sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
|---|
| 196 | Hall of Fame (All Top Ranked)
|
|---|
| 197 | <IconButton onClick={() => setOpenRankedPopup(false)}><CloseIcon /></IconButton>
|
|---|
| 198 | </DialogTitle>
|
|---|
| 199 | <DialogContent dividers>
|
|---|
| 200 | <Grid container spacing={3}>
|
|---|
| 201 |
|
|---|
| 202 | {data.prebuilts.concat(data.communityBuilds).map((build: any) => (
|
|---|
| 203 | <Grid item xs={12} sm={6} md={3} key={`popup-${build.id}`}>
|
|---|
| 204 | <BuildCard build={build} onClick={() => {
|
|---|
| 205 | setOpenRankedPopup(false);
|
|---|
| 206 | handleCardClick(build);
|
|---|
| 207 | }} />
|
|---|
| 208 | </Grid>
|
|---|
| 209 | ))}
|
|---|
| 210 | </Grid>
|
|---|
| 211 | </DialogContent>
|
|---|
| 212 | </Dialog>
|
|---|
| 213 |
|
|---|
| 214 | </Box>
|
|---|
| 215 | );
|
|---|
| 216 | }
|
|---|
| 217 |
|
|---|
| 218 | function SectionHeader({ title, subtitle }: { title: string, subtitle: string }) {
|
|---|
| 219 | return (
|
|---|
| 220 | <Box sx={{ mb: 4, borderLeft: '5px solid #1976d2', pl: 2 }}>
|
|---|
| 221 | <Typography variant="h4" fontWeight="bold" color="text.primary">{title}</Typography>
|
|---|
| 222 | <Typography variant="subtitle1" color="text.secondary">{subtitle}</Typography>
|
|---|
| 223 | </Box>
|
|---|
| 224 | );
|
|---|
| 225 | }
|
|---|
| 226 |
|
|---|
| 227 | function BuildCard({ build, onClick }: { build: any, onClick: () => void }) {
|
|---|
| 228 | return (
|
|---|
| 229 | <Card sx={{ height: '100%', display: 'flex', flexDirection: 'column', cursor: 'pointer', transition: 'all 0.2s', '&:hover': { transform: 'translateY(-4px)', boxShadow: 6 } }} onClick={onClick}>
|
|---|
| 230 | <CardMedia component="img" height="160" image={build.imageUrl || PLACEHOLDER_IMG} alt={build.name} />
|
|---|
| 231 | <CardContent sx={{ flexGrow: 1, pb: 1 }}>
|
|---|
| 232 | <Typography gutterBottom variant="h6" noWrap>{build.name}</Typography>
|
|---|
| 233 |
|
|---|
| 234 | <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mt: 1 }}>
|
|---|
| 235 | <Box sx={{ display: 'flex', alignItems: 'center', px: 1, borderRadius: 1 }}>
|
|---|
| 236 | <StarIcon fontSize="small" sx={{ color: '#faaf00', mr: 0.5 }} />
|
|---|
| 237 | <Typography variant="body2" fontWeight="bold">{Number(build.avgRating || 0).toFixed(1)}</Typography>
|
|---|
| 238 | </Box>
|
|---|
| 239 | <Typography variant="caption" color="text.secondary">
|
|---|
| 240 | {build.commentCount || 0} reviews
|
|---|
| 241 | </Typography>
|
|---|
| 242 | </Box>
|
|---|
| 243 | </CardContent>
|
|---|
| 244 | </Card>
|
|---|
| 245 | );
|
|---|
| 246 | }
|
|---|
| 247 |
|
|---|
| 248 | function ComponentRow({ name, value }: { name: string, value: string }) {
|
|---|
| 249 | return (
|
|---|
| 250 | <Box sx={{ display: 'flex', justifyContent: 'space-between', py: 1, borderBottom: '1px solid #eee' }}>
|
|---|
| 251 | <Typography variant="body2" color="text.secondary">{name}</Typography>
|
|---|
| 252 | <Typography variant="body2" fontWeight="bold">{value}</Typography>
|
|---|
| 253 | </Box>
|
|---|
| 254 | );
|
|---|
| 255 | }
|
|---|