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 (
-
-
-
- );
-}
-
-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...
}>
+
+
+
+
+ )
+};
+
+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
+});