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",
|
"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",
|
||||||
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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