This commit is contained in:
Jakob Kordež
2023-09-07 18:46:32 +02:00
parent f24d39b4e1
commit 08a27fb4fc
46 changed files with 1065 additions and 72 deletions

View File

@ -9,6 +9,10 @@
"lint": "next lint"
},
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^6.4.2",
"@fortawesome/free-solid-svg-icons": "^6.4.2",
"@fortawesome/react-fontawesome": "^0.2.0",
"@headlessui/react": "^1.7.17",
"@types/node": "20.5.8",
"@types/react": "18.2.21",
"@types/react-dom": "18.2.7",
@ -21,12 +25,14 @@
"postcss": "8.4.29",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-no-ssr": "^1.1.0",
"react-secure-storage": "^1.3.0",
"tailwindcss": "3.3.3",
"typescript": "5.2.2",
"zustand": "^4.4.1"
},
"devDependencies": {
"@types/react-no-ssr": "^1.1.3",
"prettier": "^3.0.3",
"prettier-plugin-tailwindcss": "^0.5.4",
"sass": "^1.66.1"

View File

@ -1,5 +1,6 @@
import axios from 'axios';
import { User } from './interfaces/user.interface';
import { CreateUserDto } from './interfaces/create-user-dto.interface';
const baseURL = 'http://localhost:3001/';
@ -16,6 +17,7 @@ interface LoginResponse {
}
export const apiFunctions = {
// Auth
login: async (username: string, password: string) => {
return await api.post<LoginResponse>('/auth/login', { username, password });
},
@ -24,18 +26,78 @@ export const apiFunctions = {
headers: { Authorization: `Bearer ${refreshToken}` },
});
},
getMe: async (accessToken: string) => {
return await api.get<User>('/users/me', {
headers: {
Authorization: `Bearer ${accessToken}`,
},
});
},
logout: async (accessToken: string) => {
return await api.get('/auth/logout', {
headers: {
Authorization: `Bearer ${accessToken}`,
},
headers: { Authorization: `Bearer ${accessToken}` },
});
},
// Users
createUser: async (accessToken: string, user: CreateUserDto) => {
return await api.post<User>('/users', user, {
headers: { Authorization: `Bearer ${accessToken}` },
});
},
getAllUsers: async (accessToken: string) => {
return await api.get<User[]>('/users', {
headers: { Authorization: `Bearer ${accessToken}` },
});
},
getMe: async (accessToken: string) => {
return await api.get<User>('/users/me', {
headers: { Authorization: `Bearer ${accessToken}` },
});
},
findByUsername: async (accessToken: string, username: string) => {
return await api.get<User>(`/users/search/${username}`, {
headers: { Authorization: `Bearer ${accessToken}` },
});
},
getUser: async (accessToken: string, id: string) => {
return await api.get<User>(`/users/${id}`, {
headers: { Authorization: `Bearer ${accessToken}` },
});
},
updateUser: async (accessToken: string, id: string, user: User) => {
return await api.patch<User>(`/users/${id}`, user, {
headers: { Authorization: `Bearer ${accessToken}` },
});
},
deleteUser: async (accessToken: string, id: string) => {
return await api.delete<User>(`/users/${id}`, {
headers: { Authorization: `Bearer ${accessToken}` },
});
},
// Events
createEvent: async (accessToken: string, event: Event) => {
return await api.post<Event>('/events', event, {
headers: { Authorization: `Bearer ${accessToken}` },
});
},
getAllEvents: async (accessToken: string) => {
return await api.get<Event[]>('/events/all', {
headers: { Authorization: `Bearer ${accessToken}` },
});
},
getCurrentEvents: async (accessToken: string) => {
return await api.get<Event[]>('/events', {
headers: { Authorization: `Bearer ${accessToken}` },
});
},
getEvent: async (accessToken: string, id: string) => {
return await api.get<Event>(`/events/${id}`, {
headers: { Authorization: `Bearer ${accessToken}` },
});
},
updateEvent: async (accessToken: string, id: string, event: Event) => {
return await api.patch<Event>(`/events/${id}`, event, {
headers: { Authorization: `Bearer ${accessToken}` },
});
},
deleteEvent: async (accessToken: string, id: string) => {
return await api.delete<Event>(`/events/${id}`, {
headers: { Authorization: `Bearer ${accessToken}` },
});
},
};

