Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Convert to a Next + Redux app #35

Draft
wants to merge 39 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
8f53cc6
Convert to a Next + Redux app
JacksonMeade Jan 30, 2024
45caf00
Add bin reducers
JacksonMeade Jan 30, 2024
8bebc35
Add flowsheet API service back and Artist Avatar
JacksonMeade Jan 30, 2024
2442df1
Login page complete!
JacksonMeade Jan 30, 2024
4ae4e75
Simplify Catalog Search Table
JacksonMeade Jan 31, 2024
29e3224
Return Card Catalog to function
JacksonMeade Jan 31, 2024
3808032
Import types from ORM
JacksonMeade Jan 31, 2024
464a4ca
Add backend types from Adrian to our library
JacksonMeade Jan 31, 2024
100c92c
Add mapping for rotation retrieval
JacksonMeade Jan 31, 2024
0f149fa
Maintain authorization with backend middleware
JacksonMeade Jan 31, 2024
e06e179
add view style guard
JacksonMeade Feb 1, 2024
0442475
Add classic view and enable catalog search
JacksonMeade Feb 2, 2024
cdc449a
Fix Catalog Classic UI bugs
JacksonMeade Feb 2, 2024
6f12bc4
Add access token auth
JacksonMeade Feb 3, 2024
aa52a6e
Return Flowsheet Search to function
JacksonMeade Feb 3, 2024
cdffcf5
Update readme and allow rotation searching
JacksonMeade Feb 3, 2024
e63b5fa
Finish add to flowsheet search in tsx
JacksonMeade Feb 3, 2024
d38e44a
Add previews for songs back to flowsheet in tsx
JacksonMeade Feb 3, 2024
2dc5028
Add mouse position tracker for dragged and dropped entries
JacksonMeade Feb 3, 2024
2eeb2c3
Add back public favicons and correct rotation color selector
JacksonMeade Feb 3, 2024
0348cec
Eliminate errors from flowsheet entry components
JacksonMeade Feb 3, 2024
523935c
Add Guard against invalid admin access
JacksonMeade Feb 3, 2024
a5b3359
Add initial flowsheet retrieval
JacksonMeade Feb 6, 2024
c116824
Return classic flowsheet page to function
JacksonMeade Feb 6, 2024
222a655
Add classic flowsheet font resizing
JacksonMeade Feb 6, 2024
9c04937
Solve small UI bug
JacksonMeade Feb 6, 2024
b7db8bc
Add isLive functionality
JacksonMeade Feb 6, 2024
4235baf
Require pass down of state on local search
JacksonMeade Feb 6, 2024
cf6a269
Add admin retrieval of DJs
JacksonMeade Feb 7, 2024
79f1368
Merge branch 'main' into dev
JacksonMeade May 27, 2024
578792f
Add administrative list users interface
JacksonMeade May 27, 2024
d8359b3
Add separate rotation page
JacksonMeade May 28, 2024
3d6c19c
Introduce bare-bones calendar code
JacksonMeade May 28, 2024
659385b
Add add dj flow to station management
JacksonMeade May 28, 2024
1375ec1
Split add/remove from bin/queue buttons as their own components
JacksonMeade May 30, 2024
6d7c99d
Catalog adding and removal enabled
JacksonMeade Jun 1, 2024
7fb80e0
Add autocomplete to catalog editing
JacksonMeade Jun 2, 2024
4b3324b
Push add to catalog functionality
JacksonMeade Jun 18, 2024
1105b25
chore: clear up some imports in model types
JacksonMeade Jun 19, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
155 changes: 147 additions & 8 deletions app/dashboard/admin/page.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,151 @@
import AdminGuard from "@/app/components/Admin/AdminGuard";
'use client';
import React, { useEffect, useState } from "react";
import { Box, Button, ColorPaletteProp, Link, Tab, TabList, TabPanel, Tabs, Typography } from "@mui/joy";
import { tabClasses } from "@mui/joy";
import OpenInNewIcon from '@mui/icons-material/OpenInNew';
import { toast } from "sonner";
import { getAuthenticatedUser, useSelector } from "@/lib/redux";

