| [a932c9e] | 1 | import React, { useEffect, useState } from 'react';
|
|---|
| 2 | import {
|
|---|
| 3 | Dialog, DialogContent, IconButton, Grid, Box, Typography,
|
|---|
| 4 | Slider, FormControl, InputLabel, Select, MenuItem, Checkbox, ListItemText,
|
|---|
| 5 | Card, CardContent, CardMedia, Button, CircularProgress, Divider, AppBar, Toolbar
|
|---|
| 6 | } from '@mui/material';
|
|---|
| 7 | import 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';
|
|---|
| 12 | import ComponentDetailsDialog from "./ComponentDetailsDialog";
|
|---|
| 13 |
|
|---|
| 14 | const formatMoney = (amount: number) => `$${amount}`;
|
|---|
| 15 |
|
|---|
| 16 | export default function ComponentBrowserDialog({ open, category, onClose }: any) {
|
|---|
| 17 | const [components, setComponents] = useState<any[]>([]);
|
|---|
| 18 | const [loading, setLoading] = useState(false);
|
|---|
| 19 |
|
|---|
| 20 | const [selectedComponent, setSelectedComponent] = useState<any>(null);
|
|---|
| 21 | const [priceRange, setPriceRange] = useState<number[]>([0, 2000]);
|
|---|
| 22 | const [selectedBrands, setSelectedBrands] = useState<string[]>([]);
|
|---|
| 23 | const [availableBrands, setAvailableBrands] = useState<string[]>([]);
|
|---|
| 24 |
|
|---|
| 25 | const [sortOrder, setSortOrder] = useState<string>('default');
|
|---|
| 26 |
|
|---|
| 27 | useEffect(() => {
|
|---|
| 28 | if (open && category) {
|
|---|
| 29 | setLoading(true);
|
|---|
| 30 | setSortOrder('price_desc');
|
|---|
| 31 |
|
|---|
| 32 | onGetAllComponents({ componentType: category })
|
|---|
| 33 | .then((data) => {
|
|---|
| 34 | setComponents(data);
|
|---|
| 35 |
|
|---|
| 36 | const brands = Array.from(new Set(data.map((c: any) => c.brand)));
|
|---|
| 37 | setAvailableBrands(brands as string[]);
|
|---|
| 38 |
|
|---|
| 39 | const maxPrice = data.length > 0 ? Math.max(...data.map((c: any) => Number(c.price))) : 2000;
|
|---|
| 40 | setPriceRange([0, Math.ceil(maxPrice)]);
|
|---|
| 41 | })
|
|---|
| 42 | .catch(console.error)
|
|---|
| 43 | .finally(() => setLoading(false));
|
|---|
| 44 | }
|
|---|
| 45 | }, [open, category]);
|
|---|
| 46 |
|
|---|
| 47 | let processedComponents = components.filter(comp => {
|
|---|
| 48 | const matchesBrand = selectedBrands.length === 0 || selectedBrands.includes(comp.brand);
|
|---|
| 49 | const matchesPrice = Number(comp.price) >= priceRange[0] && Number(comp.price) <= priceRange[1];
|
|---|
| 50 | return matchesBrand && matchesPrice;
|
|---|
| 51 | });
|
|---|
| 52 |
|
|---|
| 53 | if (sortOrder === 'price_asc') {
|
|---|
| 54 | processedComponents.sort((a, b) => Number(a.price) - Number(b.price));
|
|---|
| 55 | } else if (sortOrder === 'price_desc') {
|
|---|
| 56 | processedComponents.sort((a, b) => Number(b.price) - Number(a.price));
|
|---|
| 57 | }
|
|---|
| 58 |
|
|---|
| 59 | const handleBrandChange = (event: any) => {
|
|---|
| 60 | const value = event.target.value;
|
|---|
| 61 | setSelectedBrands(typeof value === 'string' ? value.split(',') : value);
|
|---|
| 62 | };
|
|---|
| 63 |
|
|---|
| 64 | if (!open) return null;
|
|---|
| 65 |
|
|---|
| 66 | return (
|
|---|
| 67 | <Dialog
|
|---|
| 68 | open={open}
|
|---|
| 69 | onClose={onClose}
|
|---|
| 70 | maxWidth="xl"
|
|---|
| 71 | fullWidth
|
|---|
| 72 | PaperProps={{ 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={{ width: 300, borderRight: '1px solid #ddd', p: 3, bgcolor: '#1e1e1e', display: { xs: 'none', md: 'block' } }}>
|
|---|
| 87 | <Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
|
|---|
| 88 | <SortIcon sx={{ mr: 1 }} />
|
|---|
| 89 | <Typography variant="h6">Sort By</Typography>
|
|---|
| 90 | </Box>
|
|---|
| 91 | <FormControl fullWidth size="small" sx={{ mb: 4 }}>
|
|---|
| 92 | <InputLabel>Price</InputLabel>
|
|---|
| 93 | <Select
|
|---|
| 94 | value={sortOrder}
|
|---|
| 95 | label="Price"
|
|---|
| 96 | onChange={(e) => setSortOrder(e.target.value)}>
|
|---|
| 97 | {/*<MenuItem value="default">Featured</MenuItem>*/}
|
|---|
| 98 | <MenuItem value="price_asc">Price: Low to High</MenuItem>
|
|---|
| 99 | <MenuItem value="price_desc">Price: High to Low</MenuItem>
|
|---|
| 100 | </Select>
|
|---|
| 101 | </FormControl>
|
|---|
| 102 |
|
|---|
| 103 | <Divider sx={{ mb: 3 }} />
|
|---|
| 104 |
|
|---|
| 105 | <Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
|
|---|
| 106 | <FilterListIcon sx={{ mr: 1 }} />
|
|---|
| 107 | <Typography variant="h6">Filters</Typography>
|
|---|
| 108 | </Box>
|
|---|
| 109 |
|
|---|
| 110 | <Typography gutterBottom fontWeight="bold">Price Range</Typography>
|
|---|
| 111 | <Slider
|
|---|
| 112 | value={priceRange}
|
|---|
| 113 | onChange={(_, newValue) => setPriceRange(newValue as number[])}
|
|---|
| 114 | valueLabelDisplay="auto"
|
|---|
| 115 | min={0}
|
|---|
| 116 | max={components.length > 0 ? Math.max(...components.map(c => Number(c.price))) : 2000}
|
|---|
| 117 | sx={{ color: '#ff8201', mb: 2}}
|
|---|
| 118 | />
|
|---|
| 119 | <Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 2 }}>
|
|---|
| 120 | <Typography variant="caption">{formatMoney(priceRange[0])}</Typography>
|
|---|
| 121 | <Typography variant="caption">{formatMoney(priceRange[1])}</Typography>
|
|---|
| 122 | </Box>
|
|---|
| 123 |
|
|---|
| 124 | <FormControl fullWidth size="small">
|
|---|
| 125 | <InputLabel>Brands</InputLabel>
|
|---|
| 126 | <Select
|
|---|
| 127 | multiple
|
|---|
| 128 | value={selectedBrands}
|
|---|
| 129 | onChange={handleBrandChange}
|
|---|
| 130 | renderValue={(selected) => selected.join(', ')}
|
|---|
| 131 | label="Brands"
|
|---|
| 132 | >
|
|---|
| 133 | {availableBrands.map((brand) => (
|
|---|
| 134 | <MenuItem key={brand} value={brand}>
|
|---|
| 135 | <Checkbox checked={selectedBrands.indexOf(brand) > -1} />
|
|---|
| 136 | <ListItemText primary={brand} />
|
|---|
| 137 | </MenuItem>
|
|---|
| 138 | ))}
|
|---|
| 139 | </Select>
|
|---|
| 140 | </FormControl>
|
|---|
| 141 | </Box>
|
|---|
| 142 |
|
|---|
| 143 | <Box sx={{ flex: 1, p: 3, overflowY: 'auto' }}>
|
|---|
| 144 | {loading ? (
|
|---|
| 145 | <Box sx={{ display: 'flex', justifyContent: 'center', mt: 10 }}>
|
|---|
| 146 | <CircularProgress sx={{ color: '#ff8201' }} />
|
|---|
| 147 | </Box>
|
|---|
| 148 | ) : (
|
|---|
| 149 | <Grid container spacing={3}>
|
|---|
| 150 | {processedComponents.map((comp) => (
|
|---|
| 151 | <Grid item xs={12} sm={6} md={4} lg={3} key={comp.id}>
|
|---|
| 152 | <Card elevation={3} sx={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
|
|---|
| 153 | <CardMedia
|
|---|
| 154 | component="img"
|
|---|
| 155 | height="140"
|
|---|
| 156 |
|
|---|
| 157 | image={`https://placehold.co/400x400?text=${encodeURIComponent(comp.name)}`}
|
|---|
| 158 | alt={comp.name}
|
|---|
| 159 | sx={{ p: 2, objectFit: 'contain' }}
|
|---|
| 160 | />
|
|---|
| 161 | <CardContent sx={{ flexGrow: 1 }}>
|
|---|
| 162 | <Typography variant="caption" color="text.secondary" sx={{ textTransform: 'uppercase' }}>
|
|---|
| 163 | {comp.brand}
|
|---|
| 164 | </Typography>
|
|---|
| 165 | <Typography variant="subtitle1" fontWeight="bold" sx={{ lineHeight: 1.2, mb: 1 }}>
|
|---|
| 166 | {comp.name}
|
|---|
| 167 | </Typography>
|
|---|
| 168 |
|
|---|
| 169 | <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mt: 2 }}>
|
|---|
| 170 | <Typography variant="h6" color="primary.main">
|
|---|
| 171 | {formatMoney(comp.price)}
|
|---|
| 172 | </Typography>
|
|---|
| 173 | </Box>
|
|---|
| 174 | </CardContent>
|
|---|
| 175 | <Box sx={{ p: 2, pt: 0}}>
|
|---|
| 176 | <Button
|
|---|
| 177 | fullWidth
|
|---|
| 178 | variant="outlined"
|
|---|
| 179 | onClick={()=> setSelectedComponent(comp)}
|
|---|
| 180 | sx={{backgroundColor:'#ff8201', color: 'white', borderColor: '#ff8201', onHover: { backgroundColor: '#e67300', borderColor: '#e67300' } }}
|
|---|
| 181 | >
|
|---|
| 182 | Details
|
|---|
| 183 | </Button>
|
|---|
| 184 | </Box>
|
|---|
| 185 | </Card>
|
|---|
| 186 | </Grid>
|
|---|
| 187 | ))}
|
|---|
| 188 | {processedComponents.length === 0 && (
|
|---|
| 189 | <Box sx={{ width: '100%', textAlign: 'center', mt: 5 }}>
|
|---|
| 190 | <Typography variant="h6" color="text.secondary">No components match.</Typography>
|
|---|
| 191 | </Box>
|
|---|
| 192 | )}
|
|---|
| 193 | </Grid>
|
|---|
| 194 | )}
|
|---|
| 195 | </Box>
|
|---|
| 196 | </DialogContent>
|
|---|
| 197 | <ComponentDetailsDialog
|
|---|
| 198 | open={!!selectedComponent}
|
|---|
| 199 | component={selectedComponent}
|
|---|
| 200 | onClose={() => setSelectedComponent(null)}
|
|---|
| 201 | />
|
|---|
| 202 | </Dialog>
|
|---|
| 203 | );
|
|---|
| 204 | }
|
|---|