View File

@ -0,0 +1,3 @@
export default function AdminEvents() {
return <h2>Events</h2>;
}

View File

@ -0,0 +1,59 @@
'use client';
import { Role } from '@/enums/role.enum';
import { useAuthState } from '@/state/auth-state';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { useEffect } from 'react';
const subPages = [
{
name: 'Events',
href: '/admin/events',
},
{
name: 'Users',
href: '/admin/users',
},
];
export default function AdminPageLayout({
children,
}: {
children: React.ReactNode;
}) {
const pathname = usePathname();
const getUser = useAuthState((s) => s.getUser);
useEffect(() => {
getUser().then((u) => {
if (!u || !u.roles.includes(Role.Admin)) window.location.replace('/');
});
}, []);
return (
<>
<div className="container flex flex-col gap-8 py-10">
<h1 className="text-4xl font-medium">Admin Page</h1>
<div className="flex flex-row flex-wrap gap-3">
{subPages.map(({ name, href }) => (
<Link
key={href}
href={href}
className={`rounded-md border border-gray-300 px-5 py-2 font-medium shadow dark:border-gray-600 ${
href === pathname
? 'bg-black/10 dark:bg-white/20'
: 'bg-black/5 hover:bg-black/10 dark:bg-white/10 dark:hover:bg-white/20'
}`}
>
{name}
</Link>
))}
</div>
{children}
</div>
</>
);
}

View File

@ -0,0 +1,3 @@
export default function AdminPage() {
return <></>;
}

View File

@ -0,0 +1,102 @@
'use client';
import { apiFunctions } from '@/api';
import { useAuthState } from '@/state/auth-state';
import { useState } from 'react';
export function CreateUserForm() {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [phone, setPhone] = useState('');
const getAccessToken = useAuthState((s) => s.getAccessToken);
function submit() {
getAccessToken().then((accessToken) => {
if (!accessToken) throw new Error('No access token');
apiFunctions
.createUser(accessToken, {
username,
password,
name,
email: email || undefined,
phone: phone || undefined,
})
.then((res) => {
console.log(res);
setUsername('');
setPassword('');
setName('');
setEmail('');
setPhone('');
})
.catch((err) => {
console.error(err);
});
});
}
return (
<div className="flex flex-col gap-4 rounded border border-gray-500 p-6">
<div className="flex flex-col gap-1">
<label htmlFor="username">Uporabniško ime (klicni znak)</label>
<input
type="text"
id="username"
className="text-input"
placeholder="S50HQ"
value={username}
onChange={(e) => setUsername(e.target.value)}
/>
</div>
<div className="flex flex-col gap-1">
<label htmlFor="password">Geslo</label>
<input
type="password"
id="password"
className="text-input"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</div>
<div className="flex flex-col gap-1">
<label htmlFor="name">Ime</label>
<input
type="text"
id="name"
className="text-input"
placeholder="Ime in priimek"
value={name}
onChange={(e) => setName(e.target.value)}
/>
</div>
<div className="flex flex-col gap-1">
<label htmlFor="email">Email</label>
<input
type="email"
id="email"
className="text-input"
placeholder="s50hq@hamradio.si"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</div>
<div className="flex flex-col gap-1">
<label htmlFor="phone">Telefon</label>
<input
type="tel"
id="phone"
className="text-input"
placeholder="+386 40 555 555"
value={phone}
onChange={(e) => setPhone(e.target.value)}
/>
</div>
<button className="button" onClick={submit}>
Ustvari
</button>
</div>
);
}

View File

