| 1 | import React, {useEffect, useState} from 'react';
|
|---|
| 2 | import {
|
|---|
| 3 | Container,
|
|---|
| 4 | Paper,
|
|---|
| 5 | Typography,
|
|---|
| 6 | Box,
|
|---|
| 7 | Avatar,
|
|---|
| 8 | Divider,
|
|---|
| 9 | Button,
|
|---|
| 10 | CircularProgress,
|
|---|
| 11 | IconButton,
|
|---|
| 12 | Dialog,
|
|---|
| 13 | DialogTitle,
|
|---|
| 14 | DialogContent,
|
|---|
| 15 | DialogActions
|
|---|
| 16 | } from '@mui/material';
|
|---|
| 17 | import CloseIcon from '@mui/icons-material/Close';
|
|---|
| 18 | import DeleteIcon from '@mui/icons-material/Delete';
|
|---|
| 19 |
|
|---|
| 20 | import PersonIcon from '@mui/icons-material/Person';
|
|---|
| 21 | import ComputerIcon from '@mui/icons-material/Computer';
|
|---|
| 22 | import FavoriteIcon from '@mui/icons-material/Favorite';
|
|---|
| 23 |
|
|---|
| 24 | import {getUserInfoAndData, onDeleteBuild} from "./userDashboard.telefunc";
|
|---|
| 25 |
|
|---|
| 26 | import BuildCard from "../../../components/BuildCard";
|
|---|
| 27 | import BuildDetailsDialog from "../../../components/BuildDetailsDialog";
|
|---|
| 28 |
|
|---|
| 29 | type DashboardData = {
|
|---|
| 30 | user: { username: string; email?: string; [key: string]: any };
|
|---|
| 31 | userBuilds: any[];
|
|---|
| 32 | favoriteBuilds: any[];
|
|---|
| 33 | };
|
|---|
| 34 |
|
|---|
| 35 | export default function UserDashboard() {
|
|---|
| 36 | const [data, setData] = useState<DashboardData | null>(null);
|
|---|
| 37 | const [loading, setLoading] = useState(true);
|
|---|
| 38 | const [error, setError] = useState<string | null>(null);
|
|---|
| 39 | const [openMyBuildsDialog, setOpenMyBuildsDialog] = useState(false);
|
|---|
| 40 | const [openFavoritesDialog, setOpenFavoritesDialog] = useState(false);
|
|---|
| 41 | const [selectedBuildId, setSelectedBuildId] = useState<number | null>(null);
|
|---|
| 42 | const [deleteDialog, setDeleteDialog] = useState<{ open: boolean, buildId: number | null, buildName: string }>({ open: false, buildId: null, buildName: '' });
|
|---|
| 43 | const [deleteLoading, setDeleteLoading] = useState(false);
|
|---|
| 44 |
|
|---|
| 45 | const loadData = () => {
|
|---|
| 46 | getUserInfoAndData()
|
|---|
| 47 | .then((result) => {
|
|---|
| 48 | setData(result);
|
|---|
| 49 | setLoading(false);
|
|---|
| 50 | })
|
|---|
| 51 | .catch((err) => {
|
|---|
| 52 | console.error(err);
|
|---|
| 53 | setError("Failed to load dashboard. Please log in.");
|
|---|
| 54 | setLoading(false);
|
|---|
| 55 | });
|
|---|
| 56 | };
|
|---|
| 57 |
|
|---|
| 58 | useEffect(() => {
|
|---|
| 59 | loadData();
|
|---|
| 60 | }, []);
|
|---|
| 61 |
|
|---|
| 62 | const openDeleteDialog = (buildId: number, buildName: string) => {
|
|---|
| 63 | setDeleteDialog({ open: true, buildId, buildName });
|
|---|
| 64 | };
|
|---|
| 65 |
|
|---|
| 66 | const handleDelete = async () => {
|
|---|
| 67 | if (!deleteDialog.buildId) return;
|
|---|
| 68 | setDeleteLoading(true);
|
|---|
| 69 | try {
|
|---|
| 70 | await onDeleteBuild({buildId: deleteDialog.buildId});
|
|---|
| 71 | setDeleteDialog({ open: false, buildId: null, buildName: '' });
|
|---|
| 72 | loadData();
|
|---|
| 73 | setSelectedBuildId(null);
|
|---|
| 74 | } catch (e) {
|
|---|
| 75 | alert("Failed to delete build");
|
|---|
| 76 | } finally {
|
|---|
| 77 | setDeleteLoading(false);
|
|---|
| 78 | }
|
|---|
| 79 | };
|
|---|
| 80 |
|
|---|
| 81 | if (loading) return <Box sx={{display: 'flex', justifyContent: 'center', mt: 10}}><CircularProgress/></Box>;
|
|---|
| 82 | if (error || !data) return <Container sx={{mt: 5, textAlign: 'center'}}><Typography color="error" variant="h6">{error}</Typography><Button href="/auth/login" variant="contained">Login</Button></Container>;
|
|---|
| 83 |
|
|---|
| 84 | return (
|
|---|
| 85 | <Container maxWidth="xl" sx={{mb: 4, color: 'white'}}>
|
|---|
| 86 | <Box sx={{
|
|---|
| 87 | display: 'grid',
|
|---|
| 88 | gridTemplateColumns: {
|
|---|
| 89 | xs: '1fr',
|
|---|
| 90 | md: '280px 1fr'
|
|---|
| 91 | },
|
|---|
| 92 | gap: 2
|
|---|
| 93 | }}>
|
|---|
| 94 | <Box>
|
|---|
| 95 | <Paper elevation={3} sx={{p: 3, display: 'flex', flexDirection: 'column', alignItems: 'center', height: '100%'}}>
|
|---|
| 96 | <Avatar sx={{width: 100, height: 100, mb: 2, bgcolor: 'primary.main'}}>
|
|---|
| 97 | <PersonIcon sx={{fontSize: 60}}/>
|
|---|
| 98 | </Avatar>
|
|---|
| 99 | <Typography variant="h5" fontWeight="bold" gutterBottom>{data.user.username}</Typography>
|
|---|
| 100 | <Typography variant="body2" color="text.secondary" gutterBottom>{data.user.email}</Typography>
|
|---|
| 101 | <Divider sx={{width: '100%', my: 2}}/>
|
|---|
| 102 | </Paper>
|
|---|
| 103 | </Box>
|
|---|
| 104 |
|
|---|
| 105 | <Box sx={{
|
|---|
| 106 | display: 'flex',
|
|---|
| 107 | flexDirection: 'column',
|
|---|
| 108 | gap: 3
|
|---|
| 109 | }}>
|
|---|
| 110 | <Paper elevation={3} sx={{p: 3, minHeight: '300px', bgcolor: '#1e1e1e'}}>
|
|---|
| 111 | <Box sx={{
|
|---|
| 112 | display: 'flex',
|
|---|
| 113 | alignItems: 'center',
|
|---|
| 114 | justifyContent: 'space-between',
|
|---|
| 115 | mb: 2
|
|---|
| 116 | }}>
|
|---|
| 117 | <Box sx={{display: 'flex', alignItems: 'center'}}>
|
|---|
| 118 | <FavoriteIcon color="error" sx={{mr: 1}}/>
|
|---|
| 119 | <Typography variant="h6" fontWeight="bold">Favorited Builds</Typography>
|
|---|
| 120 | </Box>
|
|---|
| 121 |
|
|---|
| 122 | {data.favoriteBuilds.length > 6 && (
|
|---|
| 123 | <Button
|
|---|
| 124 | variant="outlined"
|
|---|
| 125 | size="small"
|
|---|
| 126 | onClick={() => setOpenFavoritesDialog(true)}
|
|---|
| 127 | >
|
|---|
| 128 | See All ({data.favoriteBuilds.length})
|
|---|
| 129 | </Button>
|
|---|
| 130 | )}
|
|---|
| 131 | </Box>
|
|---|
| 132 | <Divider sx={{mb: 2}}/>
|
|---|
| 133 |
|
|---|
| 134 | {data.favoriteBuilds.length === 0 ? (
|
|---|
| 135 | <Typography color="text.secondary" align="center" sx={{mt: 4}}>
|
|---|
| 136 | You haven't favorited any builds yet.
|
|---|
| 137 | </Typography>
|
|---|
| 138 | ) : (
|
|---|
| 139 | <Box sx={{
|
|---|
| 140 | display: 'grid',
|
|---|
| 141 | gridTemplateColumns: {
|
|---|
| 142 | xs: '1fr',
|
|---|
| 143 | sm: 'repeat(2, 1fr)',
|
|---|
| 144 | md: 'repeat(3, 1fr)',
|
|---|
| 145 | lg: 'repeat(4, 1fr)',
|
|---|
| 146 | xl: 'repeat(6, 1fr)'
|
|---|
| 147 | },
|
|---|
| 148 | gap: 2
|
|---|
| 149 | }}>
|
|---|
| 150 | {data.favoriteBuilds.slice(0, 6).map((build: any) => (
|
|---|
| 151 | <BuildCard
|
|---|
| 152 | key={build.id}
|
|---|
| 153 | build={build}
|
|---|
| 154 | onClick={() => setSelectedBuildId(build.id)}
|
|---|
| 155 | />
|
|---|
| 156 | ))}
|
|---|
| 157 | </Box>
|
|---|
| 158 | )}
|
|---|
| 159 | </Paper>
|
|---|
| 160 |
|
|---|
| 161 | <Paper elevation={3} sx={{p: 3, minHeight: '300px', bgcolor: '#1e1e1e'}}>
|
|---|
| 162 | <Box sx={{
|
|---|
| 163 | display: 'flex',
|
|---|
| 164 | alignItems: 'center',
|
|---|
| 165 | justifyContent: 'space-between',
|
|---|
| 166 | mb: 2
|
|---|
| 167 | }}>
|
|---|
| 168 | <Box sx={{display: 'flex', alignItems: 'center'}}>
|
|---|
| 169 | <ComputerIcon color="primary" sx={{mr: 1}}/>
|
|---|
| 170 | <Typography variant="h6" fontWeight="bold">My Builds</Typography>
|
|---|
| 171 | </Box>
|
|---|
| 172 |
|
|---|
| 173 | {data.userBuilds.length > 6 && (
|
|---|
| 174 | <Button
|
|---|
| 175 | variant="outlined"
|
|---|
| 176 | size="small"
|
|---|
| 177 | onClick={() => setOpenMyBuildsDialog(true)}
|
|---|
| 178 | >
|
|---|
| 179 | See All ({data.userBuilds.length})
|
|---|
| 180 | </Button>
|
|---|
| 181 | )}
|
|---|
| 182 | </Box>
|
|---|
| 183 | <Divider sx={{mb: 2}}/>
|
|---|
| 184 |
|
|---|
| 185 | {data.userBuilds.length === 0 ? (
|
|---|
| 186 | <Box sx={{textAlign: 'center', mt: 4}}>
|
|---|
| 187 | <Typography color="text.secondary" gutterBottom>
|
|---|
| 188 | You haven't created any builds yet.
|
|---|
| 189 | </Typography>
|
|---|
| 190 | <Button variant="contained" href="/forge" sx={{color: 'white', fontWeight: 'bolder'}}>
|
|---|
| 191 | Start Forging
|
|---|
| 192 | </Button>
|
|---|
| 193 | </Box>
|
|---|
| 194 | ) : (
|
|---|
| 195 | <Box sx={{
|
|---|
| 196 | display: 'grid',
|
|---|
| 197 | gridTemplateColumns: {
|
|---|
| 198 | xs: '1fr',
|
|---|
| 199 | sm: 'repeat(2, 1fr)',
|
|---|
| 200 | md: 'repeat(3, 1fr)',
|
|---|
| 201 | lg: 'repeat(4, 1fr)',
|
|---|
| 202 | xl: 'repeat(6, 1fr)'
|
|---|
| 203 | },
|
|---|
| 204 | gap: 2
|
|---|
| 205 | }}>
|
|---|
| 206 | {data.userBuilds.slice(0, 6).map((build: any) => (
|
|---|
| 207 | <Box key={build.id} sx={{ display: 'flex', flexDirection: 'column', pb: 2 }}>
|
|---|
| 208 | <Box sx={{ flexGrow: 1 }}>
|
|---|
| 209 | <BuildCard
|
|---|
| 210 | build={build}
|
|---|
| 211 | onClick={() => setSelectedBuildId(build.id)}
|
|---|
| 212 | />
|
|---|
| 213 | </Box>
|
|---|
| 214 | <Button
|
|---|
| 215 | variant="outlined"
|
|---|
| 216 | color="error"
|
|---|
| 217 | size="small"
|
|---|
| 218 | startIcon={<DeleteIcon/>}
|
|---|
| 219 | onClick={(e) => {
|
|---|
| 220 | e.stopPropagation();
|
|---|
| 221 | openDeleteDialog(build.id, build.name);
|
|---|
| 222 | }}
|
|---|
| 223 | sx={{ mt: 1, width: '100%' }}
|
|---|
| 224 | >
|
|---|
| 225 | Delete
|
|---|
| 226 | </Button>
|
|---|
| 227 | </Box>
|
|---|
| 228 | ))}
|
|---|
| 229 | </Box>
|
|---|
| 230 | )}
|
|---|
| 231 | </Paper>
|
|---|
| 232 | </Box>
|
|---|
| 233 | </Box>
|
|---|
| 234 |
|
|---|
| 235 | <Dialog open={openMyBuildsDialog} onClose={() => setOpenMyBuildsDialog(false)} maxWidth="xl" fullWidth>
|
|---|
| 236 | <DialogTitle sx={{display: 'flex', justifyContent: 'space-between', alignItems: 'center'}}>
|
|---|
| 237 | All My Builds
|
|---|
| 238 | <IconButton onClick={() => setOpenMyBuildsDialog(false)}>
|
|---|
| 239 | <CloseIcon/>
|
|---|
| 240 | </IconButton>
|
|---|
| 241 | </DialogTitle>
|
|---|
| 242 | <DialogContent dividers>
|
|---|
| 243 | <Box sx={{
|
|---|
| 244 | display: 'grid',
|
|---|
| 245 | gridTemplateColumns: {
|
|---|
| 246 | xs: '1fr',
|
|---|
| 247 | sm: 'repeat(2, 1fr)',
|
|---|
| 248 | md: 'repeat(3, 1fr)',
|
|---|
| 249 | lg: 'repeat(4, 1fr)',
|
|---|
| 250 | xl: 'repeat(5, 1fr)'
|
|---|
| 251 | },
|
|---|
| 252 | gap: 2,
|
|---|
| 253 | mt: 1,
|
|---|
| 254 | }}>
|
|---|
| 255 | {data.userBuilds.map((build: any) => (
|
|---|
| 256 | <Box key={build.id} sx={{ pb: 4 }}>
|
|---|
| 257 | <BuildCard
|
|---|
| 258 | build={build}
|
|---|
| 259 | onClick={() => {
|
|---|
| 260 | setOpenMyBuildsDialog(false);
|
|---|
| 261 | setSelectedBuildId(build.id);
|
|---|
| 262 | }}
|
|---|
| 263 | />
|
|---|
| 264 | <Box sx={{mt: 1, display: 'flex', justifyContent: 'center'}}>
|
|---|
| 265 | <Button
|
|---|
| 266 | variant="outlined"
|
|---|
| 267 | color="error"
|
|---|
| 268 | size="small"
|
|---|
| 269 | startIcon={<DeleteIcon/>}
|
|---|
| 270 | onClick={(e) => {
|
|---|
| 271 | e.stopPropagation();
|
|---|
| 272 | openDeleteDialog(build.id, build.name);
|
|---|
| 273 | }}
|
|---|
| 274 | sx={{width: '100%'}}
|
|---|
| 275 | >
|
|---|
| 276 | Delete
|
|---|
| 277 | </Button>
|
|---|
| 278 | </Box>
|
|---|
| 279 | </Box>
|
|---|
| 280 | ))}
|
|---|
| 281 | </Box>
|
|---|
| 282 | </DialogContent>
|
|---|
| 283 | <DialogActions>
|
|---|
| 284 | <Button onClick={() => setOpenMyBuildsDialog(false)}>Close</Button>
|
|---|
| 285 | </DialogActions>
|
|---|
| 286 | </Dialog>
|
|---|
| 287 |
|
|---|
| 288 | <Dialog open={openFavoritesDialog} onClose={() => setOpenFavoritesDialog(false)} maxWidth="xl" fullWidth>
|
|---|
| 289 | <DialogTitle sx={{display: 'flex', justifyContent: 'space-between', alignItems: 'center'}}>
|
|---|
| 290 | All Favorited Builds
|
|---|
| 291 | <IconButton onClick={() => setOpenFavoritesDialog(false)}>
|
|---|
| 292 | <CloseIcon/>
|
|---|
| 293 | </IconButton>
|
|---|
| 294 | </DialogTitle>
|
|---|
| 295 | <DialogContent dividers>
|
|---|
| 296 | <Box sx={{
|
|---|
| 297 | display: 'grid',
|
|---|
| 298 | gridTemplateColumns: {
|
|---|
| 299 | xs: '1fr',
|
|---|
| 300 | sm: 'repeat(2, 1fr)',
|
|---|
| 301 | md: 'repeat(3, 1fr)',
|
|---|
| 302 | lg: 'repeat(4, 1fr)',
|
|---|
| 303 | xl: 'repeat(5, 1fr)'
|
|---|
| 304 | },
|
|---|
| 305 | gap: 2,
|
|---|
| 306 | mt: 1
|
|---|
| 307 | }}>
|
|---|
| 308 | {data.favoriteBuilds.map((build: any) => (
|
|---|
| 309 | <BuildCard
|
|---|
| 310 | key={build.id}
|
|---|
| 311 | build={build}
|
|---|
| 312 | onClick={() => {
|
|---|
| 313 | setOpenFavoritesDialog(false);
|
|---|
| 314 | setSelectedBuildId(build.id);
|
|---|
| 315 | }}
|
|---|
| 316 | />
|
|---|
| 317 | ))}
|
|---|
| 318 | </Box>
|
|---|
| 319 | </DialogContent>
|
|---|
| 320 | <DialogActions>
|
|---|
| 321 | <Button onClick={() => setOpenFavoritesDialog(false)}>Close</Button>
|
|---|
| 322 | </DialogActions>
|
|---|
| 323 | </Dialog>
|
|---|
| 324 |
|
|---|
| 325 | <Dialog open={deleteDialog.open} onClose={() => setDeleteDialog({ open: false, buildId: null, buildName: '' })}>
|
|---|
| 326 | <DialogTitle>Delete Build</DialogTitle>
|
|---|
| 327 | <DialogContent>
|
|---|
| 328 | <Typography variant="h6" gutterBottom>{deleteDialog.buildName}</Typography>
|
|---|
| 329 | <Typography color="text.secondary">
|
|---|
| 330 | Are you sure you want to permanently delete this build? This action cannot be undone.
|
|---|
| 331 | </Typography>
|
|---|
| 332 | </DialogContent>
|
|---|
| 333 | <DialogActions>
|
|---|
| 334 | <Button
|
|---|
| 335 | onClick={() => setDeleteDialog({ open: false, buildId: null, buildName: '' })}
|
|---|
| 336 | disabled={deleteLoading}
|
|---|
| 337 | >
|
|---|
| 338 | Cancel
|
|---|
| 339 | </Button>
|
|---|
| 340 | <Button
|
|---|
| 341 | onClick={handleDelete}
|
|---|
| 342 | variant="contained"
|
|---|
| 343 | color="error"
|
|---|
| 344 | disabled={deleteLoading}
|
|---|
| 345 | startIcon={deleteLoading ? <CircularProgress size={20} /> : <DeleteIcon />}
|
|---|
| 346 | >
|
|---|
| 347 | {deleteLoading ? 'Deleting...' : 'Delete Build'}
|
|---|
| 348 | </Button>
|
|---|
| 349 | </DialogActions>
|
|---|
| 350 | </Dialog>
|
|---|
| 351 |
|
|---|
| 352 | <BuildDetailsDialog
|
|---|
| 353 | open={!!selectedBuildId}
|
|---|
| 354 | buildId={selectedBuildId}
|
|---|
| 355 | currentUser={data?.user?.id ? Number(data.user.id) : undefined}
|
|---|
| 356 | onClose={() => {
|
|---|
| 357 | setSelectedBuildId(null);
|
|---|
| 358 | loadData();
|
|---|
| 359 | }}
|
|---|
| 360 | isDashboardView={true}
|
|---|
| 361 | />
|
|---|
| 362 | </Container>
|
|---|
| 363 | );
|
|---|
| 364 | }
|
|---|