@@ -29,6 +29,7 @@ import DeleteIcon from '@mui/icons-material/Delete';
2929import DownloadIcon from '@mui/icons-material/Download' ;
3030import UploadIcon from '@mui/icons-material/Upload' ;
3131import BlockIcon from '@mui/icons-material/Block' ;
32+ import CalendarMonthIcon from '@mui/icons-material/CalendarMonth' ;
3233import { useAuth } from '../contexts/AuthContext' ;
3334import { Navigate } from 'react-router-dom' ;
3435import { api } from '../api' ;
@@ -48,7 +49,11 @@ function AdminDashboard() {
4849 const [ invites , setInvites ] = useState ( [ ] ) ;
4950 const [ revokeDialogOpen , setRevokeDialogOpen ] = useState ( false ) ;
5051 const [ selectedInvite , setSelectedInvite ] = useState ( null ) ;
52+ const [ calendarImportDialogOpen , setCalendarImportDialogOpen ] = useState ( false ) ;
53+ const [ calendarImportData , setCalendarImportData ] = useState ( null ) ;
54+ const [ calendarImportMode , setCalendarImportMode ] = useState ( 'merge' ) ;
5155 const fileInputRef = useRef ( null ) ;
56+ const calendarFileInputRef = useRef ( null ) ;
5257
5358 useEffect ( ( ) => {
5459 loadUsers ( ) ;
@@ -199,6 +204,63 @@ function AdminDashboard() {
199204 }
200205 } ;
201206
207+ const handleCalendarExport = async ( ) => {
208+ try {
209+ const data = await api . exportEvents ( ) ;
210+ const blob = new Blob ( [ JSON . stringify ( data , null , 2 ) ] , { type : 'application/json' } ) ;
211+ const url = URL . createObjectURL ( blob ) ;
212+ const a = document . createElement ( 'a' ) ;
213+ a . href = url ;
214+ a . download = `sh-underground-calendar-${ new Date ( ) . toISOString ( ) . split ( 'T' ) [ 0 ] } .json` ;
215+ document . body . appendChild ( a ) ;
216+ a . click ( ) ;
217+ document . body . removeChild ( a ) ;
218+ URL . revokeObjectURL ( url ) ;
219+ setSuccess ( 'Calendar events exported successfully' ) ;
220+ setTimeout ( ( ) => setSuccess ( '' ) , 3000 ) ;
221+ } catch ( err ) {
222+ setError ( err . message ) ;
223+ }
224+ } ;
225+
226+ const handleCalendarImportClick = ( ) => {
227+ calendarFileInputRef . current ?. click ( ) ;
228+ } ;
229+
230+ const handleCalendarFileSelect = ( event ) => {
231+ const file = event . target . files ?. [ 0 ] ;
232+ if ( ! file ) return ;
233+ const reader = new FileReader ( ) ;
234+ reader . onload = ( e ) => {
235+ try {
236+ const data = JSON . parse ( e . target . result ) ;
237+ if ( ! Array . isArray ( data ) ) {
238+ setError ( 'Calendar import file must contain an array of events' ) ;
239+ return ;
240+ }
241+ setCalendarImportData ( data ) ;
242+ setCalendarImportDialogOpen ( true ) ;
243+ setError ( '' ) ;
244+ } catch {
245+ setError ( 'Invalid JSON file' ) ;
246+ }
247+ } ;
248+ reader . readAsText ( file ) ;
249+ event . target . value = '' ;
250+ } ;
251+
252+ const handleCalendarImportConfirm = async ( ) => {
253+ try {
254+ const result = await api . importEvents ( calendarImportData , calendarImportMode ) ;
255+ setCalendarImportDialogOpen ( false ) ;
256+ setCalendarImportData ( null ) ;
257+ setSuccess ( `Calendar import successful: ${ result . count } events total` ) ;
258+ setTimeout ( ( ) => setSuccess ( '' ) , 3000 ) ;
259+ } catch ( err ) {
260+ setError ( err . message ) ;
261+ }
262+ } ;
263+
202264 if ( ! isAdmin ) {
203265 return < Navigate to = "/profile" replace /> ;
204266 }
@@ -452,6 +514,72 @@ function AdminDashboard() {
452514 </ Button >
453515 </ DialogActions >
454516 </ Dialog >
517+
518+ { /* Calendar Backup Section */ }
519+ < Box sx = { { display : 'flex' , alignItems : 'center' , gap : 2 , mt : 4 , mb : 2 , flexWrap : 'wrap' } } >
520+ < CalendarMonthIcon color = "primary" />
521+ < Typography variant = "h6" >
522+ Calendar Backup
523+ </ Typography >
524+ < Box sx = { { flexGrow : 1 } } />
525+ < Button
526+ variant = "outlined"
527+ startIcon = { < DownloadIcon /> }
528+ onClick = { handleCalendarExport }
529+ size = "small"
530+ >
531+ Export Events
532+ </ Button >
533+ < Button
534+ variant = "outlined"
535+ startIcon = { < UploadIcon /> }
536+ onClick = { handleCalendarImportClick }
537+ size = "small"
538+ >
539+ Import Events
540+ </ Button >
541+ < input
542+ type = "file"
543+ ref = { calendarFileInputRef }
544+ onChange = { handleCalendarFileSelect }
545+ accept = ".json"
546+ style = { { display : 'none' } }
547+ />
548+ </ Box >
549+
550+ { /* Calendar Import Dialog */ }
551+ < Dialog open = { calendarImportDialogOpen } onClose = { ( ) => setCalendarImportDialogOpen ( false ) } maxWidth = "sm" fullWidth >
552+ < DialogTitle > Import Calendar Events</ DialogTitle >
553+ < DialogContent >
554+ < Typography sx = { { mb : 2 } } >
555+ Found { calendarImportData ? calendarImportData . length : 0 } events in the file.
556+ </ Typography >
557+ < FormControl component = "fieldset" >
558+ < FormLabel component = "legend" > Import Mode</ FormLabel >
559+ < RadioGroup
560+ value = { calendarImportMode }
561+ onChange = { ( e ) => setCalendarImportMode ( e . target . value ) }
562+ >
563+ < FormControlLabel
564+ value = "merge"
565+ control = { < Radio /> }
566+ label = "Merge - Add imported events alongside existing ones"
567+ />
568+ < FormControlLabel
569+ value = "replace"
570+ control = { < Radio /> }
571+ label = "Replace - Delete all existing events and replace with file data"
572+ />
573+ </ RadioGroup >
574+ </ FormControl >
575+ </ DialogContent >
576+ < DialogActions >
577+ < Button onClick = { ( ) => setCalendarImportDialogOpen ( false ) } > Cancel</ Button >
578+ < Button onClick = { handleCalendarImportConfirm } variant = "contained" >
579+ Import
580+ </ Button >
581+ </ DialogActions >
582+ </ Dialog >
455583 </ Box >
456584 ) ;
457585}
0 commit comments