refacto: create src dir & change project indentation to 2 spaces

This commit is contained in:
Sonny
2023-04-24 23:53:32 +02:00
parent acce5666e5
commit 426f2a52df
88 changed files with 1472 additions and 1490 deletions

9
.editorconfig Normal file
View File

@@ -0,0 +1,9 @@
root = true
# Unix-style newlines with a newline ending every file
[*]
end_of_line = lf
insert_final_newline = true
charset = utf-8
indent_style = space
indent_size = 2

View File

@@ -1,20 +0,0 @@
import { useSession } from 'next-auth/react'
import { useRouter } from 'next/router';
export default function Auth({ children }) {
const router = useRouter();
const { status } = useSession({
required: true,
onUnauthenticated: () => router.push(`/signin?info=${encodeURI('Vous devez être connecté pour accéder à cette page')}`)
});
if (status === 'loading') {
return (
<div className='App' style={{ alignItems: 'center' }}>
<p style={{ height: 'fit-content' }}>Chargement de la session en cours</p>
</div>
);
}
return children;
}

View File

@@ -1,38 +0,0 @@
@import "../../styles/keyframes.scss";
@import "../../styles/colors.scss";
.block-wrapper {
height: auto;
width: 100%;
& h4 {
user-select: none;
text-transform: uppercase;
font-size: 0.85em;
font-weight: 500;
color: $grey;
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: $white;
padding: 7px 12px;
border: 1px solid $lightest-grey;
border-bottom: 2px solid $lightest-grey;
border-radius: 3px;
transition: 0.15s;
&:not(:last-child) {
margin-bottom: 5px;
}
}
}

View File

@@ -1,156 +0,0 @@
@import "../../styles/keyframes.scss";
@import "../../styles/colors.scss";
.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: $blue;
margin-bottom: 15px;
font-weight: 500;
& .links-count {
color: $grey;
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: $blue;
background-color: $white;
padding: 10px 15px;
border: 1px solid $lightest-grey;
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 $light-blue;
& .url-pathname {
animation: fadein 0.3s both;
}
& .controls {
display: flex;
animation: fadein 0.3s both;
}
}
}
.link > a {
height: 100%;
max-width: calc(100% - 125px); // TODO: faut fix ça, c'est pas beau
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: $grey;
font-size: 0.9em;
}
& .link-url {
width: 100%;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
color: $grey;
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: $white;
& > * {
animation: rotate 1s both reverse infinite linear;
}
}

View File

@@ -1,40 +0,0 @@
@import "../../styles/colors.scss";
.info-msg {
height: fit-content;
width: 100%;
text-align: center;
font-style: italic;
font-size: 0.9em;
color: $dark-blue;
background-color: $lightest-blue;
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: $red;
background-color: $light-red;
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: $light-green;
padding: 10px;
border-radius: 3px;
animation: fadein 250ms both;
}

View File

@@ -1,54 +0,0 @@
@import "../../styles/colors.scss";
@import "../../styles/keyframes.scss";
.modal-wrapper {
z-index: 9999;
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 100%;
background: rgba($black, 0.5);
backdrop-filter: blur(0.25em);
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
animation: opacityin 0.3s both;
}
.modal-container {
background: $light-grey;
min-width: 500px;
padding: 1em 1.5em;
border-radius: 3px;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
animation: fadeintop 0.3s both;
}
.modal-header {
width: 100%;
margin-bottom: 1.5em;
display: flex;
align-items: center;
justify-content: space-between;
& button.btn-close {
color: $blue;
background-color: transparent;
border: 0;
padding: 0;
margin: 0;
}
}
.modal-body {
width: 100%;
display: flex;
flex: 1;
align-items: center;
flex-direction: column;
}

View File

@@ -1,68 +0,0 @@
import { MutableRefObject, useEffect, useState } from 'react';
import Select, { OptionsOrGroups, GroupBase } from 'react-select';
type Option = { label: string | number; value: string | number; }
interface SelectorProps {
name: string;
label?: string;
labelComponent?: JSX.Element;
innerRef?: MutableRefObject<any>;
fieldClass?: string;
options: OptionsOrGroups<Option, GroupBase<Option>>;
value?: number | string;
onChangeCallback?: (value: number | string) => void;
disabled?: boolean;
}
export default function Selector({
name,
label,
labelComponent,
innerRef = null,
fieldClass = '',
value,
options = [],
onChangeCallback,
disabled = false
}: SelectorProps): JSX.Element {
const [selectorValue, setSelectorValue] = useState<Option>();
useEffect(() => {
if (options.length === 0) return;
const option = options.find((o: Option) => o.value === value) as Option;
if (option) {
setSelectorValue(option);
}
}, [options, value]);
function handleChange(selectedOption: Option) {
setSelectorValue(selectedOption);
if (onChangeCallback) {
onChangeCallback(selectedOption.value);
}
}
return (<div className={`input-field ${fieldClass}`}>
{label && (
<label htmlFor={name} title={`${name} field`}>
{label}
</label>
)}
{labelComponent && (
<label htmlFor={name} title={`${name} field`}>
{labelComponent}
</label>
)}
<Select
value={selectorValue}
onChange={handleChange}
options={options}
ref={innerRef}
isDisabled={disabled}
/>
</div>);
}

View File

@@ -1,93 +0,0 @@
@import "../../../styles/keyframes.scss";
@import "../../../styles/colors.scss";
.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: $white;
background: $blue;
border-color: $blue;
}
&:hover:not(.active) {
color: $blue;
background: $white;
border-bottom: 2px solid $blue;
}
&.active .menu-item .option-edit svg {
fill: $white;
}
& .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: $grey;
}
}
&: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

