added api endpoints
This commit is contained in:
parent
f817343ad3
commit
15134f2f43
15
index.html
15
index.html
@ -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
4
public/config.json
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"apiURL": "http://localhost:8080/api",
|
||||||
|
"redirectURI": "http://localhost:3000/"
|
||||||
|
}
|
@ -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;
|
||||||
}
|
}
|
@ -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 ?
|
||||||
|
194
src/api.ts
194
src/api.ts
@ -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)
|
||||||
|
14
src/components/GroupItem.module.css
Normal file
14
src/components/GroupItem.module.css
Normal 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;
|
||||||
|
}
|
21
src/components/GroupItem.tsx
Normal file
21
src/components/GroupItem.tsx
Normal 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;
|
@ -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;
|
||||||
|
}
|
@ -12,4 +12,4 @@ if (import.meta.env.DEV && !(root instanceof HTMLElement)) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
render(() => <App />, root);
|
render(() => <App />, root!);
|
1
src/pages/Account.module.css
Normal file
1
src/pages/Account.module.css
Normal file
@ -0,0 +1 @@
|
|||||||
|
|
29
src/pages/Account.tsx
Normal file
29
src/pages/Account.tsx
Normal 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;
|
0
src/pages/GroupPage.module.css
Normal file
0
src/pages/GroupPage.module.css
Normal file
152
src/pages/GroupPage.tsx
Normal file
152
src/pages/GroupPage.tsx
Normal 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;
|
11
src/pages/Groups.module.css
Normal file
11
src/pages/Groups.module.css
Normal 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
51
src/pages/Groups.tsx
Normal 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;
|
3
src/pages/Login.module.css
Normal file
3
src/pages/Login.module.css
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
.page {
|
||||||
|
background-color: red;
|
||||||
|
}
|
22
src/pages/Login.tsx
Normal file
22
src/pages/Login.tsx
Normal 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
39
src/util.ts
Normal 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);
|
||||||
|
}
|
@ -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,
|
||||||
},
|
},
|
Loading…
Reference in New Issue
Block a user