Changeset 1d8f088 for pages/forge


Ignore:
Timestamp:
01/28/26 16:43:57 (5 months ago)
Author:
Mihail <mihail2.naumov@…>
Branches:
main
Children:
1b5c18b
Parents:
a744c90
Message:

Added +- buttons for ram and storage, rmeoved them from optional components.

Location:
pages/forge
Files:
3 added
1 edited

Legend:

Unmodified
Added
Removed
  • pages/forge/+Page.tsx

    ra744c90 r1d8f088  
    55    Menu, MenuItem, ListItemIcon, Dialog, DialogTitle, DialogContent, DialogActions
    66} from '@mui/material';
     7
    78import AddIcon from '@mui/icons-material/Add';
     9import RemoveIcon from '@mui/icons-material/Remove';
    810import DeleteIcon from '@mui/icons-material/Delete';
    911import CloseIcon from "@mui/icons-material/Close";
     
    2628import {onAddNewBuild, onGetComponentDetails} from "../+Layout.telefunc";
    2729import {onEditBuild} from "../dashboard/user/userDashboard.telefunc";
    28 
    29 type BuildSlot = {
    30     id: string;
    31     type: string;
    32     label: string;
    33     component: any | null;
    34     required: boolean;
    35 };
    36 
    37 const INITIAL_SLOTS: BuildSlot[] = [
    38     {id: 'cpu', type: 'cpu', label: 'CPU', component: null, required: true},
    39     {id: 'cooler', type: 'cooler', label: 'CPU Cooler', component: null, required: true},
    40     {id: 'motherboard', type: 'motherboard', label: 'Motherboard', component: null, required: true},
    41     {id: 'memory_1', type: 'memory', label: 'Memory', component: null, required: true},
    42     {id: 'gpu', type: 'gpu', label: 'Video Card', component: null, required: true},
    43     {id: 'storage_1', type: 'storage', label: 'Storage', component: null, required: true},
    44     {id: 'powersupply', type: 'power_supply', label: 'Power Supply', component: null, required: true},
    45     {id: 'case', type: 'case', label: 'Case', component: null, required: true},
    46 ];
     30import {BuildSlot, INITIAL_SLOTS} from "./types/buildTypes";
     31import {renderSpecs} from "./utils/RenderSpecs";
     32import {
     33    getMaxRamSlots,
     34    calculateUsedRamSlots,
     35    calculateUsedStorageSlots
     36} from "./utils/componentCalculations";
     37import {Snackbar, Alert} from '@mui/material';
    4738
    4839export default function ForgePage() {
     
    5849    const [detailsOpen, setDetailsOpen] = useState<any>(null);
    5950    const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
    60     const [submitDialogOpen, setSubmitDialogOpen] = useState(false);  // NEW
    61     const [tempDescription, setTempDescription] = useState("");      // NEW
     51    const [submitDialogOpen, setSubmitDialogOpen] = useState(false);
     52    const [tempDescription, setTempDescription] = useState("");
    6253    const isSubmittedRef = React.useRef(false);
    6354
     55    const [snackbar, setSnackbar] = useState<{
     56        open: boolean;
     57        message: string;
     58        severity: 'error' | 'warning' | 'info' | 'success';
     59    }>({
     60        open: false,
     61        message: '',
     62        severity: 'info'
     63    });
     64
    6465    useEffect(() => {
    65         const price = slots.reduce((sum, slot) => sum + (Number(slot.component?.price) || 0), 0);
     66        const price = slots.reduce((sum, slot) => {
     67            const quantity = slot.component?.quantity || 1;
     68            return sum + (Number(slot.component?.price) || 0) * quantity;
     69        }, 0);
    6670        setTotalPrice(price);
    6771    }, [slots]);
     
    7074        if (buildId && buildName.trim()) {
    7175            const timeoutId = setTimeout(() => {
    72                 saveBuildState({ buildId, name: buildName.trim(), description });
     76                saveBuildState({buildId, name: buildName.trim(), description});
    7377            }, 1000);
    7478
     
    105109            const loadBuildId = Number(urlBuildId);
    106110
    107             onGetBuildState({ buildId: loadBuildId })
     111            onGetBuildState({buildId: loadBuildId})
    108112                .then((buildState) => {
    109113                    if (buildState) {
     
    113117                    }
    114118                })
    115                 .catch(() => {})
     119                .catch(() => {
     120                })
    116121                .finally(() => {
    117                     onGetBuildComponents({ buildId: loadBuildId })
     122                    onGetBuildComponents({buildId: loadBuildId})
    118123                        .then(async (components) => {
    119124                            if (components && components.length > 0) {
    120125                                const detailedComponents = await Promise.all(
    121126                                    components.map(async (c: any) => {
    122                                         const full = await onGetComponentDetails({ componentId: c.id }).catch(() => null);
    123                                         return full ? { ...c, ...full, details: full?.details } : c;
     127                                        const full = await onGetComponentDetails({componentId: c.id}).catch(() => null);
     128                                        return full ? {
     129                                            ...c,
     130                                            ...full,
     131                                            details: full?.details,
     132                                            quantity: c.quantity || 1
     133                                        } : {...c, quantity: c.quantity || 1};
    124134                                    })
    125135                                );
     
    130140                                setSlots(prevSlots => prevSlots.map(slot => {
    131141                                    const match = componentMap.get(slot.type);
    132                                     return match ? { ...slot, component: match } : slot;
     142                                    return match ? {...slot, component: match} : slot;
    133143                                }));
    134144
     
    136146                            }
    137147                        })
    138                         .catch(() => {});
     148                        .catch(() => {
     149                        });
    139150                });
    140151        }
     
    158169                id = typeof result === 'number' ? result : (result as any)?.buildId;
    159170                if (!id || !Number.isInteger(id) || id <= 0) {
    160                     alert("Failed to create draft build.");
     171                    setSnackbar({
     172                        open: true,
     173                        message: 'Failed to create draft build. Please try again.',
     174                        severity: 'error'
     175                    });
    161176                    return;
    162177                }
     
    165180
    166181            const full = await onGetComponentDetails({componentId: component.id}).catch(() => null);
    167             const merged = full ? {...component, ...full, details: full.details} : component;
     182            const merged = full ? {...component, ...full, details: full.details, quantity: 1} : {
     183                ...component,
     184                quantity: 1
     185            };
    168186
    169187            setSlots(prev => prev.map(slot =>
     
    174192            await onAddComponentToBuild({buildId: id, componentId: component.id});
    175193        } catch (e) {
    176             alert("Failed to add component. Please try again.");
     194            setSnackbar({
     195                open: true,
     196                message: 'Failed to add component to build. Please try again.',
     197                severity: 'error'
     198            });
    177199        } finally {
    178200            setActiveSlotId(null);
     
    184206        if (!slot?.component || !buildId) return;
    185207
     208        const quantity = slot.component.quantity || 1;
     209
    186210        setSlots(prev => prev.map(s =>
    187211            s.id === slotId ? {...s, component: null} : s
     
    189213
    190214        try {
    191             await onRemoveComponentFromBuild({
    192                 buildId,
    193                 componentId: slot.component.id
    194             });
     215            for (let i = 0; i < quantity; i++) {
     216                await onRemoveComponentFromBuild({
     217                    buildId,
     218                    componentId: slot.component.id
     219                });
     220            }
    195221        } catch (e) {
    196222            console.error("Failed to remove component from server", e);
     223        }
     224    };
     225
     226    const handleIncrementComponent = async (slotId: string) => {
     227        const slot = slots.find(s => s.id === slotId);
     228        if (!slot?.component || !buildId) return;
     229
     230        if (slot.type === 'memory') {
     231            const maxSlots = getMaxRamSlots(slots);
     232            const currentUsed = calculateUsedRamSlots(slots);
     233            const modules = Number(slot.component.details?.modules || slot.component.modules || 1);
     234
     235            if (maxSlots && (currentUsed + modules > maxSlots)) {
     236                setSnackbar({
     237                    open: true,
     238                    message: `Cannot add more RAM. Motherboard has only ${maxSlots} slots and ${currentUsed} are currently used.`,
     239                    severity: 'error'
     240                });
     241                return;
     242            }
     243        }
     244
     245        if (slot.type === 'storage') {
     246            const formFactor = slot.component.details?.formFactor || slot.component.formFactor;
     247            const maxSlots = 4;
     248            const currentUsed = calculateUsedStorageSlots(slots, formFactor);
     249
     250            if (maxSlots && (currentUsed + 1 > maxSlots)) {
     251                setSnackbar({
     252                    open: true,
     253                    message: `Cannot add more ${formFactor} storage. Motherboard has only ${maxSlots} slots and ${currentUsed} are currently used.`,
     254                    severity: 'error'
     255                });
     256                return;
     257            }
     258        }
     259
     260        try {
     261            await onAddComponentToBuild({buildId, componentId: slot.component.id});
     262
     263            setSlots(prev => prev.map(s =>
     264                s.id === slotId && s.component
     265                    ? {...s, component: {...s.component, quantity: (s.component.quantity || 1) + 1}}
     266                    : s
     267            ));
     268        } catch (e) {
     269            setSnackbar({
     270                open: true,
     271                message: 'Failed to increment component. Please try again.',
     272                severity: 'error'
     273            });
     274        }
     275    };
     276
     277    const handleDecrementComponent = async (slotId: string) => {
     278        const slot = slots.find(s => s.id === slotId);
     279        if (!slot?.component || !buildId) return;
     280
     281        const currentQuantity = slot.component.quantity || 1;
     282        if (currentQuantity <= 1) return;
     283
     284        try {
     285            await onRemoveComponentFromBuild({buildId, componentId: slot.component.id});
     286
     287            setSlots(prev => prev.map(s =>
     288                s.id === slotId && s.component
     289                    ? {...s, component: {...s.component, quantity: (s.component.quantity || 1) - 1}}
     290                    : s
     291            ));
     292        } catch (e) {
     293            setSnackbar({
     294                open: true,
     295                message: 'Failed to decrement component. Please try again.',
     296                severity: 'error'
     297            });
    197298        }
    198299    };
     
    220321
    221322    const handleSubmit = () => {
    222         if (!buildName.trim()) return alert("Please name your build!");
    223         if (!buildId) return alert("You must add at least one component before submitting.");
     323        if (!buildName.trim()) {
     324            setSnackbar({
     325                open: true,
     326                message: 'Please give your build a name!',
     327                severity: 'warning'
     328            });
     329            return;
     330        }
     331        if (!buildId) {
     332            setSnackbar({
     333                open: true,
     334                message: 'Please add at least one component to your build before submitting!',
     335                severity: 'warning'
     336            });
     337            return;
     338        }
    224339
    225340        setTempDescription(description || "");
     
    228343
    229344    const handleSubmitConfirm = async () => {
    230         if(!buildId) return;
     345        if (!buildId) return;
    231346
    232347        setIsSubmitting(true);
     
    234349
    235350        try {
    236             await saveBuildState({ buildId, name: buildName.trim(), description: tempDescription });
     351            await saveBuildState({buildId, name: buildName.trim(), description: tempDescription});
    237352            const result = await onEditBuild({buildId});
    238353            if (!result) throw new Error("Failed to save build");
     
    242357        } catch (e) {
    243358            console.error(e);
    244             alert("Failed to save build.");
     359            setSnackbar({
     360                open: true,
     361                message: 'Failed to save build. Please try again.',
     362                severity: 'error'
     363            });
    245364        } finally {
    246365            setIsSubmitting(false);
     
    333452
    334453                                <TableCell width="10%" sx={{verticalAlign: 'top', pt: 3}}>
    335                                     {slot.component ? `$${Number(slot.component.price).toFixed(2)}` : '-'}
     454                                    {slot.component ? (
     455                                        <>
     456                                            ${(Number(slot.component.price) * (slot.component.quantity || 1)).toFixed(2)}
     457                                            {(slot.component.quantity || 1) > 1 && (
     458                                                <Typography variant="caption" display="block" color="text.secondary">
     459                                                    ${Number(slot.component.price).toFixed(2)} each
     460                                                </Typography>
     461                                            )}
     462                                        </>
     463                                    ) : '-'}
    336464                                </TableCell>
    337465
    338                                 <TableCell align="right" width="10%" sx={{verticalAlign: 'top', pt: 2}}>
     466                                <TableCell align="right" width="15%" sx={{verticalAlign: 'top', pt: 2}}>
    339467                                    {slot.component && (
    340                                         <IconButton color="error" onClick={() => handleRemovePart(slot.id)}>
    341                                             <DeleteIcon/>
    342                                         </IconButton>
    343                                     )}
    344                                     {!slot.required && (
    345                                         <IconButton color="warning" onClick={() => handleDeleteSlot(slot.id)}>
    346                                             <CloseIcon/>
    347                                         </IconButton>
     468                                        <Box sx={{
     469                                            display: 'flex',
     470                                            gap: 1,
     471                                            justifyContent: 'flex-end',
     472                                            alignItems: 'center'
     473                                        }}>
     474                                            {(slot.type === 'memory' || slot.type === 'storage') && (
     475                                                <>
     476                                                    <IconButton
     477                                                        size="small"
     478                                                        color="primary"
     479                                                        onClick={() => handleDecrementComponent(slot.id)}
     480                                                        disabled={(slot.component.quantity || 1) <= 1}
     481                                                        sx={{
     482                                                            bgcolor: 'action.hover',
     483                                                            '&:disabled': {bgcolor: 'action.disabledBackground'}
     484                                                        }}
     485                                                    >
     486                                                        <RemoveIcon/>
     487                                                    </IconButton>
     488
     489                                                    <Typography
     490                                                        variant="body2"
     491                                                        sx={{
     492                                                            minWidth: '20px',
     493                                                            textAlign: 'center',
     494                                                            fontWeight: 'bold'
     495                                                        }}
     496                                                    >
     497                                                        {slot.component.quantity || 1}
     498                                                    </Typography>
     499
     500                                                    <IconButton
     501                                                        size="small"
     502                                                        color="primary"
     503                                                        onClick={() => handleIncrementComponent(slot.id)}
     504                                                        sx={{bgcolor: 'action.hover'}}
     505                                                    >
     506                                                        <AddIcon/>
     507                                                    </IconButton>
     508                                                </>
     509                                            )}
     510
     511                                            <IconButton
     512                                                color="error"
     513                                                onClick={() => handleRemovePart(slot.id)}
     514                                            >
     515                                                <DeleteIcon/>
     516                                            </IconButton>
     517
     518                                            {!slot.required && (
     519                                                <IconButton
     520                                                    color="warning"
     521                                                    onClick={() => handleDeleteSlot(slot.id)}
     522                                                >
     523                                                    <CloseIcon/>
     524                                                </IconButton>
     525                                            )}
     526                                        </Box>
    348527                                    )}
    349528                                </TableCell>
     
    360539                </Button>
    361540                <Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={() => setAnchorEl(null)}>
     541                    {/*Removed RAM & Storage from optional components*/}
    362542                    <Typography sx={{px: 2, py: 1, display: 'block', color: 'text.secondary'}}>
    363                         Memory & Storage
    364                     </Typography>
    365                     <MenuItem onClick={() => handleAddSlot('memory', 'Memory')}>
    366                         <ListItemIcon><MemoryIcon/></ListItemIcon>
    367                         Additional Memory
    368                     </MenuItem>
    369                     <MenuItem onClick={() => handleAddSlot('storage', 'Storage')}>
    370                         <ListItemIcon><AlbumIcon/></ListItemIcon>
    371                         Additional Storage
    372                     </MenuItem>
    373 
    374                     <Typography sx={{px: 2, py: 1, display: 'block', color: 'text.secondary', mt: 1}}>
    375543                        Accessories
    376544                    </Typography>
     
    440608
    441609            <Dialog open={submitDialogOpen} onClose={() => setSubmitDialogOpen(false)} maxWidth="sm" fullWidth>
    442                 <DialogTitle sx={{ bgcolor: '#ff8201', color: 'white', fontWeight: 'bold' }}>
     610                <DialogTitle sx={{bgcolor: '#ff8201', color: 'white', fontWeight: 'bold'}}>
    443611                    Build Description
    444612                </DialogTitle>
    445                 <DialogContent sx={{ p: 3 }}>
    446                     <Typography variant="body1" sx={{ mb: 2, color: 'text.secondary' }}>
     613                <DialogContent sx={{p: 3}}>
     614                    <Typography variant="body1" sx={{mb: 2, color: 'text.secondary'}}>
    447615                        Add some notes about your build (optional):
    448616                    </Typography>
     
    451619                        multiline
    452620                        rows={4}
    453                         placeholder="e.g. Gaming build for 1440p, quiet operation, future-proof..."
     621                        placeholder="e.g. Workstation monster, great for crunching numbers!"
    454622                        value={tempDescription}
    455623                        onChange={(e) => setTempDescription(e.target.value)}
     
    457625                    />
    458626                </DialogContent>
    459                 <DialogActions sx={{ p: 3, pt: 0 }}>
     627                <DialogActions sx={{p: 3, pt: 0}}>
    460628                    <Button onClick={() => setSubmitDialogOpen(false)}>
    461629                        Cancel
     
    469637                        {isSubmitting ? (
    470638                            <>
    471                                 <CircularProgress size={20} sx={{ mr: 1 }} />
     639                                <CircularProgress size={20} sx={{mr: 1}}/>
    472640                                Submitting...
    473641                            </>
     
    478646                </DialogActions>
    479647            </Dialog>
     648            <Snackbar
     649                open={snackbar.open}
     650                autoHideDuration={4000}
     651                onClose={() => setSnackbar(prev => ({...prev, open: false}))}
     652                anchorOrigin={{vertical: 'bottom', horizontal: 'center'}}
     653            >
     654                <Alert
     655                    onClose={() => setSnackbar(prev => ({...prev, open: false}))}
     656                    severity={snackbar.severity}
     657                    variant="filled"
     658                    sx={{width: '100%'}}
     659                >
     660                    {snackbar.message}
     661                </Alert>
     662            </Snackbar>
    480663        </Container>
    481664    );
    482665}
    483 
    484 function renderSpecs(c: any, type: string) {
    485     if (!c) return null;
    486     const data = {...c, ...(c.details || {})};
    487     const chipStyle = {height: 24, fontSize: '0.75rem', bgcolor: 'rgba(0,0,0,0.05)'};
    488     const specs: string[] = [];
    489     const val = (k: string) => data[k] || data[k.toLowerCase()] || data[k.replace('_', '')];
    490 
    491     switch (type) {
    492         case 'cpu':
    493             if (val('socket')) specs.push(val('socket'));
    494             if (val('cores')) specs.push(`${val('cores')} Cores / ${val('threads')} Threads`);
    495             const base = data.baseclock || data.baseClock || data.base_clock;
    496             const boost = data.boostclock || data.boostClock || data.boost_clock;
    497             if (base) specs.push(`Base: ${base}GHz`);
    498             if (boost) specs.push(`Boost: ${boost}GHz`);
    499             break;
    500         case 'gpu':
    501             if (val('vram')) specs.push(`${val('vram')}GB VRAM`);
    502             if (val('chipset')) specs.push(val('chipset'));
    503             if (val('length')) specs.push(`L: ${val('length')}mm`);
    504             break;
    505         case 'motherboard':
    506             if (val('socket')) specs.push(val('socket'));
    507             if (val('formfactor')) specs.push(val('formfactor'));
    508             if (val('ramtype')) specs.push(val('ramtype'));
    509             break;
    510         case 'memory':
    511             if (val('capacity')) specs.push(`${val('capacity')}GB`);
    512             if (val('type')) specs.push(val('type'));
    513             if (val('speed')) specs.push(`${val('speed')} MHz`);
    514             if (val('modules')) specs.push(`${val('modules')}x`);
    515             break;
    516         case 'storage':
    517             if (val('capacity')) specs.push(`${val('capacity')}GB`);
    518             if (val('type')) specs.push(val('type'));
    519             break;
    520         case 'power_supply':
    521             if (val('wattage')) specs.push(`Wattage: ${val('wattage')}W`);
    522             if (val('type')) specs.push(val('type'));
    523             break;
    524         case 'case':
    525             if (val('gpuMaxLength')) specs.push(`Max GPU Length: ${val('gpuMaxLength')}mm`);
    526             if (val('coolerMaxHeight')) specs.push(`Max CPU Cooler Height: ${val('coolerMaxHeight')}mm`);
    527             break;
    528         case 'cooler':
    529             if (val('type')) specs.push(`${val('type')} Cooler`);
    530             if (val('height')) specs.push(`${val('height')}mm`);
    531             break;
    532         default:
    533             if (data.brand) specs.push(data.brand);
    534     }
    535 
    536     if (specs.length === 0) {
    537         if (data.brand) return <Chip label={data.brand} sx={chipStyle}/>;
    538         return <Typography variant="caption" color="text.secondary">...</Typography>;
    539     }
    540 
    541     return specs.map((label, i) => <Chip key={i} label={label} sx={chipStyle}/>);
    542 }
Note: See TracChangeset for help on using the changeset viewer.