@@ -1,57 +0,0 @@
@import "../../../styles/colors.scss";
.favorites {
height: auto;
width: 100%;
margin-bottom: 15px;
& h4 {
user-select: none;
text-transform: uppercase;
font-size: 0.85em;
font-weight: 500;
color: $grey;
margin-bottom: 5px;
}
}
.favorites ul.items li.item {
width: 100%;
background-color: $white;
padding: 0;
border: 1px solid $lightest-grey;
border-bottom: 2px solid $lightest-grey;
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: $grey;
font-size: 0.85em;
}
&:not(:last-child) {
margin-bottom: 5px;
}
&.active {
color: $white;
background: $blue;
border-color: $blue;
}
&:hover:not(.active) {
color: $blue;
background: $light-grey;
border-bottom: 2px solid $blue;
}
}

View File

@@ -1,35 +0,0 @@
@import "../../../styles/colors.scss";
.user-card-wrapper {
user-select: none;
height: fit-content;
width: 100%;
color: $black;
background-color: $white;
border: 1px solid $lightest-grey;
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: $blue;
display: flex;
transition: 0.15s;
&:hover {
transform: scale(1.3);
}
}
}

View File

@@ -1,27 +0,0 @@
@import "../../styles/colors.scss";
.side-menu {
height: 100%;
width: 325px;
padding: 0 25px 0 10px;
border-right: 1px solid $lightest-grey;
margin-right: 15px;
display: flex;
align-items: center;
flex-direction: column;
overflow: hidden;
}
.menu-controls {
margin: 10px 0;
display: flex;
gap: 0.25em;
align-items: center;
justify-content: center;
flex-direction: column;
& .action {
display: flex;
gap: 0.5em;
}
}

View File

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

View File

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

View File

@@ -1,23 +0,0 @@
import { Head, Html, Main, NextScript } from 'next/document';
const Document = () => (
<Html lang='fr'>
<Head>
<meta name="theme-color" content="#f0eef6" />
<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'
rel='stylesheet'
/>
<meta charSet='UTF-8' />
</Head>
<body>
<noscript>
Vous devez activer JavaScript pour utiliser ce site
</noscript>
<Main />
<NextScript />
</body>
</Html>
)
export default Document;

View File

@@ -1,99 +0,0 @@
import { PrismaClient } from "@prisma/client";
import NextAuth from "next-auth";
import GoogleProvider from "next-auth/providers/google";
const prisma = new PrismaClient();
export default NextAuth({
providers: [
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
authorization: {
params: {
prompt: "consent",
access_type: "offline",
response_type: "code",
},
},
}),
],
callbacks: {
async signIn({ account: accountParam, profile }) {
// TODO: Auth
console.log(
"Connexion via",
accountParam.provider,
accountParam.providerAccountId,
profile.email,
profile.name
);
if (accountParam.provider !== "google") {
return (
"/signin?error=" +
encodeURI("Authentitifcation via Google requise")
);
}
const email = profile?.email;
if (email === "") {
return (
"/signin?error=" +
encodeURI(
"Impossible de récupérer l'email associé à ce compte Google"
)
);
}
const googleId = profile?.sub;
if (googleId === "") {
return (
"/signin?error=" +
encodeURI(
"Impossible de récupérer l'identifiant associé à ce compte Google"
)
);
}
try {
const account = await prisma.user.findFirst({
where: {
google_id: googleId,
email,
},
});
const accountCount = await prisma.user.count();
if (!account) {
if (accountCount === 0) {
await prisma.user.create({
data: {
email,
google_id: googleId,
},
});
return true;
}
return (
"/signin?error=" +
encodeURI(
"Vous n'êtes pas autorisé à vous connecter avec ce compte Google"
)
);
} else {
return true;
}
} catch (error) {
console.error(error);
return (
"/signin?error=" +
encodeURI(
"Une erreur est survenue lors de l'authentification"
)
);
}
},
},
pages: {
signIn: "/signin",
error: "/signin",
},
session: {
maxAge: 60 * 60 * 6, // Session de 6 heures
},
});

View File

@@ -1,30 +0,0 @@
import { NextApiRequest, NextApiResponse } from 'next';
import { prisma } from '../../../../utils/back';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const { cid } = req.query;
try {
const category = await prisma.category.findFirst({
where: { id: Number(cid) }
});
if (!category) {
return res.status(400).send({ error: 'Categorie introuvable' });
}
} catch (error) {
console.error(error);
return res.status(400).send({ error: 'Une erreur est survenue lors de la suppression de la catégorie (category/remove->findCategory)' });
}
try {
await prisma.category.delete({
where: { id: Number(cid) }
});
return res.status(200).send({ success: 'La catégorie a été supprimée avec succès' });
} catch (error) {
console.error(error);
return res.status(400).send({ error: 'Une erreur est survenue lors de la suppression de la catégorie (category/remove->deleteCategory)' });
}
};

View File

