| 1 | import React, {useEffect, useState} from 'react';
|
|---|
| 2 | import {
|
|---|
| 3 | Dialog, DialogContent, IconButton, Box, Typography,
|
|---|
| 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, Drawer, Badge
|
|---|
| 7 | } from '@mui/material';
|
|---|
| 8 | import CloseIcon from '@mui/icons-material/Close';
|
|---|
| 9 | import AddCircleIcon from '@mui/icons-material/AddCircle';
|
|---|
| 10 | import FilterListIcon from '@mui/icons-material/FilterList';
|
|---|
| 11 | import SearchIcon from "@mui/icons-material/Search";
|
|---|
| 12 |
|
|---|
| 13 | import {onGetAllComponents, onGetAuthState, onSuggestComponent} from '../pages/+Layout.telefunc'
|
|---|
| 14 | import {onGetCompatibleComponents} from '../pages/forge/forge.telefunc';
|
|---|
| 15 | import ComponentDetailsDialog from "./ComponentDetailsDialog";
|
|---|
| 16 |
|
|---|
| 17 | const formatMoney = (amount: number) => `$${Number(amount).toFixed(2)}`;
|
|---|
| 18 |
|
|---|
| 19 | export default function ComponentDialog({open, category, onClose, mode, onSelect, currentBuildId}: any) {
|
|---|
| 20 | const [components, setComponents] = useState<any[]>([]);
|
|---|
| 21 | const [loading, setLoading] = useState(false);
|
|---|
| 22 | const [selectedComponent, setSelectedComponent] = useState<any>(null);
|
|---|
| 23 | const [priceRange, setPriceRange] = useState<number[]>([0, 2000]);
|
|---|
| 24 | const [selectedBrands, setSelectedBrands] = useState<string[]>([]);
|
|---|
| 25 | const [availableBrands, setAvailableBrands] = useState<string[]>([]);
|
|---|
| 26 | const [sortOrder, setSortOrder] = useState<string>('default');
|
|---|
| 27 |
|
|---|
| 28 | const [tempSearchQuery, setTempSearchQuery] = useState("");
|
|---|
| 29 | const [searchQuery, setSearchQuery] = useState("");
|
|---|
| 30 |
|
|---|
| 31 | const [suggestOpen, setSuggestOpen] = useState(false);
|
|---|
| 32 | const [suggestForm, setSuggestForm] = useState({
|
|---|
| 33 | link: '',
|
|---|
| 34 | description: '',
|
|---|
| 35 | componentType: category || ''
|
|---|
| 36 | });
|
|---|
| 37 | const [suggestLoading, setSuggestLoading] = useState(false);
|
|---|
| 38 | const [snackbarOpen, setSnackbarOpen] = useState(false);
|
|---|
| 39 | const [snackbarMessage, setSnackbarMessage] = useState("");
|
|---|
| 40 | const [userId, setUserId] = useState<number | null>(null);
|
|---|
| 41 | const [mobileFiltersOpen, setMobileFiltersOpen] = useState(false);
|
|---|
| 42 |
|
|---|
| 43 | useEffect(() => {
|
|---|
| 44 | if (open && category) {
|
|---|
| 45 | setSuggestForm({
|
|---|
| 46 | link: '',
|
|---|
| 47 | description: '',
|
|---|
| 48 | componentType: category
|
|---|
| 49 | });
|
|---|
| 50 | }
|
|---|
| 51 | }, [open, category]);
|
|---|
| 52 |
|
|---|
| 53 | useEffect(() => {
|
|---|
| 54 | const timeoutId = setTimeout(() => {
|
|---|
| 55 | setSearchQuery(tempSearchQuery);
|
|---|
| 56 | }, 300);
|
|---|
| 57 | return () => clearTimeout(timeoutId);
|
|---|
| 58 | }, [tempSearchQuery]);
|
|---|
| 59 |
|
|---|
| 60 | useEffect(() => {
|
|---|
| 61 | if (open) {
|
|---|
| 62 | onGetAuthState().then(userData => {
|
|---|
| 63 | setUserId(userData.userId);
|
|---|
| 64 | });
|
|---|
| 65 | }
|
|---|
| 66 | }, [open]);
|
|---|
| 67 |
|
|---|
| 68 | useEffect(() => {
|
|---|
| 69 | if (open && category) {
|
|---|
| 70 | setLoading(true);
|
|---|
| 71 | setSortOrder('price_desc');
|
|---|
| 72 |
|
|---|
| 73 | const shouldUseCompatibility = mode === 'forge' && currentBuildId;
|
|---|
| 74 | const fetcher = shouldUseCompatibility
|
|---|
| 75 | ? onGetCompatibleComponents({
|
|---|
| 76 | buildId: currentBuildId,
|
|---|
| 77 | componentType: category,
|
|---|
| 78 | limit: 100,
|
|---|
| 79 | sort: 'price_desc'
|
|---|
| 80 | })
|
|---|
| 81 | : onGetAllComponents({
|
|---|
| 82 | componentType: category,
|
|---|
| 83 | limit: 100,
|
|---|
| 84 | sort: 'price_desc'
|
|---|
| 85 | });
|
|---|
| 86 |
|
|---|
| 87 | fetcher
|
|---|
| 88 | .then((data) => {
|
|---|
| 89 | const comps = data || [];
|
|---|
| 90 | setComponents(comps);
|
|---|
| 91 |
|
|---|
| 92 | const brands = Array.from(new Set(comps.map((c: any) => c.brand)));
|
|---|
| 93 | setAvailableBrands(brands as string[]);
|
|---|
| 94 | const maxPrice = comps.length > 0 ? Math.max(...comps.map((c: any) => Number(c.price))) : 2000;
|
|---|
| 95 | setPriceRange([0, Math.ceil(maxPrice)]);
|
|---|
| 96 | })
|
|---|
| 97 | .catch((err) => {
|
|---|
| 98 | console.error('[Dialog] fetch error:', err);
|
|---|
| 99 | setComponents([]);
|
|---|
| 100 | })
|
|---|
| 101 | .finally(() => setLoading(false));
|
|---|
| 102 | }
|
|---|
| 103 | }, [open, category, mode, currentBuildId]);
|
|---|
| 104 |
|
|---|
| 105 | const handleSuggestChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|---|
| 106 | setSuggestForm({
|
|---|
| 107 | ...suggestForm,
|
|---|
| 108 | [e.target.name]: e.target.value
|
|---|
| 109 | });
|
|---|
| 110 | }
|
|---|
| 111 |
|
|---|
| 112 | const submitSuggestion = async () => {
|
|---|
| 113 | if (!userId) {
|
|---|
| 114 | setSnackbarMessage("Please login to submit suggestions!");
|
|---|
| 115 | setSnackbarOpen(true);
|
|---|
| 116 | return;
|
|---|
| 117 | }
|
|---|
| 118 |
|
|---|
| 119 | if (!suggestForm.link || !suggestForm.description) {
|
|---|
| 120 | setSnackbarMessage("Please fill in all fields!");
|
|---|
| 121 | setSnackbarOpen(true);
|
|---|
| 122 | return;
|
|---|
| 123 | }
|
|---|
| 124 |
|
|---|
| 125 | setSuggestLoading(true);
|
|---|
| 126 | try {
|
|---|
| 127 | const suggestionId = await onSuggestComponent({
|
|---|
| 128 | link: suggestForm.link,
|
|---|
| 129 | description: suggestForm.description,
|
|---|
| 130 | componentType: suggestForm.componentType
|
|---|
| 131 | });
|
|---|
| 132 |
|
|---|
| 133 | setSnackbarMessage("Suggestion submitted! Admin will review it.");
|
|---|
| 134 | setSnackbarOpen(true);
|
|---|
| 135 | setSuggestOpen(false);
|
|---|
| 136 | setSuggestForm({link: '', description: '', componentType: category || ''});
|
|---|
| 137 | } catch (error) {
|
|---|
| 138 | console.error('Suggestion error:', error);
|
|---|
| 139 | setSnackbarMessage("Failed to submit suggestion. Try again.");
|
|---|
| 140 | setSnackbarOpen(true);
|
|---|
| 141 | } finally {
|
|---|
| 142 | setSuggestLoading(false);
|
|---|
| 143 | }
|
|---|
| 144 | };
|
|---|
| 145 |
|
|---|
| 146 | let processedComponents = components.filter(comp => {
|
|---|
| 147 | const matchesBrand = selectedBrands.length === 0 || selectedBrands.includes(comp.brand);
|
|---|
| 148 | const matchesPrice = Number(comp.price) >= priceRange[0] && Number(comp.price) <= priceRange[1];
|
|---|
| 149 | const matchesSearch = searchQuery === "" ||
|
|---|
| 150 | comp.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
|---|
| 151 | comp.brand.toLowerCase().includes(searchQuery.toLowerCase());
|
|---|
| 152 |
|
|---|
| 153 | return matchesBrand && matchesPrice && matchesSearch;
|
|---|
| 154 | });
|
|---|
| 155 |
|
|---|
| 156 | if (sortOrder === 'price_asc') {
|
|---|
| 157 | processedComponents.sort((a, b) => Number(a.price) - Number(b.price));
|
|---|
| 158 | } else if (sortOrder === 'price_desc') {
|
|---|
| 159 | processedComponents.sort((a, b) => Number(b.price) - Number(a.price));
|
|---|
| 160 | }
|
|---|
| 161 |
|
|---|
| 162 | const handleBrandChange = (event: any) => {
|
|---|
| 163 | const value = event.target.value;
|
|---|
| 164 | setSelectedBrands(typeof value === 'string' ? value.split(',') : value);
|
|---|
| 165 | };
|
|---|
| 166 |
|
|---|
| 167 | const activeFiltersCount = selectedBrands.length + (sortOrder !== 'default' ? 1 : 0);
|
|---|
| 168 |
|
|---|
| 169 | const FilterContent = React.useMemo(() => (
|
|---|
| 170 | <Box sx={{width: {xs: 280, md: 300}, p: 3, bgcolor: 'transparent'}}>
|
|---|
| 171 | <TextField
|
|---|
| 172 | fullWidth
|
|---|
| 173 | size="small"
|
|---|
| 174 | placeholder="Search components..."
|
|---|
| 175 | value={tempSearchQuery}
|
|---|
| 176 | onChange={(e) => setTempSearchQuery(e.target.value)}
|
|---|
| 177 | sx={{mb: 2}}
|
|---|
| 178 | InputProps={{
|
|---|
| 179 | startAdornment: <InputAdornment position="start"><SearchIcon/></InputAdornment>,
|
|---|
| 180 | }}
|
|---|
| 181 | />
|
|---|
| 182 |
|
|---|
| 183 | <FormControl fullWidth size="small" sx={{mb: 2}}>
|
|---|
| 184 | <InputLabel>Sort By</InputLabel>
|
|---|
| 185 | <Select
|
|---|
| 186 | value={sortOrder}
|
|---|
| 187 | label="Sort By"
|
|---|
| 188 | onChange={(e) => setSortOrder(e.target.value)}
|
|---|
| 189 | MenuProps={{
|
|---|
| 190 | container: typeof window !== 'undefined' ? document.body : undefined,
|
|---|
| 191 | disablePortal: false,
|
|---|
| 192 | sx: {
|
|---|
| 193 | zIndex: 1500
|
|---|
| 194 | }
|
|---|
| 195 | }}
|
|---|
| 196 | >
|
|---|
| 197 | <MenuItem value="price_asc">Price: Low to High</MenuItem>
|
|---|
| 198 | <MenuItem value="price_desc">Price: High to Low</MenuItem>
|
|---|
| 199 | </Select>
|
|---|
| 200 | </FormControl>
|
|---|
| 201 |
|
|---|
| 202 | <FormControl fullWidth size="small" sx={{mb: 2}}>
|
|---|
| 203 | <InputLabel>Brands</InputLabel>
|
|---|
| 204 | <Select
|
|---|
| 205 | multiple
|
|---|
| 206 | value={selectedBrands}
|
|---|
| 207 | onChange={handleBrandChange}
|
|---|
| 208 | label="Brands"
|
|---|
| 209 | renderValue={(s) => s.join(', ')}
|
|---|
| 210 | MenuProps={{
|
|---|
| 211 | container: typeof window !== 'undefined' ? document.body : undefined,
|
|---|
| 212 | disablePortal: false,
|
|---|
| 213 | sx: {
|
|---|
| 214 | zIndex: 1500
|
|---|
| 215 | },
|
|---|
| 216 | PaperProps: {
|
|---|
| 217 | sx: {
|
|---|
| 218 | maxHeight: 300
|
|---|
| 219 | }
|
|---|
| 220 | }
|
|---|
| 221 | }}
|
|---|
| 222 | >
|
|---|
| 223 | {availableBrands.map((brand) => (
|
|---|
| 224 | <MenuItem key={brand} value={brand}>
|
|---|
| 225 | <Checkbox checked={selectedBrands.indexOf(brand) > -1}/>
|
|---|
| 226 | <ListItemText primary={brand}/>
|
|---|
| 227 | </MenuItem>
|
|---|
| 228 | ))}
|
|---|
| 229 | </Select>
|
|---|
| 230 | </FormControl>
|
|---|
| 231 |
|
|---|
| 232 | <Typography gutterBottom fontWeight="bold">Price Range</Typography>
|
|---|
| 233 | <Slider value={priceRange} onChange={(_, v) => setPriceRange(v as number[])} min={0} max={2000}
|
|---|
| 234 | sx={{color: '#ff8201'}}/>
|
|---|
| 235 | <Box sx={{display: 'flex', justifyContent: 'space-between', mt: 1}}>
|
|---|
| 236 | <Typography variant="caption">${priceRange[0]}</Typography>
|
|---|
| 237 | <Typography variant="caption">${priceRange[1]}+</Typography>
|
|---|
| 238 | </Box>
|
|---|
| 239 |
|
|---|
| 240 | <Button
|
|---|
| 241 | fullWidth
|
|---|
| 242 | variant="contained"
|
|---|
| 243 | startIcon={<AddCircleIcon/>}
|
|---|
| 244 | onClick={() => setSuggestOpen(true)}
|
|---|
| 245 | sx={{
|
|---|
| 246 | mt: 2,
|
|---|
| 247 | bgcolor: '#ff8201',
|
|---|
| 248 | fontWeight: 'bold',
|
|---|
| 249 | fontSize: '0.875rem',
|
|---|
| 250 | '&:hover': {bgcolor: '#e67300'}
|
|---|
| 251 | }}
|
|---|
| 252 | >
|
|---|
| 253 | Suggest Component
|
|---|
| 254 | </Button>
|
|---|
| 255 | </Box>
|
|---|
| 256 | ), [tempSearchQuery, sortOrder, selectedBrands, availableBrands, priceRange]);
|
|---|
| 257 |
|
|---|
| 258 | if (!open) return null;
|
|---|
| 259 |
|
|---|
| 260 | return (
|
|---|
| 261 | <>
|
|---|
| 262 | <Dialog
|
|---|
| 263 | open={open}
|
|---|
| 264 | onClose={onClose}
|
|---|
| 265 | maxWidth="xl"
|
|---|
| 266 | fullWidth
|
|---|
| 267 | fullScreen
|
|---|
| 268 | sx={{
|
|---|
| 269 | '& .MuiDialog-paper': {
|
|---|
| 270 | height: {xs: '100%', md: '90vh'},
|
|---|
| 271 | m: {xs: 0, md: 2}
|
|---|
| 272 | }
|
|---|
| 273 | }}
|
|---|
| 274 | >
|
|---|
| 275 | <AppBar position="relative" sx={{bgcolor: '#ff8201'}}>
|
|---|
| 276 | <Toolbar>
|
|---|
| 277 | <IconButton
|
|---|
| 278 | edge="start"
|
|---|
| 279 | color="inherit"
|
|---|
| 280 | onClick={() => setMobileFiltersOpen(true)}
|
|---|
| 281 | sx={{
|
|---|
| 282 | display: {xs: 'flex', md: 'none'},
|
|---|
| 283 | mr: 2
|
|---|
| 284 | }}
|
|---|
| 285 | >
|
|---|
| 286 | <Badge badgeContent={activeFiltersCount} color="error">
|
|---|
| 287 | <FilterListIcon/>
|
|---|
| 288 | </Badge>
|
|---|
| 289 | </IconButton>
|
|---|
| 290 |
|
|---|
| 291 | <Typography
|
|---|
| 292 | sx={{ml: {xs: 0, md: 2}, flex: 1, fontSize: {xs: '1rem', sm: '1.25rem'}}}
|
|---|
| 293 | variant="h6"
|
|---|
| 294 | component="div"
|
|---|
| 295 | >
|
|---|
| 296 | <Box component="span" sx={{display: {xs: 'none', sm: 'inline'}}}>
|
|---|
| 297 | Browsing:
|
|---|
| 298 | </Box>
|
|---|
| 299 | <b> {category === 'gpu' ? 'Graphics Cards' : category?.toUpperCase()}</b>
|
|---|
| 300 | {mode === 'forge' && currentBuildId && (
|
|---|
| 301 | <Typography
|
|---|
| 302 | variant="caption"
|
|---|
| 303 | sx={{
|
|---|
| 304 | ml: {xs: 1, sm: 2},
|
|---|
| 305 | bgcolor: 'rgba(0,0,0,0.2)',
|
|---|
| 306 | px: 1,
|
|---|
| 307 | py: 0.5,
|
|---|
| 308 | borderRadius: 1,
|
|---|
| 309 | display: {xs: 'none', sm: 'inline-block'}
|
|---|
| 310 | }}
|
|---|
| 311 | >
|
|---|
| 312 | COMPATIBILITY MODE
|
|---|
| 313 | </Typography>
|
|---|
| 314 | )}
|
|---|
| 315 | </Typography>
|
|---|
| 316 | <IconButton edge="end" color="inherit" onClick={onClose}>
|
|---|
| 317 | <CloseIcon/>
|
|---|
| 318 | </IconButton>
|
|---|
| 319 | </Toolbar>
|
|---|
| 320 | </AppBar>
|
|---|
| 321 |
|
|---|
| 322 | <DialogContent sx={{p: 0, display: 'flex', height: '100%', overflow: 'hidden'}}>
|
|---|
| 323 | <Box sx={{
|
|---|
| 324 | display: {xs: 'none', md: 'block'},
|
|---|
| 325 | borderRight: '1px solid #ddd',
|
|---|
| 326 | overflowY: 'auto',
|
|---|
| 327 | bgcolor: '#1e1e1e'
|
|---|
| 328 | }}>
|
|---|
| 329 | {FilterContent}
|
|---|
| 330 | </Box>
|
|---|
| 331 | <Box sx={{flex: 1, p: {xs: 1.5, sm: 2, md: 3}, overflowY: 'auto', bgcolor: '#121212'}}>
|
|---|
| 332 | {loading ? (
|
|---|
| 333 | <Box sx={{display: 'flex', justifyContent: 'center', mt: 10}}>
|
|---|
| 334 | <CircularProgress sx={{color: '#ff8201'}}/>
|
|---|
| 335 | </Box>
|
|---|
| 336 | ) : (
|
|---|
| 337 | <Box sx={{
|
|---|
| 338 | display: 'grid',
|
|---|
| 339 | gridTemplateColumns: {
|
|---|
| 340 | xs: '1fr',
|
|---|
| 341 | sm: 'repeat(2, 1fr)',
|
|---|
| 342 | md: 'repeat(2, 1fr)',
|
|---|
| 343 | lg: 'repeat(3, 1fr)',
|
|---|
| 344 | xl: 'repeat(4, 1fr)'
|
|---|
| 345 | },
|
|---|
| 346 | gap: {xs: 1.5, sm: 2, md: 3},
|
|---|
| 347 | width: '100%'
|
|---|
| 348 | }}>
|
|---|
| 349 | {processedComponents.map((comp) => (
|
|---|
| 350 | <Box key={comp.id} sx={{width: '100%'}}>
|
|---|
| 351 | <Card
|
|---|
| 352 | elevation={3}
|
|---|
| 353 | sx={{
|
|---|
| 354 | width: '100%',
|
|---|
| 355 | height: '100%',
|
|---|
| 356 | display: 'flex',
|
|---|
| 357 | flexDirection: 'column',
|
|---|
| 358 | transition: 'transform 0.2s, box-shadow 0.2s',
|
|---|
| 359 | '&:hover': {
|
|---|
| 360 | transform: 'translateY(-4px)',
|
|---|
| 361 | boxShadow: 6
|
|---|
| 362 | }
|
|---|
| 363 | }}
|
|---|
| 364 | >
|
|---|
| 365 | <Box sx={{position: 'relative', paddingTop: '75%', bgcolor: '#1e1e1e'}}>
|
|---|
| 366 | <CardMedia
|
|---|
| 367 | component="img"
|
|---|
| 368 | image={comp.imgUrl || comp.img_url || `https://placehold.co/400x400?text=${comp.name}`}
|
|---|
| 369 | alt={comp.name}
|
|---|
| 370 | sx={{
|
|---|
| 371 | position: 'absolute',
|
|---|
| 372 | top: 0,
|
|---|
| 373 | left: 0,
|
|---|
| 374 | width: '100%',
|
|---|
| 375 | height: '100%',
|
|---|
| 376 | objectFit: 'contain',
|
|---|
| 377 | p: {xs: 1, sm: 2}
|
|---|
| 378 | }}
|
|---|
| 379 | />
|
|---|
| 380 | </Box>
|
|---|
| 381 |
|
|---|
| 382 | <CardContent sx={{
|
|---|
| 383 | flexGrow: 1,
|
|---|
| 384 | display: 'flex',
|
|---|
| 385 | flexDirection: 'column',
|
|---|
| 386 | justifyContent: 'space-between',
|
|---|
| 387 | p: {xs: 1.5, sm: 2}
|
|---|
| 388 | }}>
|
|---|
| 389 | <Box>
|
|---|
| 390 | <Typography
|
|---|
| 391 | variant="caption"
|
|---|
| 392 | color="text.secondary"
|
|---|
| 393 | sx={{
|
|---|
| 394 | textTransform: 'uppercase',
|
|---|
| 395 | letterSpacing: 0.5,
|
|---|
| 396 | fontSize: {xs: '0.65rem', sm: '0.75rem'}
|
|---|
| 397 | }}
|
|---|
| 398 | >
|
|---|
| 399 | {comp.brand}
|
|---|
| 400 | </Typography>
|
|---|
| 401 | <Typography
|
|---|
| 402 | variant="subtitle1"
|
|---|
| 403 | fontWeight="bold"
|
|---|
| 404 | sx={{
|
|---|
| 405 | lineHeight: 1.2,
|
|---|
| 406 | mt: 0.5,
|
|---|
| 407 | mb: 1,
|
|---|
| 408 | overflow: 'hidden',
|
|---|
| 409 | textOverflow: 'ellipsis',
|
|---|
| 410 | display: '-webkit-box',
|
|---|
| 411 | WebkitLineClamp: 2,
|
|---|
| 412 | WebkitBoxOrient: 'vertical',
|
|---|
| 413 | minHeight: '2.4em',
|
|---|
| 414 | fontSize: {xs: '0.875rem', sm: '1rem'}
|
|---|
| 415 | }}
|
|---|
| 416 | >
|
|---|
| 417 | {comp.name}
|
|---|
| 418 | </Typography>
|
|---|
| 419 | </Box>
|
|---|
| 420 | <Typography
|
|---|
| 421 | variant="h6"
|
|---|
| 422 | color="primary.main"
|
|---|
| 423 | fontWeight="bold"
|
|---|
| 424 | sx={{fontSize: {xs: '1rem', sm: '1.25rem'}}}
|
|---|
| 425 | >
|
|---|
| 426 | {formatMoney(comp.price)}
|
|---|
| 427 | </Typography>
|
|---|
| 428 | </CardContent>
|
|---|
| 429 |
|
|---|
| 430 | <Box sx={{
|
|---|
| 431 | p: {xs: 1.5, sm: 2},
|
|---|
| 432 | pt: 0,
|
|---|
| 433 | display: 'flex',
|
|---|
| 434 | flexDirection: 'column',
|
|---|
| 435 | gap: 1
|
|---|
| 436 | }}>
|
|---|
| 437 | <Button
|
|---|
| 438 | fullWidth
|
|---|
| 439 | variant="contained"
|
|---|
| 440 | onClick={() => setSelectedComponent(comp)}
|
|---|
| 441 | size="small"
|
|---|
| 442 | sx={{
|
|---|
| 443 | bgcolor: '#ff8201',
|
|---|
| 444 | fontWeight: 'bold',
|
|---|
| 445 | fontSize: {xs: '0.75rem', sm: '0.875rem'},
|
|---|
| 446 | '&:hover': {bgcolor: '#e67300'}
|
|---|
| 447 | }}
|
|---|
| 448 | >
|
|---|
| 449 | Details
|
|---|
| 450 | </Button>
|
|---|
| 451 | {mode === 'forge' && onSelect && (
|
|---|
| 452 | <Button
|
|---|
| 453 | fullWidth
|
|---|
| 454 | variant="contained"
|
|---|
| 455 | onClick={() => onSelect(comp)}
|
|---|
| 456 | size="small"
|
|---|
| 457 | sx={{
|
|---|
| 458 | bgcolor: '#4caf50',
|
|---|
| 459 | fontWeight: 'bold',
|
|---|
| 460 | fontSize: {xs: '0.75rem', sm: '0.875rem'},
|
|---|
| 461 | '&:hover': {bgcolor: '#388e3c'}
|
|---|
| 462 | }}
|
|---|
| 463 | >
|
|---|
| 464 | Add
|
|---|
| 465 | </Button>
|
|---|
| 466 | )}
|
|---|
| 467 | </Box>
|
|---|
| 468 | </Card>
|
|---|
| 469 | </Box>
|
|---|
| 470 | ))}
|
|---|
| 471 | {processedComponents.length === 0 && (
|
|---|
| 472 | <Box sx={{gridColumn: '1 / -1', width: '100%', textAlign: 'center', mt: 5}}>
|
|---|
| 473 | <Typography variant="h6" color="text.secondary"
|
|---|
| 474 | sx={{fontSize: {xs: '1rem', sm: '1.25rem'}}}>
|
|---|
| 475 | No compatible components found.
|
|---|
| 476 | </Typography>
|
|---|
| 477 | {mode === 'forge' && (
|
|---|
| 478 | <Typography variant="body2" color="error">
|
|---|
| 479 | (Try removing incompatible parts)
|
|---|
| 480 | </Typography>
|
|---|
| 481 | )}
|
|---|
| 482 | </Box>
|
|---|
| 483 | )}
|
|---|
| 484 | </Box>
|
|---|
| 485 | )}
|
|---|
| 486 | </Box>
|
|---|
| 487 | </DialogContent>
|
|---|
| 488 |
|
|---|
| 489 | <ComponentDetailsDialog
|
|---|
| 490 | open={!!selectedComponent}
|
|---|
| 491 | component={selectedComponent}
|
|---|
| 492 | onClose={() => setSelectedComponent(null)}
|
|---|
| 493 | />
|
|---|
| 494 | </Dialog>
|
|---|
| 495 |
|
|---|
| 496 | <Drawer
|
|---|
| 497 | anchor="left"
|
|---|
| 498 | open={mobileFiltersOpen && open}
|
|---|
| 499 | onClose={() => setMobileFiltersOpen(false)}
|
|---|
| 500 | sx={{zIndex: 1400}}
|
|---|
| 501 | PaperProps={{
|
|---|
| 502 | sx: {
|
|---|
| 503 | bgcolor: '#1e1e1e'
|
|---|
| 504 | }
|
|---|
| 505 | }}
|
|---|
| 506 | disableScrollLock={true}
|
|---|
| 507 | >
|
|---|
| 508 | <Box sx={{pt: 2, width: 280}}>
|
|---|
| 509 | <Box sx={{
|
|---|
| 510 | display: 'flex',
|
|---|
| 511 | justifyContent: 'space-between',
|
|---|
| 512 | alignItems: 'center',
|
|---|
| 513 | px: 3,
|
|---|
| 514 | pb: 2
|
|---|
| 515 | }}>
|
|---|
| 516 | <Typography variant="h6" fontWeight="bold">Filters</Typography>
|
|---|
| 517 | <IconButton onClick={() => setMobileFiltersOpen(false)}>
|
|---|
| 518 | <CloseIcon/>
|
|---|
| 519 | </IconButton>
|
|---|
| 520 | </Box>
|
|---|
| 521 | {FilterContent}
|
|---|
| 522 | </Box>
|
|---|
| 523 | </Drawer>
|
|---|
| 524 |
|
|---|
| 525 | <Dialog open={suggestOpen} onClose={() => setSuggestOpen(false)} maxWidth="sm" fullWidth>
|
|---|
| 526 | <DialogTitle>Suggest a New Component</DialogTitle>
|
|---|
| 527 | <DialogContent>
|
|---|
| 528 | <MuiTextField
|
|---|
| 529 | fullWidth
|
|---|
| 530 | label="Product Link *"
|
|---|
| 531 | name="link"
|
|---|
| 532 | value={suggestForm.link}
|
|---|
| 533 | onChange={handleSuggestChange}
|
|---|
| 534 | placeholder="https://example.com/product"
|
|---|
| 535 | sx={{mt: 1}}
|
|---|
| 536 | size="small"
|
|---|
| 537 | />
|
|---|
| 538 | <MuiTextField
|
|---|
| 539 | fullWidth
|
|---|
| 540 | label="Description *"
|
|---|
| 541 | name="description"
|
|---|
| 542 | value={suggestForm.description}
|
|---|
| 543 | onChange={handleSuggestChange}
|
|---|
| 544 | multiline
|
|---|
| 545 | rows={3}
|
|---|
| 546 | placeholder="Why should we add this component? Any specs/details?"
|
|---|
| 547 | sx={{mt: 2}}
|
|---|
| 548 | size="small"
|
|---|
| 549 | />
|
|---|
| 550 | <Typography variant="caption" color="text.secondary" sx={{mt: 1, display: 'block'}}>
|
|---|
| 551 | Category: <b>{category?.toUpperCase()}</b>
|
|---|
| 552 | </Typography>
|
|---|
| 553 | </DialogContent>
|
|---|
| 554 | <DialogActions>
|
|---|
| 555 | <Button onClick={() => setSuggestOpen(false)}>Cancel</Button>
|
|---|
| 556 | <Button
|
|---|
| 557 | onClick={submitSuggestion}
|
|---|
| 558 | disabled={suggestLoading || !userId}
|
|---|
| 559 | variant="contained"
|
|---|
| 560 | startIcon={suggestLoading ? <CircularProgress size={20}/> : null}
|
|---|
| 561 | >
|
|---|
| 562 | {suggestLoading ? 'Submitting...' : 'Submit Suggestion'}
|
|---|
| 563 | </Button>
|
|---|
| 564 | </DialogActions>
|
|---|
| 565 | </Dialog>
|
|---|
| 566 |
|
|---|
| 567 | <Snackbar
|
|---|
| 568 | open={snackbarOpen}
|
|---|
| 569 | autoHideDuration={4000}
|
|---|
| 570 | onClose={() => setSnackbarOpen(false)}
|
|---|
| 571 | anchorOrigin={{vertical: 'bottom', horizontal: 'right'}}
|
|---|
| 572 | >
|
|---|
| 573 | <Alert onClose={() => setSnackbarOpen(false)} sx={{width: '100%'}}>
|
|---|
| 574 | {snackbarMessage}
|
|---|
| 575 | </Alert>
|
|---|
| 576 | </Snackbar>
|
|---|
| 577 | </>
|
|---|
| 578 | );
|
|---|
| 579 | } |
|---|