it started

This commit is contained in:
Akito123321 2024-06-17 13:04:31 +02:00
parent 92e4417e33
commit f817343ad3
8 changed files with 349 additions and 25 deletions

18
package-lock.json generated
View File

@ -9,6 +9,8 @@
"version": "0.0.0", "version": "0.0.0",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@solidjs/router": "^0.13.5",
"solid-icons": "^1.1.0",
"solid-js": "^1.8.11" "solid-js": "^1.8.11"
}, },
"devDependencies": { "devDependencies": {
@ -1230,6 +1232,14 @@
"solid-js": "^1.6.12" "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": { "node_modules/@types/babel__core": {
"version": "7.20.5", "version": "7.20.5",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", "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": { "node_modules/solid-js": {
"version": "1.8.17", "version": "1.8.17",
"resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.8.17.tgz", "resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.8.17.tgz",

View File

@ -15,6 +15,8 @@
"vite-plugin-solid": "^2.8.2" "vite-plugin-solid": "^2.8.2"
}, },
"dependencies": { "dependencies": {
"@solidjs/router": "^0.13.5",
"solid-icons": "^1.1.0",
"solid-js": "^1.8.11" "solid-js": "^1.8.11"
} }
} }

View File

@ -1,25 +0,0 @@
import logo from './logo.svg';
import styles from './App.module.css';
function App() {
return (
<div class={styles.App}>
<header class={styles.header}>
<img src={logo} class={styles.logo} alt="logo" />
<p>
Edit <code>src/App.jsx</code> and save to reload.
</p>
<a
class={styles.link}
href="https://github.com/solidjs/solid"
target="_blank"
rel="noopener noreferrer"
>
Learn Solid
</a>
</header>
</div>
);
}
export default App;

106
src/App.tsx Normal file
View File

@ -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<RouteSectionProps> = (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 (
<div class={styles.App}>
<For each={dialogs()}>{d => <Dialog {...d} />}</For>
<Show when={!loading()} fallback={<div>Loading...</div>}>
<header class={styles.header}>
<div></div>
<nav class={styles.navigation}>
<A href='/wishlists'><BsBookmarkHeartFill class={styles.icon} />Wishlists</A>
<A href='/groups'><BsPeopleFill class={styles.icon} /> Groups</A>
</nav>
<div class={styles.loggedIn}>
{
localState.accountInfo != null ?
<>
<A href='/account'>{localState.accountInfo.user.displayName}</A>
<button onClick={logout}><BsArrowBarRight class={styles.icon} /></button>
</> :
<A href='/login'>Not Logged In</A>
}
</div>
</header>
<div class={styles.pageContainer}>
<div class={styles.pageContent}>
{props.children}
</div>
</div>
</Show>
</div >
)
};
const App: Component = () => {
return (
<HashRouter root={AppLayout}>
<Route path={['/', 'wishlists']} component={Wishlists} />
<Route path='wishlist/:id' component={WishlistPage} />
<Route path='groups' component={Groups} />
<Route path='group/:id' component={GroupPage} />
{localState.accountInfo != null ?
<>
<Route path='account' component={Account} />
</> :
<>
<Route path='login' component={Login} />
</>
}
</HashRouter >
);
};
export default App;

65
src/api.ts Normal file
View File

@ -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<AuthConfig> {
const response = await fetch(this.apiURL + '/config');
await this.checkResponse(response);
return await response.json();
}
public async login(code: string): Promise<AuthenticationResponse> {
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();
}
}

View File

@ -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);
}

55
src/components/Dialog.tsx Normal file
View File

@ -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 <button onclick={dismiss} class={styles.defaultButton}>{button}</button>;
} else {
return <button onclick={() => { button.action?.(); dismiss?.(); }} class={styles[button.type + 'Button']}>{button.name}</button>;
}
}
function createInputField(field: DialogInputField) {
return <input placeholder={field.placeholder} onInput={e => field.onInput(e.currentTarget.value)}></input>
}
const Dialog: Component<DialogProps> = props => {
const buttons: () => (string | DialogButton)[] = () => props.buttons == null ? [{ name: 'Okay', action: props.onDismiss }] : props.buttons;
return (
<div class={styles.dialogContainer}>
<div class={styles.dialog}>
<div class={styles.dialogTitle}>{props.title}</div>
<div class={styles.dialogText}>{props.text}</div>
<div class={styles.dialogInputFields}>
<For each={props.inputFields}>{f => createInputField(f)}</For>
</div>
<div class={styles.dialogButtons}>
<For each={buttons()}>{b => createButton(b, props.onDismiss)}</For>
</div>
</div>
</div>
);
};
export default Dialog;

57
src/state.ts Normal file
View File

@ -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<T extends object>(
name: string,
init: T
): [Store<T>, SetStoreFunction<T>] {
const localState = localStorage.getItem(name);
const [state, setState] = createStore<T>(
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>('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
});