From a28b19a296896db22720a2be26e78eec1da86747 Mon Sep 17 00:00:00 2001 From: SKairinos Date: Mon, 10 Jun 2024 12:31:45 +0000 Subject: [PATCH 01/28] fix launch --- .devcontainer.json | 2 +- .gcloudignore | 2 +- .vscode/launch.json | 4 ++-- .vscode/tasks.json | 12 +++++++++--- scripts/hard-install | 8 ++++++++ setup => scripts/setup | 2 -- 6 files changed, 21 insertions(+), 9 deletions(-) create mode 100644 scripts/hard-install rename setup => scripts/setup (62%) diff --git a/.devcontainer.json b/.devcontainer.json index 697ab8b..9cb651b 100644 --- a/.devcontainer.json +++ b/.devcontainer.json @@ -47,7 +47,7 @@ "source=./codeforlife-package-python,target=/workspace/codeforlife-package-python,type=bind,consistency=cached" ], "name": "portal-frontend", - "postCreateCommand": "sudo chmod u+x ./setup && ./setup", + "postCreateCommand": "sudo chmod u+x scripts/setup && scripts/setup", "remoteUser": "root", "service": "base-service", "shutdownAction": "none", diff --git a/.gcloudignore b/.gcloudignore index 09c35ea..8fb98f4 100644 --- a/.gcloudignore +++ b/.gcloudignore @@ -10,7 +10,7 @@ /codecov.yml /*.code-* /*.md -/setup +/scripts /tsconfig.json /tsconfig.node.json /vite.config.ts diff --git a/.vscode/launch.json b/.vscode/launch.json index 0d97f88..9f1a90e 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,8 +1,8 @@ { "configurations": [ { - "name": "React Dev Server", - "preLaunchTask": "start-react-dev-server", + "name": "Vite Server", + "preLaunchTask": "start-vite-server", "request": "launch", "type": "chrome", "url": "http://localhost:5173" diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 6a0dd90..20c1b9d 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -1,16 +1,22 @@ { "tasks": [ { + "command": "sudo chmod u+x scripts/setup && scripts/setup && yarn dev", "isBackground": true, - "label": "start-react-dev-server", + "label": "start-vite-server", "options": { "env": { "BROWSER": "none" } }, "problemMatcher": [], - "script": "start", - "type": "npm" + "type": "shell" + }, + { + "command": "sudo chmod u+x scripts/hard-install && scripts/hard-install", + "label": "hard-install", + "problemMatcher": [], + "type": "shell" } ], "version": "2.0.0" diff --git a/scripts/hard-install b/scripts/hard-install new file mode 100644 index 0000000..236bf92 --- /dev/null +++ b/scripts/hard-install @@ -0,0 +1,8 @@ +#!/bin/bash +set -e + +cd "${BASH_SOURCE%/*}" + +rm -f ../yarn.lock +rm -rf ../node_modules +yarn install --production=false diff --git a/setup b/scripts/setup similarity index 62% rename from setup rename to scripts/setup index 1272bff..ce6aabd 100755 --- a/setup +++ b/scripts/setup @@ -3,6 +3,4 @@ set -e cd "${BASH_SOURCE%/*}" -printf "Setting up Node.js environment\n\n" - yarn install --production=false From 9717c7a21a2bf45bf2d21294038c92d21bca3746 Mon Sep 17 00:00:00 2001 From: SKairinos Date: Mon, 10 Jun 2024 12:55:02 +0000 Subject: [PATCH 02/28] create run script --- .vscode/tasks.json | 2 +- scripts/run | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 scripts/run diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 20c1b9d..73af9a8 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -1,7 +1,7 @@ { "tasks": [ { - "command": "sudo chmod u+x scripts/setup && scripts/setup && yarn dev", + "command": "sudo chmod u+x scripts/run && scripts/run", "isBackground": true, "label": "start-vite-server", "options": { diff --git a/scripts/run b/scripts/run new file mode 100644 index 0000000..8ce071e --- /dev/null +++ b/scripts/run @@ -0,0 +1,8 @@ +#!/bin/bash +set -e + +cd "${BASH_SOURCE%/*}" + +source ./setup + +yarn dev From 7d8607a128c607483573742a72a3be30779fb020 Mon Sep 17 00:00:00 2001 From: SKairinos Date: Mon, 10 Jun 2024 15:16:05 +0000 Subject: [PATCH 03/28] clean codeforlife in yarn cache --- scripts/hard-install | 1 + 1 file changed, 1 insertion(+) mode change 100644 => 100755 scripts/hard-install diff --git a/scripts/hard-install b/scripts/hard-install old mode 100644 new mode 100755 index 236bf92..ea46059 --- a/scripts/hard-install +++ b/scripts/hard-install @@ -5,4 +5,5 @@ cd "${BASH_SOURCE%/*}" rm -f ../yarn.lock rm -rf ../node_modules +yarn cache clean codeforlife yarn install --production=false From 341eb4e0f2fbb922468cdb2f2fadbee5d36e111c Mon Sep 17 00:00:00 2001 From: SKairinos Date: Tue, 11 Jun 2024 14:05:32 +0000 Subject: [PATCH 04/28] fix paths --- src/api/authFactor.ts | 2 +- src/api/klass.ts | 2 +- src/api/school.ts | 2 +- src/api/schoolTeacherInvitation.ts | 2 +- src/api/student.ts | 2 +- src/api/teacher.ts | 2 +- src/api/user.ts | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/api/authFactor.ts b/src/api/authFactor.ts index 1daaa2d..f4bc253 100644 --- a/src/api/authFactor.ts +++ b/src/api/authFactor.ts @@ -8,7 +8,7 @@ import { type DestroyResult, type ListArg, type ListResult, -} from "codeforlife/utils/rtkQuery" +} from "codeforlife/utils/api" import api from "." diff --git a/src/api/klass.ts b/src/api/klass.ts index 7e59ead..71f980c 100644 --- a/src/api/klass.ts +++ b/src/api/klass.ts @@ -12,7 +12,7 @@ import { type RetrieveResult, type UpdateArg, type UpdateResult, -} from "codeforlife/utils/rtkQuery" +} from "codeforlife/utils/api" import api from "." diff --git a/src/api/school.ts b/src/api/school.ts index aead92e..67dd433 100644 --- a/src/api/school.ts +++ b/src/api/school.ts @@ -8,7 +8,7 @@ import { type RetrieveResult, type UpdateArg, type UpdateResult, -} from "codeforlife/utils/rtkQuery" +} from "codeforlife/utils/api" import api from "." diff --git a/src/api/schoolTeacherInvitation.ts b/src/api/schoolTeacherInvitation.ts index e568a32..36a34e4 100644 --- a/src/api/schoolTeacherInvitation.ts +++ b/src/api/schoolTeacherInvitation.ts @@ -14,7 +14,7 @@ import { type RetrieveResult, type UpdateArg, type UpdateResult, -} from "codeforlife/utils/rtkQuery" +} from "codeforlife/utils/api" import api from "." diff --git a/src/api/student.ts b/src/api/student.ts index 36fb51d..d74cb29 100644 --- a/src/api/student.ts +++ b/src/api/student.ts @@ -8,7 +8,7 @@ import { type BulkDestroyResult, type BulkUpdateArg, type BulkUpdateResult, -} from "codeforlife/utils/rtkQuery" +} from "codeforlife/utils/api" import api from "." diff --git a/src/api/teacher.ts b/src/api/teacher.ts index f3b6a9a..ab228ce 100644 --- a/src/api/teacher.ts +++ b/src/api/teacher.ts @@ -8,7 +8,7 @@ import { type DestroyResult, type UpdateArg, type UpdateResult, -} from "codeforlife/utils/rtkQuery" +} from "codeforlife/utils/api" import api from "." diff --git a/src/api/user.ts b/src/api/user.ts index 7f037de..e64f1c9 100644 --- a/src/api/user.ts +++ b/src/api/user.ts @@ -13,7 +13,7 @@ import { type RetrieveResult, type UpdateArg, type UpdateResult, -} from "codeforlife/utils/rtkQuery" +} from "codeforlife/utils/api" import api from "." From 6faa5d1f39e5f20e049099ab35c210d63ee1bc94 Mon Sep 17 00:00:00 2001 From: SKairinos Date: Tue, 11 Jun 2024 15:54:01 +0000 Subject: [PATCH 05/28] quick save --- .env | 2 +- package.json | 2 +- scripts/run | 0 src/App.css | 39 ------- src/App.test.tsx | 105 ------------------ src/App.tsx | 72 ++---------- src/api/sso.ts | 73 ++++++++++++ src/app/schemas.ts | 36 ++++++ src/app/store.ts | 37 ++---- src/app/theme.ts | 16 +++ src/features/counter/Counter.module.css | 81 -------------- src/features/counter/Counter.tsx | 78 ------------- src/features/counter/counterAPI.ts | 6 - src/features/counter/counterSlice.test.ts | 58 ---------- src/features/counter/counterSlice.ts | 89 --------------- src/features/quotes/Quotes.module.css | 20 ---- src/features/quotes/Quotes.tsx | 59 ---------- src/features/quotes/quotesApiSlice.ts | 38 ------- src/index.css | 13 --- src/logo.svg | 1 - src/main.tsx | 4 +- src/pages/login/BaseForm.tsx | 45 ++++++++ src/pages/login/IndyForm.tsx | 37 ++++++ src/pages/login/Login.tsx | 51 +++++++++ src/pages/login/StudentForm.tsx | 37 ++++++ src/pages/login/index.tsx | 0 src/pages/login/teacherForms/Otp.tsx | 37 ++++++ .../login/teacherForms/OtpBypassToken.tsx | 37 ++++++ src/pages/login/teacherForms/Password.tsx | 53 +++++++++ src/pages/login/teacherForms/index.tsx | 12 ++ src/router/Router.tsx | 28 +++++ src/router/index.ts | 5 + src/router/paths.ts | 92 +++++++++++++++ src/router/routes/authentication.tsx | 42 +++++++ yarn.lock | 62 +++++------ 35 files changed, 656 insertions(+), 711 deletions(-) mode change 100644 => 100755 scripts/run delete mode 100644 src/App.css delete mode 100644 src/App.test.tsx create mode 100644 src/api/sso.ts create mode 100644 src/app/schemas.ts create mode 100644 src/app/theme.ts delete mode 100644 src/features/counter/Counter.module.css delete mode 100644 src/features/counter/Counter.tsx delete mode 100644 src/features/counter/counterAPI.ts delete mode 100644 src/features/counter/counterSlice.test.ts delete mode 100644 src/features/counter/counterSlice.ts delete mode 100644 src/features/quotes/Quotes.module.css delete mode 100644 src/features/quotes/Quotes.tsx delete mode 100644 src/features/quotes/quotesApiSlice.ts delete mode 100644 src/index.css delete mode 100644 src/logo.svg create mode 100644 src/pages/login/BaseForm.tsx create mode 100644 src/pages/login/IndyForm.tsx create mode 100644 src/pages/login/Login.tsx create mode 100644 src/pages/login/StudentForm.tsx create mode 100644 src/pages/login/index.tsx create mode 100644 src/pages/login/teacherForms/Otp.tsx create mode 100644 src/pages/login/teacherForms/OtpBypassToken.tsx create mode 100644 src/pages/login/teacherForms/Password.tsx create mode 100644 src/pages/login/teacherForms/index.tsx create mode 100644 src/router/Router.tsx create mode 100644 src/router/index.ts create mode 100644 src/router/paths.ts create mode 100644 src/router/routes/authentication.tsx diff --git a/.env b/.env index 0a6ea68..af0d8db 100644 --- a/.env +++ b/.env @@ -1 +1 @@ -VITE_API_BASE_URL=REPLACE_ME +VITE_API_BASE_URL=http://localhost:8000/ diff --git a/package.json b/package.json index 419e3b0..357981b 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ }, "//": "🚫 Don't add `dependencies` below that are inherited from the CFL package.", "dependencies": { - "codeforlife": "github:ocadotechnology/codeforlife-package-javascript#v2.0.0" + "codeforlife": "github:ocadotechnology/codeforlife-package-javascript#auth_flow" }, "//": "✅ Do add `devDependencies` below that are `peerDependencies` in the CFL package.", "devDependencies": { diff --git a/scripts/run b/scripts/run old mode 100644 new mode 100755 diff --git a/src/App.css b/src/App.css deleted file mode 100644 index 01cc586..0000000 --- a/src/App.css +++ /dev/null @@ -1,39 +0,0 @@ -.App { - text-align: center; -} - -.App-logo { - height: 40vmin; - pointer-events: none; -} - -@media (prefers-reduced-motion: no-preference) { - .App-logo { - animation: App-logo-float infinite 3s ease-in-out; - } -} - -.App-header { - min-height: 100vh; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - font-size: calc(10px + 2vmin); -} - -.App-link { - color: rgb(112, 76, 182); -} - -@keyframes App-logo-float { - 0% { - transform: translateY(0); - } - 50% { - transform: translateY(10px); - } - 100% { - transform: translateY(0px); - } -} diff --git a/src/App.test.tsx b/src/App.test.tsx deleted file mode 100644 index 06b45be..0000000 --- a/src/App.test.tsx +++ /dev/null @@ -1,105 +0,0 @@ -import { screen, waitFor } from "@testing-library/react" -import App from "./App" -import { renderWithProviders } from "./utils/test-utils" - -test("App should have correct initial render", () => { - renderWithProviders() - - // The app should be rendered correctly - expect(screen.getByText(/learn/i)).toBeInTheDocument() - - // Initial state: count should be 0, incrementValue should be 2 - expect(screen.getByLabelText("Count")).toHaveTextContent("0") - expect(screen.getByLabelText("Set increment amount")).toHaveValue(2) -}) - -test("Increment value and Decrement value should work as expected", async () => { - const { user } = renderWithProviders() - - // Click on "+" => Count should be 1 - await user.click(screen.getByLabelText("Increment value")) - expect(screen.getByLabelText("Count")).toHaveTextContent("1") - - // Click on "-" => Count should be 0 - await user.click(screen.getByLabelText("Decrement value")) - expect(screen.getByLabelText("Count")).toHaveTextContent("0") -}) - -test("Add Amount should work as expected", async () => { - const { user } = renderWithProviders() - - // "Add Amount" button is clicked => Count should be 2 - await user.click(screen.getByText("Add Amount")) - expect(screen.getByLabelText("Count")).toHaveTextContent("2") - - const incrementValueInput = screen.getByLabelText("Set increment amount") - // incrementValue is 2, click on "Add Amount" => Count should be 4 - await user.clear(incrementValueInput) - await user.type(incrementValueInput, "2") - await user.click(screen.getByText("Add Amount")) - expect(screen.getByLabelText("Count")).toHaveTextContent("4") - - // [Negative number] incrementValue is -1, click on "Add Amount" => Count should be 3 - await user.clear(incrementValueInput) - await user.type(incrementValueInput, "-1") - await user.click(screen.getByText("Add Amount")) - expect(screen.getByLabelText("Count")).toHaveTextContent("3") -}) - -it("Add Async should work as expected", async () => { - const { user } = renderWithProviders() - - // "Add Async" button is clicked => Count should be 2 - await user.click(screen.getByText("Add Async")) - - await waitFor(() => - expect(screen.getByLabelText("Count")).toHaveTextContent("2"), - ) - - const incrementValueInput = screen.getByLabelText("Set increment amount") - // incrementValue is 2, click on "Add Async" => Count should be 4 - await user.clear(incrementValueInput) - await user.type(incrementValueInput, "2") - - await user.click(screen.getByText("Add Async")) - await waitFor(() => - expect(screen.getByLabelText("Count")).toHaveTextContent("4"), - ) - - // [Negative number] incrementValue is -1, click on "Add Async" => Count should be 3 - await user.clear(incrementValueInput) - await user.type(incrementValueInput, "-1") - await user.click(screen.getByText("Add Async")) - await waitFor(() => - expect(screen.getByLabelText("Count")).toHaveTextContent("3"), - ) -}) - -test("Add If Odd should work as expected", async () => { - const { user } = renderWithProviders() - - // "Add If Odd" button is clicked => Count should stay 0 - await user.click(screen.getByText("Add If Odd")) - expect(screen.getByLabelText("Count")).toHaveTextContent("0") - - // Click on "+" => Count should be updated to 1 - await user.click(screen.getByLabelText("Increment value")) - expect(screen.getByLabelText("Count")).toHaveTextContent("1") - - // "Add If Odd" button is clicked => Count should be updated to 3 - await user.click(screen.getByText("Add If Odd")) - expect(screen.getByLabelText("Count")).toHaveTextContent("3") - - const incrementValueInput = screen.getByLabelText("Set increment amount") - // incrementValue is 1, click on "Add If Odd" => Count should be updated to 4 - await user.clear(incrementValueInput) - await user.type(incrementValueInput, "1") - await user.click(screen.getByText("Add If Odd")) - expect(screen.getByLabelText("Count")).toHaveTextContent("4") - - // click on "Add If Odd" => Count should stay 4 - await user.clear(incrementValueInput) - await user.type(incrementValueInput, "-1") - await user.click(screen.getByText("Add If Odd")) - expect(screen.getByLabelText("Count")).toHaveTextContent("4") -}) diff --git a/src/App.tsx b/src/App.tsx index 08ec755..27ec12f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,67 +1,15 @@ -import "./App.css" -import { Counter } from "./features/counter/Counter" -import { Quotes } from "./features/quotes/Quotes" -import logo from "./logo.svg" +import { CssBaseline, ThemeProvider } from "@mui/material" +import type { FC } from "react" -const App = () => { +import theme from "./app/theme" +import Router from "./router" + +const App: FC = () => { return ( -
-
- logo - -

- Edit src/App.tsx and save to reload. -

- - - Learn - - React - - , - - Redux - - , - - Redux Toolkit - - , - - React Redux - - , and - - Reselect - - -
-
+ + + + ) } diff --git a/src/api/sso.ts b/src/api/sso.ts new file mode 100644 index 0000000..ccc235e --- /dev/null +++ b/src/api/sso.ts @@ -0,0 +1,73 @@ +// TODO: rename this file to session.ts and move to codeforlife-sso-frontend. + +import { type Class, type OtpBypassToken, type User } from "codeforlife/api" +import { type SessionMetadata } from "codeforlife/hooks" +import { type Arg } from "codeforlife/utils/api" + +import api from "." + +const baseUrl = "sso/session/" + +const ssoApi = api.injectEndpoints({ + endpoints: build => ({ + loginWithEmail: build.mutation< + SessionMetadata, + Arg + >({ + query: body => ({ + url: baseUrl + "login-with-email/", + method: "POST", + body, + }), + }), + loginWithOtp: build.mutation({ + query: body => ({ + url: baseUrl + "otp/", + method: "POST", + body, + }), + }), + loginWithOtpBypassToken: build.mutation< + SessionMetadata, + Arg + >({ + query: body => ({ + url: baseUrl + "login-with-otp-bypass-token/", + method: "POST", + body, + }), + }), + loginAsStudent: build.mutation< + SessionMetadata, + Arg & { class_id: Class["id"] } + >({ + query: body => ({ + url: baseUrl + "login-as-student/", + method: "POST", + body, + }), + }), + autoLoginAsStudent: build.mutation< + SessionMetadata, + { + user_id: User["id"] + auto_gen_password: string + } + >({ + query: body => ({ + url: baseUrl + "auto-login-as-student/", + method: "POST", + body, + }), + }), + }), +}) + +export default ssoApi +export const { + useLoginWithEmailMutation, + useLoginWithOtpMutation, + useLoginWithOtpBypassTokenMutation, + useLoginAsStudentMutation, + useAutoLoginAsStudentMutation, +} = ssoApi diff --git a/src/app/schemas.ts b/src/app/schemas.ts new file mode 100644 index 0000000..5ab681e --- /dev/null +++ b/src/app/schemas.ts @@ -0,0 +1,36 @@ +import * as yup from "yup" + +export const accessCodeSchema = yup + .string() + .matches(/^[A-Z0-9]{5}$/, "Invalid access code") + +const passwordSchema = yup.string().required("required") + +export const teacherPasswordSchema = passwordSchema.test({ + message: "too-weak", + test: password => + password.length >= 10 && + !( + password.search(/[A-Z]/) === -1 || + password.search(/[a-z]/) === -1 || + password.search(/[0-9]/) === -1 || + password.search(/[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]/) === -1 + ), +}) + +export const studentPasswordSchema = passwordSchema.test({ + message: "too-weak", + test: password => password.length >= 6, +}) + +// TODO: make indy password schema the same as teacher's. +export const indyPasswordSchema = passwordSchema.test({ + message: "too-weak", + test: password => + password.length >= 8 && + !( + password.search(/[A-Z]/) === -1 || + password.search(/[a-z]/) === -1 || + password.search(/[0-9]/) === -1 + ), +}) diff --git a/src/app/store.ts b/src/app/store.ts index 9de8802..6c8e48e 100644 --- a/src/app/store.ts +++ b/src/app/store.ts @@ -1,38 +1,21 @@ import type { Action, ThunkAction } from "@reduxjs/toolkit" -import { combineSlices, configureStore } from "@reduxjs/toolkit" -import { setupListeners } from "@reduxjs/toolkit/query" -import { counterSlice } from "../features/counter/counterSlice" -import { quotesApiSlice } from "../features/quotes/quotesApiSlice" +import { combineSlices } from "@reduxjs/toolkit" + +import { makeStore } from "codeforlife/utils/store" + +import api from "../api" // `combineSlices` automatically combines the reducers using // their `reducerPath`s, therefore we no longer need to call `combineReducers`. -const rootReducer = combineSlices(counterSlice, quotesApiSlice) -// Infer the `RootState` type from the root reducer -export type RootState = ReturnType +const reducer = combineSlices(api) -// The store setup is wrapped in `makeStore` to allow reuse -// when setting up tests that need the same store config -export const makeStore = (preloadedState?: Partial) => { - const store = configureStore({ - reducer: rootReducer, - // Adding the api middleware enables caching, invalidation, polling, - // and other useful features of `rtk-query`. - middleware: getDefaultMiddleware => { - return getDefaultMiddleware().concat(quotesApiSlice.middleware) - }, - preloadedState, - }) - // configure listeners using the provided defaults - // optional, but required for `refetchOnFocus`/`refetchOnReconnect` behaviors - setupListeners(store.dispatch) - return store -} +// Infer the `RootState` type from the root reducer +export type RootState = ReturnType -export const store = makeStore() +const store = makeStore({ reducer, middlewares: [api.middleware] }) -// Infer the type of `store` +export default store export type AppStore = typeof store -// Infer the `AppDispatch` type from the store itself export type AppDispatch = AppStore["dispatch"] export type AppThunk = ThunkAction< ThunkReturnType, diff --git a/src/app/theme.ts b/src/app/theme.ts new file mode 100644 index 0000000..e0abe14 --- /dev/null +++ b/src/app/theme.ts @@ -0,0 +1,16 @@ +import { + createTheme, + responsiveFontSizes, + type ThemeOptions, +} from "@mui/material" + +import { themeOptions as cflThemeOptions } from "codeforlife/theme" + +// Unpack the base options to extend the theme +export const themeOptions: ThemeOptions = { + ...cflThemeOptions, +} + +const theme = responsiveFontSizes(createTheme(themeOptions)) + +export default theme diff --git a/src/features/counter/Counter.module.css b/src/features/counter/Counter.module.css deleted file mode 100644 index a0e619d..0000000 --- a/src/features/counter/Counter.module.css +++ /dev/null @@ -1,81 +0,0 @@ -.row { - display: flex; - align-items: center; - justify-content: center; -} - -.row > button { - margin-left: 4px; - margin-right: 8px; -} - -.row:not(:last-child) { - margin-bottom: 16px; -} - -.value { - font-size: 78px; - padding-left: 16px; - padding-right: 16px; - margin-top: 2px; - font-family: "Courier New", Courier, monospace; -} - -.button { - appearance: none; - background: none; - font-size: 32px; - padding-left: 12px; - padding-right: 12px; - outline: none; - border: 2px solid transparent; - color: rgb(112, 76, 182); - padding-bottom: 4px; - cursor: pointer; - background-color: rgba(112, 76, 182, 0.1); - border-radius: 2px; - transition: all 0.15s; -} - -.textbox { - font-size: 32px; - padding: 2px; - width: 64px; - text-align: center; - margin-right: 4px; -} - -.button:hover, -.button:focus { - border: 2px solid rgba(112, 76, 182, 0.4); -} - -.button:active { - background-color: rgba(112, 76, 182, 0.2); -} - -.asyncButton { - composes: button; - position: relative; -} - -.asyncButton:after { - content: ""; - background-color: rgba(112, 76, 182, 0.15); - display: block; - position: absolute; - width: 100%; - height: 100%; - left: 0; - top: 0; - opacity: 0; - transition: - width 1s linear, - opacity 0.5s ease 1s; -} - -.asyncButton:active:after { - width: 0%; - opacity: 1; - transition: 0s; -} diff --git a/src/features/counter/Counter.tsx b/src/features/counter/Counter.tsx deleted file mode 100644 index a286d80..0000000 --- a/src/features/counter/Counter.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import { useState } from "react" - -import { useAppDispatch, useAppSelector } from "../../app/hooks" -import styles from "./Counter.module.css" -import { - decrement, - increment, - incrementAsync, - incrementByAmount, - incrementIfOdd, - selectCount, - selectStatus, -} from "./counterSlice" - -export const Counter = () => { - const dispatch = useAppDispatch() - const count = useAppSelector(selectCount) - const status = useAppSelector(selectStatus) - const [incrementAmount, setIncrementAmount] = useState("2") - - const incrementValue = Number(incrementAmount) || 0 - - return ( -
-
- - - {count} - - -
-
- { - setIncrementAmount(e.target.value) - }} - /> - - - -
-
- ) -} diff --git a/src/features/counter/counterAPI.ts b/src/features/counter/counterAPI.ts deleted file mode 100644 index aca3ef6..0000000 --- a/src/features/counter/counterAPI.ts +++ /dev/null @@ -1,6 +0,0 @@ -// A mock function to mimic making an async request for data -export const fetchCount = (amount = 1) => { - return new Promise<{ data: number }>(resolve => - setTimeout(() => resolve({ data: amount }), 500), - ) -} diff --git a/src/features/counter/counterSlice.test.ts b/src/features/counter/counterSlice.test.ts deleted file mode 100644 index 12eafe1..0000000 --- a/src/features/counter/counterSlice.test.ts +++ /dev/null @@ -1,58 +0,0 @@ -import type { AppStore } from "../../app/store" -import { makeStore } from "../../app/store" -import type { CounterSliceState } from "./counterSlice" -import { - counterSlice, - decrement, - increment, - incrementByAmount, - selectCount, -} from "./counterSlice" - -interface LocalTestContext { - store: AppStore -} - -describe("counter reducer", it => { - beforeEach(context => { - const initialState: CounterSliceState = { - value: 3, - status: "idle", - } - - const store = makeStore({ counter: initialState }) - - context.store = store - }) - - it("should handle initial state", () => { - expect(counterSlice.reducer(undefined, { type: "unknown" })).toStrictEqual({ - value: 0, - status: "idle", - }) - }) - - it("should handle increment", ({ store }) => { - expect(selectCount(store.getState())).toBe(3) - - store.dispatch(increment()) - - expect(selectCount(store.getState())).toBe(4) - }) - - it("should handle decrement", ({ store }) => { - expect(selectCount(store.getState())).toBe(3) - - store.dispatch(decrement()) - - expect(selectCount(store.getState())).toBe(2) - }) - - it("should handle incrementByAmount", ({ store }) => { - expect(selectCount(store.getState())).toBe(3) - - store.dispatch(incrementByAmount(2)) - - expect(selectCount(store.getState())).toBe(5) - }) -}) diff --git a/src/features/counter/counterSlice.ts b/src/features/counter/counterSlice.ts deleted file mode 100644 index 07bc1f5..0000000 --- a/src/features/counter/counterSlice.ts +++ /dev/null @@ -1,89 +0,0 @@ -import type { PayloadAction } from "@reduxjs/toolkit" -import { createAppSlice } from "../../app/createAppSlice" -import type { AppThunk } from "../../app/store" -import { fetchCount } from "./counterAPI" - -export interface CounterSliceState { - value: number - status: "idle" | "loading" | "failed" -} - -const initialState: CounterSliceState = { - value: 0, - status: "idle", -} - -// If you are not using async thunks you can use the standalone `createSlice`. -export const counterSlice = createAppSlice({ - name: "counter", - // `createSlice` will infer the state type from the `initialState` argument - initialState, - // The `reducers` field lets us define reducers and generate associated actions - reducers: create => ({ - increment: create.reducer(state => { - // Redux Toolkit allows us to write "mutating" logic in reducers. It - // doesn't actually mutate the state because it uses the Immer library, - // which detects changes to a "draft state" and produces a brand new - // immutable state based off those changes - state.value += 1 - }), - decrement: create.reducer(state => { - state.value -= 1 - }), - // Use the `PayloadAction` type to declare the contents of `action.payload` - incrementByAmount: create.reducer( - (state, action: PayloadAction) => { - state.value += action.payload - }, - ), - // The function below is called a thunk and allows us to perform async logic. It - // can be dispatched like a regular action: `dispatch(incrementAsync(10))`. This - // will call the thunk with the `dispatch` function as the first argument. Async - // code can then be executed and other actions can be dispatched. Thunks are - // typically used to make async requests. - incrementAsync: create.asyncThunk( - async (amount: number) => { - const response = await fetchCount(amount) - // The value we return becomes the `fulfilled` action payload - return response.data - }, - { - pending: state => { - state.status = "loading" - }, - fulfilled: (state, action) => { - state.status = "idle" - state.value += action.payload - }, - rejected: state => { - state.status = "failed" - }, - }, - ), - }), - // You can define your selectors here. These selectors receive the slice - // state as their first argument. - selectors: { - selectCount: counter => counter.value, - selectStatus: counter => counter.status, - }, -}) - -// Action creators are generated for each case reducer function. -export const { decrement, increment, incrementByAmount, incrementAsync } = - counterSlice.actions - -// Selectors returned by `slice.selectors` take the root state as their first argument. -export const { selectCount, selectStatus } = counterSlice.selectors - -// We can also write thunks by hand, which may contain both sync and async logic. -// Here's an example of conditionally dispatching actions based on current state. -export const incrementIfOdd = - (amount: number): AppThunk => - (dispatch, getState) => { - const currentValue = selectCount(getState()) - - if (currentValue % 2 === 1 || currentValue % 2 === -1) { - dispatch(incrementByAmount(amount)) - } - } diff --git a/src/features/quotes/Quotes.module.css b/src/features/quotes/Quotes.module.css deleted file mode 100644 index 1f85690..0000000 --- a/src/features/quotes/Quotes.module.css +++ /dev/null @@ -1,20 +0,0 @@ -.select { - font-size: 25px; - padding: 5px; - padding-top: 2px; - padding-bottom: 2px; - size: 50; - outline: none; - border: 2px solid transparent; - color: rgb(112, 76, 182); - cursor: pointer; - background-color: rgba(112, 76, 182, 0.1); - border-radius: 5px; - transition: all 0.15s; -} - -.container { - display: flex; - flex-direction: column; - align-items: center; -} diff --git a/src/features/quotes/Quotes.tsx b/src/features/quotes/Quotes.tsx deleted file mode 100644 index c490c4a..0000000 --- a/src/features/quotes/Quotes.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import { useState } from "react" -import styles from "./Quotes.module.css" -import { useGetQuotesQuery } from "./quotesApiSlice" - -const options = [5, 10, 20, 30] - -export const Quotes = () => { - const [numberOfQuotes, setNumberOfQuotes] = useState(10) - // Using a query hook automatically fetches data and returns query values - const { data, isError, isLoading, isSuccess } = - useGetQuotesQuery(numberOfQuotes) - - if (isError) { - return ( -
-

There was an error!!!

-
- ) - } - - if (isLoading) { - return ( -
-

Loading...

-
- ) - } - - if (isSuccess) { - return ( -
-

Select the Quantity of Quotes to Fetch:

- - {data.quotes.map(({ author, quote, id }) => ( -
- “{quote}” -
- {author} -
-
- ))} -
- ) - } - - return null -} diff --git a/src/features/quotes/quotesApiSlice.ts b/src/features/quotes/quotesApiSlice.ts deleted file mode 100644 index a1c7b5a..0000000 --- a/src/features/quotes/quotesApiSlice.ts +++ /dev/null @@ -1,38 +0,0 @@ -// Need to use the React-specific entry point to import `createApi` -import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react" - -interface Quote { - id: number - quote: string - author: string -} - -interface QuotesApiResponse { - quotes: Quote[] - total: number - skip: number - limit: number -} - -// Define a service using a base URL and expected endpoints -export const quotesApiSlice = createApi({ - baseQuery: fetchBaseQuery({ baseUrl: "https://dummyjson.com/quotes" }), - reducerPath: "quotesApi", - // Tag types are used for caching and invalidation. - tagTypes: ["Quotes"], - endpoints: build => ({ - // Supply generics for the return type (in this case `QuotesApiResponse`) - // and the expected query argument. If there is no argument, use `void` - // for the argument type instead. - getQuotes: build.query({ - query: (limit = 10) => `?limit=${limit}`, - // `providesTags` determines which 'tag' is attached to the - // cached data returned by the query. - providesTags: (result, error, id) => [{ type: "Quotes", id }], - }), - }), -}) - -// Hooks are auto-generated by RTK-Query -// Same as `quotesApiSlice.endpoints.getQuotes.useQuery` -export const { useGetQuotesQuery } = quotesApiSlice diff --git a/src/index.css b/src/index.css deleted file mode 100644 index 4a1df4d..0000000 --- a/src/index.css +++ /dev/null @@ -1,13 +0,0 @@ -body { - margin: 0; - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", - "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", - sans-serif; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -code { - font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", - monospace; -} diff --git a/src/logo.svg b/src/logo.svg deleted file mode 100644 index 8466738..0000000 --- a/src/logo.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/main.tsx b/src/main.tsx index 45c0705..ef92ba8 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,9 +1,9 @@ import React from "react" import { createRoot } from "react-dom/client" import { Provider } from "react-redux" + import App from "./App" -import { store } from "./app/store" -import "./index.css" +import store from "./app/store" const container = document.getElementById("root") diff --git a/src/pages/login/BaseForm.tsx b/src/pages/login/BaseForm.tsx new file mode 100644 index 0000000..eb516bf --- /dev/null +++ b/src/pages/login/BaseForm.tsx @@ -0,0 +1,45 @@ +import { Stack, Typography, useTheme } from "@mui/material" +import { type FormikValues } from "formik" + +import { Form, type FormProps } from "codeforlife/components/form" +import { ThemedBox, type ThemedBoxProps } from "codeforlife/theme" + +import { themeOptions } from "../../app/theme" + +export interface BaseFormProps extends FormProps { + themedBoxProps: Omit + header: string + subheader?: string +} + +const BaseForm = ({ + themedBoxProps, + header, + subheader, + ...formProps +}: BaseFormProps): JSX.Element => { + const theme = useTheme() + + return ( + + + + {header} + + {subheader && ( + + {subheader} + + )} +
+ + + ) +} + +export default BaseForm diff --git a/src/pages/login/IndyForm.tsx b/src/pages/login/IndyForm.tsx new file mode 100644 index 0000000..62b4608 --- /dev/null +++ b/src/pages/login/IndyForm.tsx @@ -0,0 +1,37 @@ +import { Stack } from "@mui/material" +import type { FC } from "react" + +import * as form from "codeforlife/components/form" +import { useNavigate } from "codeforlife/hooks" +import { submitForm } from "codeforlife/utils/form" + +import { useLoginWithEmailMutation } from "../../api/sso" +import { paths } from "../../router" +import BaseForm from "./BaseForm" + +export interface IndyFormProps {} + +const IndyForm: FC = () => { + const [loginWithEmail] = useLoginWithEmailMutation() + const navigate = useNavigate() + + return ( + { + navigate(paths.indy.dashboard._) + }, + })} + > + + Log in + + + ) +} + +export default IndyForm diff --git a/src/pages/login/Login.tsx b/src/pages/login/Login.tsx new file mode 100644 index 0000000..8406b28 --- /dev/null +++ b/src/pages/login/Login.tsx @@ -0,0 +1,51 @@ +import type { FC } from "react" +// import * as yup from "yup" + +import * as page from "codeforlife/components/page" +// import { useSearchParamEntries } from "codeforlife/hooks" +// import { tryValidateSync } from "codeforlife/utils/schema" + +import IndyForm from "./IndyForm" +import StudentForm from "./StudentForm" +import * as teacherForms from "./teacherForms" + +export interface LoginProps { + form: + | "teacher-password" + | "teacher-otp" + | "teacher-otp-bypass-token" + | "student" + | "indy" +} + +const Login: FC = ({ form }) => { + // const searchParams = tryValidateSync( + // useSearchParamEntries(), + // yup.object({ + // verifyEmail: yup.boolean().default(false), + // }), + // ) + + return ( + + {/* {searchParams?.verifyEmail && ( + + Your email address was successfully verified, please log in. + + )} */} + + { + { + "teacher-password": , + "teacher-otp": , + "teacher-otp-bypass-token": , + student: , + indy: , + }[form] + } + + + ) +} + +export default Login diff --git a/src/pages/login/StudentForm.tsx b/src/pages/login/StudentForm.tsx new file mode 100644 index 0000000..f359427 --- /dev/null +++ b/src/pages/login/StudentForm.tsx @@ -0,0 +1,37 @@ +import { Stack } from "@mui/material" +import type { FC } from "react" + +import * as form from "codeforlife/components/form" +import { useNavigate } from "codeforlife/hooks" +import { submitForm } from "codeforlife/utils/form" + +import { useLoginAsStudentMutation } from "../../api/sso" +import { paths } from "../../router" +import BaseForm from "./BaseForm" + +export interface StudentFormProps {} + +const StudentForm: FC = () => { + const [loginAsStudent] = useLoginAsStudentMutation() + const navigate = useNavigate() + + return ( + { + navigate(paths.indy.dashboard._) + }, + })} + > + + Log in + + + ) +} + +export default StudentForm diff --git a/src/pages/login/index.tsx b/src/pages/login/index.tsx new file mode 100644 index 0000000..e69de29 diff --git a/src/pages/login/teacherForms/Otp.tsx b/src/pages/login/teacherForms/Otp.tsx new file mode 100644 index 0000000..671047d --- /dev/null +++ b/src/pages/login/teacherForms/Otp.tsx @@ -0,0 +1,37 @@ +import { Stack } from "@mui/material" +import type { FC } from "react" + +import * as form from "codeforlife/components/form" +import { useNavigate } from "codeforlife/hooks" +import { submitForm } from "codeforlife/utils/form" + +import { useLoginWithOtpMutation } from "../../../api/sso" +import { paths } from "../../../router" +import BaseForm from "../BaseForm" + +export interface OtpProps {} + +const Otp: FC = () => { + const [loginWithOtp] = useLoginWithOtpMutation() + const navigate = useNavigate() + + return ( + { + navigate(paths.teacher.dashboard.school._) + }, + })} + > + + Log in + + + ) +} + +export default Otp diff --git a/src/pages/login/teacherForms/OtpBypassToken.tsx b/src/pages/login/teacherForms/OtpBypassToken.tsx new file mode 100644 index 0000000..e6bc378 --- /dev/null +++ b/src/pages/login/teacherForms/OtpBypassToken.tsx @@ -0,0 +1,37 @@ +import { Stack } from "@mui/material" +import type { FC } from "react" + +import * as form from "codeforlife/components/form" +import { useNavigate } from "codeforlife/hooks" +import { submitForm } from "codeforlife/utils/form" + +import { useLoginWithOtpBypassTokenMutation } from "../../../api/sso" +import { paths } from "../../../router" +import BaseForm from "../BaseForm" + +export interface OtpBypassTokenProps {} + +const OtpBypassToken: FC = () => { + const [loginWithOtpBypassToken] = useLoginWithOtpBypassTokenMutation() + const navigate = useNavigate() + + return ( + { + navigate(paths.teacher.dashboard.school._) + }, + })} + > + + Log in + + + ) +} + +export default OtpBypassToken diff --git a/src/pages/login/teacherForms/Password.tsx b/src/pages/login/teacherForms/Password.tsx new file mode 100644 index 0000000..7313511 --- /dev/null +++ b/src/pages/login/teacherForms/Password.tsx @@ -0,0 +1,53 @@ +import { Stack, Typography } from "@mui/material" +import type { FC } from "react" + +import * as form from "codeforlife/components/form" +import { Link } from "codeforlife/components/router" +import { useNavigate } from "codeforlife/hooks" +import { submitForm } from "codeforlife/utils/form" + +import { useLoginWithEmailMutation } from "../../../api/sso" +import { paths } from "../../../router" +import BaseForm from "../BaseForm" + +export interface PasswordProps {} + +const Password: FC = () => { + const [loginWithEmail] = useLoginWithEmailMutation() + const navigate = useNavigate() + + return ( + { + navigate( + auth_factors.includes("otp") + ? paths.login.teacher.otp._ + : paths.teacher.dashboard.school._, + ) + }, + })} + > + + + + + Forgotten your password? + + + Don't worry, you can  + reset your password. + + + + Log in + + + ) +} + +export default Password diff --git a/src/pages/login/teacherForms/index.tsx b/src/pages/login/teacherForms/index.tsx new file mode 100644 index 0000000..f9e2dc6 --- /dev/null +++ b/src/pages/login/teacherForms/index.tsx @@ -0,0 +1,12 @@ +import Otp, { type OtpProps } from "./Otp" +import OtpBypassToken, { type OtpBypassTokenProps } from "./OtpBypassToken" +import Password, { type PasswordProps } from "./Password" + +export { + Otp, + OtpBypassToken, + Password, + type OtpBypassTokenProps, + type OtpProps, + type PasswordProps, +} diff --git a/src/router/Router.tsx b/src/router/Router.tsx new file mode 100644 index 0000000..5758aab --- /dev/null +++ b/src/router/Router.tsx @@ -0,0 +1,28 @@ +import type { FC } from "react" +import { BrowserRouter, Routes } from "react-router-dom" + +// import Header from '../../features/header/Header'; +// import Footer from '../../features/footer/Footer'; +// import general from './routes/general'; +import authentication from "./routes/authentication" +// import teacher from './routes/teacher'; +// import student from './routes/student'; +// import error from './routes/error'; + +export interface RouterProps {} + +const Router: FC = () => ( + + {/*
*/} + + {/* {general} */} + {authentication} + {/* {teacher} */} + {/* {student} */} + {/* {error} */} {/* this must be last */} + + {/*