Upgrade design & moved backend here & login and settings work with api

This commit is contained in:
2024-09-29 18:04:41 +02:00
parent 42d3212624
commit f2d1eb4289
43 changed files with 1881 additions and 359 deletions
+71
View File
@@ -0,0 +1,71 @@
import { NextRequest, NextResponse } from 'next/server';
import { getDBConnexion } from '../../utils';
import { compareSync } from 'bcrypt';
type RequestBody = {
username?: string;
password?: string;
};
export const POST = async (req: NextRequest) => {
const { username, password }: RequestBody = await req.json();
if (!username || !password) {
return NextResponse.json(
{
error: true,
message: 'Missing username or password field in request body',
},
{
status: 400,
}
);
}
try {
const pool = getDBConnexion();
const { rows } = await pool.query(
`SELECT * FROM "user" WHERE "username"=$1 LIMIT 1;`,
[username]
);
await pool.end();
if (rows.length != 1) {
return NextResponse.json({
error: true,
message: 'User not found',
});
}
const { id, password: hashedPassword } = rows[0];
if (!compareSync(password, hashedPassword)) {
return NextResponse.json(
{
error: true,
message: 'Wrong password',
},
{
status: 403,
}
);
}
return NextResponse.json({
token: id,
});
} catch (error) {
console.error(error);
return NextResponse.json(
{
error: true,
message: 'Internal error',
},
{
status: 500,
}
);
}
};
+285
View File
@@ -0,0 +1,285 @@
import { NextRequest, NextResponse } from 'next/server';
import { getDBConnexion } from '../utils';
import { compareSync, hashSync } from 'bcrypt';
export const GET = async (req: NextRequest) => {
const userID = req.headers.get('Authorization');
if (!userID) {
return NextResponse.json(
{
error: true,
message: 'Unauthorized request',
},
{
status: 403,
}
);
}
try {
const pool = getDBConnexion();
const { rows } = await pool.query(
`SELECT * FROM "user" WHERE "id"=$1 LIMIT 1;`,
[userID]
);
if (rows.length != 1) {
return NextResponse.json(
{
error: true,
message: 'User not found',
},
{
status: 404,
}
);
}
await pool.end();
return NextResponse.json(rows[0]);
} catch (error) {
console.error(error);
return NextResponse.json(
{
error: true,
message: 'Internal error',
},
{
status: 500,
}
);
}
};
type RequestBody = {
type?: 'username' | 'password';
username?: string;
currentPassword?: string;
newPassword?: string;
};
export const PUT = async (req: NextRequest) => {
const userID = req.headers.get('Authorization');
const { type, username, currentPassword, newPassword }: RequestBody =
await req.json();
if (!userID) {
return NextResponse.json(
{
error: true,
message: 'Unauthorized request',
},
{
status: 403,
}
);
}
if (!type) {
return NextResponse.json(
{
error: true,
message: 'Missing type field in request body',
},
{
status: 400,
}
);
}
if (type == 'username') {
if (!username) {
return NextResponse.json(
{
error: true,
message: 'Missing username field in request body',
},
{
status: 400,
}
);
}
try {
const pool = getDBConnexion();
const { rowCount } = await pool.query(
`SELECT COUNT(*) FROM "user" WHERE "username"=$1 LIMIT 1;`,
[username]
);
if (rowCount > 0) {
return NextResponse.json(
{
error: true,
message: 'Username already used',
},
{
status: 409,
}
);
}
const { rows } = await pool.query(
`SELECT * FROM "user" WHERE "id"=$1 LIMIT 1;`,
[userID]
);
if (rows.length != 1) {
return NextResponse.json(
{
error: true,
message: 'User not found',
},
{
status: 404,
}
);
}
await pool.query(`UPDATE "user" SET "username"=$1 WHERE "id"=$2;`, [
username,
userID,
]);
await pool.end();
return NextResponse.json({
message: 'Successfully updated username',
});
} catch (error) {
console.error(error);
return NextResponse.json(
{
error: true,
message: 'Internal error',
},
{
status: 500,
}
);
}
}
if (type == 'password') {
if (!currentPassword || !newPassword) {
return NextResponse.json(
{
error: true,
message:
'Missing currentPassword and/or newPassword field(s) in request body',
},
{
status: 400,
}
);
}
try {
const pool = getDBConnexion();
const { rows } = await pool.query(
`SELECT * FROM "user" WHERE "id"=$1 LIMIT 1;`,
[userID]
);
if (rows.length != 1) {
return NextResponse.json(
{
error: true,
message: 'User not found',
},
{
status: 404,
}
);
}
const { password: currentHashedPassword } = rows[0];
if (!compareSync(currentPassword, currentHashedPassword)) {
return NextResponse.json(
{
error: true,
message: 'Wrong password',
},
{
status: 403,
}
);
}
const newHashedPassword = hashSync(newPassword, 10);
await pool.query(`UPDATE "user" SET "password"=$1 WHERE "id"=$2`, [
newHashedPassword,
userID,
]);
await pool.end();
return NextResponse.json({
message: 'Successfully updated password',
});
} catch (error) {
console.error(error);
return NextResponse.json(
{
error: true,
message: 'Internal error',
},
{
status: 500,
}
);
}
}
};
export const DELETE = async (req: NextRequest) => {
const userID = req.headers.get('Authorization');
try {
const pool = getDBConnexion();
const { rows } = await pool.query(
`SELECT * FROM "user" WHERE "id"=$1 LIMIT 1;`,
[userID]
);
if (rows.length != 1) {
return NextResponse.json(
{
error: true,
message: 'User not found',
},
{
status: 404,
}
);
}
await pool.query(`DELETE FROM "user" WHERE "id"=$1 LIMIT 1;`, [userID]);
await pool.end();
return NextResponse.json({
message: 'Successfully delete user',
});
} catch (error) {
console.error(error);
return NextResponse.json(
{
error: true,
message: 'Internal error',
},
{
status: 500,
}
);
}
};
+9
View File
@@ -0,0 +1,9 @@
import { createPool } from '@vercel/postgres';
export const getDBConnexion = () => {
const pool = createPool({
connectionString: process.env.POSTGRES_URL,
});
return pool;
};
+10 -14
View File
@@ -17,19 +17,16 @@ export const useActions = ({ setError, setIsLoading }) => {
setIsLoading(true);
try {
const request = await fetch(
'https://lovemap-backend.vercel.app/auth/login',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
username,
password,
}),
}
);
const request = await fetch('/api/auth/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
username,
password,
}),
});
const response = await request.json();
@@ -44,7 +41,6 @@ export const useActions = ({ setError, setIsLoading }) => {
return;
}
if (request.status == 500) {
console.log('request.status: ', request.status);
setError('Erreur interne. Veuillez réessayer');
setIsLoading(false);
return;
+1 -1
View File
@@ -18,7 +18,7 @@ import { useActions } from './hooks/useActions';
// ------------------------------------------------- Assets & Styles ---------------------------------------------------
import Loader from '@/assets/Loader';
import '../styles.scss';
import './styles.scss';
// ---------------------------------------------------------------------------------------------------------------------
const Login = () => {
@@ -39,7 +39,7 @@
display: flex;
flex-direction: column;
align-items: stretch;
gap: 15px;
gap: 20px;
& > .error {
text-align: center;
+1 -1
View File
@@ -18,7 +18,7 @@ body {
flex-direction: column;
width: 100svw;
height: 100svh;
background-color: #d94253;
background-color: #ffffff;
}
a {
-21
View File
@@ -1,21 +0,0 @@
import { Inter } from 'next/font/google';
import Provider from './provider';
import './globals.scss';
const inter = Inter({ subsets: ['latin'] });
export const metadata = {
title: 'Lovemap ❤️',
};
const RootLayout = ({ children }) => {
return (
<html lang='fr'>
<body className={inter.className}>
<Provider>{children}</Provider>
</body>
</html>
);
};
export default RootLayout;
+35
View File
@@ -0,0 +1,35 @@
// ---------------------------------------------------------------------------------------------------------------------
//! Imports
// ---------------------------------------------------------------------------------------------------------------------
// ------------------------------------------------------ Next ---------------------------------------------------------
import { Metadata } from 'next';
// ---------------------------------------------------------------------------------------------------------------------
// --------------------------------------------------- Components ------------------------------------------------------
import Provider from './provider';
// ---------------------------------------------------------------------------------------------------------------------
// ------------------------------------------------- Assets & Styles ---------------------------------------------------
import { Inter } from 'next/font/google';
import './globals.scss';
// ---------------------------------------------------------------------------------------------------------------------
const inter = Inter({ subsets: ['latin'] });
export const metadata: Metadata = {
title: 'Lovemap ❤️',
robots: 'none',
};
const RootLayout = ({ children }) => {
return (
<html lang='fr'>
<body className={inter.className}>
<Provider>{children}</Provider>
</body>
</html>
);
};
export default RootLayout;
View File
+159
View File
@@ -0,0 +1,159 @@
// ---------------------------------------------------------------------------------------------------------------------
//! Imports
// ---------------------------------------------------------------------------------------------------------------------
// -------------------------------------------------- Hooks & Utils ----------------------------------------------------
import { useRouter } from 'next/navigation';
import { FormEvent } from 'react';
// ---------------------------------------------------------------------------------------------------------------------
export const useActions = ({
userData,
setIsPopupDisplayed,
setUsernameMessage,
setPasswordMessage,
}) => {
const router = useRouter();
const resetMessage = () => {
setUsernameMessage(null);
setPasswordMessage(null);
};
const updateUsername = async (value: string) => {
resetMessage();
if (value == userData.username) {
return;
}
const token = localStorage.getItem('token');
try {
const request = await fetch('/api/user', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
Authorization: token,
},
body: JSON.stringify({
type: 'username',
username: value,
}),
});
if (request.status == 409) {
setUsernameMessage("Nom d'utilisateur déjà pris");
return;
}
if (request.status != 200) {
setUsernameMessage('Erreur interne. Veuillez réessayer');
return;
}
setUsernameMessage("Nom d'utilisateur modifié");
} catch (error) {
console.error(error);
setUsernameMessage('Erreur interne. Veuillez réessayer');
}
};
const updatePassword = async (ev: FormEvent<HTMLFormElement>) => {
ev.preventDefault();
if (!ev.currentTarget.reportValidity()) {
return;
}
resetMessage();
const formData = new FormData(ev.currentTarget);
const currentPassword = formData.get('currentPassword');
const newPassword = formData.get('newPassword');
const confirmNewPassword = formData.get('confirmNewPassword');
const token = localStorage.getItem('token');
if (newPassword != confirmNewPassword) {
setPasswordMessage('Les mots de passes ne correspondent pas');
return;
}
try {
const request = await fetch('/api/user', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
Authorization: token,
},
body: JSON.stringify({
type: 'password',
currentPassword,
newPassword,
}),
});
if (request.status == 403) {
setPasswordMessage('Mauvais mot de passe');
return;
}
if (request.status == 500) {
setPasswordMessage('Erreur interne. Veuillez réessayer');
return;
}
setPasswordMessage('Mot de passe modifié');
} catch (error) {
console.error(error);
setPasswordMessage('Erreur interne. Veuillez réessayer');
}
};
const handleClosePopup = (ev) => {
ev.stopPropagation();
setIsPopupDisplayed(false);
};
const handleDeleteUser = (ev) => {
ev.stopPropagation();
resetMessage();
setIsPopupDisplayed(true);
};
const deleteUser = async () => {
resetMessage();
const token = localStorage.getItem('token');
try {
const request = await fetch(
'https://lovemap-backend.vercel.app/user/delete',
{
method: 'DELETE',
headers: {
Authorization: token,
},
}
);
if (request.status != 200) {
// setMessage('Erreur interne. Veuillez réessayer');
return;
}
localStorage.clear();
router.push('/auth/login');
} catch (error) {
console.error(error);
// setMessage('Erreur interne. Veuillez réessayer');
}
};
return {
updateUsername,
updatePassword,
handleClosePopup,
handleDeleteUser,
deleteUser,
};
};
+42
View File
@@ -0,0 +1,42 @@
import { useEffect, useState } from 'react';
export const useData = () => {
const [userData, setUserData] = useState(null);
const [isPopupDisplayed, setIsPopupDisplayed] = useState(false);
const [usernameMessage, setUsernameMessage] = useState(null);
const [passwordMessage, setPasswordMessage] = useState(null);
useEffect(() => {
(async () => {
const token = localStorage.getItem('token');
const request = await fetch('/api/user', {
method: 'GET',
headers: {
Authorization: token,
},
});
const response = await request.json();
const formattedResponse = {
...response,
createdat: new Date(response.createdat).toLocaleDateString(
'fr-FR'
),
};
setUserData(formattedResponse);
})();
}, []);
return {
userData,
isPopupDisplayed,
setIsPopupDisplayed,
usernameMessage,
setUsernameMessage,
passwordMessage,
setPasswordMessage,
};
};
-73
View File
@@ -1,73 +0,0 @@
// ---------------------------------------------------------------------------------------------------------------------
//! Imports
// ---------------------------------------------------------------------------------------------------------------------
// --------------------------------------------------- Components ------------------------------------------------------
import Link from 'next/link';
import Input from '@/components/Input/Input';
// ---------------------------------------------------------------------------------------------------------------------
// ------------------------------------------------- Assets & Styles ---------------------------------------------------
import ArrowRightIcon from '@/assets/ArrowRightIcon';
import './styles.scss';
// ---------------------------------------------------------------------------------------------------------------------
const Settings = () => {
return (
<div className='settings'>
<div className='header'>
<Link
href={'/statistics'}
className='backButton'
>
<ArrowRightIcon
style={{
rotate: '180deg',
}}
/>
</Link>
<p className='title'>Éditer le profil</p>
</div>
<form className='form'>
<Input
label="Nom d'utilisateur"
type='text'
name='username'
defaultValue='Un Épicier'
autoComplete='off'
required
/>
<Input
label='Mot de passe'
type='password'
name='password'
defaultValue='********'
autoComplete='off'
required
/>
<div className='buttonsContainer'>
<button
type='reset'
className='resetButton'
>
Annuler
</button>
<button
type='submit'
className='submitButton'
>
Appliquer
</button>
</div>
</form>
<div className='separator' />
<button className='deleteButton'>Supprimer mon compte</button>
</div>
);
};
export default Settings;
+187
View File
@@ -0,0 +1,187 @@
'use client';
// ---------------------------------------------------------------------------------------------------------------------
//! Imports
// ---------------------------------------------------------------------------------------------------------------------
// --------------------------------------------------- Components ------------------------------------------------------
import Link from 'next/link';
import Input from '@/components/Input/Input';
import InlineInput from '@/components/InlineInput/InlineInput';
// ---------------------------------------------------------------------------------------------------------------------
// -------------------------------------------------- Hooks & Utils ----------------------------------------------------
import { useData } from './hooks/useData';
import { useActions } from './hooks/useActions';
// ---------------------------------------------------------------------------------------------------------------------
// ------------------------------------------------- Assets & Styles ---------------------------------------------------
import ArrowRightIcon from '@/assets/ArrowRightIcon';
import TrashIcon from '@/assets/TrashIcon';
import CloseIcon from '@/assets/CloseIcon';
import CheckIcon from '@/assets/CheckIcon';
import './styles.scss';
// ---------------------------------------------------------------------------------------------------------------------
const Settings = () => {
const {
userData,
isPopupDisplayed,
setIsPopupDisplayed,
usernameMessage,
setUsernameMessage,
passwordMessage,
setPasswordMessage,
} = useData();
const {
updateUsername,
updatePassword,
handleClosePopup,
handleDeleteUser,
deleteUser,
} = useActions({
userData,
setIsPopupDisplayed,
setUsernameMessage,
setPasswordMessage,
});
if (!userData) {
return;
}
return (
<div className='settings'>
<div className='header'>
<Link
href={'/statistics'}
className='backButton'
>
<ArrowRightIcon
style={{
rotate: '180deg',
}}
/>
</Link>
<p className='title'>Éditer le profil</p>
</div>
<div className='settingsContainer'>
{usernameMessage && (
<p className='message'>{usernameMessage}</p>
)}
<InlineInput
label="Nom d'utilisateur"
type='text'
name='username'
placeholder="Nom d'utilisateur"
defaultValue={userData.username}
autoComplete='username'
onSubmit={updateUsername}
/>
<div className='separator' />
<form
className='passwordForm'
onSubmit={updatePassword}
>
{passwordMessage && (
<p className='message'>{passwordMessage}</p>
)}
<Input
type='password'
name='currentPassword'
placeholder='Mot de passe actuel'
autoComplete='password'
required
/>
<Input
type='password'
name='newPassword'
placeholder='Nouveau mot de passe'
autoComplete='off'
required
/>
<Input
type='password'
name='confirmNewPassword'
placeholder='Confirmer le nouveau mot de passe'
autoComplete='off'
required
/>
<button
type='submit'
className='submitButton'
>
Appliquer
</button>
</form>
<div className='separator'></div>
<button
className='deleteButton'
onClick={handleDeleteUser}
>
<TrashIcon />
Supprimer mon compte
</button>
{isPopupDisplayed && (
<div
className='popupOverlay'
onClick={handleClosePopup}
>
<div
className='popup'
onClick={(ev) => ev.stopPropagation()}
>
<div className='popupHeader'>
<p className='popupTitle'>
Supprimer le compte ?
</p>
<button
className='popupCloseButton'
onClick={handleClosePopup}
>
<CloseIcon />
</button>
</div>
<div className='popupContent'>
<p>
Êtes-vous sûr de vouloir supprimer votre
compte ? Cette action est irréversible.
</p>
</div>
<div className='popupButtonsContainer'>
<button
type='button'
className='popupButton'
onClick={handleClosePopup}
>
<CloseIcon />
Non
</button>
<button
type='button'
className='popupButton'
onClick={deleteUser}
>
<CheckIcon />
Oui
</button>
</div>
</div>
</div>
)}
</div>
</div>
);
};
export default Settings;
+133 -54
View File
@@ -4,7 +4,6 @@
flex-direction: column;
gap: 20px;
width: 100%;
background-color: #d94253;
& > .header {
display: grid;
@@ -12,6 +11,7 @@
align-items: center;
gap: 15px;
padding: 15px;
background-color: #d94253;
color: #ffffff;
box-shadow: 0 0 6px rgba(255, 255, 255, 0.2);
@@ -26,68 +26,147 @@
}
}
& > .form {
display: grid;
grid-template-rows: repeat(2, 1fr);
align-items: center;
& > .settingsContainer {
flex-grow: 1;
position: relative;
display: flex;
flex-direction: column;
gap: 20px;
padding: 20px;
padding-bottom: 0;
padding: 0 20px;
& > .buttonsContainer {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20px;
& .message {
padding: 5px;
border-radius: 5px;
background-color: #d94253;
font-size: 1rem;
text-align: center;
color: #ffffff;
}
& > .submitButton,
& > .resetButton {
display: flex;
justify-content: center;
align-items: center;
gap: 5px;
padding: 10px;
border: 1px solid #ffffff;
& > .separator {
display: block;
margin: 20px auto;
width: 90%;
height: 1px;
background-color: #d94253;
background: linear-gradient(
90deg,
rgba(#d94253, 0) 0%,
rgba(#d94253, 1) 10%,
rgba(#d94253, 1) 90%,
rgba(#d94253, 0) 100%
);
}
& > .passwordForm {
display: flex;
flex-direction: column;
gap: 10px;
& > .submitButton {
padding: 7px 21px;
border: none;
border-radius: 5px;
background-color: #ffffff;
background-color: #d94253;
font-size: 1rem;
color: #d94253;
color: #ffffff;
cursor: pointer;
}
}
& > .resetButton {
background-color: #d94253;
color: #ffffff;
& > .deleteButton {
display: grid;
grid-template-columns: 20px max-content;
justify-content: center;
align-items: center;
gap: 10px;
padding: 7px 21px;
border: 1px solid #d94253;
border-radius: 5px;
background-color: #d94253;
font-size: 1rem;
color: #ffffff;
cursor: pointer;
}
& > .popupOverlay {
position: absolute;
top: 0;
left: 0;
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
background-color: rgba(#ffffff, 0.5);
backdrop-filter: blur(5px);
& > .popup {
z-index: 1;
position: relative;
display: flex;
flex-direction: column;
margin: 10px;
background-color: #ffffff;
border: 1px solid #d94253;
border-radius: 5px;
& > .popupHeader {
display: grid;
grid-template-columns: 1fr 20px;
align-items: center;
gap: 20px;
padding: 10px;
& > .popupTitle {
font-size: 1.2rem;
color: #d94253;
}
& > .popupCloseButton {
display: flex;
justify-content: center;
align-items: center;
border: none;
border-radius: 5px;
background-color: transparent;
color: #d94253;
cursor: pointer;
transition: background-color 0.3s ease;
&:hover {
background-color: rgba(#d94253, 0.2);
}
}
}
& > .popupContent {
padding: 10px;
color: #d94253;
}
& > .popupButtonsContainer {
display: flex;
flex-wrap: nowrap;
justify-content: space-evenly;
align-items: center;
padding: 10px;
& > .popupButton {
display: grid;
grid-template-columns: 20px 1fr;
align-items: center;
gap: 5px;
padding: 5px 15px;
border: none;
border-radius: 5px;
background-color: #d94253;
font-size: 1rem;
color: #ffffff;
cursor: pointer;
}
}
}
}
}
& > .separator {
display: block;
margin: 20px auto;
width: 90%;
height: 1px;
background-color: rgb(254, 254, 250);
background: linear-gradient(
90deg,
rgba(254, 254, 250, 0) 0%,
rgba(254, 254, 250, 1) 10%,
rgba(254, 254, 250, 1) 90%,
rgba(254, 254, 250, 0) 100%
);
}
& > .deleteButton {
display: flex;
justify-content: center;
align-items: center;
gap: 5px;
margin: 0 20px;
padding: 10px;
border: 1px solid #d80135;
border-radius: 5px;
background-color: #d80135;
font-size: 1rem;
color: #ffffff;
cursor: pointer;
}
}
+8 -11
View File
@@ -7,21 +7,18 @@ export const useData = () => {
(async () => {
const token = localStorage.getItem('token');
const request = await fetch(
'https://lovemap-backend.vercel.app/user',
{
method: 'GET',
headers: {
Authorization: token,
},
}
);
const request = await fetch('/api/user', {
method: 'GET',
headers: {
Authorization: token,
},
});
const response = await request.json();
const formattedResponse = {
...response.user,
createdat: new Date(response.user.createdat).toLocaleDateString(
...response,
createdat: new Date(response.createdat).toLocaleDateString(
'fr-FR'
),
};
@@ -10,21 +10,29 @@ import SlotCounter from 'react-slot-counter';
// ---------------------------------------------------------------------------------------------------------------------
// -------------------------------------------------- Hooks & Utils ----------------------------------------------------
import { useRouter } from 'next/navigation';
import { useData } from './hooks/useData';
// ---------------------------------------------------------------------------------------------------------------------
// ------------------------------------------------- Assets & Styles ---------------------------------------------------
import SettingsIcon from '@/assets/SettingsIcon';
import { Logout } from '@/assets/Logout';
import './styles.scss';
// ---------------------------------------------------------------------------------------------------------------------
const Statistics = () => {
const router = useRouter();
const { userData } = useData();
if (!userData) {
return;
}
const logout = () => {
localStorage.clear();
router.push('/auth/login');
};
return (
<div className='accountContainer'>
<div className='header'>
@@ -33,12 +41,21 @@ const Statistics = () => {
<p className='sinceDate'>Créé le: {userData.createdat}</p>
</div>
<Link
href={'/settings'}
className='settingsButton'
>
<SettingsIcon />
</Link>
<div className='buttonsContainer'>
<button
type='button'
className='button'
onClick={logout}
>
<Logout />
</button>
<Link
href={'/settings'}
className='button'
>
<SettingsIcon />
</Link>
</div>
</div>
<div className='separator' />
<div className='statistics'>
@@ -48,7 +65,7 @@ const Statistics = () => {
startValue={'00'}
startValueOnce
dummyCharacterCount={10}
direction='top-bottom'
direction='top-down'
containerClassName='slotCounter'
/>
<p className='statName'>Loves par semaine</p>
@@ -59,7 +76,7 @@ const Statistics = () => {
startValue={'00'}
startValueOnce
dummyCharacterCount={10}
direction='top-bottom'
direction='top-down'
containerClassName='slotCounter'
/>
<p className='statName'>Loves par mois</p>
@@ -70,7 +87,7 @@ const Statistics = () => {
startValue={'00'}
startValueOnce
dummyCharacterCount={10}
direction='top-bottom'
direction='top-down'
containerClassName='slotCounter'
/>
<p className='statName'>Loves par an</p>
@@ -81,7 +98,7 @@ const Statistics = () => {
startValue={'00'}
startValueOnce
dummyCharacterCount={10}
direction='top-bottom'
direction='top-down'
containerClassName='slotCounter'
/>
<p className='statName'>Loves total</p>
+24 -45
View File
@@ -21,21 +21,31 @@
& > .username {
font-size: 1.1rem;
color: #ffffff;
color: #d94253;
}
& > .sinceDate {
font-size: 0.9rem;
font-weight: 300;
color: #eee;
color: #d94253;
}
}
& > .settingsButton {
$size: 25px;
width: $size;
height: $size;
color: #ffffff;
& > .buttonsContainer {
display: flex;
flex-wrap: nowrap;
align-items: center;
gap: 10px;
& > .button {
$size: 25px;
width: $size;
height: $size;
border: none;
background-color: transparent;
color: #d94253;
cursor: pointer;
}
}
}
@@ -43,13 +53,13 @@
display: block;
width: 90%;
height: 1px;
background-color: rgb(254, 254, 250);
background-color: #d94253;
background: linear-gradient(
90deg,
rgba(254, 254, 250, 0) 0%,
rgba(254, 254, 250, 1) 10%,
rgba(254, 254, 250, 1) 90%,
rgba(254, 254, 250, 0) 100%
rgba(#d94253, 0) 0%,
rgba(#d94253, 1) 10%,
rgba(#d94253, 1) 90%,
rgba(#d94253, 0) 100%
);
}
@@ -68,45 +78,14 @@
flex-direction: column;
align-items: center;
font-weight: 300;
color: #ffffff;
color: #d94253;
& > .slotCounter {
font-size: 3rem;
}
& > .statName {
color: #eee;
}
}
}
& > .buttonsContainer {
display: flex;
flex-direction: column;
align-items: stretch;
gap: 2px;
width: 100%;
box-shadow: 0 0 6px rgba(255, 255, 255, 0.2);
& > .button {
display: flex;
flex-wrap: nowrap;
justify-content: space-between;
gap: 20px;
padding: 15px 20px;
border: none;
background-color: #d94253;
font-size: 1rem;
color: #ffffff;
filter: brightness(1.1);
transition: filter 0.3s ease;
&:hover {
filter: brightness(1.3);
}
& > svg {
width: 1.2em;
color: #d94253;
}
}
}
@@ -1,4 +1,6 @@
const ArrowRightIcon = (props) => {
import { SVGProps } from 'react';
const ArrowRightIcon = (props?: SVGProps<SVGSVGElement>) => {
return (
<svg
xmlns='http://www.w3.org/2000/svg'
+22
View File
@@ -0,0 +1,22 @@
import { SVGProps } from 'react';
const CheckIcon = (props: SVGProps<SVGSVGElement>) => {
return (
<svg
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 512 512'
{...props}
>
<path
fill='none'
stroke='currentColor'
strokeLinecap='round'
strokeLinejoin='round'
strokeWidth='32'
d='M416 128L192 384l-96-96'
/>
</svg>
);
};
export default CheckIcon;
+22
View File
@@ -0,0 +1,22 @@
import { SVGProps } from 'react';
const CloseIcon = (props: SVGProps<SVGSVGElement>) => {
return (
<svg
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 512 512'
{...props}
>
<path
fill='none'
stroke='currentColor'
strokeLinecap='round'
strokeLinejoin='round'
strokeWidth='32'
d='M368 368L144 144M368 144L144 368'
/>
</svg>
);
};
export default CloseIcon;
+3 -1
View File
@@ -1,4 +1,6 @@
const EyeIcon = (props) => {
import { SVGProps } from 'react';
const EyeIcon = (props?: SVGProps<SVGSVGElement>) => {
return (
<svg
xmlns='http://www.w3.org/2000/svg'
@@ -1,10 +1,12 @@
const EyeSlashIcon = (props) => {
import { SVGProps } from 'react';
const EyeSlashIcon = (props?: SVGProps<SVGSVGElement>) => {
return (
<svg
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 512 512'
{...props}
fill='currentColor'
{...props}
>
<path d='M432 448a15.92 15.92 0 01-11.31-4.69l-352-352a16 16 0 0122.62-22.62l352 352A16 16 0 01432 448zM255.66 384c-41.49 0-81.5-12.28-118.92-36.5-34.07-22-64.74-53.51-88.7-91v-.08c19.94-28.57 41.78-52.73 65.24-72.21a2 2 0 00.14-2.94L93.5 161.38a2 2 0 00-2.71-.12c-24.92 21-48.05 46.76-69.08 76.92a31.92 31.92 0 00-.64 35.54c26.41 41.33 60.4 76.14 98.28 100.65C162 402 207.9 416 255.66 416a239.13 239.13 0 0075.8-12.58 2 2 0 00.77-3.31l-21.58-21.58a4 4 0 00-3.83-1 204.8 204.8 0 01-51.16 6.47zM490.84 238.6c-26.46-40.92-60.79-75.68-99.27-100.53C349 110.55 302 96 255.66 96a227.34 227.34 0 00-74.89 12.83 2 2 0 00-.75 3.31l21.55 21.55a4 4 0 003.88 1 192.82 192.82 0 0150.21-6.69c40.69 0 80.58 12.43 118.55 37 34.71 22.4 65.74 53.88 89.76 91a.13.13 0 010 .16 310.72 310.72 0 01-64.12 72.73 2 2 0 00-.15 2.95l19.9 19.89a2 2 0 002.7.13 343.49 343.49 0 0068.64-78.48 32.2 32.2 0 00-.1-34.78z' />
<path d='M256 160a95.88 95.88 0 00-21.37 2.4 2 2 0 00-1 3.38l112.59 112.56a2 2 0 003.38-1A96 96 0 00256 160zM165.78 233.66a2 2 0 00-3.38 1 96 96 0 00115 115 2 2 0 001-3.38z' />
@@ -1,4 +1,6 @@
const HeartIcon = (props) => {
import { SVGProps } from 'react';
const HeartIcon = (props?: SVGProps<SVGSVGElement>) => {
return (
<svg
xmlns='http://www.w3.org/2000/svg'
+3 -1
View File
@@ -1,4 +1,6 @@
const Loader = (props) => {
import { SVGProps } from 'react';
const Loader = (props?: SVGProps<SVGSVGElement>) => {
return (
<svg
viewBox='0 0 57 57'
+20
View File
@@ -0,0 +1,20 @@
import { SVGProps } from 'react';
export const Logout = (props?: SVGProps<SVGSVGElement>) => {
return (
<svg
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 512 512'
{...props}
>
<path
d='M304 336v40a40 40 0 01-40 40H104a40 40 0 01-40-40V136a40 40 0 0140-40h152c22.09 0 48 17.91 48 40v40M368 336l80-80-80-80M176 256h256'
fill='none'
stroke='currentColor'
strokeLinecap='round'
strokeLinejoin='round'
strokeWidth='32'
/>
</svg>
);
};
+3 -1
View File
@@ -1,4 +1,6 @@
const MapIcon = (props) => {
import { SVGProps } from 'react';
const MapIcon = (props?: SVGProps<SVGSVGElement>) => {
return (
<svg
xmlns='http://www.w3.org/2000/svg'
@@ -1,4 +1,6 @@
const SettingsIcon = (props) => {
import { SVGProps } from 'react';
const SettingsIcon = (props?: SVGProps<SVGSVGElement>) => {
return (
<svg
xmlns='http://www.w3.org/2000/svg'
@@ -1,4 +1,6 @@
const StatsIcon = (props) => {
import { SVGProps } from 'react';
const StatsIcon = (props?: SVGProps<SVGSVGElement>) => {
return (
<svg
xmlns='http://www.w3.org/2000/svg'
+37
View File
@@ -0,0 +1,37 @@
import { SVGProps } from 'react';
const TrashIcon = (props: SVGProps<SVGSVGElement>) => {
return (
<svg
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 512 512'
{...props}
>
<path
d='M112 112l20 320c.95 18.49 14.4 32 32 32h184c17.67 0 30.87-13.51 32-32l20-320'
fill='none'
stroke='currentColor'
strokeLinecap='round'
strokeLinejoin='round'
strokeWidth='32'
/>
<path
stroke='currentColor'
strokeLinecap='round'
stroke-miterlimit='10'
strokeWidth='32'
d='M80 112h352'
/>
<path
d='M192 112V72h0a23.93 23.93 0 0124-24h80a23.93 23.93 0 0124 24h0v40M256 176v224M184 176l8 224M328 176l-8 224'
fill='none'
stroke='currentColor'
strokeLinecap='round'
strokeLinejoin='round'
strokeWidth='32'
/>
</svg>
);
};
export default TrashIcon;
+80
View File
@@ -0,0 +1,80 @@
'use client';
// ---------------------------------------------------------------------------------------------------------------------
//! Imports
// ---------------------------------------------------------------------------------------------------------------------
// ------------------------------------------------------ React --------------------------------------------------------
import { FormEvent } from 'react';
// ---------------------------------------------------------------------------------------------------------------------
// ------------------------------------------------- Assets & Styles ---------------------------------------------------
import CheckIcon from '@/assets/CheckIcon';
import CloseIcon from '@/assets/CloseIcon';
import './styles.scss';
// ---------------------------------------------------------------------------------------------------------------------
const InlineInput = ({
label = '',
name = '',
type = 'text',
placeholder = '',
defaultValue = '',
autoComplete = 'off',
onSubmit = (value?: string) => {},
}) => {
const handleSubmit = (ev: FormEvent<HTMLFormElement>) => {
ev.preventDefault();
if (!ev.currentTarget.reportValidity()) {
return;
}
const formData = new FormData(ev.currentTarget);
const value = formData.get(name);
onSubmit(value ? value.toString() : null);
};
return (
<form
className='inlineInputContainer'
onSubmit={handleSubmit}
>
<label
htmlFor={name}
className='inlineInputLabel'
>
{label}
</label>
<div className='inlineInputRow'>
<input
type={type}
name={name}
id={name}
defaultValue={defaultValue}
placeholder={placeholder}
autoComplete={autoComplete}
className='inlineInput'
required
/>
<button
type='reset'
className='inlineInputButton'
>
<CloseIcon />
</button>
<button
type='submit'
className='inlineInputButton'
>
<CheckIcon />
</button>
</div>
</form>
);
};
export default InlineInput;
+50
View File
@@ -0,0 +1,50 @@
.inlineInputContainer {
display: flex;
flex-direction: column;
gap: 5px;
& > .inlineInputLabel {
margin-left: 5px;
font-size: 1.1rem;
color: #d94253;
}
& > .inlineInputRow {
display: grid;
grid-template-columns: 1fr repeat(2, calc(1rem + 14px));
align-items: center;
gap: 5px;
& > .inlineInput {
padding: 7px 14px;
border: 1px solid #d94253;
border-radius: 5px;
background-color: #f5cfd3;
outline: none;
font-size: 0.9rem;
color: #d94253;
outline: none;
&::placeholder {
color: #d94253;
}
}
& > .inlineInputButton {
display: flex;
justify-content: center;
align-items: center;
padding: 5px;
border: none;
border-radius: 5px;
background-color: transparent;
color: #d94253;
cursor: pointer;
transition: background-color 0.3s ease;
&:hover {
background-color: rgba(#d94253, 0.5);
}
}
}
}
@@ -5,7 +5,7 @@
// ---------------------------------------------------------------------------------------------------------------------
// ------------------------------------------------------ React --------------------------------------------------------
import { forwardRef, useState } from 'react';
import { forwardRef, MouseEvent, useState } from 'react';
// ---------------------------------------------------------------------------------------------------------------------
// ------------------------------------------------- Assets & Styles ---------------------------------------------------
@@ -14,75 +14,51 @@ import EyeIcon from '@/assets/EyeIcon';
import './styles.scss';
// ---------------------------------------------------------------------------------------------------------------------
const Input = forwardRef(function InputComponent(
{
type = 'text',
label,
name,
value,
defaultValue,
autoComplete,
required = false,
onChange,
onKeyUp,
onKeyDown,
},
ref
) {
const [isFocused, setIsFocused] = useState(
defaultValue || value ? true : false
);
const [passwordVisibility, setPasswordVisibility] = useState(false);
const Input = ({
name = '',
type = '',
placeholder = '',
defaultValue = '',
required = false,
autoComplete = 'off',
}) => {
const [isPasswordVisible, setIsPasswordVisible] = useState(false);
const defaultOnBlur = (ev) => {
if (ev.currentTarget.value != '') {
return;
}
const togglePasswordVisibility = (ev: MouseEvent) => {
ev.stopPropagation();
setIsFocused(false);
setIsPasswordVisible((prev) => !prev);
};
return (
<div className='inputContainer'>
<input
ref={ref}
type={
type == 'password'
? passwordVisibility
? isPasswordVisible
? 'text'
: 'password'
: type
}
name={name}
id={name}
value={value}
placeholder={placeholder}
defaultValue={defaultValue}
autoComplete={autoComplete}
required={required}
onChange={onChange}
onKeyUp={onKeyUp}
onKeyDown={onKeyDown}
className='input'
onFocus={() => setIsFocused(true)}
onBlur={defaultOnBlur}
/>
<label
htmlFor={name}
className={`inputLabel ${isFocused ? 'focused' : ''}`}
>
{label}
</label>
{type == 'password' && (
<button
type='button'
className='passwordToggleButton'
onClick={() => setPasswordVisibility((prev) => !prev)}
onClick={togglePasswordVisibility}
>
{passwordVisibility ? <EyeSlashIcon /> : <EyeIcon />}
{isPasswordVisible ? <EyeSlashIcon /> : <EyeIcon />}
</button>
)}
</div>
);
});
};
export default Input;
+10 -26
View File
@@ -2,35 +2,19 @@
position: relative;
font-size: 1.1rem;
& > .inputLabel {
position: absolute;
top: 48%;
left: 10px;
transform: translateY(-50%);
padding: 0 4px;
background-color: #d94253;
font-size: 1em;
font-weight: 300;
color: #ffffff;
transition: top 0.3s ease, font-size 0.3s ease;
pointer-events: none;
&.focused {
top: 0;
font-size: 0.8em;
}
}
& > .input {
padding: 10px 20px;
padding: 7px 14px;
width: 100%;
border: 1px solid #ffffff;
border: 1px solid #d94253;
border-radius: 5px;
background-color: #d94253;
font-size: 1em;
font-weight: 300;
color: #ffffff;
background-color: #f5cfd3;
font-size: 0.9rem;
color: #d94253;
outline: none;
&::placeholder {
color: #d94253;
}
}
& > .passwordToggleButton {
@@ -45,6 +29,6 @@
height: 100%;
border: none;
background-color: transparent;
color: #ffffff;
color: #d94253;
}
}
-34
View File
@@ -1,34 +0,0 @@
'use client';
import Link from 'next/link';
import config from './config';
import './styles.scss';
import { usePathname } from 'next/navigation';
const Navbar = () => {
const pathname = usePathname();
return (
<nav
className='navbar'
style={{
gridTemplateColumns: `repeat(${config.length}, 1fr)`,
}}
>
{config.map((element) => (
<Link
key={element.title}
href={element.link}
className={`navButton ${
element.link == pathname ? 'selected' : ''
}`}
>
{element.icon()}
<span className='navButtonLabel'>{element.title}</span>
</Link>
))}
</nav>
);
};
export default Navbar;
+46
View File
@@ -0,0 +1,46 @@
'use client';
// ---------------------------------------------------------------------------------------------------------------------
//! Imports
// ---------------------------------------------------------------------------------------------------------------------
// --------------------------------------------------- Components ------------------------------------------------------
import Link from 'next/link';
// ---------------------------------------------------------------------------------------------------------------------
// -------------------------------------------------- Hooks & Utils ----------------------------------------------------
import { usePathname } from 'next/navigation';
import config from './config';
// ---------------------------------------------------------------------------------------------------------------------
// ------------------------------------------------- Assets & Styles ---------------------------------------------------
import './styles.scss';
// ---------------------------------------------------------------------------------------------------------------------
const Navbar = () => {
const pathname = usePathname();
return (
<nav
className='navbar'
style={{
gridTemplateColumns: `repeat(${config.length}, 1fr)`,
}}
>
{config.map((element) => (
<Link
key={element.title}
href={element.link}
className={`navButton ${
element.link == pathname ? 'selected' : ''
}`}
>
{element.icon()}
<span className='navButtonLabel'>{element.title}</span>
</Link>
))}
</nav>
);
};
export default Navbar;
-7
View File
@@ -1,7 +0,0 @@
{
"compilerOptions": {
"paths": {
"@/*": ["./*"]
}
}
}
+528 -10
View File
@@ -11,6 +11,8 @@
"@capacitor/android": "^6.1.2",
"@capacitor/cli": "^6.1.2",
"@capacitor/core": "^6.1.2",
"@vercel/postgres": "^0.10.0",
"bcrypt": "^5.1.1",
"deck.gl": "^9.0.28",
"mapbox-gl": "^3.6.0",
"next": "14.2.7",
@@ -20,6 +22,8 @@
"react-slot-counter": "^3.0.1"
},
"devDependencies": {
"@types/bcrypt": "^5.0.2",
"@types/react": "18.3.10",
"eslint": "^8",
"eslint-config-next": "14.2.7",
"sass": "^1.77.8"
@@ -1369,6 +1373,26 @@
"integrity": "sha512-7hFhtkb0KTLEls+TRw/rWayq5EeHtTaErgm/NskVoXmtgAQu/9D299aeyj6mzAR/6XUnYRp2lU+4IcrYRFjVsQ==",
"license": "ISC"
},
"node_modules/@mapbox/node-pre-gyp": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz",
"integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==",
"license": "BSD-3-Clause",
"dependencies": {
"detect-libc": "^2.0.0",
"https-proxy-agent": "^5.0.0",
"make-dir": "^3.1.0",
"node-fetch": "^2.6.7",
"nopt": "^5.0.0",
"npmlog": "^5.0.1",
"rimraf": "^3.0.2",
"semver": "^7.3.5",
"tar": "^6.1.11"
},
"bin": {
"node-pre-gyp": "bin/node-pre-gyp"
}
},
"node_modules/@mapbox/point-geometry": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/@mapbox/point-geometry/-/point-geometry-0.1.0.tgz",
@@ -1487,6 +1511,15 @@
"integrity": "sha512-eJ0nDw8140kJorf8ASyKRC53rI+UG6vPxpsKJiGRD6lXsoKTeKYebeEAXiGDWTvi2AMe6+xngxTqqwm58fL3Fw==",
"license": "MIT"
},
"node_modules/@neondatabase/serverless": {
"version": "0.9.5",
"resolved": "https://registry.npmjs.org/@neondatabase/serverless/-/serverless-0.9.5.tgz",
"integrity": "sha512-siFas6gItqv6wD/pZnvdu34wEqgG3nSE6zWZdq5j2DEsa+VvX8i/5HXJOo06qrw5axPXn+lGCxeR+NLaSPIXug==",
"license": "MIT",
"dependencies": {
"@types/pg": "8.11.6"
}
},
"node_modules/@next/env": {
"version": "14.2.7",
"resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.7.tgz",
@@ -1837,6 +1870,16 @@
"@turf/meta": "^5.1.5"
}
},
"node_modules/@types/bcrypt": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-5.0.2.tgz",
"integrity": "sha512-6atioO8Y75fNcbmj0G7UjI9lXN2pQ/IGJ2FWT4a/btd0Lk9lQalHLKhkgKVZ3r+spnmWUKfbMi1GEe9wyHQfNQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/brotli": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/@types/brotli/-/brotli-1.3.4.tgz",
@@ -1993,6 +2036,35 @@
"integrity": "sha512-j3pOPiEcWZ34R6a6mN07mUkM4o4Lwf6hPNt8eilOeZhTFbxFXmKhvXl9Y28jotFPaI1bpPDJsbCprUoNke6OrA==",
"license": "MIT"
},
"node_modules/@types/pg": {
"version": "8.11.6",
"resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.11.6.tgz",
"integrity": "sha512-/2WmmBXHLsfRqzfHW7BNZ8SbYzE8OSk7i3WjFYvfgRHj7S1xj+16Je5fUKv3lVdVzk/zn9TXOqf+avFCFIE0yQ==",
"license": "MIT",
"dependencies": {
"@types/node": "*",
"pg-protocol": "*",
"pg-types": "^4.0.1"
}
},
"node_modules/@types/prop-types": {
"version": "15.7.13",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz",
"integrity": "sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/react": {
"version": "18.3.10",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.10.tgz",
"integrity": "sha512-02sAAlBnP39JgXwkAq3PeU9DVaaGpZyF3MGcC0MKgQVkZor5IiiDAipVaxQHtDJAmO4GIy/rVBy/LzVj76Cyqg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/prop-types": "*",
"csstype": "^3.0.2"
}
},
"node_modules/@types/slice-ansi": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@types/slice-ansi/-/slice-ansi-4.0.0.tgz",
@@ -2348,6 +2420,20 @@
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
}
},
"node_modules/@vercel/postgres": {
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/@vercel/postgres/-/postgres-0.10.0.tgz",
"integrity": "sha512-fSD23DxGND40IzSkXjcFcxr53t3Tiym59Is0jSYIFpG4/0f0KO9SGtcp1sXiebvPaGe7N/tU05cH4yt2S6/IPg==",
"license": "Apache-2.0",
"dependencies": {
"@neondatabase/serverless": "^0.9.3",
"bufferutil": "^4.0.8",
"ws": "^8.17.1"
},
"engines": {
"node": ">=18.14"
}
},
"node_modules/@webcomponents/shadycss": {
"version": "1.11.2",
"resolved": "https://registry.npmjs.org/@webcomponents/shadycss/-/shadycss-1.11.2.tgz",
@@ -2376,6 +2462,12 @@
"node": ">=16.5.0"
}
},
"node_modules/abbrev": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
"license": "ISC"
},
"node_modules/acorn": {
"version": "8.12.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz",
@@ -2399,6 +2491,18 @@
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
}
},
"node_modules/agent-base": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
"license": "MIT",
"dependencies": {
"debug": "4"
},
"engines": {
"node": ">= 6.0.0"
}
},
"node_modules/ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
@@ -2454,6 +2558,40 @@
"node": ">= 8"
}
},
"node_modules/aproba": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz",
"integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==",
"license": "ISC"
},
"node_modules/are-we-there-yet": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz",
"integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==",
"deprecated": "This package is no longer supported.",
"license": "ISC",
"dependencies": {
"delegates": "^1.0.0",
"readable-stream": "^3.6.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/are-we-there-yet/node_modules/readable-stream": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"license": "MIT",
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
@@ -2744,6 +2882,20 @@
],
"license": "MIT"
},
"node_modules/bcrypt": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz",
"integrity": "sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"@mapbox/node-pre-gyp": "^1.0.11",
"node-addon-api": "^5.0.0"
},
"engines": {
"node": ">= 10.0.0"
}
},
"node_modules/big-integer": {
"version": "1.6.52",
"resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz",
@@ -2782,7 +2934,6 @@
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
@@ -2830,6 +2981,19 @@
"node": "*"
}
},
"node_modules/bufferutil": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.8.tgz",
"integrity": "sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw==",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"node-gyp-build": "^4.3.0"
},
"engines": {
"node": ">=6.14.2"
}
},
"node_modules/busboy": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
@@ -3047,6 +3211,15 @@
"simple-swizzle": "^0.2.2"
}
},
"node_modules/color-support": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz",
"integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==",
"license": "ISC",
"bin": {
"color-support": "bin.js"
}
},
"node_modules/colorbrewer": {
"version": "1.5.6",
"resolved": "https://registry.npmjs.org/colorbrewer/-/colorbrewer-1.5.6.tgz",
@@ -3079,9 +3252,14 @@
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
"dev": true,
"license": "MIT"
},
"node_modules/console-control-strings": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
"integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==",
"license": "ISC"
},
"node_modules/core-assert": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/core-assert/-/core-assert-0.2.1.tgz",
@@ -3137,6 +3315,13 @@
"license": "MIT",
"peer": true
},
"node_modules/csstype": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"dev": true,
"license": "MIT"
},
"node_modules/d3-array": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
@@ -3447,6 +3632,21 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/delegates": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
"integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==",
"license": "MIT"
},
"node_modules/detect-libc": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz",
"integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==",
"license": "Apache-2.0",
"engines": {
"node": ">=8"
}
},
"node_modules/dir-glob": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
@@ -4508,6 +4708,53 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/gauge": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz",
"integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==",
"deprecated": "This package is no longer supported.",
"license": "ISC",
"dependencies": {
"aproba": "^1.0.3 || ^2.0.0",
"color-support": "^1.1.2",
"console-control-strings": "^1.0.0",
"has-unicode": "^2.0.1",
"object-assign": "^4.1.1",
"signal-exit": "^3.0.0",
"string-width": "^4.2.3",
"strip-ansi": "^6.0.1",
"wide-align": "^1.1.2"
},
"engines": {
"node": ">=10"
}
},
"node_modules/gauge/node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"license": "MIT"
},
"node_modules/gauge/node_modules/signal-exit": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
"license": "ISC"
},
"node_modules/gauge/node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"license": "MIT",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/geojson-vt": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/geojson-vt/-/geojson-vt-4.0.2.tgz",
@@ -4823,6 +5070,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-unicode": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
"integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==",
"license": "ISC"
},
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
@@ -4836,6 +5089,19 @@
"node": ">= 0.4"
}
},
"node_modules/https-proxy-agent": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
"integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
"license": "MIT",
"dependencies": {
"agent-base": "6",
"debug": "4"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
@@ -4923,7 +5189,6 @@
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
"deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
"dev": true,
"license": "ISC",
"dependencies": {
"once": "^1.3.0",
@@ -5821,6 +6086,30 @@
"integrity": "sha512-VKlnoJRFrB8SdJhlVKvW5vI1gGwcZ+mvChEXcSX6r2xDNc/Q2FD9esfBmGCuPZdrJ1feO+YcVFd2PTk0c137Gw==",
"license": "BSD-2-Clause"
},
"node_modules/make-dir": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
"integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
"license": "MIT",
"dependencies": {
"semver": "^6.0.0"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/make-dir/node_modules/semver": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
}
},
"node_modules/mapbox-gl": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/mapbox-gl/-/mapbox-gl-3.6.0.tgz",
@@ -5924,7 +6213,6 @@
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^1.1.7"
@@ -6135,6 +6423,58 @@
}
}
},
"node_modules/node-addon-api": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz",
"integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==",
"license": "MIT"
},
"node_modules/node-fetch": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
"license": "MIT",
"dependencies": {
"whatwg-url": "^5.0.0"
},
"engines": {
"node": "4.x || >=6.0.0"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
}
},
"node_modules/node-gyp-build": {
"version": "4.8.2",
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.2.tgz",
"integrity": "sha512-IRUxE4BVsHWXkV/SFOut4qTlagw2aM8T5/vnTsmrHJvVoKueJHRc/JaFND7QDDc61kLYUJ6qlZM3sqTSyx2dTw==",
"license": "MIT",
"bin": {
"node-gyp-build": "bin.js",
"node-gyp-build-optional": "optional.js",
"node-gyp-build-test": "build-test.js"
}
},
"node_modules/nopt": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz",
"integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==",
"license": "ISC",
"dependencies": {
"abbrev": "1"
},
"bin": {
"nopt": "bin/nopt.js"
},
"engines": {
"node": ">=6"
}
},
"node_modules/normalize-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
@@ -6145,11 +6485,23 @@
"node": ">=0.10.0"
}
},
"node_modules/npmlog": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz",
"integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==",
"deprecated": "This package is no longer supported.",
"license": "ISC",
"dependencies": {
"are-we-there-yet": "^2.0.0",
"console-control-strings": "^1.1.0",
"gauge": "^3.0.0",
"set-blocking": "^2.0.0"
}
},
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
@@ -6281,11 +6633,16 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/obuf": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz",
"integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==",
"license": "MIT"
},
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"dev": true,
"license": "ISC",
"dependencies": {
"wrappy": "1"
@@ -6391,7 +6748,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
@@ -6458,6 +6814,48 @@
"integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==",
"license": "MIT"
},
"node_modules/pg-int8": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
"integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
"license": "ISC",
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/pg-numeric": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/pg-numeric/-/pg-numeric-1.0.2.tgz",
"integrity": "sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw==",
"license": "ISC",
"engines": {
"node": ">=4"
}
},
"node_modules/pg-protocol": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.7.0.tgz",
"integrity": "sha512-hTK/mE36i8fDDhgDFjy6xNOG+LCorxLG3WO17tku+ij6sVHXh1jQUJ8hYAnRhNla4QVD2H8er/FOjc/+EgC6yQ==",
"license": "MIT"
},
"node_modules/pg-types": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/pg-types/-/pg-types-4.0.2.tgz",
"integrity": "sha512-cRL3JpS3lKMGsKaWndugWQoLOCoP+Cic8oseVcbr0qhPzYD5DWXK+RZ9LY9wxRf7RQia4SCwQlXk0q6FCPrVng==",
"license": "MIT",
"dependencies": {
"pg-int8": "1.0.1",
"pg-numeric": "1.0.2",
"postgres-array": "~3.0.1",
"postgres-bytea": "~3.0.0",
"postgres-date": "~2.1.0",
"postgres-interval": "^3.0.0",
"postgres-range": "^1.1.1"
},
"engines": {
"node": ">=10"
}
},
"node_modules/picocolors": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz",
@@ -6529,6 +6927,51 @@
"node": "^10 || ^12 || >=14"
}
},
"node_modules/postgres-array": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-3.0.2.tgz",
"integrity": "sha512-6faShkdFugNQCLwucjPcY5ARoW1SlbnrZjmGl0IrrqewpvxvhSLHimCVzqeuULCbG0fQv7Dtk1yDbG3xv7Veog==",
"license": "MIT",
"engines": {
"node": ">=12"
}
},
"node_modules/postgres-bytea": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-3.0.0.tgz",
"integrity": "sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw==",
"license": "MIT",
"dependencies": {
"obuf": "~1.1.2"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/postgres-date": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-2.1.0.tgz",
"integrity": "sha512-K7Juri8gtgXVcDfZttFKVmhglp7epKb1K4pgrkLxehjqkrgPhfG6OO8LHLkfaqkbpjNRnra018XwAr1yQFWGcA==",
"license": "MIT",
"engines": {
"node": ">=12"
}
},
"node_modules/postgres-interval": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-3.0.0.tgz",
"integrity": "sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw==",
"license": "MIT",
"engines": {
"node": ">=12"
}
},
"node_modules/postgres-range": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/postgres-range/-/postgres-range-1.1.4.tgz",
"integrity": "sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w==",
"license": "MIT"
},
"node_modules/potpack": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/potpack/-/potpack-2.0.0.tgz",
@@ -6857,7 +7300,6 @@
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
"integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
"deprecated": "Rimraf versions prior to v4 are no longer supported",
"dev": true,
"license": "ISC",
"dependencies": {
"glob": "^7.1.3"
@@ -6874,7 +7316,6 @@
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"deprecated": "Glob versions prior to v9 are no longer supported",
"dev": true,
"license": "ISC",
"dependencies": {
"fs.realpath": "^1.0.0",
@@ -7018,6 +7459,12 @@
"node": ">=4.0.0"
}
},
"node_modules/set-blocking": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
"license": "ISC"
},
"node_modules/set-function-length": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
@@ -7723,6 +8170,12 @@
"node": ">=8.0"
}
},
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
"license": "MIT"
},
"node_modules/tree-kill": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
@@ -7988,12 +8441,28 @@
"pbf": "^3.2.1"
}
},
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
"license": "BSD-2-Clause"
},
"node_modules/wgsl_reflect": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/wgsl_reflect/-/wgsl_reflect-1.0.10.tgz",
"integrity": "sha512-70yzTaAhLFvasD+awEdHmgqKk3Qm2y6CVUU4OiS1rokB/TGi8CFEweUzDIfYn5pt6qhRdi9uArNykKaJy+CnwQ==",
"license": "MIT"
},
"node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"license": "MIT",
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
}
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@@ -8092,6 +8561,35 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/wide-align": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz",
"integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==",
"license": "ISC",
"dependencies": {
"string-width": "^1.0.2 || 2 || 3 || 4"
}
},
"node_modules/wide-align/node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"license": "MIT"
},
"node_modules/wide-align/node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"license": "MIT",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/word-wrap": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
@@ -8207,9 +8705,29 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"dev": true,
"license": "ISC"
},
"node_modules/ws": {
"version": "8.18.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz",
"integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/xml2js": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz",
+4
View File
@@ -12,6 +12,8 @@
"@capacitor/android": "^6.1.2",
"@capacitor/cli": "^6.1.2",
"@capacitor/core": "^6.1.2",
"@vercel/postgres": "^0.10.0",
"bcrypt": "^5.1.1",
"deck.gl": "^9.0.28",
"mapbox-gl": "^3.6.0",
"next": "14.2.7",
@@ -21,6 +23,8 @@
"react-slot-counter": "^3.0.1"
},
"devDependencies": {
"@types/bcrypt": "^5.0.2",
"@types/react": "18.3.10",
"eslint": "^8",
"eslint-config-next": "14.2.7",
"sass": "^1.77.8"
+26
View File
@@ -0,0 +1,26 @@
{
"compilerOptions": {
"paths": {
"@/*": ["./*"]
},
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": false,
"noEmit": true,
"incremental": true,
"module": "esnext",
"esModuleInterop": true,
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"plugins": [
{
"name": "next"
}
]
},
"include": ["next-env.d.ts", ".next/types/**/*.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
}