From 00a6629e6057aaac748b99a7aa2c73448c2a07c7 Mon Sep 17 00:00:00 2001 From: Benjamin Sherriff Date: Thu, 19 Oct 2023 19:48:24 -0400 Subject: [PATCH 01/24] Updated fetch cookie --- ui/src/api/auth.ts | 41 +++++++++++++++++----- ui/src/api/guilds.ts | 33 +++++++++--------- ui/src/api/index.ts | 55 ++++++++++++------------------ ui/src/api/spells.ts | 36 +++++++++---------- ui/src/components/Topbar/index.tsx | 25 +++++++------- 5 files changed, 101 insertions(+), 89 deletions(-) diff --git a/ui/src/api/auth.ts b/ui/src/api/auth.ts index 9b460b0..f757fd6 100644 --- a/ui/src/api/auth.ts +++ b/ui/src/api/auth.ts @@ -1,17 +1,18 @@ -import { getRequest, postRequest } from '.'; +import Cookies from 'js-cookie'; +import { get, post } from '.'; import { RegisterUser, ResponseAuth } from './auth.types'; export async function login(email: string, password: string): Promise { - const response = await postRequest('auth/login', { email, password }); + const response = await post('auth/login', { email, password }); if (response?.status === 200) { - return response.data as ResponseAuth; + return response.json(); } else { return undefined; } } export async function register(user: RegisterUser): Promise { - const response = await postRequest('auth/register', user); + const response = await post('auth/register', user); if (response?.status === 201) { return true; } else { @@ -20,23 +21,45 @@ export async function register(user: RegisterUser): Promise { } export async function logout() { - return await postRequest('auth/logout', {}); + return await post('auth/logout', {}); } export async function refresh(refresh_token_rotation?: boolean): Promise { - const response = await getRequest('auth/refresh', { params: { refresh_token_rotation } }); + const response = await get('auth/refresh', { params: { refresh_token_rotation } }); if (response?.status === 200) { - return response.data as ResponseAuth; + return response.json(); } else { return undefined; } } export async function me(): Promise { - const response = await getRequest('auth/me'); + const response = await get('auth/me'); if (response?.status === 200) { - return response.data; + return response.json(); } else { return undefined; } } + +/** + * Refreshes the logged_in cookie every interval. By default, the interval is 14 minutes. + * @param interval + * @returns interval id + */ +export function refreshLoggedIn(interval = 840000) { + let loggedIn = Cookies.get('logged_in'); + const id = setInterval(async () => { + const cookie = Cookies.get('logged_in'); + if (cookie != loggedIn) { + loggedIn = cookie; + const response = await refresh(true); + if (response) { + Cookies.set('logged_in', 'true'); + } else { + Cookies.set('test', 'failed'); + } + } + }, interval); + return id; +} diff --git a/ui/src/api/guilds.ts b/ui/src/api/guilds.ts index a3f662e..0a3fd1d 100644 --- a/ui/src/api/guilds.ts +++ b/ui/src/api/guilds.ts @@ -1,50 +1,51 @@ -import { getRequest, postRequest } from '.'; +import { get, post } from '.'; import { GuildChannel, GuildInfo } from './guilds.types'; export async function getGuilds(): Promise { - const response = await getRequest('guilds'); - return response?.data || { data: [] }; + const response = await get('guilds'); + return response?.json() || { data: [] }; } export async function getTextChannels(guildId: number): Promise { - const response = await getRequest(`guilds/${guildId}/text`); - return response?.data || { data: [] }; + const response = await get(`guilds/${guildId}/text`); + return response?.json() || { data: [] }; } export async function sendMessage(guildId: number, channelId: number, message: string): Promise { - await postRequest(`guilds/${guildId}/text/${channelId}/message`, { message }); + await post(`guilds/${guildId}/text/${channelId}/message`, { message }); } export async function getVoiceChannels(guildId: number): Promise { - const response = await getRequest(`guilds/${guildId}/voice`); - return response?.data || { data: [] }; + const response = await get(`guilds/${guildId}/voice`); + return response?.json() || { data: [] }; } export async function playTrack(guildId: number, channelId: number, track: string): Promise { - await postRequest(`guilds/${guildId}/voice/${channelId}/play`, { track_url: track }); + await post(`guilds/${guildId}/voice/${channelId}/play`, { track_url: track }); } export async function stopTrack(guildId: number): Promise { - await postRequest(`guilds/${guildId}/voice/stop`, {}); + await post(`guilds/${guildId}/voice/stop`, {}); } export async function pauseTrack(guildId: number): Promise { - await postRequest(`guilds/${guildId}/voice/pause`, {}); + await post(`guilds/${guildId}/voice/pause`, {}); } export async function resumeTrack(guildId: number): Promise { - await postRequest(`guilds/${guildId}/voice/resume`, {}); + await post(`guilds/${guildId}/voice/resume`, {}); } export async function setVolume(guildId: number, volume: number): Promise { - await postRequest(`guilds/${guildId}/voice/volume`, { volume: `${volume}` }); + await post(`guilds/${guildId}/voice/volume`, { volume: `${volume}` }); } export async function skipTrack(guildId: number): Promise { - await postRequest(`guilds/${guildId}/voice/skip`, {}); + await post(`guilds/${guildId}/voice/skip`, {}); } export async function getVolume(guildId: number): Promise { - const response = await getRequest(`guilds/${guildId}/voice/volume`); - return response?.data?.volume || 0; + const response = await get(`guilds/${guildId}/voice/volume`); + const volume: number = await response?.json(); + return volume || 0; } diff --git a/ui/src/api/index.ts b/ui/src/api/index.ts index 5facad5..d990220 100644 --- a/ui/src/api/index.ts +++ b/ui/src/api/index.ts @@ -1,43 +1,32 @@ -import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'; +// import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'; const serviceHost = process.env.SERVICE_HOST || 'http://localhost'; const servicePort = process.env.SERVICE_PORT || 5000; +const baseURL = `${serviceHost}:${servicePort}`; -function createAxiosClient(): AxiosInstance { - const axiosClient = axios.create({ - baseURL: `${serviceHost}:${servicePort}` +export async function get(endpoint: string, params: Record = {}): Promise { + // Remove undefined params + Object.keys(params).forEach((key) => params[key] === undefined && delete params[key]); + const urlParams = new URLSearchParams(params); + const url = urlParams ? `${baseURL}/${endpoint}?${urlParams}` : `${baseURL}/${endpoint}`; + const response = await fetch(url, { + method: 'GET', + credentials: 'include' }); + return response; +} - axiosClient.interceptors.request.use( - (request) => { - request.withCredentials = true; - return request; +export async function post(endpoint: string, body = {}): Promise { + const url = `${baseURL}/${endpoint}`; + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' }, - (error) => { - console.error(error); - return Promise.reject(error); - } - ); - return axiosClient; -} - -const axiosClient = createAxiosClient(); - -export async function getRequest( - url: string, - config?: AxiosRequestConfig -): Promise | undefined> { - const response = await axiosClient.get(`/${url}`, config); - return response || undefined; -} - -export async function postRequest( - url: string, - data?: any, - config?: AxiosRequestConfig -): Promise | undefined> { - const response = await axiosClient.post(`/${url}`, data, config); - return response || undefined; + credentials: 'include', + body: JSON.stringify(body) + }); + return response; } export interface Metadata { diff --git a/ui/src/api/spells.ts b/ui/src/api/spells.ts index 426c5d3..db838f4 100644 --- a/ui/src/api/spells.ts +++ b/ui/src/api/spells.ts @@ -1,4 +1,4 @@ -import { getRequest } from '.'; +import { get } from '.'; import { GetSpellsResponse } from './spells.types'; interface GetSpellsParams { @@ -19,23 +19,21 @@ interface GetSpellsParams { } export async function getSpells(params?: GetSpellsParams): Promise { - const response = await getRequest('dnd/spells', { - params: { - name: params?.name, - like_name: params?.like_name, - schools: params?.schools?.join(','), - levels: params?.levels?.join(','), - ritual: params?.ritual, - concentration: params?.concentration, - classes: params?.classes?.join(','), - damage_inflict: params?.damage_inflict?.join(','), - damage_resist: params?.damage_resist?.join(','), - conditions: params?.conditions?.join(','), - saving_throw: params?.saving_throw?.join(','), - attack_type: params?.attack_type?.join(','), - limit: params?.limit, - page: params?.page - } + const response = await get('dnd/spells', { + name: params?.name, + like_name: params?.like_name, + schools: params?.schools?.join(','), + levels: params?.levels?.join(','), + ritual: params?.ritual, + concentration: params?.concentration, + classes: params?.classes?.join(','), + damage_inflict: params?.damage_inflict?.join(','), + damage_resist: params?.damage_resist?.join(','), + conditions: params?.conditions?.join(','), + saving_throw: params?.saving_throw?.join(','), + attack_type: params?.attack_type?.join(','), + limit: params?.limit, + page: params?.page }); - return response?.data || { data: [] }; + return response?.json() || { data: [] }; } diff --git a/ui/src/components/Topbar/index.tsx b/ui/src/components/Topbar/index.tsx index b68a663..7efb7bb 100644 --- a/ui/src/components/Topbar/index.tsx +++ b/ui/src/components/Topbar/index.tsx @@ -24,7 +24,7 @@ import { import Cookies from 'js-cookie'; import { useEffect, useState } from 'react'; import { useForm } from '@mantine/form'; -import { login, register, logout, me, refresh } from '@/api/auth'; +import { login, register, logout, me, refreshLoggedIn } from '@/api/auth'; import { User } from '@/api/auth.types'; import { useToggle } from '@mantine/hooks'; import { notifications } from '@mantine/notifications'; @@ -76,23 +76,18 @@ export default function Topbar() { const [modalType, toggle] = useToggle([undefined, 'login', 'register', 'reset']); const [headers, setHeaders] = useState([]); const [user, setUser] = useState(undefined); + const [refreshId, setRefreshId] = useState(undefined); + useEffect(() => { if (Cookies.get('logged_in')) { me().then((response) => { if (response) { + setRefreshId(refreshLoggedIn()); setUser(response.user); } }); - } else { - refresh(true).then((response) => { - if (response) { - setUser(response.user); - } else { - setUser(undefined); - } - }); } - }, [pathName]); + }, []); useEffect(() => { const h: HeaderItem[] = []; @@ -172,6 +167,9 @@ export default function Topbar() { const response = await logout(); if (response?.status == 200) { Cookies.remove('logged_in'); + if (refreshId) { + clearInterval(refreshId); + } setUser(undefined); } }} @@ -193,7 +191,7 @@ export default function Topbar() { )} - + ); } @@ -202,9 +200,10 @@ interface LoginModalProps { type?: string; toggle: any; setUser: (user: User) => void; + setRefreshId: (id: NodeJS.Timeout) => void; } -function LoginModal({ type, toggle, setUser }: LoginModalProps) { +function LoginModal({ type, toggle, setUser, setRefreshId }: LoginModalProps) { function passwordValidator(value: string) { if (value.trim().length < 10) { return 'Password must be at least 10 characters'; @@ -325,6 +324,7 @@ function LoginModal({ type, toggle, setUser }: LoginModalProps) { const loginResponse = await login(values.email, values.password); if (loginResponse) { setUser(loginResponse.user); + setRefreshId(refreshLoggedIn()); onClose(); notifications.update({ id, @@ -400,6 +400,7 @@ function LoginModal({ type, toggle, setUser }: LoginModalProps) { const response = await login(values.email, values.password); if (response) { setUser(response.user); + setRefreshId(refreshLoggedIn()); onClose(); } else { notifications.show({ From f358b6c467c7a6cabb3c294b790a9ad28222fb06 Mon Sep 17 00:00:00 2001 From: Benjamin Sherriff Date: Thu, 19 Oct 2023 20:35:43 -0400 Subject: [PATCH 02/24] Split up topbar --- ui/src/app/profile/page.tsx | 5 + ui/src/components/Topbar/LoginModal.tsx | 260 +++++++++++++++++++ ui/src/components/Topbar/headerItems.ts | 41 +++ ui/src/components/Topbar/index.tsx | 324 +----------------------- 4 files changed, 315 insertions(+), 315 deletions(-) create mode 100644 ui/src/app/profile/page.tsx create mode 100644 ui/src/components/Topbar/LoginModal.tsx create mode 100644 ui/src/components/Topbar/headerItems.ts diff --git a/ui/src/app/profile/page.tsx b/ui/src/app/profile/page.tsx new file mode 100644 index 0000000..e892a07 --- /dev/null +++ b/ui/src/app/profile/page.tsx @@ -0,0 +1,5 @@ +import React from 'react'; + +export default function Page() { + return <>; +} diff --git a/ui/src/components/Topbar/LoginModal.tsx b/ui/src/components/Topbar/LoginModal.tsx new file mode 100644 index 0000000..7adde5a --- /dev/null +++ b/ui/src/components/Topbar/LoginModal.tsx @@ -0,0 +1,260 @@ +'use client'; + +import { login, register, refreshLoggedIn } from '@/api/auth'; +import { User } from '@/api/auth.types'; +import { + Modal, + Container, + Title, + Anchor, + Paper, + TextInput, + Button, + PasswordInput, + Group, + Checkbox, + Text +} from '@mantine/core'; +import { useForm } from '@mantine/form'; +import { notifications } from '@mantine/notifications'; + +interface LoginModalProps { + type?: string; + toggle: any; + setUser: (user: User) => void; + setRefreshId: (id: NodeJS.Timeout) => void; +} + +export function LoginModal({ type, toggle, setUser, setRefreshId }: LoginModalProps) { + function passwordValidator(value: string) { + if (value.trim().length < 10) { + return 'Password must be at least 10 characters'; + } + if (value.trim().length >= 128) { + return 'Password must be at most 128 characters'; + } + if (!/(\d)/.test(value)) { + return 'Password must contain at least one number'; + } + if (!/[a-z]/.test(value)) { + return 'Password must contain at least one lowercase letter'; + } + if (!/[A-Z]/.test(value)) { + return 'Password must contain at least one uppercase letter'; + } + if (!/[!@#$%^&*]/.test(value)) { + return 'Password must contain at least one special character'; + } + return null; + } + + function emailValidator(value: string) { + if (value.trim().length == 0) { + return 'Email is required'; + } + if (!/^\S+@\S+$/.test(value)) { + return 'Invalid email'; + } + return null; + } + + const registerForm = useForm({ + initialValues: { + firstName: '', + lastName: '', + email: '', + password: '' + }, + validate: { + firstName: (value) => (value.trim().length > 0 ? null : 'First name is required'), + lastName: (value) => (value.trim().length > 0 ? null : 'Last name is required'), + email: emailValidator, + password: passwordValidator + } + }); + + const loginForm = useForm({ + initialValues: { + email: '', + password: '', + remember: false + } + }); + + const resetForm = useForm({ + initialValues: { + email: '' + } + }); + + function onClose() { + toggle(undefined); + registerForm.reset(); + resetForm.reset(); + if (!loginForm.values.remember) { + loginForm.reset(); + } + } + + return ( + + {type == 'reset' ? ( + + Reset password + + Enter your email and we will send you a link to reset your password.{' '} + toggle('login')}> + Go Back + + + +
console.log(values))}> + + + +
+
+ ) : type == 'register' ? ( + + Create account + + Already have an account?{' '} + toggle('login')}> + Sign in + + + + +
{ + const id = notifications.show({ + loading: true, + title: `Creating account`, + message: `Please wait...`, + autoClose: false, + withCloseButton: false + }); + const registerResponse = await register({ + first_name: values.firstName, + last_name: values.lastName, + email: values.email, + password: values.password + }); + if (registerResponse) { + const loginResponse = await login(values.email, values.password); + if (loginResponse) { + setUser(loginResponse.user); + setRefreshId(refreshLoggedIn()); + onClose(); + notifications.update({ + id, + title: `Account created`, + message: `Welcome ${loginResponse.user.first_name}!`, + color: 'green', + autoClose: 2000, + loading: false + }); + } else { + notifications.update({ + id, + title: `Unable to Login`, + message: `Please try again.`, + color: 'red', + autoClose: 2000, + loading: false + }); + } + } else { + notifications.update({ + id, + title: `Unable to Register`, + message: `Please try again.`, + color: 'error', + autoClose: 2000, + loading: false + }); + } + })} + > + + + + + + +
+
+ ) : ( + + Welcome back! + + Do not have an account yet?{' '} + toggle('register')}> + Create account + + + + +
{ + const response = await login(values.email, values.password); + if (response) { + setUser(response.user); + setRefreshId(refreshLoggedIn()); + onClose(); + } else { + notifications.show({ + title: `Unable to Login`, + message: `Please try again.`, + color: 'red', + autoClose: 2000 + }); + } + })} + > + + + + + toggle('reset')}> + Forgot password? + + + + +
+
+ )} +
+ ); +} diff --git a/ui/src/components/Topbar/headerItems.ts b/ui/src/components/Topbar/headerItems.ts new file mode 100644 index 0000000..024a65e --- /dev/null +++ b/ui/src/components/Topbar/headerItems.ts @@ -0,0 +1,41 @@ +export interface HeaderItem { + name: string; + link: string; + role?: string; +} + +export const headerItems: HeaderItem[] = [ + { + name: 'Races', + link: '/races' + }, + { + name: 'Classes', + link: '/classes' + }, + { + name: 'Feats', + link: '/feats' + }, + { + name: 'Options & Features', + link: '/options' + }, + { + name: 'Backgrounds', + link: '/backgrounds' + }, + { + name: 'Items', + link: '/items' + }, + { + name: 'Spells', + link: '/spells' + }, + { + name: 'Management', + link: '/management', + role: 'admin' + } +]; diff --git a/ui/src/components/Topbar/index.tsx b/ui/src/components/Topbar/index.tsx index 7efb7bb..3d31c94 100644 --- a/ui/src/components/Topbar/index.tsx +++ b/ui/src/components/Topbar/index.tsx @@ -3,73 +3,14 @@ import Link from 'next/link'; import { usePathname } from 'next/navigation'; import './topbar.css'; -import { - Anchor, - Avatar, - Button, - Card, - Checkbox, - Container, - Grid, - Group, - Menu, - Modal, - Paper, - PasswordInput, - Text, - TextInput, - Title, - UnstyledButton -} from '@mantine/core'; +import { Avatar, Button, Card, Grid, Group, Menu, Text, UnstyledButton } from '@mantine/core'; import Cookies from 'js-cookie'; import { useEffect, useState } from 'react'; -import { useForm } from '@mantine/form'; -import { login, register, logout, me, refreshLoggedIn } from '@/api/auth'; +import { logout, me, refreshLoggedIn } from '@/api/auth'; import { User } from '@/api/auth.types'; import { useToggle } from '@mantine/hooks'; -import { notifications } from '@mantine/notifications'; - -interface HeaderItem { - name: string; - link: string; - role?: string; -} - -const headerItems: HeaderItem[] = [ - { - name: 'Races', - link: '/races' - }, - { - name: 'Classes', - link: '/classes' - }, - { - name: 'Feats', - link: '/feats' - }, - { - name: 'Options & Features', - link: '/options' - }, - { - name: 'Backgrounds', - link: '/backgrounds' - }, - { - name: 'Items', - link: '/items' - }, - { - name: 'Spells', - link: '/spells' - }, - { - name: 'Management', - link: '/management', - role: 'admin' - } -]; +import { LoginModal } from './LoginModal'; +import { HeaderItem, headerItems } from './headerItems'; export default function Topbar() { const pathName = usePathname(); @@ -145,17 +86,11 @@ export default function Topbar() { - + + + - - - - ) : type == 'register' ? ( - - Create account - - Already have an account?{' '} - toggle('login')}> - Sign in - - - - -
{ - const id = notifications.show({ - loading: true, - title: `Creating account`, - message: `Please wait...`, - autoClose: false, - withCloseButton: false - }); - const registerResponse = await register({ - first_name: values.firstName, - last_name: values.lastName, - email: values.email, - password: values.password - }); - if (registerResponse) { - const loginResponse = await login(values.email, values.password); - if (loginResponse) { - setUser(loginResponse.user); - setRefreshId(refreshLoggedIn()); - onClose(); - notifications.update({ - id, - title: `Account created`, - message: `Welcome ${loginResponse.user.first_name}!`, - color: 'green', - autoClose: 2000, - loading: false - }); - } else { - notifications.update({ - id, - title: `Unable to Login`, - message: `Please try again.`, - color: 'red', - autoClose: 2000, - loading: false - }); - } - } else { - notifications.update({ - id, - title: `Unable to Register`, - message: `Please try again.`, - color: 'error', - autoClose: 2000, - loading: false - }); - } - })} - > - - - - - - -
-
- ) : ( - - Welcome back! - - Do not have an account yet?{' '} - toggle('register')}> - Create account - - - - -
{ - const response = await login(values.email, values.password); - if (response) { - setUser(response.user); - setRefreshId(refreshLoggedIn()); - onClose(); - } else { - notifications.show({ - title: `Unable to Login`, - message: `Please try again.`, - color: 'red', - autoClose: 2000 - }); - } - })} - > - - - - - toggle('reset')}> - Forgot password? - - - - -
-
- )} - - ); -} From e63d03445050cb250b285c4672df342c043b3f36 Mon Sep 17 00:00:00 2001 From: Benjamin Sherriff Date: Thu, 19 Oct 2023 20:36:59 -0400 Subject: [PATCH 03/24] Renamed topbar to header --- ui/src/app/layout.tsx | 4 ++-- .../{Topbar/LoginModal.tsx => Header/HeaderModal.tsx} | 4 ++-- .../components/{Topbar/topbar.css => Header/header.css} | 0 ui/src/components/{Topbar => Header}/headerItems.ts | 0 ui/src/components/{Topbar => Header}/index.tsx | 8 ++++---- 5 files changed, 8 insertions(+), 8 deletions(-) rename ui/src/components/{Topbar/LoginModal.tsx => Header/HeaderModal.tsx} (98%) rename ui/src/components/{Topbar/topbar.css => Header/header.css} (100%) rename ui/src/components/{Topbar => Header}/headerItems.ts (100%) rename ui/src/components/{Topbar => Header}/index.tsx (95%) diff --git a/ui/src/app/layout.tsx b/ui/src/app/layout.tsx index 3ead78f..84eacba 100644 --- a/ui/src/app/layout.tsx +++ b/ui/src/app/layout.tsx @@ -1,6 +1,6 @@ import React from 'react'; import RecoilRootWrapper from '@app/recoil-root-wrapper'; -import Topbar from '@/components/Topbar'; +import Header from '@/components/Header'; import { Inter } from 'next/font/google'; import { Box, MantineProvider } from '@mantine/core'; import { ModalsProvider } from '@mantine/modals'; @@ -27,7 +27,7 @@ export default function RootLayout({ children }: { children: React.ReactNode }) - +
{children} diff --git a/ui/src/components/Topbar/LoginModal.tsx b/ui/src/components/Header/HeaderModal.tsx similarity index 98% rename from ui/src/components/Topbar/LoginModal.tsx rename to ui/src/components/Header/HeaderModal.tsx index 7adde5a..83adedd 100644 --- a/ui/src/components/Topbar/LoginModal.tsx +++ b/ui/src/components/Header/HeaderModal.tsx @@ -18,14 +18,14 @@ import { import { useForm } from '@mantine/form'; import { notifications } from '@mantine/notifications'; -interface LoginModalProps { +interface HeaderModalProps { type?: string; toggle: any; setUser: (user: User) => void; setRefreshId: (id: NodeJS.Timeout) => void; } -export function LoginModal({ type, toggle, setUser, setRefreshId }: LoginModalProps) { +export function HeaderModal({ type, toggle, setUser, setRefreshId }: HeaderModalProps) { function passwordValidator(value: string) { if (value.trim().length < 10) { return 'Password must be at least 10 characters'; diff --git a/ui/src/components/Topbar/topbar.css b/ui/src/components/Header/header.css similarity index 100% rename from ui/src/components/Topbar/topbar.css rename to ui/src/components/Header/header.css diff --git a/ui/src/components/Topbar/headerItems.ts b/ui/src/components/Header/headerItems.ts similarity index 100% rename from ui/src/components/Topbar/headerItems.ts rename to ui/src/components/Header/headerItems.ts diff --git a/ui/src/components/Topbar/index.tsx b/ui/src/components/Header/index.tsx similarity index 95% rename from ui/src/components/Topbar/index.tsx rename to ui/src/components/Header/index.tsx index 3d31c94..c133219 100644 --- a/ui/src/components/Topbar/index.tsx +++ b/ui/src/components/Header/index.tsx @@ -2,17 +2,17 @@ import Link from 'next/link'; import { usePathname } from 'next/navigation'; -import './topbar.css'; +import './header.css'; import { Avatar, Button, Card, Grid, Group, Menu, Text, UnstyledButton } from '@mantine/core'; import Cookies from 'js-cookie'; import { useEffect, useState } from 'react'; import { logout, me, refreshLoggedIn } from '@/api/auth'; import { User } from '@/api/auth.types'; import { useToggle } from '@mantine/hooks'; -import { LoginModal } from './LoginModal'; +import { HeaderModal } from './HeaderModal'; import { HeaderItem, headerItems } from './headerItems'; -export default function Topbar() { +export default function Header() { const pathName = usePathname(); const [modalType, toggle] = useToggle([undefined, 'login', 'register', 'reset']); const [headers, setHeaders] = useState([]); @@ -126,7 +126,7 @@ export default function Topbar() { )} - + ); } From a1090e2a0f9181a8d1c393fd6a8d3691175ee39d Mon Sep 17 00:00:00 2001 From: Benjamin Sherriff Date: Thu, 19 Oct 2023 22:07:51 -0400 Subject: [PATCH 04/24] Updated management/profile pages --- ui/package-lock.json | 8 ++++---- ui/package.json | 2 +- ui/src/app/profile/page.tsx | 26 +++++++++++++++++++++++-- ui/src/components/Header/headerItems.ts | 5 ----- ui/src/components/Header/index.tsx | 9 +++++++++ 5 files changed, 38 insertions(+), 12 deletions(-) diff --git a/ui/package-lock.json b/ui/package-lock.json index c36e38f..b481145 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -37,7 +37,7 @@ "eslint-plugin-prettier": "^5.0.0", "postcss": "^8.4.31", "postcss-import": "^15.1.0", - "postcss-preset-mantine": "^1.8.0", + "postcss-preset-mantine": "^1.9.0", "prettier": "^3.0.3", "typescript": "5.2.2" } @@ -4059,9 +4059,9 @@ } }, "node_modules/postcss-preset-mantine": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/postcss-preset-mantine/-/postcss-preset-mantine-1.8.0.tgz", - "integrity": "sha512-aLc+EoDXsvnXM2lWWF1MI+lgGqbd5xatVJ3KyTmsheNoXBYN0OFAkRFqyy3tfdveH64Fno2SLNEr4w/njPSInw==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/postcss-preset-mantine/-/postcss-preset-mantine-1.9.0.tgz", + "integrity": "sha512-ZurmjL+5UK9FZq4GGKOoksC7UMVFZVXxRMO0WwQAiMeElZ8jPXIXIALnwdQhslyoVDzpezkRuHYtXGo65DwvqA==", "dev": true, "dependencies": { "postcss-mixins": "^9.0.4", diff --git a/ui/package.json b/ui/package.json index b32fc76..7d69569 100644 --- a/ui/package.json +++ b/ui/package.json @@ -38,7 +38,7 @@ "eslint-plugin-prettier": "^5.0.0", "postcss": "^8.4.31", "postcss-import": "^15.1.0", - "postcss-preset-mantine": "^1.8.0", + "postcss-preset-mantine": "^1.9.0", "prettier": "^3.0.3", "typescript": "5.2.2" } diff --git a/ui/src/app/profile/page.tsx b/ui/src/app/profile/page.tsx index e892a07..121e814 100644 --- a/ui/src/app/profile/page.tsx +++ b/ui/src/app/profile/page.tsx @@ -1,5 +1,27 @@ -import React from 'react'; +'use client'; + +import { me } from '@/api/auth'; +import { User } from '@/api/auth.types'; +import React, { useEffect, useState } from 'react'; +import { useRouter } from 'next/navigation'; export default function Page() { - return <>; + const [user, setUser] = useState(undefined); + const router = useRouter(); + + useEffect(() => { + me().then((response) => { + if (response) { + setUser(response.user); + } else { + router.push('/'); + } + }); + }, []); + + if (user) { + return
Logged in as {user.email}
; + } else { + return
Not logged in
; + } } diff --git a/ui/src/components/Header/headerItems.ts b/ui/src/components/Header/headerItems.ts index 024a65e..1925049 100644 --- a/ui/src/components/Header/headerItems.ts +++ b/ui/src/components/Header/headerItems.ts @@ -32,10 +32,5 @@ export const headerItems: HeaderItem[] = [ { name: 'Spells', link: '/spells' - }, - { - name: 'Management', - link: '/management', - role: 'admin' } ]; diff --git a/ui/src/components/Header/index.tsx b/ui/src/components/Header/index.tsx index c133219..2076664 100644 --- a/ui/src/components/Header/index.tsx +++ b/ui/src/components/Header/index.tsx @@ -112,6 +112,15 @@ export default function Header() { Logout + {user.role == 'admin' && ( + + + + + + )} From 11facd9badb28f3f1b459f796ed78671442df861 Mon Sep 17 00:00:00 2001 From: Benjamin Sherriff Date: Fri, 20 Oct 2023 07:36:10 -0400 Subject: [PATCH 05/24] Add recoil state --- ui/src/app/profile/page.tsx | 25 ++++++++++++++----------- ui/src/components/Header/index.tsx | 9 +++++---- ui/src/state/auth.ts | 12 ++++++++++++ 3 files changed, 31 insertions(+), 15 deletions(-) create mode 100644 ui/src/state/auth.ts diff --git a/ui/src/app/profile/page.tsx b/ui/src/app/profile/page.tsx index 121e814..96a50d0 100644 --- a/ui/src/app/profile/page.tsx +++ b/ui/src/app/profile/page.tsx @@ -1,23 +1,26 @@ 'use client'; import { me } from '@/api/auth'; -import { User } from '@/api/auth.types'; -import React, { useEffect, useState } from 'react'; +import React, { useEffect } from 'react'; import { useRouter } from 'next/navigation'; +import { useRecoilState } from 'recoil'; +import { userState } from '@/state/auth'; export default function Page() { - const [user, setUser] = useState(undefined); + const [user, setUser] = useRecoilState(userState); const router = useRouter(); useEffect(() => { - me().then((response) => { - if (response) { - setUser(response.user); - } else { - router.push('/'); - } - }); - }, []); + if (!user) { + me().then((response) => { + if (response) { + setUser(response.user); + } else { + router.push('/'); + } + }); + } + }, [user]); if (user) { return
Logged in as {user.email}
; diff --git a/ui/src/components/Header/index.tsx b/ui/src/components/Header/index.tsx index 2076664..23ad73b 100644 --- a/ui/src/components/Header/index.tsx +++ b/ui/src/components/Header/index.tsx @@ -7,20 +7,21 @@ import { Avatar, Button, Card, Grid, Group, Menu, Text, UnstyledButton } from '@ import Cookies from 'js-cookie'; import { useEffect, useState } from 'react'; import { logout, me, refreshLoggedIn } from '@/api/auth'; -import { User } from '@/api/auth.types'; import { useToggle } from '@mantine/hooks'; import { HeaderModal } from './HeaderModal'; import { HeaderItem, headerItems } from './headerItems'; +import { userState } from '@/state/auth'; +import { useRecoilState } from 'recoil'; export default function Header() { const pathName = usePathname(); const [modalType, toggle] = useToggle([undefined, 'login', 'register', 'reset']); const [headers, setHeaders] = useState([]); - const [user, setUser] = useState(undefined); + const [user, setUser] = useRecoilState(userState); const [refreshId, setRefreshId] = useState(undefined); useEffect(() => { - if (Cookies.get('logged_in')) { + if (!user && Cookies.get('logged_in')) { me().then((response) => { if (response) { setRefreshId(refreshLoggedIn()); @@ -28,7 +29,7 @@ export default function Header() { } }); } - }, []); + }, [user]); useEffect(() => { const h: HeaderItem[] = []; diff --git a/ui/src/state/auth.ts b/ui/src/state/auth.ts new file mode 100644 index 0000000..356cdd8 --- /dev/null +++ b/ui/src/state/auth.ts @@ -0,0 +1,12 @@ +import { User } from '@/api/auth.types'; +import { atom } from 'recoil'; + +export const userState = atom({ + key: 'userState', + default: undefined as User | undefined +}); + +export const isAuthenticatedState = atom({ + key: 'isAuthenticatedState', + default: false +}); From 379de4bdb596a346e01a6c27090ee26d567e07ce Mon Sep 17 00:00:00 2001 From: Benjamin Sherriff Date: Sat, 21 Oct 2023 12:32:50 -0400 Subject: [PATCH 06/24] Refactored db directory --- service/Cargo.toml | 3 +- service/migrations/000011_create_users/up.sql | 2 + service/src/auth/mod.rs | 4 +- service/src/auth/model.rs | 14 +++- service/src/auth/routes.rs | 8 +- service/src/bot/api/routes.rs | 2 +- service/src/bot/commands/audio/play.rs | 2 +- service/src/bot/commands/audio/volume.rs | 2 +- service/src/bot/handler.rs | 2 +- service/src/{db => dnd}/backgrounds/mod.rs | 0 service/src/{db => dnd}/bestiary/mod.rs | 0 service/src/{db => dnd}/classes/mod.rs | 0 service/src/{db => dnd}/classes/model.rs | 0 service/src/{db => dnd}/conditions/mod.rs | 0 service/src/{db => dnd}/feats/mod.rs | 0 service/src/{db => dnd}/items/mod.rs | 0 service/src/dnd/mod.rs | 13 ++++ service/src/{db => dnd}/options/mod.rs | 0 service/src/{db => dnd}/races/mod.rs | 0 service/src/{db => dnd}/spells/mod.rs | 0 service/src/{db => dnd}/spells/model.rs | 16 ++-- service/src/{db => dnd}/spells/routes.rs | 2 +- service/src/{db => dnd}/spells/types.rs | 0 service/src/lib.rs | 18 +++++ service/src/main.rs | 15 ++-- service/src/{db => storage}/guilds/mod.rs | 0 service/src/{db => storage}/guilds/model.rs | 2 +- service/src/{db => storage}/messages/mod.rs | 0 service/src/{db => storage}/messages/model.rs | 8 +- .../src/{db => storage}/messages/routes.rs | 2 +- service/src/{db => storage}/mod.rs | 74 ++++++++++--------- service/src/{db => storage}/schema.rs | 2 + 32 files changed, 124 insertions(+), 67 deletions(-) rename service/src/{db => dnd}/backgrounds/mod.rs (100%) rename service/src/{db => dnd}/bestiary/mod.rs (100%) rename service/src/{db => dnd}/classes/mod.rs (100%) rename service/src/{db => dnd}/classes/model.rs (100%) rename service/src/{db => dnd}/conditions/mod.rs (100%) rename service/src/{db => dnd}/feats/mod.rs (100%) rename service/src/{db => dnd}/items/mod.rs (100%) rename service/src/{db => dnd}/options/mod.rs (100%) rename service/src/{db => dnd}/races/mod.rs (100%) rename service/src/{db => dnd}/spells/mod.rs (100%) rename service/src/{db => dnd}/spells/model.rs (96%) rename service/src/{db => dnd}/spells/routes.rs (98%) rename service/src/{db => dnd}/spells/types.rs (100%) rename service/src/{db => storage}/guilds/mod.rs (100%) rename service/src/{db => storage}/guilds/model.rs (95%) rename service/src/{db => storage}/messages/mod.rs (100%) rename service/src/{db => storage}/messages/model.rs (96%) rename service/src/{db => storage}/messages/routes.rs (96%) rename service/src/{db => storage}/mod.rs (53%) rename service/src/{db => storage}/schema.rs (94%) diff --git a/service/Cargo.toml b/service/Cargo.toml index fc7dd6d..36385a4 100644 --- a/service/Cargo.toml +++ b/service/Cargo.toml @@ -29,6 +29,7 @@ jsonwebtoken = "9.0.0" redis = { version = "0.23.3", features = ["tokio-comp", "connection-manager", "r2d2"] } base64 = "0.21.4" rust-s3 = "0.33.0" +minio = "0.1.0" [dependencies.tokio] version = "1.32.0" @@ -46,7 +47,7 @@ features = ["json", "rustls-tls"] [dependencies.diesel] version = "2.1.2" default-features = false -features = ["postgres", "32-column-tables", "serde_json", "r2d2", "with-deprecated"] +features = ["postgres", "chrono", "32-column-tables", "serde_json", "r2d2", "with-deprecated"] [dependencies.serenity] version = "0.11.6" diff --git a/service/migrations/000011_create_users/up.sql b/service/migrations/000011_create_users/up.sql index 0adabf0..ac207e6 100644 --- a/service/migrations/000011_create_users/up.sql +++ b/service/migrations/000011_create_users/up.sql @@ -4,5 +4,7 @@ CREATE TABLE IF NOT EXISTS users ( role TEXT NOT NULL, first_name TEXT NOT NULL, last_name TEXT NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + updated_at TIMESTAMP NOT NULL DEFAULT NOW(), verified BOOLEAN NOT NULL DEFAULT FALSE ); \ No newline at end of file diff --git a/service/src/auth/mod.rs b/service/src/auth/mod.rs index 8968b48..fc63e4d 100644 --- a/service/src/auth/mod.rs +++ b/service/src/auth/mod.rs @@ -15,7 +15,8 @@ use siren::ServiceError; #[derive(Debug, Serialize, Deserialize)] struct TokenClaims { sub: String, // Subject - token_uuid: String, // Issuer + token_uuid: String, // Token UUID + iss: String, // Issuer exp: i64, // Expiration time iat: i64, // Issued At nbf: i64 // Not Before @@ -73,6 +74,7 @@ pub fn generate_token(email: &str, ttl: i64, private_key: &str) -> Result Result { - let mut conn = crate::db::connection()?; + let mut conn = connection()?; // Check if the user exists by email, case insensitive let user = users::table @@ -69,12 +73,14 @@ pub struct InsertUser { pub role: String, pub first_name: String, pub last_name: String, + pub updated_at: chrono::NaiveDateTime, + pub created_at: chrono::NaiveDateTime, pub verified: bool, } impl InsertUser { pub fn insert(user: Self) -> Result { - let mut conn = crate::db::connection()?; + let mut conn = connection()?; let user = diesel::insert_into(users::table) .values(user) .get_result(&mut conn)?; @@ -141,7 +147,7 @@ impl FromRequest for JwtAuth { let access_token_uuid = uuid::Uuid::parse_str(&access_token_details.token_uuid.to_string()).unwrap(); - let mut conn = match crate::db::redis_connection() { + let mut conn = match crate::storage::redis_connection() { Ok(conn) => conn, Err(err) => { error!("Failed to get redis connection: {}", err); diff --git a/service/src/auth/routes.rs b/service/src/auth/routes.rs index 24480a8..895461d 100644 --- a/service/src/auth/routes.rs +++ b/service/src/auth/routes.rs @@ -6,7 +6,7 @@ use redis::AsyncCommands; use serde::{Serialize, Deserialize}; use siren::ServiceError; -use crate::{auth::{LoginRequest, RegisterUser, InsertUser, QueryUser, verify_password, JwtAuth, verify_token, generate_access_token, generate_refresh_token}, db}; +use crate::{auth::{LoginRequest, RegisterUser, InsertUser, QueryUser, verify_password, JwtAuth, verify_token, generate_access_token, generate_refresh_token}, storage}; #[post("/register")] async fn register(user: web::Json) -> HttpResponse { @@ -58,7 +58,7 @@ async fn login(request: web::Json) -> HttpResponse { } }; - let mut conn = match db::redis_async_connection().await { + let mut conn = match storage::redis_async_connection().await { Ok(conn) => conn, Err(err) => { error!("Failed to get redis connection: {}", err); @@ -169,7 +169,7 @@ async fn refresh(req: HttpRequest) -> HttpResponse { } }; - let mut conn = match db::redis_async_connection().await { + let mut conn = match storage::redis_async_connection().await { Ok(conn) => conn, Err(err) => { error!("Failed to get redis connection: {}", err); @@ -292,7 +292,7 @@ async fn logout(req: HttpRequest, auth: JwtAuth) -> HttpResponse { Err(err) => return ResponseError::error_response(&err) }; - let mut conn = match db::redis_async_connection().await { + let mut conn = match storage::redis_async_connection().await { Ok(conn) => conn, Err(err) => { error!("Failed to get redis connection: {}", err); diff --git a/service/src/bot/api/routes.rs b/service/src/bot/api/routes.rs index d1e2576..25c8be5 100644 --- a/service/src/bot/api/routes.rs +++ b/service/src/bot/api/routes.rs @@ -6,7 +6,7 @@ use serde::{Serialize, Deserialize}; use serenity::model::prelude::{GuildChannel, ChannelType}; use siren::ServiceError; -use crate::{AppState, bot::commands::audio::{play::play_track, join}, db::guilds::QueryGuild, auth::{JwtAuth, verify_role}}; +use crate::{AppState, bot::commands::audio::{play::play_track, join}, storage::guilds::QueryGuild, auth::{JwtAuth, verify_role}}; #[get("/guilds")] async fn get_guilds(data: web::Data>, auth: JwtAuth) -> HttpResponse { diff --git a/service/src/bot/commands/audio/play.rs b/service/src/bot/commands/audio/play.rs index 3fb0c7c..b4ffbae 100644 --- a/service/src/bot/commands/audio/play.rs +++ b/service/src/bot/commands/audio/play.rs @@ -10,7 +10,7 @@ use siren::ServiceError; use songbird::{EventHandler, Songbird}; use crate::bot::commands::audio::{leave, add_song, get_songbird}; -use crate::db::guilds::QueryGuild; +use crate::storage::guilds::QueryGuild; use super::{create_response, edit_response, join_by_user}; diff --git a/service/src/bot/commands/audio/volume.rs b/service/src/bot/commands/audio/volume.rs index 9a3ece3..7bf96ec 100644 --- a/service/src/bot/commands/audio/volume.rs +++ b/service/src/bot/commands/audio/volume.rs @@ -7,7 +7,7 @@ use serenity::builder::CreateApplicationCommand; use serenity::model::application::interaction::application_command::ApplicationCommandInteraction; use songbird::Songbird; -use crate::db::guilds::InsertGuild; +use crate::storage::guilds::InsertGuild; use super::{get_songbird, create_response, edit_response}; diff --git a/service/src/bot/handler.rs b/service/src/bot/handler.rs index f45d3d1..b7c3db5 100644 --- a/service/src/bot/handler.rs +++ b/service/src/bot/handler.rs @@ -5,7 +5,7 @@ use serenity::model::gateway::Ready; use serenity::model::channel::Message; use serenity::prelude::*; -use crate::db::guilds::InsertGuild; +use crate::storage::guilds::InsertGuild; use super::commands; use super::commands::audio::create_response; diff --git a/service/src/db/backgrounds/mod.rs b/service/src/dnd/backgrounds/mod.rs similarity index 100% rename from service/src/db/backgrounds/mod.rs rename to service/src/dnd/backgrounds/mod.rs diff --git a/service/src/db/bestiary/mod.rs b/service/src/dnd/bestiary/mod.rs similarity index 100% rename from service/src/db/bestiary/mod.rs rename to service/src/dnd/bestiary/mod.rs diff --git a/service/src/db/classes/mod.rs b/service/src/dnd/classes/mod.rs similarity index 100% rename from service/src/db/classes/mod.rs rename to service/src/dnd/classes/mod.rs diff --git a/service/src/db/classes/model.rs b/service/src/dnd/classes/model.rs similarity index 100% rename from service/src/db/classes/model.rs rename to service/src/dnd/classes/model.rs diff --git a/service/src/db/conditions/mod.rs b/service/src/dnd/conditions/mod.rs similarity index 100% rename from service/src/db/conditions/mod.rs rename to service/src/dnd/conditions/mod.rs diff --git a/service/src/db/feats/mod.rs b/service/src/dnd/feats/mod.rs similarity index 100% rename from service/src/db/feats/mod.rs rename to service/src/dnd/feats/mod.rs diff --git a/service/src/db/items/mod.rs b/service/src/dnd/items/mod.rs similarity index 100% rename from service/src/db/items/mod.rs rename to service/src/dnd/items/mod.rs diff --git a/service/src/dnd/mod.rs b/service/src/dnd/mod.rs index e69de29..d8a2b34 100644 --- a/service/src/dnd/mod.rs +++ b/service/src/dnd/mod.rs @@ -0,0 +1,13 @@ +pub mod backgrounds; +pub mod bestiary; +pub mod classes; +pub mod conditions; +pub mod feats; +pub mod items; +pub mod options; +pub mod races; +pub mod spells; + +pub fn load_data(data_dir_path: &str) { + spells::load_data(data_dir_path); +} \ No newline at end of file diff --git a/service/src/db/options/mod.rs b/service/src/dnd/options/mod.rs similarity index 100% rename from service/src/db/options/mod.rs rename to service/src/dnd/options/mod.rs diff --git a/service/src/db/races/mod.rs b/service/src/dnd/races/mod.rs similarity index 100% rename from service/src/db/races/mod.rs rename to service/src/dnd/races/mod.rs diff --git a/service/src/db/spells/mod.rs b/service/src/dnd/spells/mod.rs similarity index 100% rename from service/src/db/spells/mod.rs rename to service/src/dnd/spells/mod.rs diff --git a/service/src/db/spells/model.rs b/service/src/dnd/spells/model.rs similarity index 96% rename from service/src/db/spells/model.rs rename to service/src/dnd/spells/model.rs index a2d460b..9604178 100644 --- a/service/src/db/spells/model.rs +++ b/service/src/dnd/spells/model.rs @@ -2,7 +2,9 @@ use diesel::prelude::*; use serde::{Deserialize, Serialize}; use siren::ServiceError; -use crate::db::{schema::spells::{self}, classes::AbilityType, conditions::ConditionType}; +use crate::storage::connection; +use crate::storage::schema::spells::{self}; +use crate::dnd::{classes::AbilityType, conditions::ConditionType}; use super::{SchoolType, CastingTime, SpellAttackType, SpellDamageType, Range, Area, Components, Duration, Source, Description, DurationType, Effect}; @@ -61,7 +63,7 @@ impl Default for QueryFilters { impl QuerySpell { pub fn get_all(filters: &QueryFilters, limit: i32, page: i32) -> Result, ServiceError> { - let mut conn = crate::db::connection()?; + let mut conn = connection()?; let mut query = spells::table.limit(limit as i64).into_boxed(); // Limit query to page and limit let offset = (page - 1) * limit; @@ -108,7 +110,7 @@ impl QuerySpell { } pub fn get_count(filters: &QueryFilters) -> Result { - let mut conn = crate::db::connection()?; + let mut conn = connection()?; let mut query = spells::table.count().into_boxed(); if let Some(name) = &filters.by_name { query = query.filter(spells::name.ilike(format!("%{}%", name))); @@ -149,7 +151,7 @@ impl QuerySpell { } pub fn get_by_id(id: i32) -> Result { - let mut conn = crate::db::connection()?; + let mut conn = connection()?; let spell = spells::table .filter(spells::id.eq(id)) .first::(&mut conn)?; @@ -157,7 +159,7 @@ impl QuerySpell { } pub fn delete(id: i32) -> Result { - let mut conn = crate::db::connection()?; + let mut conn = connection()?; let spell = diesel::delete(spells::table.filter(spells::id.eq(id))).get_result(&mut conn)?; Ok(spell) } @@ -182,13 +184,13 @@ pub struct InsertSpell { impl InsertSpell { pub fn insert(spell: Self) -> Result { - let mut conn = crate::db::connection()?; + let mut conn = connection()?; let spell = diesel::insert_into(spells::table).values(spell).get_result(&mut conn)?; Ok(spell) } pub fn update(id: i32, spell: Self) -> Result { - let mut conn = crate::db::connection()?; + let mut conn = connection()?; let spell = diesel::update(spells::table.filter(spells::id.eq(id))).set(spell).get_result(&mut conn)?; Ok(spell) } diff --git a/service/src/db/spells/routes.rs b/service/src/dnd/spells/routes.rs similarity index 98% rename from service/src/db/spells/routes.rs rename to service/src/dnd/spells/routes.rs index 4b95e90..62f8eec 100644 --- a/service/src/db/spells/routes.rs +++ b/service/src/dnd/spells/routes.rs @@ -3,7 +3,7 @@ use log::error; use serde::{Serialize, Deserialize}; use siren::{GetResponse, Metadata, ServiceError}; -use crate::{db::spells::{QuerySpell, QueryFilters}, auth::{JwtAuth, verify_role}}; +use crate::{dnd::spells::{QuerySpell, QueryFilters}, auth::{JwtAuth, verify_role}}; use super::{Spell, InsertSpell}; diff --git a/service/src/db/spells/types.rs b/service/src/dnd/spells/types.rs similarity index 100% rename from service/src/db/spells/types.rs rename to service/src/dnd/spells/types.rs diff --git a/service/src/lib.rs b/service/src/lib.rs index 67645ec..9f87271 100644 --- a/service/src/lib.rs +++ b/service/src/lib.rs @@ -112,6 +112,24 @@ impl From for ServiceError { } } +impl From for ServiceError { + fn from(error: s3::error::S3Error) -> ServiceError { + ServiceError::new(500, format!("Unknown s3 error: {}", error)) + } +} + +impl From for ServiceError { + fn from(error: s3::creds::error::CredentialsError) -> ServiceError { + ServiceError::new(500, format!("Unknown credentials error: {}", error)) + } +} + +impl From for ServiceError { + fn from(error: minio::s3::error::Error) -> ServiceError { + ServiceError::new(500, format!("Unknown minio error: {}", error)) + } +} + impl ResponseError for ServiceError { fn error_response(&self) -> HttpResponse { let status_code = match StatusCode::from_u16(self.status) { diff --git a/service/src/main.rs b/service/src/main.rs index 4322e3f..d114881 100644 --- a/service/src/main.rs +++ b/service/src/main.rs @@ -21,15 +21,15 @@ use dotenv::dotenv; mod auth; mod dnd; mod bot; -mod db; +mod storage; #[actix_web::main] async fn main() -> std::io::Result<()> { dotenv().ok(); env_logger::init_from_env(env_logger::Env::default().filter_or("RUST_LOG", "warn,siren=info")); - db::init(); + storage::init(); match env::var("DATA_DIR_PATH") { - Ok(data_dir_path) => db::load_data(&data_dir_path), + Ok(data_dir_path) => dnd::load_data(&data_dir_path), Err(err) => warn!("Unable to load initial database data: {}", err) }; @@ -111,6 +111,11 @@ async fn main() -> std::io::Result<()> { let host = env::var("SERVICE_HOST").unwrap_or("localhost".to_string()); let port = env::var("SERVICE_PORT").unwrap_or("5000".to_string()); + match crate::storage::create_bucket("siren").await { + Ok(_) => {}, + Err(err) => {} + }; + let server = match HttpServer::new(move || { let cors = Cors::default() .allow_any_origin() @@ -121,8 +126,8 @@ async fn main() -> std::io::Result<()> { App::new() .wrap(cors) .app_data(web::Data::new(Arc::clone(&app_data))) - .configure(crate::db::messages::init_routes) - .configure(crate::db::spells::init_routes) + .configure(crate::storage::messages::init_routes) + .configure(crate::dnd::spells::init_routes) .configure(crate::auth::init_routes) .configure(crate::bot::api::init_routes) }) diff --git a/service/src/db/guilds/mod.rs b/service/src/storage/guilds/mod.rs similarity index 100% rename from service/src/db/guilds/mod.rs rename to service/src/storage/guilds/mod.rs diff --git a/service/src/db/guilds/model.rs b/service/src/storage/guilds/model.rs similarity index 95% rename from service/src/db/guilds/model.rs rename to service/src/storage/guilds/model.rs index ababc25..ea89714 100644 --- a/service/src/db/guilds/model.rs +++ b/service/src/storage/guilds/model.rs @@ -2,7 +2,7 @@ use diesel::prelude::*; use serde::{Serialize, Deserialize}; use siren::ServiceError; -use crate::db::{schema::guilds, connection}; +use crate::storage::{schema::guilds, connection}; #[derive(Queryable, QueryableByName, Serialize, Deserialize)] #[diesel(table_name = guilds)] diff --git a/service/src/db/messages/mod.rs b/service/src/storage/messages/mod.rs similarity index 100% rename from service/src/db/messages/mod.rs rename to service/src/storage/messages/mod.rs diff --git a/service/src/db/messages/model.rs b/service/src/storage/messages/model.rs similarity index 96% rename from service/src/db/messages/model.rs rename to service/src/storage/messages/model.rs index a5f9ebb..58351ed 100644 --- a/service/src/db/messages/model.rs +++ b/service/src/storage/messages/model.rs @@ -2,7 +2,7 @@ use diesel::prelude::*; use serde::{Deserialize, Serialize}; use siren::ServiceError; -use crate::db::schema::messages::{self}; +use crate::storage::{schema::messages::{self}, connection}; #[derive(Queryable, Selectable, Serialize, Deserialize)] #[diesel(table_name = messages)] @@ -49,7 +49,7 @@ impl Default for QueryFilters { impl QueryMessage { pub fn get_all(filters: &QueryFilters, limit: i32, page: i32) -> Result, ServiceError> { - let mut conn = crate::db::connection()?; + let mut conn = connection()?; let mut query = messages::table.limit(limit as i64).order(messages::created.asc()).into_boxed(); // Limit query to page and limit let offset = (page - 1) * limit; @@ -88,7 +88,7 @@ impl QueryMessage { } pub fn get_count(fitlers: &QueryFilters) -> Result { - let mut conn = crate::db::connection()?; + let mut conn = connection()?; let mut query = messages::table.into_boxed(); // Apply filters if let Some(id) = &fitlers.by_id { @@ -141,7 +141,7 @@ pub struct InsertMessage { impl InsertMessage { pub fn insert(message: Self) -> Result { - let mut conn = crate::db::connection()?; + let mut conn = connection()?; let message = diesel::insert_into(messages::table) .values(message) .get_result(&mut conn)?; diff --git a/service/src/db/messages/routes.rs b/service/src/storage/messages/routes.rs similarity index 96% rename from service/src/db/messages/routes.rs rename to service/src/storage/messages/routes.rs index 507a361..82f284c 100644 --- a/service/src/db/messages/routes.rs +++ b/service/src/storage/messages/routes.rs @@ -3,7 +3,7 @@ use log::error; use serde::{Serialize, Deserialize}; use siren::{GetResponse, Metadata, ServiceError}; -use crate::{db::messages::{QueryMessage, QueryFilters, InsertMessage}, auth::{JwtAuth, verify_role}}; +use crate::{storage::messages::{QueryMessage, QueryFilters, InsertMessage}, auth::{JwtAuth, verify_role}}; #[derive(Serialize, Deserialize)] struct GetAllParams { diff --git a/service/src/db/mod.rs b/service/src/storage/mod.rs similarity index 53% rename from service/src/db/mod.rs rename to service/src/storage/mod.rs index 96b1ac4..6205dc2 100644 --- a/service/src/db/mod.rs +++ b/service/src/storage/mod.rs @@ -1,6 +1,6 @@ use diesel::{r2d2::ConnectionManager as DieselConnectionManager, PgConnection}; -// use redis::{aio::{Connection as RedisConnection, ConnectionManager as RedisConnectionManager}, AsyncCommands}; -use redis::aio::Connection as RedisConnection; +use minio::s3::{client::Client as MinioClient, http::BaseUrl, creds::StaticProvider, args::{MakeBucketArgs, BucketExistsArgs}}; +use redis::{Client as RedisClient, aio::Connection as RedisConnection}; use siren::ServiceError; use crate::diesel_migrations::MigrationHarness; use lazy_static::lazy_static; @@ -8,22 +8,12 @@ use log::{error, info}; use r2d2; use std::env; -pub mod backgrounds; -pub mod bestiary; -pub mod classes; -pub mod conditions; -pub mod feats; pub mod guilds; -pub mod items; pub mod messages; -pub mod options; -pub mod races; -pub mod spells; pub mod schema; type DbPool = r2d2::Pool>; pub type DbConnection = r2d2::PooledConnection>; -// type RedisPool = r2d2::Pool; pub const MIGRATIONS: diesel_migrations::EmbeddedMigrations = embed_migrations!(); @@ -38,18 +28,39 @@ lazy_static! { let manager = DieselConnectionManager::::new(url); DbPool::builder().test_on_check_out(true).build(manager).expect("Failed to create db pool") }; - // static ref REDIS_POOL: RedisPool = { - // let host = env::var("REDIS_HOST").unwrap_or("localhost".to_string()); - // let port = env::var("REDIS_PORT").unwrap_or("6379".to_string()); - // let url = format!("redis://{}:{}", host, port); - // let client = redis::Client::open(url).expect("Failed to create redis client"); - // let manager = RedisConnectionManager::new(client); - // "".to_string() - // }; + static ref REDIS: RedisClient = { + let host = env::var("REDIS_HOST").unwrap_or("localhost".to_string()); + let port = env::var("REDIS_PORT").unwrap_or("6379".to_string()); + let url = format!("redis://{}:{}", host, port); + RedisClient::open(url).expect("Failed to create redis client") + }; + static ref MINIO: MinioClient = { + let url = env::var("MINIO_URL").unwrap_or("localhost".to_string()); + let port = env::var("MINIO_PORT").unwrap_or("9000".to_string()); + let base_url = format!("http://{}:{}", url, port).parse::().unwrap(); + + let user = env::var("MINIO_ROOT_USER").expect("MINIO_ROOT_USER is not set"); + let password = env::var("MINIO_ROOT_PASSWORD").expect("MINIO_ROOT_PASSWORD is not set"); + + let static_provider = StaticProvider::new( + &user, + &password, + None + ); + + MinioClient::new( + base_url, + Some(Box::new(static_provider)), + None, + None + ).expect("Failed to create minio client") + }; } pub fn init() { lazy_static::initialize(&POOL); + lazy_static::initialize(&REDIS); + lazy_static::initialize(&MINIO); let mut pool: DbConnection = connection().expect("Failed to get db connection"); match pool.run_pending_migrations(MIGRATIONS) { Ok(_) => info!("Database initialized"), @@ -62,26 +73,21 @@ pub fn connection() -> Result { .map_err(|e| ServiceError::new(500, format!("Failed getting db connection: {}", e))) } -pub fn redis_client() -> Result { - let host = env::var("REDIS_HOST").unwrap_or("localhost".to_string()); - let port = env::var("REDIS_PORT").unwrap_or("6379".to_string()); - let url = format!("redis://{}:{}", host, port); - let client = redis::Client::open(url)?; - Ok(client) -} - pub fn redis_connection() -> Result { - let client = redis_client()?; - let conn = client.get_connection()?; + let conn = REDIS.get_connection()?; Ok(conn) } pub async fn redis_async_connection() -> Result { - let client = redis_client()?; - let conn = client.get_async_connection().await?; + let conn = REDIS.get_async_connection().await?; Ok(conn) } -pub fn load_data(data_dir_path: &str) { - spells::load_data(data_dir_path); +pub async fn create_bucket(bucket_name: &str) -> Result<(), ServiceError> { + let exists = MINIO.bucket_exists(&BucketExistsArgs::new(&bucket_name).unwrap()).await?; + if !exists { + MINIO.make_bucket(&MakeBucketArgs::new(&bucket_name).unwrap()).await?; + } + + Ok(()) } diff --git a/service/src/db/schema.rs b/service/src/storage/schema.rs similarity index 94% rename from service/src/db/schema.rs rename to service/src/storage/schema.rs index 3e6f3e5..6523bf4 100644 --- a/service/src/db/schema.rs +++ b/service/src/storage/schema.rs @@ -46,6 +46,8 @@ diesel::table! { role -> Text, first_name -> Text, last_name -> Text, + updated_at -> Timestamp, + created_at -> Timestamp, verified -> Bool, } } \ No newline at end of file From 8b4d4e1b1f83eb65ed85f58bd9c55169a74e6586 Mon Sep 17 00:00:00 2001 From: Benjamin Sherriff Date: Sat, 21 Oct 2023 16:39:01 -0400 Subject: [PATCH 07/24] Switched s3 crates --- service/Cargo.toml | 1 - service/src/lib.rs | 6 --- service/src/main.rs | 8 ++-- service/src/storage/mod.rs | 77 +++++++++++++++++++++++++------------- ui/src/api/auth.ts | 6 +-- 5 files changed, 56 insertions(+), 42 deletions(-) diff --git a/service/Cargo.toml b/service/Cargo.toml index 36385a4..8a4eedb 100644 --- a/service/Cargo.toml +++ b/service/Cargo.toml @@ -29,7 +29,6 @@ jsonwebtoken = "9.0.0" redis = { version = "0.23.3", features = ["tokio-comp", "connection-manager", "r2d2"] } base64 = "0.21.4" rust-s3 = "0.33.0" -minio = "0.1.0" [dependencies.tokio] version = "1.32.0" diff --git a/service/src/lib.rs b/service/src/lib.rs index 9f87271..02f2ecb 100644 --- a/service/src/lib.rs +++ b/service/src/lib.rs @@ -124,12 +124,6 @@ impl From for ServiceError { } } -impl From for ServiceError { - fn from(error: minio::s3::error::Error) -> ServiceError { - ServiceError::new(500, format!("Unknown minio error: {}", error)) - } -} - impl ResponseError for ServiceError { fn error_response(&self) -> HttpResponse { let status_code = match StatusCode::from_u16(self.status) { diff --git a/service/src/main.rs b/service/src/main.rs index d114881..492d25f 100644 --- a/service/src/main.rs +++ b/service/src/main.rs @@ -27,7 +27,7 @@ mod storage; async fn main() -> std::io::Result<()> { dotenv().ok(); env_logger::init_from_env(env_logger::Env::default().filter_or("RUST_LOG", "warn,siren=info")); - storage::init(); + storage::init().await; match env::var("DATA_DIR_PATH") { Ok(data_dir_path) => dnd::load_data(&data_dir_path), Err(err) => warn!("Unable to load initial database data: {}", err) @@ -111,10 +111,8 @@ async fn main() -> std::io::Result<()> { let host = env::var("SERVICE_HOST").unwrap_or("localhost".to_string()); let port = env::var("SERVICE_PORT").unwrap_or("5000".to_string()); - match crate::storage::create_bucket("siren").await { - Ok(_) => {}, - Err(err) => {} - }; + crate::storage::upload_file("test.txt", b"Test").await.unwrap(); + crate::storage::delete_file("test.txt").await.unwrap(); let server = match HttpServer::new(move || { let cors = Cors::default() diff --git a/service/src/storage/mod.rs b/service/src/storage/mod.rs index 6205dc2..619978f 100644 --- a/service/src/storage/mod.rs +++ b/service/src/storage/mod.rs @@ -1,10 +1,10 @@ use diesel::{r2d2::ConnectionManager as DieselConnectionManager, PgConnection}; -use minio::s3::{client::Client as MinioClient, http::BaseUrl, creds::StaticProvider, args::{MakeBucketArgs, BucketExistsArgs}}; use redis::{Client as RedisClient, aio::Connection as RedisConnection}; +use s3::{Region, creds::Credentials, Bucket, BucketConfiguration, request::ResponseData}; use siren::ServiceError; use crate::diesel_migrations::MigrationHarness; use lazy_static::lazy_static; -use log::{error, info}; +use log::{error, info, warn}; use r2d2; use std::env; @@ -34,33 +34,35 @@ lazy_static! { let url = format!("redis://{}:{}", host, port); RedisClient::open(url).expect("Failed to create redis client") }; - static ref MINIO: MinioClient = { + static ref BUCKET: Bucket = { let url = env::var("MINIO_URL").unwrap_or("localhost".to_string()); let port = env::var("MINIO_PORT").unwrap_or("9000".to_string()); - let base_url = format!("http://{}:{}", url, port).parse::().unwrap(); - let user = env::var("MINIO_ROOT_USER").expect("MINIO_ROOT_USER is not set"); let password = env::var("MINIO_ROOT_PASSWORD").expect("MINIO_ROOT_PASSWORD is not set"); - - let static_provider = StaticProvider::new( - &user, - &password, - None - ); - - MinioClient::new( - base_url, - Some(Box::new(static_provider)), - None, - None - ).expect("Failed to create minio client") + let base_url = format!("http://{}:{}", url, port); + + let region = Region::Custom { + region: "".to_string(), + endpoint: base_url, + }; + + let credentials = Credentials { + access_key: Some(user), + secret_key: Some(password), + security_token: None, + session_token: None, + expiration: None + }; + + Bucket::new("siren", region.clone(), credentials.clone()).expect("Failed to create S3 Bucket").with_path_style() }; } -pub fn init() { +pub async fn init() { lazy_static::initialize(&POOL); lazy_static::initialize(&REDIS); - lazy_static::initialize(&MINIO); + lazy_static::initialize(&BUCKET); + create_bucket().await; let mut pool: DbConnection = connection().expect("Failed to get db connection"); match pool.run_pending_migrations(MIGRATIONS) { Ok(_) => info!("Database initialized"), @@ -83,11 +85,34 @@ pub async fn redis_async_connection() -> Result { Ok(conn) } -pub async fn create_bucket(bucket_name: &str) -> Result<(), ServiceError> { - let exists = MINIO.bucket_exists(&BucketExistsArgs::new(&bucket_name).unwrap()).await?; - if !exists { - MINIO.make_bucket(&MakeBucketArgs::new(&bucket_name).unwrap()).await?; - } +async fn create_bucket() { + let url = env::var("MINIO_URL").unwrap_or("localhost".to_string()); + let port = env::var("MINIO_PORT").unwrap_or("9000".to_string()); + let user = env::var("MINIO_ROOT_USER").expect("MINIO_ROOT_USER is not set"); + let password = env::var("MINIO_ROOT_PASSWORD").expect("MINIO_ROOT_PASSWORD is not set"); + let base_url = format!("http://{}:{}", url, port); - Ok(()) + let region = Region::Custom { + region: "".to_string(), + endpoint: base_url, + }; + + let credentials = Credentials { + access_key: Some(user), + secret_key: Some(password), + security_token: None, + session_token: None, + expiration: None + }; + let _ = Bucket::create_with_path_style("siren", region, credentials, BucketConfiguration::default()).await; +} + +pub async fn upload_file(path: &str, content: &[u8]) -> Result { + let response = BUCKET.put_object(path, content).await?; + Ok(response) +} + +pub async fn delete_file(path: &str) -> Result { + let response = BUCKET.delete_object(path).await?; + Ok(response) } diff --git a/ui/src/api/auth.ts b/ui/src/api/auth.ts index f757fd6..89a31ca 100644 --- a/ui/src/api/auth.ts +++ b/ui/src/api/auth.ts @@ -54,10 +54,8 @@ export function refreshLoggedIn(interval = 840000) { if (cookie != loggedIn) { loggedIn = cookie; const response = await refresh(true); - if (response) { - Cookies.set('logged_in', 'true'); - } else { - Cookies.set('test', 'failed'); + if (!response) { + Cookies.remove('logged_in'); } } }, interval); From 3eb888b57d6d0ca8e3228294b4b3c1172909468e Mon Sep 17 00:00:00 2001 From: Benjamin Sherriff Date: Mon, 23 Oct 2023 16:17:07 -0400 Subject: [PATCH 08/24] Working on upload images and tilemap --- service/Cargo.toml | 1 + service/migrations/000011_create_users/up.sql | 1 + service/src/auth/model.rs | 12 + service/src/lib.rs | 7 +- service/src/main.rs | 4 +- service/src/storage/mod.rs | 6 + service/src/storage/schema.rs | 1 + service/src/users/mod.rs | 3 + service/src/users/routes.rs | 136 ++ ui/package-lock.json | 1278 +++++++++-------- ui/package.json | 33 +- ui/src/api/auth.ts | 2 +- ui/src/api/index.ts | 10 +- ui/src/api/users.ts | 24 + ui/src/app/layout.tsx | 6 +- ui/src/app/page.tsx | 7 +- ui/src/components/Header/header.css | 1 + ui/src/components/Header/index.tsx | 53 +- ui/src/components/TileGrid/Viewport.tsx | 0 ui/src/components/TileGrid/index.tsx | 38 + ui/src/components/TileGrid/tileGrid.css | 9 + ui/tsconfig.json | 11 +- 22 files changed, 987 insertions(+), 656 deletions(-) create mode 100644 service/src/users/mod.rs create mode 100644 service/src/users/routes.rs create mode 100644 ui/src/api/users.ts create mode 100644 ui/src/components/TileGrid/Viewport.tsx create mode 100644 ui/src/components/TileGrid/index.tsx create mode 100644 ui/src/components/TileGrid/tileGrid.css diff --git a/service/Cargo.toml b/service/Cargo.toml index 8a4eedb..615fb9e 100644 --- a/service/Cargo.toml +++ b/service/Cargo.toml @@ -29,6 +29,7 @@ jsonwebtoken = "9.0.0" redis = { version = "0.23.3", features = ["tokio-comp", "connection-manager", "r2d2"] } base64 = "0.21.4" rust-s3 = "0.33.0" +actix-multipart = "0.6.1" [dependencies.tokio] version = "1.32.0" diff --git a/service/migrations/000011_create_users/up.sql b/service/migrations/000011_create_users/up.sql index ac207e6..4911cc2 100644 --- a/service/migrations/000011_create_users/up.sql +++ b/service/migrations/000011_create_users/up.sql @@ -6,5 +6,6 @@ CREATE TABLE IF NOT EXISTS users ( last_name TEXT NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT NOW(), updated_at TIMESTAMP NOT NULL DEFAULT NOW(), + profile TEXT, verified BOOLEAN NOT NULL DEFAULT FALSE ); \ No newline at end of file diff --git a/service/src/auth/model.rs b/service/src/auth/model.rs index 2d0c8e4..6aac1de 100644 --- a/service/src/auth/model.rs +++ b/service/src/auth/model.rs @@ -29,6 +29,7 @@ impl RegisterUser { last_name: self.last_name, updated_at: chrono::Utc::now().naive_utc(), created_at: chrono::Utc::now().naive_utc(), + profile: None, verified: false, }) } @@ -50,6 +51,7 @@ pub struct QueryUser { pub last_name: String, pub updated_at: chrono::NaiveDateTime, pub created_at: chrono::NaiveDateTime, + pub profile: Option, pub verified: bool, } @@ -75,6 +77,7 @@ pub struct InsertUser { pub last_name: String, pub updated_at: chrono::NaiveDateTime, pub created_at: chrono::NaiveDateTime, + pub profile: Option, pub verified: bool, } @@ -86,6 +89,15 @@ impl InsertUser { .get_result(&mut conn)?; Ok(user) } + + pub fn update_profile(email: &str, profile: Option<&str>) -> Result { + let mut conn = connection()?; + let user = diesel::update(users::table) + .filter(users::email.eq(&email)) + .set(users::profile.eq(profile)) + .get_result(&mut conn)?; + Ok(user) + } } #[derive(Debug, Serialize, Deserialize)] diff --git a/service/src/lib.rs b/service/src/lib.rs index 02f2ecb..d04808b 100644 --- a/service/src/lib.rs +++ b/service/src/lib.rs @@ -114,7 +114,12 @@ impl From for ServiceError { impl From for ServiceError { fn from(error: s3::error::S3Error) -> ServiceError { - ServiceError::new(500, format!("Unknown s3 error: {}", error)) + match error { + s3::error::S3Error::Http(code, message) => { + ServiceError::new(code, message) + }, + _ => ServiceError::new(500, format!("Unknown s3 error: {}", error)) + } } } diff --git a/service/src/main.rs b/service/src/main.rs index 492d25f..02e886d 100644 --- a/service/src/main.rs +++ b/service/src/main.rs @@ -22,6 +22,7 @@ mod auth; mod dnd; mod bot; mod storage; +mod users; #[actix_web::main] async fn main() -> std::io::Result<()> { @@ -125,8 +126,9 @@ async fn main() -> std::io::Result<()> { .wrap(cors) .app_data(web::Data::new(Arc::clone(&app_data))) .configure(crate::storage::messages::init_routes) - .configure(crate::dnd::spells::init_routes) .configure(crate::auth::init_routes) + .configure(crate::users::init_routes) + .configure(crate::dnd::spells::init_routes) .configure(crate::bot::api::init_routes) }) .bind(format!("{}:{}", host, port)) { diff --git a/service/src/storage/mod.rs b/service/src/storage/mod.rs index 619978f..83763bf 100644 --- a/service/src/storage/mod.rs +++ b/service/src/storage/mod.rs @@ -112,6 +112,12 @@ pub async fn upload_file(path: &str, content: &[u8]) -> Result Result, ServiceError> { + let response = BUCKET.get_object(path).await?; + let bytes = response.bytes(); + Ok(bytes.to_vec()) +} + pub async fn delete_file(path: &str) -> Result { let response = BUCKET.delete_object(path).await?; Ok(response) diff --git a/service/src/storage/schema.rs b/service/src/storage/schema.rs index 6523bf4..24fb11f 100644 --- a/service/src/storage/schema.rs +++ b/service/src/storage/schema.rs @@ -48,6 +48,7 @@ diesel::table! { last_name -> Text, updated_at -> Timestamp, created_at -> Timestamp, + profile -> Nullable, verified -> Bool, } } \ No newline at end of file diff --git a/service/src/users/mod.rs b/service/src/users/mod.rs new file mode 100644 index 0000000..1688bdf --- /dev/null +++ b/service/src/users/mod.rs @@ -0,0 +1,3 @@ +mod routes; + +pub use routes::init_routes; \ No newline at end of file diff --git a/service/src/users/routes.rs b/service/src/users/routes.rs new file mode 100644 index 0000000..d9c1990 --- /dev/null +++ b/service/src/users/routes.rs @@ -0,0 +1,136 @@ +use actix_multipart::Multipart; +use actix_web::{web, HttpResponse, post, delete, get, ResponseError}; +use log::error; +use serenity::futures::StreamExt; +use siren::ServiceError; + +use crate::{auth::{JwtAuth, InsertUser, QueryUser}, storage::{upload_file, get_file, delete_file}}; + +#[post("/picture")] +async fn set_picture(mut payload: Multipart, auth: JwtAuth) -> HttpResponse { + while let Some(item) = payload.next().await { + let mut bytes = web::BytesMut::new(); + let mut field = match item { + Ok(field) => field, + Err(err) => return ResponseError::error_response(&err) + }; + let content_type = field.content_disposition(); + // Get file name and construct the file path + let file_name = match content_type.get_filename() { + Some(name) => { + // Verify extension is supported + match name.split(".").last() { + Some(ext) => { + match ext { + "png" | "jpg" | "jpeg" => name, + _ => return ResponseError::error_response(&ServiceError { + status: 400, + message: "File extension is not supported".to_string() + }) + } + }, + None => return ResponseError::error_response(&ServiceError { + status: 400, + message: "Unknown file extension".to_string() + }) + } + }, + None => return ResponseError::error_response(&ServiceError { + status: 400, + message: "File name is not provided".to_string() + }) + }; + let path = format!("users/{}/{}", auth.user.email, file_name); + + // Build the file and store it in minio + while let Some(chunk) = field.next().await { + let data = match chunk { + Ok(data) => data, + Err(err) => { + error!("Failed to get chunk: {}", err); + return ResponseError::error_response(&err); + } + }; + bytes.extend_from_slice(&data); + } + match upload_file(&path, &bytes).await { + Ok(_) => { + match InsertUser::update_profile(&auth.user.email, Some(&path)) { + Ok(_) => {} + Err(err) => { + error!("Failed to update user profile: {}", err); + return ResponseError::error_response(&err); + } + }; + }, + Err(err) => { + error!("Failed to upload file: {}", err); + return ResponseError::error_response(&err); + } + } + }; + return HttpResponse::Ok().finish(); +} + +#[get("/picture")] +async fn get_picture(auth: JwtAuth) -> HttpResponse { + let user = match QueryUser::get_by_email(&auth.user.email) { + Ok(user) => user, + Err(err) => { + error!("Failed to get user: {}", err); + return ResponseError::error_response(&err); + } + }; + if let Some(path) = user.profile { + match get_file(&path).await { + Ok(bytes) => return HttpResponse::Ok().body(bytes), + Err(err) => { + error!("Failed to get file: {}", err); + return ResponseError::error_response(&err); + } + } + } else { + return HttpResponse::NotFound().finish(); + } +} + +#[delete("/picture")] +async fn delete_picture(auth: JwtAuth) -> HttpResponse { + match QueryUser::get_by_email(&auth.user.email) { + Ok(user) => { + match user.profile { + Some(path) => { + match delete_file(&path).await { + Ok(_) => { + match InsertUser::update_profile(&auth.user.email, None) { + Ok(_) => {} + Err(err) => { + error!("Failed to update user profile: {}", err); + return ResponseError::error_response(&err); + } + }; + } + Err(err) => { + error!("Failed to delete file: {}", err); + return ResponseError::error_response(&err); + } + }; + }, + None => {} + } + }, + Err(err) => { + error!("Failed to get user: {}", err); + return ResponseError::error_response(&err); + } + }; + return HttpResponse::Ok().finish(); +} + +pub fn init_routes(config: &mut web::ServiceConfig) { + config.service(web::scope("users") + .service(set_picture) + .service(get_picture) + .service(delete_picture) + ); +} \ No newline at end of file diff --git a/ui/package-lock.json b/ui/package-lock.json index b481145..f8b02dc 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -8,31 +8,30 @@ "name": "siren-ui", "version": "0.1.0", "dependencies": { - "@mantine/core": "^7.1.2", - "@mantine/form": "^7.1.2", - "@mantine/hooks": "^7.1.2", - "@mantine/modals": "^7.1.2", - "@mantine/notifications": "^7.1.2", - "axios": "^1.5.1", + "@mantine/core": "^7.1.5", + "@mantine/form": "^7.1.5", + "@mantine/hooks": "^7.1.5", + "@mantine/modals": "^7.1.5", + "@mantine/notifications": "^7.1.5", + "@pixi/react": "^7.1.1", "js-cookie": "^3.0.5", - "next": "^13.5.4", + "next": "^13.5.6", + "pixi.js": "^7.3.2", "react": "^18.2.0", "react-dom": "^18.2.0", "react-icons": "^4.11.0", - "react-leaflet": "^4.2.1", - "recharts": "^2.8.0", "recoil": "^0.7.7" }, "devDependencies": { - "@types/js-cookie": "^3.0.4", - "@types/node": "20.8.2", - "@types/react": "18.2.24", - "@types/react-dom": "18.2.8", - "@typescript-eslint/eslint-plugin": "^6.7.4", - "@typescript-eslint/parser": "^6.7.4", + "@types/js-cookie": "^3.0.5", + "@types/node": "20.8.7", + "@types/react": "18.2.31", + "@types/react-dom": "18.2.14", + "@typescript-eslint/eslint-plugin": "^6.8.0", + "@typescript-eslint/parser": "^6.8.0", "autoprefixer": "^10.4.16", - "eslint": "8.50.0", - "eslint-config-next": "13.5.4", + "eslint": "8.52.0", + "eslint-config-next": "13.5.6", "eslint-config-prettier": "^9.0.0", "eslint-plugin-prettier": "^5.0.0", "postcss": "^8.4.31", @@ -110,9 +109,9 @@ } }, "node_modules/@eslint/js": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.50.0.tgz", - "integrity": "sha512-NCC3zz2+nvYd+Ckfh87rA47zfu2QsQpvc6k1yzTk+b9KzRj0wkGa8LSoGOXN6Zv4lRf/EIoZ80biDh9HOI+RNQ==", + "version": "8.52.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.52.0.tgz", + "integrity": "sha512-mjZVbpaeMZludF2fsWLD0Z9gCref1Tk4i9+wddjRvpUNqqcndPkBD09N/Mapey0b3jaXbLm2kICwFv2E64QinA==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -167,12 +166,12 @@ "integrity": "sha512-qprfWkn82Iw821mcKofJ5Pk9wgioHicxcQMxx+5zt5GSKoqdWvgG5AxVmpmUUjzTLPVSH5auBrhI93Deayn/DA==" }, "node_modules/@humanwhocodes/config-array": { - "version": "0.11.11", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.11.tgz", - "integrity": "sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA==", + "version": "0.11.13", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", + "integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==", "dev": true, "dependencies": { - "@humanwhocodes/object-schema": "^1.2.1", + "@humanwhocodes/object-schema": "^2.0.1", "debug": "^4.1.1", "minimatch": "^3.0.5" }, @@ -194,15 +193,15 @@ } }, "node_modules/@humanwhocodes/object-schema": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", - "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz", + "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==", "dev": true }, "node_modules/@mantine/core": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/@mantine/core/-/core-7.1.2.tgz", - "integrity": "sha512-EZg82V/+uA2bM981mEUOUGfqKIRsMfvxLdAPQpurhtqsnq4yBj1xjC3KzX/Eas9QhhHuR+4DJJyGTuO9aOK6nQ==", + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/@mantine/core/-/core-7.1.5.tgz", + "integrity": "sha512-4jBuy26V4Wdrt7r2dT6d3SKSyU9Gfzxp0ycVTBd2FUb6PvsI/xyZIn8T/aHsJFQ1L5p7IHPcJCIThbmBpVvVtA==", "dependencies": { "@floating-ui/react": "^0.24.8", "clsx": "2.0.0", @@ -212,7 +211,7 @@ "type-fest": "^3.13.1" }, "peerDependencies": { - "@mantine/hooks": "7.1.2", + "@mantine/hooks": "7.1.5", "react": "^18.2.0", "react-dom": "^18.2.0" } @@ -229,9 +228,9 @@ } }, "node_modules/@mantine/form": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/@mantine/form/-/form-7.1.2.tgz", - "integrity": "sha512-FnUu5XNmRM265G0wy19qSRiItG/2eQ0GQCctnokw6ws9ZnCU1NqvsmpuDE/UiV4YCAOhAVHfqnjG/8tsrlw7ug==", + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/@mantine/form/-/form-7.1.5.tgz", + "integrity": "sha512-icBVvmkYdbD1Nea63GyPv0dF7Dq7kJUfIelFpG+BFLhCh/ctiCN+zL6PRGaXYtB9RDTN+/cyaHyvyRlCbtWQPg==", "dependencies": { "fast-deep-equal": "^3.1.3", "klona": "^2.0.5" @@ -241,35 +240,35 @@ } }, "node_modules/@mantine/hooks": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-7.1.2.tgz", - "integrity": "sha512-2sqfBKse/aJq93zEpIn4OY+jRACmDIWBixfBgobRfltDyeL8G+3223LSAaeT6ZD8+h2YBJVmbCD5QY7bx2l11Q==", + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-7.1.5.tgz", + "integrity": "sha512-LuKlJ5VDLYBMcleyKcL6nvcJZQaeJF4mIU5ryEiucy7IleZoD+lqWwNC1VAAN1fsjBRQfhFtFoRihUdIy/vDCA==", "peerDependencies": { "react": "^18.2.0" } }, "node_modules/@mantine/modals": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/@mantine/modals/-/modals-7.1.2.tgz", - "integrity": "sha512-5OOSUzWpnYwnmLILfA8TAmnXogWpzu4Z8v0V+NiiQb2lFADZTl7qnBmw4BbAoT9mwWo3sXlCvepj5bGjsl7pMg==", + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/@mantine/modals/-/modals-7.1.5.tgz", + "integrity": "sha512-VI3stQ2bqPQEhsqQdeBEhwK/Mi2iKlio+Y5TX1jaiYVbrB0WHdC2tGh2oY9W4ehsAkkX7OYbu8+L7hn9IGO3pw==", "peerDependencies": { - "@mantine/core": "7.1.2", - "@mantine/hooks": "7.1.2", + "@mantine/core": "7.1.5", + "@mantine/hooks": "7.1.5", "react": "^18.2.0", "react-dom": "^18.2.0" } }, "node_modules/@mantine/notifications": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/@mantine/notifications/-/notifications-7.1.2.tgz", - "integrity": "sha512-aakf3KRGOnfh+qxGGH/B0ifS5myFi3xO2S0AKD6t//sbQrrUW+SUKh0qyuatnKIx7dxf+DA/sMobyqLUgyzAmg==", + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/@mantine/notifications/-/notifications-7.1.5.tgz", + "integrity": "sha512-/WRxNNgPvRr4munHjCTZaMVjSIpz8ydheccpPGrqOgAN/zfPNWYYcv7kaqXdlb+ag9ZMFsixQB97svvhCRxPCA==", "dependencies": { - "@mantine/store": "7.1.2", + "@mantine/store": "7.1.5", "react-transition-group": "4.4.5" }, "peerDependencies": { - "@mantine/core": "7.1.2", - "@mantine/hooks": "7.1.2", + "@mantine/core": "7.1.5", + "@mantine/hooks": "7.1.5", "react": "^18.2.0", "react-dom": "^18.2.0" } @@ -299,31 +298,31 @@ } }, "node_modules/@mantine/store": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/@mantine/store/-/store-7.1.2.tgz", - "integrity": "sha512-Lf3FLymM0q92BuRC4tZxTxrb9EjVa+J8fqEV147u/Q3aUSNmkhJCqN2MXPbTHIBJ2PsbLtDhy/2edNyIK1KhKQ==", + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/@mantine/store/-/store-7.1.5.tgz", + "integrity": "sha512-iPAt8auWyUs5TyUr31MziCILlLCYCfw6fSqPvLxOwUYpSf9BvtCAoE9JmrRrVi2q5+xO0KPSyK+OHWyBwsAqcQ==", "peerDependencies": { "react": "^18.2.0" } }, "node_modules/@next/env": { - "version": "13.5.4", - "resolved": "https://registry.npmjs.org/@next/env/-/env-13.5.4.tgz", - "integrity": "sha512-LGegJkMvRNw90WWphGJ3RMHMVplYcOfRWf2Be3td3sUa+1AaxmsYyANsA+znrGCBjXJNi4XAQlSoEfUxs/4kIQ==" + "version": "13.5.6", + "resolved": "https://registry.npmjs.org/@next/env/-/env-13.5.6.tgz", + "integrity": "sha512-Yac/bV5sBGkkEXmAX5FWPS9Mmo2rthrOPRQQNfycJPkjUAUclomCPH7QFVCDQ4Mp2k2K1SSM6m0zrxYrOwtFQw==" }, "node_modules/@next/eslint-plugin-next": { - "version": "13.5.4", - "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-13.5.4.tgz", - "integrity": "sha512-vI94U+D7RNgX6XypSyjeFrOzxGlZyxOplU0dVE5norIfZGn/LDjJYPHdvdsR5vN1eRtl6PDAsOHmycFEOljK5A==", + "version": "13.5.6", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-13.5.6.tgz", + "integrity": "sha512-ng7pU/DDsxPgT6ZPvuprxrkeew3XaRf4LAT4FabaEO/hAbvVx4P7wqnqdbTdDn1kgTvsI4tpIgT4Awn/m0bGbg==", "dev": true, "dependencies": { "glob": "7.1.7" } }, "node_modules/@next/swc-darwin-arm64": { - "version": "13.5.4", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.5.4.tgz", - "integrity": "sha512-Df8SHuXgF1p+aonBMcDPEsaahNo2TCwuie7VXED4FVyECvdXfRT9unapm54NssV9tF3OQFKBFOdlje4T43VO0w==", + "version": "13.5.6", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.5.6.tgz", + "integrity": "sha512-5nvXMzKtZfvcu4BhtV0KH1oGv4XEW+B+jOfmBdpFI3C7FrB/MfujRpWYSBBO64+qbW8pkZiSyQv9eiwnn5VIQA==", "cpu": [ "arm64" ], @@ -336,9 +335,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "13.5.4", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-13.5.4.tgz", - "integrity": "sha512-siPuUwO45PnNRMeZnSa8n/Lye5ZX93IJom9wQRB5DEOdFrw0JjOMu1GINB8jAEdwa7Vdyn1oJ2xGNaQpdQQ9Pw==", + "version": "13.5.6", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-13.5.6.tgz", + "integrity": "sha512-6cgBfxg98oOCSr4BckWjLLgiVwlL3vlLj8hXg2b+nDgm4bC/qVXXLfpLB9FHdoDu4057hzywbxKvmYGmi7yUzA==", "cpu": [ "x64" ], @@ -351,9 +350,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "13.5.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.5.4.tgz", - "integrity": "sha512-l/k/fvRP/zmB2jkFMfefmFkyZbDkYW0mRM/LB+tH5u9pB98WsHXC0WvDHlGCYp3CH/jlkJPL7gN8nkTQVrQ/2w==", + "version": "13.5.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.5.6.tgz", + "integrity": "sha512-txagBbj1e1w47YQjcKgSU4rRVQ7uF29YpnlHV5xuVUsgCUf2FmyfJ3CPjZUvpIeXCJAoMCFAoGnbtX86BK7+sg==", "cpu": [ "arm64" ], @@ -366,9 +365,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "13.5.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.5.4.tgz", - "integrity": "sha512-YYGb7SlLkI+XqfQa8VPErljb7k9nUnhhRrVaOdfJNCaQnHBcvbT7cx/UjDQLdleJcfyg1Hkn5YSSIeVfjgmkTg==", + "version": "13.5.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.5.6.tgz", + "integrity": "sha512-cGd+H8amifT86ZldVJtAKDxUqeFyLWW+v2NlBULnLAdWsiuuN8TuhVBt8ZNpCqcAuoruoSWynvMWixTFcroq+Q==", "cpu": [ "arm64" ], @@ -381,9 +380,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "13.5.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.5.4.tgz", - "integrity": "sha512-uE61vyUSClnCH18YHjA8tE1prr/PBFlBFhxBZis4XBRJoR+txAky5d7gGNUIbQ8sZZ7LVkSVgm/5Fc7mwXmRAg==", + "version": "13.5.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.5.6.tgz", + "integrity": "sha512-Mc2b4xiIWKXIhBy2NBTwOxGD3nHLmq4keFk+d4/WL5fMsB8XdJRdtUlL87SqVCTSaf1BRuQQf1HvXZcy+rq3Nw==", "cpu": [ "x64" ], @@ -396,9 +395,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "13.5.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.5.4.tgz", - "integrity": "sha512-qVEKFYML/GvJSy9CfYqAdUexA6M5AklYcQCW+8JECmkQHGoPxCf04iMh7CPR7wkHyWWK+XLt4Ja7hhsPJtSnhg==", + "version": "13.5.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.5.6.tgz", + "integrity": "sha512-CFHvP9Qz98NruJiUnCe61O6GveKKHpJLloXbDSWRhqhkJdZD2zU5hG+gtVJR//tyW897izuHpM6Gtf6+sNgJPQ==", "cpu": [ "x64" ], @@ -411,9 +410,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "13.5.4", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.5.4.tgz", - "integrity": "sha512-mDSQfqxAlfpeZOLPxLymZkX0hYF3juN57W6vFHTvwKlnHfmh12Pt7hPIRLYIShk8uYRsKPtMTth/EzpwRI+u8w==", + "version": "13.5.6", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.5.6.tgz", + "integrity": "sha512-aFv1ejfkbS7PUa1qVPwzDHjQWQtknzAZWGTKYIAaS4NMtBlk3VyA6AYn593pqNanlicewqyl2jUhQAaFV/qXsg==", "cpu": [ "arm64" ], @@ -426,9 +425,9 @@ } }, "node_modules/@next/swc-win32-ia32-msvc": { - "version": "13.5.4", - "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.5.4.tgz", - "integrity": "sha512-aoqAT2XIekIWoriwzOmGFAvTtVY5O7JjV21giozBTP5c6uZhpvTWRbmHXbmsjZqY4HnEZQRXWkSAppsIBweKqw==", + "version": "13.5.6", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.5.6.tgz", + "integrity": "sha512-XqqpHgEIlBHvzwG8sp/JXMFkLAfGLqkbVsyN+/Ih1mR8INb6YCc2x/Mbwi6hsAgUnqQztz8cvEbHJUbSl7RHDg==", "cpu": [ "ia32" ], @@ -441,9 +440,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "13.5.4", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.5.4.tgz", - "integrity": "sha512-cyRvlAxwlddlqeB9xtPSfNSCRy8BOa4wtMo0IuI9P7Y0XT2qpDrpFKRyZ7kUngZis59mPVla5k8X1oOJ8RxDYg==", + "version": "13.5.6", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.5.6.tgz", + "integrity": "sha512-Cqfe1YmOS7k+5mGu92nl5ULkzpKuxJrP3+4AEuPmrpFZ3BHxTY3TnHmU1On3bFmFFs6FbTcdF58CCUProGpIGQ==", "cpu": [ "x64" ], @@ -490,6 +489,384 @@ "node": ">= 8" } }, + "node_modules/@pixi/accessibility": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@pixi/accessibility/-/accessibility-7.3.2.tgz", + "integrity": "sha512-MdkU22HTauRvq9cMeWZIQGaDDa86sr+m12rKNdLV+FaDQgP/AhP+qCVpK7IKeJa9BrWGXaYMw/vueij7HkyDSA==", + "peerDependencies": { + "@pixi/core": "7.3.2", + "@pixi/display": "7.3.2", + "@pixi/events": "7.3.2" + } + }, + "node_modules/@pixi/app": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@pixi/app/-/app-7.3.2.tgz", + "integrity": "sha512-3YRFSMvAxDebAz3/JJv+2jzbPkT8cHC0IHmmLRN8krDL1pZV+YjMLgMwN/Oeyv5TSbwNqnrF5su5whNkRaxeZQ==", + "peerDependencies": { + "@pixi/core": "7.3.2", + "@pixi/display": "7.3.2" + } + }, + "node_modules/@pixi/assets": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@pixi/assets/-/assets-7.3.2.tgz", + "integrity": "sha512-yteq6ptAxA09EcwU9D9hl7qr5yWIqy+c2PsXkTDkc76vTAwIamLY3KxLq2aR5y1U4L4O6aHFJd26uNhHcuTPmw==", + "dependencies": { + "@types/css-font-loading-module": "^0.0.7" + }, + "peerDependencies": { + "@pixi/core": "7.3.2", + "@pixi/utils": "7.3.2" + } + }, + "node_modules/@pixi/color": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@pixi/color/-/color-7.3.2.tgz", + "integrity": "sha512-jur5PvdOtUBEUTjmPudW5qdQq6yYGlVGsi3HyhasJw14bN+GKJwiCKgIsyrsiNL5HBUXmje4ICwQohf6BqKqxA==", + "dependencies": { + "@pixi/colord": "^2.9.6" + } + }, + "node_modules/@pixi/colord": { + "version": "2.9.6", + "resolved": "https://registry.npmjs.org/@pixi/colord/-/colord-2.9.6.tgz", + "integrity": "sha512-nezytU2pw587fQstUu1AsJZDVEynjskwOL+kibwcdxsMBFqPsFFNA7xl0ii/gXuDi6M0xj3mfRJj8pBSc2jCfA==" + }, + "node_modules/@pixi/compressed-textures": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@pixi/compressed-textures/-/compressed-textures-7.3.2.tgz", + "integrity": "sha512-J3ENMHDPQO6CJRei55gqI0WmiZJIK6SgsW5AEkShT0aAe5miEBSomv70pXw/58ru+4/Hx8cXjamsGt4aQB2D0Q==", + "peerDependencies": { + "@pixi/assets": "7.3.2", + "@pixi/core": "7.3.2" + } + }, + "node_modules/@pixi/constants": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@pixi/constants/-/constants-7.3.2.tgz", + "integrity": "sha512-Q8W3ncsFxmfgC5EtokpG92qJZabd+Dl+pbQAdHwiPY3v+8UNq77u4VN2qtl1Z04864hCcg7AStIYEDrzqTLF6Q==" + }, + "node_modules/@pixi/core": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@pixi/core/-/core-7.3.2.tgz", + "integrity": "sha512-Pta3ee8MtJ3yKxGXzglBWgwbEOKMB6Eth+FpLTjL0rgxiqTB550YX6jsNEQQAzcGjCBlO3rC/IF57UZ2go/X6w==", + "dependencies": { + "@pixi/color": "7.3.2", + "@pixi/constants": "7.3.2", + "@pixi/extensions": "7.3.2", + "@pixi/math": "7.3.2", + "@pixi/runner": "7.3.2", + "@pixi/settings": "7.3.2", + "@pixi/ticker": "7.3.2", + "@pixi/utils": "7.3.2", + "@types/offscreencanvas": "^2019.6.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/pixijs" + } + }, + "node_modules/@pixi/display": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@pixi/display/-/display-7.3.2.tgz", + "integrity": "sha512-cY5AnZ3TWt5GYGx4e5AQ2/2U9kP+RorBg/O30amJ+8e9bFk9rS8cjh/DDq/hc4lql96BkXAInTl40eHnAML5lQ==", + "peerDependencies": { + "@pixi/core": "7.3.2" + } + }, + "node_modules/@pixi/events": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@pixi/events/-/events-7.3.2.tgz", + "integrity": "sha512-Moca9epu8jk1wIQCdVYjhz2pD9Ol21m50wvWUKvpgt9yM/AjkCLSDt8HO/PmTpavDrkhx5pVVWeDDA6FyUNaGA==", + "peerDependencies": { + "@pixi/core": "7.3.2", + "@pixi/display": "7.3.2" + } + }, + "node_modules/@pixi/extensions": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@pixi/extensions/-/extensions-7.3.2.tgz", + "integrity": "sha512-Qw84ADfvmVu4Mwj+zTik/IEEK9lWS5n4trbrpQCcEZ+Mb8oRAXWvKz199mi1s7+LaZXDqeCY1yr2PHQaFf1KBA==" + }, + "node_modules/@pixi/extract": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@pixi/extract/-/extract-7.3.2.tgz", + "integrity": "sha512-KsoflvQZV/XD8A8xbtRnmI4reYekbI4MOi7ilwQe5tMz6O1mO7IzrSukxkSMD02f6SpbAqbi7a1EayTjvY0ECQ==", + "peerDependencies": { + "@pixi/core": "7.3.2" + } + }, + "node_modules/@pixi/filter-alpha": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@pixi/filter-alpha/-/filter-alpha-7.3.2.tgz", + "integrity": "sha512-nZMdn310wH5ZK1slwv3X4qT8eLoAGO7SgYGCy5IsMtpCtNObzE9XA4tAfhXrjihyzPS9KvszgAbnv1Qpfh0/uw==", + "peerDependencies": { + "@pixi/core": "7.3.2" + } + }, + "node_modules/@pixi/filter-blur": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@pixi/filter-blur/-/filter-blur-7.3.2.tgz", + "integrity": "sha512-unu3zhwHMhN+iAe7Td2rK40i2UJ2GOhzWK+6jcU3ZkMOsFCT5kgBoMRTejeQVcvCs6GoYK8imbkE7mXt05Vj6A==", + "peerDependencies": { + "@pixi/core": "7.3.2" + } + }, + "node_modules/@pixi/filter-color-matrix": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@pixi/filter-color-matrix/-/filter-color-matrix-7.3.2.tgz", + "integrity": "sha512-rbyjes/9SMoV9jjPiK0sLMkmLfN8D17GoTJIfq/KLv1x9646W5fL2QSKkN04UkZ+020ndWvIOxK1S97tvRyCfg==", + "peerDependencies": { + "@pixi/core": "7.3.2" + } + }, + "node_modules/@pixi/filter-displacement": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@pixi/filter-displacement/-/filter-displacement-7.3.2.tgz", + "integrity": "sha512-ZHl7Sfb8JYd9Z6j96OHCC0NhMKhhXJRE5AbkSDohjEMVCK1BV5rDGAHV8WVt/2MJ/j83CXUpydzyMhdM4lMchg==", + "peerDependencies": { + "@pixi/core": "7.3.2" + } + }, + "node_modules/@pixi/filter-fxaa": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@pixi/filter-fxaa/-/filter-fxaa-7.3.2.tgz", + "integrity": "sha512-9brtlxDnQTZk2XiFBKdBK9e+8CX9LdxxcL7LRpjEyiHuAPvTlQgu9B85LrJ4GzWKqJJKaIIZBzhIoiCLUnfeXg==", + "peerDependencies": { + "@pixi/core": "7.3.2" + } + }, + "node_modules/@pixi/filter-noise": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@pixi/filter-noise/-/filter-noise-7.3.2.tgz", + "integrity": "sha512-F8GQQ20n7tCjThX6GCXckiXz2YffOCxicTJ0oat9aVDZh+sVsAxYX0aKSdHh0hhv18F0yuc6tPsSL5DYb63xFg==", + "peerDependencies": { + "@pixi/core": "7.3.2" + } + }, + "node_modules/@pixi/graphics": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@pixi/graphics/-/graphics-7.3.2.tgz", + "integrity": "sha512-PhU6j1yub4tH/s+/gqByzgZ3mLv1mfb6iGXbquycg3+WypcxHZn0opFtI/axsazaQ9SEaWxw1m3i40WG5ANH5g==", + "peerDependencies": { + "@pixi/core": "7.3.2", + "@pixi/display": "7.3.2", + "@pixi/sprite": "7.3.2" + } + }, + "node_modules/@pixi/math": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@pixi/math/-/math-7.3.2.tgz", + "integrity": "sha512-dutoZ0IVJ5ME7UtYNo2szu4D7qsgtJB7e3ylujBVu7BOP2e710BVtFwFSFV768N14h9H5roGnuzVoDiJac2u+w==" + }, + "node_modules/@pixi/mesh": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@pixi/mesh/-/mesh-7.3.2.tgz", + "integrity": "sha512-LFkt7ELYXQLgbgHpjl68j6JD5ejUwma8zoPn2gqSBbY+6pK/phjvV1Wkh76muF46VvNulgXF0+qLIDdCsfrDaA==", + "peerDependencies": { + "@pixi/core": "7.3.2", + "@pixi/display": "7.3.2" + } + }, + "node_modules/@pixi/mesh-extras": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@pixi/mesh-extras/-/mesh-extras-7.3.2.tgz", + "integrity": "sha512-s/tg9TsTZZxLEdCDKWnBChDGkc041HCTP7ykJv4fEROzb9B0lskULYyvv+/YNNKa2Ugb9WnkMknpOdOXCpjyyg==", + "peerDependencies": { + "@pixi/core": "7.3.2", + "@pixi/mesh": "7.3.2" + } + }, + "node_modules/@pixi/mixin-cache-as-bitmap": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@pixi/mixin-cache-as-bitmap/-/mixin-cache-as-bitmap-7.3.2.tgz", + "integrity": "sha512-bZRlyUN5+9kCUjn67V0IFtYIrbmx9Vs4sMOmXyrX3Q4B4gPLE46IzZz3v0IVaTjp32udlQztfJalIaWbuqgb3A==", + "peerDependencies": { + "@pixi/core": "7.3.2", + "@pixi/display": "7.3.2", + "@pixi/sprite": "7.3.2" + } + }, + "node_modules/@pixi/mixin-get-child-by-name": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@pixi/mixin-get-child-by-name/-/mixin-get-child-by-name-7.3.2.tgz", + "integrity": "sha512-mbUi3WxXrkViH7qOgjk4fu2BN36NwNb7u+Fy1J5dS8Bntj57ZVKmEV9PbUy0zYjXE8rVmeAvSu/2kbn5n9UutQ==", + "peerDependencies": { + "@pixi/display": "7.3.2" + } + }, + "node_modules/@pixi/mixin-get-global-position": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@pixi/mixin-get-global-position/-/mixin-get-global-position-7.3.2.tgz", + "integrity": "sha512-1nhWbBgmw6rK7yQJxzeI9yjKYYEkM5i3pee8qVu4YWo3b1xWVQA7osQG7aGM/4qywDkXaA1ZvciA5hfg6f4Q5Q==", + "peerDependencies": { + "@pixi/core": "7.3.2", + "@pixi/display": "7.3.2" + } + }, + "node_modules/@pixi/particle-container": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@pixi/particle-container/-/particle-container-7.3.2.tgz", + "integrity": "sha512-JYc4j4z97KmxyLp+1Lg0SNi8hy6RxcBBNQGk+CSLNXeDWxx3hykT5gj/ORX1eXyzHh1ZCG1XzeVS9Yr8QhlFHA==", + "peerDependencies": { + "@pixi/core": "7.3.2", + "@pixi/display": "7.3.2", + "@pixi/sprite": "7.3.2" + } + }, + "node_modules/@pixi/prepare": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@pixi/prepare/-/prepare-7.3.2.tgz", + "integrity": "sha512-aLPAXSYLUhMwxzJtn9m0TSZe+dQlZCt09QNBqYbSi8LZId54QMDyvfBb4zBOJZrD2xAZgYL5RIJuKHwZtFX6lQ==", + "peerDependencies": { + "@pixi/core": "7.3.2", + "@pixi/display": "7.3.2", + "@pixi/graphics": "7.3.2", + "@pixi/text": "7.3.2" + } + }, + "node_modules/@pixi/react": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@pixi/react/-/react-7.1.1.tgz", + "integrity": "sha512-W3LILsYiUxavrLoDGiIuQneFgSzsUuHkt6VVuAaqUPEGjMBzicgA5v5R2dPDmu3B+5BRwAHcJZttUKH7zFjbGw==", + "dependencies": { + "lodash.isnil": "4.0.0", + "lodash.times": "4.3.2", + "performance-now": "2.1.0", + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "@babel/runtime": "^7.14.8", + "@pixi/app": ">=6.0.0", + "@pixi/constants": ">=6.0.0", + "@pixi/core": ">=6.0.0", + "@pixi/display": ">=6.0.0", + "@pixi/extensions": ">=6.0.0", + "@pixi/graphics": ">=6.0.0", + "@pixi/math": ">=6.0.0", + "@pixi/mesh": ">=6.0.0", + "@pixi/mesh-extras": ">=6.0.0", + "@pixi/particle-container": ">=6.0.0", + "@pixi/sprite": ">=6.0.0", + "@pixi/sprite-animated": ">=6.0.0", + "@pixi/sprite-tiling": ">=6.0.0", + "@pixi/text": ">=6.0.0", + "@pixi/text-bitmap": ">=6.0.0", + "@pixi/ticker": ">=6.0.0", + "prop-types": "^15.8.1", + "react": ">=17.0.0", + "react-dom": ">=17.0.0" + } + }, + "node_modules/@pixi/runner": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@pixi/runner/-/runner-7.3.2.tgz", + "integrity": "sha512-maKotoKJCQiQGBJwfM+iYdQKjrPN/Tn9+72F4WIf706zp/5vKoxW688Rsktg5BX4Mcn7ZkZvcJYTxj2Mv87lFA==" + }, + "node_modules/@pixi/settings": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@pixi/settings/-/settings-7.3.2.tgz", + "integrity": "sha512-vtxzuARDTbFe0fRYSqB53B+mPpX7v+QjjnCUmVMVvZiWr3QcngMWVml6c6dQDln7IakWoKZRrNG4FpggvDgLVg==", + "dependencies": { + "@pixi/constants": "7.3.2", + "@types/css-font-loading-module": "^0.0.7", + "ismobilejs": "^1.1.0" + } + }, + "node_modules/@pixi/sprite": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@pixi/sprite/-/sprite-7.3.2.tgz", + "integrity": "sha512-IpWTKXExJNXVcY7ITopJ+JW48DahdbCo/81D2IYzBImq3jyiJM2Km5EoJgvAM5ZQ3Ev3KPPIBzYLD+HoPWcxdw==", + "peerDependencies": { + "@pixi/core": "7.3.2", + "@pixi/display": "7.3.2" + } + }, + "node_modules/@pixi/sprite-animated": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@pixi/sprite-animated/-/sprite-animated-7.3.2.tgz", + "integrity": "sha512-j9pyUe4cefxE9wecNfbWQyL5fBQKvCGYaOA0DE1X46ukBHrIuhA8u3jg2X3N3r4IcbVvxpWFYDrDsWXWeiBmSw==", + "peerDependencies": { + "@pixi/core": "7.3.2", + "@pixi/sprite": "7.3.2" + } + }, + "node_modules/@pixi/sprite-tiling": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@pixi/sprite-tiling/-/sprite-tiling-7.3.2.tgz", + "integrity": "sha512-tWVVb/rMIx5AczfUrVxa0dZaIufP5C0IOL7IGfFUDQqDu5JSAUC0mwLe4F12jAXBVsqYhCGYx5bIHbPiI5vcSQ==", + "peerDependencies": { + "@pixi/core": "7.3.2", + "@pixi/display": "7.3.2", + "@pixi/sprite": "7.3.2" + } + }, + "node_modules/@pixi/spritesheet": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@pixi/spritesheet/-/spritesheet-7.3.2.tgz", + "integrity": "sha512-UkwqrPYDqrEdK5ub9qn/9VBvt5caA8ffV5iYR6ssCvrpaQovBKmS+b5pr/BYf8xNTExDpR3OmPIo8iDEYWWLuw==", + "peerDependencies": { + "@pixi/assets": "7.3.2", + "@pixi/core": "7.3.2" + } + }, + "node_modules/@pixi/text": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@pixi/text/-/text-7.3.2.tgz", + "integrity": "sha512-LdtNj+K5tPB/0UcDcO52M/C7xhwFTGFhtdF42fPhRuJawM23M3zm1Y8PapXv+mury+IxCHT1w30YlAi0qTVpKQ==", + "peerDependencies": { + "@pixi/core": "7.3.2", + "@pixi/sprite": "7.3.2" + } + }, + "node_modules/@pixi/text-bitmap": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@pixi/text-bitmap/-/text-bitmap-7.3.2.tgz", + "integrity": "sha512-p8KLgtZSPowWU/Zj+GVtfsUT8uGYo4TtKKYbLoWuxkRA5Pc1+4C9/rV/EOSFfoZIdW5C+iFg5VxRgBllUQf+aA==", + "peerDependencies": { + "@pixi/assets": "7.3.2", + "@pixi/core": "7.3.2", + "@pixi/display": "7.3.2", + "@pixi/mesh": "7.3.2", + "@pixi/text": "7.3.2" + } + }, + "node_modules/@pixi/text-html": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@pixi/text-html/-/text-html-7.3.2.tgz", + "integrity": "sha512-IYhBWEPOvqUtlHkS5/c1Hseuricj5jrrGd21ivcvHmcnK/x2m+CRGvvzeBp1mqoYBnDbQVrD2wSXSe4Dv9tEJA==", + "peerDependencies": { + "@pixi/core": "7.3.2", + "@pixi/display": "7.3.2", + "@pixi/sprite": "7.3.2", + "@pixi/text": "7.3.2" + } + }, + "node_modules/@pixi/ticker": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@pixi/ticker/-/ticker-7.3.2.tgz", + "integrity": "sha512-5kIPhBeXwDJohCzKzJJ6T7f1oAGbHAgeiwOjlTO+9lNXUX8ZPj0407V3syuF+64kFqJzIBCznBRpI+fmT4c9SA==", + "dependencies": { + "@pixi/extensions": "7.3.2", + "@pixi/settings": "7.3.2", + "@pixi/utils": "7.3.2" + } + }, + "node_modules/@pixi/utils": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@pixi/utils/-/utils-7.3.2.tgz", + "integrity": "sha512-KhNvj9YcY7Zi2dTKZgDpx8C6OxKKR541vwtG6JgdBZZYDeMBOIghN2Vi5zn4diW5BhDfHBmdSJ1wZXEtE2MDwg==", + "dependencies": { + "@pixi/color": "7.3.2", + "@pixi/constants": "7.3.2", + "@pixi/settings": "7.3.2", + "@types/earcut": "^2.1.0", + "earcut": "^2.2.4", + "eventemitter3": "^4.0.0", + "url": "^0.11.0" + } + }, "node_modules/@pkgr/utils": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/@pkgr/utils/-/utils-2.4.2.tgz", @@ -510,16 +887,6 @@ "url": "https://opencollective.com/unts" } }, - "node_modules/@react-leaflet/core": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@react-leaflet/core/-/core-2.1.0.tgz", - "integrity": "sha512-Qk7Pfu8BSarKGqILj4x7bCSZ1pjuAPZ+qmRwH5S7mDS91VSbVVsJSrW4qA+GPrro8t69gFYVMWb1Zc4yFmPiVg==", - "peerDependencies": { - "leaflet": "^1.9.0", - "react": "^18.0.0", - "react-dom": "^18.0.0" - } - }, "node_modules/@rushstack/eslint-patch": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.3.3.tgz", @@ -534,70 +901,26 @@ "tslib": "^2.4.0" } }, - "node_modules/@types/d3-array": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.0.8.tgz", - "integrity": "sha512-2xAVyAUgaXHX9fubjcCbGAUOqYfRJN1em1EKR2HfzWBpObZhwfnZKvofTN4TplMqJdFQao61I+NVSai/vnBvDQ==" + "node_modules/@types/css-font-loading-module": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/@types/css-font-loading-module/-/css-font-loading-module-0.0.7.tgz", + "integrity": "sha512-nl09VhutdjINdWyXxHWN/w9zlNCfr60JUqJbd24YXUuCwgeL0TpFSdElCwb6cxfB6ybE19Gjj4g0jsgkXxKv1Q==" }, - "node_modules/@types/d3-color": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.1.tgz", - "integrity": "sha512-CSAVrHAtM9wfuLJ2tpvvwCU/F22sm7rMHNN+yh9D6O6hyAms3+O0cgMpC1pm6UEUMOntuZC8bMt74PteiDUdCg==" - }, - "node_modules/@types/d3-ease": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.0.tgz", - "integrity": "sha512-aMo4eaAOijJjA6uU+GIeW018dvy9+oH5Y2VPPzjjfxevvGQ/oRDs+tfYC9b50Q4BygRR8yE2QCLsrT0WtAVseA==" - }, - "node_modules/@types/d3-interpolate": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.2.tgz", - "integrity": "sha512-zAbCj9lTqW9J9PlF4FwnvEjXZUy75NQqPm7DMHZXuxCFTpuTrdK2NMYGQekf4hlasL78fCYOLu4EE3/tXElwow==", - "dependencies": { - "@types/d3-color": "*" - } - }, - "node_modules/@types/d3-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.0.0.tgz", - "integrity": "sha512-0g/A+mZXgFkQxN3HniRDbXMN79K3CdTpLsevj+PXiTcb2hVyvkZUBg37StmgCQkaD84cUJ4uaDAWq7UJOQy2Tg==" - }, - "node_modules/@types/d3-scale": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.5.tgz", - "integrity": "sha512-w/C++3W394MHzcLKO2kdsIn5KKNTOqeQVzyPSGPLzQbkPw/jpeaGtSRlakcKevGgGsjJxGsbqS0fPrVFDbHrDA==", - "dependencies": { - "@types/d3-time": "*" - } - }, - "node_modules/@types/d3-shape": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.3.tgz", - "integrity": "sha512-cHMdIq+rhF5IVwAV7t61pcEXfEHsEsrbBUPkFGBwTXuxtTAkBBrnrNA8++6OWm3jwVsXoZYQM8NEekg6CPJ3zw==", - "dependencies": { - "@types/d3-path": "*" - } - }, - "node_modules/@types/d3-time": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.1.tgz", - "integrity": "sha512-5j/AnefKAhCw4HpITmLDTPlf4vhi8o/dES+zbegfPb7LaGfNyqkLxBR6E+4yvTAgnJLmhe80EXFMzUs38fw4oA==" - }, - "node_modules/@types/d3-timer": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.0.tgz", - "integrity": "sha512-HNB/9GHqu7Fo8AQiugyJbv6ZxYz58wef0esl4Mv828w1ZKpAshw/uFWVDUcIB9KKFeFKoxS3cHY07FFgtTRZ1g==" + "node_modules/@types/earcut": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@types/earcut/-/earcut-2.1.3.tgz", + "integrity": "sha512-pskpibEbm73+7nA9RqxGEnAiALRO92DdoSVxasyjGrqzEndaSDjFG73GCtstMzhdOowZMItVw2fhTdxVrY221w==" }, "node_modules/@types/js-cookie": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-3.0.4.tgz", - "integrity": "sha512-vMMnFF+H5KYqdd/myCzq6wLDlPpteJK+jGFgBus3Da7lw+YsDmx2C8feGTzY2M3Fo823yON+HC2CL240j4OV+w==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-3.0.5.tgz", + "integrity": "sha512-dtLshqoiGRDHbHueIT9sjkd2F4tW1qPSX2xKAQK8p1e6pM+Z913GM1shv7dOqqasEMYbC5zEaClJomQe8OtQLA==", "dev": true }, "node_modules/@types/json-schema": { - "version": "7.0.13", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.13.tgz", - "integrity": "sha512-RbSSoHliUbnXj3ny0CNFOoxrIDV6SUGyStHsvDqosw6CkdPV8TtWGlfecuK4ToyMEAql6pzNxgCFKanovUzlgQ==", + "version": "7.0.14", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.14.tgz", + "integrity": "sha512-U3PUjAudAdJBeC2pgN8uTIKgxrb4nlDF3SF0++EldXQvQBGkpFZMSnwQiIoDU77tv45VgNkl/L4ouD+rEomujw==", "dev": true }, "node_modules/@types/json5": { @@ -607,10 +930,18 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.8.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.2.tgz", - "integrity": "sha512-Vvycsc9FQdwhxE3y3DzeIxuEJbWGDsnrxvMADzTDF/lcdR9/K+AQIeAghTQsHtotg/q0j3WEOYS/jQgSdWue3w==", - "dev": true + "version": "20.8.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.7.tgz", + "integrity": "sha512-21TKHHh3eUHIi2MloeptJWALuCu5H7HQTdTrWIFReA8ad+aggoX+lRes3ex7/FtpC+sVUpFMQ+QTfYr74mruiQ==", + "dev": true, + "dependencies": { + "undici-types": "~5.25.1" + } + }, + "node_modules/@types/offscreencanvas": { + "version": "2019.7.2", + "resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.7.2.tgz", + "integrity": "sha512-ujCjOxeA07IbEBQYAkoOI+XFw5sT3nhWJ/xZfPR6reJppDG7iPQPZacQiLTtWH1b3a2NYXWlxvYqa40y/LAixQ==" }, "node_modules/@types/prop-types": { "version": "15.7.5", @@ -619,9 +950,9 @@ "devOptional": true }, "node_modules/@types/react": { - "version": "18.2.24", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.24.tgz", - "integrity": "sha512-Ee0Jt4sbJxMu1iDcetZEIKQr99J1Zfb6D4F3qfUWoR1JpInkY1Wdg4WwCyBjL257D0+jGqSl1twBjV8iCaC0Aw==", + "version": "18.2.31", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.31.tgz", + "integrity": "sha512-c2UnPv548q+5DFh03y8lEDeMfDwBn9G3dRwfkrxQMo/dOtRHUUO57k6pHvBIfH/VF4Nh+98mZ5aaSe+2echD5g==", "devOptional": true, "dependencies": { "@types/prop-types": "*", @@ -630,9 +961,9 @@ } }, "node_modules/@types/react-dom": { - "version": "18.2.8", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.8.tgz", - "integrity": "sha512-bAIvO5lN/U8sPGvs1Xm61rlRHHaq5rp5N3kp9C+NJ/Q41P8iqjkXSu0+/qu8POsjH9pNWb0OYabFez7taP7omw==", + "version": "18.2.14", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.14.tgz", + "integrity": "sha512-V835xgdSVmyQmI1KLV2BEIUgqEuinxp9O4G6g3FqO/SqLac049E53aysv0oEFD2kHfejeKU+ZqL2bcFWj9gLAQ==", "dev": true, "dependencies": { "@types/react": "*" @@ -645,22 +976,22 @@ "devOptional": true }, "node_modules/@types/semver": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.3.tgz", - "integrity": "sha512-OxepLK9EuNEIPxWNME+C6WwbRAOOI2o2BaQEGzz5Lu2e4Z5eDnEo+/aVEDMIXywoJitJ7xWd641wrGLZdtwRyw==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-MMzuxN3GdFwskAnb6fz0orFvhfqi752yjaXylr0Rp4oDg5H0Zn1IuyRhDVvYOwAXoJirx2xuS16I3WjxnAIHiQ==", "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "6.7.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.7.4.tgz", - "integrity": "sha512-DAbgDXwtX+pDkAHwiGhqP3zWUGpW49B7eqmgpPtg+BKJXwdct79ut9+ifqOFPJGClGKSHXn2PTBatCnldJRUoA==", + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.8.0.tgz", + "integrity": "sha512-GosF4238Tkes2SHPQ1i8f6rMtG6zlKwMEB0abqSJ3Npvos+doIlc/ATG+vX1G9coDF3Ex78zM3heXHLyWEwLUw==", "dev": true, "dependencies": { "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "6.7.4", - "@typescript-eslint/type-utils": "6.7.4", - "@typescript-eslint/utils": "6.7.4", - "@typescript-eslint/visitor-keys": "6.7.4", + "@typescript-eslint/scope-manager": "6.8.0", + "@typescript-eslint/type-utils": "6.8.0", + "@typescript-eslint/utils": "6.8.0", + "@typescript-eslint/visitor-keys": "6.8.0", "debug": "^4.3.4", "graphemer": "^1.4.0", "ignore": "^5.2.4", @@ -686,15 +1017,15 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "6.7.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.7.4.tgz", - "integrity": "sha512-I5zVZFY+cw4IMZUeNCU7Sh2PO5O57F7Lr0uyhgCJmhN/BuTlnc55KxPonR4+EM3GBdfiCyGZye6DgMjtubQkmA==", + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.8.0.tgz", + "integrity": "sha512-5tNs6Bw0j6BdWuP8Fx+VH4G9fEPDxnVI7yH1IAPkQH5RUtvKwRoqdecAPdQXv4rSOADAaz1LFBZvZG7VbXivSg==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "6.7.4", - "@typescript-eslint/types": "6.7.4", - "@typescript-eslint/typescript-estree": "6.7.4", - "@typescript-eslint/visitor-keys": "6.7.4", + "@typescript-eslint/scope-manager": "6.8.0", + "@typescript-eslint/types": "6.8.0", + "@typescript-eslint/typescript-estree": "6.8.0", + "@typescript-eslint/visitor-keys": "6.8.0", "debug": "^4.3.4" }, "engines": { @@ -714,13 +1045,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "6.7.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.7.4.tgz", - "integrity": "sha512-SdGqSLUPTXAXi7c3Ob7peAGVnmMoGzZ361VswK2Mqf8UOYcODiYvs8rs5ILqEdfvX1lE7wEZbLyELCW+Yrql1A==", + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.8.0.tgz", + "integrity": "sha512-xe0HNBVwCph7rak+ZHcFD6A+q50SMsFwcmfdjs9Kz4qDh5hWhaPhFjRs/SODEhroBI5Ruyvyz9LfwUJ624O40g==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.7.4", - "@typescript-eslint/visitor-keys": "6.7.4" + "@typescript-eslint/types": "6.8.0", + "@typescript-eslint/visitor-keys": "6.8.0" }, "engines": { "node": "^16.0.0 || >=18.0.0" @@ -731,13 +1062,13 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "6.7.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.7.4.tgz", - "integrity": "sha512-n+g3zi1QzpcAdHFP9KQF+rEFxMb2KxtnJGID3teA/nxKHOVi3ylKovaqEzGBbVY2pBttU6z85gp0D00ufLzViQ==", + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.8.0.tgz", + "integrity": "sha512-RYOJdlkTJIXW7GSldUIHqc/Hkto8E+fZN96dMIFhuTJcQwdRoGN2rEWA8U6oXbLo0qufH7NPElUb+MceHtz54g==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "6.7.4", - "@typescript-eslint/utils": "6.7.4", + "@typescript-eslint/typescript-estree": "6.8.0", + "@typescript-eslint/utils": "6.8.0", "debug": "^4.3.4", "ts-api-utils": "^1.0.1" }, @@ -758,9 +1089,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "6.7.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.7.4.tgz", - "integrity": "sha512-o9XWK2FLW6eSS/0r/tgjAGsYasLAnOWg7hvZ/dGYSSNjCh+49k5ocPN8OmG5aZcSJ8pclSOyVKP2x03Sj+RrCA==", + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.8.0.tgz", + "integrity": "sha512-p5qOxSum7W3k+llc7owEStXlGmSl8FcGvhYt8Vjy7FqEnmkCVlM3P57XQEGj58oqaBWDQXbJDZxwUWMS/EAPNQ==", "dev": true, "engines": { "node": "^16.0.0 || >=18.0.0" @@ -771,13 +1102,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "6.7.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.7.4.tgz", - "integrity": "sha512-ty8b5qHKatlNYd9vmpHooQz3Vki3gG+3PchmtsA4TgrZBKWHNjWfkQid7K7xQogBqqc7/BhGazxMD5vr6Ha+iQ==", + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.8.0.tgz", + "integrity": "sha512-ISgV0lQ8XgW+mvv5My/+iTUdRmGspducmQcDw5JxznasXNnZn3SKNrTRuMsEXv+V/O+Lw9AGcQCfVaOPCAk/Zg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.7.4", - "@typescript-eslint/visitor-keys": "6.7.4", + "@typescript-eslint/types": "6.8.0", + "@typescript-eslint/visitor-keys": "6.8.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -798,17 +1129,17 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "6.7.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.7.4.tgz", - "integrity": "sha512-PRQAs+HUn85Qdk+khAxsVV+oULy3VkbH3hQ8hxLRJXWBEd7iI+GbQxH5SEUSH7kbEoTp6oT1bOwyga24ELALTA==", + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.8.0.tgz", + "integrity": "sha512-dKs1itdE2qFG4jr0dlYLQVppqTE+Itt7GmIf/vX6CSvsW+3ov8PbWauVKyyfNngokhIO9sKZeRGCUo1+N7U98Q==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@types/json-schema": "^7.0.12", "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "6.7.4", - "@typescript-eslint/types": "6.7.4", - "@typescript-eslint/typescript-estree": "6.7.4", + "@typescript-eslint/scope-manager": "6.8.0", + "@typescript-eslint/types": "6.8.0", + "@typescript-eslint/typescript-estree": "6.8.0", "semver": "^7.5.4" }, "engines": { @@ -823,12 +1154,12 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "6.7.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.7.4.tgz", - "integrity": "sha512-pOW37DUhlTZbvph50x5zZCkFn3xzwkGtNoJHzIM3svpiSkJzwOYr/kVBaXmf+RAQiUDs1AHEZVNPg6UJCJpwRA==", + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.8.0.tgz", + "integrity": "sha512-oqAnbA7c+pgOhW2OhGvxm0t1BULX5peQI/rLsNDpGM78EebV3C9IGbX5HNZabuZ6UQrYveCLjKo8Iy/lLlBkkg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.7.4", + "@typescript-eslint/types": "6.8.0", "eslint-visitor-keys": "^3.4.1" }, "engines": { @@ -839,6 +1170,12 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true + }, "node_modules/acorn": { "version": "8.10.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", @@ -1071,11 +1408,6 @@ "has-symbols": "^1.0.3" } }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" - }, "node_modules/autoprefixer": { "version": "10.4.16", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.16.tgz", @@ -1134,16 +1466,6 @@ "node": ">=4" } }, - "node_modules/axios": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.5.1.tgz", - "integrity": "sha512-Q28iYCWzNHjAm+yEAot5QaAMxhMghWLFVf7rRdwhUI+c2jix2DUXjAHXVi+s1ibs3mjPO/cCgbA++3BjD0vP/A==", - "dependencies": { - "follow-redirects": "^1.15.0", - "form-data": "^4.0.0", - "proxy-from-env": "^1.1.0" - } - }, "node_modules/axobject-query": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz", @@ -1264,7 +1586,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", - "dev": true, "dependencies": { "function-bind": "^1.1.1", "get-intrinsic": "^1.0.2" @@ -1326,11 +1647,6 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/classnames": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz", - "integrity": "sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==" - }, "node_modules/client-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", @@ -1362,17 +1678,6 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -1393,11 +1698,6 @@ "node": ">= 8" } }, - "node_modules/css-unit-converter": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/css-unit-converter/-/css-unit-converter-1.1.2.tgz", - "integrity": "sha512-IiJwMC8rdZE0+xiEZHeru6YoONC4rfPMqGm2W85jMIbkFvv5nFTwJVFHam2eFrN6txmoUYFAFXiv8ICVeTO0MA==" - }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -1415,116 +1715,6 @@ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" }, - "node_modules/d3-array": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", - "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", - "dependencies": { - "internmap": "1 - 2" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-color": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", - "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-ease": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", - "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-format": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", - "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-interpolate": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", - "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", - "dependencies": { - "d3-color": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-path": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", - "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-scale": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", - "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", - "dependencies": { - "d3-array": "2.10.0 - 3", - "d3-format": "1 - 3", - "d3-interpolate": "1.2.0 - 3", - "d3-time": "2.1.1 - 3", - "d3-time-format": "2 - 4" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-shape": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", - "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", - "dependencies": { - "d3-path": "^3.1.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-time": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", - "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", - "dependencies": { - "d3-array": "2 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-time-format": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", - "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", - "dependencies": { - "d3-time": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-timer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", - "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", - "engines": { - "node": ">=12" - } - }, "node_modules/damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -1548,11 +1738,6 @@ } } }, - "node_modules/decimal.js-light": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", - "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==" - }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -1621,14 +1806,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -1667,13 +1844,10 @@ "node": ">=6.0.0" } }, - "node_modules/dom-helpers": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-3.4.0.tgz", - "integrity": "sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA==", - "dependencies": { - "@babel/runtime": "^7.1.2" - } + "node_modules/earcut": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/earcut/-/earcut-2.2.4.tgz", + "integrity": "sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==" }, "node_modules/electron-to-chromium": { "version": "1.4.512", @@ -1837,18 +2011,19 @@ } }, "node_modules/eslint": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.50.0.tgz", - "integrity": "sha512-FOnOGSuFuFLv/Sa+FDVRZl4GGVAAFFi8LecRsI5a1tMO5HIE8nCm4ivAlzt4dT3ol/PaaGC0rJEEXQmHJBGoOg==", + "version": "8.52.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.52.0.tgz", + "integrity": "sha512-zh/JHnaixqHZsolRB/w9/02akBk9EPrOs9JwcTP2ek7yL5bVvXuRariiaAjjoJ5DvuwQ1WAE/HsMz+w17YgBCg==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.2", - "@eslint/js": "8.50.0", - "@humanwhocodes/config-array": "^0.11.11", + "@eslint/js": "8.52.0", + "@humanwhocodes/config-array": "^0.11.13", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", @@ -1891,12 +2066,12 @@ } }, "node_modules/eslint-config-next": { - "version": "13.5.4", - "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-13.5.4.tgz", - "integrity": "sha512-FzQGIj4UEszRX7fcRSJK6L1LrDiVZvDFW320VVntVKh3BSU8Fb9kpaoxQx0cdFgf3MQXdeSbrCXJ/5Z/NndDkQ==", + "version": "13.5.6", + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-13.5.6.tgz", + "integrity": "sha512-o8pQsUHTo9aHqJ2YiZDym5gQAMRf7O2HndHo/JZeY7TDD+W4hk6Ma8Vw54RHiBeb7OWWO5dPirQB+Is/aVQ7Kg==", "dev": true, "dependencies": { - "@next/eslint-plugin-next": "13.5.4", + "@next/eslint-plugin-next": "13.5.6", "@rushstack/eslint-patch": "^1.3.3", "@typescript-eslint/parser": "^5.4.2 || ^6.0.0", "eslint-import-resolver-node": "^0.3.6", @@ -2334,14 +2509,6 @@ "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", "dev": true }, - "node_modules/fast-equals": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.0.1.tgz", - "integrity": "sha512-WF1Wi8PwwSY7/6Kx0vKXtw8RwuSGoM1bvDaJbu7MxDlR1vovZjIAKrnzyrThgAjm6JDTu0fVgWXDlMGspodfoQ==", - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/fast-glob": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", @@ -2451,25 +2618,6 @@ "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", "dev": true }, - "node_modules/follow-redirects": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", - "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, "node_modules/for-each": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", @@ -2479,19 +2627,6 @@ "is-callable": "^1.1.3" } }, - "node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/fraction.js": { "version": "4.3.6", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.6.tgz", @@ -2514,8 +2649,7 @@ "node_modules/function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" }, "node_modules/function.prototype.name": { "version": "1.1.6", @@ -2548,7 +2682,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", - "dev": true, "dependencies": { "function-bind": "^1.1.1", "has": "^1.0.3", @@ -2726,7 +2859,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, "dependencies": { "function-bind": "^1.1.1" }, @@ -2768,7 +2900,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -2780,7 +2911,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -2876,14 +3006,6 @@ "node": ">= 0.4" } }, - "node_modules/internmap": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", - "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", - "engines": { - "node": ">=12" - } - }, "node_modules/invariant": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", @@ -3290,6 +3412,11 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, + "node_modules/ismobilejs": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ismobilejs/-/ismobilejs-1.1.1.tgz", + "integrity": "sha512-VaFW53yt8QO61k2WJui0dHf4SlL8lxBofUuUmwBo0ljPk0Drz2TiuDW4jo3wDcv41qy/SxrJ+VAzJ/qYqsmzRw==" + }, "node_modules/iterator.prototype": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.1.tgz", @@ -3404,12 +3531,6 @@ "language-subtag-registry": "~0.3.2" } }, - "node_modules/leaflet": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz", - "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==", - "peer": true - }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -3438,10 +3559,10 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + "node_modules/lodash.isnil": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/lodash.isnil/-/lodash.isnil-4.0.0.tgz", + "integrity": "sha512-up2Mzq3545mwVnMhTDMdfoG1OurpA/s5t88JmQX809eH3C8491iu2sfKhTfhQtKY78oPNhiaHJUpT/dUDAAtng==" }, "node_modules/lodash.merge": { "version": "4.6.2", @@ -3449,6 +3570,11 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "node_modules/lodash.times": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/lodash.times/-/lodash.times-4.3.2.tgz", + "integrity": "sha512-FfaJzl0SA35CRPDh5SWe2BTght6y5KSK7yJv166qIp/8q7qOwBDCvuDZE2RUSMRpBkLF6rZKbLEUoTmaP3qg6A==" + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -3500,25 +3626,6 @@ "node": ">=8.6" } }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/mimic-fn": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", @@ -3582,11 +3689,11 @@ "dev": true }, "node_modules/next": { - "version": "13.5.4", - "resolved": "https://registry.npmjs.org/next/-/next-13.5.4.tgz", - "integrity": "sha512-+93un5S779gho8y9ASQhb/bTkQF17FNQOtXLKAj3lsNgltEcF0C5PMLLncDmH+8X1EnJH1kbqAERa29nRXqhjA==", + "version": "13.5.6", + "resolved": "https://registry.npmjs.org/next/-/next-13.5.6.tgz", + "integrity": "sha512-Y2wTcTbO4WwEsVb4A8VSnOsG1I9ok+h74q0ZdxkwM3EODqrs4pasq7O0iUxbcS9VtWMicG7f3+HAj0r1+NtKSw==", "dependencies": { - "@next/env": "13.5.4", + "@next/env": "13.5.6", "@swc/helpers": "0.5.2", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001406", @@ -3601,15 +3708,15 @@ "node": ">=16.14.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "13.5.4", - "@next/swc-darwin-x64": "13.5.4", - "@next/swc-linux-arm64-gnu": "13.5.4", - "@next/swc-linux-arm64-musl": "13.5.4", - "@next/swc-linux-x64-gnu": "13.5.4", - "@next/swc-linux-x64-musl": "13.5.4", - "@next/swc-win32-arm64-msvc": "13.5.4", - "@next/swc-win32-ia32-msvc": "13.5.4", - "@next/swc-win32-x64-msvc": "13.5.4" + "@next/swc-darwin-arm64": "13.5.6", + "@next/swc-darwin-x64": "13.5.6", + "@next/swc-linux-arm64-gnu": "13.5.6", + "@next/swc-linux-arm64-musl": "13.5.6", + "@next/swc-linux-x64-gnu": "13.5.6", + "@next/swc-linux-x64-musl": "13.5.6", + "@next/swc-win32-arm64-msvc": "13.5.6", + "@next/swc-win32-ia32-msvc": "13.5.6", + "@next/swc-win32-x64-msvc": "13.5.6" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", @@ -3680,7 +3787,6 @@ "version": "1.12.3", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", - "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -3928,6 +4034,11 @@ "node": ">=8" } }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==" + }, "node_modules/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -3954,6 +4065,47 @@ "node": ">=0.10.0" } }, + "node_modules/pixi.js": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/pixi.js/-/pixi.js-7.3.2.tgz", + "integrity": "sha512-GJickUrT3UcBInGT1CU6cv2oktCdocE5QM74CD3t+weiJPPWIzleNlp7zrBR5QIDdU6bEO8CUgUXH2Y9QvlCMw==", + "dependencies": { + "@pixi/accessibility": "7.3.2", + "@pixi/app": "7.3.2", + "@pixi/assets": "7.3.2", + "@pixi/compressed-textures": "7.3.2", + "@pixi/core": "7.3.2", + "@pixi/display": "7.3.2", + "@pixi/events": "7.3.2", + "@pixi/extensions": "7.3.2", + "@pixi/extract": "7.3.2", + "@pixi/filter-alpha": "7.3.2", + "@pixi/filter-blur": "7.3.2", + "@pixi/filter-color-matrix": "7.3.2", + "@pixi/filter-displacement": "7.3.2", + "@pixi/filter-fxaa": "7.3.2", + "@pixi/filter-noise": "7.3.2", + "@pixi/graphics": "7.3.2", + "@pixi/mesh": "7.3.2", + "@pixi/mesh-extras": "7.3.2", + "@pixi/mixin-cache-as-bitmap": "7.3.2", + "@pixi/mixin-get-child-by-name": "7.3.2", + "@pixi/mixin-get-global-position": "7.3.2", + "@pixi/particle-container": "7.3.2", + "@pixi/prepare": "7.3.2", + "@pixi/sprite": "7.3.2", + "@pixi/sprite-animated": "7.3.2", + "@pixi/sprite-tiling": "7.3.2", + "@pixi/spritesheet": "7.3.2", + "@pixi/text": "7.3.2", + "@pixi/text-bitmap": "7.3.2", + "@pixi/text-html": "7.3.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/pixijs" + } + }, "node_modules/postcss": { "version": "8.4.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", @@ -4152,11 +4304,6 @@ "react-is": "^16.13.1" } }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" - }, "node_modules/punycode": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", @@ -4166,6 +4313,20 @@ "node": ">=6" } }, + "node_modules/qs": { + "version": "6.11.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz", + "integrity": "sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==", + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -4222,24 +4383,6 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, - "node_modules/react-leaflet": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/react-leaflet/-/react-leaflet-4.2.1.tgz", - "integrity": "sha512-p9chkvhcKrWn/H/1FFeVSqLdReGwn2qmiobOQGO3BifX+/vV/39qhY8dGqbdcPh1e6jxh/QHriLXr7a4eLFK4Q==", - "dependencies": { - "@react-leaflet/core": "^2.1.0" - }, - "peerDependencies": { - "leaflet": "^1.9.0", - "react": "^18.0.0", - "react-dom": "^18.0.0" - } - }, - "node_modules/react-lifecycles-compat": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", - "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" - }, "node_modules/react-number-format": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/react-number-format/-/react-number-format-5.3.1.tgz", @@ -4297,32 +4440,6 @@ } } }, - "node_modules/react-resize-detector": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/react-resize-detector/-/react-resize-detector-8.1.0.tgz", - "integrity": "sha512-S7szxlaIuiy5UqLhLL1KY3aoyGHbZzsTpYal9eYMwCyKqoqoVLCmIgAgNyIM1FhnP2KyBygASJxdhejrzjMb+w==", - "dependencies": { - "lodash": "^4.17.21" - }, - "peerDependencies": { - "react": "^16.0.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/react-smooth": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-2.0.4.tgz", - "integrity": "sha512-OkFsrrMBTvQUwEJthE1KXSOj79z57yvEWeFefeXPib+RmQEI9B1Ub1PgzlzzUyBOvl/TjXt5nF2hmD4NsgAh8A==", - "dependencies": { - "fast-equals": "^5.0.0", - "react-transition-group": "2.9.0" - }, - "peerDependencies": { - "prop-types": "^15.6.0", - "react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" - } - }, "node_modules/react-style-singleton": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz", @@ -4361,21 +4478,6 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, - "node_modules/react-transition-group": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-2.9.0.tgz", - "integrity": "sha512-+HzNTCHpeQyl4MJ/bdE0u6XRMe9+XG/+aL4mCxVN4DnPBQ0/5bfHWPDuOZUzYdMj94daZaZdCCc1Dzt9R/xSSg==", - "dependencies": { - "dom-helpers": "^3.4.0", - "loose-envify": "^1.4.0", - "prop-types": "^15.6.2", - "react-lifecycles-compat": "^3.0.4" - }, - "peerDependencies": { - "react": ">=15.0.0", - "react-dom": ">=15.0.0" - } - }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -4385,38 +4487,6 @@ "pify": "^2.3.0" } }, - "node_modules/recharts": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.8.0.tgz", - "integrity": "sha512-nciXqQDh3aW8abhwUlA4EBOBusRHLNiKHfpRZiG/yjups1x+auHb2zWPuEcTn/IMiN47vVMMuF8Sr+vcQJtsmw==", - "dependencies": { - "classnames": "^2.2.5", - "eventemitter3": "^4.0.1", - "lodash": "^4.17.19", - "react-is": "^16.10.2", - "react-resize-detector": "^8.0.4", - "react-smooth": "^2.0.2", - "recharts-scale": "^0.4.4", - "reduce-css-calc": "^2.1.8", - "victory-vendor": "^36.6.8" - }, - "engines": { - "node": ">=12" - }, - "peerDependencies": { - "prop-types": "^15.6.0", - "react": "^16.0.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/recharts-scale": { - "version": "0.4.5", - "resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz", - "integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==", - "dependencies": { - "decimal.js-light": "^2.4.1" - } - }, "node_modules/recoil": { "version": "0.7.7", "resolved": "https://registry.npmjs.org/recoil/-/recoil-0.7.7.tgz", @@ -4436,20 +4506,6 @@ } } }, - "node_modules/reduce-css-calc": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/reduce-css-calc/-/reduce-css-calc-2.1.8.tgz", - "integrity": "sha512-8liAVezDmUcH+tdzoEGrhfbGcP7nOV4NkGE3a74+qqvE7nt9i4sKLGBuZNOnpI4WiGksiNPklZxva80061QiPg==", - "dependencies": { - "css-unit-converter": "^1.1.1", - "postcss-value-parser": "^3.3.0" - } - }, - "node_modules/reduce-css-calc/node_modules/postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==" - }, "node_modules/reflect.getprototypeof": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.4.tgz", @@ -4759,7 +4815,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", - "dev": true, "dependencies": { "call-bind": "^1.0.0", "get-intrinsic": "^1.0.2", @@ -5177,6 +5232,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/undici-types": { + "version": "5.25.3", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.25.3.tgz", + "integrity": "sha512-Ga1jfYwRn7+cP9v8auvEXN1rX3sWqlayd4HP7OKk4mZWylEmu3KzXDUGrQUN6Ol7qo1gPvB2e5gX6udnyEPgdA==", + "dev": true + }, "node_modules/untildify": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", @@ -5225,6 +5286,20 @@ "punycode": "^2.1.0" } }, + "node_modules/url": { + "version": "0.11.3", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.3.tgz", + "integrity": "sha512-6hxOLGfZASQK/cijlZnZJTq8OXAkt/3YGfQX45vvMYXpZoo8NdWZcY73K108Jf759lS1Bv/8wXnHDTSz17dSRw==", + "dependencies": { + "punycode": "^1.4.1", + "qs": "^6.11.2" + } + }, + "node_modules/url/node_modules/punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==" + }, "node_modules/use-callback-ref": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.0.tgz", @@ -5309,27 +5384,6 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "dev": true }, - "node_modules/victory-vendor": { - "version": "36.6.11", - "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.6.11.tgz", - "integrity": "sha512-nT8kCiJp8dQh8g991J/R5w5eE2KnO8EAIP0xocWlh9l2okngMWglOPoMZzJvek8Q1KUc4XE/mJxTZnvOB1sTYg==", - "dependencies": { - "@types/d3-array": "^3.0.3", - "@types/d3-ease": "^3.0.0", - "@types/d3-interpolate": "^3.0.1", - "@types/d3-scale": "^4.0.2", - "@types/d3-shape": "^3.1.0", - "@types/d3-time": "^3.0.0", - "@types/d3-timer": "^3.0.0", - "d3-array": "^3.1.6", - "d3-ease": "^3.0.1", - "d3-interpolate": "^3.0.1", - "d3-scale": "^4.0.2", - "d3-shape": "^3.1.0", - "d3-time": "^3.0.0", - "d3-timer": "^3.0.1" - } - }, "node_modules/watchpack": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", diff --git a/ui/package.json b/ui/package.json index 7d69569..e4ec8fe 100644 --- a/ui/package.json +++ b/ui/package.json @@ -9,31 +9,30 @@ "lint": "next lint" }, "dependencies": { - "@mantine/core": "^7.1.2", - "@mantine/form": "^7.1.2", - "@mantine/hooks": "^7.1.2", - "@mantine/modals": "^7.1.2", - "@mantine/notifications": "^7.1.2", - "axios": "^1.5.1", + "@mantine/core": "^7.1.5", + "@mantine/form": "^7.1.5", + "@mantine/hooks": "^7.1.5", + "@mantine/modals": "^7.1.5", + "@mantine/notifications": "^7.1.5", + "@pixi/react": "^7.1.1", "js-cookie": "^3.0.5", - "next": "^13.5.4", + "next": "^13.5.6", + "pixi.js": "^7.3.2", "react": "^18.2.0", "react-dom": "^18.2.0", "react-icons": "^4.11.0", - "react-leaflet": "^4.2.1", - "recharts": "^2.8.0", "recoil": "^0.7.7" }, "devDependencies": { - "@types/js-cookie": "^3.0.4", - "@types/node": "20.8.2", - "@types/react": "18.2.24", - "@types/react-dom": "18.2.8", - "@typescript-eslint/eslint-plugin": "^6.7.4", - "@typescript-eslint/parser": "^6.7.4", + "@types/js-cookie": "^3.0.5", + "@types/node": "20.8.7", + "@types/react": "18.2.31", + "@types/react-dom": "18.2.14", + "@typescript-eslint/eslint-plugin": "^6.8.0", + "@typescript-eslint/parser": "^6.8.0", "autoprefixer": "^10.4.16", - "eslint": "8.50.0", - "eslint-config-next": "13.5.4", + "eslint": "8.52.0", + "eslint-config-next": "13.5.6", "eslint-config-prettier": "^9.0.0", "eslint-plugin-prettier": "^5.0.0", "postcss": "^8.4.31", diff --git a/ui/src/api/auth.ts b/ui/src/api/auth.ts index 89a31ca..3978e7a 100644 --- a/ui/src/api/auth.ts +++ b/ui/src/api/auth.ts @@ -25,7 +25,7 @@ export async function logout() { } export async function refresh(refresh_token_rotation?: boolean): Promise { - const response = await get('auth/refresh', { params: { refresh_token_rotation } }); + const response = await get('auth/refresh', { refresh_token_rotation }); if (response?.status === 200) { return response.json(); } else { diff --git a/ui/src/api/index.ts b/ui/src/api/index.ts index d990220..4aa2eee 100644 --- a/ui/src/api/index.ts +++ b/ui/src/api/index.ts @@ -8,7 +8,7 @@ export async function get(endpoint: string, params: Record = {}): P // Remove undefined params Object.keys(params).forEach((key) => params[key] === undefined && delete params[key]); const urlParams = new URLSearchParams(params); - const url = urlParams ? `${baseURL}/${endpoint}?${urlParams}` : `${baseURL}/${endpoint}`; + const url = urlParams && urlParams.size > 0 ? `${baseURL}/${endpoint}?${urlParams}` : `${baseURL}/${endpoint}`; const response = await fetch(url, { method: 'GET', credentials: 'include' @@ -16,11 +16,15 @@ export async function get(endpoint: string, params: Record = {}): P return response; } -export async function post(endpoint: string, body = {}): Promise { +interface PostOptions { + headers?: Record; +} + +export async function post(endpoint: string, body = {}, options?: PostOptions): Promise { const url = `${baseURL}/${endpoint}`; const response = await fetch(url, { method: 'POST', - headers: { + headers: options?.headers || { 'Content-Type': 'application/json' }, credentials: 'include', diff --git a/ui/src/api/users.ts b/ui/src/api/users.ts new file mode 100644 index 0000000..3331602 --- /dev/null +++ b/ui/src/api/users.ts @@ -0,0 +1,24 @@ +import { get, post } from '.'; + +export async function getPicture(): Promise { + const response = await get('users/picture'); + if (response?.status === 200) { + return response.blob(); + } else { + return undefined; + } +} + +export async function setPicture(payload: File): Promise { + const data = new FormData(); + data.append('file', payload); + // TODO: Figure out why the form data object is empty + const response = await post('users/picture', data, { + headers: { 'Content-Type': 'multipart/form-data' } + }); + if (response?.status === 200) { + return true; + } else { + return false; + } +} diff --git a/ui/src/app/layout.tsx b/ui/src/app/layout.tsx index 84eacba..765fa60 100644 --- a/ui/src/app/layout.tsx +++ b/ui/src/app/layout.tsx @@ -22,15 +22,13 @@ export default function RootLayout({ children }: { children: React.ReactNode }) Siren - +
- - {children} - + {children} diff --git a/ui/src/app/page.tsx b/ui/src/app/page.tsx index 736bead..78da4b0 100644 --- a/ui/src/app/page.tsx +++ b/ui/src/app/page.tsx @@ -1,6 +1,11 @@ +import TileGrid from '@/components/TileGrid'; import React from 'react'; // Home page for siren export default function Page() { - return
; + return ( +
+ +
+ ); } diff --git a/ui/src/components/Header/header.css b/ui/src/components/Header/header.css index 446a264..4ba9397 100644 --- a/ui/src/components/Header/header.css +++ b/ui/src/components/Header/header.css @@ -3,6 +3,7 @@ justify-content: space-between; color: black; border-bottom: 1px solid #e6e6e6; + max-height: 70px; } .navbar .left { diff --git a/ui/src/components/Header/index.tsx b/ui/src/components/Header/index.tsx index 23ad73b..85b27e5 100644 --- a/ui/src/components/Header/index.tsx +++ b/ui/src/components/Header/index.tsx @@ -3,15 +3,16 @@ import Link from 'next/link'; import { usePathname } from 'next/navigation'; import './header.css'; -import { Avatar, Button, Card, Grid, Group, Menu, Text, UnstyledButton } from '@mantine/core'; +import { Avatar, Button, Card, FileButton, Grid, Group, Menu, Text, UnstyledButton } from '@mantine/core'; import Cookies from 'js-cookie'; import { useEffect, useState } from 'react'; -import { logout, me, refreshLoggedIn } from '@/api/auth'; +import { logout, refresh, refreshLoggedIn } from '@/api/auth'; import { useToggle } from '@mantine/hooks'; import { HeaderModal } from './HeaderModal'; import { HeaderItem, headerItems } from './headerItems'; import { userState } from '@/state/auth'; import { useRecoilState } from 'recoil'; +import { getPicture, setPicture } from '@/api/users'; export default function Header() { const pathName = usePathname(); @@ -19,13 +20,19 @@ export default function Header() { const [headers, setHeaders] = useState([]); const [user, setUser] = useRecoilState(userState); const [refreshId, setRefreshId] = useState(undefined); + const [profilePicture, setProfilePicture] = useState(null); useEffect(() => { - if (!user && Cookies.get('logged_in')) { - me().then((response) => { + if (!user || !Cookies.get('logged_in')) { + refresh().then((response) => { if (response) { setRefreshId(refreshLoggedIn()); setUser(response.user); + getPicture().then((response) => { + if (response) { + setProfilePicture(response as File); + } + }); } }); } @@ -62,27 +69,51 @@ export default function Header() { - +
{user.first_name} {user.last_name} - - + {user.role}
- + - - + + { + if (payload) { + setPicture(payload).then((response) => { + if (response) { + setProfilePicture(payload); + } + }); + } + }} + accept='image/png,image/jpeg,image/jpg' + multiple={false} + > + {(props) => ( + + )} + {user.first_name} {user.last_name} - + {user.role} diff --git a/ui/src/components/TileGrid/Viewport.tsx b/ui/src/components/TileGrid/Viewport.tsx new file mode 100644 index 0000000..e69de29 diff --git a/ui/src/components/TileGrid/index.tsx b/ui/src/components/TileGrid/index.tsx new file mode 100644 index 0000000..77fb11d --- /dev/null +++ b/ui/src/components/TileGrid/index.tsx @@ -0,0 +1,38 @@ +'use client'; + +import { Graphics, Stage } from '@pixi/react'; +import { Graphics as PixiGraphics } from '@pixi/graphics'; +import { useCallback } from 'react'; + +// export default function TileGrid({ width, height }: TileGridProps) { +export default function TIleGrid() { + // Offset height of navbar from window height + const height = window.innerHeight - 75; + // Offset width of layout padding from window width + const width = window.innerWidth; + + const draw = useCallback((g: PixiGraphics) => { + g.clear(); + // Draw dot in the corner of each tile + for (let x = 0; x < width; x += 32) { + for (let y = 0; y < height; y += 32) { + g.beginFill(0xffffff, 0.5); + g.drawCircle(x, y, 1); + g.endFill(); + } + } + }, []); + + return ( + + + + ); +} diff --git a/ui/src/components/TileGrid/tileGrid.css b/ui/src/components/TileGrid/tileGrid.css new file mode 100644 index 0000000..5f7c2a2 --- /dev/null +++ b/ui/src/components/TileGrid/tileGrid.css @@ -0,0 +1,9 @@ +.tile { + padding: 0; + margin: 0; + width: 100vw; + max-width: 100%; + height: 100vh; + max-height: 100%; + user-select: none; +} \ No newline at end of file diff --git a/ui/tsconfig.json b/ui/tsconfig.json index 5753bf1..b6a416d 100755 --- a/ui/tsconfig.json +++ b/ui/tsconfig.json @@ -1,11 +1,11 @@ { "compilerOptions": { - "target": "ESNext", + "target": "ES2022", "downlevelIteration": true, "lib": [ "dom", "dom.iterable", - "esnext" + "ES2022" ], "allowJs": true, "skipLibCheck": true, @@ -13,8 +13,8 @@ "forceConsistentCasingInFileNames": true, "noEmit": true, "esModuleInterop": true, - "module": "esnext", - "moduleResolution": "node", + "module": "ES2022", + "moduleResolution": "Node", "resolveJsonModule": true, "isolatedModules": true, "jsx": "preserve", @@ -30,7 +30,8 @@ "@api/*": ["src/api"], "@app/*": ["./src/app/*"], "@components/*": ["src/components/*"], - "@lib/*": ["src/components/*"] + "@js/*": ["src/js/*"], + "@state/*": ["src/state/*"] } }, "include": [ From 8c246c96e7ff0c40cd2991f69318b93dc348b8b4 Mon Sep 17 00:00:00 2001 From: Benjamin Sherriff Date: Mon, 23 Oct 2023 20:19:17 -0400 Subject: [PATCH 09/24] Tweaks, working on api error handling --- ui/src/api/index.ts | 18 ++++++++----- ui/src/api/users.ts | 4 +-- ui/src/app/profile/page.tsx | 30 ++++++++++++++++++++-- ui/src/app/spells/page.tsx | 4 +-- ui/src/components/Header/index.tsx | 28 ++++++++++++++------ ui/src/components/SpellModal.tsx | 41 ++++++++++++++++-------------- ui/tsconfig.json | 26 ++++++++++++++----- 7 files changed, 104 insertions(+), 47 deletions(-) diff --git a/ui/src/api/index.ts b/ui/src/api/index.ts index 4aa2eee..b5f4076 100644 --- a/ui/src/api/index.ts +++ b/ui/src/api/index.ts @@ -1,5 +1,3 @@ -// import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'; - const serviceHost = process.env.SERVICE_HOST || 'http://localhost'; const servicePort = process.env.SERVICE_PORT || 5000; const baseURL = `${serviceHost}:${servicePort}`; @@ -18,17 +16,23 @@ export async function get(endpoint: string, params: Record = {}): P interface PostOptions { headers?: Record; + type?: 'json' | 'form'; } -export async function post(endpoint: string, body = {}, options?: PostOptions): Promise { +export async function post(endpoint: string, body: any, options?: PostOptions): Promise { const url = `${baseURL}/${endpoint}`; + const headers = options?.headers || {}; + if (!options?.type || options.type === 'json') { + body = JSON.stringify(body); + headers['Content-Type'] = 'application/json'; + } else if (options.type === 'form') { + headers['Content-Type'] = 'multipart/form-data'; + } const response = await fetch(url, { method: 'POST', - headers: options?.headers || { - 'Content-Type': 'application/json' - }, + headers: headers, credentials: 'include', - body: JSON.stringify(body) + body }); return response; } diff --git a/ui/src/api/users.ts b/ui/src/api/users.ts index 3331602..c6ece1f 100644 --- a/ui/src/api/users.ts +++ b/ui/src/api/users.ts @@ -11,10 +11,10 @@ export async function getPicture(): Promise { export async function setPicture(payload: File): Promise { const data = new FormData(); - data.append('file', payload); + data.append('data', payload); // TODO: Figure out why the form data object is empty const response = await post('users/picture', data, { - headers: { 'Content-Type': 'multipart/form-data' } + type: 'form' }); if (response?.status === 200) { return true; diff --git a/ui/src/app/profile/page.tsx b/ui/src/app/profile/page.tsx index 96a50d0..1507597 100644 --- a/ui/src/app/profile/page.tsx +++ b/ui/src/app/profile/page.tsx @@ -5,6 +5,7 @@ import React, { useEffect } from 'react'; import { useRouter } from 'next/navigation'; import { useRecoilState } from 'recoil'; import { userState } from '@/state/auth'; +import { Card, Container, Grid, SimpleGrid } from '@mantine/core'; export default function Page() { const [user, setUser] = useRecoilState(userState); @@ -23,8 +24,33 @@ export default function Page() { }, [user]); if (user) { - return
Logged in as {user.email}
; + return ( + + + + +

+ {user.first_name} {user.last_name} +

+ {user.role} +
+
+ + + + test + + + + + test + + + +
+
+ ); } else { - return
Not logged in
; + return <>; } } diff --git a/ui/src/app/spells/page.tsx b/ui/src/app/spells/page.tsx index ae12099..1cf1dad 100644 --- a/ui/src/app/spells/page.tsx +++ b/ui/src/app/spells/page.tsx @@ -90,9 +90,9 @@ function SpellSection({ title, spells, onClick }: { title: string; spells: Spell

{title}

    - {spells.map((spell) => ( + {spells.map((spell, index) => (
  • onClick(spell)} diff --git a/ui/src/components/Header/index.tsx b/ui/src/components/Header/index.tsx index 85b27e5..ce51b26 100644 --- a/ui/src/components/Header/index.tsx +++ b/ui/src/components/Header/index.tsx @@ -106,6 +106,7 @@ export default function Header() { mx={'auto'} mt={-30} style={{ cursor: 'pointer' }} + bg={profilePicture ? 'transparent' : 'white'} src={profilePicture ? URL.createObjectURL(profilePicture) : undefined} /> )} @@ -131,13 +132,12 @@ export default function Header() { size='xs' variant='default' onClick={async () => { - const response = await logout(); - if (response?.status == 200) { - Cookies.remove('logged_in'); - if (refreshId) { - clearInterval(refreshId); - } - setUser(undefined); + await logout(); + Cookies.remove('logged_in'); + setUser(undefined); + setProfilePicture(null); + if (refreshId) { + clearInterval(refreshId); } }} > @@ -167,7 +167,19 @@ export default function Header() { )} - + { + setUser(u); + getPicture().then((response) => { + if (response) { + setProfilePicture(response as File); + } + }); + }} + setRefreshId={setRefreshId} + /> ); } diff --git a/ui/src/components/SpellModal.tsx b/ui/src/components/SpellModal.tsx index 2c28f2a..0e537f7 100644 --- a/ui/src/components/SpellModal.tsx +++ b/ui/src/components/SpellModal.tsx @@ -38,8 +38,8 @@ export default function SpellModal({ spell, isOpen, onClose }: SpellModalProps) Sources: - {spell.sources.map((s) => ( - + {spell.sources.map((s, index) => ( + {s.source} {s.page ? `.${s.page}` : ''} @@ -48,8 +48,12 @@ export default function SpellModal({ spell, isOpen, onClose }: SpellModalProps) Classes: - {spell.classes.map((c) => ( - + {spell.classes.map((c, index) => ( + {parseText(c, true)} ))} @@ -71,8 +75,8 @@ export default function SpellModal({ spell, isOpen, onClose }: SpellModalProps) Duration: - {spell.durations.map((d) => ( - + {spell.durations.map((d, index) => ( + {capitalize(d.type)} {d.value} {capitalize(d.unit)} ))} @@ -86,7 +90,7 @@ export default function SpellModal({ spell, isOpen, onClose }: SpellModalProps) ); } -function parseText(text: string, capitalizeFirst?: boolean) { +function parseText(text: string, capitalizeFirst?: boolean): (string | JSX.Element)[] { const regex = /{@(.*?) (.*?)}/g; const matches = text.matchAll(regex); const result = []; @@ -148,18 +152,17 @@ function handleLink(type: string, name: string) { } function SpellDescription({ spell }: { spell: Spell }) { - return ( <> {spell.description && ( <> - {spell.description.entries.map((e) => ( - <> + {spell.description.entries.map((e, index) => ( +
    {e.text &&

    {parseText(e.text)}

    } {e.list && (
      - {e.list.map((text) => ( -
    • {parseText(text)}
    • + {e.list.map((text, index) => ( +
    • {parseText(text)}
    • ))}
    )} @@ -167,23 +170,23 @@ function SpellDescription({ spell }: { spell: Spell }) { - {e.table.headers.map((label) => ( - + {e.table.headers.map((label, index) => ( + ))} - {e.table.rows.map((row) => ( - - {row.map((cell) => ( - + {e.table.rows.map((row, index) => ( + + {row.map((cell, index) => ( + ))} ))}
    {label}{label}
    {parseText(cell)}
    {parseText(cell)}
    )} - +
    ))} )} diff --git a/ui/tsconfig.json b/ui/tsconfig.json index b6a416d..597235c 100755 --- a/ui/tsconfig.json +++ b/ui/tsconfig.json @@ -13,7 +13,7 @@ "forceConsistentCasingInFileNames": true, "noEmit": true, "esModuleInterop": true, - "module": "ES2022", + "module": "esnext", "moduleResolution": "Node", "resolveJsonModule": true, "isolatedModules": true, @@ -26,12 +26,24 @@ ], "baseUrl": ".", "paths": { - "@/*": ["./src/*"], - "@api/*": ["src/api"], - "@app/*": ["./src/app/*"], - "@components/*": ["src/components/*"], - "@js/*": ["src/js/*"], - "@state/*": ["src/state/*"] + "@/*": [ + "./src/*" + ], + "@api/*": [ + "src/api" + ], + "@app/*": [ + "./src/app/*" + ], + "@components/*": [ + "src/components/*" + ], + "@js/*": [ + "src/js/*" + ], + "@state/*": [ + "src/state/*" + ] } }, "include": [ From daf76071ea7885d798a163bce0985989fc1ae555 Mon Sep 17 00:00:00 2001 From: Benjamin Sherriff Date: Mon, 23 Oct 2023 20:36:25 -0400 Subject: [PATCH 10/24] Fixed spell description key issue --- ui/src/components/SpellModal.tsx | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/ui/src/components/SpellModal.tsx b/ui/src/components/SpellModal.tsx index 0e537f7..385da29 100644 --- a/ui/src/components/SpellModal.tsx +++ b/ui/src/components/SpellModal.tsx @@ -95,13 +95,15 @@ function parseText(text: string, capitalizeFirst?: boolean): (string | JSX.Eleme const matches = text.matchAll(regex); const result = []; let lastIndex = 0; + for (const match of matches) { + const key = crypto.randomUUID(); const [full, type, name] = match; - result.push(text.slice(lastIndex, match.index)); + result.push({text.slice(lastIndex, match.index)}); if (match.index !== undefined) { if (type == 'dice') { result.push( - handleLink(type, name)} className='link'> + handleLink(type, name)} className='link' key={key}> {name} ); @@ -109,26 +111,31 @@ function parseText(text: string, capitalizeFirst?: boolean): (string | JSX.Eleme // scaledice format is {@scaledice 1d6|1-9|1d6|}. Parse this out into dice, levels, and dice again. const [dice, levels] = name.split('|'); result.push( - handleLink('dice', dice)} className='link'> + handleLink('dice', dice)} className='link' key={key}> {dice} ); } else if (type == 'bold') { - result.push({name}); + result.push( + + {name} + + ); } else if (type == 'subclass') { const [className, subclassName] = name.split('|'); result.push( - + {capitalize(className)} ({capitalize(subclassName)}) ); } else { - result.push({capitalizeFirst ? capitalize(name) : name}); + result.push({capitalizeFirst ? capitalize(name) : name}); } lastIndex = match.index + full.length; } } - result.push(text.slice(lastIndex)); + result.push({text.slice(lastIndex)}); + return result; } From f3eff8e3107ac7b6bad988d34f5af9f92e18ccf5 Mon Sep 17 00:00:00 2001 From: Benjamin Sherriff Date: Mon, 23 Oct 2023 20:52:22 -0400 Subject: [PATCH 11/24] Fixed capitalize classes --- ui/src/app/spells/page.tsx | 71 ++++++++++++++++++++++++++++++++ ui/src/components/SpellModal.tsx | 7 +++- 2 files changed, 76 insertions(+), 2 deletions(-) diff --git a/ui/src/app/spells/page.tsx b/ui/src/app/spells/page.tsx index 1cf1dad..2862034 100644 --- a/ui/src/app/spells/page.tsx +++ b/ui/src/app/spells/page.tsx @@ -73,6 +73,77 @@ export default function Page() { }} />
    + s.name.toLowerCase().includes(searchName.toLowerCase()))} + onClick={(spell) => { + setActiveSpell(spell); + setIsOpen(true); + }} + /> +
    + s.name.toLowerCase().includes(searchName.toLowerCase()))} + onClick={(spell) => { + setActiveSpell(spell); + setIsOpen(true); + }} + /> +
    + s.name.toLowerCase().includes(searchName.toLowerCase()))} + onClick={(spell) => { + setActiveSpell(spell); + setIsOpen(true); + }} + /> +
    + s.name.toLowerCase().includes(searchName.toLowerCase()))} + onClick={(spell) => { + setActiveSpell(spell); + setIsOpen(true); + }} + /> +
    + s.name.toLowerCase().includes(searchName.toLowerCase()))} + onClick={(spell) => { + setActiveSpell(spell); + setIsOpen(true); + }} + /> +
    + s.name.toLowerCase().includes(searchName.toLowerCase()))} + onClick={(spell) => { + setActiveSpell(spell); + setIsOpen(true); + }} + /> +
    + s.name.toLowerCase().includes(searchName.toLowerCase()))} + onClick={(spell) => { + setActiveSpell(spell); + setIsOpen(true); + }} + /> +
    + s.name.toLowerCase().includes(searchName.toLowerCase()))} + onClick={(spell) => { + setActiveSpell(spell); + setIsOpen(true); + }} + /> {activeSpell && setIsOpen(false)} />} ); diff --git a/ui/src/components/SpellModal.tsx b/ui/src/components/SpellModal.tsx index 385da29..aee0b7e 100644 --- a/ui/src/components/SpellModal.tsx +++ b/ui/src/components/SpellModal.tsx @@ -95,8 +95,10 @@ function parseText(text: string, capitalizeFirst?: boolean): (string | JSX.Eleme const matches = text.matchAll(regex); const result = []; let lastIndex = 0; + let noMatches = true; for (const match of matches) { + noMatches = false; const key = crypto.randomUUID(); const [full, type, name] = match; result.push({text.slice(lastIndex, match.index)}); @@ -108,7 +110,7 @@ function parseText(text: string, capitalizeFirst?: boolean): (string | JSX.Eleme
    ); } else if (type == 'scaledice') { - // scaledice format is {@scaledice 1d6|1-9|1d6|}. Parse this out into dice, levels, and dice again. + // scaledice format is {@scaledice 1d6|1-9}. Parse this out into dice, levels, and dice again. const [dice, levels] = name.split('|'); result.push( handleLink('dice', dice)} className='link' key={key}> @@ -134,7 +136,8 @@ function parseText(text: string, capitalizeFirst?: boolean): (string | JSX.Eleme lastIndex = match.index + full.length; } } - result.push({text.slice(lastIndex)}); + const lastString = text.slice(lastIndex); + result.push({noMatches ? capitalize(lastString) : lastString}); return result; } From ece4154b4e25a6d5e9d7f810a1b7b5853f2d27b5 Mon Sep 17 00:00:00 2001 From: Benjamin Sherriff Date: Tue, 24 Oct 2023 08:42:21 -0400 Subject: [PATCH 12/24] Fixed file upload --- service/migrations/000011_create_users/up.sql | 2 +- service/src/auth/model.rs | 12 +++++---- service/src/storage/schema.rs | 2 +- service/src/users/routes.rs | 4 +-- ui/src/api/auth.types.ts | 1 + ui/src/api/index.ts | 26 +++++++++++-------- ui/src/components/Header/index.tsx | 24 ++++++++++------- 7 files changed, 41 insertions(+), 30 deletions(-) diff --git a/service/migrations/000011_create_users/up.sql b/service/migrations/000011_create_users/up.sql index 4911cc2..97e07e6 100644 --- a/service/migrations/000011_create_users/up.sql +++ b/service/migrations/000011_create_users/up.sql @@ -6,6 +6,6 @@ CREATE TABLE IF NOT EXISTS users ( last_name TEXT NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT NOW(), updated_at TIMESTAMP NOT NULL DEFAULT NOW(), - profile TEXT, + profile_picture TEXT, verified BOOLEAN NOT NULL DEFAULT FALSE ); \ No newline at end of file diff --git a/service/src/auth/model.rs b/service/src/auth/model.rs index 6aac1de..af557ef 100644 --- a/service/src/auth/model.rs +++ b/service/src/auth/model.rs @@ -29,7 +29,7 @@ impl RegisterUser { last_name: self.last_name, updated_at: chrono::Utc::now().naive_utc(), created_at: chrono::Utc::now().naive_utc(), - profile: None, + profile_picture: None, verified: false, }) } @@ -51,7 +51,7 @@ pub struct QueryUser { pub last_name: String, pub updated_at: chrono::NaiveDateTime, pub created_at: chrono::NaiveDateTime, - pub profile: Option, + pub profile_picture: Option, pub verified: bool, } @@ -77,7 +77,7 @@ pub struct InsertUser { pub last_name: String, pub updated_at: chrono::NaiveDateTime, pub created_at: chrono::NaiveDateTime, - pub profile: Option, + pub profile_picture: Option, pub verified: bool, } @@ -90,11 +90,11 @@ impl InsertUser { Ok(user) } - pub fn update_profile(email: &str, profile: Option<&str>) -> Result { + pub fn update_profile(email: &str, profile_picture: Option<&str>) -> Result { let mut conn = connection()?; let user = diesel::update(users::table) .filter(users::email.eq(&email)) - .set(users::profile.eq(profile)) + .set(users::profile_picture.eq(profile_picture)) .get_result(&mut conn)?; Ok(user) } @@ -106,6 +106,7 @@ pub struct ResponseUser { pub role: String, pub first_name: String, pub last_name: String, + pub profile_picture: Option, } impl From for ResponseUser { @@ -115,6 +116,7 @@ impl From for ResponseUser { role: user.role, first_name: user.first_name, last_name: user.last_name, + profile_picture: user.profile_picture, } } } diff --git a/service/src/storage/schema.rs b/service/src/storage/schema.rs index 24fb11f..1a64dad 100644 --- a/service/src/storage/schema.rs +++ b/service/src/storage/schema.rs @@ -48,7 +48,7 @@ diesel::table! { last_name -> Text, updated_at -> Timestamp, created_at -> Timestamp, - profile -> Nullable, + profile_picture -> Nullable, verified -> Bool, } } \ No newline at end of file diff --git a/service/src/users/routes.rs b/service/src/users/routes.rs index d9c1990..0e4babd 100644 --- a/service/src/users/routes.rs +++ b/service/src/users/routes.rs @@ -81,7 +81,7 @@ async fn get_picture(auth: JwtAuth) -> HttpResponse { return ResponseError::error_response(&err); } }; - if let Some(path) = user.profile { + if let Some(path) = user.profile_picture { match get_file(&path).await { Ok(bytes) => return HttpResponse::Ok().body(bytes), Err(err) => { @@ -98,7 +98,7 @@ async fn get_picture(auth: JwtAuth) -> HttpResponse { async fn delete_picture(auth: JwtAuth) -> HttpResponse { match QueryUser::get_by_email(&auth.user.email) { Ok(user) => { - match user.profile { + match user.profile_picture { Some(path) => { match delete_file(&path).await { Ok(_) => { diff --git a/ui/src/api/auth.types.ts b/ui/src/api/auth.types.ts index 8228ac1..76ac70a 100644 --- a/ui/src/api/auth.types.ts +++ b/ui/src/api/auth.types.ts @@ -15,4 +15,5 @@ export interface User { role: string; first_name: string; last_name: string; + profile_picture?: string; } diff --git a/ui/src/api/index.ts b/ui/src/api/index.ts index b5f4076..16f2132 100644 --- a/ui/src/api/index.ts +++ b/ui/src/api/index.ts @@ -21,19 +21,23 @@ interface PostOptions { export async function post(endpoint: string, body: any, options?: PostOptions): Promise { const url = `${baseURL}/${endpoint}`; - const headers = options?.headers || {}; + let response; if (!options?.type || options.type === 'json') { - body = JSON.stringify(body); - headers['Content-Type'] = 'application/json'; - } else if (options.type === 'form') { - headers['Content-Type'] = 'multipart/form-data'; + response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + credentials: 'include', + body: JSON.stringify(body) + }); + } else { + response = await fetch(url, { + method: 'POST', + credentials: 'include', + body + }); } - const response = await fetch(url, { - method: 'POST', - headers: headers, - credentials: 'include', - body - }); return response; } diff --git a/ui/src/components/Header/index.tsx b/ui/src/components/Header/index.tsx index ce51b26..815a319 100644 --- a/ui/src/components/Header/index.tsx +++ b/ui/src/components/Header/index.tsx @@ -28,11 +28,13 @@ export default function Header() { if (response) { setRefreshId(refreshLoggedIn()); setUser(response.user); - getPicture().then((response) => { - if (response) { - setProfilePicture(response as File); - } - }); + if (response.user.profile_picture) { + getPicture().then((response) => { + if (response) { + setProfilePicture(response as File); + } + }); + } } }); } @@ -172,11 +174,13 @@ export default function Header() { toggle={toggle} setUser={(u) => { setUser(u); - getPicture().then((response) => { - if (response) { - setProfilePicture(response as File); - } - }); + if (u.profile_picture) { + getPicture().then((response) => { + if (response) { + setProfilePicture(response as File); + } + }); + } }} setRefreshId={setRefreshId} /> From bdd1fc7e37342ac4c83add0b49e520481a93e26b Mon Sep 17 00:00:00 2001 From: Benjamin Sherriff Date: Tue, 24 Oct 2023 09:12:53 -0400 Subject: [PATCH 13/24] Working on tilemap listeners --- ui/src/components/TileGrid/index.tsx | 29 ++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/ui/src/components/TileGrid/index.tsx b/ui/src/components/TileGrid/index.tsx index 77fb11d..c186348 100644 --- a/ui/src/components/TileGrid/index.tsx +++ b/ui/src/components/TileGrid/index.tsx @@ -2,10 +2,14 @@ import { Graphics, Stage } from '@pixi/react'; import { Graphics as PixiGraphics } from '@pixi/graphics'; -import { useCallback } from 'react'; +import { MouseEvent, WheelEvent, useCallback, useState } from 'react'; // export default function TileGrid({ width, height }: TileGridProps) { export default function TIleGrid() { + const [mouseDown, setMouseDown] = useState(false); + const [lastPosition, setLastPosition] = useState({ x: 0, y: 0 }); + const [position, setPosition] = useState({ x: 0, y: 0 }); + // Offset height of navbar from window height const height = window.innerHeight - 75; // Offset width of layout padding from window width @@ -23,6 +27,19 @@ export default function TIleGrid() { } }, []); + function pan(e: MouseEvent) { + if (mouseDown) { + const dx = position.x + e.clientX - lastPosition.x; + const dy = position.y + e.clientY - lastPosition.y; + setPosition({ x: dx, y: dy }); + setLastPosition({ x: e.clientX, y: e.clientY }); + } + } + + function zoom(e: WheelEvent) { + console.log('zoom', e); + } + return ( { + setMouseDown(true); + setLastPosition({ x: e.clientX, y: e.clientY }); + }} + onMouseUp={() => setMouseDown(false)} + onMouseMove={(e) => pan(e)} + onWheel={(e) => zoom(e)} + onClick={(e) => console.log(e)} > - + ); } From 75fd00fb7f52958a68e3f1182a2faa6483da43d5 Mon Sep 17 00:00:00 2001 From: Benjamin Sherriff Date: Tue, 24 Oct 2023 16:02:48 -0400 Subject: [PATCH 14/24] Added tile controls --- ui/src/app/page.tsx | 2 +- ui/src/components/Header/header.css | 1 + ui/src/components/TileGrid/TileControls.tsx | 168 ++++++++++++++++++++ ui/src/components/TileGrid/index.tsx | 109 +++++++++---- 4 files changed, 246 insertions(+), 34 deletions(-) create mode 100644 ui/src/components/TileGrid/TileControls.tsx diff --git a/ui/src/app/page.tsx b/ui/src/app/page.tsx index 78da4b0..d4fc22c 100644 --- a/ui/src/app/page.tsx +++ b/ui/src/app/page.tsx @@ -4,7 +4,7 @@ import React from 'react'; // Home page for siren export default function Page() { return ( -
    +
    ); diff --git a/ui/src/components/Header/header.css b/ui/src/components/Header/header.css index 4ba9397..dabaca1 100644 --- a/ui/src/components/Header/header.css +++ b/ui/src/components/Header/header.css @@ -4,6 +4,7 @@ color: black; border-bottom: 1px solid #e6e6e6; max-height: 70px; + user-select: none; } .navbar .left { diff --git a/ui/src/components/TileGrid/TileControls.tsx b/ui/src/components/TileGrid/TileControls.tsx new file mode 100644 index 0000000..516b572 --- /dev/null +++ b/ui/src/components/TileGrid/TileControls.tsx @@ -0,0 +1,168 @@ +import { ActionIcon, Box, ColorPicker, Menu } from '@mantine/core'; +import { FaSquare, FaCircle, FaHandPaper, FaRegCircle } from 'react-icons/fa'; +import { FaMagnifyingGlass, FaPencil } from 'react-icons/fa6'; + +export enum Tool { + HAND, + ZOOM, + EDIT, + TOKEN +} + +export enum EditTool { + SQUARE, + CIRCLE +} + +export const defaultColors = [ + '#000000', + '#1D2B53', + '#7E2553', + '#008751', + '#AB5236', + '#5F574F', + '#C2C3C7', + '#FFF1E8', + '#FF004D' +]; + +interface TileControlsProps { + tool: Tool; + setTool: (tool: Tool) => void; + editTool: EditTool; + setEditTool: (editTool: EditTool) => void; + colors: string[]; + setColors: (colors: string[]) => void; + selectedColor: number; + setSelectedColor: (selectedColor: number) => void; +} + +export default function TileControls({ + tool, + setTool, + editTool, + setEditTool, + colors, + setColors, + selectedColor, + setSelectedColor +}: TileControlsProps) { + window.addEventListener( + 'keydown', + (e) => { + if (e.key === ' ') { + setTool(Tool.HAND); + } else if (e.key === 'z') { + setTool(Tool.ZOOM); + } else if (e.key === 'e') { + setTool(Tool.EDIT); + } else if (e.key === 't') { + setTool(Tool.TOKEN); + } else if (e.key === '1') { + setSelectedColor(0); + } else if (e.key === '2') { + setSelectedColor(1); + } else if (e.key === '3') { + setSelectedColor(2); + } else if (e.key === '4') { + setSelectedColor(3); + } else if (e.key === '5') { + setSelectedColor(4); + } else if (e.key === '6') { + setSelectedColor(5); + } else if (e.key === '7') { + setSelectedColor(6); + } else if (e.key === '8') { + setSelectedColor(7); + } else if (e.key === '9') { + setSelectedColor(8); + } + }, + { passive: false } + ); + + function checkIfColorIsDark(color: string) { + // If the color is dark, return white, otherwise return black + const r = parseInt(color.slice(1, 3), 16); + const g = parseInt(color.slice(3, 5), 16); + const b = parseInt(color.slice(5, 7), 16); + const brightness = (r * 299 + g * 587 + b * 114) / 1000; + return brightness < 128 ? '#ffffff' : '#000000'; + } + + return ( + + {tool === Tool.EDIT && ( + + setEditTool(EditTool.SQUARE)} + > + + + setEditTool(EditTool.CIRCLE)} + > + + + + )} + + setTool(Tool.HAND)}> + + + setTool(Tool.ZOOM)}> + + + setTool(Tool.EDIT)}> + + + setTool(Tool.TOKEN)}> + + + + + {colors.map((color, index) => ( + + + setSelectedColor(index)} + > + + {index + 1} + + + + + { + const newColors = [...colors]; + newColors[index] = v; + setColors(newColors); + }} + /> + + + ))} + + + ); +} diff --git a/ui/src/components/TileGrid/index.tsx b/ui/src/components/TileGrid/index.tsx index c186348..5823da1 100644 --- a/ui/src/components/TileGrid/index.tsx +++ b/ui/src/components/TileGrid/index.tsx @@ -1,25 +1,45 @@ 'use client'; -import { Graphics, Stage } from '@pixi/react'; +import { Container, Graphics, Stage } from '@pixi/react'; import { Graphics as PixiGraphics } from '@pixi/graphics'; -import { MouseEvent, WheelEvent, useCallback, useState } from 'react'; - -// export default function TileGrid({ width, height }: TileGridProps) { -export default function TIleGrid() { - const [mouseDown, setMouseDown] = useState(false); - const [lastPosition, setLastPosition] = useState({ x: 0, y: 0 }); - const [position, setPosition] = useState({ x: 0, y: 0 }); +import { MouseEvent, WheelEvent, useCallback, useEffect, useState } from 'react'; +import TileControls, { EditTool, Tool, defaultColors } from './TileControls'; +export default function TileGrid() { // Offset height of navbar from window height - const height = window.innerHeight - 75; + const height = window ? window.innerHeight - 75 : 0; // Offset width of layout padding from window width - const width = window.innerWidth; + const width = window ? window.innerWidth : 0; + + const [scale, setScale] = useState(1); + const [zoom, setZoom] = useState(1); + const [size, setSize] = useState({ width: width * 2, height: height * 2 }); + const [boundaries, setBoundaries] = useState({ top: 0, bottom: 0, left: 0, right: 0 }); + const [mouseDown, setMouseDown] = useState(false); + const [lastPosition, setLastPosition] = useState({ x: -width / 2, y: -height / 2 }); + const [position, setPosition] = useState({ x: -width / 2, y: -height / 2 }); + const [tool, setTool] = useState(Tool.HAND); + const [editTool, setEditTool] = useState(EditTool.SQUARE); + const [colors, setColors] = useState(defaultColors); + const [selectedColor, setSelectedColor] = useState(0); + + useEffect(() => { + if (window) { + window.addEventListener( + 'wheel', + (event) => { + event.preventDefault(); + }, + { passive: false } + ); + } + }, []); const draw = useCallback((g: PixiGraphics) => { g.clear(); // Draw dot in the corner of each tile - for (let x = 0; x < width; x += 32) { - for (let y = 0; y < height; y += 32) { + for (let x = 0; x < size.width; x += 32 * scale) { + for (let y = 0; y < size.height; y += 32 * scale) { g.beginFill(0xffffff, 0.5); g.drawCircle(x, y, 1); g.endFill(); @@ -27,37 +47,60 @@ export default function TIleGrid() { } }, []); - function pan(e: MouseEvent) { + function clickEvent(e: MouseEvent) { + if (tool === Tool.HAND) { + setMouseDown(true); + setLastPosition({ x: e.clientX, y: e.clientY }); + } + } + + function moveEvent(e: MouseEvent) { if (mouseDown) { const dx = position.x + e.clientX - lastPosition.x; const dy = position.y + e.clientY - lastPosition.y; + // TODO: Boundaries setPosition({ x: dx, y: dy }); setLastPosition({ x: e.clientX, y: e.clientY }); } } - function zoom(e: WheelEvent) { - console.log('zoom', e); + function zoomEvent(e: WheelEvent) { + const delta = e.deltaY; + if (delta > 0) { + setZoom(zoom / 1.1); + } else { + setZoom(zoom * 1.1); + } + setScale(scale * zoom); } return ( - { - setMouseDown(true); - setLastPosition({ x: e.clientX, y: e.clientY }); - }} - onMouseUp={() => setMouseDown(false)} - onMouseMove={(e) => pan(e)} - onWheel={(e) => zoom(e)} - onClick={(e) => console.log(e)} - > - - + <> + clickEvent(e)} + onMouseUp={() => setMouseDown(false)} + onMouseMove={(e) => moveEvent(e)} + onWheel={(e) => zoomEvent(e)} + > + + + + + ); } From f972a77909af0ab5362df9655b009fd6ac788c55 Mon Sep 17 00:00:00 2001 From: Benjamin Sherriff Date: Tue, 24 Oct 2023 16:35:32 -0400 Subject: [PATCH 15/24] Added bounds to grid --- ui/src/components/TileGrid/index.tsx | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/ui/src/components/TileGrid/index.tsx b/ui/src/components/TileGrid/index.tsx index 5823da1..a1911d4 100644 --- a/ui/src/components/TileGrid/index.tsx +++ b/ui/src/components/TileGrid/index.tsx @@ -7,14 +7,13 @@ import TileControls, { EditTool, Tool, defaultColors } from './TileControls'; export default function TileGrid() { // Offset height of navbar from window height - const height = window ? window.innerHeight - 75 : 0; + const height = window ? window.innerHeight - 70 : 0; // Offset width of layout padding from window width const width = window ? window.innerWidth : 0; const [scale, setScale] = useState(1); const [zoom, setZoom] = useState(1); - const [size, setSize] = useState({ width: width * 2, height: height * 2 }); - const [boundaries, setBoundaries] = useState({ top: 0, bottom: 0, left: 0, right: 0 }); + const [gridSize, setGridSize] = useState({ width: width * 2, height: height * 2 }); const [mouseDown, setMouseDown] = useState(false); const [lastPosition, setLastPosition] = useState({ x: -width / 2, y: -height / 2 }); const [position, setPosition] = useState({ x: -width / 2, y: -height / 2 }); @@ -38,8 +37,8 @@ export default function TileGrid() { const draw = useCallback((g: PixiGraphics) => { g.clear(); // Draw dot in the corner of each tile - for (let x = 0; x < size.width; x += 32 * scale) { - for (let y = 0; y < size.height; y += 32 * scale) { + for (let x = 0; x < gridSize.width; x += 32 * scale) { + for (let y = 0; y < gridSize.height; y += 32 * scale) { g.beginFill(0xffffff, 0.5); g.drawCircle(x, y, 1); g.endFill(); @@ -56,9 +55,13 @@ export default function TileGrid() { function moveEvent(e: MouseEvent) { if (mouseDown) { - const dx = position.x + e.clientX - lastPosition.x; - const dy = position.y + e.clientY - lastPosition.y; - // TODO: Boundaries + let dx = position.x + e.clientX - lastPosition.x; + let dy = position.y + e.clientY - lastPosition.y; + // Prevent coordinates from going out of bounds + dx = Math.min(dx, 0); + dx = Math.max(dx, -gridSize.width * scale + width); + dy = Math.min(dy, 0); + dy = Math.max(dy, -gridSize.height * scale + height); setPosition({ x: dx, y: dy }); setLastPosition({ x: e.clientX, y: e.clientY }); } @@ -75,7 +78,7 @@ export default function TileGrid() { } return ( - <> + - + ); } From c5a6b762f0af79bc2d012dd1bb70812f79e490ee Mon Sep 17 00:00:00 2001 From: Benjamin Sherriff Date: Mon, 30 Oct 2023 10:45:53 -0400 Subject: [PATCH 16/24] Added drawing squares --- service/src/main.rs | 3 - service/src/storage/mod.rs | 4 +- ui/src/components/TileGrid/TileControls.tsx | 2 +- ui/src/components/TileGrid/index.tsx | 122 +++++++++++++++----- 4 files changed, 93 insertions(+), 38 deletions(-) diff --git a/service/src/main.rs b/service/src/main.rs index 02e886d..9799f30 100644 --- a/service/src/main.rs +++ b/service/src/main.rs @@ -112,9 +112,6 @@ async fn main() -> std::io::Result<()> { let host = env::var("SERVICE_HOST").unwrap_or("localhost".to_string()); let port = env::var("SERVICE_PORT").unwrap_or("5000".to_string()); - crate::storage::upload_file("test.txt", b"Test").await.unwrap(); - crate::storage::delete_file("test.txt").await.unwrap(); - let server = match HttpServer::new(move || { let cors = Cors::default() .allow_any_origin() diff --git a/service/src/storage/mod.rs b/service/src/storage/mod.rs index 83763bf..21e5b5d 100644 --- a/service/src/storage/mod.rs +++ b/service/src/storage/mod.rs @@ -4,7 +4,7 @@ use s3::{Region, creds::Credentials, Bucket, BucketConfiguration, request::Respo use siren::ServiceError; use crate::diesel_migrations::MigrationHarness; use lazy_static::lazy_static; -use log::{error, info, warn}; +use log::{error, info}; use r2d2; use std::env; @@ -35,7 +35,7 @@ lazy_static! { RedisClient::open(url).expect("Failed to create redis client") }; static ref BUCKET: Bucket = { - let url = env::var("MINIO_URL").unwrap_or("localhost".to_string()); + let url = env::var("MINIO_HOST").unwrap_or("localhost".to_string()); let port = env::var("MINIO_PORT").unwrap_or("9000".to_string()); let user = env::var("MINIO_ROOT_USER").expect("MINIO_ROOT_USER is not set"); let password = env::var("MINIO_ROOT_PASSWORD").expect("MINIO_ROOT_PASSWORD is not set"); diff --git a/ui/src/components/TileGrid/TileControls.tsx b/ui/src/components/TileGrid/TileControls.tsx index 516b572..49104bd 100644 --- a/ui/src/components/TileGrid/TileControls.tsx +++ b/ui/src/components/TileGrid/TileControls.tsx @@ -131,7 +131,7 @@ export default function TileControls({ {colors.map((color, index) => ( - + (EditTool.SQUARE); const [colors, setColors] = useState(defaultColors); const [selectedColor, setSelectedColor] = useState(0); + const [edits, setEdits] = useState([]); useEffect(() => { if (window) { @@ -31,54 +38,105 @@ export default function TileGrid() { }, { passive: false } ); + // Disable right click context menu + window.addEventListener( + 'contextmenu', + (event) => { + event.preventDefault(); + }, + { passive: false } + ); } }, []); - const draw = useCallback((g: PixiGraphics) => { - g.clear(); - // Draw dot in the corner of each tile - for (let x = 0; x < gridSize.width; x += 32 * scale) { - for (let y = 0; y < gridSize.height; y += 32 * scale) { - g.beginFill(0xffffff, 0.5); - g.drawCircle(x, y, 1); - g.endFill(); + const drawGrid = useCallback( + (g: PixiGraphics) => { + g.clear(); + // Draw dot in the corner of each tile + for (let x = 0; x < gridSize.width; x += 32 * zoom) { + for (let y = 0; y < gridSize.height; y += 32 * zoom) { + g.beginFill(0xffffff, 0.5); + g.drawCircle(x, y, 1); + g.endFill(); + } } - } - }, []); + }, + [gridSize, zoom] + ); + + const drawEdits = useCallback( + (g: PixiGraphics) => { + g.clear(); + edits.forEach((edit) => { + g.beginFill(parseInt(edit.color.replace('#', ''), 16)); + g.drawRect(edit.x * 32 * zoom, edit.y * 32 * zoom, 32 * zoom, 32 * zoom); + g.endFill(); + }); + }, + [edits] + ); function clickEvent(e: MouseEvent) { - if (tool === Tool.HAND) { - setMouseDown(true); - setLastPosition({ x: e.clientX, y: e.clientY }); + setMouseDown(true); + setLastPosition({ x: e.clientX, y: e.clientY }); + if (tool == Tool.ZOOM) { + handleZoom(e.button === 0 ? -100 : 100); } } function moveEvent(e: MouseEvent) { if (mouseDown) { - let dx = position.x + e.clientX - lastPosition.x; - let dy = position.y + e.clientY - lastPosition.y; - // Prevent coordinates from going out of bounds - dx = Math.min(dx, 0); - dx = Math.max(dx, -gridSize.width * scale + width); - dy = Math.min(dy, 0); - dy = Math.max(dy, -gridSize.height * scale + height); - setPosition({ x: dx, y: dy }); - setLastPosition({ x: e.clientX, y: e.clientY }); + if (tool == Tool.HAND) { + let dx = position.x + e.clientX - lastPosition.x; + let dy = position.y + e.clientY - lastPosition.y; + // Prevent coordinates from going out of bounds + dx = Math.min(dx, 0); + dx = Math.max(dx, -gridSize.width * zoom + width); + dy = Math.min(dy, 0); + dy = Math.max(dy, -gridSize.height * zoom + height); + setPosition({ x: dx, y: dy }); + setLastPosition({ x: e.clientX, y: e.clientY }); + } else if (tool === Tool.EDIT) { + if (editTool === EditTool.SQUARE) { + // Calculate tile coordinates + const x = Math.floor((e.clientX - position.x) / (32 * zoom)); + const y = Math.floor((e.clientY - position.y - 64) / (32 * zoom)); + // Check if tile is already edited, and remove it + const index = edits.findIndex((edit) => edit.x === x && edit.y === y); + if (index !== -1) { + setEdits([...edits.slice(0, index), ...edits.slice(index + 1)]); + } + // Add new edit if left mouse button is pressed + if (e.buttons === 1) { + setEdits([...edits, { x, y, color: colors[selectedColor] }]); + } + } else if (editTool === EditTool.CIRCLE) { + // handle circle + } + } else if (tool === Tool.TOKEN) { + // handle token + } } } function zoomEvent(e: WheelEvent) { - const delta = e.deltaY; + handleZoom(e.deltaY); + } + + function handleZoom(delta: number) { + let newZoom = zoom; if (delta > 0) { - setZoom(zoom / 1.1); + newZoom = zoom / 1.1; } else { - setZoom(zoom * 1.1); + newZoom = zoom * 1.1; } - setScale(scale * zoom); + newZoom = Math.min(newZoom, 3); + newZoom = Math.max(newZoom, 0.6); + setZoom(newZoom); } return ( - + moveEvent(e)} onWheel={(e) => zoomEvent(e)} > - - + + - + ); } From 046bf51697a52d91695671afd58b72e4bb3097f3 Mon Sep 17 00:00:00 2001 From: Benjamin Sherriff Date: Tue, 31 Oct 2023 08:48:21 -0400 Subject: [PATCH 17/24] Working on layout, tilegrid --- .../bot/page.tsx => api/characters.types.ts} | 0 ui/src/app/campaigns/page.tsx | 5 ++ ui/src/app/characters/[id]/page.tsx | 5 ++ ui/src/app/characters/create/page.tsx | 5 ++ ui/src/app/characters/page.tsx | 5 ++ ui/src/app/page.tsx | 10 +++ ui/src/components/Header/headerItems.ts | 63 +++++++++------ ui/src/components/Header/index.tsx | 58 +++++++++----- ui/src/components/TileGrid/index.tsx | 80 +++++++++++-------- 9 files changed, 155 insertions(+), 76 deletions(-) rename ui/src/{app/bot/page.tsx => api/characters.types.ts} (100%) create mode 100644 ui/src/app/campaigns/page.tsx create mode 100644 ui/src/app/characters/[id]/page.tsx create mode 100644 ui/src/app/characters/create/page.tsx create mode 100644 ui/src/app/characters/page.tsx diff --git a/ui/src/app/bot/page.tsx b/ui/src/api/characters.types.ts similarity index 100% rename from ui/src/app/bot/page.tsx rename to ui/src/api/characters.types.ts diff --git a/ui/src/app/campaigns/page.tsx b/ui/src/app/campaigns/page.tsx new file mode 100644 index 0000000..e892a07 --- /dev/null +++ b/ui/src/app/campaigns/page.tsx @@ -0,0 +1,5 @@ +import React from 'react'; + +export default function Page() { + return <>; +} diff --git a/ui/src/app/characters/[id]/page.tsx b/ui/src/app/characters/[id]/page.tsx new file mode 100644 index 0000000..3cd2f3f --- /dev/null +++ b/ui/src/app/characters/[id]/page.tsx @@ -0,0 +1,5 @@ +import React from 'react'; + +export default function Page({ params }: { params: { id: string } }) { + return <>{params.id}; +} diff --git a/ui/src/app/characters/create/page.tsx b/ui/src/app/characters/create/page.tsx new file mode 100644 index 0000000..c8442ed --- /dev/null +++ b/ui/src/app/characters/create/page.tsx @@ -0,0 +1,5 @@ +import React from 'react'; + +export default function Page() { + return

    Create new Character

    ; +} diff --git a/ui/src/app/characters/page.tsx b/ui/src/app/characters/page.tsx new file mode 100644 index 0000000..e892a07 --- /dev/null +++ b/ui/src/app/characters/page.tsx @@ -0,0 +1,5 @@ +import React from 'react'; + +export default function Page() { + return <>; +} diff --git a/ui/src/app/page.tsx b/ui/src/app/page.tsx index d4fc22c..7b29c53 100644 --- a/ui/src/app/page.tsx +++ b/ui/src/app/page.tsx @@ -4,6 +4,16 @@ import React from 'react'; // Home page for siren export default function Page() { return ( + //
    + //

    Siren is a Dungeon Master's best friend.

    + //

    Features:

    + //
      + //
    • Manage your campaign and players
    • + //
    • Create battlemaps on the fly and track initiative
    • + //
    • Connect the Discord Bot to play online with friends
    • + //
    • Reference Races, Classes, Items, Spells, and more
    • + //
    + //
    diff --git a/ui/src/components/Header/headerItems.ts b/ui/src/components/Header/headerItems.ts index 1925049..d0f5f22 100644 --- a/ui/src/components/Header/headerItems.ts +++ b/ui/src/components/Header/headerItems.ts @@ -1,36 +1,49 @@ export interface HeaderItem { - name: string; - link: string; - role?: string; + label: string; + link?: string; + links?: HeaderItem[]; } export const headerItems: HeaderItem[] = [ { - name: 'Races', - link: '/races' + label: 'Campaigns', + link: '/campaigns' }, { - name: 'Classes', - link: '/classes' + label: 'Characters', + link: '/characters' }, { - name: 'Feats', - link: '/feats' - }, - { - name: 'Options & Features', - link: '/options' - }, - { - name: 'Backgrounds', - link: '/backgrounds' - }, - { - name: 'Items', - link: '/items' - }, - { - name: 'Spells', - link: '/spells' + label: 'Resources', + links: [ + { + label: 'Races', + link: '/races' + }, + { + label: 'Classes', + link: '/classes' + }, + { + label: 'Feats', + link: '/feats' + }, + { + label: 'Options & Features', + link: '/options' + }, + { + label: 'Backgrounds', + link: '/backgrounds' + }, + { + label: 'Items', + link: '/items' + }, + { + label: 'Spells', + link: '/spells' + } + ] } ]; diff --git a/ui/src/components/Header/index.tsx b/ui/src/components/Header/index.tsx index 815a319..5ef5d8a 100644 --- a/ui/src/components/Header/index.tsx +++ b/ui/src/components/Header/index.tsx @@ -1,9 +1,9 @@ 'use client'; import Link from 'next/link'; -import { usePathname } from 'next/navigation'; +import { usePathname, useRouter } from 'next/navigation'; import './header.css'; -import { Avatar, Button, Card, FileButton, Grid, Group, Menu, Text, UnstyledButton } from '@mantine/core'; +import { Avatar, Button, Card, Center, FileButton, Grid, Group, Menu, Text, UnstyledButton } from '@mantine/core'; import Cookies from 'js-cookie'; import { useEffect, useState } from 'react'; import { logout, refresh, refreshLoggedIn } from '@/api/auth'; @@ -13,14 +13,16 @@ import { HeaderItem, headerItems } from './headerItems'; import { userState } from '@/state/auth'; import { useRecoilState } from 'recoil'; import { getPicture, setPicture } from '@/api/users'; +import { BsChevronDown } from 'react-icons/bs'; export default function Header() { const pathName = usePathname(); const [modalType, toggle] = useToggle([undefined, 'login', 'register', 'reset']); - const [headers, setHeaders] = useState([]); + const [headers] = useState(headerItems); const [user, setUser] = useRecoilState(userState); const [refreshId, setRefreshId] = useState(undefined); const [profilePicture, setProfilePicture] = useState(null); + const router = useRouter(); useEffect(() => { if (!user || !Cookies.get('logged_in')) { @@ -40,16 +42,6 @@ export default function Header() { } }, [user]); - useEffect(() => { - const h: HeaderItem[] = []; - headerItems.forEach((item) => { - if (item.role == undefined || user?.role == item.role) { - h.push(item); - } - setHeaders(h); - }); - }, [user]); - return ( <>
    diff --git a/ui/src/components/TileGrid/index.tsx b/ui/src/components/TileGrid/index.tsx index 6abbb56..06e3f2c 100644 --- a/ui/src/components/TileGrid/index.tsx +++ b/ui/src/components/TileGrid/index.tsx @@ -1,10 +1,11 @@ 'use client'; -import { Graphics, Stage } from '@pixi/react'; +import { Graphics, Stage, Text } from '@pixi/react'; import { Graphics as PixiGraphics } from '@pixi/graphics'; import { MouseEvent, WheelEvent, useCallback, useEffect, useState } from 'react'; import TileControls, { EditTool, Tool, defaultColors } from './TileControls'; import { Box } from '@mantine/core'; +import { TextStyle } from 'pixi.js'; interface SquareEdit { x: number; @@ -31,21 +32,21 @@ export default function TileGrid() { useEffect(() => { if (window) { - window.addEventListener( - 'wheel', - (event) => { - event.preventDefault(); - }, - { passive: false } - ); - // Disable right click context menu - window.addEventListener( - 'contextmenu', - (event) => { - event.preventDefault(); - }, - { passive: false } - ); + // window.addEventListener( + // 'wheel', + // (event) => { + // event.preventDefault(); + // }, + // { passive: false } + // ); + // // Disable right click context menu + // window.addEventListener( + // 'contextmenu', + // (event) => { + // event.preventDefault(); + // }, + // { passive: false } + // ); } }, []); @@ -73,20 +74,39 @@ export default function TileGrid() { g.endFill(); }); }, - [edits] + [edits, zoom] ); function clickEvent(e: MouseEvent) { setMouseDown(true); setLastPosition({ x: e.clientX, y: e.clientY }); if (tool == Tool.ZOOM) { - handleZoom(e.button === 0 ? -100 : 100); + handleZoom(e.button === 0 ? -100 : 100, e.clientX, e.clientY); + } else if (tool == Tool.EDIT) { + if (editTool === EditTool.SQUARE) { + drawSquare(e.button, e.clientX, e.clientY); + } else if (editTool === EditTool.CIRCLE) { + // handle circle + } + } + } + + function drawSquare(button: number, clientX: number, clientY: number) { + // Calculate tile coordinates + const x = Math.floor((clientX - position.x) / (32 * zoom)); + const y = Math.floor((clientY - position.y) / (32 * zoom)); + if (button === 1) { + // Add new edit if left mouse button is pressed + setEdits([...edits, { x, y, color: colors[selectedColor] }]); + } else if (button == 2) { + // Remove edit if right mouse button is pressed + setEdits(edits.filter((edit) => edit.x !== x || edit.y !== y)); } } function moveEvent(e: MouseEvent) { if (mouseDown) { - if (tool == Tool.HAND) { + if (tool == Tool.HAND || e.buttons == 4) { let dx = position.x + e.clientX - lastPosition.x; let dy = position.y + e.clientY - lastPosition.y; // Prevent coordinates from going out of bounds @@ -98,18 +118,7 @@ export default function TileGrid() { setLastPosition({ x: e.clientX, y: e.clientY }); } else if (tool === Tool.EDIT) { if (editTool === EditTool.SQUARE) { - // Calculate tile coordinates - const x = Math.floor((e.clientX - position.x) / (32 * zoom)); - const y = Math.floor((e.clientY - position.y - 64) / (32 * zoom)); - // Check if tile is already edited, and remove it - const index = edits.findIndex((edit) => edit.x === x && edit.y === y); - if (index !== -1) { - setEdits([...edits.slice(0, index), ...edits.slice(index + 1)]); - } - // Add new edit if left mouse button is pressed - if (e.buttons === 1) { - setEdits([...edits, { x, y, color: colors[selectedColor] }]); - } + drawSquare(e.buttons, e.clientX, e.clientY); } else if (editTool === EditTool.CIRCLE) { // handle circle } @@ -120,10 +129,10 @@ export default function TileGrid() { } function zoomEvent(e: WheelEvent) { - handleZoom(e.deltaY); + handleZoom(e.deltaY, e.clientX, e.clientY); } - function handleZoom(delta: number) { + function handleZoom(delta: number, clientX: number, clientY: number) { let newZoom = zoom; if (delta > 0) { newZoom = zoom / 1.1; @@ -132,7 +141,12 @@ export default function TileGrid() { } newZoom = Math.min(newZoom, 3); newZoom = Math.max(newZoom, 0.6); + console.log(newZoom); setZoom(newZoom); + // Adjust position to zoom in on mouse position + const dx = (position.x - clientX) * (newZoom / zoom) + clientX; + const dy = (position.y - clientY) * (newZoom / zoom) + clientY; + setPosition({ x: dx, y: dy }); } return ( From dc2ff172b0ee9f747af70189d322e01feb9d58a3 Mon Sep 17 00:00:00 2001 From: Benjamin Sherriff Date: Sat, 2 Dec 2023 13:52:01 -0500 Subject: [PATCH 18/24] Refactored and fixed api endpoints --- service/docker-compose.yml | 2 +- service/src/bot/api/routes.rs | 39 ++--- service/src/bot/commands/oai.rs | 6 +- service/src/dnd/spells/routes.rs | 6 +- service/src/lib.rs | 2 +- service/src/storage/messages/routes.rs | 4 +- ui/src/api/guilds.ts | 11 +- ui/src/api/index.ts | 5 + ui/src/app/admin/page.tsx | 201 +++++++++++++++++++++++++ ui/src/app/management/page.tsx | 140 ----------------- ui/src/components/Header/index.tsx | 4 +- 11 files changed, 238 insertions(+), 182 deletions(-) create mode 100644 ui/src/app/admin/page.tsx delete mode 100644 ui/src/app/management/page.tsx diff --git a/service/docker-compose.yml b/service/docker-compose.yml index 1997cd7..6550479 100644 --- a/service/docker-compose.yml +++ b/service/docker-compose.yml @@ -1,6 +1,6 @@ version: '3.8' -x-env_file_personifi: &env +x-env_file: &env - .env name: siren diff --git a/service/src/bot/api/routes.rs b/service/src/bot/api/routes.rs index 25c8be5..a75ff26 100644 --- a/service/src/bot/api/routes.rs +++ b/service/src/bot/api/routes.rs @@ -4,7 +4,7 @@ use actix_web::{get, post, web, HttpResponse, ResponseError}; use log::warn; use serde::{Serialize, Deserialize}; use serenity::model::prelude::{GuildChannel, ChannelType}; -use siren::ServiceError; +use siren::{ServiceError, Response}; use crate::{AppState, bot::commands::audio::{play::play_track, join}, storage::guilds::QueryGuild, auth::{JwtAuth, verify_role}}; @@ -22,7 +22,10 @@ async fn get_guilds(data: web::Data>, auth: JwtAuth) -> HttpRespon message: err.to_string() }) }; - HttpResponse::Ok().json(guilds) + HttpResponse::Ok().json(Response { + data: guilds, + metadata: None + }) } #[get("/{id}/text")] @@ -39,7 +42,10 @@ async fn get_text_channels(id: web::Path, data: web::Data> message: err.to_string() }) }; - HttpResponse::Ok().json(channels) + HttpResponse::Ok().json(Response { + data: channels, + metadata: None + }) } #[get("/{id}/voice")] @@ -56,7 +62,10 @@ async fn get_voice_channels(id: web::Path, data: web::Data message: err.to_string() }) }; - HttpResponse::Ok().json(channels) + HttpResponse::Ok().json(Response { + data: channels, + metadata: None + }) } #[derive(Serialize, Deserialize)] @@ -74,7 +83,6 @@ async fn send_message(path: web::Path<(String, String)>, text: web::Json() { Ok(id) => id, Err(err) => { - warn!("Could not parse guild id: {:?}", err); return ResponseError::error_response(&ServiceError { status: 422, message: err.to_string() @@ -84,7 +92,6 @@ async fn send_message(path: web::Path<(String, String)>, text: web::Json() { Ok(id) => id, Err(err) => { - warn!("Could not parse channel id: {:?}", err); return ResponseError::error_response(&ServiceError { status: 422, message: err.to_string() @@ -95,7 +102,6 @@ async fn send_message(path: web::Path<(String, String)>, text: web::Json channels, Err(err) => { - warn!("Could not get channels: {:?}", err); return ResponseError::error_response(&ServiceError { status: 422, message: err.to_string() @@ -106,7 +112,6 @@ async fn send_message(path: web::Path<(String, String)>, text: web::Json channel, None => { - warn!("Could not find channel with id {}", channel_id); return ResponseError::error_response(&ServiceError { status: 422, message: format!("Could not find channel with id {}", channel_id) @@ -115,7 +120,6 @@ async fn send_message(path: web::Path<(String, String)>, text: web::Json, play_request: web::Json() { Ok(id) => id, Err(err) => { - warn!("Could not parse guild id: {:?}", err); return ResponseError::error_response(&ServiceError { status: 422, message: err.to_string() }) } }; let channel_id = match channel_id.parse::() { Ok(id) => id, Err(err) => { - warn!("Could not parse channel id: {:?}", err); return ResponseError::error_response(&ServiceError { status: 422, message: err.to_string() }) } }; @@ -155,14 +157,12 @@ async fn play(path: web::Path<(String, String)>, play_request: web::Json guild, Err(err) => { - warn!("Could not get guild: {:?}", err); return ResponseError::error_response(&ServiceError { status: 422, message: err.to_string() }) } }; let channel = match http.get_channel(channel_id).await { Ok(channel) => channel, Err(err) => { - warn!("Could not get channel: {:?}", err); return ResponseError::error_response(&ServiceError { status: 422, message: err.to_string() }) } }; @@ -174,13 +174,11 @@ async fn play(path: web::Path<(String, String)>, play_request: web::Json HttpResponse::Ok().finish(), Err(err) => { - warn!("Could not play track: {:?}", err); return ResponseError::error_response(&err) } } }, Err(err) => { - warn!("Could not join channel: {:?}", err); return ResponseError::error_response(&ServiceError { status: 500, message: err.to_string() }) } } @@ -196,7 +194,6 @@ async fn stop(path: web::Path, data: web::Data>, auth: Jwt let guild_id = match guild_id.parse::() { Ok(id) => id, Err(err) => { - warn!("Could not parse guild id: {:?}", err); return ResponseError::error_response(&ServiceError { status: 422, message: err.to_string() @@ -222,7 +219,6 @@ async fn resume(path: web::Path, data: web::Data>, auth: J let guild_id = match guild_id.parse::() { Ok(id) => id, Err(err) => { - warn!("Could not parse guild id: {:?}", err); return ResponseError::error_response(&ServiceError { status: 422, message: err.to_string() @@ -233,7 +229,6 @@ async fn resume(path: web::Path, data: web::Data>, auth: J if let Some(handler_lock) = data.songbird.get(guild_id) { let handler = handler_lock.lock().await; if let Err(err) = handler.queue().resume() { - warn!("Could not resume track: {:?}", err); return ResponseError::error_response(&ServiceError { status: 422, message: err.to_string() @@ -254,7 +249,6 @@ async fn pause(path: web::Path, data: web::Data>, auth: Jw let guild_id = match guild_id.parse::() { Ok(id) => id, Err(err) => { - warn!("Could not parse guild id: {:?}", err); return ResponseError::error_response(&ServiceError { status: 422, message: err.to_string() @@ -265,7 +259,6 @@ async fn pause(path: web::Path, data: web::Data>, auth: Jw if let Some(handler_lock) = data.songbird.get(guild_id) { let handler = handler_lock.lock().await; if let Err(err) = handler.queue().pause() { - warn!("Could not pause track: {:?}", err); return ResponseError::error_response(&ServiceError { status: 422, message: err.to_string() @@ -291,7 +284,6 @@ async fn get_volume(path: web::Path, auth: JwtAuth) -> HttpResponse { let guild_id = match guild_id.parse::() { Ok(id) => id, Err(err) => { - warn!("Could not parse guild id: {:?}", err); return ResponseError::error_response(&ServiceError { status: 422, message: err.to_string() @@ -302,7 +294,6 @@ async fn get_volume(path: web::Path, auth: JwtAuth) -> HttpResponse { let volume = match QueryGuild::get(guild_id as i64) { Ok(guild) => guild.volume, Err(err) => { - warn!("Could not get volume: {:?}", err); return ResponseError::error_response(&ServiceError { status: 422, message: err.to_string() @@ -323,7 +314,6 @@ async fn set_volume(path: web::Path, volume: web::Json::, dat let guild_id = match guild_id.parse::() { Ok(id) => id, Err(err) => { - warn!("Could not parse guild id: {:?}", err); return ResponseError::error_response(&ServiceError { status: 422, message: err.to_string() @@ -337,7 +327,6 @@ async fn set_volume(path: web::Path, volume: web::Json::, dat let guild = match http.get_guild(guild_id).await { Ok(guild) => guild, Err(err) => { - warn!("Could not get guild: {:?}", err); return ResponseError::error_response(&ServiceError { status: 422, message: err.to_string() }) } }; @@ -356,7 +345,6 @@ async fn skip(path: web::Path, data: web::Data>, auth: Jwt let guild_id = match guild_id.parse::() { Ok(id) => id, Err(err) => { - warn!("Could not parse guild id: {:?}", err); return ResponseError::error_response(&ServiceError { status: 422, message: err.to_string() @@ -367,7 +355,6 @@ async fn skip(path: web::Path, data: web::Data>, auth: Jwt if let Some(handler_lock) = data.songbird.get(guild_id) { let handler = handler_lock.lock().await; if let Err(err) = handler.queue().skip() { - warn!("Could not skip track: {:?}", err); return ResponseError::error_response(&ServiceError { status: 422, message: err.to_string() diff --git a/service/src/bot/commands/oai.rs b/service/src/bot/commands/oai.rs index 2727f58..7a5cc44 100644 --- a/service/src/bot/commands/oai.rs +++ b/service/src/bot/commands/oai.rs @@ -6,7 +6,7 @@ use serenity::model::Permissions; use serenity::model::channel::Message; use serenity::model::prelude::{ChannelType, PermissionOverwrite, PermissionOverwriteType}; use serenity::prelude::*; -use siren::{GetResponse, ServiceError}; +use siren::{Response, ServiceError}; pub struct OAI { pub client: reqwest::Client, @@ -160,7 +160,7 @@ impl OAI { Ok(response) } - async fn get_messages(&self, guild_id: u64, channel_id: u64, author_id: u64) -> Result>, ServiceError> { + async fn get_messages(&self, guild_id: u64, channel_id: u64, author_id: u64) -> Result>, ServiceError> { let uri = format!("{}/messages?guild_id={}&channel_id={}&author_id={}&limit={}", self.service_url, guild_id, channel_id, author_id, self.max_context_questions); let value = self.client .get(&uri) @@ -169,7 +169,7 @@ impl OAI { .json::() .await?; - let response = serde_json::from_value::>>(value)?; + let response = serde_json::from_value::>>(value)?; Ok(response) } diff --git a/service/src/dnd/spells/routes.rs b/service/src/dnd/spells/routes.rs index 62f8eec..095028f 100644 --- a/service/src/dnd/spells/routes.rs +++ b/service/src/dnd/spells/routes.rs @@ -1,7 +1,7 @@ use actix_web::{get, post, put, delete, web, HttpResponse, HttpRequest, ResponseError}; use log::error; use serde::{Serialize, Deserialize}; -use siren::{GetResponse, Metadata, ServiceError}; +use siren::{Response, Metadata, ServiceError}; use crate::{dnd::spells::{QuerySpell, QueryFilters}, auth::{JwtAuth, verify_role}}; @@ -90,7 +90,7 @@ async fn get_all(req: HttpRequest) -> HttpResponse { spell.id = Some(id); response.push(spell); } - HttpResponse::Ok().json(GetResponse { + HttpResponse::Ok().json(Response { data: response, metadata: Some(Metadata { total: total_count as i32, @@ -121,7 +121,7 @@ async fn get_by_id(id: web::Path) -> HttpResponse { let id = query_spell.id; let mut spell = Spell::from(query_spell); spell.id = Some(id); - HttpResponse::Ok().json(GetResponse { + HttpResponse::Ok().json(Response { data: spell, metadata: None }) diff --git a/service/src/lib.rs b/service/src/lib.rs index d04808b..da781df 100644 --- a/service/src/lib.rs +++ b/service/src/lib.rs @@ -19,7 +19,7 @@ pub struct Message { } #[derive(Serialize, Deserialize)] -pub struct GetResponse { +pub struct Response { pub data: T, #[serde(skip_serializing_if = "Option::is_none")] pub metadata: Option diff --git a/service/src/storage/messages/routes.rs b/service/src/storage/messages/routes.rs index 82f284c..da2f560 100644 --- a/service/src/storage/messages/routes.rs +++ b/service/src/storage/messages/routes.rs @@ -1,7 +1,7 @@ use actix_web::{get, post, web, HttpResponse, HttpRequest, ResponseError}; use log::error; use serde::{Serialize, Deserialize}; -use siren::{GetResponse, Metadata, ServiceError}; +use siren::{Response, Metadata, ServiceError}; use crate::{storage::messages::{QueryMessage, QueryFilters, InsertMessage}, auth::{JwtAuth, verify_role}}; @@ -50,7 +50,7 @@ async fn get_all(req: HttpRequest, auth: JwtAuth) -> HttpResponse { match QueryMessage::get_all(&filters, limit, page) { Ok(messages) => { - HttpResponse::Ok().json(GetResponse { + HttpResponse::Ok().json(Response { data: messages, metadata: Some(Metadata { total: total_count as i32, diff --git a/ui/src/api/guilds.ts b/ui/src/api/guilds.ts index 0a3fd1d..900a247 100644 --- a/ui/src/api/guilds.ts +++ b/ui/src/api/guilds.ts @@ -1,14 +1,16 @@ -import { get, post } from '.'; +import { APIResponse, get, post } from '.'; import { GuildChannel, GuildInfo } from './guilds.types'; export async function getGuilds(): Promise { const response = await get('guilds'); - return response?.json() || { data: [] }; + const guilds: APIResponse = await response?.json(); + return guilds.data || []; } export async function getTextChannels(guildId: number): Promise { const response = await get(`guilds/${guildId}/text`); - return response?.json() || { data: [] }; + const channels: APIResponse = await response?.json(); + return channels.data || []; } export async function sendMessage(guildId: number, channelId: number, message: string): Promise { @@ -17,7 +19,8 @@ export async function sendMessage(guildId: number, channelId: number, message: s export async function getVoiceChannels(guildId: number): Promise { const response = await get(`guilds/${guildId}/voice`); - return response?.json() || { data: [] }; + const channels: APIResponse = await response?.json(); + return channels.data || []; } export async function playTrack(guildId: number, channelId: number, track: string): Promise { diff --git a/ui/src/api/index.ts b/ui/src/api/index.ts index 16f2132..572ad22 100644 --- a/ui/src/api/index.ts +++ b/ui/src/api/index.ts @@ -41,6 +41,11 @@ export async function post(endpoint: string, body: any, options?: PostOptions): return response; } +export interface APIResponse { + data: T; + metadata: Metadata; +} + export interface Metadata { limit: number; page: number; diff --git a/ui/src/app/admin/page.tsx b/ui/src/app/admin/page.tsx new file mode 100644 index 0000000..ea66e85 --- /dev/null +++ b/ui/src/app/admin/page.tsx @@ -0,0 +1,201 @@ +'use client'; + +import { + getGuilds, + getTextChannels, + getVoiceChannels, + getVolume, + pauseTrack, + playTrack, + resumeTrack, + sendMessage, + setVolume, + skipTrack, + stopTrack +} from '@/api/guilds'; +import { GuildChannel, GuildInfo } from '@/api/guilds.types'; +import { Button, Card, Grid, Select, Slider, Tabs, TextInput, Textarea } from '@mantine/core'; +import { useForm } from '@mantine/form'; +import React, { useEffect, useState } from 'react'; + +export default function Page() { + const [guilds, setGuilds] = useState([]); + const [activeGuild, setActiveGuild] = useState(null); + const [voiceChannels, setVoiceChannels] = useState([]); + const [guildVolume, setGuildVolume] = useState(50.0); + + useEffect(() => { + getGuilds().then((g) => { + setGuilds(g); + if (g.length > 0) { + setActiveGuild(g[0]); + } + }); + }, []); + + useEffect(() => { + if (activeGuild) { + getVoiceChannels(activeGuild.id).then((c) => setVoiceChannels(c)); + getVolume(activeGuild.id).then((v) => setGuildVolume(v)); + } + }, [activeGuild]); + + return ( + + + {guilds && guilds.map((guild) => ( + setActiveGuild(guild)}> + {guild.name} + + ))} + + {guilds && guilds.map((guild) => ( + +

    {guild.name}

    + + + + + + + + +
    + ))} +
    + ); +} + +function TextChannelCard({ guild }: { guild: GuildInfo | null }) { + const [textChannels, setTextChannels] = useState([]); + const [activeChannel, setActiveChannel] = useState(null); + + const form = useForm({ + initialValues: { + message: '' + } + }); + + useEffect(() => { + if (guild) { + getTextChannels(guild.id).then((c) => setTextChannels(c)); + } + }, [guild]); + + return ( + + +

    Text Channels

    +