From f817343ad36ee160a77bf30328823a07a5736738 Mon Sep 17 00:00:00 2001 From: Akito123321 Date: Mon, 17 Jun 2024 13:04:31 +0200 Subject: [PATCH] it started --- package-lock.json | 18 ++++++ package.json | 2 + src/App.jsx | 25 -------- src/App.tsx | 106 +++++++++++++++++++++++++++++++ src/api.ts | 65 +++++++++++++++++++ src/components/Dialog.module.css | 46 ++++++++++++++ src/components/Dialog.tsx | 55 ++++++++++++++++ src/state.ts | 57 +++++++++++++++++ 8 files changed, 349 insertions(+), 25 deletions(-) delete mode 100644 src/App.jsx create mode 100644 src/App.tsx create mode 100644 src/api.ts create mode 100644 src/components/Dialog.module.css create mode 100644 src/components/Dialog.tsx create mode 100644 src/state.ts diff --git a/package-lock.json b/package-lock.json index 7223781..b842915 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,8 @@ "version": "0.0.0", "license": "MIT", "dependencies": { + "@solidjs/router": "^0.13.5", + "solid-icons": "^1.1.0", "solid-js": "^1.8.11" }, "devDependencies": { @@ -1230,6 +1232,14 @@ "solid-js": "^1.6.12" } }, + "node_modules/@solidjs/router": { + "version": "0.13.5", + "resolved": "https://registry.npmjs.org/@solidjs/router/-/router-0.13.5.tgz", + "integrity": "sha512-I/bR5ZHCz2Dx80qL+6uGwSdclqXRqoT49SJ5cvLbOuT3HnYysSIxSfULCTWUMLFVcgPh5GrdHV6KwEoyrbPZZA==", + "peerDependencies": { + "solid-js": "^1.8.6" + } + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -1766,6 +1776,14 @@ } } }, + "node_modules/solid-icons": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/solid-icons/-/solid-icons-1.1.0.tgz", + "integrity": "sha512-IesTfr/F1ElVwH2E1110s2RPXH4pujKfSs+koT8rwuTAdleO5s26lNSpqJV7D1+QHooJj18mcOiz2PIKs0ic+A==", + "peerDependencies": { + "solid-js": "*" + } + }, "node_modules/solid-js": { "version": "1.8.17", "resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.8.17.tgz", diff --git a/package.json b/package.json index 97827f6..95bf7fa 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,8 @@ "vite-plugin-solid": "^2.8.2" }, "dependencies": { + "@solidjs/router": "^0.13.5", + "solid-icons": "^1.1.0", "solid-js": "^1.8.11" } } diff --git a/src/App.jsx b/src/App.jsx deleted file mode 100644 index ead3703..0000000 --- a/src/App.jsx +++ /dev/null @@ -1,25 +0,0 @@ -import logo from './logo.svg'; -import styles from './App.module.css'; - -function App() { - return ( -
-
- logo -

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

