Skip to content

Commit c6192fc

Browse files
authored
fix: /admin/[id] (#110)
1 parent 4f7cfe8 commit c6192fc

File tree

2 files changed

+347
-128
lines changed

2 files changed

+347
-128
lines changed

src/pages/admin/[id]/index.tsx

+344
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,344 @@
1+
import { useEffect, useState, MouseEvent } from 'react';
2+
import { useRouter} from 'next/router';
3+
import styled from 'styled-components';
4+
import {
5+
CssBaseline,
6+
Container,
7+
Box,
8+
AppBar,
9+
Toolbar,
10+
Menu,
11+
MenuItem,
12+
Divider,
13+
Typography,
14+
Button,
15+
IconButton,
16+
FormControl,
17+
InputLabel,
18+
TextField,
19+
Select,
20+
Dialog,
21+
DialogActions,
22+
DialogContent,
23+
DialogTitle,
24+
} from '@mui/material';
25+
import { makeStyles } from '@mui/styles'
26+
import AddIcon from '@mui/icons-material/Add';
27+
import WidgetsIcon from '@mui/icons-material/Widgets';
28+
import AccountCircleIcon from '@mui/icons-material/AccountCircle';
29+
import { User } from '@firebase/auth';
30+
import { ref, set, onValue, DataSnapshot } from '@firebase/database';
31+
32+
import { AuthProvider } from '@/lib/AuthProvider';
33+
import { auth, db } from '@/lib/firebase';
34+
import { Signin } from '@/components/admin/signin';
35+
import { TextWidgetEditor } from '@/components/TextWidget';
36+
import { TimeWidgetEditor } from '@/components/TimeWidget';
37+
import { IFrameWidgetEditor } from '@/components/IFrameWidget';
38+
39+
const Editors = {
40+
text: TextWidgetEditor,
41+
time: TimeWidgetEditor,
42+
iframe: IFrameWidgetEditor,
43+
};
44+
45+
const useStyles = makeStyles((theme) => ({
46+
root: {
47+
display: 'flex',
48+
flexDirection: 'column',
49+
width: '100%',
50+
height: '100vh',
51+
overflow: 'hidden',
52+
},
53+
content: {
54+
flex: 1,
55+
overflow: 'auto',
56+
},
57+
title: {
58+
flexGrow: 1,
59+
},
60+
profile: {
61+
flexGrow: 1
62+
},
63+
}));
64+
65+
type Widget = {
66+
name: string;
67+
props: any;
68+
}
69+
70+
type WidgetList = { [key: string]: Widget }
71+
72+
const Widgets = ({ profile }: { profile: string }) => {
73+
const [widgets, setWidgets] = useState<WidgetList>({});
74+
75+
useEffect(() => {
76+
const widgetsRef = ref(db, `/profiles/${profile}/widgets`);
77+
onValue(widgetsRef, (snap: DataSnapshot) => {
78+
setWidgets(snap.val() || {});
79+
});
80+
}, [profile]);
81+
82+
return (
83+
<div>
84+
{
85+
Object.keys(widgets).map((id) => {
86+
const widget: any = widgets[id];
87+
const Editor = Editors[widget.name];
88+
return <Editor key={`${profile}-${id}`} id={id} props={widget.props} profile={profile} />
89+
})
90+
}
91+
</div>
92+
);
93+
};
94+
95+
const AddWidgetDialog = ({ profile, open, onClose }: { profile: string, open: boolean, onClose: () => void }) => {
96+
const [widgetId, setWidgetId] = useState("");
97+
const [widgetType, setWidgetType] = useState("text");
98+
99+
const FormGroup = styled.div`
100+
display: flex;
101+
margin-bottom: 1rem;
102+
& > div {
103+
flex-grow: 1;
104+
margin-left: 0.25rem;
105+
}
106+
`;
107+
108+
const style = {
109+
display: 'flex',
110+
flexDirection: 'column',
111+
position: 'absolute' as 'absolute',
112+
top: '50%',
113+
left: '50%',
114+
transform: 'translate(-50%, -50%)',
115+
width: 640,
116+
bgcolor: 'background.paper',
117+
border: '1px solid #ddd',
118+
borderRadius: '4px',
119+
boxShadow: 24,
120+
pt: 4,
121+
px: 4,
122+
pb: 3,
123+
};
124+
125+
return (
126+
<Dialog
127+
open={open}
128+
onClose={onClose}
129+
>
130+
<DialogTitle>Add Widget</DialogTitle>
131+
<DialogContent>
132+
<FormGroup>
133+
<FormControl variant="standard">
134+
<TextField autoFocus fullWidth label="ID" value={widgetId} variant="standard" onChange={(e) => { setWidgetId(e.target.value); }}/>
135+
</FormControl>
136+
</FormGroup>
137+
138+
<FormGroup>
139+
<FormControl variant="standard">
140+
<InputLabel id="widget-type-label">Widget</InputLabel>
141+
<Select
142+
labelId="widget-type-label"
143+
id="widget-type"
144+
value={widgetType}
145+
label="Widget"
146+
onChange={(e) => { setWidgetType(e.target.value); }}
147+
>
148+
<MenuItem value={"text"}>Text</MenuItem>
149+
<MenuItem value={"time"}>Time</MenuItem>
150+
<MenuItem value={"iframe"}>IFrame</MenuItem>
151+
</Select>
152+
</FormControl>
153+
</FormGroup>
154+
</DialogContent>
155+
<DialogActions>
156+
<Button color="primary" variant="contained" onClick={() => {
157+
set(ref(db, `/profiles/${profile}/widgets/${widgetId}`), {
158+
name: widgetType,
159+
props: Editors[widgetType].defaultProps
160+
});
161+
162+
setWidgetId("");
163+
setWidgetType("text");
164+
onClose();
165+
}}>Add</Button>
166+
</DialogActions>
167+
</Dialog>
168+
);
169+
};
170+
171+
const AddProfileDialog = ({ open, onClose }: { open: boolean, onClose: () => void}) => {
172+
const [profileId, setProfileId] = useState("");
173+
174+
return (
175+
<Dialog open={open} onClose={onClose}>
176+
<DialogTitle>Add Profile</DialogTitle>
177+
<DialogContent>
178+
<FormControl variant="standard">
179+
<TextField required autoFocus fullWidth label="ID" value={profileId} variant="standard" onChange={(e) => { setProfileId(e.target.value); }} />
180+
</FormControl>
181+
</DialogContent>
182+
<DialogActions>
183+
<Button color="primary" variant="contained" onClick={() =>{
184+
if (profileId.length > 0) {
185+
set(ref(db, `/profiles/${profileId}/name`), profileId);
186+
setProfileId("");
187+
onClose();
188+
}
189+
}}>Add</Button>
190+
</DialogActions>
191+
</Dialog>
192+
);
193+
};
194+
195+
const AdminIndexPage = () => {
196+
const classes = useStyles();
197+
const router = useRouter();
198+
const [currentUser, setCurrentUser] = useState<User | null>(null);
199+
const [userAnchorEl, setUserAnchorEl] = useState<HTMLElement | null>(null);
200+
const [profileAnchorEl, setProfileAnchorEl] = useState<HTMLElement | null>(null);
201+
const [addProfileDialogOpened, setAddProfileDialogOpened] = useState(false);
202+
const [addWidgetDialogOpened, setAddWidgetDialogOpened] = useState(false);
203+
const [profiles, setProfiles] = useState<string[]>([]);
204+
205+
const currentProfile = router.query.id as string;
206+
207+
const isUserMenuOpen = Boolean(userAnchorEl);
208+
const isProfileMenuOpen = Boolean(profileAnchorEl);
209+
210+
useEffect(() => {
211+
auth.onAuthStateChanged((user) => {
212+
setCurrentUser(user);
213+
});
214+
});
215+
216+
useEffect(() => {
217+
const profilesRef = ref(db, `/profiles`);
218+
onValue(profilesRef, (snap: DataSnapshot) => {
219+
if (snap?.val()) {
220+
setProfiles(Object.keys(snap.val()));
221+
}
222+
});
223+
}, []);
224+
225+
const signout = async () => {
226+
setUserAnchorEl(null);
227+
setProfileAnchorEl(null);
228+
try {
229+
await auth.signOut();
230+
} catch (err) {
231+
alert(err.message);
232+
}
233+
};
234+
235+
const userMenuId = 'user-menu';
236+
const handleUserMenuOpen = (event: MouseEvent<HTMLElement>) => {
237+
setUserAnchorEl(event.currentTarget);
238+
};
239+
const handleUserMenuClose = () => {
240+
setUserAnchorEl(null);
241+
};
242+
243+
const profileMenuId = 'profile-menu';
244+
const handleProfileMenuOpen = (event: MouseEvent<HTMLElement>) => {
245+
setProfileAnchorEl(event.currentTarget);
246+
};
247+
const handleProfileMenuClose = () => {
248+
setProfileAnchorEl(null);
249+
};
250+
251+
return currentUser !== null ? (
252+
<AuthProvider>
253+
<CssBaseline />
254+
<div className={classes.root}>
255+
<AppBar position="static">
256+
<Toolbar>
257+
<Typography variant="h6" className={classes.title}>
258+
Admin
259+
</Typography>
260+
<Typography variant="h6" className={classes.title}>
261+
Profile:{' '}
262+
{currentProfile}
263+
</Typography>
264+
<Box sx={{ flexGrow: 1 }} />
265+
<Box sx={{ display: { xs: 'none', md: 'flex' } }}>
266+
<IconButton
267+
size="large"
268+
color="inherit"
269+
edge="end"
270+
aria-controls={profileMenuId}
271+
aria-haspopup="true"
272+
aria-expanded={isProfileMenuOpen ? 'true' : undefined}
273+
onClick={handleProfileMenuOpen}
274+
>
275+
<WidgetsIcon />
276+
</IconButton>
277+
<IconButton
278+
size="large"
279+
color="inherit"
280+
onClick={()=>{setAddWidgetDialogOpened(true);}}
281+
>
282+
<AddIcon />
283+
</IconButton>
284+
<IconButton
285+
size="large"
286+
color="inherit"
287+
edge="end"
288+
aria-controls={userMenuId}
289+
aria-haspopup="true"
290+
aria-expanded={isUserMenuOpen ? 'true' : undefined}
291+
onClick={handleUserMenuOpen}
292+
>
293+
<AccountCircleIcon />
294+
</IconButton>
295+
</Box>
296+
</Toolbar>
297+
</AppBar>
298+
<Menu
299+
id={profileMenuId}
300+
anchorEl={profileAnchorEl}
301+
open={isProfileMenuOpen}
302+
onClose={handleProfileMenuClose}
303+
>
304+
{profiles.map((profile) => (
305+
<MenuItem key={profile} color="inherit" onClick={() => { router.push(`/admin/${profile}`); }}>{profile}</MenuItem>
306+
))}
307+
<Divider />
308+
<MenuItem color="inherit" onClick={() => { setAddProfileDialogOpened(true);}}>Add</MenuItem>
309+
</Menu>
310+
<Menu
311+
id={userMenuId}
312+
anchorEl={userAnchorEl}
313+
open={isUserMenuOpen}
314+
onClose={handleUserMenuClose}
315+
>
316+
<MenuItem color="inherit" onClick={signout}>Logout</MenuItem>
317+
</Menu>
318+
<AddProfileDialog
319+
open={addProfileDialogOpened}
320+
onClose={() => {
321+
setAddProfileDialogOpened(false);
322+
}}
323+
/>
324+
<AddWidgetDialog
325+
profile={currentProfile}
326+
open={addWidgetDialogOpened}
327+
onClose={() => {
328+
setAddWidgetDialogOpened(false);
329+
}}
330+
/>
331+
332+
<Container className={classes.content}>
333+
<Box my={4}>
334+
<Widgets profile={currentProfile} />
335+
</Box>
336+
</Container>
337+
</div>
338+
</AuthProvider>
339+
) : (
340+
<Signin />
341+
);
342+
};
343+
344+
export default AdminIndexPage;

0 commit comments

Comments
 (0)