mirror of
https://github.com/Sonny93/my-links.git
synced 2025-12-08 14:43:24 +00:00
refacto: create src dir & change project indentation to 2 spaces
This commit is contained in:
9
.editorconfig
Normal file
9
.editorconfig
Normal 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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
</>)
|
||||
}
|
||||
@@ -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>
|
||||
</>)
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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
|
||||
},
|
||||
});
|
||||
@@ -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)' });
|
||||
}
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
27
src/components/AuthRequired.tsx
Normal file
27
src/components/AuthRequired.tsx
Normal 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;
|
||||
}
|
||||
38
src/components/BlockWrapper/block-wrapper.module.scss
Normal file
38
src/components/BlockWrapper/block-wrapper.module.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
@@ -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";
|
||||
156
src/components/Links/links.module.scss
Normal file
156
src/components/Links/links.module.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
40
src/components/MessageManager/message-manager.module.scss
Normal file
40
src/components/MessageManager/message-manager.module.scss
Normal 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;
|
||||
}
|
||||
54
src/components/Modal/modal.module.scss
Normal file
54
src/components/Modal/modal.module.scss
Normal 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;
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
70
src/components/Selector.tsx
Normal file
70
src/components/Selector.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Category } from "../../../types";
|
||||
import { Category } from "types";
|
||||
import CategoryItem from "./CategoryItem";
|
||||
|
||||
import styles from "./categories.module.scss";
|
||||
@@ -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 {
|
||||
93
src/components/SideMenu/Categories/categories.module.scss
Normal file
93
src/components/SideMenu/Categories/categories.module.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Link } from "../../../types";
|
||||
import { Link } from "types";
|
||||
import FavoriteItem from "./FavoriteItem";
|
||||
|
||||
import styles from "./favorites.module.scss";
|
||||
57
src/components/SideMenu/Favorites/favorites.module.scss
Normal file
57
src/components/SideMenu/Favorites/favorites.module.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
35
src/components/SideMenu/UserCard/user-card.module.scss
Normal file
35
src/components/SideMenu/UserCard/user-card.module.scss
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
27
src/components/SideMenu/sidemenu.module.scss
Normal file
27
src/components/SideMenu/sidemenu.module.scss
Normal 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
15
src/pages/404.tsx
Normal 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
15
src/pages/500.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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
21
src/pages/_document.tsx
Normal 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;
|
||||
97
src/pages/api/auth/[...nextauth].ts
Normal file
97
src/pages/api/auth/[...nextauth].ts
Normal 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
|
||||
},
|
||||
});
|
||||
@@ -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,
|
||||
@@ -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,
|
||||
41
src/pages/api/category/remove/[cid].ts
Normal file
41
src/pages/api/category/remove/[cid].ts
Normal 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
163
src/pages/api/favicon.ts
Normal 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;
|
||||
}
|
||||
@@ -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,
|
||||
@@ -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(
|
||||
@@ -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,
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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";
|
||||
@@ -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();
|
||||
@@ -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,
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
34
src/styles/create.module.scss
Normal file
34
src/styles/create.module.scss
Normal 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%;
|
||||
}
|
||||
}
|
||||
28
src/styles/error-page.module.scss
Normal file
28
src/styles/error-page.module.scss
Normal 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
201
src/styles/globals.scss
Normal 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
42
src/styles/keyframes.scss
Normal 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);
|
||||
}
|
||||
}
|
||||
44
src/styles/login.module.scss
Normal file
44
src/styles/login.module.scss
Normal 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%;
|
||||
}
|
||||
}
|
||||
0
types.d.ts → src/types.d.ts
vendored
0
types.d.ts → src/types.d.ts
vendored
3
src/utils/back.ts
Normal file
3
src/utils/back.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
|
||||
export const prisma = new PrismaClient();
|
||||
7
src/utils/client.ts
Normal file
7
src/utils/client.ts
Normal 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
70
src/utils/front.ts
Normal 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
3
src/utils/link.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export function faviconLinkBuilder(origin: string) {
|
||||
return `http://localhost:3000/api/favicon?url=${origin}`;
|
||||
}
|
||||
@@ -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%;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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%;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
60
test.js
@@ -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");
|
||||
}
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
export const prisma = new PrismaClient();
|
||||
@@ -1,7 +0,0 @@
|
||||
import { NextRouter } from 'next/router';
|
||||
|
||||
export function redirectWithoutClientCache(router: NextRouter, url: string) {
|
||||
router.push(url, undefined, {
|
||||
unstable_skipClientCache: true,
|
||||
});
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export function faviconLinkBuilder(origin: string, size: number = 32) {
|
||||
return `http://localhost:3000/api/favicon?url=${origin}`;
|
||||
}
|
||||
Reference in New Issue
Block a user