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