From 1d050f014a5f6ec3604b260283bccac444ed99ab Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Mon, 17 Feb 2025 08:48:46 -0500 Subject: [PATCH] Set up rudimentary state, App, and a test for the App --- visions/client/package.json | 1 + visions/ui/src/App.css | 42 +++++++++---------- visions/ui/src/App.test.tsx | 49 ++++++++++++++++++++++ visions/ui/src/App.tsx | 44 +++++++------------ visions/ui/src/index.css | 84 ++++++++++++++++++------------------- visions/ui/src/main.tsx | 6 +-- visions/ui/src/state.ts | 72 +++++++++++++++++++++++++++++++ 7 files changed, 203 insertions(+), 95 deletions(-) create mode 100644 visions/ui/src/App.test.tsx create mode 100644 visions/ui/src/state.ts diff --git a/visions/client/package.json b/visions/client/package.json index 953c1ac..df47a63 100644 --- a/visions/client/package.json +++ b/visions/client/package.json @@ -3,6 +3,7 @@ "version": "0.0.1", "description": "Shared data types for Visions", "main": "visions.js", + "types": "dist/lib.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, diff --git a/visions/ui/src/App.css b/visions/ui/src/App.css index b9d355d..f44fb79 100644 --- a/visions/ui/src/App.css +++ b/visions/ui/src/App.css @@ -1,42 +1,42 @@ #root { - max-width: 1280px; - margin: 0 auto; - padding: 2rem; - text-align: center; + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; } .logo { - height: 6em; - padding: 1.5em; - will-change: filter; - transition: filter 300ms; + height: 6em; + padding: 1.5em; + will-change: filter; + transition: filter 300ms; } .logo:hover { - filter: drop-shadow(0 0 2em #646cffaa); + filter: drop-shadow(0 0 2em #646cffaa); } .logo.react:hover { - filter: drop-shadow(0 0 2em #61dafbaa); + filter: drop-shadow(0 0 2em #61dafbaa); } @keyframes logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } } @media (prefers-reduced-motion: no-preference) { - a:nth-of-type(2) .logo { - animation: logo-spin infinite 20s linear; - } + a:nth-of-type(2) .logo { + animation: logo-spin infinite 20s linear; + } } .card { - padding: 2em; + padding: 2em; } .read-the-docs { - color: #888; + color: #888; } diff --git a/visions/ui/src/App.test.tsx b/visions/ui/src/App.test.tsx new file mode 100644 index 0000000..b988c11 --- /dev/null +++ b/visions/ui/src/App.test.tsx @@ -0,0 +1,49 @@ +import { render } from '@testing-library/react' +import { State, Action, Controller, initialState, reducer } from './state' +import { Client, ClientResponse } from 'visions-client' +import { AuthResponse, SessionId, UserOverview } from 'visions-types' +import { useReducer } from 'react' +import App from './App' + +class MockClient implements Client { + constructor() {} + + async auth( + username: string, + password: string, + ): Promise>> { + if (username === 'vakarian' && password === 'aoeu') { + return { + status: 'ok', + content: { type: 'success', content: 'vakarian-session-id' }, + } + } else if (username === 'shephard' && password === 'aoeu') { + return { + status: 'ok', + content: { + type: 'password-reset', + content: 'shephard-session-id', + }, + } + } else { + return { status: 'unauthorized' } + } + } + + async listUsers( + sessionId: SessionId, + ): Promise> { + return { status: 'ok', content: [] } + } +} + + +describe('App tests', async () => { + it('shows the login page when the user isn not logged in', () => { + const client = new MockClient; + const [state, dispatch] = useReducer(reducer, initialState()) + let controller = new Controller(client, state, dispatch) + render() + expect(screen.getByText(/Login Page/)).toBeInTheDocument(); + }) +}) diff --git a/visions/ui/src/App.tsx b/visions/ui/src/App.tsx index 3d7ded3..55d2f61 100644 --- a/visions/ui/src/App.tsx +++ b/visions/ui/src/App.tsx @@ -1,35 +1,21 @@ -import { useState } from 'react' -import reactLogo from './assets/react.svg' -import viteLogo from '/vite.svg' import './App.css' +import { State, Controller, sessionId } from "./state" -function App() { - const [count, setCount] = useState(0) +const LoginPage = () => ( +
Login Page
+) - return ( - <> -
- - Vite logo - - - React logo - -
-

Vite + React

-
- -

- Edit src/App.tsx and save to test HMR -

-
-

- Click on the Vite and React logos to learn more -

- - ) +interface AppProps { + state: State + controller: Controller +} + +const App = ({ state, controller }: AppProps) => { + if (sessionId(state)) { +
User is logged in
+ } else { + + } } export default App diff --git a/visions/ui/src/index.css b/visions/ui/src/index.css index 6119ad9..279fff5 100644 --- a/visions/ui/src/index.css +++ b/visions/ui/src/index.css @@ -1,68 +1,68 @@ :root { - font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; - line-height: 1.5; - font-weight: 400; + font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; - color-scheme: light dark; - color: rgba(255, 255, 255, 0.87); - background-color: #242424; + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; - font-synthesis: none; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } a { - font-weight: 500; - color: #646cff; - text-decoration: inherit; + font-weight: 500; + color: #646cff; + text-decoration: inherit; } a:hover { - color: #535bf2; + color: #535bf2; } body { - margin: 0; - display: flex; - place-items: center; - min-width: 320px; - min-height: 100vh; + margin: 0; + display: flex; + place-items: center; + min-width: 320px; + min-height: 100vh; } h1 { - font-size: 3.2em; - line-height: 1.1; + font-size: 3.2em; + line-height: 1.1; } button { - border-radius: 8px; - border: 1px solid transparent; - padding: 0.6em 1.2em; - font-size: 1em; - font-weight: 500; - font-family: inherit; - background-color: #1a1a1a; - cursor: pointer; - transition: border-color 0.25s; + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; } button:hover { - border-color: #646cff; + border-color: #646cff; } button:focus, button:focus-visible { - outline: 4px auto -webkit-focus-ring-color; + outline: 4px auto -webkit-focus-ring-color; } @media (prefers-color-scheme: light) { - :root { - color: #213547; - background-color: #ffffff; - } - a:hover { - color: #747bff; - } - button { - background-color: #f9f9f9; - } + :root { + color: #213547; + background-color: #ffffff; + } + a:hover { + color: #747bff; + } + button { + background-color: #f9f9f9; + } } diff --git a/visions/ui/src/main.tsx b/visions/ui/src/main.tsx index bef5202..3b08b76 100644 --- a/visions/ui/src/main.tsx +++ b/visions/ui/src/main.tsx @@ -4,7 +4,7 @@ import './index.css' import App from './App.tsx' createRoot(document.getElementById('root')!).render( - - - , + + + , ) diff --git a/visions/ui/src/state.ts b/visions/ui/src/state.ts new file mode 100644 index 0000000..e46ccaf --- /dev/null +++ b/visions/ui/src/state.ts @@ -0,0 +1,72 @@ +import { Client } from 'visions-client' +import { ActionDispatch } from 'react' + +export type AuthState = + | { type: 'unauthed' } + | { type: 'authed'; sessionId: string } + +export type State = { + auth: AuthState +} + +export const initialState = (): State => ({ + auth: { type: 'unauthed' }, +}) + +export const sessionId = (state: State) => string | undefined { + if (state.type === 'authed') { + return state.sessionId + } else { + return undefined + } +} + +export type Action = { type: 'set-auth'; content: AuthState } + +export const reducer = (state: State, action: Action) => { + switch (action.type) { + case 'set-auth': { + return { ...state, auth: action.content } + } + default: { + return state + } + } +} + +export class Controller { + client: Client + state: State + dispatch: ActionDispatch<[action: Action]> + + constructor( + client: Client, + state: State, + dispatch: ActionDispatch<[action: Action]>, + ) { + this.client = client + this.state = state + this.dispatch = dispatch + } + + // On any request, there are four options. + // The request succeeds. No problem. + // The request succeeds, but the user needs to reset their password. + // The action fails. + // The HTTP request itself fails. + async auth(username: string, password: string) { + let response = await this.client.auth(username, password) + switch (response.status) { + case 'ok': { + this.dispatch({ + type: 'set-auth', + content: { + type: 'authed', + sessionId: response.content.content, + }, + }) + return + } + } + } +}