mirror of
https://github.com/Sonny93/my-links.git
synced 2025-12-11 08:43:04 +00:00
feat/fix/chore: refactor project structure + add favicon
- Changement de structure de fichier - Ajout des favicons des sites - Suppression et mise à jour de dépendances - Ajout React-Icons pour gérer les icons - Amélioration du l'UI
This commit is contained in:
16
components/BlockWrapper/BlockWrapper.tsx
Normal file
16
components/BlockWrapper/BlockWrapper.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import { CSSProperties, ReactNode } from "react";
|
||||
|
||||
import styles from "./block-wrapper.module.scss";
|
||||
|
||||
interface BlockWrapperProps {
|
||||
children: ReactNode;
|
||||
style?: CSSProperties;
|
||||
}
|
||||
|
||||
export default function BlockWrapper({ children, style }: BlockWrapperProps) {
|
||||
return (
|
||||
<section className={styles["block-wrapper"]} style={style}>
|
||||
{children}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
47
components/BlockWrapper/block-wrapper.module.scss
Normal file
47
components/BlockWrapper/block-wrapper.module.scss
Normal file
@@ -0,0 +1,47 @@
|
||||
.block-wrapper {
|
||||
height: auto;
|
||||
width: 100%;
|
||||
|
||||
& h4 {
|
||||
user-select: none;
|
||||
text-transform: uppercase;
|
||||
font-size: 0.85em;
|
||||
font-weight: 500;
|
||||
color: #bbb;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
& ul {
|
||||
animation: fadein 0.3s both;
|
||||
}
|
||||
|
||||
& ul li {
|
||||
position: relative;
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
height: fit-content;
|
||||
width: 100%;
|
||||
background-color: #fff;
|
||||
padding: 7px 12px;
|
||||
border: 1px solid #dadce0;
|
||||
border-bottom: 2px solid #dadce0;
|
||||
border-radius: 3px;
|
||||
transition: 0.15s;
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadein {
|
||||
0% {
|
||||
transform: translateX(-15px);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
import LinkTag from "next/link";
|
||||
|
||||
import { Link } from "../../types";
|
||||
|
||||
import styles from "../../styles/home/categories.module.scss";
|
||||
|
||||
export default function Favorites({ favorites }: { favorites: Link[] }) {
|
||||
return (
|
||||
<div className={`${styles["block-wrapper"]} ${styles["favorites"]}`}>
|
||||
<h4>Favoris</h4>
|
||||
<ul className={styles["items"]}>
|
||||
{favorites.length === 0 ? (
|
||||
<NoFavLink />
|
||||
) : (
|
||||
favorites.map((link, key) => <LinkFavorite link={link} key={key} />)
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function NoFavLink(): JSX.Element {
|
||||
return <li className={styles["no-fav-link"]}>Aucun favoris</li>;
|
||||
}
|
||||
|
||||
function LinkFavorite({ link }: { link: Link }): JSX.Element {
|
||||
const { name, url, category } = link;
|
||||
return (
|
||||
<li className={styles["item"]}>
|
||||
<LinkTag href={url} target={"_blank"} rel={"noreferrer"}>
|
||||
{name}
|
||||
<span className={styles["category"]}> - {category.name}</span>
|
||||
</LinkTag>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
import { Session } from "next-auth";
|
||||
import LinkTag from "next/link";
|
||||
|
||||
import Categories from "./Categories";
|
||||
import Favorites from "./Favorites";
|
||||
import UserCard from "./UserCard";
|
||||
|
||||
import { Category, Link } from "../../types";
|
||||
|
||||
import styles from "../../styles/home/categories.module.scss";
|
||||
|
||||
interface SideMenuProps {
|
||||
categories: Category[];
|
||||
favorites: Link[];
|
||||
handleSelectCategory: (category: Category) => void;
|
||||
categoryActive: Category;
|
||||
session: Session;
|
||||
}
|
||||
export default function SideMenu({
|
||||
categories,
|
||||
favorites,
|
||||
handleSelectCategory,
|
||||
categoryActive,
|
||||
session,
|
||||
}: SideMenuProps) {
|
||||
return (
|
||||
<div className={styles["categories-wrapper"]}>
|
||||
<Favorites favorites={favorites} />
|
||||
<Categories
|
||||
categories={categories}
|
||||
categoryActive={categoryActive}
|
||||
handleSelectCategory={handleSelectCategory}
|
||||
/>
|
||||
<MenuControls />
|
||||
<UserCard session={session} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function MenuControls() {
|
||||
return (
|
||||
<div className={styles["controls"]}>
|
||||
<LinkTag href={"/category/create"}>Créer categorie</LinkTag>
|
||||
<LinkTag href={"/link/create"}>Créer lien</LinkTag>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import { Session } from 'next-auth';
|
||||
import { signOut } from 'next-auth/react';
|
||||
import Image from 'next/image';
|
||||
|
||||
import styles from '../../styles/home/categories.module.scss';
|
||||
|
||||
export default function UserCard({ session }: { session: Session; }) {
|
||||
return (
|
||||
<div className={styles['user-card-wrapper']}>
|
||||
<div className={styles['user-card']}>
|
||||
<Image
|
||||
src={session.user.image}
|
||||
width={28}
|
||||
height={28}
|
||||
alt={`${session.user.name}'s avatar`}
|
||||
/>
|
||||
{session.user.name}
|
||||
</div>
|
||||
<button onClick={() => signOut({ callbackUrl: '/signin' })} className={styles['disconnect-btn']}>
|
||||
Se déconnecter
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,12 +1,10 @@
|
||||
import Head from "next/head";
|
||||
import { NextSeo } from "next-seo";
|
||||
import Link from "next/link";
|
||||
|
||||
import MessageManager from "./MessageManager";
|
||||
import MessageManager from "./MessageManager/MessageManager";
|
||||
|
||||
import styles from "../styles/create.module.scss";
|
||||
|
||||
import { config } from "../config";
|
||||
|
||||
interface FormProps {
|
||||
title: string;
|
||||
errorMessage?: string;
|
||||
@@ -34,11 +32,7 @@ export default function Form({
|
||||
}: FormProps) {
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>
|
||||
{config.siteName} — {title}
|
||||
</title>
|
||||
</Head>
|
||||
<NextSeo title={title} />
|
||||
<div className={`App ${styles["create-app"]}`}>
|
||||
<h2>{title}</h2>
|
||||
<form onSubmit={handleSubmit}>
|
||||
|
||||
58
components/Links/LinkFavicon.tsx
Normal file
58
components/Links/LinkFavicon.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import Image from "next/image";
|
||||
import { useState } from "react";
|
||||
import { TbLoader3 } from "react-icons/tb";
|
||||
import { TfiWorld } from "react-icons/tfi";
|
||||
|
||||
import { faviconLinkBuilder } from "../../utils/link";
|
||||
|
||||
import styles from "./links.module.scss";
|
||||
|
||||
interface LinkFaviconProps {
|
||||
url: string;
|
||||
size?: number;
|
||||
noMargin?: boolean;
|
||||
}
|
||||
export default function LinkFavicon({
|
||||
url,
|
||||
size = 32,
|
||||
noMargin = false,
|
||||
}: LinkFaviconProps) {
|
||||
const [isFailed, setFailed] = useState<boolean>(false);
|
||||
const [isLoading, setLoading] = useState<boolean>(true);
|
||||
|
||||
const setFallbackFavicon = () => setFailed(true);
|
||||
const handleStopLoading = () => setLoading(false);
|
||||
|
||||
const { origin } = new URL(url);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles["favicon"]}
|
||||
style={{ marginRight: !noMargin ? "1em" : "0" }}
|
||||
>
|
||||
{!isFailed ? (
|
||||
<Image
|
||||
src={faviconLinkBuilder(origin)}
|
||||
onError={() => {
|
||||
setFallbackFavicon();
|
||||
handleStopLoading();
|
||||
}}
|
||||
onLoadingComplete={handleStopLoading}
|
||||
height={size}
|
||||
width={size}
|
||||
alt="icon"
|
||||
/>
|
||||
) : (
|
||||
<TfiWorld size={size} />
|
||||
)}
|
||||
{isLoading && (
|
||||
<span
|
||||
className={styles["favicon-loader"]}
|
||||
style={{ height: `${size}px`, width: `${size}px` }}
|
||||
>
|
||||
<TbLoader3 size={size} />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
81
components/Links/LinkItem.tsx
Normal file
81
components/Links/LinkItem.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
import LinkTag from "next/link";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
AiFillDelete,
|
||||
AiFillEdit,
|
||||
AiFillStar,
|
||||
AiOutlineStar,
|
||||
} from "react-icons/ai";
|
||||
|
||||
import { Link } from "../../types";
|
||||
import LinkFavicon from "./LinkFavicon";
|
||||
|
||||
import styles from "./links.module.scss";
|
||||
|
||||
export default function LinkItem({ link }: { link: Link }) {
|
||||
const { id, name, url, favorite } = link;
|
||||
const [isFavorite, setFavorite] = useState(favorite);
|
||||
|
||||
return (
|
||||
<li className={styles["link"]} key={id}>
|
||||
<LinkFavicon url={url} />
|
||||
<LinkTag href={url} target={"_blank"} rel={"noreferrer"}>
|
||||
<span className={styles["link-name"]}>
|
||||
{name} {isFavorite && <AiFillStar color="#ffc107" />}
|
||||
</span>
|
||||
<LinkItemURL url={url} />
|
||||
</LinkTag>
|
||||
<div className={styles["controls"]}>
|
||||
<div onClick={() => setFavorite((v) => !v)} className={styles["edit"]}>
|
||||
{isFavorite ? (
|
||||
<AiFillStar color="#ffc107" />
|
||||
) : (
|
||||
<AiOutlineStar color="#ffc107" />
|
||||
)}
|
||||
</div>
|
||||
<LinkTag
|
||||
href={`/link/edit/${id}`}
|
||||
className={styles["edit"]}
|
||||
title="Edit link"
|
||||
>
|
||||
<AiFillEdit />
|
||||
</LinkTag>
|
||||
<LinkTag
|
||||
href={`/link/remove/${id}`}
|
||||
className={styles["remove"]}
|
||||
title="Remove link"
|
||||
>
|
||||
<AiFillDelete color="red" />
|
||||
</LinkTag>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
function LinkItemURL({ url }: { url: string }) {
|
||||
try {
|
||||
const { origin, pathname, search } = new URL(url);
|
||||
let text = "";
|
||||
|
||||
if (pathname !== "/") {
|
||||
text += pathname;
|
||||
}
|
||||
|
||||
if (search !== "") {
|
||||
if (text === "") {
|
||||
text += "/";
|
||||
}
|
||||
text += search;
|
||||
}
|
||||
|
||||
return (
|
||||
<span className={styles["link-url"]}>
|
||||
{origin}
|
||||
<span className={styles["url-pathname"]}>{text}</span>
|
||||
</span>
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("error", error);
|
||||
return <span className={styles["link-url"]}>{url}</span>;
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,9 @@
|
||||
import LinkTag from "next/link";
|
||||
|
||||
import { Category, Link } from "../../types";
|
||||
import { Category } from "../../types";
|
||||
import LinkItem from "./LinkItem";
|
||||
|
||||
import EditSVG from "../../public/icons/edit.svg";
|
||||
import RemoveSVG from "../../public/icons/remove.svg";
|
||||
|
||||
import styles from "../../styles/home/links.module.scss";
|
||||
import styles from "./links.module.scss";
|
||||
|
||||
export default function Links({ category }: { category: Category }) {
|
||||
if (category === null) {
|
||||
@@ -24,7 +22,9 @@ export default function Links({ category }: { category: Category }) {
|
||||
<p>
|
||||
Aucun lien pour <b>{category.name}</b>
|
||||
</p>
|
||||
<LinkTag href="/link/create">Créer un lien</LinkTag>
|
||||
<LinkTag href={`/link/create?categoryId=${category.id}`}>
|
||||
Créer un lien
|
||||
</LinkTag>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -43,54 +43,3 @@ export default function Links({ category }: { category: Category }) {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function LinkItem({ link }: { link: Link }) {
|
||||
const { id, name, url, category } = link;
|
||||
return (
|
||||
<li className={styles["link"]} key={id}>
|
||||
<LinkTag href={url} target={"_blank"} rel={"noreferrer"}>
|
||||
<span className={styles["link-name"]}>
|
||||
{name}
|
||||
<span className={styles["link-category"]}> — {category.name}</span>
|
||||
</span>
|
||||
<LinkItemURL url={url} />
|
||||
</LinkTag>
|
||||
<div className={styles["controls"]}>
|
||||
<LinkTag href={`/link/edit/${id}`} className={styles["edit"]}>
|
||||
<EditSVG />
|
||||
</LinkTag>
|
||||
<LinkTag href={`/link/remove/${id}`} className={styles["remove"]}>
|
||||
<RemoveSVG />
|
||||
</LinkTag>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
function LinkItemURL({ url }: { url: string }) {
|
||||
try {
|
||||
const { origin, pathname, search } = new URL(url);
|
||||
let text = "";
|
||||
|
||||
if (pathname !== "/") {
|
||||
text += pathname;
|
||||
}
|
||||
|
||||
if (search !== "") {
|
||||
if (text === "") {
|
||||
text += "/";
|
||||
}
|
||||
text += search;
|
||||
}
|
||||
|
||||
return (
|
||||
<span className={styles["link-url"]}>
|
||||
{origin}
|
||||
<span className={styles["url-pathname"]}>{text}</span>
|
||||
</span>
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("error", error);
|
||||
return <span className={styles["link-url"]}>{url}</span>;
|
||||
}
|
||||
}
|
||||
|
||||
174
components/Links/links.module.scss
Normal file
174
components/Links/links.module.scss
Normal file
@@ -0,0 +1,174 @@
|
||||
.no-link,
|
||||
.no-category {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
animation: fadein 0.3s both;
|
||||
}
|
||||
|
||||
.links-wrapper {
|
||||
height: 100%;
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
overflow-x: hidden;
|
||||
overflow-y: scroll;
|
||||
|
||||
& h2 {
|
||||
color: #3f88c5;
|
||||
margin-bottom: 15px;
|
||||
font-weight: 500;
|
||||
|
||||
& .links-count {
|
||||
color: #bbb;
|
||||
font-weight: 300;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.links {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
animation: fadein 0.3s both; // bug on drag start
|
||||
}
|
||||
|
||||
.link {
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
height: fit-content;
|
||||
width: 100%;
|
||||
color: #3f88c5;
|
||||
background-color: #fff;
|
||||
padding: 10px 15px;
|
||||
border: 1px solid #dadce0;
|
||||
border-radius: 3px;
|
||||
margin-bottom: 10px;
|
||||
outline: 3px solid transparent;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
transition: 0.15s;
|
||||
|
||||
&:hover {
|
||||
border: 1px solid transparent;
|
||||
outline: 3px solid #82c5fede;
|
||||
|
||||
& .url-pathname {
|
||||
animation: fadein 0.3s both;
|
||||
}
|
||||
|
||||
& .controls {
|
||||
display: flex;
|
||||
animation: fadein 0.3s both;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.link > a {
|
||||
height: 100%;
|
||||
max-width: calc(100% - 50px);
|
||||
text-decoration: none;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
transition: 0.1s;
|
||||
|
||||
&,
|
||||
&:hover {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
& .link-name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5em;
|
||||
}
|
||||
|
||||
& .link-name .link-category {
|
||||
color: #bbb;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
& .link-url {
|
||||
width: 100%;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
color: #bbb;
|
||||
font-size: 0.8em;
|
||||
|
||||
& .url-pathname {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.link .controls {
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
|
||||
& > * {
|
||||
border: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: 0.1s;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.3);
|
||||
}
|
||||
|
||||
& svg {
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.favicon {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.favicon-loader {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background-color: #fff;
|
||||
|
||||
& > * {
|
||||
animation: rotate 1s both reverse infinite linear;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes rotate {
|
||||
to {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
from {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadein {
|
||||
0% {
|
||||
transform: translateX(-15px);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import styles from '../styles/components/message-manager.module.scss';
|
||||
|
||||
interface MessageManagerProps {
|
||||
error?: string;
|
||||
success?: string;
|
||||
info?: string;
|
||||
}
|
||||
export default function MessageManager({ error, success, info }: MessageManagerProps) {
|
||||
return (<>
|
||||
{info && (<div className={styles['info-msg']}>{info}</div>)}
|
||||
{error && (<div className={styles['error-msg']}>{error}</div>)}
|
||||
{success && (<div className={styles['success-msg']}>{success}</div>)}
|
||||
</>);
|
||||
}
|
||||
20
components/MessageManager/MessageManager.tsx
Normal file
20
components/MessageManager/MessageManager.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import styles from "./message-manager.module.scss";
|
||||
|
||||
interface MessageManagerProps {
|
||||
error?: string;
|
||||
success?: string;
|
||||
info?: string;
|
||||
}
|
||||
export default function MessageManager({
|
||||
error,
|
||||
success,
|
||||
info,
|
||||
}: MessageManagerProps) {
|
||||
return (
|
||||
<>
|
||||
{info && <div className={styles["info-msg"]}>{info}</div>}
|
||||
{error && <div className={styles["error-msg"]}>{error}</div>}
|
||||
{success && <div className={styles["success-msg"]}>{success}</div>}
|
||||
</>
|
||||
);
|
||||
}
|
||||
38
components/MessageManager/message-manager.module.scss
Normal file
38
components/MessageManager/message-manager.module.scss
Normal file
@@ -0,0 +1,38 @@
|
||||
.info-msg {
|
||||
height: fit-content;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
font-style: italic;
|
||||
font-size: 0.9em;
|
||||
color: #005aa5;
|
||||
background-color: #d3e8fa;
|
||||
padding: 10px;
|
||||
border-radius: 3px;
|
||||
animation: fadein 250ms both;
|
||||
}
|
||||
|
||||
.error-msg {
|
||||
height: fit-content;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
font-style: italic;
|
||||
font-size: 0.9em;
|
||||
color: #d8000c;
|
||||
background-color: #ffbabab9;
|
||||
padding: 10px;
|
||||
border-radius: 3px;
|
||||
animation: fadein 250ms both;
|
||||
}
|
||||
|
||||
.success-msg {
|
||||
height: fit-content;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
font-style: italic;
|
||||
font-size: 0.9em;
|
||||
color: green;
|
||||
background-color: #c1ffbab9;
|
||||
padding: 10px;
|
||||
border-radius: 3px;
|
||||
animation: fadein 250ms both;
|
||||
}
|
||||
31
components/SideMenu/Categories/Categories.tsx
Normal file
31
components/SideMenu/Categories/Categories.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import { Category } from "../../../types";
|
||||
import CategoryItem from "./CategoryItem";
|
||||
|
||||
import styles from "./categories.module.scss";
|
||||
|
||||
interface CategoriesProps {
|
||||
categories: Category[];
|
||||
categoryActive: Category;
|
||||
handleSelectCategory: (category: Category) => void;
|
||||
}
|
||||
export default function Categories({
|
||||
categories,
|
||||
categoryActive,
|
||||
handleSelectCategory,
|
||||
}: CategoriesProps) {
|
||||
return (
|
||||
<div className={styles["categories"]}>
|
||||
<h4>Catégories</h4>
|
||||
<ul className={styles["items"]}>
|
||||
{categories.map((category, key) => (
|
||||
<CategoryItem
|
||||
category={category}
|
||||
categoryActive={categoryActive}
|
||||
handleSelectCategory={handleSelectCategory}
|
||||
key={key}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,44 +1,17 @@
|
||||
import LinkTag from "next/link";
|
||||
import { AiFillDelete, AiFillEdit } from "react-icons/ai";
|
||||
|
||||
import styles from "../../styles/home/categories.module.scss";
|
||||
import { Category } from "../../types";
|
||||
import { Category } from "../../../types";
|
||||
|
||||
import EditSVG from "../../public/icons/edit.svg";
|
||||
import RemoveSVG from "../../public/icons/remove.svg";
|
||||
|
||||
interface CategoriesProps {
|
||||
categories: Category[];
|
||||
categoryActive: Category;
|
||||
handleSelectCategory: (category: Category) => void;
|
||||
}
|
||||
export default function Categories({
|
||||
categories,
|
||||
categoryActive,
|
||||
handleSelectCategory,
|
||||
}: CategoriesProps) {
|
||||
return (
|
||||
<div className={`${styles["block-wrapper"]} ${styles["categories"]}`}>
|
||||
<h4>Catégories</h4>
|
||||
<ul className={styles["items"]}>
|
||||
{categories.map((category, key) => (
|
||||
<CategoryItem
|
||||
category={category}
|
||||
categoryActive={categoryActive}
|
||||
handleSelectCategory={handleSelectCategory}
|
||||
key={key}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
import styles from "./categories.module.scss";
|
||||
|
||||
interface CategoryItemProps {
|
||||
category: Category;
|
||||
categoryActive: Category;
|
||||
handleSelectCategory: (category: Category) => void;
|
||||
}
|
||||
function CategoryItem({
|
||||
|
||||
export default function CategoryItem({
|
||||
category,
|
||||
categoryActive,
|
||||
handleSelectCategory,
|
||||
@@ -63,13 +36,13 @@ function MenuOptions({ id }: { id: number }): JSX.Element {
|
||||
return (
|
||||
<div className={styles["menu-item"]}>
|
||||
<LinkTag href={`/category/edit/${id}`} className={styles["option-edit"]}>
|
||||
<EditSVG />
|
||||
<AiFillEdit />
|
||||
</LinkTag>
|
||||
<LinkTag
|
||||
href={`/category/remove/${id}`}
|
||||
className={styles["option-remove"]}
|
||||
>
|
||||
<RemoveSVG />
|
||||
<AiFillDelete color="red" />
|
||||
</LinkTag>
|
||||
</div>
|
||||
);
|
||||
90
components/SideMenu/Categories/categories.module.scss
Normal file
90
components/SideMenu/Categories/categories.module.scss
Normal file
@@ -0,0 +1,90 @@
|
||||
.categories {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.items {
|
||||
padding-right: 5px;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
&.active {
|
||||
color: #fff;
|
||||
background: #3f88c5;
|
||||
border-color: #3f88c5;
|
||||
}
|
||||
|
||||
&:hover:not(.active) {
|
||||
color: #3f88c5;
|
||||
background: #f0eef6;
|
||||
border-bottom: 2px solid #3f88c5;
|
||||
}
|
||||
|
||||
&.active .menu-item .option-edit svg {
|
||||
fill: #fff;
|
||||
}
|
||||
|
||||
& .content {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
|
||||
& .name {
|
||||
margin-right: 5px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
& .links-count {
|
||||
min-width: fit-content;
|
||||
font-size: 0.85em;
|
||||
color: #bbb;
|
||||
}
|
||||
}
|
||||
&:hover .content {
|
||||
width: calc(100% - 42px);
|
||||
}
|
||||
|
||||
& .menu-item {
|
||||
height: 100%;
|
||||
min-width: fit-content;
|
||||
margin-left: 5px;
|
||||
display: none;
|
||||
gap: 2px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
animation: fadein 0.3s both;
|
||||
|
||||
& > a {
|
||||
border: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.25);
|
||||
}
|
||||
|
||||
& svg {
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
&:hover .menu-item {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
19
components/SideMenu/Favorites/FavoriteItem.tsx
Normal file
19
components/SideMenu/Favorites/FavoriteItem.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import LinkTag from "next/link";
|
||||
|
||||
import { Link } from "../../../types";
|
||||
import LinkFavicon from "../../Links/LinkFavicon";
|
||||
|
||||
import styles from "./favorites.module.scss";
|
||||
|
||||
export default function FavoriteItem({ link }: { link: Link }): JSX.Element {
|
||||
const { name, url, category } = link;
|
||||
return (
|
||||
<li className={styles["item"]}>
|
||||
<LinkTag href={url} target={"_blank"} rel={"noreferrer"}>
|
||||
<LinkFavicon url={url} size={24} />
|
||||
<span>{name}</span>
|
||||
<span className={styles["category"]}> - {category.name}</span>
|
||||
</LinkTag>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
19
components/SideMenu/Favorites/Favorites.tsx
Normal file
19
components/SideMenu/Favorites/Favorites.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import { Link } from "../../../types";
|
||||
import FavoriteItem from "./FavoriteItem";
|
||||
|
||||
import styles from "./favorites.module.scss";
|
||||
|
||||
export default function Favorites({ favorites }: { favorites: Link[] }) {
|
||||
return (
|
||||
favorites.length !== 0 && (
|
||||
<div className={styles["favorites"]}>
|
||||
<h4>Favoris</h4>
|
||||
<ul className={styles["items"]}>
|
||||
{favorites.map((link, key) => (
|
||||
<FavoriteItem link={link} key={key} />
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
55
components/SideMenu/Favorites/favorites.module.scss
Normal file
55
components/SideMenu/Favorites/favorites.module.scss
Normal file
@@ -0,0 +1,55 @@
|
||||
.favorites {
|
||||
height: auto;
|
||||
width: 100%;
|
||||
margin-bottom: 15px;
|
||||
|
||||
& h4 {
|
||||
user-select: none;
|
||||
text-transform: uppercase;
|
||||
font-size: 0.85em;
|
||||
font-weight: 500;
|
||||
color: #bbb;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.favorites ul.items li.item {
|
||||
width: 100%;
|
||||
background-color: #fff;
|
||||
padding: 0;
|
||||
border: 1px solid #dadce0;
|
||||
border-bottom: 2px solid #dadce0;
|
||||
border-radius: 3px;
|
||||
transition: 0.15s;
|
||||
|
||||
& a {
|
||||
width: 100%;
|
||||
color: inherit;
|
||||
padding: 0.65em 1.15em;
|
||||
border: 0 !important;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25em;
|
||||
}
|
||||
|
||||
& .category {
|
||||
color: #bbb;
|
||||
font-size: 0.85em;
|
||||
}
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: #fff;
|
||||
background: #3f88c5;
|
||||
border-color: #3f88c5;
|
||||
}
|
||||
|
||||
&:hover:not(.active) {
|
||||
color: #3f88c5;
|
||||
background: #f0eef6;
|
||||
border-bottom: 2px solid #3f88c5;
|
||||
}
|
||||
}
|
||||
62
components/SideMenu/SideMenu.tsx
Normal file
62
components/SideMenu/SideMenu.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import { Session } from "next-auth";
|
||||
import LinkTag from "next/link";
|
||||
|
||||
import BlockWrapper from "../BlockWrapper/BlockWrapper";
|
||||
import Categories from "./Categories/Categories";
|
||||
import Favorites from "./Favorites/Favorites";
|
||||
import UserCard from "./UserCard/UserCard";
|
||||
|
||||
import { Category, Link } from "../../types";
|
||||
|
||||
import styles from "./sidemenu.module.scss";
|
||||
|
||||
interface SideMenuProps {
|
||||
categories: Category[];
|
||||
favorites: Link[];
|
||||
handleSelectCategory: (category: Category) => void;
|
||||
categoryActive: Category;
|
||||
session: Session;
|
||||
}
|
||||
export default function SideMenu({
|
||||
categories,
|
||||
favorites,
|
||||
handleSelectCategory,
|
||||
categoryActive,
|
||||
session,
|
||||
}: SideMenuProps) {
|
||||
return (
|
||||
<div className={styles["side-menu"]}>
|
||||
<BlockWrapper>
|
||||
<Favorites favorites={favorites} />
|
||||
</BlockWrapper>
|
||||
<BlockWrapper style={{ minHeight: "0" }}>
|
||||
<Categories
|
||||
categories={categories}
|
||||
categoryActive={categoryActive}
|
||||
handleSelectCategory={handleSelectCategory}
|
||||
/>
|
||||
</BlockWrapper>
|
||||
<BlockWrapper>
|
||||
<MenuControls categoryActive={categoryActive} />
|
||||
</BlockWrapper>
|
||||
<BlockWrapper>
|
||||
<UserCard session={session} />
|
||||
</BlockWrapper>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function MenuControls({
|
||||
categoryActive,
|
||||
}: {
|
||||
categoryActive: SideMenuProps["categoryActive"];
|
||||
}) {
|
||||
return (
|
||||
<div className={styles["menu-controls"]}>
|
||||
<LinkTag href={"/category/create"}>Créer categorie</LinkTag>
|
||||
<LinkTag href={`/link/create?categoryId=${categoryActive.id}`}>
|
||||
Créer lien
|
||||
</LinkTag>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
28
components/SideMenu/UserCard/UserCard.tsx
Normal file
28
components/SideMenu/UserCard/UserCard.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import { Session } from "next-auth";
|
||||
import { signOut } from "next-auth/react";
|
||||
import Image from "next/image";
|
||||
import { FiLogOut } from "react-icons/fi";
|
||||
|
||||
import styles from "./user-card.module.scss";
|
||||
|
||||
export default function UserCard({ session }: { session: Session }) {
|
||||
return (
|
||||
<div className={styles["user-card-wrapper"]}>
|
||||
<div className={styles["user-card"]}>
|
||||
<Image
|
||||
src={session.user.image}
|
||||
width={28}
|
||||
height={28}
|
||||
alt={`${session.user.name}'s avatar`}
|
||||
/>
|
||||
{session.user.name}
|
||||
</div>
|
||||
<button
|
||||
onClick={() => signOut({ callbackUrl: "/signin" })}
|
||||
className="reset"
|
||||
>
|
||||
<FiLogOut size={24} />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
33
components/SideMenu/UserCard/user-card.module.scss
Normal file
33
components/SideMenu/UserCard/user-card.module.scss
Normal file
@@ -0,0 +1,33 @@
|
||||
.user-card-wrapper {
|
||||
user-select: none;
|
||||
height: fit-content;
|
||||
width: 100%;
|
||||
color: #333;
|
||||
background-color: #fff;
|
||||
border: 1px solid #dadce0;
|
||||
padding: 7px 12px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
& .user-card {
|
||||
display: flex;
|
||||
gap: 0.5em;
|
||||
align-items: center;
|
||||
|
||||
& img {
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
& button {
|
||||
cursor: pointer;
|
||||
color: #3d7bab;
|
||||
display: flex;
|
||||
transition: 0.15s;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.3);
|
||||
}
|
||||
}
|
||||
}
|
||||
19
components/SideMenu/sidemenu.module.scss
Normal file
19
components/SideMenu/sidemenu.module.scss
Normal file
@@ -0,0 +1,19 @@
|
||||
.side-menu {
|
||||
height: 100%;
|
||||
width: 300px;
|
||||
padding: 0 25px 0 10px;
|
||||
border-right: 1px solid #dadce0;
|
||||
margin-right: 15px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.menu-controls {
|
||||
margin: 10px 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
Reference in New Issue
Block a user