@@ -1,163 +0,0 @@
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("[Favicon] Unable to find favicon path");
}
if (isBase64Image(faviconPath)) {
console.log("[Favicon] base64, convert it to buffer");
const buffer = convertBase64ToBuffer(faviconPath);
return sendImage({
content: buffer,
res,
type: "image/x-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

@@ -0,0 +1,27 @@
import { useSession } from "next-auth/react";
import { useRouter } from "next/router";
export default function Auth({ children }) {
const router = useRouter();
const { status } = useSession({
required: true,
onUnauthenticated: () =>
router.push(
`/signin?info=${encodeURI(
"Vous devez être connecté pour accéder à cette page"
)}`
),
});
if (status === "loading") {
return (
<div className="App" style={{ alignItems: "center" }}>
<p style={{ height: "fit-content" }}>
Chargement de la session en cours
</p>
</div>
);
}
return children;
}

View File

@@ -0,0 +1,38 @@
@import "styles/keyframes.scss";
@import "styles/colors.scss";
.block-wrapper {
height: auto;
width: 100%;
& h4 {
user-select: none;
text-transform: uppercase;
font-size: 0.85em;
font-weight: 500;
color: $grey;
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: $white;
padding: 7px 12px;
border: 1px solid $lightest-grey;
border-bottom: 2px solid $lightest-grey;
border-radius: 3px;
transition: 0.15s;
&:not(:last-child) {
margin-bottom: 5px;
}
}
}

View File

@@ -1,9 +1,9 @@
import { NextSeo } from "next-seo";
import Link from "next/link";
import MessageManager from "./MessageManager/MessageManager";
import MessageManager from "components/MessageManager/MessageManager";
import styles from "../styles/create.module.scss";
import styles from "styles/create.module.scss";
interface FormProps {
title: string;

View File

@@ -3,7 +3,7 @@ import { useState } from "react";
import { TbLoader3 } from "react-icons/tb";
import { TfiWorld } from "react-icons/tfi";
import { faviconLinkBuilder } from "../../utils/link";
import { faviconLinkBuilder } from "utils/link";
import styles from "./links.module.scss";

View File

@@ -7,7 +7,7 @@ import {
AiOutlineStar,
} from "react-icons/ai";
import { Link } from "../../types";
import { Link } from "types";
import LinkFavicon from "./LinkFavicon";
import styles from "./links.module.scss";

View File

@@ -1,6 +1,6 @@
import LinkTag from "next/link";
import { Category } from "../../types";
import { Category } from "types";
import LinkItem from "./LinkItem";
import styles from "./links.module.scss";

View File

@@ -0,0 +1,156 @@
@import "styles/keyframes.scss";
@import "styles/colors.scss";
.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: $blue;
margin-bottom: 15px;
font-weight: 500;
& .links-count {
color: $grey;
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: $blue;
background-color: $white;
padding: 10px 15px;
border: 1px solid $lightest-grey;
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 $light-blue;
& .url-pathname {
animation: fadein 0.3s both;
}
& .controls {
display: flex;
animation: fadein 0.3s both;
}
}
}
.link > a {
height: 100%;
max-width: calc(100% - 125px); // TODO: faut fix ça, c'est pas beau
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: $grey;
font-size: 0.9em;
}
& .link-url {
width: 100%;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
color: $grey;
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: $white;
& > * {
animation: rotate 1s both reverse infinite linear;
}
}

View File

@@ -0,0 +1,40 @@
@import "styles/colors.scss";
.info-msg {
height: fit-content;
width: 100%;
text-align: center;
font-style: italic;
font-size: 0.9em;
color: $dark-blue;
background-color: $lightest-blue;
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: $red;
background-color: $light-red;
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: $light-green;
padding: 10px;
border-radius: 3px;
animation: fadein 250ms both;
}

View File

@@ -0,0 +1,54 @@
@import "styles/colors.scss";
@import "styles/keyframes.scss";
.modal-wrapper {
z-index: 9999;
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 100%;
background: rgba($black, 0.5);
backdrop-filter: blur(0.25em);
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
animation: opacityin 0.3s both;
}
.modal-container {
background: $light-grey;
min-width: 500px;
padding: 1em 1.5em;
border-radius: 3px;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
animation: fadeintop 0.3s both;
}
.modal-header {
width: 100%;
margin-bottom: 1.5em;
display: flex;
align-items: center;
justify-content: space-between;
& button.btn-close {
color: $blue;
background-color: transparent;
border: 0;
padding: 0;
margin: 0;
}
}
.modal-body {
width: 100%;
display: flex;
flex: 1;
align-items: center;
flex-direction: column;
}

View File

@@ -2,13 +2,13 @@ import LinkTag from "next/link";
import { ReactNode, useCallback, useMemo, useState } from "react";
import { FcGoogle } from "react-icons/fc";
import useAutoFocus from "../../hooks/useAutoFocus";
import useAutoFocus from "hooks/useAutoFocus";
import LinkFavicon from "../Links/LinkFavicon";
import Modal from "../Modal/Modal";
import TextBox from "../TextBox";
import LinkFavicon from "components/Links/LinkFavicon";
import Modal from "components/Modal/Modal";
import TextBox from "components/TextBox";
import { Category, ItemComplete, Link } from "../../types";
import { Category, ItemComplete, Link } from "types";
import styles from "./search.module.scss";

View File

@@ -0,0 +1,70 @@
import { MutableRefObject, useEffect, useState } from "react";
import Select, { OptionsOrGroups, GroupBase } from "react-select";
type Option = { label: string | number; value: string | number };
interface SelectorProps {
name: string;
label?: string;
labelComponent?: JSX.Element;
innerRef?: MutableRefObject<any>;
fieldClass?: string;
options: OptionsOrGroups<Option, GroupBase<Option>>;
value?: number | string;
onChangeCallback?: (value: number | string) => void;
disabled?: boolean;
}
export default function Selector({
name,
label,
labelComponent,
innerRef = null,
fieldClass = "",
value,
options = [],
onChangeCallback,
disabled = false,
}: SelectorProps): JSX.Element {
const [selectorValue, setSelectorValue] = useState<Option>();
useEffect(() => {
if (options.length === 0) return;
const option = options.find((o: Option) => o.value === value) as Option;
if (option) {
setSelectorValue(option);
}
}, [options, value]);
function handleChange(selectedOption: Option) {
setSelectorValue(selectedOption);
if (onChangeCallback) {
onChangeCallback(selectedOption.value);
}
}
return (
<div className={`input-field ${fieldClass}`}>
{label && (
<label htmlFor={name} title={`${name} field`}>
{label}
</label>
)}
{labelComponent && (
<label htmlFor={name} title={`${name} field`}>
{labelComponent}
</label>
)}
<Select
value={selectorValue}
onChange={handleChange}
options={options}
ref={innerRef}
isDisabled={disabled}
/>
</div>
);
}

View File

@@ -1,4 +1,4 @@
import { Category } from "../../../types";
import { Category } from "types";
import CategoryItem from "./CategoryItem";
import styles from "./categories.module.scss";

View File

@@ -1,9 +1,9 @@
import LinkTag from "next/link";
import { useEffect, useRef } from "react";
import { AiFillDelete, AiFillEdit } from "react-icons/ai";
import { Category } from "../../../types";
import { Category } from "types";
import { useEffect, useRef } from "react";
import styles from "./categories.module.scss";
interface CategoryItemProps {

View File

@@ -0,0 +1,93 @@
@import "styles/keyframes.scss";
@import "styles/colors.scss";
.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: $white;
background: $blue;
border-color: $blue;
}
&:hover:not(.active) {
color: $blue;
background: $white;
border-bottom: 2px solid $blue;
}
&.active .menu-item .option-edit svg {
fill: $white;
}
& .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: $grey;
}
}
&: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

@@ -1,7 +1,7 @@
import LinkTag from "next/link";
import { Link } from "../../../types";
import LinkFavicon from "../../Links/LinkFavicon";
import LinkFavicon from "components/Links/LinkFavicon";
import { Link } from "types";
import styles from "./favorites.module.scss";

View File

@@ -1,4 +1,4 @@
import { Link } from "../../../types";
import { Link } from "types";
import FavoriteItem from "./FavoriteItem";
import styles from "./favorites.module.scss";

View File

@@ -0,0 +1,57 @@
@import "styles/colors.scss";
.favorites {
height: auto;
width: 100%;
margin-bottom: 15px;
& h4 {
user-select: none;
text-transform: uppercase;
font-size: 0.85em;
font-weight: 500;
color: $grey;
margin-bottom: 5px;
}
}
.favorites ul.items li.item {
width: 100%;
background-color: $white;
padding: 0;
border: 1px solid $lightest-grey;
border-bottom: 2px solid $lightest-grey;
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: $grey;
font-size: 0.85em;
}
&:not(:last-child) {
margin-bottom: 5px;
}
&.active {
color: $white;
background: $blue;
border-color: $blue;
}
&:hover:not(.active) {
color: $blue;
background: $light-grey;
border-bottom: 2px solid $blue;
}
}

