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;
}
}
}