source: components/BuildDetailsDialog.tsx@ 4f2900a

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

Fixed clone auth errors and component names in the component dialogs.

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