View File

@@ -1,11 +1,11 @@
import LinkTag from "next/link";
import BlockWrapper from "../BlockWrapper/BlockWrapper";
import BlockWrapper from "components/BlockWrapper/BlockWrapper";
import Categories from "./Categories/Categories";
import Favorites from "./Favorites/Favorites";
import UserCard from "./UserCard/UserCard";
import { Category, Link } from "../../types";
import { Category, Link } from "types";
import styles from "./sidemenu.module.scss";

View File

@@ -0,0 +1,35 @@
@import "styles/colors.scss";
.user-card-wrapper {
user-select: none;
height: fit-content;
width: 100%;
color: $black;
background-color: $white;
border: 1px solid $lightest-grey;
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: $blue;
display: flex;
transition: 0.15s;
&:hover {
transform: scale(1.3);
}
}
}

View File

@@ -0,0 +1,27 @@
@import "styles/colors.scss";
.side-menu {
height: 100%;
width: 325px;
padding: 0 25px 0 10px;
border-right: 1px solid $lightest-grey;
margin-right: 15px;
display: flex;
align-items: center;
flex-direction: column;
overflow: hidden;
}
.menu-controls {
margin: 10px 0;
display: flex;
gap: 0.25em;
align-items: center;
justify-content: center;
flex-direction: column;
& .action {
display: flex;
gap: 0.5em;
}
}

15
src/pages/404.tsx Normal file
View File

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

15
src/pages/500.tsx Normal file
View File

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

View File

@@ -4,10 +4,10 @@ import { useRouter } from "next/router";
import nProgress from "nprogress";
import { useEffect } from "react";
import AuthRequired from "../components/AuthRequired";
import AuthRequired from "components/AuthRequired";
import "nprogress/nprogress.css";
import "../styles/globals.scss";
import "styles/globals.scss";
function MyApp({ Component, pageProps: { session, ...pageProps } }) {
const router = useRouter();

21
src/pages/_document.tsx Normal file
View File

@@ -0,0 +1,21 @@
import { Head, Html, Main, NextScript } from "next/document";
const Document = () => (
<Html lang="fr">
<Head>
<meta name="theme-color" content="#f0eef6" />
<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"
rel="stylesheet"
/>
<meta charSet="UTF-8" />
</Head>
<body>
<noscript>Vous devez activer JavaScript pour utiliser ce site</noscript>
<Main />
<NextScript />
</body>
</Html>
);
export default Document;

View File

@@ -0,0 +1,97 @@
import { PrismaClient } from "@prisma/client";
import NextAuth from "next-auth";
import GoogleProvider from "next-auth/providers/google";
const prisma = new PrismaClient();
export default NextAuth({
providers: [
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
authorization: {
params: {
prompt: "consent",
access_type: "offline",
response_type: "code",
},
},
}),
],
callbacks: {
async signIn({ account: accountParam, profile }) {
// TODO: Auth
console.log(
"Connexion via",
accountParam.provider,
accountParam.providerAccountId,
profile.email,
profile.name
);
if (accountParam.provider !== "google") {
return (
"/signin?error=" + encodeURI("Authentitifcation via Google requise")
);
}
const email = profile?.email;
if (email === "") {
return (
"/signin?error=" +
encodeURI(
"Impossible de récupérer l'email associé à ce compte Google"
)
);
}
const googleId = profile?.sub;
if (googleId === "") {
return (
"/signin?error=" +
encodeURI(
"Impossible de récupérer l'identifiant associé à ce compte Google"
)
);
}
try {
const account = await prisma.user.findFirst({
where: {
google_id: googleId,
email,
},
});
const accountCount = await prisma.user.count();
if (!account) {
if (accountCount === 0) {
await prisma.user.create({
data: {
email,
google_id: googleId,
},
});
return true;
}
return (
"/signin?error=" +
encodeURI(
"Vous n'êtes pas autorisé à vous connecter avec ce compte Google"
)
);
} else {
return true;
}
} catch (error) {
console.error(error);
return (
"/signin?error=" +
encodeURI("Une erreur est survenue lors de l'authentification")
);
}
},
},
pages: {
signIn: "/signin",
error: "/signin",
},
session: {
maxAge: 60 * 60 * 6, // Session de 6 heures
},
});

View File

