source: components/BuildDetailsDialog.tsx@ be22289

main
Last change on this file since be22289 was 41a2f81, checked in by Mihail <mihail2.naumov@…>, 6 months ago

Added mini photos for components

  • Property mode set to 100644
File size: 19.2 KB
Line 
1import React, {useEffect, useState} from 'react';
2import {
3 Dialog, DialogTitle, DialogContent, DialogActions, Button, Grid, Box, Typography,
4 IconButton, Tab, Tabs, Table, TableBody, TableCell, TableRow, Rating, TextField, Avatar, Chip, Alert
5} from '@mui/material';
6import CloseIcon from '@mui/icons-material/Close';
7import AutoFixHighIcon from '@mui/icons-material/AutoFixHigh';
8import FavoriteIcon from '@mui/icons-material/Favorite';
9import FavoriteBorderIcon from '@mui/icons-material/FavoriteBorder';
10import PersonIcon from "@mui/icons-material/Person";
11import {onGetBuildDetails, onSetReview, onToggleFavorite, onCloneBuild, onSetRating} from '../pages/+Layout.telefunc';
12import {onGetBuildState} from '../pages/forge/forge.telefunc';
13
14const formatPrice = (price: any) => new Intl.NumberFormat('en-US', {
15 style: 'currency',
16 currency: 'USD'
17}).format(Number(price) || 0);
18
19export default function BuildDetailsDialog({open, buildId, onClose, currentUser, isDashboardView = false}: {
20 open: boolean;
21 buildId: number | null;
22 onClose: () => void;
23 currentUser: any;
24 isDashboardView?: boolean;
25}) {
26 const [details, setDetails] = useState<any>(null);
27 const [loading, setLoading] = useState(false);
28 const [tabIndex, setTabIndex] = useState(0);
29 const [cloneDialogOpen, setCloneDialogOpen] = useState(false);
30 const [cloningBuildId, setCloningBuildId] = useState<number | null>(null);
31 const [isOwner, setIsOwner] = useState(false);
32
33 const [reviewText, setReviewText] = useState("");
34 const [ratingVal, setRatingVal] = useState(5);
35
36 // Main details fetch
37 useEffect(() => {
38 if (open && buildId !== null && typeof buildId === 'number') {
39 setLoading(true);
40 setReviewText("");
41 setRatingVal(5);
42
43 onGetBuildDetails({buildId})
44 .then(data => {
45 setDetails(data);
46 if (data.userRating) setRatingVal(data.userRating);
47 if (data.userReview) setReviewText(data.userReview);
48 })
49 .finally(() => setLoading(false));
50 }
51 }, [open, buildId]);
52
53 useEffect(() => {
54 if (open && buildId !== null && typeof buildId === 'number') {
55 onGetBuildState({buildId})
56 .then(state => {
57 setIsOwner(!!state);
58 })
59 .catch(() => setIsOwner(false));
60 } else {
61 setIsOwner(false);
62 }
63 }, [open, buildId]);
64
65 const handleFavorite = async () => {
66 if (!currentUser || buildId === null) return alert("Please login to favorite builds.");
67 const res = await onToggleFavorite({buildId});
68 setDetails((prev: any) => ({...prev, isFavorite: res}));
69 };
70
71 const handleSubmitReview = async () => {
72 if (!currentUser || buildId === null) return alert("Please login to review.");
73
74 await onSetReview({
75 buildId,
76 content: reviewText,
77 });
78
79 await onSetRating({
80 buildId,
81 value: ratingVal
82 });
83
84 const refreshed = await onGetBuildDetails({buildId});
85 setDetails(refreshed);
86 };
87
88 const handleCloneConfirm = async () => {
89 if (!cloningBuildId) return;
90
91 try {
92 const newBuildId = await onCloneBuild({buildId: cloningBuildId});
93 window.location.href = `/forge?buildId=${newBuildId}`;
94 setCloneDialogOpen(false);
95 setCloningBuildId(null);
96 } catch (e) {
97 alert("Failed to clone build. Please try again.");
98 }
99 };
100
101 if (!open) return null;
102
103 return (
104 <>
105 <Dialog open={open} onClose={onClose} maxWidth="md" fullWidth scroll="paper">
106 {loading || !details ? (
107 <Box sx={{p: 5, textAlign: 'center'}}>Loading Forge Schematics...</Box>
108 ) : (
109 <>
110 <DialogTitle sx={{
111 display: 'flex',
112 justifyContent: 'space-between',
113 alignItems: 'center',
114 bgcolor: '#ff8201'
115 }}>
116 <Box>
117 <Typography variant="h5" fontWeight="bold">{details.name}</Typography>
118 <Box sx={{display: 'flex', alignItems: 'center', gap: 1}}>
119 <PersonIcon sx={{fontSize: 16}}/>
120 <Typography variant="subtitle2" color="text.secondary" fontWeight="bold">
121 by {details.creator}
122 </Typography>
123 <Chip label={formatPrice(details.totalPrice)} size="small" color="primary"
124 variant="outlined"/>
125 </Box>
126 </Box>
127 <IconButton onClick={onClose}><CloseIcon/></IconButton>
128 </DialogTitle>
129
130 <DialogContent sx={{p: 0}}>
131 <Box sx={{
132 borderBottom: 1,
133 borderColor: 'divider',
134 px: 2,
135 bgcolor: 'primary',
136 position: 'sticky',
137 top: 0,
138 zIndex: 1
139 }}>
140 <Tabs value={tabIndex} onChange={(_, v) => setTabIndex(v)}>
141 <Tab label="Specs"/>
142 <Tab label={`Reviews (${details.ratingStatistics.ratingCount})`}/>
143 </Tabs>
144 </Box>
145
146 <Box sx={{p: 3}}>
147 {tabIndex === 0 && (
148 <Grid container spacing={2}>
149 <Grid item xs={12} md={8}>
150 <Table size="small">
151 <TableBody>
152 {details.components.map((comp: any) => (
153 <TableRow key={comp.id}>
154 <TableCell sx={{width: 50}}>
155 <Avatar
156 src={comp.imgUrl || undefined}
157 variant="rounded"
158 sx={{width: 50, height: 50, bgcolor: '#ff8201'}}
159 >
160 {comp.type?.substring(0, 3)?.toUpperCase()}
161 </Avatar>
162 </TableCell>
163 <TableCell>
164 <Typography variant="body2" color="text.secondary" sx={{
165 fontSize: '0.75rem',
166 textTransform: 'uppercase'
167 }}>
168 {comp.type}
169 </Typography>
170 <Typography variant="body1" fontWeight="500">
171 {comp.brand} {comp.name}
172 </Typography>
173 </TableCell>
174 <TableCell align="right"
175 sx={{fontWeight: 'bold', color: '#ff8201'}}>
176 {formatPrice(comp.price)}
177 </TableCell>
178 </TableRow>
179 ))}
180 <TableRow sx={{bgcolor: '#424343'}}>
181 <TableCell colSpan={2} sx={{
182 fontWeight: 'bold',
183 color: '#ff8201'
184 }}>TOTAL</TableCell>
185 <TableCell align="right" sx={{
186 fontWeight: 'bold',
187 fontSize: '1.1rem',
188 color: 'primary.main'
189 }}>
190 {formatPrice(details.totalPrice)}
191 </TableCell>
192 </TableRow>
193 </TableBody>
194 </Table>
195 </Grid>
196
197 <Grid item xs={12} md={4}>
198 <Box sx={{bgcolor: '#424343', p: 2, borderRadius: 2, mb: 2}}>
199 <Typography color="primary.main" gutterBottom fontWeight="bold">Builder's
200 Notes</Typography>
201 <Typography color="primary.main" variant="body2"
202 sx={{fontStyle: 'italic'}}>
203 "{details.description || "No notes provided."}"
204 </Typography>
205 </Box>
206
207 <Box sx={{display: 'flex', flexDirection: 'column', gap: 1}}>
208 {isDashboardView && isOwner ? (
209 <Button
210 variant="contained"
211 color="primary"
212 size="large"
213 startIcon={<AutoFixHighIcon/>}
214 onClick={() => {
215 window.location.href = `/forge?buildId=${details.id}`;
216 onClose();
217 }}
218 >
219 Edit Build
220 </Button>
221 ) : (
222 <Button
223 variant="contained"
224 color="primary"
225 size="large"
226 startIcon={<AutoFixHighIcon/>}
227 onClick={() => {
228 setCloningBuildId(details.id);
229 setCloneDialogOpen(true);
230 }}
231 >
232 Clone & Edit
233 </Button>
234 )}
235 <Button
236 variant={details.isFavorite ? "contained" : "outlined"}
237 color={details.isFavorite ? "error" : "primary"}
238 startIcon={details.isFavorite ? <FavoriteIcon/> :
239 <FavoriteBorderIcon/>}
240 onClick={handleFavorite}
241 >
242 {details.isFavorite ? "Favorited" : "Add to Favorites"}
243 </Button>
244 </Box>
245 </Grid>
246 </Grid>
247 )}
248
249 {tabIndex === 1 && (
250 <Box>
251 <Box sx={{
252 display: 'flex',
253 alignItems: 'center',
254 gap: 2,
255 mb: 4,
256 p: 2,
257 bgcolor: '#5e5e5e',
258 borderRadius: 2
259 }}>
260 <Typography variant="h3"
261 fontWeight="bold">{details.ratingStatistics.averageRating.toFixed(1)}</Typography>
262 <Box>
263 <Rating value={details.ratingStatistics.averageRating} readOnly
264 precision={0.5}/>
265 <Typography variant="body2"
266 color="text.secondary">{details.ratingStatistics.ratingCount} ratings</Typography>
267 </Box>
268 </Box>
269
270 {currentUser && details.userId !== currentUser.id && (
271 <Box sx={{mb: 4, p: 2, border: '1px solid #ddd', borderRadius: 2}}>
272 <Typography variant="subtitle2" gutterBottom>Your Review</Typography>
273 <Box sx={{display: 'flex', alignItems: 'center', mb: 1}}>
274 <Rating value={ratingVal}
275 onChange={(_, v) => setRatingVal(v || 5)}/>
276 </Box>
277 <TextField
278 fullWidth
279 multiline
280 rows={2}
281 placeholder="Share your thoughts on this build..."
282 value={reviewText}
283 onChange={(e) => setReviewText(e.target.value)}
284 sx={{mb: 1}}
285 />
286 <Button size="small" variant="contained" onClick={handleSubmitReview}>
287 Submit Review
288 </Button>
289 </Box>
290 )}
291
292 {currentUser && details.userId === currentUser.id && (
293 <Alert severity="info" sx={{mb: 4}}>
294 You cannot rate your own builds.
295 </Alert>
296 )}
297
298 <Box sx={{display: 'flex', flexDirection: 'column', gap: 2}}>
299 {details.reviews.map((rev: any, i: number) => (
300 <Box key={i} sx={{pb: 2, borderBottom: '1px solid #eee'}}>
301 <Box sx={{
302 display: 'flex',
303 justifyContent: 'space-between',
304 mb: 0.5
305 }}>
306 <Typography fontWeight="bold"
307 variant="body2">{rev.username}</Typography>
308 <Typography variant="caption"
309 color="text.secondary">{rev.createdAt}</Typography>
310 </Box>
311 <Typography variant="body2">{rev.content}</Typography>
312 </Box>
313 ))}
314 {details.reviews.length === 0 && (
315 <Typography color="text.secondary" align="center">No reviews yet. Be the
316 first!</Typography>
317 )}
318 </Box>
319 </Box>
320 )}
321 </Box>
322 </DialogContent>
323 </>
324 )}
325 </Dialog>
326
327 <Dialog open={cloneDialogOpen} onClose={() => setCloneDialogOpen(false)}>
328 <DialogTitle>Clone Build</DialogTitle>
329 <DialogContent>
330 <Typography>
331 This will create a copy of "{details?.name}" in your Forge so you can edit it.
332 All components will be copied over.
333 </Typography>
334 </DialogContent>
335 <DialogActions>
336 <Button onClick={() => setCloneDialogOpen(false)}>Cancel</Button>
337 <Button
338 variant="contained"
339 onClick={handleCloneConfirm}
340 disabled={!cloningBuildId}
341 >
342 Clone Build
343 </Button>
344 </DialogActions>
345 </Dialog>
346 </>
347 );
348}
Note: See TracBrowser for help on using the repository browser.