diff --git a/src/App.tsx b/src/App.tsx index bf507a8..a2585ee 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -5,7 +5,8 @@ import { A, HashRouter, Route, RouteSectionProps, useNavigate } from '@solidjs/r import Login from './pages/Login'; import Groups from './pages/Groups'; import Account from './pages/Account'; -import GroupPage from './pages/GroupPage' +import GroupPage from './pages/GroupPage'; +import StratPage from './pages/StratPage'; import { BsArrowBarRight, BsBookmarkHeartFill, BsPeopleFill } from 'solid-icons/bs'; import { API, api, setAPI, setAuthConfig } from './api'; @@ -88,6 +89,7 @@ const App: Component = () => { + {localState.accountInfo != null ? <> diff --git a/src/api.ts b/src/api.ts index 529f459..74a9b0b 100644 --- a/src/api.ts +++ b/src/api.ts @@ -81,13 +81,11 @@ export interface StratRequest { title: string, description: string, stratType: string, - attempts: number, - success: number, } export interface MapRequest { name: string, - image: File, + image: string, } export interface ProfileRequest { @@ -102,12 +100,12 @@ export interface PlayerTypeRequest { export interface StratStateRequest { description: string, - image: File, + image: string, } export interface LineupRequest { description: string, - image: File, + image: string, } export class API { diff --git a/src/components/Dialog.tsx b/src/components/Dialog.tsx index 308ab2e..d3e5f98 100644 --- a/src/components/Dialog.tsx +++ b/src/components/Dialog.tsx @@ -1,58 +1,54 @@ -import { Component, For, Setter, Show } from 'solid-js'; +import { Component, createSignal, For, JSX } from 'solid-js'; import styles from './Dialog.module.css'; -import { UploadFile, FileUploader } from '@solid-primitives/upload'; +import DialogButtonComponent from './DialogButtonComponent'; export interface DialogButton { name: string, - action?: () => void, - type?: 'default' | 'danger' | 'success', - closeOnClick?: boolean + action?: () => void | boolean | Promise | Promise, + type?: 'default' | 'danger' | 'success' } export interface DialogInputField { - placeholder: string, - onInput: (text: string) => void, -} - -export interface ImageInputField { name: string, - onInput: () => Setter, + placeholder?: string, + onInput: (text: string) => void, } export interface DialogProps { title: string, - text: string, + text?: string, buttons?: (string | DialogButton)[], onDismiss?: () => void, inputFields?: DialogInputField[], -} - -function createButton(button: (string | DialogButton), dismiss?: () => void) { //TODO configurable if dismiss on click - if (typeof button == 'string') { - return ; - } else { - return ; - } + content?: JSX.Element, } function createInputField(field: DialogInputField) { - return field.onInput(e.currentTarget.value)}> + return
+ {field.name} + field.onInput(e.currentTarget.value)}> +
; } const Dialog: Component = props => { - const buttons: () => (string | DialogButton)[] = () => props.buttons == null ? [{ name: 'Okay', action: props.onDismiss }] : props.buttons; + const buttons: () => (string | DialogButton)[] = () => props.buttons == null ? ["Okay"] : props.buttons; + const [loading, setLoading] = createSignal(false); return (
{props.title}
{props.text}
-
+ {props.inputFields && props.inputFields.length > 0 &&
{f => createInputField(f)} -
+
} + {props.content}
- {b => createButton(b, props.onDismiss)} + {b => //createButton(b, props.onDismiss); + + } +
diff --git a/src/components/DialogButtonComponent.tsx b/src/components/DialogButtonComponent.tsx new file mode 100644 index 0000000..f5f230d --- /dev/null +++ b/src/components/DialogButtonComponent.tsx @@ -0,0 +1,27 @@ +import { Component } from "solid-js"; +import { DialogButton } from "./Dialog"; +import styles from './Dialog.module.css'; + +export interface DialogButtonProps { + dismiss?: () => void + button: string | DialogButton + loading: boolean + setLoading: (b: boolean) => void +} + +const DialogButtonComponent: Component = (props) => { + if (typeof props.button == 'string') { + return ; + } else { + const clickHandler = () => { + props.setLoading(true); + Promise.resolve((props.button as DialogButton).action?.()).then(v => { + if (v === undefined || v === true) props.dismiss?.(); + props.setLoading(false); + }); + }; + return ; + } +} + +export default DialogButtonComponent; diff --git a/src/components/ProfileItem.tsx b/src/components/ProfileItem.tsx index 065b7cf..e77ae70 100644 --- a/src/components/ProfileItem.tsx +++ b/src/components/ProfileItem.tsx @@ -21,7 +21,6 @@ const ProfileItem: Component = (props) => { { name: 'Remove', type: 'danger', - closeOnClick: true, action: async () => { try { await api().removeProfileFromGroup(props.group.id, props.profile.id); @@ -31,11 +30,7 @@ const ProfileItem: Component = (props) => { } } }, - { - name: 'Cancel', - closeOnClick: true, - action: () => { } - } + 'Cancel' ] }); }; diff --git a/src/components/StratItem.tsx b/src/components/StratItem.tsx index 814d07d..24e9f30 100644 --- a/src/components/StratItem.tsx +++ b/src/components/StratItem.tsx @@ -4,6 +4,7 @@ import { Group, Strat, api } from "../api"; import { showDialog, showMessageDialog } from "../state"; import { errorToString } from "../util"; import { BsTrash } from 'solid-icons/bs'; +import { useNavigate } from '@solidjs/router'; export interface StratItemProps { group: Group, @@ -12,6 +13,7 @@ export interface StratItemProps { } const StratItem: Component = (props) => { + const navigate = useNavigate(); const deleteMember = async () => { showDialog({ @@ -41,10 +43,10 @@ const StratItem: Component = (props) => { return (
-
+
navigate('/group/' + props.group.id + '/strat/' + props.strat.id)}> + -
diff --git a/src/pages/GroupPage.tsx b/src/pages/GroupPage.tsx index 98c46e5..c38aac1 100644 --- a/src/pages/GroupPage.tsx +++ b/src/pages/GroupPage.tsx @@ -1,10 +1,10 @@ import { useNavigate, useParams } from "@solidjs/router"; -import { Component, Show, createEffect, createSignal } from "solid-js"; +import { Component, Show, createEffect, createSignal, onMount } from "solid-js"; import styles from './GroupPage.module.css' import { Group, Map, Profile, Strat, User, api } from "../api"; import { showDialog, showInputDialog, showMessageDialog } from "../state"; -import { _arrayBufferToBase64, errorToString, normalize } from "../util"; +import { _arrayBufferToBase64, addMap, addMember, addProfile, addStratType, errorToString, normalize } from "../util"; import { BsCheck2, BsPencil, BsTrash } from "solid-icons/bs"; import { Collapse } from "solid-collapse"; import MemberItem from "../components/MemberItem"; @@ -29,7 +29,7 @@ const GroupPage: Component = () => { const [profilesIsExpanded, setProfilesIsExpanded] = createSignal(false); const [stratTypesIsExpanded, setStratTypesIsExpanded] = createSignal(false); - createEffect(async () => { + onMount(async () => { try { setGroup(await api().getGroup(params.id)); setName(group().name); @@ -37,7 +37,7 @@ const GroupPage: Component = () => { } catch (e) { showMessageDialog('Failed to load group', errorToString(e)); } - }, []); + }); const updateGroup = async (newValue: Group) => { try { @@ -73,66 +73,34 @@ const GroupPage: Component = () => { }); }; - const addMember = async () => { - showInputDialog('Add member to Group', 'enter the member id you want to add', 'member ID', async (memberId) => { - try { - const newGroup = await api().addMember(group().id, memberId); - setGroup(newGroup); - } catch (e) { - showMessageDialog('Failed to add member', errorToString(e)); - } - }) - }; - - const addStratType = async () => { - showInputDialog('Add strat type to Group', 'enter the name you want to add', 'strat type', async (stratType) => { - try { - const newGroup = await api().addStratType(group().id, stratType); - setGroup(newGroup); - } catch (e) { - showMessageDialog('Failed to add strat type', errorToString(e)); - } - }) - }; - - const addProfile = async () => { - const [profileName, setProfileName] = createSignal(''); - const { files, selectFiles } = createFileUploader({ - multiple: false, - accept: "image/*", - }); - - let fileReader = new FileReader(); - - fileReader.onload = async event => { - try { - const newGroup = await api().addProfileToGroup(group().id, { name: profileName(), image: _arrayBufferToBase64(event.target!.result as ArrayBuffer) }); - setGroup(newGroup); - } catch (e) { - showMessageDialog('Failed to add profile', errorToString(e)); - } - } + const createStrat = async () => { + const [title, setTitle] = createSignal(''); + const [description, setDescription] = createSignal(''); + const [stratType, setStratType] = createSignal(''); showDialog({ - title: 'Add profile to Group', - text: 'create a new profile', + title: 'Add a strat to Group', + text: 'create a new strat', inputFields: [{ - onInput: setProfileName, - placeholder: 'name' + onInput: setTitle, + placeholder: 'title', + name: 'title' + }, { + onInput: setDescription, + placeholder: 'description', + name: 'description' }], onDismiss: async () => { - fileReader.readAsArrayBuffer(files()[0].file); + const strat = await api().addStrat(group().id, { title: title(), description: description(), stratType: stratType() }); + navigate('/group/' + group().id + '/strat/' + strat.id); }, - buttons: [{ - name: 'add image', action: () => selectFiles(([{ source, name, size, file }]) => { - console.log({ source, name, size, file }); - }), - closeOnClick: false - }, - 'confirm' - ], - }) - }; + content: , + buttons: ['confirm'], + + }); + } const toggleEdit = async () => { if (!editing()) { @@ -178,7 +146,7 @@ const GroupPage: Component = () => {
- +
{group().members?.map(m => )} @@ -186,6 +154,7 @@ const GroupPage: Component = () => {
+
{group().strats?.map(s => )} @@ -193,6 +162,7 @@ const GroupPage: Component = () => {
+
{group().maps?.map(m => )} @@ -200,7 +170,7 @@ const GroupPage: Component = () => {
- +
{group().stratTypes?.map(s => )} @@ -208,7 +178,7 @@ const GroupPage: Component = () => {
- +
{group().profiles?.map(p => )} diff --git a/src/pages/StratPage.tsx b/src/pages/StratPage.tsx new file mode 100644 index 0000000..ba7538d --- /dev/null +++ b/src/pages/StratPage.tsx @@ -0,0 +1,7 @@ +import { Component } from "solid-js"; + +const StratPage: Component = () => { + return <>strat page +} + +export default StratPage \ No newline at end of file diff --git a/src/state.ts b/src/state.ts index bbc371c..99bfe30 100644 --- a/src/state.ts +++ b/src/state.ts @@ -1,7 +1,7 @@ import { createEffect, createSignal } from 'solid-js'; import { SetStoreFunction, Store, createStore } from 'solid-js/store'; import { AuthenticationResponse } from './api'; -import { DialogInputField, DialogProps, ImageInputField } from './components/Dialog'; +import { DialogInputField, DialogProps } from './components/Dialog'; // Source: https://www.solidjs.com/examples/todos function createLocalStore( @@ -30,7 +30,9 @@ export const showDialog = (dialog: DialogProps) => { onDismiss: () => { setDialogs(ds => { const newDs = [...ds]; - newDs.splice(ds.indexOf(newDialog), 1); + let idx = ds.indexOf(newDialog); + if (idx == -1) return newDs; + newDs.splice(idx, 1); return newDs; }); dialog.onDismiss?.(); @@ -40,14 +42,14 @@ export const showDialog = (dialog: DialogProps) => { setDialogs([...dialogs(), newDialog]); }; -export const showInputDialog = (title: string, message: string, placeholder: string, callback: (text: string) => void) => { +export const showInputDialog = (title: string, message: string, name: string, callback: (text: string) => void) => { const [text, setText] = createSignal(''); showDialog({ title, text: message, - inputFields: [{ placeholder, onInput: setText }], - buttons: [{ name: 'Okay', action: () => callback(text()), closeOnClick: true }] + inputFields: [{ name, onInput: setText }], + buttons: [{ name: 'Okay', action: () => callback(text()) }, "cancel"] }); } @@ -56,7 +58,7 @@ export const showInputsDialog = async (title: string, message: string, inputFiel title, text: message, inputFields: inputFields, - buttons: [{ name: 'Okay', action: () => callback(), closeOnClick: true }] + buttons: [{ name: 'Okay', action: () => callback() }] }); } diff --git a/src/util.ts b/src/util.ts deleted file mode 100644 index 9f1a5ee..0000000 --- a/src/util.ts +++ /dev/null @@ -1,69 +0,0 @@ -export function errorToString(e: any): string { // eslint-disable-line @typescript-eslint/no-explicit-any - if (typeof e == 'string') { - return e; - } else if (e.toString) { - return e.toString() as string; - } else { - return 'Unknown error'; - } -} - -export function normalize(text: string, maxLength: number, oneLine: boolean) { - if (oneLine) text = text.replaceAll('\n', ''); - text = text.substring(0, maxLength); - return text; -} - -export function normalizeQuantity(text: string) { - let quantity = parseInt(text); - - if (isNaN(quantity) || quantity < 0) { - return 1; - } - - return quantity; -} - -function clamp(value: number, min: number, max: number) { - return Math.min(Math.max(value, min), max); -}; - -export function normalizeRating(text: string) { - let rating = parseInt(text); - - if (isNaN(rating)) { - return 1; - } - - return clamp(rating, 0, 5); -} - -export function _arrayBufferToBase64(buffer: ArrayBuffer) { - var binary = ''; - var bytes = new Uint8Array(buffer); - var len = bytes.byteLength; - for (var i = 0; i < len; i++) { - binary += String.fromCharCode(bytes[i]); - } - return window.btoa(binary); -} - -export const b64toBlob = (b64Data: string, contentType = '', sliceSize = 512) => { - const byteCharacters = atob(b64Data); - const byteArrays = []; - - for (let offset = 0; offset < byteCharacters.length; offset += sliceSize) { - const slice = byteCharacters.slice(offset, offset + sliceSize); - - const byteNumbers = new Array(slice.length); - for (let i = 0; i < slice.length; i++) { - byteNumbers[i] = slice.charCodeAt(i); - } - - const byteArray = new Uint8Array(byteNumbers); - byteArrays.push(byteArray); - } - - const blob = new Blob(byteArrays, { type: contentType }); - return blob; -} diff --git a/src/util.tsx b/src/util.tsx new file mode 100644 index 0000000..992291e --- /dev/null +++ b/src/util.tsx @@ -0,0 +1,164 @@ +import { createSignal } from "solid-js"; +import { api, Group } from "./api"; +import { showDialog, showInputDialog, showMessageDialog } from "./state"; +import { createFileUploader } from "@solid-primitives/upload"; + +export function errorToString(e: any): string { // eslint-disable-line @typescript-eslint/no-explicit-any + if (typeof e == 'string') { + return e; + } else if (e.toString) { + return e.toString() as string; + } else { + return 'Unknown error'; + } +} + +export function normalize(text: string, maxLength: number, oneLine: boolean) { + if (oneLine) text = text.replaceAll('\n', ''); + text = text.substring(0, maxLength); + return text; +} + +export function normalizeQuantity(text: string) { + let quantity = parseInt(text); + + if (isNaN(quantity) || quantity < 0) { + return 1; + } + + return quantity; +} + +function clamp(value: number, min: number, max: number) { + return Math.min(Math.max(value, min), max); +}; + +export function normalizeRating(text: string) { + let rating = parseInt(text); + + if (isNaN(rating)) { + return 1; + } + + return clamp(rating, 0, 5); +} + +export function _arrayBufferToBase64(buffer: ArrayBuffer) { + var binary = ''; + var bytes = new Uint8Array(buffer); + var len = bytes.byteLength; + for (var i = 0; i < len; i++) { + binary += String.fromCharCode(bytes[i]); + } + return window.btoa(binary); +} + +export const b64toBlob = (b64Data: string, contentType = '', sliceSize = 512) => { + const byteCharacters = atob(b64Data); + const byteArrays = []; + + for (let offset = 0; offset < byteCharacters.length; offset += sliceSize) { + const slice = byteCharacters.slice(offset, offset + sliceSize); + + const byteNumbers = new Array(slice.length); + for (let i = 0; i < slice.length; i++) { + byteNumbers[i] = slice.charCodeAt(i); + } + + const byteArray = new Uint8Array(byteNumbers); + byteArrays.push(byteArray); + } + + const blob = new Blob(byteArrays, { type: contentType }); + return blob; +} + +export const addMember = (group: Group, onClose: (newGroup: Group) => void) => { + showInputDialog('Add member to Group', 'enter the member id you want to add', 'member ID', async (memberId) => { + try { + onClose(await api().addMember(group.id, memberId)); + } catch (e) { + showMessageDialog('Failed to add member', errorToString(e)); + } + }) +}; + +export const addStratType = (group: Group, onClose: (newGroup: Group) => void) => { + showInputDialog('Add strat type to Group', 'enter the name you want to add', 'strat type', async (stratType) => { + try { + onClose(await api().addStratType(group.id, stratType)); + } catch (e) { + showMessageDialog('Failed to add strat type', errorToString(e)); + } + }) +}; + +export const addProfile = (group: Group, onClose: (newGroup: Group) => void) => { + const [profileName, setProfileName] = createSignal(''); + const { files, selectFiles } = createFileUploader({ + multiple: false, + accept: "image/*", + }); + + let fileReader = new FileReader(); + + fileReader.onload = async event => { + try { + onClose(await api().addProfileToGroup(group.id, { name: profileName(), image: _arrayBufferToBase64(event.target!.result as ArrayBuffer) })); + } catch (e) { + showMessageDialog('Failed to add profile', errorToString(e)); + } + } + + showDialog({ + title: 'Add profile to Group', + text: 'create a new profile', + inputFields: [{ + onInput: setProfileName, + placeholder: 'name', + name: 'name' + }], + onDismiss: async () => { + fileReader.readAsArrayBuffer(files()[0].file); + }, + content: , + buttons: ['confirm'], + }); +}; + +export const addMap = async (group: Group, onClose: (newGroup: Group) => void) => { + const [mapName, setMapName] = createSignal(''); + const { files, selectFiles } = createFileUploader({ + multiple: false, + accept: "image/*", + }); + + let fileReader = new FileReader(); + + fileReader.onload = async event => { + try { + onClose(await api().addMap(group.id, { name: mapName(), image: _arrayBufferToBase64(event.target!.result as ArrayBuffer) })); + } catch (e) { + showMessageDialog('Failed to add map', errorToString(e)); + } + } + + showDialog({ + title: 'Add map to Group', + text: 'create a new map', + inputFields: [{ + onInput: setMapName, + placeholder: 'name', + name: 'name' + }], + onDismiss: async () => { + fileReader.readAsArrayBuffer(files()[0].file); + }, + content: , + buttons: ['confirm'], + }); +}; \ No newline at end of file