source: components/BuildDetailsDialog.tsx@ 8a7f936

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

Added Forge Page, added suggest component, fixed styling

  • Property mode set to 100644
File size: 15.3 KB
Line 
1import React, { useEffect, useState } from 'react';
2import {
3 Dialog, DialogTitle, DialogContent, DialogActions, Button, Grid, Box, Typography,
4 IconButton, Tab, Tabs, Table, TableBody, TableCell, TableRow, Rating, TextField, Avatar, Chip, Alert
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';
12
13const formatPrice = (price: any) => new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(Number(price) || 0);
14
15export default function BuildDetailsDialog({ open, buildId, onClose, currentUser }: any) {
16 const [details, setDetails] = useState<any>(null);
17 const [loading, setLoading] = useState(false);
18 const [tabIndex, setTabIndex] = useState(0);
19 const [cloneDialogOpen, setCloneDialogOpen] = useState(false);
20 const [cloningBuildId, setCloningBuildId] = useState<number | null>(null);
21
22 const [reviewText, setReviewText] = useState("");
23 const [ratingVal, setRatingVal] = useState(5);
24
25 useEffect(() => {
26 if (open && buildId) {
27 setLoading(true);
28 setReviewText("");
29 setRatingVal(5);
30
31 onGetBuildDetails({ buildId })
32 .then(data => {
33 setDetails(data);
34 if (data.userRating) setRatingVal(data.userRating);
35 if (data.userReview) setReviewText(data.userReview);
36 })
37 .finally(() => setLoading(false));
38 }
39 }, [open, buildId]);
40
41 const handleFavorite = async () => {
42 if (!currentUser) return alert("Please login to favorite builds.");
43 const res = await onToggleFavorite({ buildId });
44 setDetails((prev: any) => ({ ...prev, isFavorite: res }));
45 };
46
47 const handleSubmitReview = async () => {
48 if (!currentUser) return alert("Please login to review.");
49
50 await onSetReview({
51 buildId,
52 content: reviewText,
53 // rating: ratingVal
54 });
55
56 await onSetRating({
57 buildId,
58 value: ratingVal
59 })
60
61 const refreshed = await onGetBuildDetails({ buildId });
62 setDetails(refreshed);
63 };
64
65 const handleCloneConfirm = async () => {
66 if (!cloningBuildId) return;
67
68 try {
69 const newBuildId = await onCloneBuild({ buildId: cloningBuildId });
70
71 window.location.href = `/forge?buildId=${newBuildId}`;
72
73 setCloneDialogOpen(false);
74 setCloningBuildId(null);
75 } catch (e) {
76 alert("Failed to clone build. Please try again.");
77 }
78 };
79
80
81 if (!open) return null;
82
83 return (
84 <>
85 <Dialog open={open} onClose={onClose} maxWidth="md" fullWidth scroll="paper">
86 {loading || !details ? (
87 <Box sx={{ p: 5, textAlign: 'center' }}>Loading Forge Schematics...</Box>
88 ) : (
89 <>
90 <DialogTitle sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', bgcolor: '#ff8201' }}>
91 <Box>
92 <Typography variant="h5" fontWeight="bold">{details.name}</Typography>
93 <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
94 <PersonIcon sx={{ fontSize: 16 }} />
95 <Typography variant="subtitle2" color="text.secondary" fontWeight="bold">
96 by {details.creator}
97 </Typography>
98 <Chip label={formatPrice(details.totalPrice)} size="small" color="primary" variant="outlined" />
99 </Box>
100 </Box>
101 <IconButton onClick={onClose}><CloseIcon /></IconButton>
102 </DialogTitle>
103
104 <DialogContent sx={{ p: 0 }}>
105 <Box sx={{ borderBottom: 1, borderColor: 'divider', px: 2, bgcolor: 'primary', position: 'sticky', top: 0, zIndex: 1 }}>
106 <Tabs value={tabIndex} onChange={(_, v) => setTabIndex(v)}>
107 <Tab label="Specs" />
108 <Tab label={`Reviews (${details.ratingStatistics.ratingCount})`} />
109 </Tabs>
110 </Box>
111
112 <Box sx={{ p: 3 }}>
113 {tabIndex === 0 && (
114 <Grid container spacing={2}>
115 <Grid item xs={12} md={8}>
116 <Table size="small">
117 <TableBody>
118 {details.components.map((comp: any) => (
119 <TableRow key={comp.id}>
120 <TableCell sx={{ width: 50 }}>
121 <Avatar
122 src={comp.img_url || undefined}
123 variant="rounded"
124 sx={{ width: 45, height: 45, bgcolor: '#ff8201'}}
125 >
126 {comp.type?.substring(0, 3)?.toUpperCase()}
127 </Avatar>
128 </TableCell>
129 <TableCell>
130 <Typography variant="body2" color="text.secondary" sx={{ fontSize: '0.75rem', textTransform: 'uppercase' }}>
131 {comp.type}
132 </Typography>
133 <Typography variant="body1" fontWeight="500">
134 {comp.brand} {comp.name}
135 </Typography>
136 </TableCell>
137 <TableCell align="right" sx={{ fontWeight: 'bold', color: '#ff8201' }}>
138 {formatPrice(comp.price)}
139 </TableCell>
140 </TableRow>
141 ))}
142 <TableRow sx={{ bgcolor: '#424343' }}>
143 <TableCell colSpan={2} sx={{ fontWeight: 'bold', color: '#ff8201' }}>TOTAL</TableCell>
144 <TableCell align="right" sx={{ fontWeight: 'bold', fontSize: '1.1rem', color: 'primary.main' }}>
145 {formatPrice(details.totalPrice)}
146 </TableCell>
147 </TableRow>
148 </TableBody>
149 </Table>
150 </Grid>
151
152 <Grid item xs={12} md={4}>
153 <Box sx={{ bgcolor: '#424343', p: 2, borderRadius: 2, mb: 2 }}>
154 <Typography color="primary.main" gutterBottom fontWeight="bold">Builder's Notes</Typography>
155 <Typography color="primary.main" variant="body2" sx={{ fontStyle: 'italic' }}>
156 "{details.description || "No notes provided."}"
157 </Typography>
158 </Box>
159
160 <Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
161 <Button
162 variant="contained"
163 color="primary"
164 size="large"
165 startIcon={<AutoFixHighIcon />}
166 onClick={() => {
167 setCloningBuildId(details.id);
168 setCloneDialogOpen(true);
169 }}
170 >
171 Clone & Edit
172 </Button>
173 <Button
174 variant={details.isFavorite ? "contained" : "outlined"}
175 color={details.isFavorite ? "error" : "primary"}
176 startIcon={details.isFavorite ? <FavoriteIcon /> : <FavoriteBorderIcon />}
177 onClick={handleFavorite}
178 >
179 {details.isFavorite ? "Favorited" : "Add to Favorites"}
180 </Button>
181 </Box>
182 </Grid>
183 </Grid>
184 )}
185
186 {tabIndex === 1 && (
187 <Box>
188 <Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 4, p: 2, bgcolor: '#5e5e5e', borderRadius: 2 }}>
189 <Typography variant="h3" fontWeight="bold">{details.ratingStatistics.averageRating.toFixed(1)}</Typography>
190 <Box>
191 <Rating value={details.ratingStatistics.averageRating} readOnly precision={0.5} />
192 <Typography variant="body2" color="text.secondary">{details.ratingStatistics.ratingCount} ratings</Typography>
193 </Box>
194 </Box>
195
196 {currentUser && details.userId !== currentUser.id && (
197 <Box sx={{ mb: 4, p: 2, border: '1px solid #ddd', borderRadius: 2 }}>
198 <Typography variant="subtitle2" gutterBottom>Your Review</Typography>
199 <Box sx={{ display: 'flex', alignItems: 'center', mb: 1 }}>
200 <Rating value={ratingVal} onChange={(_, v) => setRatingVal(v || 5)} />
201 </Box>
202 <TextField
203 fullWidth
204 multiline
205 rows={2}
206 placeholder="Share your thoughts on this build..."
207 value={reviewText}
208 onChange={(e) => setReviewText(e.target.value)}
209 sx={{ mb: 1 }}
210 />
211 <Button size="small" variant="contained" onClick={handleSubmitReview}>
212 Submit Review
213 </Button>
214 </Box>
215 )}
216
217 {currentUser && details.userId === currentUser.id && (
218 <Alert severity="info" sx={{ mb: 4 }}>
219 You cannot rate your own builds.
220 </Alert>
221 )}
222
223 <Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
224 {details.reviews.map((rev: any, i: number) => (
225 <Box key={i} sx={{ pb: 2, borderBottom: '1px solid #eee' }}>
226 <Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 0.5 }}>
227 <Typography fontWeight="bold" variant="body2">{rev.username}</Typography>
228 <Typography variant="caption" color="text.secondary">{rev.createdAt}</Typography>
229 </Box>
230 <Typography variant="body2">{rev.content}</Typography>
231 </Box>
232 ))}
233 {details.reviews.length === 0 && (
234 <Typography color="text.secondary" align="center">No reviews yet. Be the first!</Typography>
235 )}
236 </Box>
237 </Box>
238 )}
239 </Box>
240 </DialogContent>
241 </>
242 )}
243 </Dialog>
244
245 <Dialog open={cloneDialogOpen} onClose={() => setCloneDialogOpen(false)}>
246 <DialogTitle>Clone Build</DialogTitle>
247 <DialogContent>
248 <Typography>
249 This will create a copy of "{details?.name}" in your Forge so you can edit it.
250 All components will be copied over.
251 </Typography>
252 </DialogContent>
253 <DialogActions>
254 <Button onClick={() => setCloneDialogOpen(false)}>Cancel</Button>
255 <Button
256 variant="contained"
257 onClick={handleCloneConfirm}
258 disabled={!cloningBuildId}
259 >
260 Clone Build
261 </Button>
262 </DialogActions>
263 </Dialog>
264 </>
265 );
266}
Note: See TracBrowser for help on using the repository browser.