/**
* Depicts the station management page from a station manager perspective.
* @page
* @category Station Management
*
* @param {Object} props - The component props.
* @param {string} [props.style] - The style of the page. Defaults to the success color.
*
* @returns {JSX.Element} The rendered StationManagementPage component.
* @example
* return (
* <StationManagementPage />
* )
*/
const StationManagementPage = (props: { style: ColorPaletteProp }) => {

const user = useSelector(getAuthenticatedUser);

const [djs, setDjs] = useState([]);
const [loading, setLoading] = useState(true);

const updateDjs = async () => {
setLoading(true);
listUsers().then((data: any) => {
setDjs(data);
setLoading(false);
});
};

useEffect(() => {
updateDjs();
}, []);

const AdminPage = () => {
return (
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you fix the indentation, please?

<div>
<AdminGuard />
<h1>Admin Page</h1>
</div>
<>
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-end',
my: 1,
gap: 1,
flexWrap: 'wrap',
'& > *': {
minWidth: 'clamp(0px, (500px - 100%) * 999, 100%)',
flexGrow: 1,
},
}}
>
<Typography level="h1">
Station Management
</Typography>
<Box sx = {{ justifyContent: 'flex-end', display: 'flex' }}>
<Link
href="https://wxyc.awsapps.com/start#/"
target="_blank"
sx = {{
'&:hover': {
textDecoration: 'none',
},
}}
>
<Button variant="outlined" color={props.style ?? 'success'} endDecorator={<OpenInNewIcon fontSize="small" />} >AWS Management Console</Button>
</Link>
</Box>
</Box>
<Tabs
size="sm"
aria-label="Station Management Tabs"
defaultValue={0}
color={props.style ?? 'success'}
sx = {(theme) => ({
'--Tabs-gap': '0px',
borderRadius: 'lg',
border: `1px solid ${theme.palette.divider}`,
width: '100%',
flex: 1,


position: 'relative',
})}
>
<TabList
sx={{
'--ListItem-radius': '0px',
borderRadius: 0,
[`& .${tabClasses.root}`]: {
fontWeight: 'md',
flex: 1,
bgcolor: 'background.body',
[`&.${tabClasses.selected}`]: {
color: `${props.style ?? 'success'}.500`,
},
[`&.${tabClasses.selected}:before`]: {
content: '""',
display: 'block',
position: 'absolute',
bottom: -1,
width: '100%',
height: 2,
bgcolor: `${props.style ?? 'success'}.400`,
},
[`&.${tabClasses.focusVisible}`]: {
outlineOffset: '-3px',
},
},
}}
>
<Tab sx = {{ py: 1.5 }}>Roster</Tab>
<Tab sx = {{ py: 1.5 }}>Schedule</Tab>
<Tab sx = {{ py: 1.5 }}>Rotation</Tab>
<Tab sx = {{ py: 1.5 }}>Catalog</Tab>
</TabList>
<TabPanel value={0}>
<DJRoster
roster={djs}
updateDjs={updateDjs}
setDjs={setDjs}
loading={loading}
setLoading={setLoading}
/>
</TabPanel>
<TabPanel value={1}
sx = {{
flex: '0 1 auto',
}}
>
<StationSchedule
roster={djs}
/>
</TabPanel>
<TabPanel value={2}>
<RotationManagement />
</TabPanel>
<TabPanel value={3}>
<CatalogEditor />
</TabPanel>
</Tabs>
</>
);
};
}

export default AdminPage;
export default StationManagementPage;
22 changes: 10 additions & 12 deletions app/dashboard/verify/page.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,26 @@
'use client';

import { getAuthenticatedUser, useSelector } from "@/lib/redux";
import { fetchDJs, getAuthenticatedUser, useDispatch, useSelector } from "@/lib/redux";
import { getter } from "@/lib/services";
import { useEffect } from "react";
import { toast } from "sonner";

export default function VerifyPage() {

const user = useSelector(getAuthenticatedUser);
const dispatch = useDispatch();

useEffect(() => {
console.log("mounted");

const process = async (e: KeyboardEvent) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you rename this to something more descriptive like processKeyboardInput or something?

if ((e as KeyboardEvent).key === 'v') {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you leave a comment that documents why v is special? Is this v for verify?

const { data, error } = await getter('testAuth')();

if (error) {
console.error(error);
toast.error(error.message);
} else {
console.table(data);
toast.success(data.message);
let token = sessionStorage.getItem("idToken");
if (!token) {
toast.error("No TOKEN!!!");
return;
}
await dispatch(fetchDJs(token));
}
}

Expand All @@ -30,9 +30,7 @@ export default function VerifyPage() {
console.log('Verify page unmounted');
window.removeEventListener('keydown', process);
}
}, []);

const user = useSelector(getAuthenticatedUser);
}, [user?.djId]);