@@ -1,5 +1,5 @@
import { NextApiRequest, NextApiResponse } from "next";
import { prisma } from "../../../utils/back";
import { prisma } from "utils/back";
export default async function handler(
req: NextApiRequest,

View File

@@ -1,5 +1,5 @@
import { NextApiRequest, NextApiResponse } from "next";
import { prisma } from "../../../../utils/back";
import { prisma } from "utils/back";
export default async function handler(
req: NextApiRequest,

View File

@@ -0,0 +1,41 @@
import { NextApiRequest, NextApiResponse } from "next";
import { prisma } from "utils/back";
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
const { cid } = req.query;
try {
const category = await prisma.category.findFirst({
where: { id: Number(cid) },
});
if (!category) {
return res.status(400).send({ error: "Categorie introuvable" });
}
} catch (error) {
console.error(error);
return res.status(400).send({
error:
"Une erreur est survenue lors de la suppression de la catégorie (category/remove->findCategory)",
});
}
try {
await prisma.category.delete({
where: { id: Number(cid) },
});
return res
.status(200)
.send({ success: "La catégorie a été supprimée avec succès" });
} catch (error) {
console.error(error);
return res.status(400).send({
error:
"Une erreur est survenue lors de la suppression de la catégorie (category/remove->deleteCategory)",
});
}
}

163
src/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("[Favicon] Unable to find favicon path");
}
if (isBase64Image(faviconPath)) {
console.log("[Favicon] base64, convert it to buffer");
const buffer = convertBase64ToBuffer(faviconPath);
return sendImage({
content: buffer,
res,
type: "image/x-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,5 +1,5 @@
import { NextApiRequest, NextApiResponse } from "next";
import { prisma } from "../../../utils/back";
import { prisma } from "utils/back";
export default async function handler(
req: NextApiRequest,

View File

@@ -1,5 +1,5 @@
import { NextApiRequest, NextApiResponse } from "next";
import { prisma } from "../../../../utils/back";
import { prisma } from "utils/back";
// TODO: Ajouter vérification -> l'utilisateur doit changer au moins un champ
export default async function handler(

View File

@@ -1,5 +1,5 @@
import { NextApiRequest, NextApiResponse } from "next";
import { prisma } from "../../../../utils/back";
import { prisma } from "utils/back";
export default async function handler(
req: NextApiRequest,

View File

@@ -3,15 +3,15 @@ import { useRouter } from "next/router";
import nProgress from "nprogress";
import { useMemo, useState } from "react";
import useAutoFocus from "../../hooks/useAutoFocus";
import useAutoFocus from "hooks/useAutoFocus";
import FormLayout from "../../components/FormLayout";
import TextBox from "../../components/TextBox";
import FormLayout from "components/FormLayout";
import TextBox from "components/TextBox";
import { redirectWithoutClientCache } from "../../utils/client";
import { HandleAxiosError } from "../../utils/front";
import { redirectWithoutClientCache } from "utils/client";
import { HandleAxiosError } from "utils/front";
import styles from "../../styles/create.module.scss";
import styles from "styles/create.module.scss";
function CreateCategory() {
const autoFocusRef = useAutoFocus();

View File

@@ -3,16 +3,16 @@ 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 FormLayout from "components/FormLayout";
import TextBox from "components/TextBox";
import useAutoFocus from "../../../hooks/useAutoFocus";
import useAutoFocus from "hooks/useAutoFocus";
import { Category } from "../../../types";
import { prisma } from "../../../utils/back";
import { BuildCategory, HandleAxiosError } from "../../../utils/front";
import { Category } from "types";
import { prisma } from "utils/back";
import { BuildCategory, HandleAxiosError } from "utils/front";
import styles from "../../../styles/create.module.scss";
import styles from "styles/create.module.scss";
function EditCategory({ category }: { category: Category }) {
const autoFocusRef = useAutoFocus();

View File

@@ -3,15 +3,15 @@ import { useRouter } from "next/router";
import nProgress from "nprogress";
import { useMemo, useState } from "react";
import Checkbox from "../../../components/Checkbox";
import FormLayout from "../../../components/FormLayout";
import TextBox from "../../../components/TextBox";
import Checkbox from "components/Checkbox";
import FormLayout from "components/FormLayout";
import TextBox from "components/TextBox";
import { Category } from "../../../types";
import { prisma } from "../../../utils/back";
import { BuildCategory, HandleAxiosError } from "../../../utils/front";
import { Category } from "types";
import { prisma } from "utils/back";
import { BuildCategory, HandleAxiosError } from "utils/front";
import styles from "../../../styles/create.module.scss";
import styles from "styles/create.module.scss";
function RemoveCategory({ category }: { category: Category }) {
const router = useRouter();

View File

@@ -2,15 +2,15 @@ import { useRouter } from "next/router";
import { useCallback, useState } from "react";
import { useHotkeys } from "react-hotkeys-hook";
import useModal from "../hooks/useModal";
import useModal from "hooks/useModal";
import Links from "../components/Links/Links";
import SearchModal from "../components/SearchModal/SearchModal";
import SideMenu from "../components/SideMenu/SideMenu";
import Links from "components/Links/Links";
import SearchModal from "components/SearchModal/SearchModal";
import SideMenu from "components/SideMenu/SideMenu";
import { Category, ItemComplete, Link } from "../types";
import { prisma } from "../utils/back";
import { BuildCategory } from "../utils/front";
import { Category, ItemComplete, Link } from "types";
import { prisma } from "utils/back";
import { BuildCategory } from "utils/front";
const OPEN_SEARCH_KEY = "s";
const CLOSE_SEARCH_KEY = "escape";

View File

@@ -3,18 +3,18 @@ import { useRouter } from "next/router";
import nProgress from "nprogress";
import { useMemo, useState } from "react";
import Checkbox from "../../components/Checkbox";
import FormLayout from "../../components/FormLayout";
import Selector from "../../components/Selector";
import TextBox from "../../components/TextBox";
import Checkbox from "components/Checkbox";
import FormLayout from "components/FormLayout";
import Selector from "components/Selector";
import TextBox from "components/TextBox";
import useAutoFocus from "../../hooks/useAutoFocus";
import useAutoFocus from "hooks/useAutoFocus";
import { Category, Link } from "../../types";
import { prisma } from "../../utils/back";
import { BuildCategory, HandleAxiosError, IsValidURL } from "../../utils/front";
import { Category, Link } from "types";
import { prisma } from "utils/back";
import { BuildCategory, HandleAxiosError, IsValidURL } from "utils/front";
import styles from "../../styles/create.module.scss";
import styles from "styles/create.module.scss";
function CreateLink({ categories }: { categories: Category[] }) {
const autoFocusRef = useAutoFocus();

View File

@@ -3,23 +3,23 @@ import { useRouter } from "next/router";
import nProgress from "nprogress";
import { useMemo, useState } from "react";
import Checkbox from "../../../components/Checkbox";
import FormLayout from "../../../components/FormLayout";
import Selector from "../../../components/Selector";
import TextBox from "../../../components/TextBox";
import Checkbox from "components/Checkbox";
import FormLayout from "components/FormLayout";
import Selector from "components/Selector";
import TextBox from "components/TextBox";
import useAutoFocus from "../../../hooks/useAutoFocus";
import useAutoFocus from "hooks/useAutoFocus";
import { Category, Link } from "../../../types";
import { prisma } from "../../../utils/back";
import { Category, Link } from "types";
import { prisma } from "utils/back";
import {
BuildCategory,
BuildLink,
HandleAxiosError,
IsValidURL,
} from "../../../utils/front";
} from "utils/front";
import styles from "../../../styles/create.module.scss";
import styles from "styles/create.module.scss";
function EditLink({
link,

View File

@@ -3,15 +3,15 @@ import { useRouter } from "next/router";
import nProgress from "nprogress";
import { useMemo, useState } from "react";
import Checkbox from "../../../components/Checkbox";
import FormLayout from "../../../components/FormLayout";
import TextBox from "../../../components/TextBox";
import Checkbox from "components/Checkbox";
import FormLayout from "components/FormLayout";
import TextBox from "components/TextBox";
import { Link } from "../../../types";
import { prisma } from "../../../utils/back";
import { BuildLink, HandleAxiosError } from "../../../utils/front";
import { Link } from "types";
import { prisma } from "utils/back";
import { BuildLink, HandleAxiosError } from "utils/front";
import styles from "../../../styles/create.module.scss";
import styles from "styles/create.module.scss";
function RemoveLink({ link }: { link: Link }) {
const router = useRouter();

View File

@@ -4,9 +4,9 @@ import { NextSeo } from "next-seo";
import Link from "next/link";
import { useRouter } from "next/router";
import MessageManager from "../components/MessageManager/MessageManager";
import MessageManager from "components/MessageManager/MessageManager";
import styles from "../styles/login.module.scss";
import styles from "styles/login.module.scss";
export default function SignIn({ providers }: { providers: Provider[] }) {
const { data: session, status } = useSession();

View File

@@ -0,0 +1,34 @@
@import "keyframes.scss";
@import "colors.scss";
.create-app {
height: fit-content;
width: 680px;
margin-top: 150px;
flex-direction: column;
gap: 15px;
& h2 {
color: $blue;
}
& form {
height: 100%;
width: 100%;
display: flex;
gap: 5px;
flex-direction: column;
}
& .input-field {
display: flex;
gap: 5px;
flex-direction: column;
}
}
@media (max-width: 680px) {
.create-app {
width: 100%;
}
}

View File

@@ -0,0 +1,28 @@
@import "keyframes.scss";
.App {
height: 100%;
display: flex;
align-items: center;
justify-content: center;
animation: fadein 250ms both;
& h1 {
display: inline-block;
border-right: 1px solid rgba(0, 0, 0, 0.3);
margin: 0;
margin-right: 20px;
padding: 10px 23px 10px 0;
font-size: 24px;
font-weight: 500;
vertical-align: top;
}
& h2 {
font-size: 14px;
font-weight: normal;
line-height: inherit;
margin: 0;
padding: 0;
}
}

201
src/styles/globals.scss Normal file
View File

@@ -0,0 +1,201 @@
@import "keyframes.scss";
@import "colors.scss";
* {
box-sizing: border-box;
outline: 0;
margin: 0;
padding: 0;
}
html,
body {
height: 100%;
width: 100%;
color: $black;
background-color: $light-grey;
font-family: "Poppins", sans-serif;
padding: 0;
margin: 0;
overflow: hidden;
}
#__next {
height: 100%;
width: 100%;
display: flex;
align-items: center;
flex-direction: column;
}
.App {
height: 100%;
width: 1280px;
padding: 10px;
display: flex;
justify-content: center;
animation: fadein 250ms both;
}
a {
width: fit-content;
color: $blue;
border-bottom: 1px solid transparent;
text-decoration: none;
transition: 0.15s;
&:hover,
&:focus {
color: $dark-blue;
border-bottom: 1px solid $dark-blue;
}
}
h1,
h2,
h3,
h4,
h5,
h6 {
color: $blue;
}
ul,
li {
list-style: none;
}
/* width */
::-webkit-scrollbar {
width: 7px;
}
/* Track */
::-webkit-scrollbar-track {
background: $light-grey;
border-radius: 2px;
}
/* Handle */
::-webkit-scrollbar-thumb {
background: $blue;
border-radius: 2px;
&:hover {
background: $dark-blue;
}
}
button:not(.reset) {
cursor: pointer;
width: 100%;
color: $white;
background: $blue;
padding: 10px;
border: 1px solid $blue;
border-radius: 3px;
transition: 0.15s;
&:disabled {
cursor: default;
opacity: 0.5;
}
&:not(:disabled):hover {
box-shadow: $dark-blue 0 0 3px 1px;
background: $dark-blue;
color: $white;
}
}
button.red-btn {
cursor: pointer;
width: 100%;
color: $white;
background: $red;
padding: 10px;
border: 1px solid $red;
border-radius: 3px;
transition: 0.15s;
&:disabled {
cursor: default;
opacity: 0.5;
}
&:not(:disabled):hover {
box-shadow: $red 0 0 3px 1px;
background: $red;
border: 1px solid $light-red;
color: $white;
}
}
input:not(.nostyle) {
color: $black;
background: $white;
padding: 10px;
border: 1px solid $lightest-grey;
border-bottom: 3px solid $lightest-grey;
transition: 0.15s;
&:focus {
border-bottom: 3px solid $blue;
}
}
input::placeholder {
font-style: italic;
color: $lightest-grey;
}
.input-field {
& label,
& input,
& select {
width: 100%;
}
}
.checkbox-field {
display: flex;
align-items: center;
gap: 5px;
}
select:not(.nostyle) {
color: $black;
background: $white;
padding: 10px;
border: 1px solid $lightest-grey;
border-bottom: 3px solid $lightest-grey;
transition: 0.15s;
&:focus {
border-bottom: 3px solid $blue;
}
}
.reset {
background-color: inherit;
color: inherit;
padding: 0;
margin: 0;
border: 0;
}
kbd {
text-shadow: 0 1px 0 #fff;
color: rgb(51, 51, 51);
background-color: rgb(247, 247, 247);
padding: 0.25em 0.5em;
border-radius: 3px;
border: 1px solid rgb(204, 204, 204);
box-shadow: 0 1px 0px rgba(0, 0, 0, 0.2), 0 0 0 2px #ffffff inset;
display: inline-block;
}
@media (max-width: 1280px) {
.App {
width: 100%;
}
}

42
src/styles/keyframes.scss Normal file
View File

@@ -0,0 +1,42 @@
@keyframes fadein {
0% {
transform: translateX(-15px);
opacity: 0;
}
100% {
transform: translateX(0);
opacity: 1;
}
}
@keyframes fadeintop {
0% {
transform: translateY(-15px);
opacity: 0;
}
100% {
transform: translateY(0);
opacity: 1;
}
}
@keyframes opacityin {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@keyframes rotate {
to {
transform: rotate(0deg);
}
from {
transform: rotate(360deg);
}
}

View File

@@ -0,0 +1,44 @@
@import "keyframes.scss";
@import "colors.scss";
.wrapper {
height: 100%;
width: 480px;
display: flex;
gap: 20px;
justify-content: center;
flex-direction: column;
animation: fadein 250ms both;
& .providers {
height: fit-content;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
& button {
width: 100%;
}
}
& .error {
height: fit-content;
width: 100%;
text-align: center;
font-style: italic;
font-size: 0.9em;
color: $red;
background-color: $light-red;
padding: 10px;
border-radius: 3px;
animation: fadein 250ms both;
}
}
@media (max-width: 680px) {
.login {
width: 100%;
}
}

View File

3
src/utils/back.ts Normal file
View File

@@ -0,0 +1,3 @@
import { PrismaClient } from "@prisma/client";
export const prisma = new PrismaClient();

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

70
src/utils/front.ts Normal file
View File

@@ -0,0 +1,70 @@
import axios from "axios";
import { Category, Link } from "types";
export function BuildCategory({
id,
name,
nextCategoryId,
links = [],
createdAt,
updatedAt,
}): Category {
return {
id,
name,
links: links.map((link) =>
BuildLink(link, { categoryId: id, categoryName: name })
),
nextCategoryId,
createdAt,
updatedAt,
};
}
export function BuildLink(
{ id, name, url, nextLinkId, favorite, createdAt, updatedAt },
{ categoryId, categoryName }
): Link {
return {
id,
name,
url,
category: {
id: categoryId,
name: categoryName,
},
nextLinkId,
favorite,
createdAt,
updatedAt,
};
}
export function IsValidURL(url: string): boolean {
const regex = new RegExp(
/^(?:http(s)?:\/\/)[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\$&'\(\)\*\+,;=.\%]+$/
);
return url.match(regex) ? true : false;
}
export function HandleAxiosError(error): string {
let errorText: string;
if (axios.isAxiosError(error)) {
if (error.response) {
const responseError =
error.response.data?.["error"] || error.response.data;
errorText = responseError || "Une erreur est survenue";
} else if (error.request) {
errorText = "Aucune donnée renvoyée par le serveur";
} else {
errorText = "Une erreur inconnue est survenue";
}
} else {
errorText = "Une erreur est survenue";
}
console.error(error);
return errorText;
}

3
src/utils/link.ts Normal file
View File

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

View File

@@ -1,34 +0,0 @@
@import "keyframes.scss";
@import "colors.scss";
.create-app {
height: fit-content;
width: 680px;
margin-top: 150px;
flex-direction: column;
gap: 15px;
& h2 {
color: $blue;
}
& form {
height: 100%;
width: 100%;
display: flex;
gap: 5px;
flex-direction: column;
}
& .input-field {
display: flex;
gap: 5px;
flex-direction: column;
}
}
@media (max-width: 680px) {
.create-app {
width: 100%;
}
}

View File

@@ -1,28 +0,0 @@
@import "keyframes.scss";
.App {
height: 100%;
display: flex;
align-items: center;
justify-content: center;
animation: fadein 250ms both;
& h1 {
display: inline-block;
border-right: 1px solid rgba(0, 0, 0, 0.3);
margin: 0;
margin-right: 20px;
padding: 10px 23px 10px 0;
font-size: 24px;
font-weight: 500;
vertical-align: top;
}
& h2 {
font-size: 14px;
font-weight: normal;
line-height: inherit;
margin: 0;
padding: 0;
}
}

View File

@@ -1,201 +0,0 @@
@import "keyframes.scss";
@import "colors.scss";
* {
box-sizing: border-box;
outline: 0;
margin: 0;
padding: 0;
}
html,
body {
height: 100%;
width: 100%;
color: $black;
background-color: $light-grey;
font-family: "Poppins", sans-serif;
padding: 0;
margin: 0;
overflow: hidden;
}
#__next {
height: 100%;
width: 100%;
display: flex;
align-items: center;
flex-direction: column;
}
.App {
height: 100%;
width: 1280px;
padding: 10px;
display: flex;
justify-content: center;
animation: fadein 250ms both;
}
a {
width: fit-content;
color: $blue;
border-bottom: 1px solid transparent;
text-decoration: none;
transition: 0.15s;
&:hover,
&:focus {
color: $dark-blue;
border-bottom: 1px solid $dark-blue;
}
}
h1,
h2,
h3,
h4,
h5,
h6 {
color: $blue;
}
ul,
li {
list-style: none;
}
/* width */
::-webkit-scrollbar {
width: 7px;
}
/* Track */
::-webkit-scrollbar-track {
background: $light-grey;
border-radius: 2px;
}
/* Handle */
::-webkit-scrollbar-thumb {
background: $blue;
border-radius: 2px;
&:hover {
background: $dark-blue;
}
}
button:not(.reset) {
cursor: pointer;
width: 100%;
color: $white;
background: $blue;
padding: 10px;
border: 1px solid $blue;
border-radius: 3px;
transition: 0.15s;
&:disabled {
cursor: default;
opacity: 0.5;
}
&:not(:disabled):hover {
box-shadow: $dark-blue 0 0 3px 1px;
background: $dark-blue;
color: $white;
}
}
button.red-btn {
cursor: pointer;
width: 100%;
color: $white;
background: $red;
padding: 10px;
border: 1px solid $red;
border-radius: 3px;
transition: 0.15s;
&:disabled {
cursor: default;
opacity: 0.5;
}
&:not(:disabled):hover {
box-shadow: $red 0 0 3px 1px;
background: $red;
border: 1px solid $light-red;
color: $white;
}
}
input:not(.nostyle) {
color: $black;
background: $white;
padding: 10px;
border: 1px solid $lightest-grey;
border-bottom: 3px solid $lightest-grey;
transition: 0.15s;
&:focus {
border-bottom: 3px solid $blue;
}
}
input::placeholder {
font-style: italic;
color: $lightest-grey;
}
.input-field {
& label,
& input,
& select {
width: 100%;
}
}
.checkbox-field {
display: flex;
align-items: center;
gap: 5px;
}
select:not(.nostyle) {
color: $black;
background: $white;
padding: 10px;
border: 1px solid $lightest-grey;
border-bottom: 3px solid $lightest-grey;
transition: 0.15s;
&:focus {
border-bottom: 3px solid $blue;
}
}
.reset {
background-color: inherit;
color: inherit;
padding: 0;
margin: 0;
border: 0;
}
kbd {
text-shadow: 0 1px 0 #fff;
color: rgb(51, 51, 51);
background-color: rgb(247, 247, 247);
padding: 0.25em 0.5em;
border-radius: 3px;
border: 1px solid rgb(204, 204, 204);
box-shadow: 0 1px 0px rgba(0, 0, 0, 0.2), 0 0 0 2px #ffffff inset;
display: inline-block;
}
@media (max-width: 1280px) {
.App {
width: 100%;
}
}

View File

@@ -1,42 +0,0 @@
@keyframes fadein {
0% {
transform: translateX(-15px);
opacity: 0;
}
100% {
transform: translateX(0);
opacity: 1;
}
}
@keyframes fadeintop {
0% {
transform: translateY(-15px);
opacity: 0;
}
100% {
transform: translateY(0);
opacity: 1;
}
}
@keyframes opacityin {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@keyframes rotate {
to {
transform: rotate(0deg);
}
from {
transform: rotate(360deg);
}
}

View File

@@ -1,44 +0,0 @@
@import "keyframes.scss";
@import "colors.scss";
.wrapper {
height: 100%;
width: 480px;
display: flex;
gap: 20px;
justify-content: center;
flex-direction: column;
animation: fadein 250ms both;
& .providers {
height: fit-content;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
& button {
width: 100%;
}
}
& .error {
height: fit-content;
width: 100%;
text-align: center;
font-style: italic;
font-size: 0.9em;
color: $red;
background-color: $light-red;
padding: 10px;
border-radius: 3px;
animation: fadein 250ms both;
}
}
@media (max-width: 680px) {
.login {
width: 100%;
}
}

60
test.js
View File

@@ -1,60 +0,0 @@
(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

@@ -13,8 +13,9 @@
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true
"incremental": true,
"baseUrl": "./src"
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"include": ["next-env.d.ts", "@/**.*", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
}

View File

@@ -1,3 +0,0 @@
import { PrismaClient } from '@prisma/client';
export const prisma = new PrismaClient();

View File

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

View File

@@ -1,54 +0,0 @@
import axios from 'axios';
import { Category, Link } from '../types';
export function BuildCategory({ id, name, nextCategoryId, links = [], createdAt, updatedAt }): Category {
return {
id,
name,
links: links.map((link) => BuildLink(link, { categoryId: id, categoryName: name })),
nextCategoryId,
createdAt,
updatedAt
}
}
export function BuildLink({ id, name, url, nextLinkId, favorite, createdAt, updatedAt }, { categoryId, categoryName }): Link {
return {
id,
name,
url,
category: {
id: categoryId,
name: categoryName
},
nextLinkId,
favorite,
createdAt,
updatedAt
}
}
export function IsValidURL(url: string): boolean {
const regex = new RegExp(/^(?:http(s)?:\/\/)[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\$&'\(\)\*\+,;=.\%]+$/);
return url.match(regex) ? true : false;
}
export function HandleAxiosError(error): string {
let errorText: string;
if (axios.isAxiosError(error)) {
if (error.response) {
const responseError = error.response.data?.['error'] || error.response.data;
errorText = responseError || 'Une erreur est survenue';
} else if (error.request) {
errorText = 'Aucune donnée renvoyée par le serveur';
} else {
errorText = 'Une erreur inconnue est survenue';
}
} else {
errorText = 'Une erreur est survenue';
}
console.error(error);
return errorText;
}

View File

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