Display & Delete in love list

This commit is contained in:
2024-10-11 19:08:52 +02:00
parent b702bc629c
commit 1077400260
23 changed files with 644 additions and 523 deletions
@@ -19,11 +19,13 @@ import { useActions } from './hooks/useActions';
// ------------------------------------------------- Assets & Styles --------------------------------------------------- // ------------------------------------------------- Assets & Styles ---------------------------------------------------
import Loader from '@/assets/Loader'; import Loader from '@/assets/Loader';
import './styles.scss'; import './styles.scss';
import Input from '@/components/Input/Input';
// --------------------------------------------------------------------------------------------------------------------- // ---------------------------------------------------------------------------------------------------------------------
const Login = () => { const Login = () => {
const [error, setError] = useState(null); const [error, setError] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const { handleSubmit } = useActions({ setError, setIsLoading }); const { handleSubmit } = useActions({ setError, setIsLoading });
return ( return (
@@ -37,36 +39,29 @@ const Login = () => {
priority priority
/> />
<form <div className='formContainer'>
className='formContainer'
onSubmit={handleSubmit}
>
<h1 className='formTitle'>Connexion</h1> <h1 className='formTitle'>Connexion</h1>
<div className='form'> <form
className='form'
onSubmit={handleSubmit}
>
{error && <p className='error'>{error}</p>} {error && <p className='error'>{error}</p>}
<div className='inputContainer'> <Input
<input name='username'
type='text' type='text'
name='username' placeholder="Nom d'utilisateur"
className='input' autoComplete='username'
required required
/> />
<label className='inputLabel'> <Input
Nom d&apos;utilisateur type='password'
</label> name='password'
</div> placeholder='Mot de passe'
<div className='inputContainer'> autoComplete='password'
<input required
type='password' />
name='password'
className='input'
required
/>
<label className='inputLabel'>Mot de passe</label>
</div>
</div>
<div className='buttonsContainer'>
<button <button
type='submit' type='submit'
className='submitButton' className='submitButton'
@@ -75,8 +70,8 @@ const Login = () => {
Se connecter Se connecter
{isLoading && <Loader />} {isLoading && <Loader />}
</button> </button>
</div> </form>
</form> </div>
</div> </div>
); );
}; };
+18 -55
View File
@@ -20,69 +20,31 @@
flex-direction: column; flex-direction: column;
align-items: stretch; align-items: stretch;
gap: 10px; gap: 10px;
padding: 20px 30px;
width: 95svw; width: 95svw;
max-width: 450px; max-width: 450px;
border: 2px solid #fefefa;
border-radius: 7px;
background-color: #d94253;
& > .formTitle { & > .formTitle {
margin-bottom: 20px;
font-size: 2rem; font-size: 2rem;
font-weight: 300; font-weight: 300;
text-align: center; text-align: center;
color: #fefefa; color: #d94253;
} }
& > .form { & > .form {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: stretch; align-items: stretch;
gap: 20px; gap: 15px;
padding: 20px;
border: 1px solid #d94253;
border-radius: 10px;
background-color: #f9e2e5;
& > .error { & > .error {
text-align: center; text-align: center;
color: #fefefa; color: #fefefa;
} }
& > .inputContainer {
position: relative;
display: grid;
& > .inputLabel {
position: absolute;
top: 50%;
left: 15px;
transform: translateY(-50%);
padding: 5px;
background-color: #d94253;
color: #fefefa;
user-select: none;
pointer-events: none;
transition: top 0.3s ease, left 0.3s ease,
font-size 0.3s ease, transform 0.3s ease;
}
& > .input {
padding: 10px;
border: 1px solid #fefefa;
border-radius: 5px;
background-color: #d94253;
outline: none;
font-size: 1em;
color: #fefefa;
&:focus + .inputLabel,
&:not(:placeholder-shown) + .inputLabel {
top: 0;
left: 10px;
transform: translateY(-50%);
font-size: 0.9em;
}
}
}
& > .forgetLink { & > .forgetLink {
position: relative; position: relative;
width: max-content; width: max-content;
@@ -106,28 +68,29 @@
width: 100%; width: 100%;
} }
} }
}
& > .buttonsContainer {
display: flex;
justify-content: center;
align-items: center;
gap: 30px;
margin-top: 15px;
& > .submitButton { & > .submitButton {
align-self: center;
position: relative; position: relative;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
gap: 10px; gap: 10px;
padding: 7px 42px; padding: 7px 42px;
border: 1px solid #fefefa; border: 1px solid #e1e1e1;
border-radius: 5px; border-radius: 5px;
background-color: #fefefa; background-color: #ffffff;
font-size: 0.9rem; font-size: 1rem;
color: #d94253; color: #d94253;
cursor: pointer; cursor: pointer;
transition: border-color 0.3s ease, background-color 0.3s ease,
color 0.3s ease;
&:hover {
border-color: #d94253;
background-color: #d94253;
color: #ffffff;
}
& > svg { & > svg {
position: absolute; position: absolute;
+1 -1
View File
@@ -18,7 +18,7 @@ body {
flex-direction: column; flex-direction: column;
width: 100svw; width: 100svw;
height: 100svh; height: 100svh;
background-color: #ffffff; background-color: #fbeced;
overflow: hidden; overflow: hidden;
} }
+30 -35
View File
@@ -1,12 +1,25 @@
// ---------------------------------------------------------------------------------------------------------------------
//! Imports
// ---------------------------------------------------------------------------------------------------------------------
// -------------------------------------------------- React & Next -----------------------------------------------------
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
// ---------------------------------------------------------------------------------------------------------------------
// --------------------------------------------------- Components ------------------------------------------------------
import { IconLayer, ScatterplotLayer } from 'deck.gl'; import { IconLayer, ScatterplotLayer } from 'deck.gl';
// ---------------------------------------------------------------------------------------------------------------------
// ------------------------------------------------- Assets & Styles ---------------------------------------------------
import { StringifiedPinIcon } from '@/assets/PinIcon'; import { StringifiedPinIcon } from '@/assets/PinIcon';
import { getDataset } from '../request';
// ---------------------------------------------------------------------------------------------------------------------
export const useData = () => { export const useData = () => {
const deckRef = useRef(null); const deckRef = useRef(null);
const [canUseGeolocation, setCanUseGeolocation] = useState(false); const [canUseGeolocation, setCanUseGeolocation] = useState<boolean>(false);
const [geoError, setGeoError] = useState(null); const [geoError, setGeoError] = useState<string | null>(null);
const [userLocation, setUserLocation] = useState<{ [key: string]: number }>( const [userLocation, setUserLocation] = useState<{ [key: string]: number }>(
{} {}
); );
@@ -14,9 +27,9 @@ export const useData = () => {
const [source, setSource] = useState([]); const [source, setSource] = useState([]);
const [layers, setLayers] = useState([]); const [layers, setLayers] = useState([]);
const [isAddLoveOpened, setIsAddLoveOpened] = useState(false); const [isAddLoveOpened, setIsAddLoveOpened] = useState<boolean>(false);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState<boolean>(false);
const [addLoveMessage, setAddLoveMessage] = useState(null); const [addLoveMessage, setAddLoveMessage] = useState<string | null>(null);
const [viewState, setViewState] = useState<any>({ const [viewState, setViewState] = useState<any>({
longitude: 2.209666999999996, longitude: 2.209666999999996,
@@ -61,7 +74,18 @@ export const useData = () => {
(async () => { (async () => {
const dataset = await getDataset(); const dataset = await getDataset();
setSource(dataset); const formattedDataset = dataset.map((point) => {
const properties = { ...point };
delete properties.geopoint;
return {
type: 'Feature',
geometry: point.geopoint,
properties: properties,
};
});
setSource(formattedDataset);
})(); })();
}, []); }, []);
@@ -127,32 +151,3 @@ export const useData = () => {
setAddLoveMessage, setAddLoveMessage,
}; };
}; };
const getDataset = async () => {
const token = localStorage.getItem('token');
const request = await fetch('/api/point', {
method: 'GET',
headers: {
Authorization: token,
},
});
const response = await request.json();
if (response) {
const formattedData = response.map((point) => {
const properties = { ...point };
delete properties.geopoint;
return {
type: 'Feature',
geometry: point.geopoint,
properties: properties,
};
});
return formattedData;
}
return [];
};
+54
View File
@@ -0,0 +1,54 @@
export const useActions = ({
loveToDelete,
setLoveToDelete,
setDeleteMessage,
refreshDataset,
}) => {
const handleEdit = (id: string) => {
alert('Fonctionnalité en cours de développement');
};
const openPopup = (id: string) => {
setDeleteMessage(null);
setLoveToDelete(id);
};
const closePopup = () => {
setDeleteMessage(null);
setLoveToDelete(null);
};
const deleteLove = async () => {
const token = localStorage.getItem('token');
try {
const response = await fetch('/api/point', {
method: 'DELETE',
headers: {
Authorization: token,
'Content-Type': 'application/json',
},
body: JSON.stringify({
pointId: loveToDelete,
}),
});
if (response.ok) {
await refreshDataset();
setLoveToDelete(null);
return;
}
} catch (error) {
console.error(error);
}
setDeleteMessage("Une erreur s'est produite. Veuillez réessayer");
};
return {
handleEdit,
openPopup,
closePopup,
deleteLove,
};
};
+45
View File
@@ -0,0 +1,45 @@
// ---------------------------------------------------------------------------------------------------------------------
//! Imports
// ---------------------------------------------------------------------------------------------------------------------
// -------------------------------------------------- React & Next -----------------------------------------------------
import { useEffect, useState } from 'react';
// ---------------------------------------------------------------------------------------------------------------------
// -------------------------------------------------- Hooks & Utils ----------------------------------------------------
import { getDataset } from '@/app/request';
// ---------------------------------------------------------------------------------------------------------------------
export const useData = () => {
const [dataset, setDataset] = useState([]);
const [loveToDelete, setLoveToDelete] = useState<string | null>(null);
const [deleteMessage, setDeleteMessage] = useState<string | null>(null);
useEffect(() => {
refreshDataset();
}, []);
const refreshDataset = async () => {
const data = await getDataset();
const formattedDataset = data.map((d) => {
const formattedDate = new Date(d.createdat).toLocaleString('fr-FR');
return {
...d,
createdat: formattedDate,
};
});
setDataset(formattedDataset);
};
return {
dataset,
loveToDelete,
setLoveToDelete,
deleteMessage,
setDeleteMessage,
refreshDataset,
};
};
-35
View File
@@ -1,35 +0,0 @@
// ---------------------------------------------------------------------------------------------------------------------
//! Imports
// ---------------------------------------------------------------------------------------------------------------------
// ------------------------------------------------- Assets & Styles ---------------------------------------------------
import './styles.scss';
// ---------------------------------------------------------------------------------------------------------------------
const MyLoves = () => {
const data = [];
return (
<div className='myLovesContainer'>
<h1 className='title'>Mes loves</h1>
<div className='myLovesList'>
{data.map((love, index) => (
<div
key={love.id || index}
className='loveItem'
>
<p className='date'>
Le: {love.createdAt || '20/09/2024'}
</p>
<p className='description'>
{love.description || 'Description'}
</p>
</div>
))}
</div>
</div>
);
};
export default MyLoves;
+90
View File
@@ -0,0 +1,90 @@
'use client';
import TrashIcon from '@/assets/TrashIcon';
// ---------------------------------------------------------------------------------------------------------------------
//! Imports
// ---------------------------------------------------------------------------------------------------------------------
// --------------------------------------------------- Components ------------------------------------------------------
import ConfirmPopup from '@/components/ConfirmPopup/ConfirmPopup';
// ---------------------------------------------------------------------------------------------------------------------
// -------------------------------------------------- Hooks & Utils ----------------------------------------------------
import { useData } from './hooks/useData';
import { useActions } from './hooks/useActions';
// ---------------------------------------------------------------------------------------------------------------------
// ------------------------------------------------- Assets & Styles ---------------------------------------------------
import PenIcon from '@/assets/PenIcon';
import './styles.scss';
// ---------------------------------------------------------------------------------------------------------------------
const MyLoves = () => {
const {
dataset,
loveToDelete,
setLoveToDelete,
deleteMessage,
setDeleteMessage,
refreshDataset,
} = useData();
const { handleEdit, openPopup, closePopup, deleteLove } = useActions({
loveToDelete,
setLoveToDelete,
setDeleteMessage,
refreshDataset,
});
return (
<div className='myLovesContainer'>
<div className='header'>
<p className='title'>Mes loves</p>
</div>
<div className='myLovesList'>
{dataset.map((love) => (
<div
key={love.id}
className='loveItem'
>
<div className='infos'>
<p className='location'>{love.location}</p>
<p className='comment'>{love.comment || '-'}</p>
<p className='date'>Le: {love.createdat}</p>
</div>
<div className='actionButtons'>
<button
type='button'
className='actionButton'
onClick={() => handleEdit(love.id)}
>
<PenIcon />
</button>
<button
type='button'
className='actionButton'
onClick={() => openPopup(love.id)}
>
<TrashIcon />
</button>
</div>
</div>
))}
</div>
{loveToDelete && (
<ConfirmPopup
title='Supprimer ce love'
content='Es-tu sûr de vouloir supprimer ce love ?'
onClose={closePopup}
message={deleteMessage}
onAccept={deleteLove}
/>
)}
</div>
);
};
export default MyLoves;
+61 -15
View File
@@ -2,8 +2,21 @@
flex-grow: 1; flex-grow: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 10px;
padding: 10px; & > .header {
display: flex;
flex-wrap: nowrap;
align-items: center;
gap: 20px;
padding: 20px;
width: 100%;
background-color: #d94253;
color: #ffffff;
& > .title {
font-size: 1.2rem;
}
}
& > .title { & > .title {
font-size: 1.5rem; font-size: 1.5rem;
@@ -16,24 +29,57 @@
flex-direction: column; flex-direction: column;
& > .loveItem { & > .loveItem {
display: flex; $buttonSize: 30px;
flex-direction: column; display: grid;
gap: 5px; grid-template-columns: 1fr $buttonSize;
padding: 10px; padding: 10px;
background-color: #d94253; border-bottom: 1px solid #d94253;
background-color: #f9e2e5;
&:not(:last-child) { & > .infos {
border-bottom: 2px solid #d80135; display: flex;
flex-direction: column;
gap: 5px;
& > .location {
font-size: 1.1rem;
color: #d94253;
}
& > .comment {
color: #d94253;
}
& > .date {
font-size: 0.9rem;
font-weight: 300;
color: #d94253;
}
} }
& > .date { & > .actionButtons {
font-size: 1.1rem; display: flex;
color: #ffffff; flex-direction: column;
} gap: 10px;
& > .description { & > .actionButton {
font-weight: 300; display: flex;
color: #ffffff; justify-content: center;
align-items: center;
padding: 5px;
width: $buttonSize;
height: $buttonSize;
border: none;
border-radius: 50%;
background-color: transparent;
color: #d94253;
transition: background-color 0.3s ease, color 0.3s ease;
&:hover {
background-color: #d94253;
color: #ffffff;
}
}
} }
} }
} }
+17
View File
@@ -0,0 +1,17 @@
export const getDataset = async () => {
const token = localStorage.getItem('token');
const request = await fetch('/api/point', {
method: 'GET',
headers: {
Authorization: token,
},
});
const response = await request.json();
if (response) {
return response;
}
return [];
};
+10 -2
View File
@@ -20,10 +20,18 @@ export const useActions = ({
setPasswordMessage(null); setPasswordMessage(null);
}; };
const updateUsername = async (value: string) => { const updateUsername = async (ev: FormEvent<HTMLFormElement>) => {
resetMessage(); resetMessage();
if (value == userData.username) { if (!ev.currentTarget.reportValidity()) {
return;
}
const formData = new FormData(ev.currentTarget);
const value = formData.get('username').toString();
if (value == userData.username || value.length <= 1) {
return; return;
} }
+57 -85
View File
@@ -7,7 +7,7 @@
// --------------------------------------------------- Components ------------------------------------------------------ // --------------------------------------------------- Components ------------------------------------------------------
import Link from 'next/link'; import Link from 'next/link';
import Input from '@/components/Input/Input'; import Input from '@/components/Input/Input';
import InlineInput from '@/components/InlineInput/InlineInput'; import ConfirmPopup from '@/components/ConfirmPopup/ConfirmPopup';
// --------------------------------------------------------------------------------------------------------------------- // ---------------------------------------------------------------------------------------------------------------------
// -------------------------------------------------- Hooks & Utils ---------------------------------------------------- // -------------------------------------------------- Hooks & Utils ----------------------------------------------------
@@ -18,8 +18,6 @@ import { useActions } from './hooks/useActions';
// ------------------------------------------------- Assets & Styles --------------------------------------------------- // ------------------------------------------------- Assets & Styles ---------------------------------------------------
import ArrowRightIcon from '@/assets/ArrowRightIcon'; import ArrowRightIcon from '@/assets/ArrowRightIcon';
import TrashIcon from '@/assets/TrashIcon'; import TrashIcon from '@/assets/TrashIcon';
import CloseIcon from '@/assets/CloseIcon';
import CheckIcon from '@/assets/CheckIcon';
import './styles.scss'; import './styles.scss';
// --------------------------------------------------------------------------------------------------------------------- // ---------------------------------------------------------------------------------------------------------------------
@@ -72,44 +70,18 @@ const Settings = () => {
<p className='message'>{usernameMessage}</p> <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 <form
className='passwordForm' className='card'
onSubmit={updatePassword} onSubmit={updateUsername}
> >
{passwordMessage && ( <p className='cardTitle'>Nom d&apos;utilisateur</p>
<p className='message'>{passwordMessage}</p>
)}
<Input <Input
type='password' type='text'
name='currentPassword' name='username'
placeholder='Mot de passe actuel' placeholder="Nom d'utilisateur"
autoComplete='password' defaultValue={userData.username}
required autoComplete='username'
/>
<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 required
/> />
@@ -121,7 +93,47 @@ const Settings = () => {
</button> </button>
</form> </form>
<div className='separator'></div> <form
className='card'
onSubmit={updatePassword}
>
<p className='cardTitle'>Modifier le mot de passe</p>
{passwordMessage && (
<p className='message'>{passwordMessage}</p>
)}
<div className='inputsContainer'>
<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
/>
</div>
<button
type='submit'
className='submitButton'
>
Appliquer
</button>
</form>
<button <button
className='deleteButton' className='deleteButton'
@@ -132,52 +144,12 @@ const Settings = () => {
</button> </button>
{isPopupDisplayed && ( {isPopupDisplayed && (
<div <ConfirmPopup
className='popupOverlay' title='Supprimer le compte ?'
onClick={handleClosePopup} content='Es-tu sûr de vouloir supprimer ton compte ? Cette action est irréversible.'
> onClose={handleClosePopup}
<div onAccept={deleteUser}
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>
</div> </div>
+34 -107
View File
@@ -9,11 +9,11 @@
display: grid; display: grid;
grid-template-columns: 25px 1fr; grid-template-columns: 25px 1fr;
align-items: center; align-items: center;
gap: 15px; gap: 10px;
padding: 15px; padding: 15px;
width: 100%;
background-color: #d94253; background-color: #d94253;
color: #ffffff; color: #ffffff;
box-shadow: 0 0 6px rgba(255, 255, 255, 0.2);
& > .backButton { & > .backButton {
display: flex; display: flex;
@@ -27,8 +27,6 @@
} }
& > .settingsContainer { & > .settingsContainer {
flex-grow: 1;
position: relative;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 20px; gap: 20px;
@@ -43,34 +41,43 @@
color: #ffffff; color: #ffffff;
} }
& > .separator { & > .card {
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; display: flex;
flex-direction: column; flex-direction: column;
gap: 10px; gap: 5px;
padding: 10px;
border: 1px solid #d94253;
border-radius: 10px;
background-color: #f9e2e5;
& > .cardTitle {
font-size: 1rem;
color: #d94253;
}
& > .inputsContainer {
display: flex;
flex-direction: column;
gap: 5px;
padding: 15px 0;
}
& > .submitButton { & > .submitButton {
padding: 7px 21px; padding: 7px 21px;
border: none; border: 1px solid #e1e1e1;
border-radius: 5px; border-radius: 5px;
background-color: #d94253; background-color: #ffffff;
font-size: 1rem; font-size: 1rem;
color: #ffffff; color: #d94253;
cursor: pointer; cursor: pointer;
transition: border-color 0.3s ease, background-color 0.3s ease,
color 0.3s ease;
&:hover {
border-color: #d94253;
background-color: #d94253;
color: #ffffff;
}
} }
} }
@@ -81,92 +88,12 @@
align-items: center; align-items: center;
gap: 10px; gap: 10px;
padding: 7px 21px; padding: 7px 21px;
border: 1px solid #d94253; border: none;
border-radius: 5px; border-radius: 5px;
background-color: #d94253; background-color: #f9e2e5;
font-size: 1rem; font-size: 1rem;
color: #ffffff; color: #d94253;
cursor: pointer; 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;
}
}
}
}
} }
} }
@@ -1,6 +1,7 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
export const useData = () => { export const useData = () => {
const [isLoading, setIsLoading] = useState(true);
const [userData, setUserData] = useState(null); const [userData, setUserData] = useState(null);
useEffect(() => { useEffect(() => {
@@ -23,11 +24,13 @@ export const useData = () => {
), ),
}; };
setIsLoading(false);
setUserData(formattedResponse); setUserData(formattedResponse);
})(); })();
}, []); }, []);
return { return {
isLoading,
userData, userData,
}; };
}; };
+6 -2
View File
@@ -22,7 +22,7 @@ import './styles.scss';
const Statistics = () => { const Statistics = () => {
const router = useRouter(); const router = useRouter();
const { userData } = useData(); const { isLoading, userData } = useData();
if (!userData) { if (!userData) {
return; return;
@@ -33,6 +33,10 @@ const Statistics = () => {
router.push('/auth/login'); router.push('/auth/login');
}; };
if (isLoading) {
return <></>;
}
return ( return (
<div className='accountContainer'> <div className='accountContainer'>
<div className='header'> <div className='header'>
@@ -57,7 +61,7 @@ const Statistics = () => {
</Link> </Link>
</div> </div>
</div> </div>
<div className='separator' />
<div className='statistics'> <div className='statistics'>
<div className='statistic'> <div className='statistic'>
<SlotCounter <SlotCounter
+9 -20
View File
@@ -13,6 +13,7 @@
gap: 20px; gap: 20px;
padding: 20px; padding: 20px;
width: 100%; width: 100%;
background-color: #d94253;
& > .infosContainer { & > .infosContainer {
display: flex; display: flex;
@@ -21,13 +22,13 @@
& > .username { & > .username {
font-size: 1.1rem; font-size: 1.1rem;
color: #d94253; color: #ffffff;
} }
& > .sinceDate { & > .sinceDate {
font-size: 0.9rem; font-size: 0.9rem;
font-weight: 300; font-weight: 300;
color: #d94253; color: #e1e1e1;
} }
} }
@@ -43,33 +44,17 @@
height: $size; height: $size;
border: none; border: none;
background-color: transparent; background-color: transparent;
color: #d94253; color: #ffffff;
cursor: pointer; cursor: pointer;
} }
} }
} }
& > .separator {
display: block;
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%
);
}
& > .statistics { & > .statistics {
display: grid; display: grid;
grid-template-columns: repeat(2, 1fr); grid-template-columns: repeat(2, 1fr);
grid-template-rows: repeat(2, 1fr); grid-template-rows: repeat(2, 1fr);
gap: 20px; gap: 15px;
justify-items: center;
align-items: center;
padding: 40px 20px; padding: 40px 20px;
width: 100%; width: 100%;
@@ -77,6 +62,10 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
padding: 20px 10px;
border: 1px solid #d94253;
border-radius: 10px;
background-color: #f9e2e5;
font-weight: 300; font-weight: 300;
color: #d94253; color: #d94253;
+1 -1
View File
@@ -12,7 +12,7 @@ const ArrowRightIcon = (props?: SVGProps<SVGSVGElement>) => {
stroke='currentColor' stroke='currentColor'
strokeLinecap='round' strokeLinecap='round'
strokeLinejoin='round' strokeLinejoin='round'
strokeWidth='48' strokeWidth='36'
d='M184 112l144 144-144 144' d='M184 112l144 144-144 144'
/> />
</svg> </svg>
+16
View File
@@ -0,0 +1,16 @@
import { SVGProps } from 'react';
const PenIcon = (props: SVGProps<SVGSVGElement>) => {
return (
<svg
xmlns='http://www.w3.org/2000/svg'
fill='currentColor'
viewBox='0 0 16 16'
{...props}
>
<path d='M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293zm-9.761 5.175-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325' />
</svg>
);
};
export default PenIcon;
+68
View File
@@ -0,0 +1,68 @@
// ---------------------------------------------------------------------------------------------------------------------
//! Imports
// ---------------------------------------------------------------------------------------------------------------------
// ------------------------------------------------- Assets & Styles ---------------------------------------------------
import CheckIcon from '@/assets/CheckIcon';
import CloseIcon from '@/assets/CloseIcon';
import './styles.scss';
// ---------------------------------------------------------------------------------------------------------------------
type Props = {
title: string;
content: string | JSX.Element;
message?: string;
onClose: (ev: React.MouseEvent<HTMLButtonElement>) => void;
onAccept?: (ev: React.MouseEvent<HTMLButtonElement>) => void;
onDeny?: (ev: React.MouseEvent<HTMLButtonElement>) => void;
};
const ConfirmPopup = ({
title,
content,
message,
onClose,
onAccept,
onDeny = onClose,
}: Props) => {
return (
<div className='popupOverlay'>
<div
className='popup'
onClick={(ev) => ev.stopPropagation()}
>
<div className='popupHeader'>
<p className='popupTitle'>{title}</p>
<button
className='popupCloseButton'
onClick={onClose}
>
<CloseIcon />
</button>
</div>
<div className='popupContent'>{message || content}</div>
<div className='popupButtonsContainer'>
<button
type='button'
className='popupButton'
onClick={onDeny}
>
<CloseIcon />
Non
</button>
<button
type='button'
className='popupButton'
onClick={onAccept}
>
<CheckIcon />
{message ? 'Réessayer' : 'Oui'}
</button>
</div>
</div>
</div>
);
};
export default ConfirmPopup;
+88
View File
@@ -0,0 +1,88 @@
.popupOverlay {
position: absolute;
top: 0;
left: 0;
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
background-color: rgba(#ffffff, 0.2);
backdrop-filter: blur(2px);
& > .popup {
z-index: 1;
position: relative;
display: flex;
flex-direction: column;
margin: 10px;
border: 1px solid #e1e1e1;
border-radius: 10px;
background-color: #ffffff;
overflow: hidden;
& > .popupHeader {
display: grid;
grid-template-columns: 1fr 20px;
align-items: center;
gap: 20px;
padding: 10px;
border-bottom: 1px solid #e1e1e1;
background-color: #f9e2e5;
& > .popupTitle {
font-size: 1.1rem;
color: #d94253;
}
& > .popupCloseButton {
display: flex;
justify-content: center;
align-items: center;
border: none;
border-radius: 5px;
background-color: transparent;
color: #d94253;
cursor: pointer;
}
}
& > .popupContent {
padding: 10px;
font-size: 0.95rem;
font-weight: 300;
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: 1px solid #a8a5a6;
border-radius: 5px;
background-color: #ffffff;
font-size: 1rem;
color: #d94253;
cursor: pointer;
transition: background-color 0.3s ease;
&:first-child {
border: none;
}
&:hover {
background-color: #f9e2e5;
}
}
}
}
}
-80
View File
@@ -1,80 +0,0 @@
'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
@@ -1,50 +0,0 @@
.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);
}
}
}
}
+11 -5
View File
@@ -5,15 +5,21 @@
& > .input { & > .input {
padding: 7px 14px; padding: 7px 14px;
width: 100%; width: 100%;
border: 1px solid #d94253; border: 1px solid #e1e1e1;
border-radius: 5px; border-radius: 5px;
background-color: #f5cfd3; background-color: #ffffff;
font-size: 0.9rem; font-size: 0.95rem;
font-weight: 300;
color: #d94253; color: #d94253;
outline: none; outline: none;
transition: border-color 0.3s ease;
&::placeholder { &::placeholder {
color: #d94253; color: #eca0a9;
}
&:focus {
border-color: #d94253;
} }
} }
@@ -29,6 +35,6 @@
height: 100%; height: 100%;
border: none; border: none;
background-color: transparent; background-color: transparent;
color: #d94253; color: #eca0a9;
} }
} }