| 1 | import React, {useEffect, useState} from 'react';
|
|---|
| 2 | import {
|
|---|
| 3 | Dialog, DialogContent, IconButton, Box, Typography, Chip,
|
|---|
| 4 | Slider, FormControl, InputLabel, Select, MenuItem, Checkbox, ListItemText,
|
|---|
| 5 | Card, CardContent, CardMedia, Button, CircularProgress, AppBar, Toolbar, InputAdornment, TextField,
|
|---|
| 6 | TextField as MuiTextField, DialogTitle, DialogActions, Snackbar, Alert
|
|---|
| 7 | } from '@mui/material';
|
|---|
| 8 | import CloseIcon from '@mui/icons-material/Close';
|
|---|
| 9 | import AddCircleIcon from '@mui/icons-material/AddCircle';
|
|---|
| 10 |
|
|---|
| 11 | import {onGetAllComponents, onGetAuthState, onSuggestComponent} from '../pages/+Layout.telefunc'
|
|---|
| 12 | import {onGetCompatibleComponents} from '../pages/forge/forge.telefunc';
|
|---|
| 13 | import ComponentDetailsDialog from "./ComponentDetailsDialog";
|
|---|
| 14 | import SearchIcon from "@mui/icons-material/Search";
|
|---|
| 15 |
|
|---|
| 16 | const formatMoney = (amount: number) => `$${Number(amount).toFixed(2)}`;
|
|---|
| 17 |
|
|---|
| 18 | export default function ComponentDialog({open, category, onClose, mode, onSelect, currentBuildId}: any) {
|
|---|
| 19 | const [components, setComponents] = useState<any[]>([]);
|
|---|
| 20 | const [loading, setLoading] = useState(false);
|
|---|
| 21 | const [selectedComponent, setSelectedComponent] = useState<any>(null);
|
|---|
| 22 | const [priceRange, setPriceRange] = useState<number[]>([0, 2000]);
|
|---|
| 23 | const [selectedBrands, setSelectedBrands] = useState<string[]>([]);
|
|---|
| 24 | const [availableBrands, setAvailableBrands] = useState<string[]>([]);
|
|---|
| 25 | 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]);
|
|---|
| 56 |
|
|---|
| 57 | useEffect(() => {
|
|---|
| 58 | if (open && category) {
|
|---|
| 59 | setLoading(true);
|
|---|
| 60 | setSortOrder('price_desc');
|
|---|
| 61 |
|
|---|
| 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
|
|---|
| 77 | .then((data) => {
|
|---|
| 78 | const comps = data || [];
|
|---|
| 79 | setComponents(comps);
|
|---|
| 80 |
|
|---|
| 81 | const brands = Array.from(new Set(comps.map((c: any) => c.brand)));
|
|---|
| 82 | setAvailableBrands(brands as string[]);
|
|---|
| 83 | const maxPrice = comps.length > 0 ? Math.max(...comps.map((c: any) => Number(c.price))) : 2000;
|
|---|
| 84 | setPriceRange([0, Math.ceil(maxPrice)]);
|
|---|
| 85 | })
|
|---|
| 86 | .catch((err) => {
|
|---|
| 87 | console.error('[Dialog] fetch error:', err);
|
|---|
| 88 | setComponents([]);
|
|---|
| 89 | })
|
|---|
| 90 | .finally(() => setLoading(false));
|
|---|
| 91 | }
|
|---|
| 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 | };
|
|---|
| 134 |
|
|---|
| 135 | let processedComponents = components.filter(comp => {
|
|---|
| 136 | const matchesBrand = selectedBrands.length === 0 || selectedBrands.includes(comp.brand);
|
|---|
| 137 | const matchesPrice = Number(comp.price) >= priceRange[0] && Number(comp.price) <= priceRange[1];
|
|---|
| 138 | const matchesSearch = searchQuery === "" ||
|
|---|
| 139 | comp.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
|---|
| 140 | comp.brand.toLowerCase().includes(searchQuery.toLowerCase());
|
|---|
| 141 |
|
|---|
| 142 | return matchesBrand && matchesPrice && matchesSearch;
|
|---|
| 143 | });
|
|---|
| 144 |
|
|---|
| 145 | if (sortOrder === 'price_asc') {
|
|---|
| 146 | processedComponents.sort((a, b) => Number(a.price) - Number(b.price));
|
|---|
| 147 | } else if (sortOrder === 'price_desc') {
|
|---|
| 148 | processedComponents.sort((a, b) => Number(b.price) - Number(a.price));
|
|---|
| 149 | }
|
|---|
| 150 |
|
|---|
| 151 | const handleBrandChange = (event: any) => {
|
|---|
| 152 | const value = event.target.value;
|
|---|
| 153 | setSelectedBrands(typeof value === 'string' ? value.split(',') : value);
|
|---|
| 154 | };
|
|---|
| 155 |
|
|---|
| 156 | if (!open) return null;
|
|---|
| 157 |
|
|---|
| 158 | return (
|
|---|
| 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>
|
|---|
| 244 | </Box>
|
|---|
| 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'
|
|---|
| 302 | }}>
|
|---|
| 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">
|
|---|
| 323 | {formatMoney(comp.price)}
|
|---|
| 324 | </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 | )}
|
|---|
| 354 | </Box>
|
|---|
| 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 | </>
|
|---|
| 435 | );
|
|---|
| 436 | }
|
|---|