import React, {useState, useEffect, useMemo} from 'react'; import { Container, Paper, Typography, Box, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Button, IconButton, Avatar, TextField, Grid, Chip, CircularProgress, Menu, MenuItem, ListItemIcon, Dialog, DialogTitle, DialogContent, DialogActions } from '@mui/material'; import AddIcon from '@mui/icons-material/Add'; import RemoveIcon from '@mui/icons-material/Remove'; import DeleteIcon from '@mui/icons-material/Delete'; import CloseIcon from "@mui/icons-material/Close"; import AlbumIcon from "@mui/icons-material/Album"; import CableIcon from "@mui/icons-material/Cable"; import RouterIcon from "@mui/icons-material/Router"; import MemoryIcon from "@mui/icons-material/Memory"; import { saveBuildState, onAddComponentToBuild, onRemoveComponentFromBuild, onDeleteBuild, onGetBuildState, onGetBuildComponents } from './forge.telefunc'; import ComponentDialog from '../../components/ComponentDialog'; import ComponentDetailsDialog from '../../components/ComponentDetailsDialog'; import {onAddNewBuild, onGetComponentDetails} from "../+Layout.telefunc"; import {onEditBuild} from "../dashboard/user/userDashboard.telefunc"; import {BuildSlot, INITIAL_SLOTS} from "./types/buildTypes"; import {renderSpecs} from "./utils/RenderSpecs"; import { getMaxRamSlots, calculateUsedRamSlots, calculateUsedStorageSlots } from "./utils/componentCalculations"; import {Snackbar, Alert} from '@mui/material'; export default function ForgePage() { const [slots, setSlots] = useState(INITIAL_SLOTS); const [buildId, setBuildId] = useState(null); const [buildName, setBuildName] = useState(""); const [description, setDescription] = useState(""); const [totalPrice, setTotalPrice] = useState(0); const [isSubmitting, setIsSubmitting] = useState(false); const [browserOpen, setBrowserOpen] = useState(false); const [activeSlotId, setActiveSlotId] = useState(null); const [detailsOpen, setDetailsOpen] = useState(null); const [anchorEl, setAnchorEl] = useState(null); const [submitDialogOpen, setSubmitDialogOpen] = useState(false); const [tempDescription, setTempDescription] = useState(""); const isSubmittedRef = React.useRef(false); const [snackbar, setSnackbar] = useState<{ open: boolean; message: string; severity: 'error' | 'warning' | 'info' | 'success'; }>({ open: false, message: '', severity: 'info' }); useEffect(() => { const price = slots.reduce((sum, slot) => { const quantity = slot.component?.quantity || 1; return sum + (Number(slot.component?.price) || 0) * quantity; }, 0); setTotalPrice(price); }, [slots]); useEffect(() => { if (buildId && buildName.trim()) { const timeoutId = setTimeout(() => { saveBuildState({buildId, name: buildName.trim(), description}); }, 1000); return () => clearTimeout(timeoutId); } }, [buildId, buildName, description]); useEffect(() => { if (!buildId) return; const handleBeforeUnload = () => { if (!isSubmittedRef.current) { onDeleteBuild({buildId}).catch(() => { }); } }; window.addEventListener('beforeunload', handleBeforeUnload); return () => { window.removeEventListener('beforeunload', handleBeforeUnload); if (!isSubmittedRef.current) { onDeleteBuild({buildId}).catch(() => { }); } }; }, [buildId]); useEffect(() => { const urlParams = new URLSearchParams(window.location.search); const urlBuildId = urlParams.get('buildId'); if (urlBuildId && Number.isInteger(Number(urlBuildId)) && Number(urlBuildId) > 0) { const loadBuildId = Number(urlBuildId); onGetBuildState({buildId: loadBuildId}) .then((buildState) => { if (buildState) { setBuildId(loadBuildId); setBuildName(buildState.build.name); setDescription(buildState.build.description || ""); } }) .catch(() => { }) .finally(() => { onGetBuildComponents({buildId: loadBuildId}) .then(async (components) => { if (components && components.length > 0) { const detailedComponents = await Promise.all( components.map(async (c: any) => { const full = await onGetComponentDetails({componentId: c.id}).catch(() => null); return full ? { ...c, ...full, details: full?.details, quantity: c.quantity || 1 } : {...c, quantity: c.quantity || 1}; }) ); const componentMap = new Map(); detailedComponents.forEach((c: any) => componentMap.set(c.type, c)); setSlots(prevSlots => prevSlots.map(slot => { const match = componentMap.get(slot.type); return match ? {...slot, component: match} : slot; })); window.history.replaceState({}, document.title, "/forge"); } }) .catch(() => { }); }); } }, []); const handlePickPart = (slotId: string) => { setActiveSlotId(slotId); setTimeout(() => setBrowserOpen(true), 0); }; const handleSelectComponent = async (component: any) => { if (!activeSlotId) return; try { let id = buildId; if (!id) { const result = await onAddNewBuild({ name: buildName.trim() || "New Build", description: description || "Work in progress" }); id = typeof result === 'number' ? result : (result as any)?.buildId; if (!id || !Number.isInteger(id) || id <= 0) { setSnackbar({ open: true, message: 'Failed to create draft build. Please try again.', severity: 'error' }); return; } setBuildId(id); } const full = await onGetComponentDetails({componentId: component.id}).catch(() => null); const merged = full ? {...component, ...full, details: full.details, quantity: 1} : { ...component, quantity: 1 }; setSlots(prev => prev.map(slot => slot.id === activeSlotId ? {...slot, component: merged} : slot )); setBrowserOpen(false); await onAddComponentToBuild({buildId: id, componentId: component.id}); } catch (e) { setSnackbar({ open: true, message: 'Failed to add component to build. Please try again.', severity: 'error' }); } finally { setActiveSlotId(null); } }; const handleRemovePart = async (slotId: string) => { const slot = slots.find(s => s.id === slotId); if (!slot?.component || !buildId) return; const quantity = slot.component.quantity || 1; setSlots(prev => prev.map(s => s.id === slotId ? {...s, component: null} : s )); try { for (let i = 0; i < quantity; i++) { await onRemoveComponentFromBuild({ buildId, componentId: slot.component.id }); } } catch (e) { console.error("Failed to remove component from server", e); } }; const handleIncrementComponent = async (slotId: string) => { const slot = slots.find(s => s.id === slotId); if (!slot?.component || !buildId) return; if (slot.type === 'memory') { const maxSlots = getMaxRamSlots(slots); const currentUsed = calculateUsedRamSlots(slots); const modules = Number(slot.component.details?.modules || slot.component.modules || 1); if (maxSlots && (currentUsed + modules > maxSlots)) { setSnackbar({ open: true, message: `Cannot add more RAM. Motherboard has only ${maxSlots} slots and ${currentUsed} are currently used.`, severity: 'error' }); return; } } if (slot.type === 'storage') { const formFactor = slot.component.details?.formFactor || slot.component.formFactor; const maxSlots = 4; const currentUsed = calculateUsedStorageSlots(slots, formFactor); if (maxSlots && (currentUsed + 1 > maxSlots)) { setSnackbar({ open: true, message: `Cannot add more ${formFactor} storage. Motherboard has only ${maxSlots} slots and ${currentUsed} are currently used.`, severity: 'error' }); return; } } try { await onAddComponentToBuild({buildId, componentId: slot.component.id}); setSlots(prev => prev.map(s => s.id === slotId && s.component ? {...s, component: {...s.component, quantity: (s.component.quantity || 1) + 1}} : s )); } catch (e) { setSnackbar({ open: true, message: 'Failed to increment component. Please try again.', severity: 'error' }); } }; const handleDecrementComponent = async (slotId: string) => { const slot = slots.find(s => s.id === slotId); if (!slot?.component || !buildId) return; const currentQuantity = slot.component.quantity || 1; if (currentQuantity <= 1) return; try { await onRemoveComponentFromBuild({buildId, componentId: slot.component.id}); setSlots(prev => prev.map(s => s.id === slotId && s.component ? {...s, component: {...s.component, quantity: (s.component.quantity || 1) - 1}} : s )); } catch (e) { setSnackbar({ open: true, message: 'Failed to decrement component. Please try again.', severity: 'error' }); } }; const handleAddSlot = (type: string, label: string) => { const count = slots.filter(s => s.type === type).length; const newSlot: BuildSlot = { id: `${type}_${count + 1}`, type, label: `${label} ${count > 0 ? count + 1 : ''}`, component: null, required: false }; setSlots(prev => [...prev, newSlot]); setAnchorEl(null); }; const handleDeleteSlot = (slotId: string) => { const slot = slots.find(s => s.id === slotId); if (slot?.component) { handleRemovePart(slotId); } setSlots(prev => prev.filter(s => s.id !== slotId)); }; const handleSubmit = () => { if (!buildName.trim()) { setSnackbar({ open: true, message: 'Please give your build a name!', severity: 'warning' }); return; } if (!buildId) { setSnackbar({ open: true, message: 'Please add at least one component to your build before submitting!', severity: 'warning' }); return; } setTempDescription(description || ""); setSubmitDialogOpen(true); }; const handleSubmitConfirm = async () => { if (!buildId) return; setIsSubmitting(true); setSubmitDialogOpen(false); try { await saveBuildState({buildId, name: buildName.trim(), description: tempDescription}); const result = await onEditBuild({buildId}); if (!result) throw new Error("Failed to save build"); isSubmittedRef.current = true; window.location.href = "/dashboard/user"; } catch (e) { console.error(e); setSnackbar({ open: true, message: 'Failed to save build. Please try again.', severity: 'error' }); } finally { setIsSubmitting(false); } }; const activeSlotType = useMemo(() => { if (!activeSlotId) return null; return slots.find(s => s.id === activeSlotId)?.type || null; }, [slots, activeSlotId]); return ( Forge Your Machine setBuildName(e.target.value)} sx={{bgcolor: '#1e1e1e', borderRadius: 1, color: 'white'}} /> Component Selection & Specs Price Actions {slots.map((slot) => ( {slot.label} {slot.required && } {slot.component ? ( {slot.component.brand?.[0]} setDetailsOpen(slot.component)} > {slot.component.name} {renderSpecs(slot.component, slot.type)} ) : ( )} {slot.component ? ( <> ${(Number(slot.component.price) * (slot.component.quantity || 1)).toFixed(2)} {(slot.component.quantity || 1) > 1 && ( ${Number(slot.component.price).toFixed(2)} each )} ) : '-'} {slot.component && ( {(slot.type === 'memory' || slot.type === 'storage') && ( <> handleDecrementComponent(slot.id)} disabled={(slot.component.quantity || 1) <= 1} sx={{ bgcolor: 'action.hover', '&:disabled': {bgcolor: 'action.disabledBackground'} }} > {slot.component.quantity || 1} handleIncrementComponent(slot.id)} sx={{bgcolor: 'action.hover'}} > )} handleRemovePart(slot.id)} > {!slot.required && ( handleDeleteSlot(slot.id)} > )} )} ))}
setAnchorEl(null)}> {/*Removed RAM & Storage from optional components*/} Accessories handleAddSlot('optical_drive', 'Optical Drive')}> Optical Drive handleAddSlot('cables', 'Cable')}> Cables Expansion Cards handleAddSlot('memory_card', 'Storage Card')}> Storage Card handleAddSlot('sound_card', 'Sound Card')}> Sound Card handleAddSlot('network_card', 'Network Card')}> Network Card handleAddSlot('network_adapter', 'WiFi Adapter')}> WiFi Adapter Total: ${totalPrice.toFixed(2)} { setBrowserOpen(false); setActiveSlotId(null); }} mode="forge" onSelect={handleSelectComponent} currentBuildId={buildId} /> setDetailsOpen(null)} /> setSubmitDialogOpen(false)} maxWidth="sm" fullWidth> Build Description Add some notes about your build (optional): setTempDescription(e.target.value)} variant="outlined" /> setSnackbar(prev => ({...prev, open: false}))} anchorOrigin={{vertical: 'bottom', horizontal: 'center'}} > setSnackbar(prev => ({...prev, open: false}))} severity={snackbar.severity} variant="filled" sx={{width: '100%'}} > {snackbar.message}
); }