Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
14 changes: 14 additions & 0 deletions src/components/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ import { WSOToken } from "../lib/types";
// More component imports
const Scheduler = lazy(() => import("./views/CourseScheduler/Scheduler"));
const About = lazy(() => import("./views/Misc/About"));
const Dashboard = lazy(() => import("./views/AdminTools/dashboard"));

const FAQ = lazy(() => import("./views/Misc/FAQ"));
const Team = lazy(() => import("./views/Misc/Team"));
const MobilePrivacyPolicy = lazy(
Expand Down Expand Up @@ -194,15 +196,27 @@ const App = () => {
</RequireScope>
}
/>

<Route path="schedulecourses" element={<Scheduler />} />
{/* Static Content Pages */}

<Route path="about" element={<About />} />
<Route path="faq" element={<FAQ />} />
<Route path="team" element={<Team />} />
<Route
path="admin"
element={
<RequireScope token={apiToken} name="admin">
<Dashboard />
</RequireScope>
}
/>

<Route
path="mobile-privacy-policy"
element={<MobilePrivacyPolicy />}
/>

<Route path="login" element={<Login />} />
{/* Error-handling Pages */}
<Route path="403" element={<Error403 />} />
Expand Down
54 changes: 45 additions & 9 deletions src/components/Nav.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// React Imports
import React, { useEffect, useState } from "react";

import "./stylesheets/Nav.css";

// Redux imports
import { getWSO, getCurrUser } from "../lib/authSlice";
import { removeCredentials } from "../lib/authSlice";
Expand All @@ -11,6 +13,32 @@ import { Link } from "react-router-dom";
import history from "../lib/history";
import { userTypeStudent } from "../constants/general";

// Feature flag imports
import { RootState } from "../lib/store";
import { FeatureFlag, FFState } from "../lib/featureFlagSlice";

interface FeatureFlagElementProps {
element: React.ReactElement;
flag: keyof FeatureFlag;
}

const FeatureFlagElement: React.FC<FeatureFlagElementProps> = ({
element,
flag,
}) => {
const enabled = useAppSelector(
(state: RootState) => state.featureFlagState[flag]
);

return (
<div className="nav_feature_div">
{enabled === FFState.Enabled ? (
<li className="nav_feature_item">{element}</li>
) : null}
</div>
);
};

const Nav = () => {
const dispatch = useAppDispatch();
const currUser = useAppSelector(getCurrUser);
Expand Down Expand Up @@ -111,18 +139,26 @@ const Nav = () => {
</>
)}

<li>
<Link to="faq">FAQ</Link>
</li>
<li>
<a href="/wiki/">Wiki</a>
</li>
<li>
<Link to="about">About</Link>
</li>
<FeatureFlagElement
element={<Link to="faq">FAQ</Link>}
flag="enableFAQ"
/>
<FeatureFlagElement
element={<a href="/wiki/">Wiki</a>}
flag="enableWiki"
/>
<FeatureFlagElement
element={<Link to="about">About</Link>}
flag="enableAbout"
/>
<li>
<Link to="schedulecourses">Course Scheduler</Link>
</li>
{currUser?.admin && (
<li>
<Link to="admin">Admin Dashboard</Link>
</li>
)}
{ephmatchVisibility > 0 && (
<li>
<Link
Expand Down
55 changes: 55 additions & 0 deletions src/components/stylesheets/Dashboard.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
.dash-body {
padding-top: 5vh;
}

#dash-hot {
background-color: #553871;
color: white;
border: 1px solid #553871;
}

#dash-cold {
background-color: white;
color: #553871;
border: 1px solid #553871;
}

.feature-lab {
text-align: center;
color: #553871;
}

.dash-section {
text-align: center;
margin-bottom: 1vw;
}

.dash-button-container {
margin-top: 2vw;
position: relative;
left: 2.5%;
width: 25vw;
height: 60vh;
border: 1px solid #553871;
border-radius: 1vw;
padding: 1vw;
overflow: scroll;
}

.dash-button {
margin-bottom: 1vw;
margin-right: 1.4vw;
margin-left: 1.4vw;
text-align: center;

font-size: 0.8vw; /* Set initial font size using vw unit */
width: 5vw;
}

.dash-flag {
margin-top: 0.5vw;
margin-bottom: 0.5vw;

border: 1px solid #dddddd;
border-radius: 1vw;
}
7 changes: 7 additions & 0 deletions src/components/stylesheets/Nav.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.nav_feature_div {
display: inline-block;
}

.nav_feature_div:not(:empty) {
padding-right: 2em;
}
60 changes: 60 additions & 0 deletions src/components/views/AdminTools/dashboard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// React imports
import React from "react";
import {
FFState,
updateFeatureFlag,
FeatureFlag,
} from "../../../lib/featureFlagSlice";
import { RootState } from "../../../lib/store";
import { useAppDispatch, useAppSelector } from "../../../lib/store";
import "../../stylesheets/Dashboard.css";

const Dashboard = () => {
const dispatch = useAppDispatch();
const featureFlagState = useAppSelector(
(state: RootState) => state.featureFlagState
);

const setFeatureFlag = (myflag: keyof FeatureFlag, newState: FFState) => {
dispatch(updateFeatureFlag({ flag: myflag, value: newState }));
};

return (
<div className="dash-body">
<h2 className="text-center" id="logotype">
Admin Dashboard
</h2>
<div className="dash-button-container">
<h4 className="dash-section" id="tagline">
Feature Flags
</h4>

{Object.entries(featureFlagState).map(
([key, value]) =>
key[0] !== "_" && (
<div key={key} className="dash-flag">
<p className="feature-lab">{key.slice(6, key.length)}:</p>
{Object.entries(FFState).map(([stateKey, stateValue]) => (
<button
key={key + stateKey}
id={value === stateValue ? "dash-hot" : "dash-cold"}
className={"dash-button"}
onClick={() =>
setFeatureFlag(
key as keyof FeatureFlag,
stateKey as FFState
)
}
>
{"Set " + stateKey}
</button>
))}
</div>
)
)}
</div>
</div>
);
};

export default Dashboard;
16 changes: 15 additions & 1 deletion src/components/views/Misc/About.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@
import React from "react";
import DoughtyBanner from "../../../assets/images/banners/Doughty.jpg";
import { Link } from "react-router-dom";
import { useAppSelector } from "../../../lib/store";
import { FFState } from "../../../lib/featureFlagSlice";
import Error403 from "../Errors/Error403";

const About = () => {
const AboutPreRelease = () => {
return (
<div className="article">
<div className="about-banner">
Expand Down Expand Up @@ -118,4 +121,15 @@ const About = () => {
);
};

const About = () => {
const enableAbout = useAppSelector(
(state) => state.featureFlagState["enableAbout"]
);

return (
<div>
{enableAbout === FFState.Enabled ? <AboutPreRelease /> : <Error403 />}
</div>
);
};
export default About;
17 changes: 16 additions & 1 deletion src/components/views/Misc/FAQ.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
// React imports
import React from "react";
import { useAppSelector } from "../../../lib/store";
import { FFState } from "../../../lib/featureFlagSlice";
import Error403 from "../Errors/Error403";

const FAQ = () => {
const FAQPreRelease = () => {
return (
<div className="article">
<section>
Expand Down Expand Up @@ -127,4 +130,16 @@ const FAQ = () => {
);
};

const FAQ = () => {
const enableFAQ = useAppSelector(
(state) => state.featureFlagState["enableFAQ"]
);

return (
<div>
{enableFAQ === FFState.Enabled ? <FAQPreRelease /> : <Error403 />}
</div>
);
};

export default FAQ;
40 changes: 40 additions & 0 deletions src/lib/featureFlagSlice.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit";

export enum FFState {
Enabled = "Enabled",
Disabled = "Disabled",
}

export interface FeatureFlag {
enableAbout: FFState;
enableWiki: FFState;
enableFAQ: FFState;

// Add more feature flags as needed
}

export const initialState: FeatureFlag = {
enableAbout: FFState.Enabled,
enableWiki: FFState.Enabled,
enableFAQ: FFState.Enabled,
};

const featureFlagSlice = createSlice({
name: "featureFlags",
initialState,
reducers: {
updateFeatureFlag: (
state,
action: PayloadAction<{ flag: keyof FeatureFlag; value: FFState }>
) => {
const { flag, value } = action.payload;
if (flag in state) {
state[flag] = value;
}
},
},
});

export const { updateFeatureFlag } = featureFlagSlice.actions;

export default featureFlagSlice.reducer;
13 changes: 12 additions & 1 deletion src/lib/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,12 @@ import {
} from "redux-persist";
import storage from "redux-persist/lib/storage";

import { courseReducer, schedulerUtilReducer, authReducer } from "../reducers";
import {
courseReducer,
schedulerUtilReducer,
authReducer,
featureFlagReducer,
} from "../reducers";

const persistedAuthReducer = persistReducer(
{ key: "auth", storage, whitelist: ["identityToken"] },
Expand All @@ -24,11 +29,17 @@ const persistedCourseSchedulerReducer = persistReducer(
schedulerUtilReducer
);

const persistedFeatureFlagReducer = persistReducer(
{ key: "featureFlags", storage },
featureFlagReducer
);

const store = configureStore({
reducer: {
courseState: courseReducer,
schedulerUtilState: persistedCourseSchedulerReducer,
authState: persistedAuthReducer,
featureFlagState: persistedFeatureFlagReducer,
},
// this is to disable React Toolkit's error message "A non-serializable value was detected in the state"
// TODO: stop using non-serializable object `authState.wso` and `schedulerUtilState.gapi`
Expand Down
11 changes: 10 additions & 1 deletion src/reducers/index.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,23 @@
import courseReducer from "./course";
import schedulerUtilReducer from "./schedulerUtils";
import authReducer from "../lib/authSlice";
import featureFlagReducer from "../lib/featureFlagSlice";

import goodrichReducer from "./goodrich";

const rootReducer = {
courseState: courseReducer,
schedulerUtilState: schedulerUtilReducer,
authState: authReducer,
goodrichState: goodrichReducer,
featureFlagState: featureFlagReducer,
};

export default rootReducer;
export { courseReducer, schedulerUtilReducer, authReducer, goodrichReducer };
export {
courseReducer,
schedulerUtilReducer,
authReducer,
goodrichReducer,
featureFlagReducer,
};
Loading