it started
This commit is contained in:
parent
92e4417e33
commit
f817343ad3
18
package-lock.json
generated
18
package-lock.json
generated
@ -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",
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
25
src/App.jsx
25
src/App.jsx
@ -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
106
src/App.tsx
Normal 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
65
src/api.ts
Normal 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();
|
||||
}
|
||||
}
|
46
src/components/Dialog.module.css
Normal file
46
src/components/Dialog.module.css
Normal 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
55
src/components/Dialog.tsx
Normal 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
57
src/state.ts
Normal 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
|
||||
});
|
Loading…
Reference in New Issue
Block a user