source: components/BuildDetailsDialog.tsx@ 9d40151

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

Fixed total build price error and other small errors.

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