diff --git a/index.html b/index.html index 59e149c..899633b 100644 --- a/index.html +++ b/index.html @@ -1,16 +1,19 @@ - - - - - - Solid App - - - -
- - - + + + + + + Play Book + + + + +
+ + + + + \ No newline at end of file diff --git a/public/config.json b/public/config.json new file mode 100644 index 0000000..41417d4 --- /dev/null +++ b/public/config.json @@ -0,0 +1,4 @@ +{ + "apiURL": "http://localhost:8080/api", + "redirectURI": "http://localhost:3000/" +} diff --git a/src/App.module.css b/src/App.module.css index 48308b2..264a53e 100644 --- a/src/App.module.css +++ b/src/App.module.css @@ -1,33 +1,113 @@ -.App { - text-align: center; -} +html { + background-color: var(--background); + color: white; -.logo { - animation: logo-spin infinite 20s linear; - height: 40vmin; - pointer-events: none; + --background: #202020; + --background2: #101010; + --foreground: #303030; + --foreground2: #404040; + --text: white; + + --danger: orangered; + --success: #080; } .header { - background-color: #282c34; - min-height: 100vh; display: flex; - flex-direction: column; + justify-content: space-between; align-items: center; - justify-content: center; - font-size: calc(10px + 2vmin); + flex-direction: row; + background-color: var(--foreground); + height: 40px; +} + +.navigation { + display: flex; + flex-direction: row; + margin-left: 0; + padding: 0; + margin: 0; + height: 100%; +} + +.navigation>a { + list-style: none; + padding-left: 10px; + padding-right: 10px; + display: flex; + flex-direction: row; + align-items: center; + gap: 5px; +} + +.navigation>a:hover { + background-color: rgba(255, 255, 255, 0.1); +} + +a, +a:active, +a:visited { + color: var(--text); + text-decoration: none; + height: 100%; +} + +a:hover { color: white; } -.link { - color: #b318f0; +button { + background-color: var(--foreground); + outline: none; + border: none; + color: var(--text); + padding: 7px; + border-radius: 5px; } -@keyframes logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } +input { + background-color: var(--foreground); + outline: none; + border: none; + color: var(--text); + padding: 7px; + border-radius: 5px; } + +button:hover { + filter: brightness(1.2); + cursor: pointer; +} + +.loggedIn { + display: flex; + flex-direction: row; + align-items: center; + gap: 5px; + font-weight: bold; + padding-left: 10px; + padding-right: 10px; +} + +.loggedIn>button { + display: flex; + flex-direction: row; + align-items: center; +} + +.icon { + width: 16px; + height: 16px; +} + +.pageContainer { + display: flex; + flex-direction: column; + align-items: center; + padding: 10px; +} + +.pageContent { + width: 100%; + max-width: 75em; +} \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index 9464de4..baf7c57 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -87,8 +87,6 @@ const AppLayout: Component = (props) => { const App: Component = () => { return ( - - {localState.accountInfo != null ? diff --git a/src/api.ts b/src/api.ts index cc09563..1498b56 100644 --- a/src/api.ts +++ b/src/api.ts @@ -17,6 +17,70 @@ export interface AuthConfig { redirect_url: string, } +export interface Group { + id: string, + name: string, + owner: User, + members: User[], + strats: Strat[], +} + +export interface Strat { + id: string, + title: string, + description: string, + stratType: string, + attempts: number, + success: number + stratStates: StratState[], + playerTypes: PlayerType[], + map: Map, +} + +export interface Map { + id: string, + name: string, + image: File, +} + +export interface StratState { + id: string, + description: string, + image: File, + lineups: Lineup[], +} + +export interface Lineup { + id: string, + description: string, + image: File, +} + +export interface PlayerType { + id: string, + name: string, + task: string, + profiles: Profile[], +} + +export interface Profile { + id: string, + name: string, + image: File, +} + +export interface GroupRequest { + name: string, +} + +export interface StratRequest { + title: string, + description: string, + stratType: string, + attempts: number, + success: number, +} + export class API { public apiURL: string; @@ -62,4 +126,134 @@ export class API { return await response.json(); } -} \ No newline at end of file + + public async getGroups(): Promise { + const response = await fetch(this.apiURL + '/group', { + method: 'GET', + headers: { + 'Authorization': 'Bearer ' + this.ensureLoggedIn().token + } + }); + + await this.checkResponse(response); + + return await response.json(); + } + + public async createGroup(group: GroupRequest): Promise { + const response = await fetch(this.apiURL + '/group', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ' + this.ensureLoggedIn().token + }, + body: JSON.stringify(group) + }); + + await this.checkResponse(response); + + return await response.json(); + } + + public async updateGroup(id: string, group: GroupRequest): Promise { + const response = await fetch(this.apiURL + '/group/' + id, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ' + this.ensureLoggedIn().token + }, + body: JSON.stringify(group) + }); + + await this.checkResponse(response); + + return await response.json(); + } + + public async getGroup(id: string): Promise { + const response = await fetch(this.apiURL + '/group/' + id, { + method: 'GET', + headers: { + 'Authorization': 'Bearer ' + this.ensureLoggedIn().token + } + }); + + await this.checkResponse(response); + + return await response.json(); + } + + public async addMember(groupId: string, memberId: string): Promise { + const response = await fetch(this.apiURL + '/group/' + groupId + '/member', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ' + this.ensureLoggedIn().token + }, + body: JSON.stringify({ userId: memberId }) + }); + + await this.checkResponse(response); + + return await response.json(); + } + + public async removeMember(groupId: string, memberId: string): Promise { + const response = await fetch(this.apiURL + '/group/' + groupId + '/member/' + memberId, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ' + this.ensureLoggedIn().token + }, + }); + + await this.checkResponse(response); + + return await response.json(); + } + + public async addStrat(groupId: string, strat: StratRequest): Promise { + const response = await fetch(this.apiURL + '/group/' + groupId + '/strat', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ' + this.ensureLoggedIn().token + }, + body: JSON.stringify(strat) + }); + + await this.checkResponse(response); + + return await response.json(); + } + + public async removeStrat(groupId: string, stratId: string): Promise { + const response = await fetch(this.apiURL + '/group/' + groupId + '/strat/' + stratId, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ' + this.ensureLoggedIn().token + } + }); + + await this.checkResponse(response); + } + + public async updateStrat(groupId: string, stratId: string, strat: StratRequest): Promise { + const response = await fetch(this.apiURL + '/group/' + groupId + '/strat/' + stratId, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ' + this.ensureLoggedIn().token + }, + body: JSON.stringify(strat) + }); + + await this.checkResponse(response); + + return await response.json(); + } +} + +export const [api, setAPI] = createSignal(new API('')); +export const [authConfig, setAuthConfig] = createSignal(null as AuthConfig | null) diff --git a/src/components/GroupItem.module.css b/src/components/GroupItem.module.css new file mode 100644 index 0000000..b4ccff1 --- /dev/null +++ b/src/components/GroupItem.module.css @@ -0,0 +1,14 @@ +.group { + background-color: var(--foreground); + border-radius: 5px; + padding: 10px; +} + +.group:hover { + background-color: rgba(255, 255, 255, 0.1); + cursor: pointer; +} + +.groupName { + font-weight: bold; +} diff --git a/src/components/GroupItem.tsx b/src/components/GroupItem.tsx new file mode 100644 index 0000000..f0b4d03 --- /dev/null +++ b/src/components/GroupItem.tsx @@ -0,0 +1,21 @@ +import { Component } from "solid-js"; + +import styles from './GroupItem.module.css' +import { Group, Wishlist } from "../api"; +import { useNavigate } from "@solidjs/router"; + +export interface GroupItemProps { + group: Group, +} + +const GroupItem: Component = (props) => { + const navigate = useNavigate(); + + return ( +
navigate('/group/' + props.group.id)}> +
{props.group.name}
+
+ ) +}; + +export default GroupItem; diff --git a/src/index.css b/src/index.css index ec2585e..5c72e90 100644 --- a/src/index.css +++ b/src/index.css @@ -11,3 +11,8 @@ code { font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace; } + +*[contentEditable="true"] { + min-width: 10px; + outline: 1px solid #aaa; +} \ No newline at end of file diff --git a/src/index.jsx b/src/index.tsx similarity index 92% rename from src/index.jsx rename to src/index.tsx index 65fe07f..c09b9f9 100644 --- a/src/index.jsx +++ b/src/index.tsx @@ -12,4 +12,4 @@ if (import.meta.env.DEV && !(root instanceof HTMLElement)) { ); } -render(() => , root); +render(() => , root!); diff --git a/src/pages/Account.module.css b/src/pages/Account.module.css new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/pages/Account.module.css @@ -0,0 +1 @@ + diff --git a/src/pages/Account.tsx b/src/pages/Account.tsx new file mode 100644 index 0000000..54576c8 --- /dev/null +++ b/src/pages/Account.tsx @@ -0,0 +1,29 @@ +import { Component, createEffect, createSignal } from "solid-js"; + +import styles from './Account.module.css' +import { localState } from "../state"; +import { User, api, authConfig } from "../api"; + +const Account: Component = () => { + const [user, setUser] = createSignal({} as User); + + (async () => { + try { + if (localState.accountInfo == null) return; + } catch (e) { + } + })(); + + // TODO: Delete account + + return ( +
+

Your Account

+
+

Logged in as {localState.accountInfo!.user.displayName}

+ +
+ ); +} + +export default Account; diff --git a/src/pages/GroupPage.module.css b/src/pages/GroupPage.module.css new file mode 100644 index 0000000..e69de29 diff --git a/src/pages/GroupPage.tsx b/src/pages/GroupPage.tsx new file mode 100644 index 0000000..8e86b2e --- /dev/null +++ b/src/pages/GroupPage.tsx @@ -0,0 +1,152 @@ +import { useNavigate, useParams } from "@solidjs/router"; +import { Component, Show, createEffect, createSignal } from "solid-js"; + +import styles from './GroupPage.module.css' +import { Group, User, api } from "../api"; +import { showDialog, showInputDialog, showMessageDialog } from "../state"; +import { errorToString, normalize } from "../util"; +import { BsCheck2, BsPencil, BsTrash } from "solid-icons/bs"; + +const GroupPage: Component = () => { + const params = useParams(); + const navigate = useNavigate(); + + const [loading, setLoading] = createSignal(true); + const [group, setGroup] = createSignal({} as Group); + const [editing, setEditing] = createSignal(false); + const [name, setName] = createSignal(''); + + createEffect(async () => { + try { + setGroup(await api().getGroup(params.id)); + setName(group().name); + setLoading(false); + } catch (e) { + showMessageDialog('Failed to load group', errorToString(e)); + } + }, []); + + const updateGroup = async (newValue: Group) => { + try { + await api().updateGroup(group().id, newValue); + setGroup(newValue); + } catch (e) { + showMessageDialog('Failed to update group', errorToString(e)); + } + }; + + const deleteGroup = async () => { + showDialog({ + title: 'Delete group', + text: 'Do you want to delete this group?', + buttons: [ + { + name: 'Delete', + type: 'danger', + action: async () => { + try { + await api().deleteGroup(group().id); + navigate('/groups'); + } catch (e) { + showMessageDialog('Failed to delete group', errorToString(e)); + } + } + }, + { + name: 'Cancel', + action: () => { } + } + ] + }); + }; + + 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 addWishlist = async () => { + const userWishlists = await api().getWishlists(api().ensureLoggedIn().username); + showDialog({ + title: 'Add Wishlist', text: 'add wishlist to group', buttons: + [...userWishlists.map(w => { + return { + name: w.title, action: async () => { + try { + const newGroup = await api().addWishlistToGroup(group().id, w.id); + setGroup(newGroup); + } catch (e) { + showMessageDialog('Failed to add wishlist', errorToString(e)); + } + } + } + }), + 'cancel' + ] + }) + }; + + const removeWishlist = async (wishlistId: string) => { + showDialog({ + title: 'Remove Wishlist', text: 'remove wishlist from group', buttons: + [{ + name: 'remove', action: async () => { + const newGroup = await api().removeWishlistFromGroup(group().id, wishlistId); + setGroup(newGroup); + } + }, + 'cancel' + ] + }) + }; + + const toggleEdit = async () => { + if (!editing()) { + setEditing(true); + } else { + console.log(name()); + updateGroup({ ...group(), name: name() }); + setEditing(false); + } + }; + + const onDeleteMember = async (member: User) => { + setGroup({ ...group(), members: [...group().members.filter(m => m.username !== member.username)] }); + }; + + return ( + <> + Loading...}> +
+ setName(normalize(e.currentTarget.innerText, 50, true))} /> + {editing() && {name().length}/50} +
+
+ + +
+
+ +
+ {group().members?.map(m => )} +
+ +
+ {group().wishlists?.map(w =>
+ + +
)} +
+
+ + ) +} + +export default GroupPage; diff --git a/src/pages/Groups.module.css b/src/pages/Groups.module.css new file mode 100644 index 0000000..0a99acb --- /dev/null +++ b/src/pages/Groups.module.css @@ -0,0 +1,11 @@ +.page>h1 { + font-weight: bold; + margin: 0; + margin-bottom: 10px; +} + +.groups { + display: flex; + flex-direction: column; + gap: 5px; +} diff --git a/src/pages/Groups.tsx b/src/pages/Groups.tsx new file mode 100644 index 0000000..f6c0452 --- /dev/null +++ b/src/pages/Groups.tsx @@ -0,0 +1,51 @@ +import { Component, For, createSignal } from "solid-js"; + +import GroupItem from '../components/GroupItem' + +import styles from './Groups.module.css' +import { useNavigate } from "@solidjs/router"; +import { Group, api } from "../api"; +import { localState, showMessageDialog } from "../state"; +import { errorToString } from "../util"; + +const Groups: Component = () => { + const navigate = useNavigate(); + + const [loading, setLoading] = createSignal(true); + const [groups, setGroups] = createSignal([] as Group[]); + + (async () => { + try { + if (localState.accountInfo == null) return; + setGroups(await api().getGroups()); + setLoading(false); + } catch (e) { + showMessageDialog('Failed to load groups', errorToString(e)); + } + })(); + + const createGroup = async () => { + try { + const group = await api().createGroup({ name: localState.accountInfo!.user.displayName + "'s group" }); + navigate('/group/' + group.id); + } catch (e) { + showMessageDialog('Failed to create group', errorToString(e)) + } + + }; + + return ( +
+

Your Groups

+ +
+
+ + {g => } + +
+
+ ) +} + +export default Groups; diff --git a/src/pages/Login.module.css b/src/pages/Login.module.css new file mode 100644 index 0000000..59c23eb --- /dev/null +++ b/src/pages/Login.module.css @@ -0,0 +1,3 @@ +.page { + background-color: red; +} \ No newline at end of file diff --git a/src/pages/Login.tsx b/src/pages/Login.tsx new file mode 100644 index 0000000..6b1401e --- /dev/null +++ b/src/pages/Login.tsx @@ -0,0 +1,22 @@ +import { Component, createEffect } from "solid-js"; + +import styles from './Login.module.css' +import { localState } from "../state"; +import { authConfig } from "../api"; +import { Navigate, useNavigate } from "@solidjs/router"; + +const Login: Component = () => { + const navigate = useNavigate(); + + createEffect(() => { + if(localState.accountInfo == null) { + window.location.href = "https://accounts.google.com/o/oauth2/v2/auth?client_id=" + encodeURIComponent(authConfig()!.clientId) + "&redirect_uri=" + encodeURIComponent(authConfig()!.redirect_url) + "&access_type=offline&response_type=code&scope=profile&prompt=select_account"; + }else { + navigate('/'); + } + }, []); + + return
Login page
; +} + +export default Login; diff --git a/src/util.ts b/src/util.ts new file mode 100644 index 0000000..1c6377f --- /dev/null +++ b/src/util.ts @@ -0,0 +1,39 @@ +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); +} \ No newline at end of file diff --git a/jsconfig.json b/tsconfig.json similarity index 93% rename from jsconfig.json rename to tsconfig.json index 0aa8991..712e527 100644 --- a/jsconfig.json +++ b/tsconfig.json @@ -8,8 +8,10 @@ "esModuleInterop": true, "jsx": "preserve", "jsxImportSource": "solid-js", - "types": ["vite/client"], + "types": [ + "vite/client" + ], "noEmit": true, "isolatedModules": true, }, -} +} \ No newline at end of file diff --git a/vite.config.js b/vite.config.ts similarity index 100% rename from vite.config.js rename to vite.config.ts