return (
<>
Expand Down
4 changes: 4 additions & 0 deletions lib/redux/model/admin/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from "./types";
export * from "./slice";
export * from "./thunks";
export * from "./selectors";
6 changes: 6 additions & 0 deletions lib/redux/model/admin/selectors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/* Instruments */
import { type ReduxState } from "@/lib/redux";


const getAdminLoading = (state: ReduxState) => state.admin.loading;
const getDJs = (state: ReduxState) => state.admin.djs;
15 changes: 15 additions & 0 deletions lib/redux/model/admin/slice.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { createSlice } from "@reduxjs/toolkit";
import { AdminState } from "./types";

const initialState: AdminState = {
loading: false,
djs: [],
};

export const adminSlice = createSlice({
name: "admin",
initialState,
reducers: {

}
});
32 changes: 32 additions & 0 deletions lib/redux/model/admin/thunks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { CognitoIdentityProviderClient, ListUsersCommand, ListUsersCommandInput } from "@aws-sdk/client-cognito-identity-provider";
import { createAppAsyncThunk } from "../../createAppAsyncThunk";
import { User } from "../authentication";
import { getCredentials } from "../..";

export const fetchDJs = createAppAsyncThunk(
"admin/fetchDJs",
async (idToken: string): Promise<boolean> => {

const client = new CognitoIdentityProviderClient({
credentials: await getCredentials(idToken),
region: process.env.NEXT_PUBLIC_AWS_REGION
});

const params: ListUsersCommandInput = {
UserPoolId: process.env.NEXT_PUBLIC_USER_POOL_ID,
AttributesToGet: [
"email",
"name",
"custom:dj-name",
]
};

const listCommand = new ListUsersCommand(params);
const listResponse = await client.send(listCommand);

console.table(listResponse);
console.table(listResponse.Users);

return true;
}
);
11 changes: 11 additions & 0 deletions lib/redux/model/admin/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@

export interface AdminState {
loading: boolean;
djs: DJ[];
};

export interface DJ {
id: number;
real_name: string;
dj_name: string;
}
27 changes: 21 additions & 6 deletions lib/redux/model/authentication/thunks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@
import { CognitoIdentityProviderClient, GetUserCommand, GetUserCommandOutput, InitiateAuthCommand, InitiateAuthCommandInput } from "@aws-sdk/client-cognito-identity-provider";
import { jwtDecode } from 'jwt-decode';
import { toast } from 'sonner';
import { AuthenticationState, BackendResponse, DJwtPayload, getter, nullState, setter } from "../..";
import { AuthenticationState, BackendResponse, DJwtPayload, getter, nullState, setter, updater } from "../..";
import { createAppAsyncThunk } from "../../createAppAsyncThunk";
import { LoginCredentials, User } from "./types";
import { AxiosResponse } from "axios";
import { LoginCredentials } from "./types";

export const login = createAppAsyncThunk(
"authentication/authenticateAsync",
Expand Down Expand Up @@ -63,14 +62,30 @@ export const login = createAppAsyncThunk(
return nullState;
}

let djName = "";
if (!backendData.dj_name || !backendData.real_name) {
const { data: updateData, error: updateError } = await (updater(`djs/register`))({
cognito_user_name: userResponse.Username,
real_name: userResponse?.UserAttributes?.find(attr => attr.Name == 'name')?.Value,
dj_name: userResponse?.UserAttributes?.find(attr => attr.Name == 'custom:dj-name')?.Value,
});

if (updateError) {
toast.error("The backend is out of sync with the user database. This is fatal. Contact a site admin immediately.");
return nullState;
}

djName = updateData.dj_name;
}

return {
authenticating: false,
isAuthenticated: true,
user: {
username: userResponse.Username || credentials.username,
djName: userResponse.UserAttributes?.find(x => x.Name == "custom:dj-name")?.Value || "",
djId: backendData.id,
name: userResponse.UserAttributes?.find(x => x.Name == "name")?.Value || "",
djName: backendData.dj_name || userResponse.UserAttributes?.find(x => x.Name == "custom:dj-name")?.Value || "",
djId: Number(backendData.id),
name: backendData.real_name || userResponse.UserAttributes?.find(x => x.Name == "name")?.Value || "",
JacksonMeade marked this conversation as resolved.
Show resolved Hide resolved
isAdmin: isAdmin,
showRealName: false
}
Expand Down
22 changes: 20 additions & 2 deletions lib/redux/model/flowsheet/slice.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { createSlice } from "@reduxjs/toolkit";
import { FlowSheetState } from "./types";
import { getIsLive, join, leave, loadFlowsheet } from "./thunks";
import { getIsLive, join, leave, loadFlowsheet, loadFlowsheetEntries } from "./thunks";
import { toast } from "sonner";


Expand Down Expand Up @@ -90,7 +90,11 @@ export const flowSheetSlice = createSlice({
.addCase(getIsLive.fulfilled, (state, action) => {
state.changingAir = false;
state.live = action.payload;
toast.success("You are live!");
if (action.payload) {
toast.success("You are live!");
} else {
toast.info("You are not live.");
JacksonMeade marked this conversation as resolved.
Show resolved Hide resolved
}
})
.addCase(getIsLive.rejected, (state) => {
state.changingAir = false;
Expand All @@ -109,6 +113,20 @@ export const flowSheetSlice = createSlice({
.addCase(leave.fulfilled, (state, action) => {
state.changingAir = false;
state.live = !action.payload;
})
.addCase(loadFlowsheetEntries.pending, (state) => {
state.loading = true;
})
.addCase(loadFlowsheetEntries.fulfilled, (state, action) => {
state.loading = false;
const numberUpdated = action.payload.length;
// replace the first numberUpdated entries with the new ones
state.entries = action.payload.concat(state.entries.slice(numberUpdated));
toast.info(`Flowsheet saved and updated.`);
})
.addCase(loadFlowsheetEntries.rejected, (state) => {
state.loading = false;
toast.error("Could not save and update the flowsheet.");
});
}
});
Loading