Changeset 8a7f936


Ignore:
Timestamp:
12/29/25 00:37:49 (6 months ago)
Author:
Mihail <mihail2.naumov@…>
Branches:
main
Children:
5af32f0
Parents:
ae5d054
Message:

Added Forge Page, added suggest component, fixed styling

Files:
8 edited

Legend:

Unmodified
Added
Removed
  • components/BuildCard.tsx

    rae5d054 r8a7f936  
    1 import React from 'react';
    2 import { Card, CardMedia, CardContent, Typography, Box, Chip } from '@mui/material';
     1import React, {useEffect, useState} from 'react';
     2import {Card, CardContent, CardMedia, Typography, Box, Chip} from '@mui/material';
    33import StarIcon from '@mui/icons-material/Star';
    4 // import PersonIcon from '@mui/icons-material/Person';
     4import {onGetBuildDetails} from "../pages/+Layout.telefunc";
    55
    6 export default function BuildCard({ build, onClick }: { build: any, onClick: () => void }) {
    7     const formattedPrice = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'EUR' }).format(build.total_price || 0);
     6export default function BuildCard({build, onClick}: { build: any, onClick?: () => void }) {
     7    const [caseImage, setCaseImage] = useState<string>("https://placehold.co/600x400?text=PC+Build");
     8    const [imageLoading, setImageLoading] = useState(true);
     9
     10    const formattedPrice = new Intl.NumberFormat('en-US', {
     11        style: 'currency',
     12        currency: 'EUR'
     13    }).format(build.total_price || 0);
     14
     15    useEffect(() => {
     16        onGetBuildDetails({buildId: build.id})
     17            .then(details => {
     18                const caseComponent = details.components.find((c: any) => c.type === 'case');
     19                setCaseImage(caseComponent?.imgUrl || caseComponent?.imgUrl || "https://placehold.co/600x400?text=PC+Build");
     20            })
     21            .catch(() => {
     22            })
     23            .finally(() => setImageLoading(false));
     24    }, [build.id]);
    825
    926    return (
    1027        <Card
    11             sx={{ height: '100%', display: 'flex', flexDirection: 'column', cursor: 'pointer', transition: 'all 0.2s', '&:hover': { transform: 'translateY(-4px)', boxShadow: 6 } }}
    1228            onClick={onClick}
     29            sx={{
     30                width: '100%',
     31                height: '100%',
     32                display: 'flex',
     33                flexDirection: 'column',
     34                cursor: onClick ? 'pointer' : 'default',
     35                transition: 'transform 0.2s, box-shadow 0.2s',
     36                '&:hover': onClick ? {
     37                    transform: 'translateY(-4px)',
     38                    boxShadow: 6
     39                } : {},
     40                position: 'relative'
     41            }}
    1342        >
    14             <CardMedia
    15                 component="img"
    16                 height="160"
    17                 image={build.img_url || "https://placehold.co/600x400?text=PC+Build"}
    18                 alt={build.name}
    19             />
    20             <CardContent sx={{ flexGrow: 1, pb: 1 }}>
    21                 <Typography gutterBottom variant="h6" noWrap title={build.name}>
    22                     {build.name}
    23                 </Typography>
     43            <Box sx={{position: 'relative', paddingTop: '75%'}}>
     44                <CardMedia
     45                    component="img"
     46                    image={caseImage || '/placeholder-pc.png'}
     47                    alt={build.name}
     48                    sx={{
     49                        position: 'absolute',
     50                        top: 0,
     51                        left: 0,
     52                        width: '100%',
     53                        height: '100%',
     54                        objectFit: 'contain',
     55                        p: 2,
     56                        bgcolor: '#f5f5f5'
     57                    }}
     58                />
     59            </Box>
    2460
    25                 {/*Ne se renderira user-ot*/}
    26                 <Box sx={{ display: 'flex', alignItems: 'center', mb: 1, color: 'text.secondary' }}>
    27                     {/*    <PersonIcon sx={{ fontSize: 16, mr: 0.5 }} />*/}
    28                     {/*    <Typography variant="caption">{build.user || 'Unknown'}</Typography>*/}
     61            <CardContent sx={{flexGrow: 1, display: 'flex', flexDirection: 'column', justifyContent: 'space-between'}}>
     62                <Box sx={{mb: 2}}>
     63                    <Typography variant="h6" component="div" fontWeight="bold" wrap title={build.name}>
     64                        {build.name}
     65                    </Typography>
    2966                </Box>
    3067
    31                 <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mt: 'auto' }}>
    32                     <Chip label={formattedPrice} size="small" color="primary" variant="outlined" />
    33                     <Box sx={{ display: 'flex', alignItems: 'center' }}>
    34                         <StarIcon fontSize="small" sx={{ color: '#faaf00', mr: 0.5 }} />
     68                <Box sx={{display: 'flex', justifyContent: 'space-between', alignItems: 'center', mt: 'auto'}}>
     69                    <Chip
     70                        label={formattedPrice}
     71                        color="primary"
     72                        variant="outlined"
     73                        size="small"
     74                        sx={{fontWeight: 'bold'}}
     75                    />
     76                    <Box sx={{display: 'flex', alignItems: 'center'}}>
     77                        <StarIcon sx={{color: '#faaf00', fontSize: 20, mr: 0.5}}/>
    3578                        <Typography variant="body2" fontWeight="bold">
    3679                            {Number(build.avgRating || 5).toFixed(1)}
  • components/BuildDetailsDialog.tsx

    rae5d054 r8a7f936  
    11import React, { useEffect, useState } from 'react';
    22import {
    3     Dialog, DialogTitle, DialogContent, Button, Grid, Box, Typography,
    4     IconButton, Tab, Tabs, Table, TableBody, TableCell, TableRow, Rating, TextField, Avatar, Chip
     3    Dialog, DialogTitle, DialogContent, DialogActions, Button, Grid, Box, Typography,
     4    IconButton, Tab, Tabs, Table, TableBody, TableCell, TableRow, Rating, TextField, Avatar, Chip, Alert
    55} from '@mui/material';
    66import CloseIcon from '@mui/icons-material/Close';
     
    99import FavoriteBorderIcon from '@mui/icons-material/FavoriteBorder';
    1010import PersonIcon from "@mui/icons-material/Person";
    11 import { onGetBuildDetails, onSetReview, onToggleFavorite } from '../pages/+Layout.telefunc';
     11import {onGetBuildDetails, onSetReview, onToggleFavorite, onCloneBuild, onSetRating} from '../pages/+Layout.telefunc';
    1212
    1313const formatPrice = (price: any) => new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(Number(price) || 0);
    1414
    15 export default function BuildDetailsDialog({ open, buildId, onClose, onClone, currentUser }: any) {
     15export default function BuildDetailsDialog({ open, buildId, onClose, currentUser }: any) {
    1616    const [details, setDetails] = useState<any>(null);
    1717    const [loading, setLoading] = useState(false);
    1818    const [tabIndex, setTabIndex] = useState(0);
     19    const [cloneDialogOpen, setCloneDialogOpen] = useState(false);
     20    const [cloningBuildId, setCloningBuildId] = useState<number | null>(null);
    1921
    2022    const [reviewText, setReviewText] = useState("");
     
    4547    const handleSubmitReview = async () => {
    4648        if (!currentUser) return alert("Please login to review.");
    47         await onSetReview({ buildId, content: reviewText });
    48 
    49         setReviewText("");
     49
     50        await onSetReview({
     51            buildId,
     52            content: reviewText,
     53            // rating: ratingVal
     54        });
     55
     56        await onSetRating({
     57            buildId,
     58            value: ratingVal
     59        })
    5060
    5161        const refreshed = await onGetBuildDetails({ buildId });
     
    5363    };
    5464
     65    const handleCloneConfirm = async () => {
     66        if (!cloningBuildId) return;
     67
     68        try {
     69            const newBuildId = await onCloneBuild({ buildId: cloningBuildId });
     70
     71            window.location.href = `/forge?buildId=${newBuildId}`;
     72
     73            setCloneDialogOpen(false);
     74            setCloningBuildId(null);
     75        } catch (e) {
     76            alert("Failed to clone build. Please try again.");
     77        }
     78    };
     79
     80
    5581    if (!open) return null;
    5682
    57     // @ts-ignore
    5883    return (
    59         <Dialog open={open} onClose={onClose} maxWidth="md" fullWidth scroll="paper">
    60             {loading || !details ? (
    61                 <Box sx={{ p: 5, textAlign: 'center' }}>Loading Forge Schematics...</Box>
    62             ) : (
    63                 <>
    64                     <DialogTitle sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', bgcolor: '#ff8201' }}>
    65                         <Box>
    66                             <Typography variant="h5" fontWeight="bold">{details.name}</Typography>
    67                             <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
    68                                 <PersonIcon sx={{ fontSize: 16 }} />
    69                                 <Typography variant="subtitle2" color="text.secondary" fontWeight="bold">
    70                                     by {details.creator}
    71                                 </Typography>
    72                                 <Chip label={formatPrice(details.totalPrice)} size="small" color="primary" variant="outlined" />
     84        <>
     85            <Dialog open={open} onClose={onClose} maxWidth="md" fullWidth scroll="paper">
     86                {loading || !details ? (
     87                    <Box sx={{ p: 5, textAlign: 'center' }}>Loading Forge Schematics...</Box>
     88                ) : (
     89                    <>
     90                        <DialogTitle sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', bgcolor: '#ff8201' }}>
     91                            <Box>
     92                                <Typography variant="h5" fontWeight="bold">{details.name}</Typography>
     93                                <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
     94                                    <PersonIcon sx={{ fontSize: 16 }} />
     95                                    <Typography variant="subtitle2" color="text.secondary" fontWeight="bold">
     96                                        by {details.creator}
     97                                    </Typography>
     98                                    <Chip label={formatPrice(details.totalPrice)} size="small" color="primary" variant="outlined" />
     99                                </Box>
    73100                            </Box>
    74                         </Box>
    75                         <IconButton onClick={onClose}><CloseIcon /></IconButton>
    76                     </DialogTitle>
    77 
    78                     <DialogContent sx={{ p: 0 }}>
    79                         <Box sx={{ borderBottom: 1, borderColor: 'divider', px: 2, bgcolor: 'primary', position: 'sticky', top: 0, zIndex: 1 }}>
    80                             <Tabs value={tabIndex} onChange={(_, v) => setTabIndex(v)}>
    81                                 <Tab label="Specs" />
    82                                 <Tab label={`Reviews (${details.ratingStatistics.ratingCount})`} />
    83                             </Tabs>
    84                         </Box>
    85 
    86                         <Box sx={{ p: 3 }}>
    87                             {tabIndex === 0 && (
    88                                 <Grid container spacing={2}>
    89                                     <Grid item xs={12} md={8}>
    90                                         <Table size="small">
    91                                             <TableBody>
    92                                                 {details.components.map((comp: any) => (
    93                                                     <TableRow key={comp.id}>
    94                                                         <TableCell sx={{ width: 50 }}>
    95                                                             <Avatar
    96                                                                 src={comp.img_url || undefined}
    97                                                                 variant="rounded"
    98                                                                 sx={{ width: 45, height: 45, bgcolor: '#ff8201'}}
    99                                                             >
    100                                                                 {comp.type?.substring(0, 3)?.toUpperCase()}
    101                                                             </Avatar>
    102                                                         </TableCell>
    103                                                         <TableCell>
    104                                                             <Typography variant="body2" color="text.secondary" sx={{ fontSize: '0.75rem', textTransform: 'uppercase' }}>
    105                                                                 {comp.type}
    106                                                             </Typography>
    107                                                             <Typography variant="body1" fontWeight="500">
    108                                                                 {comp.brand} {comp.name}
    109                                                             </Typography>
    110                                                         </TableCell>
    111                                                         <TableCell align="right" sx={{ fontWeight: 'bold', color: '#ff8201' }}>
    112                                                             {formatPrice(comp.price)}
     101                            <IconButton onClick={onClose}><CloseIcon /></IconButton>
     102                        </DialogTitle>
     103
     104                        <DialogContent sx={{ p: 0 }}>
     105                            <Box sx={{ borderBottom: 1, borderColor: 'divider', px: 2, bgcolor: 'primary', position: 'sticky', top: 0, zIndex: 1 }}>
     106                                <Tabs value={tabIndex} onChange={(_, v) => setTabIndex(v)}>
     107                                    <Tab label="Specs" />
     108                                    <Tab label={`Reviews (${details.ratingStatistics.ratingCount})`} />
     109                                </Tabs>
     110                            </Box>
     111
     112                            <Box sx={{ p: 3 }}>
     113                                {tabIndex === 0 && (
     114                                    <Grid container spacing={2}>
     115                                        <Grid item xs={12} md={8}>
     116                                            <Table size="small">
     117                                                <TableBody>
     118                                                    {details.components.map((comp: any) => (
     119                                                        <TableRow key={comp.id}>
     120                                                            <TableCell sx={{ width: 50 }}>
     121                                                                <Avatar
     122                                                                    src={comp.img_url || undefined}
     123                                                                    variant="rounded"
     124                                                                    sx={{ width: 45, height: 45, bgcolor: '#ff8201'}}
     125                                                                >
     126                                                                    {comp.type?.substring(0, 3)?.toUpperCase()}
     127                                                                </Avatar>
     128                                                            </TableCell>
     129                                                            <TableCell>
     130                                                                <Typography variant="body2" color="text.secondary" sx={{ fontSize: '0.75rem', textTransform: 'uppercase' }}>
     131                                                                    {comp.type}
     132                                                                </Typography>
     133                                                                <Typography variant="body1" fontWeight="500">
     134                                                                    {comp.brand} {comp.name}
     135                                                                </Typography>
     136                                                            </TableCell>
     137                                                            <TableCell align="right" sx={{ fontWeight: 'bold', color: '#ff8201' }}>
     138                                                                {formatPrice(comp.price)}
     139                                                            </TableCell>
     140                                                        </TableRow>
     141                                                    ))}
     142                                                    <TableRow sx={{ bgcolor: '#424343' }}>
     143                                                        <TableCell colSpan={2} sx={{ fontWeight: 'bold', color: '#ff8201' }}>TOTAL</TableCell>
     144                                                        <TableCell align="right" sx={{ fontWeight: 'bold', fontSize: '1.1rem', color: 'primary.main' }}>
     145                                                            {formatPrice(details.totalPrice)}
    113146                                                        </TableCell>
    114147                                                    </TableRow>
    115                                                 ))}
    116                                                 <TableRow sx={{ bgcolor: '#424343' }}>
    117                                                     <TableCell colSpan={2} sx={{ fontWeight: 'bold', color: '#ff8201' }}>TOTAL</TableCell>
    118                                                     <TableCell align="right" sx={{ fontWeight: 'bold', fontSize: '1.1rem', color: 'primary.main' }}>
    119                                                         {formatPrice(details.totalPrice)}
    120                                                     </TableCell>
    121                                                 </TableRow>
    122                                             </TableBody>
    123                                         </Table>
     148                                                </TableBody>
     149                                            </Table>
     150                                        </Grid>
     151
     152                                        <Grid item xs={12} md={4}>
     153                                            <Box sx={{ bgcolor: '#424343', p: 2, borderRadius: 2, mb: 2 }}>
     154                                                <Typography color="primary.main" gutterBottom fontWeight="bold">Builder's Notes</Typography>
     155                                                <Typography color="primary.main" variant="body2" sx={{ fontStyle: 'italic' }}>
     156                                                    "{details.description || "No notes provided."}"
     157                                                </Typography>
     158                                            </Box>
     159
     160                                            <Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
     161                                                <Button
     162                                                    variant="contained"
     163                                                    color="primary"
     164                                                    size="large"
     165                                                    startIcon={<AutoFixHighIcon />}
     166                                                    onClick={() => {
     167                                                        setCloningBuildId(details.id);
     168                                                        setCloneDialogOpen(true);
     169                                                    }}
     170                                                >
     171                                                    Clone & Edit
     172                                                </Button>
     173                                                <Button
     174                                                    variant={details.isFavorite ? "contained" : "outlined"}
     175                                                    color={details.isFavorite ? "error" : "primary"}
     176                                                    startIcon={details.isFavorite ? <FavoriteIcon /> : <FavoriteBorderIcon />}
     177                                                    onClick={handleFavorite}
     178                                                >
     179                                                    {details.isFavorite ? "Favorited" : "Add to Favorites"}
     180                                                </Button>
     181                                            </Box>
     182                                        </Grid>
    124183                                    </Grid>
    125 
    126                                     <Grid item xs={12} md={4}>
    127                                         <Box sx={{ bgcolor: '#424343', p: 2, borderRadius: 2, mb: 2 }}>
    128                                             <Typography color="primary.main" gutterBottom fontWeight="bold">Builder's Notes</Typography>
    129                                             <Typography color="primary.main" variant="body2" sx={{ fontStyle: 'italic' }}>
    130                                                 "{details.description || "No notes provided."}"
    131                                             </Typography>
     184                                )}
     185
     186                                {tabIndex === 1 && (
     187                                    <Box>
     188                                        <Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 4, p: 2, bgcolor: '#5e5e5e', borderRadius: 2 }}>
     189                                            <Typography variant="h3" fontWeight="bold">{details.ratingStatistics.averageRating.toFixed(1)}</Typography>
     190                                            <Box>
     191                                                <Rating value={details.ratingStatistics.averageRating} readOnly precision={0.5} />
     192                                                <Typography variant="body2" color="text.secondary">{details.ratingStatistics.ratingCount} ratings</Typography>
     193                                            </Box>
    132194                                        </Box>
    133195
    134                                         <Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
    135                                             <Button
    136                                                 variant="contained"
    137                                                 color="primary"
    138                                                 size="large"
    139                                                 startIcon={<AutoFixHighIcon />}
    140                                                 onClick={() => onClone({ buildId: details.id })}
    141                                             >
    142                                                 Clone & Edit
    143                                             </Button>
    144                                             <Button
    145                                                 variant={details.isFavorite ? "contained" : "outlined"}
    146                                                 color={details.isFavorite ? "error" : "primary"}
    147                                                 startIcon={details.isFavorite ? <FavoriteIcon /> : <FavoriteBorderIcon />}
    148                                                 onClick={handleFavorite}
    149                                             >
    150                                                 {details.isFavorite ? "Favorited" : "Add to Favorites"}
    151                                             </Button>
    152                                         </Box>
    153                                     </Grid>
    154                                 </Grid>
    155                             )}
    156 
    157                             {tabIndex === 1 && (
    158                                 <Box>
    159                                     <Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 4, p: 2, bgcolor: '#5e5e5e', borderRadius: 2 }}>
    160                                         <Typography variant="h3" fontWeight="bold">{details.ratingStatistics.averageRating.toFixed(1)}</Typography>
    161                                         <Box>
    162                                             <Rating value={details.ratingStatistics.averageRating} readOnly precision={0.5} />
    163                                             <Typography variant="body2" color="text.secondary">{details.ratingStatistics.ratingCount} ratings</Typography>
     196                                        {currentUser && details.userId !== currentUser.id && (
     197                                            <Box sx={{ mb: 4, p: 2, border: '1px solid #ddd', borderRadius: 2 }}>
     198                                                <Typography variant="subtitle2" gutterBottom>Your Review</Typography>
     199                                                <Box sx={{ display: 'flex', alignItems: 'center', mb: 1 }}>
     200                                                    <Rating value={ratingVal} onChange={(_, v) => setRatingVal(v || 5)} />
     201                                                </Box>
     202                                                <TextField
     203                                                    fullWidth
     204                                                    multiline
     205                                                    rows={2}
     206                                                    placeholder="Share your thoughts on this build..."
     207                                                    value={reviewText}
     208                                                    onChange={(e) => setReviewText(e.target.value)}
     209                                                    sx={{ mb: 1 }}
     210                                                />
     211                                                <Button size="small" variant="contained" onClick={handleSubmitReview}>
     212                                                    Submit Review
     213                                                </Button>
     214                                            </Box>
     215                                        )}
     216
     217                                        {currentUser && details.userId === currentUser.id && (
     218                                            <Alert severity="info" sx={{ mb: 4 }}>
     219                                                You cannot rate your own builds.
     220                                            </Alert>
     221                                        )}
     222
     223                                        <Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
     224                                            {details.reviews.map((rev: any, i: number) => (
     225                                                <Box key={i} sx={{ pb: 2, borderBottom: '1px solid #eee' }}>
     226                                                    <Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 0.5 }}>
     227                                                        <Typography fontWeight="bold" variant="body2">{rev.username}</Typography>
     228                                                        <Typography variant="caption" color="text.secondary">{rev.createdAt}</Typography>
     229                                                    </Box>
     230                                                    <Typography variant="body2">{rev.content}</Typography>
     231                                                </Box>
     232                                            ))}
     233                                            {details.reviews.length === 0 && (
     234                                                <Typography color="text.secondary" align="center">No reviews yet. Be the first!</Typography>
     235                                            )}
    164236                                        </Box>
    165237                                    </Box>
    166 
    167                                     {currentUser && (
    168                                         <Box sx={{ mb: 4, p: 2, border: '1px solid #ddd', borderRadius: 2 }}>
    169                                             <Typography variant="subtitle2" gutterBottom>Your Review</Typography>
    170                                             <Box sx={{ display: 'flex', alignItems: 'center', mb: 1 }}>
    171                                                 <Rating value={ratingVal} onChange={(_, v) => setRatingVal(v || 5)} />
    172                                             </Box>
    173                                             <TextField
    174                                                 fullWidth
    175                                                 multiline
    176                                                 rows={2}
    177                                                 placeholder="Share your thoughts on this build..."
    178                                                 value={reviewText}
    179                                                 onChange={(e) => setReviewText(e.target.value)}
    180                                                 sx={{ mb: 1 }}
    181                                             />
    182                                             <Button size="small" variant="contained" onClick={handleSubmitReview}>
    183                                                 Submit Review
    184                                             </Button>
    185                                         </Box>
    186                                     )}
    187 
    188                                     <Box sx={{ display: 'flex', flexDirection: 'column', gap: 2}}>
    189                                         {details.reviews.map((rev: any, i: number) => (
    190                                             <Box key={i} sx={{ pb: 2, borderBottom: '1px solid #eee'}}>
    191                                                 <Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 0.5 }}>
    192                                                     <Typography fontWeight="bold" variant="body2">{rev.username}</Typography>
    193                                                     <Typography variant="caption" color="text.secondary">{rev.createdAt}</Typography>
    194                                                 </Box>
    195                                                 <Typography variant="body2">{rev.content}</Typography>
    196                                             </Box>
    197                                         ))}
    198                                         {details.reviews.length === 0 && (
    199                                             <Typography color="text.secondary" align="center">No reviews yet. Be the first!</Typography>
    200                                         )}
    201                                     </Box>
    202                                 </Box>
    203                             )}
    204                         </Box>
    205                     </DialogContent>
    206                 </>
    207             )}
    208         </Dialog>
     238                                )}
     239                            </Box>
     240                        </DialogContent>
     241                    </>
     242                )}
     243            </Dialog>
     244
     245            <Dialog open={cloneDialogOpen} onClose={() => setCloneDialogOpen(false)}>
     246                <DialogTitle>Clone Build</DialogTitle>
     247                <DialogContent>
     248                    <Typography>
     249                        This will create a copy of "{details?.name}" in your Forge so you can edit it.
     250                        All components will be copied over.
     251                    </Typography>
     252                </DialogContent>
     253                <DialogActions>
     254                    <Button onClick={() => setCloneDialogOpen(false)}>Cancel</Button>
     255                    <Button
     256                        variant="contained"
     257                        onClick={handleCloneConfirm}
     258                        disabled={!cloningBuildId}
     259                    >
     260                        Clone Build
     261                    </Button>
     262                </DialogActions>
     263            </Dialog>
     264        </>
    209265    );
    210266}
  • components/ComponentDetailsDialog.tsx

    rae5d054 r8a7f936  
    3333    const renderValue = (key: string, val: any) => {
    3434        if (Array.isArray(val)) {
    35             if (val.length > 0 && typeof val[0] === 'object') {
    36                 return val.map((v: any) => v.formFactor || v.socket || JSON.stringify(v)).join(', ');
     35            if (val.length === 0) return 'None';
     36
     37            if (typeof val[0] === 'string') {
     38                return val.join(', ');
    3739            }
     40
     41            if (typeof val[0] === 'object') {
     42                const parts = val
     43                    .map((v: any) =>
     44                        v.socket ||
     45                        v.formFactor ||
     46                        v.name ||
     47                        v.type ||
     48                        Object.values(v)[0]
     49                    )
     50                    .filter(Boolean);
     51                return parts.length > 0 ? parts.join(', ') : 'None';
     52            }
     53
    3854            return val.join(', ');
    3955        }
     
    4258        const lowerKey = key.toLowerCase();
    4359
    44         // Dodava merni edinici vo zavisnost koja komponenta e
    4560        if (lowerKey.includes('capacity') || lowerKey.includes('vram') || lowerKey === 'memory') {
    4661            return `${strVal} GB`;
     
    5873            return `${strVal} GHz`;
    5974        }
     75
    6076        if (lowerKey.includes('speed')) {
    6177            return `${strVal} MHz`;
  • components/ComponentDialog.tsx

    rae5d054 r8a7f936  
    11import React, {useEffect, useState} from 'react';
    22import {
    3     Dialog, DialogContent, IconButton, Grid, Box, Typography,
     3    Dialog, DialogContent, IconButton, Box, Typography, Chip,
    44    Slider, FormControl, InputLabel, Select, MenuItem, Checkbox, ListItemText,
    5     Card, CardContent, CardMedia, Button, CircularProgress, Divider, AppBar, Toolbar
     5    Card, CardContent, CardMedia, Button, CircularProgress, AppBar, Toolbar, InputAdornment, TextField,
     6    TextField as MuiTextField, DialogTitle, DialogActions, Snackbar, Alert
    67} from '@mui/material';
    78import CloseIcon from '@mui/icons-material/Close';
    8 import FilterListIcon from '@mui/icons-material/FilterList';
    9 import SortIcon from '@mui/icons-material/Sort';
    10 
    11 import {onGetAllComponents} from '../pages/+Layout.telefunc';
     9import AddCircleIcon from '@mui/icons-material/AddCircle';
     10
     11import {onGetAllComponents, onGetAuthState, onSuggestComponent} from '../pages/+Layout.telefunc'
     12import {onGetCompatibleComponents} from '../pages/forge/forge.telefunc';
    1213import ComponentDetailsDialog from "./ComponentDetailsDialog";
    13 
    14 const formatMoney = (amount: number) => `$${amount}`;
    15 
    16 export default function ComponentBrowserDialog({open, category, onClose}: any) {
     14import SearchIcon from "@mui/icons-material/Search";
     15
     16const formatMoney = (amount: number) => `$${Number(amount).toFixed(2)}`;
     17
     18export default function ComponentDialog({open, category, onClose, mode, onSelect, currentBuildId}: any) {
    1719    const [components, setComponents] = useState<any[]>([]);
    1820    const [loading, setLoading] = useState(false);
    19 
    2021    const [selectedComponent, setSelectedComponent] = useState<any>(null);
    2122    const [priceRange, setPriceRange] = useState<number[]>([0, 2000]);
    2223    const [selectedBrands, setSelectedBrands] = useState<string[]>([]);
    2324    const [availableBrands, setAvailableBrands] = useState<string[]>([]);
    24 
    2525    const [sortOrder, setSortOrder] = useState<string>('default');
     26
     27    const [tempSearchQuery, setTempSearchQuery] = useState("");
     28    const [searchQuery, setSearchQuery] = useState("");
     29
     30    const [suggestOpen, setSuggestOpen] = useState(false);
     31    const [suggestForm, setSuggestForm] = useState({
     32        link: '',
     33        description: '',
     34        componentType: category || ''
     35    });
     36    const [suggestLoading, setSuggestLoading] = useState(false);
     37    const [snackbarOpen, setSnackbarOpen] = useState(false);
     38    const [snackbarMessage, setSnackbarMessage] = useState("");
     39    const [userId, setUserId] = useState<number | null>(null);
     40
     41    useEffect(() => {
     42        const timeoutId = setTimeout(() => {
     43            setSearchQuery(tempSearchQuery);
     44        }, 300);
     45        return () => clearTimeout(timeoutId);
     46    }, [tempSearchQuery]);
     47
     48    // Load user auth
     49    useEffect(() => {
     50        if (open) {
     51            onGetAuthState().then(userData => {
     52                setUserId(userData.userId);
     53            });
     54        }
     55    }, [open]);
    2656
    2757    useEffect(() => {
     
    3060            setSortOrder('price_desc');
    3161
    32             onGetAllComponents({componentType: category})
     62            const shouldUseCompatibility = mode === 'forge' && currentBuildId;
     63            const fetcher = shouldUseCompatibility
     64                ? onGetCompatibleComponents({
     65                    buildId: currentBuildId,
     66                    componentType: category,
     67                    limit: 100,
     68                    sort: 'price_desc'
     69                })
     70                : onGetAllComponents({
     71                    componentType: category,
     72                    limit: 100,
     73                    sort: 'price_desc'
     74                });
     75
     76            fetcher
    3377                .then((data) => {
    34                     setComponents(data);
    35 
    36                     const brands = Array.from(new Set(data.map((c: any) => c.brand)));
     78                    const comps = data || [];
     79                    setComponents(comps);
     80
     81                    const brands = Array.from(new Set(comps.map((c: any) => c.brand)));
    3782                    setAvailableBrands(brands as string[]);
    38 
    39                     const maxPrice = data.length > 0 ? Math.max(...data.map((c: any) => Number(c.price))) : 2000;
     83                    const maxPrice = comps.length > 0 ? Math.max(...comps.map((c: any) => Number(c.price))) : 2000;
    4084                    setPriceRange([0, Math.ceil(maxPrice)]);
    4185                })
    42                 .catch(console.error)
     86                .catch((err) => {
     87                    console.error('[Dialog] fetch error:', err);
     88                    setComponents([]);
     89                })
    4390                .finally(() => setLoading(false));
    4491        }
    45     }, [open, category]);
     92    }, [open, category, mode, currentBuildId]);
     93
     94    const handleSuggestChange = (e: React.ChangeEvent<HTMLInputElement>) => {
     95        setSuggestForm({
     96            ...suggestForm,
     97            [e.target.name]: e.target.value
     98        });
     99    };
     100
     101    const submitSuggestion = async () => {
     102        if (!userId) {
     103            setSnackbarMessage("Please login to submit suggestions!");
     104            setSnackbarOpen(true);
     105            return;
     106        }
     107
     108        if (!suggestForm.link || !suggestForm.description) {
     109            setSnackbarMessage("Please fill in all fields!");
     110            setSnackbarOpen(true);
     111            return;
     112        }
     113
     114        setSuggestLoading(true);
     115        try {
     116            const suggestionId = await onSuggestComponent({
     117                link: suggestForm.link,
     118                description: suggestForm.description,
     119                componentType: suggestForm.componentType
     120            });
     121
     122            setSnackbarMessage("Suggestion submitted! Admin will review it.");
     123            setSnackbarOpen(true);
     124            setSuggestOpen(false);
     125            setSuggestForm({link: '', description: '', componentType: category || ''});
     126        } catch (error) {
     127            console.error('Suggestion error:', error);
     128            setSnackbarMessage("Failed to submit suggestion. Try again.");
     129            setSnackbarOpen(true);
     130        } finally {
     131            setSuggestLoading(false);
     132        }
     133    };
    46134
    47135    let processedComponents = components.filter(comp => {
    48136        const matchesBrand = selectedBrands.length === 0 || selectedBrands.includes(comp.brand);
    49137        const matchesPrice = Number(comp.price) >= priceRange[0] && Number(comp.price) <= priceRange[1];
    50         return matchesBrand && matchesPrice;
     138        const matchesSearch = searchQuery === "" ||
     139            comp.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
     140            comp.brand.toLowerCase().includes(searchQuery.toLowerCase());
     141
     142        return matchesBrand && matchesPrice && matchesSearch;
    51143    });
    52144
     
    65157
    66158    return (
    67         <Dialog
    68             open={open}
    69             onClose={onClose}
    70             maxWidth="xl"
    71             fullWidth
    72             sx={{height: '90vh'}}
    73         >
    74             <AppBar position="relative" sx={{bgcolor: '#ff8201'}}>
    75                 <Toolbar>
    76                     <Typography sx={{ml: 2, flex: 1, textTransform: 'capitalize'}} variant="h6" component="div">
    77                         Browsing: <b>{category === 'gpu' ? 'Graphics Cards' : category?.toUpperCase()}</b>
    78                     </Typography>
    79                     <IconButton edge="start" color="inherit" onClick={onClose}>
    80                         <CloseIcon/>
    81                     </IconButton>
    82                 </Toolbar>
    83             </AppBar>
    84 
    85             <DialogContent sx={{p: 0, display: 'flex', height: '100%'}}>
    86                 <Box sx={{
    87                     width: 300,
    88                     borderRight: '1px solid #ddd',
    89                     p: 3,
    90                     bgcolor: '#1e1e1e',
    91                     display: {xs: 'none', md: 'block'}
    92                 }}>
    93                     <Box sx={{display: 'flex', alignItems: 'center', mb: 2}}>
    94                         <SortIcon sx={{mr: 1}}/>
    95                         <Typography variant="h6">Sort By</Typography>
     159        <>
     160            <Dialog open={open} onClose={onClose} maxWidth="xl" fullWidth sx={{height: '90vh'}}>
     161                <AppBar position="relative" sx={{bgcolor: '#ff8201'}}>
     162                    <Toolbar>
     163                        <Typography sx={{ml: 2, flex: 1, textTransform: 'capitalize'}} variant="h6" component="div">
     164                            Browsing: <b>{category === 'gpu' ? 'Graphics Cards' : category?.toUpperCase()}</b>
     165                            {mode === 'forge' && currentBuildId && (
     166                                <Typography variant="caption"
     167                                            sx={{ml: 2, bgcolor: 'rgba(0,0,0,0.2)', px: 1, borderRadius: 1}}>
     168                                    COMPATIBILITY MODE ON
     169                                </Typography>
     170                            )}
     171                        </Typography>
     172                        <IconButton edge="start" color="inherit" onClick={onClose}>
     173                            <CloseIcon/>
     174                        </IconButton>
     175                    </Toolbar>
     176                </AppBar>
     177
     178                <DialogContent sx={{p: 0, display: 'flex', height: '100%'}}>
     179                    <Box sx={{
     180                        width: 300,
     181                        borderRight: '1px solid #ddd',
     182                        p: 3,
     183                        bgcolor: '#1e1e1e',
     184                        display: {xs: 'none', md: 'block'},
     185                        overflowY: 'auto'
     186                    }}>
     187                        <TextField
     188                            fullWidth
     189                            size="small"
     190                            placeholder="Search components..."
     191                            value={tempSearchQuery}
     192                            onChange={(e) => setTempSearchQuery(e.target.value)}
     193                            sx={{mb: 2}}
     194                            InputProps={{
     195                                startAdornment: <InputAdornment position="start"><SearchIcon/></InputAdornment>,
     196                            }}
     197                        />
     198
     199
     200                        <FormControl fullWidth size="small" sx={{mb: 2}}>
     201                            <InputLabel>Sort By</InputLabel>
     202                            <Select value={sortOrder} label="Sort By" onChange={(e) => setSortOrder(e.target.value)}>
     203                                <MenuItem value="price_asc">Price: Low to High</MenuItem>
     204                                <MenuItem value="price_desc">Price: High to Low</MenuItem>
     205                            </Select>
     206                        </FormControl>
     207
     208                        <FormControl fullWidth size="small" sx={{mb: 2}}>
     209                            <InputLabel>Brands</InputLabel>
     210                            <Select multiple value={selectedBrands} onChange={handleBrandChange} label="Brands"
     211                                    renderValue={(s) => s.join(', ')}>
     212                                {availableBrands.map((brand) => (
     213                                    <MenuItem key={brand} value={brand}>
     214                                        <Checkbox checked={selectedBrands.indexOf(brand) > -1}/>
     215                                        <ListItemText primary={brand}/>
     216                                    </MenuItem>
     217                                ))}
     218                            </Select>
     219                        </FormControl>
     220
     221                        <Typography gutterBottom fontWeight="bold">Price Range</Typography>
     222                        <Slider value={priceRange} onChange={(_, v) => setPriceRange(v as number[])} min={0} max={2000}
     223                                sx={{color: '#ff8201'}}/>
     224                        <Box sx={{display: 'flex', justifyContent: 'space-between', mt: 1}}>
     225                            <Typography variant="caption">${priceRange[0]}</Typography>
     226                            <Typography variant="caption">${priceRange[1]}+</Typography>
     227                        </Box>
     228                        {/*TO BE FINISHED*/}
     229                        <Button
     230                            fullWidth
     231                            variant="contained"
     232                            startIcon={<AddCircleIcon/>}
     233                            onClick={() => setSuggestOpen(true)}
     234                            sx={{
     235                                mt: 2,
     236                                bgcolor: '#ff8201',
     237                                fontWeight: 'bold',
     238                                fontSize: '0.875rem',
     239                                '&:hover': {bgcolor: '#e67300'}
     240                            }}
     241                        >
     242                            Suggest Component
     243                        </Button>
    96244                    </Box>
    97                     <FormControl fullWidth size="small" sx={{mb: 4}}>
    98                         <InputLabel>Price</InputLabel>
    99                         <Select
    100                             value={sortOrder}
    101                             label="Price"
    102                             onChange={(e) => setSortOrder(e.target.value)}>
    103                             {/*<MenuItem value="default">Featured</MenuItem>*/}
    104                             <MenuItem value="price_asc">Price: Low to High</MenuItem>
    105                             <MenuItem value="price_desc">Price: High to Low</MenuItem>
    106                         </Select>
    107                     </FormControl>
    108 
    109                     <Divider sx={{mb: 3}}/>
    110 
    111                     <Box sx={{display: 'flex', alignItems: 'center', mb: 2}}>
    112                         <FilterListIcon sx={{mr: 1}}/>
    113                         <Typography variant="h6">Filters</Typography>
    114                     </Box>
    115 
    116                     <Typography gutterBottom fontWeight="bold">Price Range</Typography>
    117                     <Slider
    118                         value={priceRange}
    119                         onChange={(_, newValue) => setPriceRange(newValue as number[])}
    120                         valueLabelDisplay="auto"
    121                         min={0}
    122                         max={components.length > 0 ? Math.max(...components.map(c => Number(c.price))) : 2000}
    123                         sx={{color: '#ff8201', mb: 2}}
    124                     />
    125                     <Box sx={{display: 'flex', justifyContent: 'space-between', mb: 2}}>
    126                         <Typography variant="caption">{formatMoney(priceRange[0])}</Typography>
    127                         <Typography variant="caption">{formatMoney(priceRange[1])}</Typography>
    128                     </Box>
    129 
    130                     <FormControl fullWidth size="small">
    131                         <InputLabel>Brands</InputLabel>
    132                         <Select
    133                             multiple
    134                             value={selectedBrands}
    135                             onChange={handleBrandChange}
    136                             renderValue={(selected) => selected.join(', ')}
    137                             label="Brands"
    138                         >
    139                             {availableBrands.map((brand) => (
    140                                 <MenuItem key={brand} value={brand}>
    141                                     <Checkbox checked={selectedBrands.indexOf(brand) > -1}/>
    142                                     <ListItemText primary={brand}/>
    143                                 </MenuItem>
    144                             ))}
    145                         </Select>
    146                     </FormControl>
    147                 </Box>
    148 
    149                 <Box sx={{flex: 1, p: 3, overflowY: 'auto'}}>
    150                     {loading ? (
    151                         <Box sx={{display: 'flex', justifyContent: 'center', mt: 10}}>
    152                             <CircularProgress sx={{color: '#ff8201'}}/>
    153                         </Box>
    154                     ) : (
    155                         <Grid container spacing={1}>
    156                             {processedComponents.map((comp) => (
    157                                 <Grid item xs={12} sm={6} md={4} lg={3} key={comp.id}>
    158                                     <Card elevation={1} sx={{
    159                                         height: '100%',
    160                                         display: 'flex',
    161                                         flexDirection: 'column',
    162                                         maxWidth: 220,
    163                                         width: 220,
    164                                         overflow: 'hidden',
    165                                         whiteSpace: 'nowrap',
    166                                         textOverflow: 'ellipsis'
    167                                     }}>
    168                                         <CardMedia
    169                                             component="img"
    170                                             height="140"
    171                                             // image={`https://placehold.co/400x400?text=${encodeURIComponent(comp.name)}`}
    172                                             image={comp.imgUrl}
    173                                             alt={comp.name}
    174                                             sx={{p: 2, objectFit: 'contain'}}
    175                                         />
    176                                         <CardContent sx={{flexGrow: 1}}>
    177                                             <Typography variant="caption" color="text.secondary"
    178                                                         sx={{textTransform: 'uppercase'}}>
    179                                                 {comp.brand}
    180                                             </Typography>
    181                                             <Typography variant="subtitle1" fontWeight="bold" sx={{
    182                                                 lineHeight: 1.2,
    183                                                 mb: 1,
     245
     246                    <Box sx={{flex: 1, p: 3, overflowY: 'auto', bgcolor: '#121212'}}>
     247                        {loading ? (
     248                            <Box sx={{display: 'flex', justifyContent: 'center', mt: 10}}>
     249                                <CircularProgress sx={{color: '#ff8201'}}/>
     250                            </Box>
     251                        ) : (
     252                            <Box sx={{
     253                                display: 'grid',
     254                                gridTemplateColumns: {
     255                                    xs: '1fr',
     256                                    sm: 'repeat(2, 1fr)',
     257                                    md: 'repeat(3, 1fr)',
     258                                    lg: 'repeat(4, 1fr)',
     259                                    xl: 'repeat(5, 1fr)'
     260                                },
     261                                gap: 3,
     262                                width: '100%'
     263                            }}>
     264                                {processedComponents.map((comp) => (
     265                                    <Box key={comp.id} sx={{width: '100%'}}>
     266                                        <Card
     267                                            elevation={3}
     268                                            sx={{
     269                                                width: '100%',
     270                                                height: '100%',
     271                                                display: 'flex',
     272                                                flexDirection: 'column',
     273                                                transition: 'transform 0.2s, box-shadow 0.2s',
     274                                                '&:hover': {
     275                                                    transform: 'translateY(-4px)',
     276                                                    boxShadow: 6
     277                                                }
     278                                            }}
     279                                        >
     280                                            <Box sx={{position: 'relative', paddingTop: '75%', bgcolor: '#1e1e1e'}}>
     281                                                <CardMedia
     282                                                    component="img"
     283                                                    image={comp.imgUrl || comp.img_url || `https://placehold.co/400x400?text=${comp.name}`}
     284                                                    alt={comp.name}
     285                                                    sx={{
     286                                                        position: 'absolute',
     287                                                        top: 0,
     288                                                        left: 0,
     289                                                        width: '100%',
     290                                                        height: '100%',
     291                                                        objectFit: 'contain',
     292                                                        p: 2
     293                                                    }}
     294                                                />
     295                                            </Box>
     296
     297                                            <CardContent sx={{
     298                                                flexGrow: 1,
     299                                                display: 'flex',
     300                                                flexDirection: 'column',
     301                                                justifyContent: 'space-between'
    184302                                            }}>
    185                                                 {comp.name}
    186                                             </Typography>
    187 
    188                                             <Box sx={{
    189                                                 display: 'flex',
    190                                                 justifyContent: 'space-between',
    191                                                 alignItems: 'center',
    192                                                 mt: 2
    193                                             }}>
    194                                                 <Typography variant="h6" color="primary.main">
     303                                                <Box>
     304                                                    <Typography variant="caption" color="text.secondary"
     305                                                                sx={{textTransform: 'uppercase', letterSpacing: 0.5}}>
     306                                                        {comp.brand}
     307                                                    </Typography>
     308                                                    <Typography variant="subtitle1" fontWeight="bold" sx={{
     309                                                        lineHeight: 1.2,
     310                                                        mt: 0.5,
     311                                                        mb: 1,
     312                                                        overflow: 'hidden',
     313                                                        textOverflow: 'ellipsis',
     314                                                        display: '-webkit-box',
     315                                                        WebkitLineClamp: 2,
     316                                                        WebkitBoxOrient: 'vertical',
     317                                                        minHeight: '2.4em'
     318                                                    }}>
     319                                                        {comp.name}
     320                                                    </Typography>
     321                                                </Box>
     322                                                <Typography variant="h6" color="primary.main" fontWeight="bold">
    195323                                                    {formatMoney(comp.price)}
    196324                                                </Typography>
     325                                            </CardContent>
     326
     327                                            <Box sx={{p: 2, pt: 0, display: 'flex', flexDirection: 'column', gap: 1}}>
     328                                                <Button
     329                                                    fullWidth
     330                                                    variant="contained"
     331                                                    onClick={() => setSelectedComponent(comp)}
     332                                                    sx={{
     333                                                        bgcolor: '#ff8201',
     334                                                        fontWeight: 'bold',
     335                                                        '&:hover': {bgcolor: '#e67300'}
     336                                                    }}
     337                                                >
     338                                                    Details
     339                                                </Button>
     340                                                {mode === 'forge' && onSelect && (
     341                                                    <Button
     342                                                        fullWidth
     343                                                        variant="contained"
     344                                                        onClick={() => onSelect(comp)}
     345                                                        sx={{
     346                                                            bgcolor: '#4caf50',
     347                                                            fontWeight: 'bold',
     348                                                            '&:hover': {bgcolor: '#388e3c'}
     349                                                        }}
     350                                                    >
     351                                                        Add
     352                                                    </Button>
     353                                                )}
    197354                                            </Box>
    198                                         </CardContent>
    199                                         <Box sx={{p: 2, pt: 0}}>
    200                                             <Button
    201                                                 fullWidth
    202                                                 variant="outlined"
    203                                                 onClick={() => setSelectedComponent(comp)}
    204                                                 sx={{
    205                                                     backgroundColor: '#ff8201',
    206                                                     color: 'white',
    207                                                     borderColor: '#ff8201',
    208                                                     onHover: {backgroundColor: '#e67300', borderColor: '#e67300'}
    209                                                 }}
    210                                             >
    211                                                 Details
    212                                             </Button>
    213                                         </Box>
    214                                     </Card>
    215                                 </Grid>
    216                             ))}
    217                             {processedComponents.length === 0 && (
    218                                 <Box sx={{width: '100%', textAlign: 'center', mt: 5}}>
    219                                     <Typography variant="h6" color="text.secondary">No components match.</Typography>
    220                                 </Box>
    221                             )}
    222                         </Grid>
    223                     )}
    224                 </Box>
    225             </DialogContent>
    226             <ComponentDetailsDialog
    227                 open={!!selectedComponent}
    228                 component={selectedComponent}
    229                 onClose={() => setSelectedComponent(null)}
    230             />
    231         </Dialog>
     355                                        </Card>
     356                                    </Box>
     357                                ))}
     358                                {processedComponents.length === 0 && (
     359                                    <Box sx={{gridColumn: '1 / -1', width: '100%', textAlign: 'center', mt: 5}}>
     360                                        <Typography variant="h6" color="text.secondary">
     361                                            No compatible components found.
     362                                        </Typography>
     363                                        {mode === 'forge' && (
     364                                            <Typography variant="body2" color="error">
     365                                                (Try removing incompatible parts)
     366                                            </Typography>
     367                                        )}
     368                                    </Box>
     369                                )}
     370                            </Box>
     371                        )}
     372                    </Box>
     373                </DialogContent>
     374
     375                <ComponentDetailsDialog
     376                    open={!!selectedComponent}
     377                    component={selectedComponent}
     378                    onClose={() => setSelectedComponent(null)}
     379                />
     380            </Dialog>
     381
     382            <Dialog open={suggestOpen} onClose={() => setSuggestOpen(false)} maxWidth="sm" fullWidth>
     383                <DialogTitle>Suggest a New Component</DialogTitle>
     384                <DialogContent>
     385                    <MuiTextField
     386                        fullWidth
     387                        label="Product Link *"
     388                        name="link"
     389                        value={suggestForm.link}
     390                        onChange={handleSuggestChange}
     391                        placeholder="https://example.com/product"
     392                        sx={{mt: 1}}
     393                        size="small"
     394                    />
     395                    <MuiTextField
     396                        fullWidth
     397                        label="Description *"
     398                        name="description"
     399                        value={suggestForm.description}
     400                        onChange={handleSuggestChange}
     401                        multiline
     402                        rows={3}
     403                        placeholder="Why should we add this component? Any specs/details?"
     404                        sx={{mt: 2}}
     405                        size="small"
     406                    />
     407                    <Typography variant="caption" color="text.secondary" sx={{mt: 1, display: 'block'}}>
     408                        Category: <b>{category?.toUpperCase()}</b>
     409                    </Typography>
     410                </DialogContent>
     411                <DialogActions>
     412                    <Button onClick={() => setSuggestOpen(false)}>Cancel</Button>
     413                    <Button
     414                        onClick={submitSuggestion}
     415                        disabled={suggestLoading || !userId}
     416                        variant="contained"
     417                        startIcon={suggestLoading ? <CircularProgress size={20}/> : null}
     418                    >
     419                        {suggestLoading ? 'Submitting...' : 'Submit Suggestion'}
     420                    </Button>
     421                </DialogActions>
     422            </Dialog>
     423
     424            <Snackbar
     425                open={snackbarOpen}
     426                autoHideDuration={4000}
     427                onClose={() => setSnackbarOpen(false)}
     428                anchorOrigin={{vertical: 'bottom', horizontal: 'right'}}
     429            >
     430                <Alert onClose={() => setSnackbarOpen(false)} sx={{width: '100%'}}>
     431                    {snackbarMessage}
     432                </Alert>
     433            </Snackbar>
     434        </>
    232435    );
    233436}
  • database/drizzle/queries/components.ts

    rae5d054 r8a7f936  
    247247    if(!existingComponents) return null;
    248248
     249
     250
    249251    const existing = {
    250252        cpu: existingComponents.find(c => c.type === 'cpu'),
  • pages/dashboard/admin/+Page.tsx

    rae5d054 r8a7f936  
    4949    const openSuggestionReview = (id: number, action: 'approved' | 'rejected') => {
    5050        setSuggestionDialog({ open: true, id, action });
    51         setAdminComment(action === 'approved' ? "Approved by admin." : "Rejected: ");
     51        setAdminComment(action === 'approved' ? "" : "");
    5252    };
    5353
     
    123123                                                        </a>
    124124                                                    </TableCell>
    125                                                     <TableCell>{sug.componentType}</TableCell>
     125                                                    <TableCell>{sug.componentType.toUpperCase()}</TableCell>
    126126                                                    <TableCell>{sug.userId}</TableCell>
    127127                                                    <TableCell align="right">
     
    207207
    208208            <Dialog open={suggestionDialog.open} onClose={() => setSuggestionDialog({...suggestionDialog, open: false})}>
    209                 <DialogTitle>Review Suggestion</DialogTitle>
     209                <DialogTitle>Review Component Suggestion</DialogTitle>
    210210                <DialogContent>
    211211                    <Typography gutterBottom>Status: <b>{suggestionDialog.action.toUpperCase()}</b></Typography>
  • pages/forge/+Page.tsx

    rae5d054 r8a7f936  
    1 export default function ForgePage(){
     1import React, {useState, useEffect, useMemo} from 'react';
     2import {
     3    Container, Paper, Typography, Box, Table, TableBody, TableCell, TableContainer, TableHead, TableRow,
     4    Button, IconButton, Avatar, TextField, Grid, Chip, CircularProgress,
     5    Menu, MenuItem, ListItemIcon
     6} from '@mui/material';
     7import AddIcon from '@mui/icons-material/Add';
     8import DeleteIcon from '@mui/icons-material/Delete';
     9import CloseIcon from "@mui/icons-material/Close";
     10import AlbumIcon from "@mui/icons-material/Album";
     11import CableIcon from "@mui/icons-material/Cable";
     12import RouterIcon from "@mui/icons-material/Router";
     13import MemoryIcon from "@mui/icons-material/Memory";
     14
     15import {
     16    saveBuildState,
     17    onAddComponentToBuild,
     18    onRemoveComponentFromBuild,
     19    onDeleteBuild,
     20    onGetBuildState, onGetBuildComponents
     21} from './forge.telefunc';
     22
     23import ComponentDialog from '../../components/ComponentDialog';
     24import ComponentDetailsDialog from '../../components/ComponentDetailsDialog';
     25import {onAddNewBuild, onGetComponentDetails} from "../+Layout.telefunc";
     26import {onEditBuild} from "../dashboard/user/userDashboard.telefunc";
     27
     28type BuildSlot = {
     29    id: string;
     30    type: string;
     31    label: string;
     32    component: any | null;
     33    required: boolean;
     34};
     35
     36const INITIAL_SLOTS: BuildSlot[] = [
     37    {id: 'cpu', type: 'cpu', label: 'CPU', component: null, required: true},
     38    {id: 'cooler', type: 'cooler', label: 'CPU Cooler', component: null, required: true},
     39    {id: 'motherboard', type: 'motherboard', label: 'Motherboard', component: null, required: true},
     40    {id: 'memory_1', type: 'memory', label: 'Memory', component: null, required: true},
     41    {id: 'gpu', type: 'gpu', label: 'Video Card', component: null, required: true},
     42    {id: 'storage_1', type: 'storage', label: 'Storage', component: null, required: true},
     43    {id: 'powersupply', type: 'power_supply', label: 'Power Supply', component: null, required: true},
     44    {id: 'case', type: 'case', label: 'Case', component: null, required: true},
     45];
     46
     47export default function ForgePage() {
     48    const [slots, setSlots] = useState<BuildSlot[]>(INITIAL_SLOTS);
     49    const [buildId, setBuildId] = useState<number | null>(null);
     50    const [buildName, setBuildName] = useState("");
     51    const [description, setDescription] = useState("");
     52    const [totalPrice, setTotalPrice] = useState(0);
     53    const [isSubmitting, setIsSubmitting] = useState(false);
     54
     55    const [browserOpen, setBrowserOpen] = useState(false);
     56    const [activeSlotId, setActiveSlotId] = useState<string | null>(null);
     57    const [detailsOpen, setDetailsOpen] = useState<any>(null);
     58    const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
     59    const isSubmittedRef = React.useRef(false);
     60
     61    useEffect(() => {
     62        const price = slots.reduce((sum, slot) => sum + (Number(slot.component?.price) || 0), 0);
     63        setTotalPrice(price);
     64    }, [slots]);
     65
     66    useEffect(() => {
     67        if (!buildId) return;
     68
     69        const handleBeforeUnload = () => {
     70            if (!isSubmittedRef.current) {
     71                onDeleteBuild({buildId}).catch(() => {
     72                });
     73            }
     74        };
     75
     76        window.addEventListener('beforeunload', handleBeforeUnload);
     77
     78        return () => {
     79            window.removeEventListener('beforeunload', handleBeforeUnload);
     80            if (!isSubmittedRef.current) {
     81                onDeleteBuild({buildId}).catch(() => {
     82                });
     83            }
     84        };
     85    }, [buildId]);
     86
     87    useEffect(() => {
     88        const urlParams = new URLSearchParams(window.location.search);
     89        const urlBuildId = urlParams.get('buildId');
     90
     91        if (urlBuildId && Number.isInteger(Number(urlBuildId)) && Number(urlBuildId) > 0) {
     92            const loadBuildId = Number(urlBuildId);
     93            onGetBuildComponents({ buildId: loadBuildId })
     94                .then(async (components) => {
     95                    if (components && components.length > 0) {
     96                        setBuildId(loadBuildId);
     97                        setBuildName("Cloned Build");
     98                        setDescription("");
     99
     100                        const detailedComponents = await Promise.all(
     101                            components.map(async (c: any) => {
     102                                const full = await onGetComponentDetails({ componentId: c.id }).catch(() => null);
     103                                return full ? { ...c, ...full, details: full?.details } : c;
     104                            })
     105                        );
     106
     107                        const componentMap = new Map();
     108                        detailedComponents.forEach((c: any) => componentMap.set(c.type, c));
     109
     110                        setSlots(prevSlots => prevSlots.map(slot => {
     111                            const match = componentMap.get(slot.type);
     112                            return match ? { ...slot, component: match } : slot;
     113                        }));
     114
     115                        window.history.replaceState({}, document.title, "/forge");
     116                    }
     117                })
     118                .catch(() => {});
     119        }
     120    }, []);
     121
     122    const handlePickPart = (slotId: string) => {
     123        setActiveSlotId(slotId);
     124        setTimeout(() => setBrowserOpen(true), 0);
     125    };
     126
     127    const handleSelectComponent = async (component: any) => {
     128        if (!activeSlotId) return;
     129
     130        try {
     131            let id = buildId;
     132            if (!id) {
     133                const result = await onAddNewBuild({name: "Draft Build", description: "Work in progress"});
     134                id = typeof result === 'number' ? result : (result as any)?.buildId;
     135                if (!id || !Number.isInteger(id) || id <= 0) {
     136                    alert("Failed to create draft build.");
     137                    return;
     138                }
     139                setBuildId(id);
     140            }
     141
     142            const full = await onGetComponentDetails({componentId: component.id}).catch(() => null);
     143            const merged = full ? {...component, ...full, details: full.details} : component;
     144
     145            setSlots(prev => prev.map(slot =>
     146                slot.id === activeSlotId ? {...slot, component: merged} : slot
     147            ));
     148            setBrowserOpen(false);
     149
     150            await onAddComponentToBuild({buildId: id, componentId: component.id});
     151        } catch (e) {
     152            // console.error("Failed to add component to server build", e);
     153            alert("Failed to add component. Please try again.");
     154        } finally {
     155            setActiveSlotId(null);
     156        }
     157    };
     158
     159    const handleRemovePart = async (slotId: string) => {
     160        const slot = slots.find(s => s.id === slotId);
     161        if (!slot?.component || !buildId) return;
     162
     163        setSlots(prev => prev.map(s =>
     164            s.id === slotId ? {...s, component: null} : s
     165        ));
     166
     167        try {
     168            await onRemoveComponentFromBuild({
     169                buildId,
     170                componentId: slot.component.id
     171            });
     172        } catch (e) {
     173            console.error("Failed to remove component from server", e);
     174        }
     175    };
     176
     177    const handleAddSlot = (type: string, label: string) => {
     178        const count = slots.filter(s => s.type === type).length;
     179        const newSlot: BuildSlot = {
     180            id: `${type}_${count + 1}`,
     181            type,
     182            label: `${label} ${count > 0 ? count + 1 : ''}`,
     183            component: null,
     184            required: false
     185        };
     186        setSlots(prev => [...prev, newSlot]);
     187        setAnchorEl(null);
     188    };
     189
     190    const handleDeleteSlot = (slotId: string) => {
     191        const slot = slots.find(s => s.id === slotId);
     192        if (slot?.component) {
     193            handleRemovePart(slotId);
     194        }
     195        setSlots(prev => prev.filter(s => s.id !== slotId));
     196    };
     197
     198    const handleSubmit = async () => {
     199        if (!buildName.trim()) return alert("Please name your build!");
     200        if (!buildId) return alert("You must add at least one component before submitting.");
     201
     202        setIsSubmitting(true);
     203        try {
     204            const result = await onEditBuild({buildId});
     205
     206            if (!result) throw new Error("Failed to save build");
     207
     208            isSubmittedRef.current = true;
     209            window.location.href = "/dashboard/user";
     210        } catch (e) {
     211            console.error(e);
     212            alert("Failed to save build.");
     213        } finally {
     214            setIsSubmitting(false);
     215        }
     216    };
     217
     218
     219    const activeSlotType = useMemo(() => {
     220        if (!activeSlotId) return null;
     221        return slots.find(s => s.id === activeSlotId)?.type || null;
     222    }, [slots, activeSlotId]);
     223
    2224    return (
    3         <h1>Test Forge Page</h1>
    4     )
     225        <Container maxWidth="xl" sx={{mt: 0, mb: 10}}>
     226            <Paper sx={{p: 4, mb: 0, bgcolor: '#ff8201', border: '1px solid #1e1e1e', color: 'white'}}>
     227                <Typography variant="h4" align="center" fontWeight="bold">Forge Your Machine</Typography>
     228                <Grid container spacing={2} justifyContent="center" sx={{mt: 2}}>
     229                    <Grid item xs={12} md={6}>
     230                        <TextField
     231                            fullWidth
     232                            label="Build Name *"
     233                            value={buildName}
     234                            onChange={e => setBuildName(e.target.value)}
     235                            sx={{bgcolor: '#1e1e1e', borderRadius: 1, color: 'white'}}
     236                        />
     237                    </Grid>
     238                </Grid>
     239            </Paper>
     240
     241            <TableContainer component={Paper} elevation={3}>
     242                <Table sx={{minWidth: 650}}>
     243                    <TableHead sx={{bgcolor: '#1e1e1e'}}>
     244                        <TableRow>
     245                            <TableCell sx={{color: 'white', fontWeight: 'bold'}}>Component</TableCell>
     246                            <TableCell sx={{color: 'white', fontWeight: 'bold'}}>Selection & Specs</TableCell>
     247                            <TableCell sx={{color: 'white', fontWeight: 'bold'}}>Price</TableCell>
     248                            <TableCell sx={{color: 'white', fontWeight: 'bold'}} align="right">Actions</TableCell>
     249                        </TableRow>
     250                    </TableHead>
     251                    <TableBody>
     252                        {slots.map((slot) => (
     253                            <TableRow key={slot.id} hover>
     254                                <TableCell width="15%" sx={{
     255                                    fontWeight: 'bold',
     256                                    bgcolor: '#1e1e1e',
     257                                    color: 'white',
     258                                    verticalAlign: 'top',
     259                                    pt: 3,
     260                                    borderRight: '1px solid #333'
     261                                }}>
     262                                    {slot.label}
     263                                    {slot.required &&
     264                                        <Chip label="Required" size="small" color="error" sx={{ml: 1, height: 20}}/>}
     265                                </TableCell>
     266
     267                                <TableCell>
     268                                    {slot.component ? (
     269                                        <Box sx={{display: 'flex', gap: 2, alignItems: 'flex-start'}}>
     270                                            <Avatar
     271                                                variant="rounded"
     272                                                src={slot.component.imgUrl || slot.component.img_url}
     273                                                sx={{width: 60, height: 60, bgcolor: '#eee'}}
     274                                            >
     275                                                {slot.component.brand?.[0]}
     276                                            </Avatar>
     277                                            <Box>
     278                                                <Typography
     279                                                    variant="subtitle1"
     280                                                    fontWeight="bold"
     281                                                    sx={{cursor: 'pointer', color: 'primary.main'}}
     282                                                    onClick={() => setDetailsOpen(slot.component)}
     283                                                >
     284                                                    {slot.component.name}
     285                                                </Typography>
     286                                                <Box sx={{display: 'flex', flexWrap: 'wrap', gap: 1, mt: 0.5}}>
     287                                                    {renderSpecs(slot.component, slot.type)}
     288                                                </Box>
     289                                            </Box>
     290                                        </Box>
     291                                    ) : (
     292                                        <Button
     293                                            variant="outlined"
     294                                            startIcon={<AddIcon/>}
     295                                            onClick={() => handlePickPart(slot.id)}
     296                                            sx={{textTransform: 'none', color: '#666', borderColor: '#ccc'}}
     297                                        >
     298                                            Choose {slot.label}
     299                                        </Button>
     300                                    )}
     301                                </TableCell>
     302
     303                                <TableCell width="10%" sx={{verticalAlign: 'top', pt: 3}}>
     304                                    {slot.component ? `$${Number(slot.component.price).toFixed(2)}` : '-'}
     305                                </TableCell>
     306
     307                                <TableCell align="right" width="10%" sx={{verticalAlign: 'top', pt: 2}}>
     308                                    {slot.component && (
     309                                        <IconButton color="error" onClick={() => handleRemovePart(slot.id)}>
     310                                            <DeleteIcon/>
     311                                        </IconButton>
     312                                    )}
     313                                    {!slot.required && (
     314                                        <IconButton color="warning" onClick={() => handleDeleteSlot(slot.id)}>
     315                                            <CloseIcon/>
     316                                        </IconButton>
     317                                    )}
     318                                </TableCell>
     319                            </TableRow>
     320                        ))}
     321                    </TableBody>
     322                </Table>
     323            </TableContainer>
     324
     325            <Box sx={{mt: 4, display: 'flex', justifyContent: 'center'}}>
     326                <Button variant="outlined" startIcon={<AddIcon/>} onClick={(e) => setAnchorEl(e.currentTarget)}
     327                        sx={{mr: 2}}>
     328                    Add Optional Component
     329                </Button>
     330                <Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={() => setAnchorEl(null)}>
     331                    <Typography sx={{px: 2, py: 1, display: 'block', color: 'text.secondary'}}>
     332                        Memory & Storage
     333                    </Typography>
     334                    <MenuItem onClick={() => handleAddSlot('memory', 'Memory')}>
     335                        <ListItemIcon><MemoryIcon/></ListItemIcon>
     336                        Additional Memory
     337                    </MenuItem>
     338                    <MenuItem onClick={() => handleAddSlot('storage', 'Storage')}>
     339                        <ListItemIcon><AlbumIcon/></ListItemIcon>
     340                        Additional Storage
     341                    </MenuItem>
     342
     343                    <Typography sx={{px: 2, py: 1, display: 'block', color: 'text.secondary', mt: 1}}>
     344                        Accessories
     345                    </Typography>
     346                    <MenuItem onClick={() => handleAddSlot('optical_drive', 'Optical Drive')}>
     347                        <ListItemIcon><AlbumIcon/></ListItemIcon>
     348                        Optical Drive
     349                    </MenuItem>
     350                    <MenuItem onClick={() => handleAddSlot('cables', 'Cable')}>
     351                        <ListItemIcon><CableIcon/></ListItemIcon>
     352                        Cables
     353                    </MenuItem>
     354
     355                    <Typography sx={{px: 2, py: 1, display: 'block', color: 'text.secondary', mt: 1}}>
     356                        Expansion Cards
     357                    </Typography>
     358                    <MenuItem onClick={() => handleAddSlot('memory_card', 'Storage Card')}>
     359                        <ListItemIcon><MemoryIcon/></ListItemIcon>
     360                        Storage Card
     361                    </MenuItem>
     362                    <MenuItem onClick={() => handleAddSlot('sound_card', 'Sound Card')}>
     363                        <ListItemIcon><RouterIcon/></ListItemIcon>
     364                        Sound Card
     365                    </MenuItem>
     366                    <MenuItem onClick={() => handleAddSlot('network_card', 'Network Card')}>
     367                        <ListItemIcon><RouterIcon/></ListItemIcon>
     368                        Network Card
     369                    </MenuItem>
     370                    <MenuItem onClick={() => handleAddSlot('network_adapter', 'WiFi Adapter')}>
     371                        <ListItemIcon><RouterIcon/></ListItemIcon>
     372                        WiFi Adapter
     373                    </MenuItem>
     374                </Menu>
     375            </Box>
     376
     377            <Box sx={{mt: 4, p: 4, bgcolor: '#1e1e1e', textAlign: 'center', borderRadius: 2}}>
     378                <Typography variant="h5" sx={{mb: 2, fontWeight: 'bold', color: 'white'}}>
     379                    Total: ${totalPrice.toFixed(2)}
     380                </Typography>
     381                <Button
     382                    variant="contained"
     383                    color="primary"
     384                    size="large"
     385                    onClick={handleSubmit}
     386                    disabled={isSubmitting}
     387                >
     388                    {isSubmitting ? <CircularProgress size={24}/> : 'Submit Build For Review'}
     389                </Button>
     390            </Box>
     391
     392            <ComponentDialog
     393                open={browserOpen}
     394                category={activeSlotType}
     395                onClose={() => {
     396                    setBrowserOpen(false);
     397                    setActiveSlotId(null);
     398                }}
     399                mode="forge"
     400                onSelect={handleSelectComponent}
     401                currentBuildId={buildId}
     402            />
     403
     404            <ComponentDetailsDialog
     405                open={!!detailsOpen}
     406                component={detailsOpen}
     407                onClose={() => setDetailsOpen(null)}
     408            />
     409        </Container>
     410    );
    5411}
     412
     413function renderSpecs(c: any, type: string) {
     414    if (!c) return null;
     415    const data = {...c, ...(c.details || {})};
     416    const chipStyle = {height: 24, fontSize: '0.75rem', bgcolor: 'rgba(0,0,0,0.05)'};
     417    const specs: string[] = [];
     418    const val = (k: string) => data[k] || data[k.toLowerCase()] || data[k.replace('_', '')];
     419
     420    switch (type) {
     421        case 'cpu':
     422            if (val('socket')) specs.push(val('socket'));
     423            if (val('cores')) specs.push(`${val('cores')} Cores / ${val('threads')} Threads`);
     424            const base = data.baseclock || data.baseClock || data.base_clock;
     425            const boost = data.boostclock || data.boostClock || data.boost_clock;
     426            if (base) specs.push(`Base: ${base}GHz`);
     427            if (boost) specs.push(`Boost: ${boost}GHz`);
     428            break;
     429        case 'gpu':
     430            if (val('vram')) specs.push(`${val('vram')}GB VRAM`);
     431            if (val('chipset')) specs.push(val('chipset'));
     432            if (val('length')) specs.push(`L: ${val('length')}mm`);
     433            break;
     434        case 'motherboard':
     435            if (val('socket')) specs.push(val('socket'));
     436            if (val('formfactor')) specs.push(val('formfactor'));
     437            if (val('ramtype')) specs.push(val('ramtype'));
     438            break;
     439        case 'memory':
     440            if (val('capacity')) specs.push(`${val('capacity')}GB`);
     441            if (val('type')) specs.push(val('type'));
     442            if (val('speed')) specs.push(`${val('speed')} MHz`);
     443            if (val('modules')) specs.push(`${val('modules')}x`);
     444            break;
     445        case 'storage':
     446            if (val('capacity')) specs.push(`${val('capacity')}GB`);
     447            if (val('type')) specs.push(val('type'));
     448            break;
     449        case 'power_supply':
     450            if (val('wattage')) specs.push(`Wattage: ${val('wattage')}W`);
     451            if (val('type')) specs.push(val('type'));
     452            break;
     453        case 'case':
     454            if (val('gpuMaxLength')) specs.push(`Max GPU Length: ${val('gpuMaxLength')}mm`);
     455            if (val('coolerMaxHeight')) specs.push(`Max CPU Cooler Height: ${val('coolerMaxHeight')}mm`);
     456            break;
     457        case 'cooler':
     458            if (val('type')) specs.push(`${val('type')} Cooler`);
     459            if (val('height')) specs.push(`${val('height')}mm`);
     460            break;
     461        default:
     462            if (data.brand) specs.push(data.brand);
     463    }
     464
     465    if (specs.length === 0) {
     466        if (data.brand) return <Chip label={data.brand} sx={chipStyle}/>;
     467        return <Typography variant="caption" color="text.secondary">...</Typography>;
     468    }
     469
     470    return specs.map((label, i) => <Chip key={i} label={label} sx={chipStyle}/>);
     471}
  • pages/index/+Page.tsx

    rae5d054 r8a7f936  
    1 import React, { useEffect, useState } from 'react';
     1import React, {useEffect, useState} from 'react';
    22import {
    33    Container, Box, Typography, Button, Grid, IconButton, Dialog, DialogTitle, DialogContent
     
    1010import BuildCard from '../../components/BuildCard';
    1111
    12 import { onGetApprovedBuilds, onGetAuthState, onCloneBuild } from '../+Layout.telefunc';
     12import {onGetApprovedBuilds, onGetAuthState, onCloneBuild} from '../+Layout.telefunc';
    1313
    1414export default function HomePage() {
     
    2929                ] = await Promise.all([
    3030                    onGetAuthState(),
    31                     onGetApprovedBuilds({ limit: 5 , sort: 'rating_desc' }),
    32                     onGetApprovedBuilds({ limit: 12 }),
    33                     onGetApprovedBuilds({ limit: 10, sort: 'rating_desc' })
     31                    onGetApprovedBuilds({limit: 5, sort: 'rating_desc'}),
     32                    onGetApprovedBuilds({limit: 12}),
     33                    onGetApprovedBuilds({limit: 20, sort: 'rating_desc'})
    3434                ]);
    3535
     
    4545            }
    4646        }
     47
    4748        loadSite();
    4849    }, []);
     
    5152        if (!data?.isLoggedIn) return alert("Please login to clone builds!");
    5253        if (confirm(`Clone this build to your dashboard?`)) {
    53             await onCloneBuild({ buildId });
     54            await onCloneBuild({buildId});
    5455            alert("Build cloned! Check your dashboard.");
    5556            setSelectedBuildId(null);
     
    5758    };
    5859
    59     if (!data) return <Box sx={{ p: 10, textAlign: 'center' }}>Loading Forge...</Box>;
     60    if (!data) return <Box sx={{p: 10, textAlign: 'center'}}>Loading Forge...</Box>;
    6061
    6162    return (
     
    6970                }
    7071            }}>
    71                 <Box sx={{ position: 'relative', textAlign: 'center', zIndex: 1, p: 2 }}>
     72                <Box sx={{position: 'relative', textAlign: 'center', zIndex: 1, p: 2}}>
    7273                    <Typography variant="h2" fontWeight="bold" gutterBottom>Forge Your Ultimate Machine</Typography>
    73                     <Typography variant="h5" sx={{ maxWidth: '800px', mx: 'auto', mb: 1 }}>
     74                    <Typography variant="h5" sx={{maxWidth: '800px', mx: 'auto', mb: 1}}>
    7475                        Build, share, discuss, and discover custom PC configurations.
    7576                    </Typography>
    76                     <Button variant="contained" size="large" href="/forge" startIcon={<AutoFixHighIcon />}
    77                             sx={{ fontSize: '1.2rem', px: 4, py: 1.5, mt: 2 }}>
     77                    <Button variant="contained" size="large" href="/forge" startIcon={<AutoFixHighIcon/>}
     78                            sx={{fontSize: '1.2rem', px: 4, py: 1.5, mt: 2}}>
    7879                        Start Forging
    7980                    </Button>
     
    8182            </Box>
    8283
    83             <Container maxWidth="xl" sx={{ mt: 6, mb: 10 }}>
     84            <Container maxWidth="xl" sx={{mt: 6, mb: 10}}>
    8485                <Box sx={{
    85                     mb: 4, borderLeft: '5px solid #ff8201', pl: 2,
     86                    mb: 2, borderLeft: '5px solid #ff8201', pl: 1,
    8687                    display: 'flex', justifyContent: 'space-between', alignItems: 'end'
    8788                }}>
     
    9293                        </Typography>
    9394                    </Box>
    94                     <Button variant="outlined" startIcon={<ListIcon />} onClick={() => setOpenRankedPopup(true)}>
     95                    <Button variant="outlined" startIcon={<ListIcon/>} onClick={() => setOpenRankedPopup(true)}>
    9596                        Show All Top Ranked
    9697                    </Button>
    9798                </Box>
    98 
    99                 <Grid container spacing={3} sx={{ mb: 8, alignItems: 'stretch' }}>
    100                     {data.prebuilts.slice(0, 3).map((build: any) => (
    101                         <Grid item xs={12} sm={6} md={3} key={build.id} sx={{ display: 'flex' }}>
    102                             <Box sx={{ width: '100%' }}>
    103                                 <BuildCard
    104                                     build={build}
    105                                     onClick={() => setSelectedBuildId(build.id)}
    106                                 />
    107                             </Box>
    108                         </Grid>
     99                <Box
     100                    sx={{
     101                        display: 'grid',
     102                        gridTemplateColumns: {
     103                            xs: '1fr',
     104                            sm: 'repeat(2, 1fr)',
     105                            md: 'repeat(3, 1fr)',
     106                            lg: 'repeat(4, 1fr)',
     107                            xl: 'repeat(5, 1fr)',
     108                        },
     109                        gap: 3,
     110                        width: '90%',
     111                    }}
     112                >
     113                    {data.prebuilts.map((build: any) => (
     114                        <BuildCard
     115                            key={build.id}
     116                            build={build}
     117                            onClick={() => setSelectedBuildId(build.id)}
     118                        />
    109119                    ))}
    110                 </Grid>
    111 
    112                 <SectionHeader title="Community Forge" subtitle="Fresh builds from users around the world."/>
    113                 <Grid container spacing={3}>
     120                </Box>
     121
     122                <Box sx={{
     123                    mb: 2, mt: 2, borderLeft: '5px solid #ff8201', pl: 1,
     124                }}>
     125                    <Typography variant="h4" fontWeight="bold" color="text.primary">Community Forge</Typography>
     126                    <Typography variant="subtitle1" color="text.secondary" sx={{mb: 2}}>Fresh builds from users around
     127                        the world.</Typography>
     128                </Box>
     129                <Box
     130                    sx={{
     131                        display: 'grid',
     132                        gridTemplateColumns: {
     133                            xs: '1fr',
     134                            sm: 'repeat(2, 1fr)',
     135                            md: 'repeat(3, 1fr)',
     136                            lg: 'repeat(4, 1fr)',
     137                            xl: 'repeat(5, 1fr)',
     138                        },
     139                        gap: 3,
     140                        width: '90%',
     141                    }}
     142                >
    114143                    {data.communityBuilds.map((build: any) => (
    115                         <Grid item xs={12} sm={6} md={3} key={build.id}>
    116                             <BuildCard
    117                                 build={build}
    118                                 onClick={() => setSelectedBuildId(build.id)}
    119                             />
    120                         </Grid>
     144                        <BuildCard
     145                            key={build.id}
     146                            build={build}
     147                            onClick={() => setSelectedBuildId(build.id)}
     148                        />
    121149                    ))}
    122                 </Grid>
    123 
    124                 <Box sx={{ display: 'flex', justifyContent: 'center', mt: 6 }}>
     150                </Box>
     151
     152                <Box sx={{display: 'flex', justifyContent: 'center', mt: 6}}>
    125153                    <Button variant="outlined" size="large" href="/completed-builds">View All Community Builds</Button>
    126154                </Box>
     
    135163            />
    136164
    137             <Dialog open={openRankedPopup} onClose={() => setOpenRankedPopup(false)} maxWidth="lg" fullWidth scroll="paper">
    138                 <DialogTitle sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
     165            <Dialog open={openRankedPopup} onClose={() => setOpenRankedPopup(false)} maxWidth="xl" fullWidth
     166                    scroll="paper">
     167                <DialogTitle sx={{display: 'flex', justifyContent: 'space-between', alignItems: 'center'}}>
    139168                    Hall of Fame (Top Rated)
    140                     <IconButton onClick={() => setOpenRankedPopup(false)}><CloseIcon /></IconButton>
     169                    <IconButton onClick={() => setOpenRankedPopup(false)}><CloseIcon/></IconButton>
    141170                </DialogTitle>
    142171                <DialogContent dividers>
    143                     <Grid container spacing={3}>
     172                    <Grid container spacing={3} sx={{p: 1}}>
    144173                        {allRanked.map((build: any, index: number) => (
    145                             <Grid item xs={12} sm={6} md={3} key={`ranked-${build.id}`}>
    146                                 <Box sx={{ position: 'relative' }}>
     174                            <Grid
     175                                item
     176                                xs={12}
     177                                sm={6}
     178                                md={3}
     179                                key={`ranked-${build.id}`}
     180                                sx={{display: 'flex'}}
     181                            >
     182                                <Box sx={{position: 'relative', width: '100%', display: 'flex'}}>
    147183                                    <Box sx={{
    148184                                        position: 'absolute', top: -10, left: -10, zIndex: 1,
    149                                         width: 30, height: 30, borderRadius: '50%',
    150                                         bgcolor: index < 3 ? '#ff8201' : 'grey.700', color: 'white',
    151                                         display: 'flex', alignItems: 'center', justifyContent: 'center', fontWeight: 'bold'
     185                                        width: 35, height: 35, borderRadius: '50%',
     186                                        bgcolor: index < 3 ? '#ff8201' : 'grey.800', color: 'white',
     187                                        display: 'flex', alignItems: 'center', justifyContent: 'center',
     188                                        fontWeight: 'bold', border: '2px solid white', boxShadow: 2
    152189                                    }}>
    153190                                        #{index + 1}
     
    166203                </DialogContent>
    167204            </Dialog>
    168 
    169205        </Box>
    170206    );
    171207}
    172 
    173 function SectionHeader({ title, subtitle }: { title: string, subtitle: string }) {
    174     return (
    175         <Box sx={{ mb: 4, borderLeft: '5px solid #ff8201', pl: 2 }}>
    176             <Typography variant="h4" fontWeight="bold" color="text.primary">{title}</Typography>
    177             <Typography variant="subtitle1" color="text.secondary">{subtitle}</Typography>
    178         </Box>
    179     );
    180 }
Note: See TracChangeset for help on using the changeset viewer.