source: components/BuildDetailsDialog.tsx@ ff3a614

main
Last change on this file since ff3a614 was 41825d5, checked in by Mihail <mihail2.naumov@…>, 4 months ago

Fixed auth issues

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