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:
Sonny
2023-04-20 18:18:03 +02:00
parent f68bb22a01
commit 45f5455f94
47 changed files with 1274 additions and 1350 deletions

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

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

View File

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

View File

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

View File

@@ -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>
)
}

View File

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

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

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

View File

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

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

View File

@@ -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>)}
</>);
}

View 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>}
</>
);
}

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

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

View File

@@ -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>
);

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

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

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

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

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

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

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

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