| 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 | 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>
|
|---|
| 96 | </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,
|
|---|
| 184 | }}>
|
|---|
| 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">
|
|---|
| 195 | {formatMoney(comp.price)}
|
|---|
| 196 | </Typography>
|
|---|
| 197 | </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>
|
|---|
| 232 | );
|
|---|
| 233 | }
|
|---|