- File:
-
- 1 edited
-
components/BuildDetailsDialog.tsx (modified) (15 diffs)
Legend:
- Unmodified
- Added
- Removed
-
components/BuildDetailsDialog.tsx
rb6e1b3c r8a7f936 10 10 import PersonIcon from "@mui/icons-material/Person"; 11 11 import {onGetBuildDetails, onSetReview, onToggleFavorite, onCloneBuild, onSetRating} from '../pages/+Layout.telefunc'; 12 import {onGetBuildState} from '../pages/forge/forge.telefunc'; 13 14 const formatPrice = (price: any) => new Intl.NumberFormat('en-US', { 15 style: 'currency', 16 currency: 'USD' 17 }).format(Number(price) || 0); 18 19 export 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 }) { 12 13 const formatPrice = (price: any) => new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(Number(price) || 0); 14 15 export default function BuildDetailsDialog({ open, buildId, onClose, currentUser }: any) { 26 16 const [details, setDetails] = useState<any>(null); 27 17 const [loading, setLoading] = useState(false); … … 29 19 const [cloneDialogOpen, setCloneDialogOpen] = useState(false); 30 20 const [cloningBuildId, setCloningBuildId] = useState<number | null>(null); 31 const [isOwner, setIsOwner] = useState(false);32 21 33 22 const [reviewText, setReviewText] = useState(""); 34 23 const [ratingVal, setRatingVal] = useState(5); 35 24 36 // Main details fetch37 25 useEffect(() => { 38 if (open && buildId !== null && typeof buildId === 'number') {26 if (open && buildId) { 39 27 setLoading(true); 40 28 setReviewText(""); 41 29 setRatingVal(5); 42 30 43 onGetBuildDetails({ buildId})31 onGetBuildDetails({ buildId }) 44 32 .then(data => { 45 33 setDetails(data); … … 51 39 }, [open, buildId]); 52 40 53 // Ownership check for edit button54 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 66 41 const handleFavorite = async () => { 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}));42 if (!currentUser) return alert("Please login to favorite builds."); 43 const res = await onToggleFavorite({ buildId }); 44 setDetails((prev: any) => ({ ...prev, isFavorite: res })); 70 45 }; 71 46 72 47 const handleSubmitReview = async () => { 73 if (!currentUser || buildId === null) return alert("Please login to review.");48 if (!currentUser) return alert("Please login to review."); 74 49 75 50 await onSetReview({ 76 51 buildId, 77 52 content: reviewText, 53 // rating: ratingVal 78 54 }); 79 55 … … 81 57 buildId, 82 58 value: ratingVal 83 }) ;84 85 const refreshed = await onGetBuildDetails({ buildId});59 }) 60 61 const refreshed = await onGetBuildDetails({ buildId }); 86 62 setDetails(refreshed); 87 63 }; … … 92 68 try { 93 69 const newBuildId = await onCloneBuild({ buildId: cloningBuildId }); 70 94 71 window.location.href = `/forge?buildId=${newBuildId}`; 72 95 73 setCloneDialogOpen(false); 96 74 setCloningBuildId(null); … … 100 78 }; 101 79 80 102 81 if (!open) return null; 103 82 … … 106 85 <Dialog open={open} onClose={onClose} maxWidth="md" fullWidth scroll="paper"> 107 86 {loading || !details ? ( 108 <Box sx={{ p: 5, textAlign: 'center'}}>Loading Forge Schematics...</Box>87 <Box sx={{ p: 5, textAlign: 'center' }}>Loading Forge Schematics...</Box> 109 88 ) : ( 110 89 <> 111 <DialogTitle sx={{ 112 display: 'flex', 113 justifyContent: 'space-between', 114 alignItems: 'center', 115 bgcolor: '#ff8201' 116 }}> 90 <DialogTitle sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', bgcolor: '#ff8201' }}> 117 91 <Box> 118 92 <Typography variant="h5" fontWeight="bold">{details.name}</Typography> 119 <Box sx={{ display: 'flex', alignItems: 'center', gap: 1}}>120 <PersonIcon sx={{ fontSize: 16}}/>93 <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}> 94 <PersonIcon sx={{ fontSize: 16 }} /> 121 95 <Typography variant="subtitle2" color="text.secondary" fontWeight="bold"> 122 96 by {details.creator} 123 97 </Typography> 124 <Chip label={formatPrice(details.totalPrice)} size="small" color="primary" 125 variant="outlined"/> 98 <Chip label={formatPrice(details.totalPrice)} size="small" color="primary" variant="outlined" /> 126 99 </Box> 127 100 </Box> 128 <IconButton onClick={onClose}><CloseIcon /></IconButton>101 <IconButton onClick={onClose}><CloseIcon /></IconButton> 129 102 </DialogTitle> 130 103 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 }}> 104 <DialogContent sx={{ p: 0 }}> 105 <Box sx={{ borderBottom: 1, borderColor: 'divider', px: 2, bgcolor: 'primary', position: 'sticky', top: 0, zIndex: 1 }}> 141 106 <Tabs value={tabIndex} onChange={(_, v) => setTabIndex(v)}> 142 <Tab label="Specs" />143 <Tab label={`Reviews (${details.ratingStatistics.ratingCount})`} />107 <Tab label="Specs" /> 108 <Tab label={`Reviews (${details.ratingStatistics.ratingCount})`} /> 144 109 </Tabs> 145 110 </Box> 146 111 147 <Box sx={{ p: 3}}>112 <Box sx={{ p: 3 }}> 148 113 {tabIndex === 0 && ( 149 114 <Grid container spacing={2}> … … 153 118 {details.components.map((comp: any) => ( 154 119 <TableRow key={comp.id}> 155 <TableCell sx={{ width: 50}}>120 <TableCell sx={{ width: 50 }}> 156 121 <Avatar 157 122 src={comp.img_url || undefined} 158 123 variant="rounded" 159 sx={{ width: 45, height: 45, bgcolor: '#ff8201'}}124 sx={{ width: 45, height: 45, bgcolor: '#ff8201'}} 160 125 > 161 126 {comp.type?.substring(0, 3)?.toUpperCase()} … … 163 128 </TableCell> 164 129 <TableCell> 165 <Typography variant="body2" color="text.secondary" sx={{ 166 fontSize: '0.75rem', 167 textTransform: 'uppercase' 168 }}> 130 <Typography variant="body2" color="text.secondary" sx={{ fontSize: '0.75rem', textTransform: 'uppercase' }}> 169 131 {comp.type} 170 132 </Typography> … … 173 135 </Typography> 174 136 </TableCell> 175 <TableCell align="right" 176 sx={{fontWeight: 'bold', color: '#ff8201'}}> 137 <TableCell align="right" sx={{ fontWeight: 'bold', color: '#ff8201' }}> 177 138 {formatPrice(comp.price)} 178 139 </TableCell> 179 140 </TableRow> 180 141 ))} 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 }}> 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' }}> 191 145 {formatPrice(details.totalPrice)} 192 146 </TableCell> … … 197 151 198 152 <Grid item xs={12} md={4}> 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'}}> 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' }}> 204 156 "{details.description || "No notes provided."}" 205 157 </Typography> 206 158 </Box> 207 159 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 )} 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> 236 173 <Button 237 174 variant={details.isFavorite ? "contained" : "outlined"} 238 175 color={details.isFavorite ? "error" : "primary"} 239 startIcon={details.isFavorite ? <FavoriteIcon/> : 240 <FavoriteBorderIcon/>} 176 startIcon={details.isFavorite ? <FavoriteIcon /> : <FavoriteBorderIcon />} 241 177 onClick={handleFavorite} 242 178 > … … 250 186 {tabIndex === 1 && ( 251 187 <Box> 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> 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> 263 190 <Box> 264 <Rating value={details.ratingStatistics.averageRating} readOnly 265 precision={0.5}/> 266 <Typography variant="body2" 267 color="text.secondary">{details.ratingStatistics.ratingCount} ratings</Typography> 191 <Rating value={details.ratingStatistics.averageRating} readOnly precision={0.5} /> 192 <Typography variant="body2" color="text.secondary">{details.ratingStatistics.ratingCount} ratings</Typography> 268 193 </Box> 269 194 </Box> 270 195 271 196 {currentUser && details.userId !== currentUser.id && ( 272 <Box sx={{ mb: 4, p: 2, border: '1px solid #ddd', borderRadius: 2}}>197 <Box sx={{ mb: 4, p: 2, border: '1px solid #ddd', borderRadius: 2 }}> 273 198 <Typography variant="subtitle2" gutterBottom>Your Review</Typography> 274 <Box sx={{display: 'flex', alignItems: 'center', mb: 1}}> 275 <Rating value={ratingVal} 276 onChange={(_, v) => setRatingVal(v || 5)}/> 199 <Box sx={{ display: 'flex', alignItems: 'center', mb: 1 }}> 200 <Rating value={ratingVal} onChange={(_, v) => setRatingVal(v || 5)} /> 277 201 </Box> 278 202 <TextField … … 283 207 value={reviewText} 284 208 onChange={(e) => setReviewText(e.target.value)} 285 sx={{ mb: 1}}209 sx={{ mb: 1 }} 286 210 /> 287 211 <Button size="small" variant="contained" onClick={handleSubmitReview}> … … 292 216 293 217 {currentUser && details.userId === currentUser.id && ( 294 <Alert severity="info" sx={{ mb: 4}}>218 <Alert severity="info" sx={{ mb: 4 }}> 295 219 You cannot rate your own builds. 296 220 </Alert> 297 221 )} 298 222 299 <Box sx={{ display: 'flex', flexDirection: 'column', gap: 2}}>223 <Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}> 300 224 {details.reviews.map((rev: any, i: number) => ( 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> 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> 311 229 </Box> 312 230 <Typography variant="body2">{rev.content}</Typography> … … 314 232 ))} 315 233 {details.reviews.length === 0 && ( 316 <Typography color="text.secondary" align="center">No reviews yet. Be the 317 first!</Typography> 234 <Typography color="text.secondary" align="center">No reviews yet. Be the first!</Typography> 318 235 )} 319 236 </Box>
Note:
See TracChangeset
for help on using the changeset viewer.
