mirror of
https://github.com/Sonny93/my-links.git
synced 2025-12-08 22:53:25 +00:00
feat: settings modal add profile and lang selector
This commit is contained in:
118
package-lock.json
generated
118
package-lock.json
generated
@@ -25,6 +25,7 @@
|
||||
"react-i18next": "^13.5.0",
|
||||
"react-icons": "^4.12.0",
|
||||
"react-select": "^5.8.0",
|
||||
"react-tabs": "^6.0.2",
|
||||
"sass": "^1.69.5",
|
||||
"sharp": "^0.32.6",
|
||||
"yup": "^1.3.2"
|
||||
@@ -6969,6 +6970,18 @@
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-tabs": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/react-tabs/-/react-tabs-6.0.2.tgz",
|
||||
"integrity": "sha512-aQXTKolnM28k3KguGDBSAbJvcowOQr23A+CUJdzJtOSDOtTwzEaJA+1U4KwhNL9+Obe+jFS7geuvA7ICQPXOnQ==",
|
||||
"dependencies": {
|
||||
"clsx": "^2.0.0",
|
||||
"prop-types": "^15.5.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-transition-group": {
|
||||
"version": "4.4.5",
|
||||
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
|
||||
@@ -8438,6 +8451,111 @@
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-darwin-arm64": {
|
||||
"version": "14.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.0.3.tgz",
|
||||
"integrity": "sha512-64JbSvi3nbbcEtyitNn2LEDS/hcleAFpHdykpcnrstITFlzFgB/bW0ER5/SJJwUPj+ZPY+z3e+1jAfcczRLVGw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-darwin-x64": {
|
||||
"version": "14.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.0.3.tgz",
|
||||
"integrity": "sha512-RkTf+KbAD0SgYdVn1XzqE/+sIxYGB7NLMZRn9I4Z24afrhUpVJx6L8hsRnIwxz3ERE2NFURNliPjJ2QNfnWicQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-arm64-gnu": {
|
||||
"version": "14.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.0.3.tgz",
|
||||
"integrity": "sha512-3tBWGgz7M9RKLO6sPWC6c4pAw4geujSwQ7q7Si4d6bo0l6cLs4tmO+lnSwFp1Tm3lxwfMk0SgkJT7EdwYSJvcg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-arm64-musl": {
|
||||
"version": "14.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.0.3.tgz",
|
||||
"integrity": "sha512-v0v8Kb8j8T23jvVUWZeA2D8+izWspeyeDGNaT2/mTHWp7+37fiNfL8bmBWiOmeumXkacM/AB0XOUQvEbncSnHA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-win32-arm64-msvc": {
|
||||
"version": "14.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.0.3.tgz",
|
||||
"integrity": "sha512-WRDp8QrmsL1bbGtsh5GqQ/KWulmrnMBgbnb+59qNTW1kVi1nG/2ndZLkcbs2GX7NpFLlToLRMWSQXmPzQm4tog==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-win32-ia32-msvc": {
|
||||
"version": "14.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.0.3.tgz",
|
||||
"integrity": "sha512-EKffQeqCrj+t6qFFhIFTRoqb2QwX1mU7iTOvMyLbYw3QtqTw9sMwjykyiMlZlrfm2a4fA84+/aeW+PMg1MjuTg==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-win32-x64-msvc": {
|
||||
"version": "14.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.0.3.tgz",
|
||||
"integrity": "sha512-ERhKPSJ1vQrPiwrs15Pjz/rvDHZmkmvbf/BjPN/UCOI++ODftT0GtasDPi0j+y6PPJi5HsXw+dpRaXUaw4vjuQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
"react-i18next": "^13.5.0",
|
||||
"react-icons": "^4.12.0",
|
||||
"react-select": "^5.8.0",
|
||||
"react-tabs": "^6.0.2",
|
||||
"sass": "^1.69.5",
|
||||
"sharp": "^0.32.6",
|
||||
"yup": "^1.3.2"
|
||||
|
||||
@@ -35,5 +35,11 @@
|
||||
"language": {
|
||||
"fr": "French",
|
||||
"en": "English"
|
||||
}
|
||||
},
|
||||
"lang": "Language",
|
||||
"settings": "Settings",
|
||||
"profile": "Profile",
|
||||
"select-your-lang": "Change the language",
|
||||
"name": "Name",
|
||||
"email": "Email"
|
||||
}
|
||||
|
||||
@@ -35,5 +35,11 @@
|
||||
"language": {
|
||||
"fr": "Français",
|
||||
"en": "Anglais"
|
||||
}
|
||||
},
|
||||
"lang": "Langage",
|
||||
"settings": "Paramètres",
|
||||
"profile": "Profil",
|
||||
"select-your-lang": "Modifier la langue",
|
||||
"name": "Nom",
|
||||
"email": "Email"
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
|
||||
& h2 {
|
||||
color: $blue;
|
||||
margin-bottom: 15px;
|
||||
margin-bottom: 0.25em;
|
||||
font-weight: 500;
|
||||
|
||||
& .links-count {
|
||||
|
||||
@@ -28,12 +28,13 @@
|
||||
|
||||
.modal-header {
|
||||
width: 100%;
|
||||
margin-bottom: 1.5em;
|
||||
margin-bottom: 0.75em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
& button.btn-close {
|
||||
cursor: pointer;
|
||||
color: $blue;
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
|
||||
@@ -3,8 +3,8 @@ import { useSession } from 'next-auth/react';
|
||||
import PATHS from 'constants/paths';
|
||||
import styles from './navbar.module.scss';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import Image from 'next/image';
|
||||
import { TFunctionParam } from 'types/i18next';
|
||||
import RoundedImage from '../RoundedImage/RoundedImage';
|
||||
|
||||
export default function Navbar() {
|
||||
const { data, status } = useSession();
|
||||
@@ -13,6 +13,7 @@ export default function Navbar() {
|
||||
const avatarLabel = t('common:avatar', {
|
||||
name: data?.user?.name,
|
||||
} as TFunctionParam);
|
||||
|
||||
return (
|
||||
<nav className={styles['navbar']}>
|
||||
<ul className='reset'>
|
||||
@@ -28,12 +29,9 @@ export default function Navbar() {
|
||||
{status === 'authenticated' ? (
|
||||
<>
|
||||
<li className={styles['user']}>
|
||||
<Image
|
||||
<RoundedImage
|
||||
src={data.user.image}
|
||||
width={24}
|
||||
height={24}
|
||||
alt={avatarLabel}
|
||||
title={avatarLabel}
|
||||
/>
|
||||
{data.user.name}
|
||||
</li>
|
||||
|
||||
@@ -13,8 +13,4 @@
|
||||
display: flex;
|
||||
gap: 0.25em;
|
||||
justify-content: center;
|
||||
|
||||
& img {
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
38
src/components/Profile/Profile.tsx
Normal file
38
src/components/Profile/Profile.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import { useSession } from 'next-auth/react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { TFunctionParam } from 'types/i18next';
|
||||
import RoundedImage from '../RoundedImage/RoundedImage';
|
||||
import styles from './profile.module.scss';
|
||||
import clsx from 'clsx';
|
||||
|
||||
export default function Profile() {
|
||||
const { data } = useSession();
|
||||
const { t } = useTranslation('common');
|
||||
|
||||
const avatarLabel = t('common:avatar', {
|
||||
name: data?.user?.name,
|
||||
} as TFunctionParam);
|
||||
|
||||
return (
|
||||
<ul className={clsx('reset', styles['profile'])}>
|
||||
<li
|
||||
className={styles['avatar']}
|
||||
style={{ textAlign: 'center' }}
|
||||
>
|
||||
<RoundedImage
|
||||
src={data.user.image}
|
||||
width={96}
|
||||
height={96}
|
||||
alt={avatarLabel}
|
||||
title={avatarLabel}
|
||||
/>
|
||||
</li>
|
||||
<li className={styles['name']}>
|
||||
<b>{t('common:name')}</b> {data.user.name}
|
||||
</li>
|
||||
<li className={styles['email']}>
|
||||
<b>{t('common:email')}</b> {data.user.email}
|
||||
</li>
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
14
src/components/Profile/profile.module.scss
Normal file
14
src/components/Profile/profile.module.scss
Normal file
@@ -0,0 +1,14 @@
|
||||
.profile {
|
||||
display: flex;
|
||||
gap: 0.75em;
|
||||
flex-direction: column;
|
||||
|
||||
& .avatar {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
& li {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
21
src/components/RoundedImage/RoundedImage.tsx
Normal file
21
src/components/RoundedImage/RoundedImage.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import Image from 'next/image';
|
||||
import styles from './rounded-image.module.scss';
|
||||
|
||||
export default function RoundedImage({
|
||||
src,
|
||||
width = 24,
|
||||
height = 24,
|
||||
alt,
|
||||
}: (typeof Image)['defaultProps']) {
|
||||
return (
|
||||
<div className={styles['rounded-image']}>
|
||||
<Image
|
||||
src={src}
|
||||
width={width}
|
||||
height={height}
|
||||
alt={alt}
|
||||
title={alt}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
3
src/components/RoundedImage/rounded-image.module.scss
Normal file
3
src/components/RoundedImage/rounded-image.module.scss
Normal file
@@ -0,0 +1,3 @@
|
||||
.rounded-image img {
|
||||
border-radius: 50%;
|
||||
}
|
||||
@@ -3,10 +3,20 @@ import useModal from 'hooks/useModal';
|
||||
import Modal from '../Modal/Modal';
|
||||
import * as Keys from 'constants/keys';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { IoSettingsOutline } from 'react-icons/io5';
|
||||
import { IoLogOutOutline, IoSettingsOutline } from 'react-icons/io5';
|
||||
import LangSelector from '../LangSelector';
|
||||
import { Tab, TabList, TabPanel, Tabs } from 'react-tabs';
|
||||
import { TfiWorld } from 'react-icons/tfi';
|
||||
import { signOut } from 'next-auth/react';
|
||||
import PATHS from 'constants/paths';
|
||||
import styles from './settings-modal.module.scss';
|
||||
import clsx from 'clsx';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { FaUser } from 'react-icons/fa';
|
||||
import Profile from '../Profile/Profile';
|
||||
|
||||
export default function SettingsModal() {
|
||||
const { t } = useTranslation('common');
|
||||
const modal = useModal();
|
||||
useHotkeys(Keys.CLOSE_SEARCH_KEY, modal.close, {
|
||||
enabled: modal.isShowing,
|
||||
@@ -25,12 +35,48 @@ export default function SettingsModal() {
|
||||
<AnimatePresence>
|
||||
{modal.isShowing && (
|
||||
<Modal
|
||||
title='Settings'
|
||||
title={t('common:settings')}
|
||||
close={modal.close}
|
||||
>
|
||||
<LangSelector />
|
||||
<p>about tab with all links related to MyLinks</p>
|
||||
<button>disconnect</button>
|
||||
<Tabs className={styles['tabs']}>
|
||||
<TabList className={clsx('reset', styles['tab-list'])}>
|
||||
<Tab
|
||||
className={styles['tab']}
|
||||
selectedClassName={styles['tab-selected']}
|
||||
disabledClassName={styles['tab-disabled']}
|
||||
>
|
||||
<FaUser size={18} /> {t('common:profile')}
|
||||
</Tab>
|
||||
<Tab
|
||||
className={styles['tab']}
|
||||
selectedClassName={styles['tab-selected']}
|
||||
disabledClassName={styles['tab-disabled']}
|
||||
>
|
||||
<TfiWorld size={18} /> {t('common:lang')}
|
||||
</Tab>
|
||||
<button
|
||||
className={clsx('reset', styles['tab'])}
|
||||
style={{ color: 'red' }}
|
||||
onClick={() => signOut({ callbackUrl: PATHS.LOGIN })}
|
||||
>
|
||||
<IoLogOutOutline size={18} /> {t('common:logout')}
|
||||
</button>
|
||||
</TabList>
|
||||
|
||||
<TabPanel
|
||||
className={styles['tab-panel']}
|
||||
selectedClassName={styles['tab-panel-selected']}
|
||||
>
|
||||
<Profile />
|
||||
</TabPanel>
|
||||
<TabPanel
|
||||
className={styles['tab-panel']}
|
||||
selectedClassName={styles['tab-panel-selected']}
|
||||
>
|
||||
<p>{t('common:select-your-lang')}</p>
|
||||
<LangSelector />
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
</Modal>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
59
src/components/Settings/settings-modal.module.scss
Normal file
59
src/components/Settings/settings-modal.module.scss
Normal file
@@ -0,0 +1,59 @@
|
||||
@import 'styles/colors.scss';
|
||||
|
||||
.tabs {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
gap: 2em;
|
||||
flex: 1;
|
||||
justify-content: center;
|
||||
|
||||
& .tab-list {
|
||||
width: 150px;
|
||||
display: flex;
|
||||
gap: 0.25em;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
& .tab {
|
||||
height: 40px;
|
||||
width: 100%;
|
||||
font-size: 1em;
|
||||
cursor: pointer;
|
||||
padding: 0.5em 0.75em;
|
||||
display: flex;
|
||||
gap: 0.5em;
|
||||
align-items: center;
|
||||
transition: 0.05s;
|
||||
|
||||
&:hover,
|
||||
&-selected {
|
||||
background: $white;
|
||||
border-color: $grey;
|
||||
border-radius: 0.25em;
|
||||
}
|
||||
|
||||
&-selected {
|
||||
color: $blue;
|
||||
}
|
||||
|
||||
&-disabled {
|
||||
color: transparent;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
& .tab-panel {
|
||||
display: none;
|
||||
|
||||
&-selected {
|
||||
display: flex;
|
||||
gap: 0.5em;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
}
|
||||
4
src/types/types.d.ts
vendored
4
src/types/types.d.ts
vendored
@@ -37,8 +37,8 @@ export interface SearchItem {
|
||||
id: number;
|
||||
name: string;
|
||||
url: string;
|
||||
type: "category" | "link";
|
||||
category?: undefined | Link["category"];
|
||||
type: 'category' | 'link';
|
||||
category?: undefined | Link['category'];
|
||||
}
|
||||
|
||||
export interface Favicon {
|
||||
|
||||
Reference in New Issue
Block a user