@ -0,0 +1,88 @@
'use client';
import { apiFunctions } from '@/api';
import { User } from '@/interfaces/user.interface';
import { useAuthState } from '@/state/auth-state';
import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Dialog, Transition } from '@headlessui/react';
import { Fragment, useRef } from 'react';
export function DeleteUserDialog({
user,
onCancel,
}: {
user: User | undefined;
onCancel: () => void;
}) {
const getAccessToken = useAuthState((s) => s.getAccessToken);
const cancelButtonRef = useRef(null);
return (
<Dialog
as="div"
open={!!user}
className="relative z-10"
initialFocus={cancelButtonRef}
onClose={onCancel}
>
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity dark:bg-gray-900 dark:bg-opacity-75" />
<div className="fixed inset-0 z-10 w-screen overflow-y-auto">
<div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-white text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg">
<div className="bg-white px-4 pb-4 pt-5 sm:p-6 sm:pb-4">
<div className="sm:flex sm:items-start">
<div className="mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10">
<FontAwesomeIcon
icon={faExclamationTriangle}
className="h-6 w-6 text-red-600"
aria-hidden="true"
/>
</div>
<div className="mt-3 text-center sm:ml-4 sm:mt-0 sm:text-left">
<Dialog.Title
as="h3"
className="text-base font-semibold leading-6 text-gray-900"
>
Deactivate account
</Dialog.Title>
<div className="mt-2">
<p className="text-sm text-gray-500">
Are you sure you want to deactivate the account "
<strong className="text-black">{user?.username}</strong>
"? This action cannot be undone.
</p>
</div>
</div>
</div>
</div>
<div className="px-4 pb-5 sm:flex sm:flex-row-reverse sm:px-6">
<button
type="button"
className="inline-flex w-full justify-center rounded-md bg-red-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-500 sm:ml-3 sm:w-auto"
onClick={async () => {
const token = await getAccessToken();
if (!token) return;
apiFunctions.deleteUser(token, user!._id);
onCancel();
}}
>
Deactivate
</button>
<button
type="button"
className="mt-3 inline-flex w-full justify-center rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50 sm:mt-0 sm:w-auto"
onClick={onCancel}
ref={cancelButtonRef}
>
Cancel
</button>
</div>
</Dialog.Panel>
</div>
</div>
</Dialog>
);
}

View File

@ -0,0 +1,22 @@
'use client';
import { useAuthState } from '@/state/auth-state';
import NoSSR from 'react-no-ssr';
import { CreateUserForm } from './create-user-form';
import { UsersList } from './users-list';
export default function AdminUsers() {
return (
<>
<h2 className="text-2xl font-medium">Create user</h2>
<CreateUserForm />
<h2 className="text-2xl font-medium">Users</h2>
<NoSSR>
<UsersList />
</NoSSR>
</>
);
}

View File

@ -0,0 +1,85 @@
'use client';
import { apiFunctions } from '@/api';
import { Role } from '@/enums/role.enum';
import { User } from '@/interfaces/user.interface';
import { useAuthState } from '@/state/auth-state';
import { faCrown, faTrash } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { useEffect, useState } from 'react';
import { DeleteUserDialog } from './delete-user-dialog';
import { Dialog } from '@headlessui/react';
export function UsersList() {
const [getAccessToken, getMe] = useAuthState((s) => [
s.getAccessToken,
s.getUser,
]);
const [users, setUsers] = useState<User[]>();
const [me, setMe] = useState<User>();
const [deleteUser, setDeleteUser] = useState<User>();
useEffect(() => {
const f = async () => {
const accessToken = await getAccessToken();
if (!accessToken) return;
const users = (await apiFunctions.getAllUsers(accessToken)).data;
const me = await getMe();
if (!me) return;
setUsers(users);
setMe(me);
};
f();
}, []);
if (!users || !me) return <div>Loading...</div>;
return (
<div>
{users.map((user: User) => (
<div
key={user._id}
className="flex flex-row border-b border-b-gray-500 px-4 py-2 last:border-b-0"
>
<div className="my-auto flex-1">
<div className="text-xl">
{user.username.toUpperCase()} - {user.name}
</div>{' '}
<div className="text-xs opacity-80">{user._id}</div>
</div>
<div className="my-auto flex-1">
<div className="text-sm opacity-80">Email: {user.email}</div>
<div className="text-sm opacity-80">Phone: {user.phone}</div>
</div>
<button
className={`h-14 w-14 rounded-full hover:bg-yellow-500/30 disabled:hover:bg-transparent ${
user.roles.includes(Role.Admin)
? 'text-yellow-400'
: 'text-gray-400'
}`}
disabled={user._id === me?._id}
>
<FontAwesomeIcon icon={faCrown} className="h-5 w-5 leading-none" />
</button>
<button
className={`h-14 w-14 rounded-full text-red-500 hover:bg-red-500/30 ${
user._id === me?._id ? 'invisible' : ''
}`}
onClick={() => setDeleteUser(user)}
>
<FontAwesomeIcon icon={faTrash} className="h-5 w-5 leading-none" />
</button>
</div>
))}
<DeleteUserDialog
user={deleteUser}
onCancel={() => setDeleteUser(undefined)}
/>
</div>
);
}

