source: pages/completed-builds/+Page.tsx@ 546a194

main
Last change on this file since 546a194 was 546a194, checked in by Mihail <mihail2.naumov@…>, 5 months ago

Optimized sites (changed from deprecated Grid to Box+CSS Grid)

  • Property mode set to 100644
File size: 8.4 KB
Line 
1import React, { useEffect, useState } from 'react';
2import {
3 Container, Box, Typography, TextField, MenuItem, Select,
4 Slider, Button, Paper, InputAdornment, CircularProgress
5} from '@mui/material';
6import SearchIcon from '@mui/icons-material/Search';
7import FilterListIcon from '@mui/icons-material/FilterList';
8
9import BuildCard from '../../components/BuildCard';
10import BuildDetailsDialog from '../../components/BuildDetailsDialog';
11import { onGetApprovedBuilds, onGetAuthState } from '../+Layout.telefunc';
12
13export default function CompletedBuildsPage() {
14 const [builds, setBuilds] = useState<any[]>([]);
15 const [loading, setLoading] = useState(true);
16 const [userId, setUserId] = useState<number | null>(null);
17 const [selectedBuildId, setSelectedBuildId] = useState<number | null>(null);
18
19 const [sortBy, setSortBy] = useState('price_asc');
20 const [priceRange, setPriceRange] = useState<number[]>([0, 5000]);
21 const [searchQuery, setSearchQuery] = useState("");
22
23 const loadBuilds = async () => {
24 setLoading(true);
25 try {
26 const [userData, data] = await Promise.all([
27 onGetAuthState(),
28 onGetApprovedBuilds({q: searchQuery})
29 ]);
30 setUserId(userData.userId);
31
32 let sortedData = [...data];
33
34 switch (sortBy) {
35 case 'price_asc':
36 sortedData.sort((a, b) => Number(a.total_price) - Number(b.total_price));
37 break;
38 case 'price_desc':
39 sortedData.sort((a, b) => Number(b.total_price) - Number(a.total_price));
40 break;
41 case 'rating_desc':
42 sortedData.sort((a, b) => (Number(b.avgRating) || 0) - (Number(a.avgRating) || 0));
43 break;
44 case 'oldest':
45 sortedData.sort((a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime());
46 break;
47 case 'newest':
48 default:
49 sortedData.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
50 break;
51 }
52
53 sortedData = sortedData.filter(b => {
54 const price = Number(b.total_price);
55 return price >= priceRange[0] && price <= priceRange[1];
56 });
57
58 setBuilds(sortedData);
59 } catch (e) {
60 console.error("Failed to load builds", e);
61 } finally {
62 setLoading(false);
63 }
64 };
65
66 useEffect(() => {
67 void loadBuilds();
68 }, [sortBy, searchQuery]);
69
70 return (
71 <Container maxWidth={false} sx={{ mt: 4, mb: 10, px: { xs: 2, md: 4 } }}>
72 <Typography variant="h4" fontWeight="bold" gutterBottom>Completed Builds</Typography>
73 <Typography color="text.secondary" sx={{ mb: 4 }}>
74 Browse community configurations. Filter by price, components, or popularity.
75 </Typography>
76
77 <Box sx={{ display: 'flex', flexDirection: { xs: 'column', md: 'row' }, gap: 4 }}>
78
79 <Box sx={{
80 width: { xs: '100%', md: '280px' },
81 flexShrink: 0
82 }}>
83 <Paper variant="outlined" sx={{ p: 3, position: 'sticky', top: 20 }}>
84 <Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
85 <FilterListIcon sx={{ mr: 1 }} />
86 <Typography variant="h6" fontWeight="bold">Filters</Typography>
87 </Box>
88
89 <TextField
90 fullWidth
91 size="small"
92 placeholder="Search builds..."
93 value={searchQuery}
94 onChange={(e) => setSearchQuery(e.target.value)}
95 slotProps={{
96 input: {
97 startAdornment: <InputAdornment position="start"><SearchIcon /></InputAdornment>,
98 }
99 }}
100 sx={{ mb: 3 }}
101 />
102
103 <Typography gutterBottom fontWeight="bold">Price Range</Typography>
104 <Box sx={{ px: 1, mb: 3 }}>
105 <Slider
106 value={priceRange}
107 onChange={(_, newValue) => setPriceRange(newValue as number[])}
108 valueLabelDisplay="auto"
109 min={0}
110 max={5000}
111 step={100}
112 />
113 <Box sx={{ display: 'flex', justifyContent: 'space-between', mt: 1 }}>
114 <Typography variant="caption">${priceRange[0]}</Typography>
115 <Typography variant="caption">${priceRange[1]}+</Typography>
116 </Box>
117 </Box>
118
119 <Button variant="contained" fullWidth onClick={loadBuilds}>
120 Apply Filters
121 </Button>
122 </Paper>
123 </Box>
124
125 <Box sx={{ flexGrow: 1 }}>
126 <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 3 }}>
127 <Typography fontWeight="bold">{builds.length} Builds Found</Typography>
128
129 <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
130 <Typography variant="body2" color="text.secondary">Sort by:</Typography>
131 <Select
132 size="small"
133 value={sortBy}
134 onChange={(e) => setSortBy(e.target.value)}
135 sx={{ minWidth: 150, bgcolor: 'background.paper' }}
136 >
137 <MenuItem value="newest">Newest First</MenuItem>
138 <MenuItem value="oldest">Oldest First</MenuItem>
139 <MenuItem value="price_desc">Price: High to Low</MenuItem>
140 <MenuItem value="price_asc">Price: Low to High</MenuItem>
141 <MenuItem value="rating_desc">Highest Rated</MenuItem>
142 </Select>
143 </Box>
144 </Box>
145
146 {loading ? (
147 <Box sx={{ p: 5, textAlign: 'center' }}>
148 <CircularProgress />
149 </Box>
150 ) : (
151 <Box
152 sx={{
153 display: 'grid',
154 gridTemplateColumns: {
155 xs: '1fr',
156 sm: 'repeat(2, 1fr)',
157 md: 'repeat(3, 1fr)',
158 lg: 'repeat(4, 1fr)',
159 xl: 'repeat(5, 1fr)'
160 },
161 gap: 3,
162 width: '100%'
163 }}
164 >
165 {builds.map((build) => (
166 <BuildCard
167 key={build.id}
168 build={build}
169 onClick={() => setSelectedBuildId(build.id)}
170 />
171 ))}
172 {builds.length === 0 && (
173 <Box sx={{ gridColumn: '1 / -1', p: 5, textAlign: 'center', bgcolor: '#f5f5f5', borderRadius: 2 }}>
174 <Typography>No builds found matching your filters.</Typography>
175 </Box>
176 )}
177 </Box>
178 )}
179 </Box>
180 </Box>
181
182 {/* @ts-ignore */}
183 <BuildDetailsDialog
184 open={!!selectedBuildId}
185 buildId={selectedBuildId}
186 currentUser={userId}
187 onClose={() => setSelectedBuildId(null)}
188 />
189 </Container>
190 );
191
192}
Note: See TracBrowser for help on using the repository browser.