added api endpoints

This commit is contained in:
Akito123321 2024-06-17 21:32:54 +02:00
parent f817343ad3
commit 15134f2f43
20 changed files with 669 additions and 40 deletions

View File

@ -1,16 +1,19 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head>
<head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" /> <meta name="theme-color" content="#000000" />
<link rel="shortcut icon" type="image/ico" href="/src/assets/favicon.ico" /> <link rel="shortcut icon" type="image/ico" href="/src/assets/favicon.ico" />
<title>Solid App</title> <title>Play Book</title>
</head> </head>
<body>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript> <noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div> <div id="root"></div>
<script src="/src/index.jsx" type="module"></script> <script src="/src/index.tsx" type="module"></script>
</body> </body>
</html> </html>

4
public/config.json Normal file
View File

@ -0,0 +1,4 @@
{
"apiURL": "http://localhost:8080/api",
"redirectURI": "http://localhost:3000/"
}

View File

@ -1,33 +1,113 @@
.App { html {
text-align: center; background-color: var(--background);
} color: white;
.logo { --background: #202020;
animation: logo-spin infinite 20s linear; --background2: #101010;
height: 40vmin; --foreground: #303030;
pointer-events: none; --foreground2: #404040;
--text: white;
--danger: orangered;
--success: #080;
} }
.header { .header {
background-color: #282c34;
min-height: 100vh;
display: flex; display: flex;
flex-direction: column; justify-content: space-between;
align-items: center; align-items: center;
justify-content: center; flex-direction: row;
font-size: calc(10px + 2vmin); 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; color: white;
} }
.link { button {
color: #b318f0; background-color: var(--foreground);
outline: none;
border: none;
color: var(--text);
padding: 7px;
border-radius: 5px;
} }
@keyframes logo-spin { input {
from { background-color: var(--foreground);
transform: rotate(0deg); outline: none;
} border: none;
to { color: var(--text);
transform: rotate(360deg); 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;
} }

View File

@ -87,8 +87,6 @@ const AppLayout: Component<RouteSectionProps> = (props) => {
const App: Component = () => { const App: Component = () => {
return ( return (
<HashRouter root={AppLayout}> <HashRouter root={AppLayout}>
<Route path={['/', 'wishlists']} component={Wishlists} />
<Route path='wishlist/:id' component={WishlistPage} />
<Route path='groups' component={Groups} /> <Route path='groups' component={Groups} />
<Route path='group/:id' component={GroupPage} /> <Route path='group/:id' component={GroupPage} />
{localState.accountInfo != null ? {localState.accountInfo != null ?

View File

@ -17,6 +17,70 @@ export interface AuthConfig {
redirect_url: string, 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 { export class API {
public apiURL: string; public apiURL: string;
@ -62,4 +126,134 @@ export class API {
return await response.json(); return await response.json();
} }
public async getGroups(): Promise<Group[]> {
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<Group> {
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<Group> {
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<Group> {
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<Group> {
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<Group> {
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<Strat> {
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<void> {
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<Strat> {
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)

View File

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

View File

@ -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<GroupItemProps> = (props) => {
const navigate = useNavigate();
return (
<div class={styles.group} onclick={() => navigate('/group/' + props.group.id)}>
<div class={styles.groupName}>{props.group.name}</div>
</div>
)
};
export default GroupItem;

View File

@ -11,3 +11,8 @@ code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace; monospace;
} }
*[contentEditable="true"] {
min-width: 10px;
outline: 1px solid #aaa;
}

View File

@ -12,4 +12,4 @@ if (import.meta.env.DEV && !(root instanceof HTMLElement)) {
); );
} }
render(() => <App />, root); render(() => <App />, root!);

View File

@ -0,0 +1 @@

29
src/pages/Account.tsx Normal file
View File

@ -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 (
<div class={styles.page}>
<h1>Your Account</h1>
<hr />
<h2>Logged in as {localState.accountInfo!.user.displayName}</h2>
<button>Delete my account</button>
</div>
);
}
export default Account;

View File

152
src/pages/GroupPage.tsx Normal file
View File

@ -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 (
<>
<Show when={!loading()} fallback={<div>Loading...</div>}>
<div class={styles.groupName}>
<span contentEditable={editing()} innerText={group().name} onInput={e => setName(normalize(e.currentTarget.innerText, 50, true))} />
{editing() && <span class={styles.editCount}>{name().length}/50</span>}
</div>
<div class={styles.groupButtons}>
<button class={styles.wishlistButton} onClick={deleteGroup}>Delete group</button>
<button class={styles.editButton} onClick={toggleEdit}>{editing() ? <BsCheck2 /> : <BsPencil />}</button>
</div>
<hr />
<button class={styles.groupButton} onClick={addMember}>Add Member</button>
<div class={styles.groupMembers}>
{group().members?.map(m => <MemberItem group={group()} member={m} onDelete={onDeleteMember} />)}
</div>
<button class={styles.wishlistButton} onClick={addWishlist}>Add Wishlist</button>
<div class={styles.groupWishlists}>
{group().wishlists?.map(w => <div>
<WishlistItem wishlist={w} />
<button class={styles.removeWishlistButton} onClick={() => removeWishlist(w.id)}><BsTrash /></button>
</div>)}
</div>
</Show>
</>
)
}
export default GroupPage;

View File

@ -0,0 +1,11 @@
.page>h1 {
font-weight: bold;
margin: 0;
margin-bottom: 10px;
}
.groups {
display: flex;
flex-direction: column;
gap: 5px;
}

51
src/pages/Groups.tsx Normal file
View File

@ -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 (
<div class={styles.page}>
<h1>Your Groups</h1>
<button onclick={createGroup}>Create Group</button>
<hr />
<div class={styles.groups}>
<For each={groups()}>
{g => <GroupItem group={g} />}
</For>
</div>
</div>
)
}
export default Groups;

View File

@ -0,0 +1,3 @@
.page {
background-color: red;
}

22
src/pages/Login.tsx Normal file
View File

@ -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 <div>Login page</div>;
}
export default Login;

39
src/util.ts Normal file
View File

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

View File

@ -8,7 +8,9 @@
"esModuleInterop": true, "esModuleInterop": true,
"jsx": "preserve", "jsx": "preserve",
"jsxImportSource": "solid-js", "jsxImportSource": "solid-js",
"types": ["vite/client"], "types": [
"vite/client"
],
"noEmit": true, "noEmit": true,
"isolatedModules": true, "isolatedModules": true,
}, },