source: components/BuildDetailsDialog.tsx@ 546a194

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

Optimizations & Layout/Text changes

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