View File

@ -7,5 +7,9 @@
}
.button {
@apply rounded bg-blue-500 px-4 py-2 font-bold text-white hover:bg-blue-700;
@apply bg-primary rounded px-4 py-2 font-bold text-white hover:opacity-90;
}
.header-button {
@apply h-full p-6 leading-none hover:bg-black/10 dark:hover:bg-white/20;
}

View File

@ -1,13 +1,12 @@
import { Header } from '@/components/header';
import './globals.scss';
import type { Metadata } from 'next';
import { Inter } from 'next/font/google';
const inter = Inter({ subsets: ['latin'] });
import { LayoutComponent } from './layout_component';
export const metadata: Metadata = {
title: 'Create Next App',
description: 'Generated by create next app',
title: {
default: 'Ham Reserve',
template: '%s | Ham Reserve',
},
};
export default function RootLayout({
@ -15,14 +14,5 @@ export default function RootLayout({
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body
className={`${inter.className} dark:bg-[#232323] dark:text-[#eeeeee]`}
>
<Header />
<main>{children}</main>
</body>
</html>
);
return <LayoutComponent children={children} />;
}

View File

@ -0,0 +1,28 @@
'use client';
import { Header } from '@/components/header';
import { useThemeState } from '@/state/theme-state';
import { Inter } from 'next/font/google';
import { useEffect, useState } from 'react';
const inter = Inter({ subsets: ['latin'] });
export function LayoutComponent({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState<'light' | 'dark'>('dark');
useEffect(() => {
setTheme(useThemeState.getState().theme);
useThemeState.subscribe((s) => setTheme(s.theme));
}, []);
return (
<html lang="en" className={theme}>
<body
className={`${inter.className} dark:bg-[#121212] dark:text-[#d6d6d6]`}
>
<Header />
<main>{children}</main>
</body>
</html>
);
}

View File

@ -1,7 +1,77 @@
import React from 'react';
const znaki = [
{
znak: 'S50YOTA',
od: '1. 12. 2023',
do: '31. 12. 2023',
},
];
export default function Home() {
return (
<>
<div>Wassup</div>
</>
<div className="container flex flex-col gap-10 py-8">
<div>
<h2 className="mb-4 text-2xl">Trenutni znaki</h2>
<div className="grid grid-cols-4">
{znaki.map((znak, i) => (
<div
key={i}
className="rounded-lg bg-[#f5f5f5] px-6 py-4 shadow-md dark:bg-white/5"
>
<div className="mb-2 font-mono text-2xl font-medium">
{znak.znak}
</div>
<div className="text-sm">
<div>Od: {znak.od}</div>
<div>Do: {znak.do}</div>
</div>
</div>
))}
</div>
</div>
<div className="flex flex-col gap-4">
<h2 className="text-2xl">Kako do rezervacije?</h2>
<Tile title="1. Ustvari račun">
<p>Registriraj se s klicnim znakom</p>
</Tile>
<Tile title="2. Zaprosi za dovoljenje">
<p>
Administratorji morajo odobriti tvojo prošnjo za uporabo klicnega
znaka preden lahko začneš z rezervacijo terminov.
</p>
</Tile>
<Tile title="3. Rezerviraj termin">
<p>
Izberi željen klicni znak, termin in frekvenčna območja na katerih
bi deloval.
</p>
</Tile>
<Tile title="4. Oddaj radioamaterski dnevnik">
<p>
Po delu moraš čim prej na sistem objaviti radioamaterski dnevnik v
ADI formatu. Dnevnik mora vsebovati podatke o klicnem znaku, datumu,
času (v UTC), frekvenci (ali samo frekvenčnem pasu), načinu dela.
</p>
</Tile>
</div>
</div>
);
}
interface TileProps {
title: string;
children: React.ReactNode;
image?: string;
}
function Tile({ title, children, image }: TileProps) {
return (
<div className="rounded-lg bg-[#f5f5f5] p-6 shadow-md dark:bg-white/5">
<div className="mb-2 text-xl font-medium">{title}</div>
<div>{children}</div>
</div>
);
}

View File

@ -1,17 +1,35 @@
'use client';
import { Role } from '@/enums/role.enum';
import { User } from '@/interfaces/user.interface';
import { useAuthState } from '@/state/auth-state';
import { useThemeState } from '@/state/theme-state';
import { faMoon, faSun, faUserCircle } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import Link from 'next/link';
import { useEffect, useState } from 'react';
export function Header() {
const [theme, setTheme] = useState<'light' | 'dark'>('dark');
const toggleTheme = useThemeState((s) => s.toggleTheme);
useEffect(() => {
setTheme(useThemeState.getState().theme);
useThemeState.subscribe((s) => setTheme(s.theme));
}, []);
return (
<div className="flex h-16 select-none flex-row justify-between bg-gray-100 shadow dark:bg-[#454545]">
<div className="bg-primary flex h-16 select-none flex-row justify-between text-white shadow">
<Link href="/" className="my-auto ml-4 text-2xl font-semibold">
Ham Reserve
</Link>
<div className="flex flex-row gap-4">
<div className="flex flex-row">
<button className="header-button" onClick={toggleTheme}>
<FontAwesomeIcon
icon={theme === 'dark' ? faSun : faMoon}
className="w-4"
/>
</button>
<UserHeader />
</div>
</div>
@ -30,24 +48,42 @@ function UserHeader() {
return user ? (
<div className="relative h-full">
<button
className="flex h-full items-center p-6 hover:bg-white/20"
className="header-button flex items-center"
onClick={() => setIsOpen(!isOpen)}
>
<div>{user.username.toUpperCase()}</div>
<FontAwesomeIcon icon={faUserCircle} className="mr-2 h-5" />
<span>{user.username.toUpperCase()}</span>
</button>
{isOpen && (
<div className="absolute right-0 z-10 mt-2 w-56 rounded-md border-gray-500 bg-white/20 py-2 shadow-md">
<div
className={`absolute right-2 z-10 mt-2 ${
isOpen ? '' : 'scale-0 delay-100'
}`}
>
<div
className={`w-56 origin-top-right rounded-md bg-[#f8f8f8] py-2 text-black shadow-sm ring-1 ring-inset ring-gray-300 duration-100 dark:bg-[#454545] dark:text-white dark:ring-gray-500 ${
isOpen ? 'scale-100 opacity-100' : 'scale-95 opacity-0'
}`}
>
{user.roles.includes(Role.Admin) && (
<Link
href="/admin"
className="block px-4 py-2 text-left text-red-500 hover:bg-black/5 dark:hover:bg-white/10"
onClick={() => setIsOpen(false)}
>
Admin panel
</Link>
)}
<button
onClick={() => {
logout();
window.location.reload();
}}
className="w-full px-4 py-2 text-left hover:bg-white/10"
className="block w-full px-4 py-2 text-left hover:bg-black/5 dark:hover:bg-white/10"
>
Odjava
</button>
</div>
)}
</div>
</div>
) : (
<div className="relative h-full">

View File

@ -0,0 +1,4 @@
export enum Role {
User = 'user',
Admin = 'admin',
}

View File

@ -0,0 +1,7 @@
export interface CreateUserDto {
username: string;
password: string;
name: string;
email?: string;
phone?: string;
}

View File

@ -0,0 +1,12 @@
import { User } from './user.interface';
export interface Event {
_id: string;
callsign: string;
description: string;
fromDateTime: Date;
toDateTime: Date;
access: User[];
createdAt: Date;
isDeleted: boolean;
}

View File

@ -1,5 +1,10 @@
export interface User {
id: string;
_id: string;
username: string;
name: string;
email: string;
phone: string;
createdAt: Date;
isDeleted: boolean;
roles: string[];
}

View File

@ -12,6 +12,7 @@ export interface AuthState {
getUser: () => Promise<User | null>;
isValid: () => Promise<boolean>;
getAccessToken: () => Promise<string | null>;
logout: () => void;
}
@ -37,6 +38,10 @@ export const useAuthState = create(
return null;
}
},
getAccessToken: async () => {
const isValid = await get().isValid();
return isValid ? get().accessToken : null;
},
isValid: async () => {
const { accessToken, refreshToken } = get();

View File

@ -0,0 +1,24 @@
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
interface ThemeState {
theme: 'light' | 'dark';
toggleTheme: () => void;
}
export const useThemeState = create(
persist<ThemeState>(
(set, get) => ({
theme:
typeof window !== 'undefined' &&
window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
: 'light',
toggleTheme: () =>
set({ theme: get().theme === 'dark' ? 'light' : 'dark' }),
}),
{
name: 'theme-storage',
},
),
);

View File

@ -1,12 +1,25 @@
import type { Config } from "tailwindcss";
import type { Config } from 'tailwindcss';
const config: Config = {
content: [
"./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
'./src/pages/**/*.{js,ts,jsx,tsx,mdx}',
'./src/components/**/*.{js,ts,jsx,tsx,mdx}',
'./src/app/**/*.{js,ts,jsx,tsx,mdx}',
],
theme: {},
darkMode: 'class',
theme: {
container: {
center: true,
padding: '2rem',
},
extend: {
colors: {
primary: {
DEFAULT: '#E95635',
},
},
},
},
plugins: [],
};
export default config;

View File

@ -51,6 +51,39 @@
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.48.0.tgz#642633964e217905436033a2bd08bf322849b7fb"
integrity sha512-ZSjtmelB7IJfWD2Fvb7+Z+ChTIKWq6kjda95fLcQKNS5aheVHn4IkfgRQE3sIIzTcSLwLcLZUD9UBt+V7+h+Pw==
"@fortawesome/fontawesome-common-types@6.4.2":
version "6.4.2"
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.4.2.tgz#1766039cad33f8ad87f9467b98e0d18fbc8f01c5"
integrity sha512-1DgP7f+XQIJbLFCTX1V2QnxVmpLdKdzzo2k8EmvDOePfchaIGQ9eCHj2up3/jNEbZuBqel5OxiaOJf37TWauRA==
"@fortawesome/fontawesome-svg-core@^6.4.2":
version "6.4.2"
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.4.2.tgz#37f4507d5ec645c8b50df6db14eced32a6f9be09"
integrity sha512-gjYDSKv3TrM2sLTOKBc5rH9ckje8Wrwgx1CxAPbN5N3Fm4prfi7NsJVWd1jklp7i5uSCVwhZS5qlhMXqLrpAIg==
dependencies:
"@fortawesome/fontawesome-common-types" "6.4.2"
"@fortawesome/free-solid-svg-icons@^6.4.2":
version "6.4.2"
resolved "https://registry.yarnpkg.com/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.4.2.tgz#33a02c4cb6aa28abea7bc082a9626b7922099df4"
integrity sha512-sYwXurXUEQS32fZz9hVCUUv/xu49PEJEyUOsA51l6PU/qVgfbTb2glsTEaJngVVT8VqBATRIdh7XVgV1JF1LkA==
dependencies:
"@fortawesome/fontawesome-common-types" "6.4.2"
"@fortawesome/react-fontawesome@^0.2.0":
version "0.2.0"
resolved "https://registry.yarnpkg.com/@fortawesome/react-fontawesome/-/react-fontawesome-0.2.0.tgz#d90dd8a9211830b4e3c08e94b63a0ba7291ddcf4"
integrity sha512-uHg75Rb/XORTtVt7OS9WoK8uM276Ufi7gCzshVWkUJbHhh3svsUUeqXerrM96Wm7fRiDzfKRwSoahhMIkGAYHw==
dependencies:
prop-types "^15.8.1"
"@headlessui/react@^1.7.17":
version "1.7.17"
resolved "https://registry.yarnpkg.com/@headlessui/react/-/react-1.7.17.tgz#a0ec23af21b527c030967245fd99776aa7352bc6"
integrity sha512-4am+tzvkqDSSgiwrsEpGWqgGo9dz8qU5M3znCkC4PgkpY4HcCZzEDEvozltGGGHIKl9jbXbZPSH5TWn4sWJdow==
dependencies:
client-only "^0.0.1"
"@humanwhocodes/config-array@^0.11.10":
version "0.11.11"
resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.11.tgz#88a04c570dbbc7dd943e4712429c3df09bc32844"
@ -214,6 +247,13 @@
dependencies:
"@types/react" "*"
"@types/react-no-ssr@^1.1.3":
version "1.1.3"
resolved "https://registry.yarnpkg.com/@types/react-no-ssr/-/react-no-ssr-1.1.3.tgz#895baced8f49e270289c8ad11be1a4a50328d243"
integrity sha512-uMR17qGISe0qTTiVFuRfatP+9plEe/Q0beQ47xy0OXatwb3Z2bEj3OW7FC+9PVqCYEsfR4b01LU9tXw2urzBzw==
dependencies:
"@types/react" "*"
"@types/react@*", "@types/react@18.2.21":
version "18.2.21"
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.21.tgz#774c37fd01b522d0b91aed04811b58e4e0514ed9"
@ -468,6 +508,14 @@ axobject-query@^3.1.1:
dependencies:
dequal "^2.0.3"
babel-runtime@6.x.x:
version "6.26.0"
resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe"
integrity sha512-ITKNuq2wKlW1fJg9sSW52eepoYgZBggvOAHC0u/CYu/qxQ9EVzThCgR69BnSXLHjy2f7SY5zaQ4yt7H9ZVxY2g==
dependencies:
core-js "^2.4.0"
regenerator-runtime "^0.11.0"
balanced-match@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
@ -556,7 +604,7 @@ chalk@^4.0.0:
optionalDependencies:
fsevents "~2.3.2"
client-only@0.0.1:
client-only@0.0.1, client-only@^0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/client-only/-/client-only-0.0.1.tgz#38bba5d403c41ab150bff64a95c85013cf73bca1"
integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==
@ -590,6 +638,11 @@ concat-map@0.0.1:
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==
core-js@^2.4.0:
version "2.6.12"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.12.tgz#d9333dfa7b065e347cc5682219d6f690859cc2ec"
integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==
cross-spawn@^7.0.2:
version "7.0.3"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
@ -2036,6 +2089,13 @@ react-is@^16.13.1:
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
react-no-ssr@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/react-no-ssr/-/react-no-ssr-1.1.0.tgz#313b48d2e26020f969ed98e472f10481604e3cc8"
integrity sha512-3td8iPIEFKWXOJ3Ar5xURvZAsv/aIlngJLBH6fP5QC3WhsfuO2pn7WQR0ZlkTE0puWCL0RDEvXtOfAg4qMp+xA==
dependencies:
babel-runtime "6.x.x"
react-secure-storage@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/react-secure-storage/-/react-secure-storage-1.3.0.tgz#b223c3d608aa11d28232d3530323ab9d60361471"
@ -2077,6 +2137,11 @@ reflect.getprototypeof@^1.0.3:
globalthis "^1.0.3"
which-builtin-type "^1.1.3"
regenerator-runtime@^0.11.0:
version "0.11.1"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9"
integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==
regenerator-runtime@^0.14.0:
version "0.14.0"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz#5e19d68eb12d486f797e15a3c6a918f7cec5eb45"