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

@@ -1,4 +1,4 @@
{ {
"tabWidth": 4, "tabWidth": 2,
"useTabs": true "useTabs": false
} }

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 Link from "next/link";
import MessageManager from "./MessageManager"; import MessageManager from "./MessageManager/MessageManager";
import styles from "../styles/create.module.scss"; import styles from "../styles/create.module.scss";
import { config } from "../config";
interface FormProps { interface FormProps {
title: string; title: string;
errorMessage?: string; errorMessage?: string;
@@ -34,11 +32,7 @@ export default function Form({
}: FormProps) { }: FormProps) {
return ( return (
<> <>
<Head> <NextSeo title={title} />
<title>
{config.siteName} {title}
</title>
</Head>
<div className={`App ${styles["create-app"]}`}> <div className={`App ${styles["create-app"]}`}>
<h2>{title}</h2> <h2>{title}</h2>
<form onSubmit={handleSubmit}> <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 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 styles from "./links.module.scss";
import RemoveSVG from "../../public/icons/remove.svg";
import styles from "../../styles/home/links.module.scss";
export default function Links({ category }: { category: Category }) { export default function Links({ category }: { category: Category }) {
if (category === null) { if (category === null) {
@@ -24,7 +22,9 @@ export default function Links({ category }: { category: Category }) {
<p> <p>
Aucun lien pour <b>{category.name}</b> Aucun lien pour <b>{category.name}</b>
</p> </p>
<LinkTag href="/link/create">Créer un lien</LinkTag> <LinkTag href={`/link/create?categoryId=${category.id}`}>
Créer un lien
</LinkTag>
</div> </div>
); );
} }
@@ -43,54 +43,3 @@ export default function Links({ category }: { category: Category }) {
</div> </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,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 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 styles from "./categories.module.scss";
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>
);
}
interface CategoryItemProps { interface CategoryItemProps {
category: Category; category: Category;
categoryActive: Category; categoryActive: Category;
handleSelectCategory: (category: Category) => void; handleSelectCategory: (category: Category) => void;
} }
function CategoryItem({
export default function CategoryItem({
category, category,
categoryActive, categoryActive,
handleSelectCategory, handleSelectCategory,
@@ -63,13 +36,13 @@ function MenuOptions({ id }: { id: number }): JSX.Element {
return ( return (
<div className={styles["menu-item"]}> <div className={styles["menu-item"]}>
<LinkTag href={`/category/edit/${id}`} className={styles["option-edit"]}> <LinkTag href={`/category/edit/${id}`} className={styles["option-edit"]}>
<EditSVG /> <AiFillEdit />
</LinkTag> </LinkTag>
<LinkTag <LinkTag
href={`/category/remove/${id}`} href={`/category/remove/${id}`}
className={styles["option-remove"]} className={styles["option-remove"]}
> >
<RemoveSVG /> <AiFillDelete color="red" />
</LinkTag> </LinkTag>
</div> </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;
}

View File

@@ -9,5 +9,3 @@ NEXTAUTH_SECRET=
GOOGLE_CLIENT_ID= GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET= GOOGLE_CLIENT_SECRET=
NEXT_PUBLIC_SITE_NAME=

View File

@@ -1,14 +1,17 @@
module.exports = { /** @type {import('next').NextConfig} */
reactStrictMode: true, const config = {
images: {
domains: ['lh3.googleusercontent.com']
},
webpack(config) { webpack(config) {
config.module.rules.push({ config.module.rules.push({
test: /\.svg$/, test: /\.svg$/,
use: ["@svgr/webpack"] use: ["@svgr/webpack"],
}); });
return config; return config;
} },
} images: {
domains: ["localhost", "t3.gstatic.com", "lh3.googleusercontent.com"],
formats: ["image/webp"],
},
};
module.exports = config;

510
package-lock.json generated
View File

@@ -12,13 +12,14 @@
"next": "^13.3.0", "next": "^13.3.0",
"next-auth": "^4.22.0", "next-auth": "^4.22.0",
"next-connect": "^0.13.0", "next-connect": "^0.13.0",
"next-seo": "^6.0.0",
"node-html-parser": "^6.1.5",
"nprogress": "^0.2.0", "nprogress": "^0.2.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-confirm-alert": "^3.0.6",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-icons": "^4.8.0",
"react-select": "^5.7.2", "react-select": "^5.7.2",
"sass": "^1.62.0", "sass": "^1.62.0",
"sharp": "^0.32.0",
"toastr": "^2.1.4" "toastr": "^2.1.4"
}, },
"devDependencies": { "devDependencies": {
@@ -2921,25 +2922,6 @@
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true "dev": true
}, },
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
]
},
"node_modules/binary-extensions": { "node_modules/binary-extensions": {
"version": "2.2.0", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
@@ -2948,16 +2930,6 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/bl": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
"integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
"dependencies": {
"buffer": "^5.5.0",
"inherits": "^2.0.4",
"readable-stream": "^3.4.0"
}
},
"node_modules/boolbase": { "node_modules/boolbase": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
@@ -3011,29 +2983,6 @@
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
} }
}, },
"node_modules/buffer": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"dependencies": {
"base64-js": "^1.3.1",
"ieee754": "^1.1.13"
}
},
"node_modules/busboy": { "node_modules/busboy": {
"version": "1.6.0", "version": "1.6.0",
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
@@ -3142,28 +3091,11 @@
"node": ">= 6" "node": ">= 6"
} }
}, },
"node_modules/chownr": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="
},
"node_modules/client-only": { "node_modules/client-only": {
"version": "0.0.1", "version": "0.0.1",
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==" "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="
}, },
"node_modules/color": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
"integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==",
"dependencies": {
"color-convert": "^2.0.1",
"color-string": "^1.9.0"
},
"engines": {
"node": ">=12.5.0"
}
},
"node_modules/color-convert": { "node_modules/color-convert": {
"version": "1.9.3", "version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
@@ -3177,31 +3109,6 @@
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="
}, },
"node_modules/color-string": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz",
"integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
"dependencies": {
"color-name": "^1.0.0",
"simple-swizzle": "^0.2.2"
}
},
"node_modules/color/node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/color/node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
},
"node_modules/combined-stream": { "node_modules/combined-stream": {
"version": "1.0.8", "version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
@@ -3390,28 +3297,6 @@
} }
} }
}, },
"node_modules/decompress-response": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
"integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
"dependencies": {
"mimic-response": "^3.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/deep-extend": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
"integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/deep-is": { "node_modules/deep-is": {
"version": "0.1.4", "version": "0.1.4",
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
@@ -3459,14 +3344,6 @@
"node": ">=0.4.0" "node": ">=0.4.0"
} }
}, },
"node_modules/detect-libc": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz",
"integrity": "sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==",
"engines": {
"node": ">=8"
}
},
"node_modules/dir-glob": { "node_modules/dir-glob": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
@@ -3562,14 +3439,6 @@
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
"dev": true "dev": true
}, },
"node_modules/end-of-stream": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
"integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
"dependencies": {
"once": "^1.4.0"
}
},
"node_modules/enhanced-resolve": { "node_modules/enhanced-resolve": {
"version": "5.12.0", "version": "5.12.0",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.12.0.tgz", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.12.0.tgz",
@@ -4239,14 +4108,6 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/expand-template": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
"integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==",
"engines": {
"node": ">=6"
}
},
"node_modules/fast-deep-equal": { "node_modules/fast-deep-equal": {
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@@ -4406,11 +4267,6 @@
"node": ">= 6" "node": ">= 6"
} }
}, },
"node_modules/fs-constants": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="
},
"node_modules/fs.realpath": { "node_modules/fs.realpath": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@@ -4509,11 +4365,6 @@
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
} }
}, },
"node_modules/github-from-package": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
"integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw=="
},
"node_modules/glob": { "node_modules/glob": {
"version": "7.1.7", "version": "7.1.7",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz",
@@ -4704,6 +4555,14 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/he": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
"bin": {
"he": "bin/he"
}
},
"node_modules/hoist-non-react-statics": { "node_modules/hoist-non-react-statics": {
"version": "3.3.2", "version": "3.3.2",
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
@@ -4712,25 +4571,6 @@
"react-is": "^16.7.0" "react-is": "^16.7.0"
} }
}, },
"node_modules/ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
]
},
"node_modules/ignore": { "node_modules/ignore": {
"version": "5.2.4", "version": "5.2.4",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz",
@@ -4782,12 +4622,8 @@
"node_modules/inherits": { "node_modules/inherits": {
"version": "2.0.4", "version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
}, "dev": true
"node_modules/ini": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="
}, },
"node_modules/internal-slot": { "node_modules/internal-slot": {
"version": "1.0.4", "version": "1.0.4",
@@ -5307,17 +5143,6 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/mimic-response": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
"integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/minimatch": { "node_modules/minimatch": {
"version": "3.1.2", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
@@ -5333,12 +5158,8 @@
"node_modules/minimist": { "node_modules/minimist": {
"version": "1.2.6", "version": "1.2.6",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==" "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==",
}, "dev": true
"node_modules/mkdirp-classic": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="
}, },
"node_modules/ms": { "node_modules/ms": {
"version": "2.1.2", "version": "2.1.2",
@@ -5356,11 +5177,6 @@
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
} }
}, },
"node_modules/napi-build-utils": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz",
"integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg=="
},
"node_modules/natural-compare": { "node_modules/natural-compare": {
"version": "1.4.0", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
@@ -5454,35 +5270,24 @@
"trouter": "^3.2.0" "trouter": "^3.2.0"
} }
}, },
"node_modules/node-abi": { "node_modules/next-seo": {
"version": "3.24.0",
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.24.0.tgz",
"integrity": "sha512-YPG3Co0luSu6GwOBsmIdGW6Wx0NyNDLg/hriIyDllVsNwnI6UeqaWShxC3lbH4LtEQUgoLP3XR1ndXiDAWvmRw==",
"dependencies": {
"semver": "^7.3.5"
},
"engines": {
"node": ">=10"
}
},
"node_modules/node-abi/node_modules/semver": {
"version": "7.3.7",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz",
"integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==",
"dependencies": {
"lru-cache": "^6.0.0"
},
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/node-addon-api": {
"version": "6.0.0", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.0.0.tgz", "resolved": "https://registry.npmjs.org/next-seo/-/next-seo-6.0.0.tgz",
"integrity": "sha512-GyHvgPvUXBvAkXa0YvYnhilSB1A+FRYMpIVggKzPZqdaZfevZOuzfWzyvgzOwRLHBeo/MMswmJFsrNF4Nw1pmA==" "integrity": "sha512-jKKt1p1z4otMA28AyeoAONixVjdYmgFCWwpEFtu+DwRHQDllVX3RjtyXbuCQiUZEfQ9rFPBpAI90vDeLZlMBdg==",
"peerDependencies": {
"next": "^8.1.1-canary.54 || >=9.0.0",
"react": ">=16.0.0",
"react-dom": ">=16.0.0"
}
},
"node_modules/node-html-parser": {
"version": "6.1.5",
"resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-6.1.5.tgz",
"integrity": "sha512-fAaM511feX++/Chnhe475a0NHD8M7AxDInsqQpz6x63GRF7xYNdS8Vo5dKsIVPgsOvG7eioRRTZQnWBrhDHBSg==",
"dependencies": {
"css-select": "^5.1.0",
"he": "1.2.0"
}
}, },
"node_modules/node-releases": { "node_modules/node-releases": {
"version": "2.0.10", "version": "2.0.10",
@@ -5643,6 +5448,7 @@
"version": "1.4.0", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"dev": true,
"dependencies": { "dependencies": {
"wrappy": "1" "wrappy": "1"
} }
@@ -5852,31 +5658,6 @@
"preact": ">=10" "preact": ">=10"
} }
}, },
"node_modules/prebuild-install": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.1.tgz",
"integrity": "sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==",
"dependencies": {
"detect-libc": "^2.0.0",
"expand-template": "^2.0.3",
"github-from-package": "0.0.0",
"minimist": "^1.2.3",
"mkdirp-classic": "^0.5.3",
"napi-build-utils": "^1.0.1",
"node-abi": "^3.3.0",
"pump": "^3.0.0",
"rc": "^1.2.7",
"simple-get": "^4.0.0",
"tar-fs": "^2.0.0",
"tunnel-agent": "^0.6.0"
},
"bin": {
"prebuild-install": "bin.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/prelude-ls": { "node_modules/prelude-ls": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@@ -5923,15 +5704,6 @@
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
}, },
"node_modules/pump": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
"integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
"dependencies": {
"end-of-stream": "^1.1.0",
"once": "^1.3.1"
}
},
"node_modules/punycode": { "node_modules/punycode": {
"version": "2.3.0", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz",
@@ -5961,28 +5733,6 @@
} }
] ]
}, },
"node_modules/rc": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
"integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
"dependencies": {
"deep-extend": "^0.6.0",
"ini": "~1.3.0",
"minimist": "^1.2.0",
"strip-json-comments": "~2.0.1"
},
"bin": {
"rc": "cli.js"
}
},
"node_modules/rc/node_modules/strip-json-comments": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
"integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/react": { "node_modules/react": {
"version": "18.2.0", "version": "18.2.0",
"resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
@@ -5994,15 +5744,6 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/react-confirm-alert": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/react-confirm-alert/-/react-confirm-alert-3.0.6.tgz",
"integrity": "sha512-rplP6Ed9ZSNd0KFV5BUzk4EPQ77BxsrayllBXGFuA8xPXc7sbBjgU5KUrNpl7aWFmP7mXRlVXfuy1IT5DbffYw==",
"peerDependencies": {
"react": ">=18.0.0",
"react-dom": ">=10.0.0"
}
},
"node_modules/react-dom": { "node_modules/react-dom": {
"version": "18.2.0", "version": "18.2.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz",
@@ -6015,6 +5756,14 @@
"react": "^18.2.0" "react": "^18.2.0"
} }
}, },
"node_modules/react-icons": {
"version": "4.8.0",
"resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.8.0.tgz",
"integrity": "sha512-N6+kOLcihDiAnj5Czu637waJqSnwlMNROzVZMhfX68V/9bu9qHaMIJC4UdozWoOk57gahFCNHwVvWzm0MTzRjg==",
"peerDependencies": {
"react": "*"
}
},
"node_modules/react-is": { "node_modules/react-is": {
"version": "16.13.1", "version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
@@ -6055,19 +5804,6 @@
"react-dom": ">=16.6.0" "react-dom": ">=16.6.0"
} }
}, },
"node_modules/readable-stream": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
"integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/readdirp": { "node_modules/readdirp": {
"version": "3.6.0", "version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
@@ -6308,42 +6044,6 @@
"semver": "bin/semver.js" "semver": "bin/semver.js"
} }
}, },
"node_modules/sharp": {
"version": "0.32.0",
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.32.0.tgz",
"integrity": "sha512-yLAypVcqj1toSAqRSwbs86nEzfyZVDYqjuUX8grhFpeij0DDNagKJXELS/auegDBRDg1XBtELdOGfo2X1cCpeA==",
"hasInstallScript": true,
"dependencies": {
"color": "^4.2.3",
"detect-libc": "^2.0.1",
"node-addon-api": "^6.0.0",
"prebuild-install": "^7.1.1",
"semver": "^7.3.8",
"simple-get": "^4.0.1",
"tar-fs": "^2.1.1",
"tunnel-agent": "^0.6.0"
},
"engines": {
"node": ">=14.15.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/sharp/node_modules/semver": {
"version": "7.3.8",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
"integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==",
"dependencies": {
"lru-cache": "^6.0.0"
},
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/shebang-command": { "node_modules/shebang-command": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -6379,62 +6079,6 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/simple-concat": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
"integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
]
},
"node_modules/simple-get": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz",
"integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"dependencies": {
"decompress-response": "^6.0.0",
"once": "^1.3.1",
"simple-concat": "^1.0.0"
}
},
"node_modules/simple-swizzle": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
"integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==",
"dependencies": {
"is-arrayish": "^0.3.1"
}
},
"node_modules/simple-swizzle/node_modules/is-arrayish": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
"integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="
},
"node_modules/slash": { "node_modules/slash": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
@@ -6468,33 +6112,6 @@
"node": ">=10.0.0" "node": ">=10.0.0"
} }
}, },
"node_modules/string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
"dependencies": {
"safe-buffer": "~5.2.0"
}
},
"node_modules/string_decoder/node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
]
},
"node_modules/string.prototype.matchall": { "node_modules/string.prototype.matchall": {
"version": "4.0.7", "version": "4.0.7",
"resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.7.tgz", "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.7.tgz",
@@ -6683,32 +6300,6 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/tar-fs": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz",
"integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==",
"dependencies": {
"chownr": "^1.1.1",
"mkdirp-classic": "^0.5.2",
"pump": "^3.0.0",
"tar-stream": "^2.1.4"
}
},
"node_modules/tar-stream": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
"integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
"dependencies": {
"bl": "^4.0.3",
"end-of-stream": "^1.4.1",
"fs-constants": "^1.0.0",
"inherits": "^2.0.3",
"readable-stream": "^3.1.1"
},
"engines": {
"node": ">=6"
}
},
"node_modules/text-table": { "node_modules/text-table": {
"version": "0.2.0", "version": "0.2.0",
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
@@ -6813,17 +6404,6 @@
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
"dev": true "dev": true
}, },
"node_modules/tunnel-agent": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
"integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
"dependencies": {
"safe-buffer": "^5.0.1"
},
"engines": {
"node": "*"
}
},
"node_modules/type-check": { "node_modules/type-check": {
"version": "0.4.0", "version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
@@ -6973,11 +6553,6 @@
} }
} }
}, },
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
},
"node_modules/uuid": { "node_modules/uuid": {
"version": "8.3.2", "version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
@@ -7049,7 +6624,8 @@
"node_modules/wrappy": { "node_modules/wrappy": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"dev": true
}, },
"node_modules/yallist": { "node_modules/yallist": {
"version": "4.0.0", "version": "4.0.0",

View File

@@ -14,13 +14,14 @@
"next": "^13.3.0", "next": "^13.3.0",
"next-auth": "^4.22.0", "next-auth": "^4.22.0",
"next-connect": "^0.13.0", "next-connect": "^0.13.0",
"next-seo": "^6.0.0",
"node-html-parser": "^6.1.5",
"nprogress": "^0.2.0", "nprogress": "^0.2.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-confirm-alert": "^3.0.6",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-icons": "^4.8.0",
"react-select": "^5.7.2", "react-select": "^5.7.2",
"sass": "^1.62.0", "sass": "^1.62.0",
"sharp": "^0.32.0",
"toastr": "^2.1.4" "toastr": "^2.1.4"
}, },
"devDependencies": { "devDependencies": {

View File

@@ -1,13 +1,12 @@
import Head from 'next/head'; import { NextSeo } from 'next-seo';
import styles from '../styles/error-page.module.scss';
import { config } from '../config'; import styles from '../styles/error-page.module.scss';
export default function Custom404() { export default function Custom404() {
return (<> return (<>
<Head> <NextSeo
<title>{config.siteName} Page introuvable</title> title='Page introuvable'
</Head> />
<div className={styles['App']}> <div className={styles['App']}>
<h1>404</h1> <h1>404</h1>
<h2>Cette page est introuvable.</h2> <h2>Cette page est introuvable.</h2>

View File

@@ -1,13 +1,12 @@
import Head from 'next/head'; import { NextSeo } from 'next-seo';
import styles from '../styles/error-page.module.scss';
import { config } from '../config'; import styles from '../styles/error-page.module.scss';
export default function Custom500() { export default function Custom500() {
return (<> return (<>
<Head> <NextSeo
<title>{config.siteName} Une erreur côté serveur est survenue</title> title='Une erreur est survenue'
</Head> />
<div className={styles['App']}> <div className={styles['App']}>
<h1>500</h1> <h1>500</h1>
<h2>Une erreur côté serveur est survenue.</h2> <h2>Une erreur côté serveur est survenue.</h2>

View File

@@ -1,44 +1,44 @@
import { useEffect } from 'react'; import { SessionProvider } from "next-auth/react";
import { SessionProvider } from 'next-auth/react'; import { useEffect } from "react";
import { useRouter } from 'next/router'; import { useRouter } from "next/router";
import nProgress from 'nprogress'; import nProgress from "nprogress";
import 'nprogress/nprogress.css'; import "nprogress/nprogress.css";
import AuthRequired from '../components/AuthRequired'; import AuthRequired from "../components/AuthRequired";
import '../styles/globals.scss'; import { DefaultSeo } from "next-seo";
import "../styles/globals.scss";
function MyApp({ function MyApp({ Component, pageProps: { session, ...pageProps } }) {
Component, const router = useRouter();
pageProps: { session, ...pageProps }
}) {
const router = useRouter();
useEffect(() => { // Chargement pages useEffect(() => {
router.events.on('routeChangeStart', nProgress.start); // Chargement pages
router.events.on('routeChangeComplete', nProgress.done); router.events.on("routeChangeStart", nProgress.start);
router.events.on('routeChangeError', nProgress.done); router.events.on("routeChangeComplete", nProgress.done);
router.events.on("routeChangeError", nProgress.done);
return () => { return () => {
router.events.off('routeChangeStart', nProgress.start); router.events.off("routeChangeStart", nProgress.start);
router.events.off('routeChangeComplete', nProgress.done); router.events.off("routeChangeComplete", nProgress.done);
router.events.off('routeChangeError', nProgress.done); router.events.off("routeChangeError", nProgress.done);
} };
}); });
return ( return (
<SessionProvider session={session}> <SessionProvider session={session}>
{Component.authRequired ? ( <DefaultSeo titleTemplate="MyLinks — %s" defaultTitle="MyLinks" />
<AuthRequired> {Component.authRequired ? (
<Component {...pageProps} /> <AuthRequired>
</AuthRequired> <Component {...pageProps} />
) : ( </AuthRequired>
<Component {...pageProps} /> ) : (
)} <Component {...pageProps} />
</SessionProvider> )}
); </SessionProvider>
);
} }
export default MyApp; export default MyApp;

View File

@@ -1,17 +1,15 @@
import { Html, Head, Main, NextScript } from 'next/document'; import { Head, Html, Main, NextScript } from 'next/document';
import { config } from '../config';
const Document = () => ( const Document = () => (
<Html lang='fr'> <Html lang='fr'>
<Head> <Head>
<meta name="theme-color" content="#f0eef6" />
<link <link
href='https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,400;0,500;0,700;1,400;1,500;1,700&family=Rubik:ital,wght@0,400;0,700;1,400;1,700&display=swap' href='https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,400;0,500;0,700;1,400;1,500;1,700&family=Rubik:ital,wght@0,400;0,700;1,400;1,700&display=swap'
rel='stylesheet' rel='stylesheet'
/> />
<meta charSet='UTF-8' /> <meta charSet='UTF-8' />
</Head> </Head>
<title>{config.siteName}</title>
<body> <body>
<noscript> <noscript>
Vous devez activer JavaScript pour utiliser ce site Vous devez activer JavaScript pour utiliser ce site

163
pages/api/favicon.ts Normal file
View File

@@ -0,0 +1,163 @@
import { NextApiRequest, NextApiResponse } from "next";
import { parse } from "node-html-parser";
const USER_AGENT =
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36 Edg/108.0.1462.54";
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
const urlRequest = (req.query?.url as string) || "";
if (!urlRequest) {
throw new Error("URL's missing");
}
try {
const { favicon, type, size } = await downloadImageFromUrl(
urlRequest + "/favicon.ico"
);
if (size === 0) {
throw new Error("Empty favicon");
}
if (!isImage(type)) {
throw new Error("Favicon path does not return an image");
}
return sendImage({
content: favicon,
res,
type,
size,
});
} catch (error) {
console.error(error);
}
try {
const requestDocument = await makeRequest(urlRequest);
const text = await requestDocument.text();
const faviconPath = findFaviconPath(text);
if (!faviconPath) {
throw new Error("Unable to find favicon path");
}
if (isBase64Image(faviconPath)) {
console.log("base64, convert it to buffer");
const buffer = convertBase64ToBuffer(faviconPath);
return sendImage({
content: buffer,
res,
type: "image/vnd.microsoft.icon",
size: buffer.length,
});
}
const pathWithoutFile = popLastSegment(requestDocument.url);
const finalUrl = buildFaviconUrl(faviconPath, pathWithoutFile);
const { favicon, type, size } = await downloadImageFromUrl(finalUrl);
if (!isImage(type)) {
throw new Error("Favicon path does not return an image");
}
return sendImage({
content: favicon,
res,
type,
size,
});
} catch (error) {
console.error(error);
res.status(404).send({ error: "Unable to retrieve favicon" });
}
}
async function makeRequest(url: string) {
const headers = new Headers();
headers.set("User-Agent", USER_AGENT);
const request = await fetch(url, { headers });
return request;
}
async function downloadImageFromUrl(url: string): Promise<{
favicon: Buffer;
url: string;
type: string;
size: number;
}> {
const request = await makeRequest(url);
const blob = await request.blob();
return {
favicon: Buffer.from(await blob.arrayBuffer()),
url: request.url,
type: blob.type,
size: blob.size,
};
}
function sendImage({
content,
res,
type,
size,
}: {
content: Buffer;
res: NextApiResponse;
type: string;
size: number;
}) {
res.setHeader("Content-Type", type);
res.setHeader("Content-Length", size);
res.send(content);
}
function findFaviconPath(text) {
const document = parse(text);
const links = document.querySelectorAll(
'link[rel="icon"], link[rel="shortcut icon"]'
);
const link = links.find(
(link) => !link.getAttribute("href").startsWith("data:image/")
);
if (!link) {
return console.warn("nothing, exit");
}
return link.getAttribute("href") || "";
}
function popLastSegment(url = "") {
const { href } = new URL(url);
const pathWithoutFile = href.split("/");
pathWithoutFile.pop();
return pathWithoutFile.join("/") || "";
}
function buildFaviconUrl(faviconPath, pathWithoutFile) {
if (faviconPath.startsWith("http")) {
console.log("startsWith http, result", faviconPath);
return faviconPath;
} else if (faviconPath.startsWith("/")) {
console.log("startsWith /, result", pathWithoutFile + faviconPath);
return pathWithoutFile + faviconPath;
} else {
console.log("else, result", pathWithoutFile + "/" + faviconPath);
return pathWithoutFile + "/" + faviconPath;
}
}
function isImage(type: string) {
return type.includes("image");
}
function isBase64Image(data) {
return data.startsWith("data:image/");
}
function convertBase64ToBuffer(base64 = ""): Buffer {
const buffer = Buffer.from(base64, "base64");
return buffer;
}

View File

@@ -1,40 +1,40 @@
import { useEffect, useState } from 'react'; import axios from 'axios';
import { useRouter } from 'next/router';
import nProgress from 'nprogress'; import nProgress from 'nprogress';
import axios, { AxiosResponse } from 'axios'; import { useMemo, useState } from 'react';
import { redirectWithoutClientCache } from '../../utils/client';
import { HandleAxiosError } from '../../utils/front';
import FormLayout from '../../components/FormLayout'; import FormLayout from '../../components/FormLayout';
import TextBox from '../../components/TextBox'; import TextBox from '../../components/TextBox';
import styles from '../../styles/create.module.scss'; import styles from '../../styles/create.module.scss';
import { HandleAxiosError } from '../../utils/front';
import { useRouter } from 'next/router';
function CreateCategory() { function CreateCategory() {
const router = useRouter();
const info = useRouter().query?.info as string; const info = useRouter().query?.info as string;
const [name, setName] = useState<string>(''); const [name, setName] = useState<string>('');
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState<string | null>(null); const [success, setSuccess] = useState<string | null>(null);
const [canSubmit, setCanSubmit] = useState<boolean>(false); const [submitted, setSubmitted] = useState<boolean>(false);
const canSubmit = useMemo<boolean>(() => name.length !== 0 && !submitted, [name.length, submitted]);
useEffect(() => setCanSubmit(name.length !== 0), [name]);
const handleSubmit = async (event) => { const handleSubmit = async (event) => {
event.preventDefault(); event.preventDefault();
setSuccess(null); setSuccess(null);
setError(null); setError(null);
setCanSubmit(false); setSubmitted(false);
nProgress.start(); nProgress.start();
try { try {
const payload = { name }; await axios.post('/api/category/create', { name });
const { data }: AxiosResponse<any> = await axios.post('/api/category/create', payload); redirectWithoutClientCache(router, '');
setSuccess(data?.success || 'Categorie créée avec succès'); router.push('/')
setCanSubmit(false);
} catch (error) { } catch (error) {
setError(HandleAxiosError(error)); setError(HandleAxiosError(error));
setCanSubmit(true); setSubmitted(true);
} finally { } finally {
nProgress.done(); nProgress.done();
} }

View File

@@ -1,20 +1,15 @@
import { useEffect, useState } from 'react';
import nProgress from 'nprogress';
import axios, { AxiosResponse } from 'axios'; import axios, { AxiosResponse } from 'axios';
import nProgress from 'nprogress';
import { confirmAlert } from 'react-confirm-alert'; import { useEffect, useState } from 'react';
import 'react-confirm-alert/src/react-confirm-alert.css';
import FormLayout from '../../../components/FormLayout'; import FormLayout from '../../../components/FormLayout';
import TextBox from '../../../components/TextBox'; import TextBox from '../../../components/TextBox';
import styles from '../../../styles/create.module.scss';
import { Category } from '../../../types'; import { Category } from '../../../types';
import { prisma } from '../../../utils/back';
import { BuildCategory, HandleAxiosError } from '../../../utils/front'; import { BuildCategory, HandleAxiosError } from '../../../utils/front';
import { prisma } from '../../../utils/back'; import styles from '../../../styles/create.module.scss';
function EditCategory({ category }: { category: Category; }) { function EditCategory({ category }: { category: Category; }) {
const [name, setName] = useState<string>(category.name); const [name, setName] = useState<string>(category.name);
@@ -33,32 +28,21 @@ function EditCategory({ category }: { category: Category; }) {
const handleSubmit = async (event) => { const handleSubmit = async (event) => {
event.preventDefault(); event.preventDefault();
confirmAlert({ setSuccess(null);
message: `Confirmer l'édition de la catégorie "${category.name}"`, setError(null);
buttons: [{ setCanSubmit(false);
label: 'Yes', nProgress.start();
onClick: async () => {
setSuccess(null);
setError(null);
setCanSubmit(false);
nProgress.start();
try { try {
const payload = { name }; const payload = { name };
const { data }: AxiosResponse<any> = await axios.put(`/api/category/edit/${category.id}`, payload); const { data }: AxiosResponse<any> = await axios.put(`/api/category/edit/${category.id}`, payload);
setSuccess(data?.success || 'Catégorie modifiée avec succès'); setSuccess(data?.success || 'Catégorie modifiée avec succès');
} catch (error) { } catch (error) {
setError(HandleAxiosError(error)); setError(HandleAxiosError(error));
} finally { } finally {
setCanSubmit(true); setCanSubmit(true);
nProgress.done(); nProgress.done();
} }
}
}, {
label: 'No',
onClick: () => { }
}]
});
} }
return (<> return (<>

View File

@@ -1,21 +1,16 @@
import { useEffect, useState } from 'react';
import nProgress from 'nprogress';
import axios, { AxiosResponse } from 'axios'; import axios, { AxiosResponse } from 'axios';
import nProgress from 'nprogress';
import { confirmAlert } from 'react-confirm-alert'; import { useEffect, useState } from 'react';
import 'react-confirm-alert/src/react-confirm-alert.css';
import FormLayout from '../../../components/FormLayout'; import FormLayout from '../../../components/FormLayout';
import TextBox from '../../../components/TextBox'; import TextBox from '../../../components/TextBox';
import styles from '../../../styles/create.module.scss';
import { Category } from '../../../types'; import { Category } from '../../../types';
import { BuildCategory, HandleAxiosError } from '../../../utils/front'; import { BuildCategory, HandleAxiosError } from '../../../utils/front';
import { prisma } from '../../../utils/back'; import { prisma } from '../../../utils/back';
import styles from '../../../styles/create.module.scss';
function RemoveCategory({ category }: { category: Category; }) { function RemoveCategory({ category }: { category: Category; }) {
const [canSubmit, setCanSubmit] = useState<boolean>(false); const [canSubmit, setCanSubmit] = useState<boolean>(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
@@ -36,32 +31,21 @@ function RemoveCategory({ category }: { category: Category; }) {
const handleSubmit = async (event) => { const handleSubmit = async (event) => {
event.preventDefault(); event.preventDefault();
confirmAlert({ setSuccess(null);
message: `Confirmer la suppression du lien "${category.name}"`, setError(null);
buttons: [{ setCanSubmit(false);
label: 'Yes', nProgress.start();
onClick: async () => {
setSuccess(null);
setError(null);
setCanSubmit(false);
nProgress.start();
try { try {
const { data }: AxiosResponse<any> = await axios.delete(`/api/category/remove/${category.id}`); const { data }: AxiosResponse<any> = await axios.delete(`/api/category/remove/${category.id}`);
setSuccess(data?.success || 'Categorie supprimée avec succès'); setSuccess(data?.success || 'Categorie supprimée avec succès');
setCanSubmit(false); setCanSubmit(false);
} catch (error) { } catch (error) {
setError(HandleAxiosError(error)); setError(HandleAxiosError(error));
setCanSubmit(true); setCanSubmit(true);
} finally { } finally {
nProgress.done(); nProgress.done();
} }
}
}, {
label: 'No',
onClick: () => { }
}]
});
} }
return (<> return (<>

View File

@@ -1,79 +1,68 @@
import { useState } from "react";
import { useSession } from "next-auth/react"; import { useSession } from "next-auth/react";
import Head from "next/head"; import { useState } from "react";
import Menu from "../components/Categories/SideMenu";
import Links from "../components/Links/Links"; import Links from "../components/Links/Links";
import SideMenu from "../components/SideMenu/SideMenu";
import { Category, Link } from "../types"; import { Category, Link } from "../types";
import { BuildCategory } from "../utils/front";
import { prisma } from "../utils/back"; import { prisma } from "../utils/back";
import { BuildCategory } from "../utils/front";
import { config } from "../config";
interface HomeProps { interface HomeProps {
categories: Category[]; categories: Category[];
favorites: Link[]; favorites: Link[];
} }
function Home({ categories, favorites }: HomeProps) { function Home({ categories, favorites }: HomeProps) {
const { data } = useSession({ required: true }); const { data } = useSession({ required: true });
const [categoryActive, setCategoryActive] = useState<Category | null>( const [categoryActive, setCategoryActive] = useState<Category | null>(
categories?.[0] categories?.[0]
); );
const handleSelectCategory = (category: Category) => const handleSelectCategory = (category: Category) =>
setCategoryActive(category); setCategoryActive(category);
return ( return (
<> <div className="App">
<Head> <SideMenu
<title>{config.siteName}</title> categories={categories}
</Head> favorites={favorites}
<div className="App"> handleSelectCategory={handleSelectCategory}
<Menu categoryActive={categoryActive}
categories={categories} session={data}
favorites={favorites} />
handleSelectCategory={handleSelectCategory} <Links category={categoryActive} />
categoryActive={categoryActive} </div>
session={data} );
/>
<Links category={categoryActive} />
</div>
</>
);
} }
export async function getServerSideProps() { export async function getServerSideProps() {
const categoriesDB = await prisma.category.findMany({ const categoriesDB = await prisma.category.findMany({
include: { links: true }, include: { links: true },
}); });
const favorites = [] as Link[]; const favorites = [] as Link[];
const categories = categoriesDB.map((categoryDB) => { const categories = categoriesDB.map((categoryDB) => {
const category = BuildCategory(categoryDB); const category = BuildCategory(categoryDB);
category.links.map((link) => category.links.map((link) => (link.favorite ? favorites.push(link) : null));
link.favorite ? favorites.push(link) : null return category;
); });
return category;
});
if (categories.length === 0) {
return {
redirect: {
destination:
"/category/create?info=Veuillez créer une catégorie",
},
};
}
if (categories.length === 0) {
return { return {
props: { redirect: {
categories: JSON.parse(JSON.stringify(categories)), destination: "/category/create",
favorites: JSON.parse(JSON.stringify(favorites)), },
},
}; };
}
return {
props: {
categories: JSON.parse(JSON.stringify(categories)),
favorites: JSON.parse(JSON.stringify(favorites)),
},
};
} }
Home.authRequired = true; Home.authRequired = true;

View File

@@ -1,43 +1,43 @@
import { useEffect, useState } from 'react';
import nProgress from 'nprogress';
import axios, { AxiosResponse } from 'axios'; import axios, { AxiosResponse } from 'axios';
import { useRouter } from 'next/router';
import nProgress from 'nprogress';
import { useMemo, useState } from 'react';
import FormLayout from '../../components/FormLayout';
import TextBox from '../../components/TextBox';
import Selector from '../../components/Selector';
import Checkbox from '../../components/Checkbox'; import Checkbox from '../../components/Checkbox';
import FormLayout from '../../components/FormLayout';
import Selector from '../../components/Selector';
import TextBox from '../../components/TextBox';
import styles from '../../styles/create.module.scss'; import { Category, Link } from '../../types';
import { Category } from '../../types';
import { BuildCategory, HandleAxiosError, IsValidURL } from '../../utils/front'; import { BuildCategory, HandleAxiosError, IsValidURL } from '../../utils/front';
import { prisma } from '../../utils/back'; import { prisma } from '../../utils/back';
import styles from '../../styles/create.module.scss';
function CreateLink({ categories }: { categories: Category[]; }) { function CreateLink({ categories }: { categories: Category[]; }) {
const [name, setName] = useState<string>(''); const { query } = useRouter();
const [url, setUrl] = useState<string>(''); const categoryIdQuery = Number(query.categoryId?.[0]);
const [favorite, setFavorite] = useState<boolean>(false);
const [categoryId, setCategoryId] = useState<number | null>(categories?.[0].id || null);
const [error, setError] = useState<string | null>(null); const [name, setName] = useState<Link['name']>('');
const [success, setSuccess] = useState<string | null>(null); const [url, setUrl] = useState<Link['url']>('');
const [canSubmit, setCanSubmit] = useState<boolean>(false); const [favorite, setFavorite] = useState<Link['favorite']>(false);
const [categoryId, setCategoryId] = useState<Link['category']['id']>(categoryIdQuery || categories?.[0].id || null);
useEffect(() => { const [error, setError] = useState<string>(null);
if (name !== '' && IsValidURL(url) && favorite !== null && categoryId !== null) { const [success, setSuccess] = useState<string>(null);
setCanSubmit(true); const [submitted, setSubmitted] = useState<boolean>(false);
} else {
setCanSubmit(false); const canSubmit = useMemo<boolean>(
} () => name !== '' && IsValidURL(url) && favorite !== null && categoryId !== null && !submitted,
}, [name, url, favorite, categoryId]); [name, url, favorite, categoryId, submitted]
);
const handleSubmit = async (event) => { const handleSubmit = async (event) => {
event.preventDefault(); event.preventDefault();
setSuccess(null); setSuccess(null);
setError(null); setError(null);
setCanSubmit(false); setSubmitted(false);
nProgress.start(); nProgress.start();
try { try {
@@ -47,7 +47,7 @@ function CreateLink({ categories }: { categories: Category[]; }) {
} catch (error) { } catch (error) {
setError(HandleAxiosError(error)); setError(HandleAxiosError(error));
} finally { } finally {
setCanSubmit(true); setSubmitted(true);
nProgress.done(); nProgress.done();
} }
} }

View File

@@ -1,56 +1,39 @@
import { useState } from 'react';
import axios, { AxiosResponse } from 'axios'; import axios, { AxiosResponse } from 'axios';
import nProgress from 'nprogress'; import nProgress from 'nprogress';
import { useState } from 'react';
import { confirmAlert } from 'react-confirm-alert'; import Checkbox from '../../../components/Checkbox';
import 'react-confirm-alert/src/react-confirm-alert.css';
import FormLayout from '../../../components/FormLayout'; import FormLayout from '../../../components/FormLayout';
import TextBox from '../../../components/TextBox'; import TextBox from '../../../components/TextBox';
import Checkbox from '../../../components/Checkbox';
import styles from '../../../styles/create.module.scss';
import { Link } from '../../../types'; import { Link } from '../../../types';
import { BuildLink, HandleAxiosError } from '../../../utils/front'; import { BuildLink, HandleAxiosError } from '../../../utils/front';
import { prisma } from '../../../utils/back'; import { prisma } from '../../../utils/back';
function RemoveLink({ link }: { link: Link; }) { import styles from '../../../styles/create.module.scss';
function RemoveLink({ link }: { link: Link; }) {
const [canSubmit, setCanSubmit] = useState<boolean>(true); const [canSubmit, setCanSubmit] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState<string | null>(null); const [success, setSuccess] = useState<string | null>(null);
const handleSubmit = (event) => { const handleSubmit = async (event) => {
event.preventDefault(); event.preventDefault();
confirmAlert({ setSuccess(null);
message: `Confirmer la suppression du lien "${link.name}"`, setError(null);
buttons: [{ setCanSubmit(false);
label: 'Confirmer', nProgress.start();
onClick: async () => {
setSuccess(null);
setError(null);
setCanSubmit(false);
nProgress.start();
try { try {
const { data }: AxiosResponse<any> = await axios.delete(`/api/link/remove/${link.id}`); const { data }: AxiosResponse<any> = await axios.delete(`/api/link/remove/${link.id}`);
setSuccess(data?.success || 'Lien supprimé avec succès'); setSuccess(data?.success || 'Lien supprimé avec succès');
setCanSubmit(false); setCanSubmit(false);
} catch (error) { } catch (error) {
setError(HandleAxiosError(error)); setError(HandleAxiosError(error));
setCanSubmit(true); setCanSubmit(true);
} finally { } finally {
nProgress.done(); nProgress.done();
} }
}
}, {
label: 'Annuler',
onClick: () => { }
}]
});
} }
return (<> return (<>

View File

@@ -1,11 +1,10 @@
import { Provider } from "next-auth/providers"; import { Provider } from "next-auth/providers";
import { getProviders, signIn, useSession } from "next-auth/react"; import { getProviders, signIn, useSession } from "next-auth/react";
import Head from "next/head"; import { NextSeo } from "next-seo";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import MessageManager from "../components/MessageManager"; import MessageManager from "../components/MessageManager/MessageManager";
import { config } from "../config";
import styles from "../styles/login.module.scss"; import styles from "../styles/login.module.scss";
@@ -26,9 +25,7 @@ export default function SignIn({ providers }: { providers: Provider[] }) {
return ( return (
<> <>
<Head> <NextSeo title="Authentification" />
<title>{config.siteName} Authentification</title>
</Head>
<div className="App"> <div className="App">
<div className={styles["wrapper"]}> <div className={styles["wrapper"]}>
<h2>Se connecter</h2> <h2>Se connecter</h2>

View File

@@ -83,7 +83,7 @@ li {
} }
} }
button { button:not(.reset) {
cursor: pointer; cursor: pointer;
width: 100%; width: 100%;
color: #fff; color: #fff;
@@ -172,6 +172,14 @@ select:not(.nostyle) {
} }
} }
.reset {
background-color: inherit;
color: inherit;
padding: 0;
margin: 0;
border: 0;
}
@media (max-width: 1280px) { @media (max-width: 1280px) {
.App { .App {
width: 100%; width: 100%;

View File

@@ -1,225 +0,0 @@
.categories-wrapper {
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;
& .block-wrapper {
height: auto;
width: 100%;
& h4 {
user-select: none;
text-transform: uppercase;
font-size: 0.85em;
color: #bbb;
margin-bottom: 5px;
}
// List items
& .items {
animation: fadein 0.3s both;
}
& .items .item {
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;
}
&.active {
color: #fff;
background: #3f88c5;
border-color: #3f88c5;
}
&:hover:not(.active) {
color: #3f88c5;
background: #f0eef6;
border-bottom: 2px solid #3f88c5;
}
}
}
// Favorites
& .block-wrapper.favorites {
margin-bottom: 15px;
& .items .item {
padding: 0;
display: flex;
align-items: center;
}
& .items .no-fav-link {
user-select: none;
text-align: center;
font-style: italic;
font-size: 0.9em;
color: #bbb;
}
& .items .item a {
width: 100%;
text-decoration: none;
color: inherit;
padding: 7px 12px;
border: 0 !important;
}
& .items .item .category {
color: #bbb;
font-size: 0.85em;
}
}
// Categories
& .block-wrapper.categories {
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 .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;
}
}
}
}
// Controls
.controls {
margin: 10px 0;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
}
// User Card
& .user-card-wrapper {
position: relative;
user-select: none;
height: fit-content;
width: 100%;
color: #333;
background-color: #fff;
& .user-card {
border: 1px solid #dadce0;
padding: 7px 12px;
display: flex;
gap: 10px;
align-items: center;
& img {
border-radius: 50%;
}
}
& .disconnect-btn {
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 100%;
color: #fff;
display: none;
}
&:hover .disconnect-btn {
display: block;
}
}
}
@keyframes fadein {
0% {
transform: translateX(-15px);
opacity: 0;
}
100% {
transform: translateX(0);
opacity: 1;
}
}

View File

@@ -1,139 +0,0 @@
.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;
& .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
}
& .links .link {
user-select: none;
cursor: pointer;
height: fit-content;
width: 100%;
color: #3f88c5;
background-color: #fff;
padding: 10px 15px;
border: 1px solid #dadce0;
border-bottom: 2px solid #dadce0;
border-radius: 3px;
margin-bottom: 10px;
display: flex;
align-items: center;
transition: 0.15s;
&:hover {
border-bottom-color: #3f88c5;
background: #f0eef6;
& .url-pathname {
animation: fadein 0.3s both;
}
& .controls {
display: flex;
animation: fadein 0.3s both;
}
}
& > 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 .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;
}
}
}
& .controls {
display: none;
align-items: center;
justify-content: center;
gap: 10px;
& > a {
border: 0;
margin: 0;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
&:hover {
transform: scale(1.3);
}
& svg {
height: 20px;
width: 20px;
}
}
}
}
}
@keyframes fadein {
0% {
transform: translateX(-15px);
opacity: 0;
}
100% {
transform: translateX(0);
opacity: 1;
}
}

60
test.js Normal file
View File

@@ -0,0 +1,60 @@
(async () => {
const request = await fetch("https://sdtream.sonnydata.fr");
const text = await request.text();
const faviconPath = findFaviconPath(text);
if (!faviconPath) {
return console.log("Unable to find favicon path");
}
if (isBase64Image(faviconPath)) {
console.log("base64, convert it to buffer");
const buffer = convertBase64ToBuffer(faviconPath);
return console.log(buffer);
}
const pathWithoutFile = popLastSegment(request.url);
console.log("pathWithoutFile", pathWithoutFile);
const result = buildFaviconUrl(faviconPath, pathWithoutFile);
console.log(result);
})();
function findFaviconPath(text) {
const regex = /rel=['"](?:shortcut )?icon['"] href=['"]([^?'"]+)[?'"]/i;
const found = text.match(regex);
if (!found) {
return console.warn("nothing, exit");
}
const faviconPath = found?.[1];
return faviconPath || null;
}
function popLastSegment(url = "") {
const { href } = new URL(url);
const pathWithoutFile = href.split("/");
pathWithoutFile.pop();
return pathWithoutFile.join("/") || "";
}
function buildFaviconUrl(faviconPath, pathWithoutFile) {
if (faviconPath.startsWith("http")) {
console.log("startsWith http, result", faviconPath);
return faviconPath;
} else if (faviconPath.startsWith("/")) {
console.log("startsWith /, result", pathWithoutFile + faviconPath);
return pathWithoutFile + faviconPath;
} else {
console.log("else, result", pathWithoutFile + "/" + faviconPath);
return pathWithoutFile + "/" + faviconPath;
}
}
function isBase64Image(data) {
return data.startsWith("data:image/");
}
function convertBase64ToBuffer(base64 = "") {
return new Buffer.from(base64, "base64");
}

View File

@@ -1,31 +1,20 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "es5", "target": "es5",
"lib": [ "lib": ["dom", "dom.iterable", "esnext"],
"dom", "allowJs": true,
"dom.iterable", "skipLibCheck": true,
"esnext" "strict": false,
], "forceConsistentCasingInFileNames": true,
"allowJs": true, "noEmit": true,
"skipLibCheck": true, "esModuleInterop": true,
"strict": false, "module": "esnext",
"forceConsistentCasingInFileNames": true, "moduleResolution": "node",
"noEmit": true, "resolveJsonModule": true,
"esModuleInterop": true, "isolatedModules": true,
"module": "esnext", "jsx": "preserve",
"moduleResolution": "node", "incremental": true
"resolveJsonModule": true, },
"isolatedModules": true, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"jsx": "preserve", "exclude": ["node_modules"]
"incremental": true
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx"
],
"exclude": [
"node_modules"
]
} }

7
utils/client.ts Normal file
View File

@@ -0,0 +1,7 @@
import { NextRouter } from 'next/router';
export function redirectWithoutClientCache(router: NextRouter, url: string) {
router.push(url, undefined, {
unstable_skipClientCache: true,
});
}

3
utils/link.ts Normal file
View File

@@ -0,0 +1,3 @@
export function faviconLinkBuilder(origin: string, size: number = 32) {
return `http://localhost:3000/api/favicon?url=${origin}`;
}