source: components/BuildDetailsDialog.tsx@ b6e1b3c

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

Added delete button and still fixing naming problem with build creation

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