- - Learn Solid - -
-
- ); -} - -export default App; diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..9464de4 --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,106 @@ +import { createEffect, type Component, createSignal, Show, For } from 'solid-js'; + +import styles from './App.module.css'; +import { A, HashRouter, Route, RouteSectionProps, useNavigate } from '@solidjs/router'; +import Login from './pages/Login'; +import Groups from './pages/Groups'; +import Account from './pages/Account'; +import GroupPage from './pages/GroupPage' + +import { BsArrowBarRight, BsBookmarkHeartFill, BsPeopleFill } from 'solid-icons/bs'; +import { API, api, setAPI, setAuthConfig } from './api'; +import { dialogs, localState, setLocalState, showMessageDialog } from './state'; +import Dialog from './components/Dialog'; +import { errorToString } from './util'; + +export interface APIConfig { + apiURL: string, +} + +const AppLayout: Component = (props) => { + const navigate = useNavigate(); + + const [loading, setLoading] = createSignal(true); + + createEffect(async () => { + try { + let config: APIConfig = await (await fetch('/config.json')).json(); + setAPI(new API(config.apiURL)); + + setAuthConfig(await api().authConfig()); + + let search = new URLSearchParams(window.location.search); + if (search.has("code")) { + console.log("code"); + let code = search.get("code"); + try { + let info = await api().login(code!); + setLocalState({ accountInfo: info }) + window.location.search = ""; + } catch (e) { + showMessageDialog("Failed to log in", errorToString(e)); + } + } + } catch (e) { + showMessageDialog("Failed to load", errorToString(e)); + } + + setLoading(false); + }, []); + + const logout = () => { + navigate("/"); + setLocalState({ accountInfo: null }); + }; + + return ( +
+ {d => } + Loading...
}> +
+
+ +
+ { + localState.accountInfo != null ? + <> + {localState.accountInfo.user.displayName} + + : + Not Logged In + } +
+
+
+
+ {props.children} +
+
+ + + ) +}; + +const App: Component = () => { + return ( + + + + + + {localState.accountInfo != null ? + <> + + : + <> + + + } + + ); +}; + +export default App; diff --git a/src/api.ts b/src/api.ts new file mode 100644 index 0000000..cc09563 --- /dev/null +++ b/src/api.ts @@ -0,0 +1,65 @@ +import { createSignal } from "solid-js"; +import { localState } from "./state"; + +export interface User { + username: string, + displayName: string, + googleId: string, +} + +export interface AuthenticationResponse { + user: User, + token: string, +} + +export interface AuthConfig { + clientId: string, + redirect_url: string, +} + +export class API { + public apiURL: string; + + constructor(apiURL: string) { + this.apiURL = apiURL; + } + + private async checkResponse(response: Response) { + if (!response.ok) { + let json = null; + try { + json = await response.json(); + } catch (e) { /* ignored */ } + + throw json?.error || 'Request failed: ' + response.status + ' ' + response.statusText; + } + } + + public ensureLoggedIn(): AuthenticationResponse { + const info = localState.accountInfo; + if (info == null) throw 'Not logged in'; + return info; + } + + public async authConfig(): Promise { + const response = await fetch(this.apiURL + '/config'); + + await this.checkResponse(response); + + return await response.json(); + } + + public async login(code: string): Promise { + const response = await fetch(this.apiURL + '/auth/login', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ code }) + }); + + await this.checkResponse(response); + + return await response.json(); + } +} \ No newline at end of file diff --git a/src/components/Dialog.module.css b/src/components/Dialog.module.css new file mode 100644 index 0000000..5dfbcd6 --- /dev/null +++ b/src/components/Dialog.module.css @@ -0,0 +1,46 @@ +.dialogContainer { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + background-color: rgba(0, 0, 0, 0.5); + z-index: 10; +} + +.dialog { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + z-index: 10; + background-color: var(--background); + padding: 10px; + border-radius: 5px; + min-width: 200px; +} + +.dialogTitle { + font-weight: bold; + font-size: 1.2em; +} + +.dialogText { + margin-top: 10px; + margin-bottom: 10px; + white-space: pre-wrap; +} + +.dialogButtons { + display: flex; + justify-content: end; + gap: 5px; +} + +.dangerButton { + background-color: var(--danger); +} + +.successButton { + background-color: var(--success); +} diff --git a/src/components/Dialog.tsx b/src/components/Dialog.tsx new file mode 100644 index 0000000..dcbadef --- /dev/null +++ b/src/components/Dialog.tsx @@ -0,0 +1,55 @@ +import { Component, For } from 'solid-js'; + +import styles from './Dialog.module.css'; + +export interface DialogButton { + name: string, + action?: () => void, + type?: 'default' | 'danger' | 'success' +} + +export interface DialogInputField { + placeholder: string, + onInput: (text: string) => void, +} + +export interface DialogProps { + title: string, + text: string, + buttons?: (string | DialogButton)[], + onDismiss?: () => void, + inputFields?: DialogInputField[], +} + +function createButton(button: (string | DialogButton), dismiss?: () => void) { + if (typeof button == 'string') { + return ; + } else { + return ; + } +} + +function createInputField(field: DialogInputField) { + return field.onInput(e.currentTarget.value)}> +} + +const Dialog: Component = props => { + const buttons: () => (string | DialogButton)[] = () => props.buttons == null ? [{ name: 'Okay', action: props.onDismiss }] : props.buttons; + + return ( +
+
+
{props.title}
+
{props.text}
+
+ {f => createInputField(f)} +
+
+ {b => createButton(b, props.onDismiss)} +
+
+
+ ); +}; + +export default Dialog; diff --git a/src/state.ts b/src/state.ts new file mode 100644 index 0000000..4e29dcb --- /dev/null +++ b/src/state.ts @@ -0,0 +1,57 @@ +import { createEffect, createSignal } from 'solid-js'; +import { SetStoreFunction, Store, createStore } from 'solid-js/store'; +import { AuthenticationResponse } from './api'; +import { DialogProps } from './components/Dialog'; + +// Source: https://www.solidjs.com/examples/todos +function createLocalStore( + name: string, + init: T +): [Store, SetStoreFunction] { + const localState = localStorage.getItem(name); + const [state, setState] = createStore( + localState ? JSON.parse(localState) : init + ); + createEffect(() => localStorage.setItem(name, JSON.stringify(state))); + return [state, setState]; +} + +export interface LocalState { + accountInfo: AuthenticationResponse | null, +} + +export const [localState, setLocalState] = createLocalStore('localState', { accountInfo: null }); + +export const [dialogs, setDialogs] = createSignal([] as DialogProps[]); + +export const showDialog = (dialog: DialogProps) => { + const newDialog: DialogProps = { + ...dialog, + onDismiss: () => { + setDialogs(ds => { + const newDs = [...ds]; + newDs.splice(ds.indexOf(newDialog), 1); + return newDs; + }); + dialog.onDismiss?.(); + } + }; + + setDialogs([...dialogs(), newDialog]); +}; + +export const showInputDialog = (title: string, message: string, placeholder: string, callback: (text: string) => void) => { + const [text, setText] = createSignal(''); + + showDialog({ + title, + text: message, + inputFields: [{ placeholder, onInput: setText }], + buttons: [{ name: 'Okay', action: () => callback(text()) }] + }); +} + +export const showMessageDialog = (title: string, text: string) => showDialog({ + title, + text +});