Changeset 8a7f936 for components/BuildDetailsDialog.tsx
- Timestamp:
- 12/29/25 00:37:49 (6 months ago)
- Branches:
- main
- Children:
- 5af32f0
- Parents:
- ae5d054
- File:
-
- 1 edited
-
components/BuildDetailsDialog.tsx (modified) (4 diffs)
Legend:
- Unmodified
- Added
- Removed
-
components/BuildDetailsDialog.tsx
rae5d054 r8a7f936 1 1 import React, { useEffect, useState } from 'react'; 2 2 import { 3 Dialog, DialogTitle, DialogContent, Button, Grid, Box, Typography,4 IconButton, Tab, Tabs, Table, TableBody, TableCell, TableRow, Rating, TextField, Avatar, Chip 3 Dialog, DialogTitle, DialogContent, DialogActions, Button, Grid, Box, Typography, 4 IconButton, Tab, Tabs, Table, TableBody, TableCell, TableRow, Rating, TextField, Avatar, Chip, Alert 5 5 } from '@mui/material'; 6 6 import CloseIcon from '@mui/icons-material/Close'; … … 9 9 import FavoriteBorderIcon from '@mui/icons-material/FavoriteBorder'; 10 10 import PersonIcon from "@mui/icons-material/Person"; 11 import { onGetBuildDetails, onSetReview, onToggleFavorite} from '../pages/+Layout.telefunc';11 import {onGetBuildDetails, onSetReview, onToggleFavorite, onCloneBuild, onSetRating} from '../pages/+Layout.telefunc'; 12 12 13 13 const formatPrice = (price: any) => new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(Number(price) || 0); 14 14 15 export default function BuildDetailsDialog({ open, buildId, onClose, onClone,currentUser }: any) {15 export default function BuildDetailsDialog({ open, buildId, onClose, currentUser }: any) { 16 16 const [details, setDetails] = useState<any>(null); 17 17 const [loading, setLoading] = useState(false); 18 18 const [tabIndex, setTabIndex] = useState(0); 19 const [cloneDialogOpen, setCloneDialogOpen] = useState(false); 20 const [cloningBuildId, setCloningBuildId] = useState<number | null>(null); 19 21 20 22 const [reviewText, setReviewText] = useState(""); … … 45 47 const handleSubmitReview = async () => { 46 48 if (!currentUser) return alert("Please login to review."); 47 await onSetReview({ buildId, content: reviewText }); 48 49 setReviewText(""); 49 50 await onSetReview({ 51 buildId, 52 content: reviewText, 53 // rating: ratingVal 54 }); 55 56 await onSetRating({ 57 buildId, 58 value: ratingVal 59 }) 50 60 51 61 const refreshed = await onGetBuildDetails({ buildId }); … … 53 63 }; 54 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 55 81 if (!open) return null; 56 82 57 // @ts-ignore58 83 return ( 59 <Dialog open={open} onClose={onClose} maxWidth="md" fullWidth scroll="paper"> 60 {loading || !details ? ( 61 <Box sx={{ p: 5, textAlign: 'center' }}>Loading Forge Schematics...</Box> 62 ) : ( 63 <> 64 <DialogTitle sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', bgcolor: '#ff8201' }}> 65 <Box> 66 <Typography variant="h5" fontWeight="bold">{details.name}</Typography> 67 <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}> 68 <PersonIcon sx={{ fontSize: 16 }} /> 69 <Typography variant="subtitle2" color="text.secondary" fontWeight="bold"> 70 by {details.creator} 71 </Typography> 72 <Chip label={formatPrice(details.totalPrice)} size="small" color="primary" variant="outlined" /> 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> 73 100 </Box> 74 </Box> 75 <IconButton onClick={onClose}><CloseIcon /></IconButton> 76 </DialogTitle> 77 78 <DialogContent sx={{ p: 0 }}> 79 <Box sx={{ borderBottom: 1, borderColor: 'divider', px: 2, bgcolor: 'primary', position: 'sticky', top: 0, zIndex: 1 }}> 80 <Tabs value={tabIndex} onChange={(_, v) => setTabIndex(v)}> 81 <Tab label="Specs" /> 82 <Tab label={`Reviews (${details.ratingStatistics.ratingCount})`} /> 83 </Tabs> 84 </Box> 85 86 <Box sx={{ p: 3 }}> 87 {tabIndex === 0 && ( 88 <Grid container spacing={2}> 89 <Grid item xs={12} md={8}> 90 <Table size="small"> 91 <TableBody> 92 {details.components.map((comp: any) => ( 93 <TableRow key={comp.id}> 94 <TableCell sx={{ width: 50 }}> 95 <Avatar 96 src={comp.img_url || undefined} 97 variant="rounded" 98 sx={{ width: 45, height: 45, bgcolor: '#ff8201'}} 99 > 100 {comp.type?.substring(0, 3)?.toUpperCase()} 101 </Avatar> 102 </TableCell> 103 <TableCell> 104 <Typography variant="body2" color="text.secondary" sx={{ fontSize: '0.75rem', textTransform: 'uppercase' }}> 105 {comp.type} 106 </Typography> 107 <Typography variant="body1" fontWeight="500"> 108 {comp.brand} {comp.name} 109 </Typography> 110 </TableCell> 111 <TableCell align="right" sx={{ fontWeight: 'bold', color: '#ff8201' }}> 112 {formatPrice(comp.price)} 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)} 113 146 </TableCell> 114 147 </TableRow> 115 ))} 116 <TableRow sx={{ bgcolor: '#424343' }}> 117 <TableCell colSpan={2} sx={{ fontWeight: 'bold', color: '#ff8201' }}>TOTAL</TableCell> 118 <TableCell align="right" sx={{ fontWeight: 'bold', fontSize: '1.1rem', color: 'primary.main' }}> 119 {formatPrice(details.totalPrice)} 120 </TableCell> 121 </TableRow> 122 </TableBody> 123 </Table> 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> 124 183 </Grid> 125 126 <Grid item xs={12} md={4}> 127 <Box sx={{ bgcolor: '#424343', p: 2, borderRadius: 2, mb: 2 }}> 128 <Typography color="primary.main" gutterBottom fontWeight="bold">Builder's Notes</Typography> 129 <Typography color="primary.main" variant="body2" sx={{ fontStyle: 'italic' }}> 130 "{details.description || "No notes provided."}" 131 </Typography> 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> 132 194 </Box> 133 195 134 <Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}> 135 <Button 136 variant="contained" 137 color="primary" 138 size="large" 139 startIcon={<AutoFixHighIcon />} 140 onClick={() => onClone({ buildId: details.id })} 141 > 142 Clone & Edit 143 </Button> 144 <Button 145 variant={details.isFavorite ? "contained" : "outlined"} 146 color={details.isFavorite ? "error" : "primary"} 147 startIcon={details.isFavorite ? <FavoriteIcon /> : <FavoriteBorderIcon />} 148 onClick={handleFavorite} 149 > 150 {details.isFavorite ? "Favorited" : "Add to Favorites"} 151 </Button> 152 </Box> 153 </Grid> 154 </Grid> 155 )} 156 157 {tabIndex === 1 && ( 158 <Box> 159 <Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 4, p: 2, bgcolor: '#5e5e5e', borderRadius: 2 }}> 160 <Typography variant="h3" fontWeight="bold">{details.ratingStatistics.averageRating.toFixed(1)}</Typography> 161 <Box> 162 <Rating value={details.ratingStatistics.averageRating} readOnly precision={0.5} /> 163 <Typography variant="body2" color="text.secondary">{details.ratingStatistics.ratingCount} ratings</Typography> 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 )} 164 236 </Box> 165 237 </Box> 166 167 {currentUser && ( 168 <Box sx={{ mb: 4, p: 2, border: '1px solid #ddd', borderRadius: 2 }}> 169 <Typography variant="subtitle2" gutterBottom>Your Review</Typography> 170 <Box sx={{ display: 'flex', alignItems: 'center', mb: 1 }}> 171 <Rating value={ratingVal} onChange={(_, v) => setRatingVal(v || 5)} /> 172 </Box> 173 <TextField 174 fullWidth 175 multiline 176 rows={2} 177 placeholder="Share your thoughts on this build..." 178 value={reviewText} 179 onChange={(e) => setReviewText(e.target.value)} 180 sx={{ mb: 1 }} 181 /> 182 <Button size="small" variant="contained" onClick={handleSubmitReview}> 183 Submit Review 184 </Button> 185 </Box> 186 )} 187 188 <Box sx={{ display: 'flex', flexDirection: 'column', gap: 2}}> 189 {details.reviews.map((rev: any, i: number) => ( 190 <Box key={i} sx={{ pb: 2, borderBottom: '1px solid #eee'}}> 191 <Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 0.5 }}> 192 <Typography fontWeight="bold" variant="body2">{rev.username}</Typography> 193 <Typography variant="caption" color="text.secondary">{rev.createdAt}</Typography> 194 </Box> 195 <Typography variant="body2">{rev.content}</Typography> 196 </Box> 197 ))} 198 {details.reviews.length === 0 && ( 199 <Typography color="text.secondary" align="center">No reviews yet. Be the first!</Typography> 200 )} 201 </Box> 202 </Box> 203 )} 204 </Box> 205 </DialogContent> 206 </> 207 )} 208 </Dialog> 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 </> 209 265 ); 210 266 }
Note:
See TracChangeset
for help on using the changeset viewer.
