Beaucoup trop de chose

- Ajout création, édition, suppression catégories & liens
- Ajout auth google
- Ajout/modification style pour catégories & liens
- Ajout component générique pour bouton, inputs, checkbox & selector
- Gestion des messages d'erreur/succès/infos via component dédié
- Ajout component FormLayout pour les pages création, édition, suppression catégories & liens
- Page custom 404, 500 & signin
- Modification schéma DB
This commit is contained in:
Sonny
2022-05-06 19:35:12 +02:00
parent 988dc47ecd
commit aee3e6a820
48 changed files with 7164 additions and 1122 deletions

14
pages/404.tsx Normal file
View File

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

14
pages/500.tsx Normal file
View File

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

View File

@@ -1,10 +1,14 @@
import { useEffect } from 'react';
import { SessionProvider } from 'next-auth/react';
import nProgress from "nprogress";
import { useRouter } from "next/router";
import { useRouter } from 'next/router';
import nProgress from 'nprogress';
import 'nprogress/nprogress.css';
import AuthRequired from '../components/AuthRequired';
import '../styles/globals.scss';
import "nprogress/nprogress.css"
import { useEffect } from 'react';
function MyApp({
Component,
@@ -26,7 +30,13 @@ function MyApp({
return (
<SessionProvider session={session}>
<Component {...pageProps} />
{Component.authRequired ? (
<AuthRequired>
<Component {...pageProps} />
</AuthRequired>
) : (
<Component {...pageProps} />
)}
</SessionProvider>
);
}

View File

@@ -2,7 +2,12 @@ import { Html, Head, Main, NextScript } from 'next/document';
const Document = () => (
<Html lang='fr'>
<Head />
<Head>
<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'
/>
</Head>
<title>Superpipo</title>
<body>
<noscript>

View File

@@ -1,30 +0,0 @@
import NextAuth from 'next-auth';
import GoogleProvider from 'next-auth/providers/google';
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, profile }) {
if (account.provider === "google" && profile.email !== '') {
if (profile.email_verified && profile.email.endsWith("@gmail.com")) {
return true;
} else {
return "/signin?error=" + encodeURI('Une erreur s\'est produite lors de l\'authentification');
}
}
return false;
}
}
});

View File

@@ -0,0 +1,60 @@
import NextAuth from 'next-auth';
import GoogleProvider from 'next-auth/providers/google';
import { PrismaClient } from '@prisma/client';
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
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
}
});
if (!account) {
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'
}
});

View File

@@ -0,0 +1,39 @@
import { NextApiRequest, NextApiResponse } from 'next';
import { apiRoute } from '../../../utils/back';
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
apiRoute.post(async (req: NextApiRequest, res: NextApiResponse) => {
const name = req.body?.name as string;
if (!name) {
return res.status(400).send({ error: 'Nom de catégorie manquant' });
}
try {
const category = await prisma.category.findFirst({
where: { name }
});
if (category) {
return res.status(400).send({ error: 'Une catégorie avec ce nom existe déjà' });
}
} catch (error) {
console.error(error);
return res.status(400).send({ error: 'Une erreur est survenue lors de la création de la catégorie' });
}
try {
await prisma.category.create({
data: { name }
});
return res.status(200).send({ success: 'Catégorie créée avec succès' });
} catch (error) {
console.error(error);
return res.status(400).send({ error: 'Une erreur est survenue lors de la création de la catégorie' });
}
});
export default apiRoute;

View File

@@ -0,0 +1,45 @@
import { NextApiRequest, NextApiResponse } from 'next';
import { apiRoute } from '../../../../utils/back';
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
apiRoute.put(async (req: NextApiRequest, res: NextApiResponse) => {
const { cid } = req.query;
let category;
try {
category = await prisma.category.findFirst({
where: { id: Number(cid) }
});
if (!category) {
return res.status(400).send({ error: 'Catégorie introuvable' });
}
} catch (error) {
console.error(error);
return res.status(400).send({ error: 'Une erreur est survenue lors de l\'édition de la catégorie' });
}
const name = req.body?.name as string;
if (!name) {
return res.status(400).send({ error: 'Nom de la catégorie manquante' });
} else if (name === category.name) {
return res.status(400).send({ error: 'Le nom de la catégorie doit être différent du nom actuel' });
}
try {
await prisma.category.update({
where: { id: Number(cid) },
data: { name }
});
return res.status(200).send({ success: 'Catégorie mise à jour avec succès' });
} catch (error) {
console.error(error);
return res.status(400).send({ error: 'Une erreur est survenue lors de l\'édition de la catégorie' });
}
});
export default apiRoute;

View File

@@ -0,0 +1,36 @@
import { NextApiRequest, NextApiResponse } from 'next';
import { apiRoute } from '../../../../utils/back';
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
apiRoute.delete(async (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' });
}
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' });
}
});
export default apiRoute;

68
pages/api/link/create.ts Normal file
View File

@@ -0,0 +1,68 @@
import { NextApiRequest, NextApiResponse } from 'next';
import { apiRoute } from '../../../utils/back';
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
apiRoute.post(async (req: NextApiRequest, res: NextApiResponse) => {
const name = req.body?.name as string;
const url = req.body?.url as string;
const favorite = Boolean(req.body?.favorite) || false;
const categoryId = Number(req.body?.categoryId);
if (!name) {
return res.status(400).send({ error: 'Nom du lien manquant' });
}
if (!url) {
return res.status(400).send({ error: 'URL du lien manquant' });
}
if (!categoryId) {
return res.status(400).send({ error: 'Catégorie du lien manquante' });
}
try {
const link = await prisma.link.findFirst({
where: { name }
});
if (link) {
return res.status(400).send({ error: 'Un lien avec ce nom existe déjà' });
}
} catch (error) {
console.error(error);
return res.status(400).send({ error: 'Une erreur est survenue lors de la création du lien' });
}
try {
const category = await prisma.category.findFirst({
where: { id: categoryId }
});
if (!category) {
return res.status(400).send({ error: 'Cette catégorie n\'existe pas' });
}
} catch (error) {
console.error(error);
return res.status(400).send({ error: 'Une erreur est survenue lors de la création du lien' });
}
try {
await prisma.link.create({
data: {
name,
url,
categoryId,
favorite
}
});
return res.status(200).send({ success: 'Lien créé avec succès' });
} catch (error) {
console.error(error);
return res.status(400).send({ error: 'Une erreur est survenue lors de la création du lien' });
}
});
export default apiRoute;

View File

@@ -0,0 +1,59 @@
import { NextApiRequest, NextApiResponse } from 'next';
import { apiRoute } from '../../../../utils/back';
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
apiRoute.put(async (req: NextApiRequest, res: NextApiResponse) => { // TODO: Ajouter vérification -> l'utilisateur doit changer au moins un champ
const { lid } = req.query;
try {
const link = await prisma.link.findFirst({
where: { id: Number(lid) }
});
if (!link) {
return res.status(400).send({ error: 'Lien introuvable' });
}
} catch (error) {
console.error(error);
return res.status(400).send({ error: 'Une erreur est survenue lors de l\'édition du lien' });
}
const name = req.body?.name as string;
const url = req.body?.url as string;
const favorite = Boolean(req.body?.favorite) || false;
const categoryId = Number(req.body?.categoryId);
if (!name) {
return res.status(400).send({ error: 'Nom du lien manquant' });
}
if (!url) {
return res.status(400).send({ error: 'URL du lien manquant' });
}
if (!categoryId) {
return res.status(400).send({ error: 'Catégorie du lien manquante' });
}
try {
await prisma.link.update({
where: { id: Number(lid) },
data: {
name,
url,
favorite,
categoryId
}
});
return res.status(200).send({ success: 'Lien mis à jour avec succès' });
} catch (error) {
console.error(error);
return res.status(400).send({ error: 'Une erreur est survenue lors de l\'édition du lien' });
}
});
export default apiRoute;

View File

@@ -0,0 +1,36 @@
import { NextApiRequest, NextApiResponse } from 'next';
import { apiRoute } from '../../../../utils/back';
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
apiRoute.delete(async (req: NextApiRequest, res: NextApiResponse) => {
const { lid } = req.query;
try {
const link = await prisma.link.findFirst({
where: { id: Number(lid) }
});
if (!link) {
return res.status(400).send({ error: 'Lien introuvable' });
}
} catch (error) {
console.error(error);
return res.status(400).send({ error: 'Une erreur est survenue lors de la suppression du lien' });
}
try {
await prisma.link.delete({
where: { id: Number(lid) }
});
return res.status(200).send({ success: 'Le lien a été supprimé avec succès' });
} catch (error) {
console.error(error);
return res.status(400).send({ error: 'Une erreur est survenue lors de la suppression du lien' });
}
});
export default apiRoute;

View File

@@ -1,45 +1,65 @@
import { useState } from 'react';
import { useSession } from 'next-auth/react';
import Link from 'next/link';
import { useEffect, useState } from 'react';
import nProgress from 'nprogress';
import axios, { AxiosResponse } from 'axios';
import FormLayout from '../../components/FormLayout';
import TextBox from '../../components/TextBox';
import Input from '../../components/input';
import styles from '../../styles/create.module.scss';
import Head from 'next/head';
import { HandleAxiosError } from '../../utils/front';
import { useRouter } from 'next/router';
export default function CreateCategory() {
const { data: session, status } = useSession({ required: true });
function CreateCategory() {
const info = useRouter().query?.info as string;
const [name, setName] = useState<string>('');
if (status === 'loading') {
return (<p>Chargement de la session en cours</p>)
}
const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState<string | null>(null);
const [canSubmit, setCanSubmit] = useState<boolean>(false);
const handleSubmit = (event) => {
useEffect(() => setCanSubmit(name.length !== 0), [name]);
const handleSubmit = async (event) => {
event.preventDefault();
console.log('On peut envoyer la requête pour créer une catégorie');
setSuccess(null);
setError(null);
setCanSubmit(false);
nProgress.start();
try {
const payload = { name };
const { data }: AxiosResponse<any> = await axios.post('/api/category/create', payload);
setSuccess(data?.success || 'Categorie créée avec succès');
setCanSubmit(false);
} catch (error) {
setError(HandleAxiosError(error));
setCanSubmit(true);
} finally {
nProgress.done();
}
}
return (
<div className={`App ${styles['create-app']}`}>
<Head>
<title>Superpipo Créer une categorie</title>
</Head>
<h2>Créer une categorie</h2>
<form onSubmit={handleSubmit}>
<Input
name='name'
label='Nom de la catégorie'
onChangeCallback={({ target }, value) => setName(value)}
value={name}
fieldClass={styles['input-field']}
/>
<button type='submit' disabled={name.length < 1}>
Valider
</button>
</form>
<Link href='/'>
<a> Revenir à l'accueil</a>
</Link>
</div>
);
return (<>
<FormLayout
title='Créer une catégorie'
errorMessage={error}
successMessage={success}
infoMessage={info}
canSubmit={canSubmit}
handleSubmit={handleSubmit}
>
<TextBox
name='name'
label='Nom de la catégorie'
onChangeCallback={(value) => setName(value)}
value={name}
fieldClass={styles['input-field']}
placeholder='Nom...'
/>
</FormLayout>
</>);
}
CreateCategory.authRequired = true;
export default CreateCategory;

View File

@@ -0,0 +1,109 @@
import { useEffect, useState } from 'react';
import nProgress from 'nprogress';
import axios, { AxiosResponse } from 'axios';
import { confirmAlert } from 'react-confirm-alert';
import 'react-confirm-alert/src/react-confirm-alert.css';
import FormLayout from '../../../components/FormLayout';
import TextBox from '../../../components/TextBox';
import styles from '../../../styles/create.module.scss';
import { Category } from '../../../types';
import { BuildCategory, HandleAxiosError } from '../../../utils/front';
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
function EditCategory({ category }: { category: Category; }) {
const [name, setName] = useState<string>(category.name);
const [canSubmit, setCanSubmit] = useState<boolean>(false);
const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState<string | null>(null);
useEffect(() => {
if (name !== category.name && name !== '') {
setCanSubmit(true);
} else {
setCanSubmit(false);
}
}, [category, name]);
const handleSubmit = async (event) => {
event.preventDefault();
confirmAlert({
message: `Confirmer l'édition de la catégorie "${category.name}"`,
buttons: [{
label: 'Yes',
onClick: async () => {
setSuccess(null);
setError(null);
setCanSubmit(false);
nProgress.start();
try {
const payload = { name };
const { data }: AxiosResponse<any> = await axios.put(`/api/category/edit/${category.id}`, payload);
setSuccess(data?.success || 'Catégorie modifiée avec succès');
} catch (error) {
setError(HandleAxiosError(error));
} finally {
setCanSubmit(true);
nProgress.done();
}
}
}, {
label: 'No',
onClick: () => { }
}]
});
}
return (<>
<FormLayout
title='Modifier une catégorie'
errorMessage={error}
successMessage={success}
canSubmit={canSubmit}
handleSubmit={handleSubmit}
>
<TextBox
name='name'
label='Nom'
onChangeCallback={(value) => setName(value)}
value={name}
fieldClass={styles['input-field']}
placeholder={`Nom original : ${category.name}`}
/>
</FormLayout>
</>);
}
EditCategory.authRequired = true;
export default EditCategory;
export async function getServerSideProps({ query }) {
const { cid } = query;
const categoryDB = await prisma.category.findFirst({
where: { id: Number(cid) },
include: { links: true }
});
if (!categoryDB) {
return {
redirect: {
destination: '/'
}
}
}
const category = BuildCategory(categoryDB);
return {
props: {
category: JSON.parse(JSON.stringify(category))
}
}
}

View File

@@ -0,0 +1,113 @@
import { useEffect, useState } from 'react';
import nProgress from 'nprogress';
import axios, { AxiosResponse } from 'axios';
import { confirmAlert } from 'react-confirm-alert';
import 'react-confirm-alert/src/react-confirm-alert.css';
import FormLayout from '../../../components/FormLayout';
import TextBox from '../../../components/TextBox';
import styles from '../../../styles/create.module.scss';
import { Category } from '../../../types';
import { BuildCategory, HandleAxiosError } from '../../../utils/front';
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
function RemoveCategory({ category }: { category: Category; }) {
const [canSubmit, setCanSubmit] = useState<boolean>(false);
const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState<string | null>(null);
useEffect(() => {
if (category.links.length > 0) {
setError('Vous devez supprimer tous les liens de cette catégorie avant de pouvoir supprimer cette catégorie')
setCanSubmit(false);
} else {
setCanSubmit(true);
}
}, [category]);
if (status === 'loading') {
return (<p>Chargement de la session en cours</p>)
}
const handleSubmit = async (event) => {
event.preventDefault();
confirmAlert({
message: `Confirmer la suppression du lien "${category.name}"`,
buttons: [{
label: 'Yes',
onClick: async () => {
setSuccess(null);
setError(null);
setCanSubmit(false);
nProgress.start();
try {
const { data }: AxiosResponse<any> = await axios.delete(`/api/category/remove/${category.id}`);
setSuccess(data?.success || 'Categorie supprimée avec succès');
setCanSubmit(false);
} catch (error) {
setError(HandleAxiosError(error));
setCanSubmit(true);
} finally {
nProgress.done();
}
}
}, {
label: 'No',
onClick: () => { }
}]
});
}
return (<>
<FormLayout
title='Supprimer une catégorie'
errorMessage={error}
successMessage={success}
canSubmit={canSubmit}
handleSubmit={handleSubmit}
classBtnConfirm='red-btn'
textBtnConfirm='Supprimer'
>
<TextBox
name='name'
label='Nom'
value={category.name}
fieldClass={styles['input-field']}
disabled={true}
/>
</FormLayout>
</>);
}
RemoveCategory.authRequired = true;
export default RemoveCategory;
export async function getServerSideProps({ query }) {
const { cid } = query;
const categoryDB = await prisma.category.findFirst({
where: { id: Number(cid) },
include: { links: true }
});
if (!categoryDB) {
return {
redirect: {
destination: '/'
}
}
}
const category = BuildCategory(categoryDB);
return {
props: {
category: JSON.parse(JSON.stringify(category))
}
}
}

View File

@@ -1,6 +1,5 @@
import { useState } from 'react';
import { useSession } from 'next-auth/react';
import { Provider } from 'react-redux';
import Head from 'next/head';
import Menu from '../components/Categories/SideMenu';
@@ -10,7 +9,6 @@ import { Category, Link } from '../types';
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
import { store } from '../redux';
import { BuildCategory } from '../utils/front';
import Links from '../components/Links/Links';
@@ -20,40 +18,35 @@ interface HomeProps {
favorites: Link[];
}
export default function Home({ categories, favorites }: HomeProps) {
const { data: session, status } = useSession({ required: true });
function Home({ categories, favorites }: HomeProps) {
const { data } = useSession({ required: true });
const [categoryActive, setCategoryActive] = useState<Category | null>(categories?.[0]);
const handleSelectCategory = (category: Category) => setCategoryActive(category);
if (status === 'loading') {
return (<p>Chargement de la session en cours</p>)
}
return (
<Provider store={store}>
<Head>
<title>Superpipo</title>
</Head>
<div className='App'>
<Menu
categories={categories}
favorites={favorites}
handleSelectCategory={handleSelectCategory}
categoryActive={categoryActive}
session={session}
/>
<Links category={categoryActive} />
</div>
</Provider>
)
return (<>
<Head>
<title>Superpipo</title>
</Head>
<div className='App'>
<Menu
categories={categories}
favorites={favorites}
handleSelectCategory={handleSelectCategory}
categoryActive={categoryActive}
session={data}
/>
<Links category={categoryActive} />
</div>
</>);
}
export async function getStaticProps() {
export async function getServerSideProps() {
const categoriesDB = await prisma.category.findMany({ include: { links: true } });
const favorites = [] as Link[];
const categories = categoriesDB.map((categoryDB) => {
console.log(categoryDB)
const category = BuildCategory(categoryDB);
category.links.map((link) => link.favorite ? favorites.push(link) : null);
return category;
@@ -62,7 +55,7 @@ export async function getStaticProps() {
if (categories.length === 0) {
return {
redirect: {
destination: '/category/create'
destination: '/category/create?info=Veuillez créer une catégorie'
}
}
}
@@ -73,4 +66,7 @@ export async function getStaticProps() {
favorites: JSON.parse(JSON.stringify(favorites)),
}
}
}
}
Home.authRequired = true;
export default Home;

View File

@@ -1,88 +1,103 @@
import { useEffect, useState } from 'react';
import { useSession } from 'next-auth/react';
import Link from 'next/link';
import Input from '../../components/input';
import nProgress from 'nprogress';
import axios, { AxiosResponse } from 'axios';
import FormLayout from '../../components/FormLayout';
import TextBox from '../../components/TextBox';
import Selector from '../../components/Selector';
import Checkbox from '../../components/Checkbox';
import styles from '../../styles/create.module.scss';
import { Category } from '../../types';
import { BuildCategory } from '../../utils/front';
import { BuildCategory, HandleAxiosError, IsValidURL } from '../../utils/front';
import { PrismaClient } from '@prisma/client';
import Selector from '../../components/selector';
import Head from 'next/head';
const prisma = new PrismaClient();
export default function CreateLink({ categories }: { categories: Category[]; }) {
const { status } = useSession({ required: true });
function CreateLink({ categories }: { categories: Category[]; }) {
const [name, setName] = useState<string>('');
const [url, setUrl] = useState<string>('');
const [favorite, setFavorite] = useState<boolean>(false);
const [categoryId, setCategoryId] = useState<number | null>(categories?.[0].id || null);
const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState<string | null>(null);
const [canSubmit, setCanSubmit] = useState<boolean>(false);
useEffect(() => {
const regex = new RegExp('https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|www\.[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9]+\.[^\s]{2,}|www\.[a-zA-Z0-9]+\.[^\s]{2,}');
if (name !== '' && url.match(regex) && categoryId !== null) {
setCanSubmit(false);
} else {
if (name !== '' && IsValidURL(url) && favorite !== null && categoryId !== null) {
setCanSubmit(true);
} else {
setCanSubmit(false);
}
}, [name, url, categoryId]);
}, [name, url, favorite, categoryId]);
if (status === 'loading') {
return (<p>Chargement de la session en cours</p>)
}
const handleSubmit = (event) => {
const handleSubmit = async (event) => {
event.preventDefault();
console.log('On peut envoyer la requête pour créer un lien');
setSuccess(null);
setError(null);
setCanSubmit(false);
nProgress.start();
try {
const payload = { name, url, favorite, categoryId };
const { data }: AxiosResponse<any> = await axios.post('/api/link/create', payload);
setSuccess(data?.success || 'Lien modifié avec succès');
} catch (error) {
setError(HandleAxiosError(error));
} finally {
setCanSubmit(true);
nProgress.done();
}
}
return (<>
<Head>
<title>Superpipo Créer un lien</title>
</Head>
<div className={`App ${styles['create-app']}`}>
<h2>Créer un lien</h2>
<form onSubmit={handleSubmit}>
<Input
name='name'
label='Label'
onChangeCallback={({ target }, value) => setName(value)}
value={name}
fieldClass={styles['input-field']}
placeholder='Label du lien'
/>
<Input
name='url'
label='URL'
onChangeCallback={({ target }, value) => setUrl(value)}
value={name}
fieldClass={styles['input-field']}
placeholder='URL du lien'
/>
<Selector
name='category'
label='Catégorie'
value={categoryId}
onChangeCallback={({ target }, value) => setCategoryId(value)}
>
{categories.map((category, key) => (
<option key={key} value={category.id}>{category.name}</option>
))}
</Selector>
<button type='submit' disabled={canSubmit}>
Valider
</button>
</form>
<Link href='/'>
<a> Revenir à l'accueil</a>
</Link>
</div>
<FormLayout
title='Créer un lien'
errorMessage={error}
successMessage={success}
canSubmit={canSubmit}
handleSubmit={handleSubmit}
>
<TextBox
name='name'
label='Nom'
onChangeCallback={(value) => setName(value)}
value={name}
fieldClass={styles['input-field']}
placeholder='Nom du lien'
/>
<TextBox
name='url'
label='URL'
onChangeCallback={(value) => setUrl(value)}
value={url}
fieldClass={styles['input-field']}
placeholder='https://www.example.org/'
/>
<Selector
name='category'
label='Catégorie'
value={categoryId}
onChangeCallback={(value: number) => setCategoryId(value)}
options={categories.map(({ id, name }) => ({ label: name, value: id }))}
/>
<Checkbox
name='favorite'
isChecked={favorite}
onChangeCallback={(value) => setFavorite(value)}
label='Favoris'
/>
</FormLayout>
</>);
}
export async function getStaticProps() {
CreateLink.authRequired = true;
export default CreateLink;
export async function getServerSideProps() {
const categoriesDB = await prisma.category.findMany();
const categories = categoriesDB.map((categoryDB) => BuildCategory(categoryDB));

130
pages/link/edit/[lid].tsx Normal file
View File

@@ -0,0 +1,130 @@
import { useEffect, useState } from 'react';
import axios, { AxiosResponse } from 'axios';
import nProgress from 'nprogress';
import FormLayout from '../../../components/FormLayout';
import TextBox from '../../../components/TextBox';
import Selector from '../../../components/Selector';
import Checkbox from '../../../components/Checkbox';
import styles from '../../../styles/create.module.scss';
import { Category, Link } from '../../../types';
import { BuildCategory, BuildLink, HandleAxiosError, IsValidURL } from '../../../utils/front';
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
function EditLink({ link, categories }: { link: Link; categories: Category[]; }) {
const [name, setName] = useState<string>(link.name);
const [url, setUrl] = useState<string>(link.url);
const [favorite, setFavorite] = useState<boolean>(link.favorite);
const [categoryId, setCategoryId] = useState<number | null>(link.category?.id || null);
const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState<string | null>(null);
const [canSubmit, setCanSubmit] = useState<boolean>(false);
useEffect(() => {
if (name !== link.name || url !== link.url || favorite !== link.favorite || categoryId !== link.category.id) {
if (name !== '' && IsValidURL(url) && favorite !== null && categoryId !== null) {
setCanSubmit(true);
} else {
setCanSubmit(false);
}
} else {
setCanSubmit(false);
}
}, [name, url, favorite, categoryId, link]);
const handleSubmit = async (event) => {
event.preventDefault();
setSuccess(null);
setError(null);
setCanSubmit(false);
nProgress.start();
try {
const payload = { name, url, favorite, categoryId };
const { data }: AxiosResponse<any> = await axios.put(`/api/link/edit/${link.id}`, payload);
setSuccess(data?.success || 'Lien modifié avec succès');
} catch (error) {
setError(HandleAxiosError(error));
} finally {
setCanSubmit(true);
nProgress.done();
}
}
return (<>
<FormLayout
title='Modifier un lien'
errorMessage={error}
successMessage={success}
canSubmit={canSubmit}
handleSubmit={handleSubmit}
>
<TextBox
name='name'
label='Nom'
onChangeCallback={(value) => setName(value)}
value={name}
fieldClass={styles['input-field']}
placeholder={`Nom original : ${link.name}`}
/>
<TextBox
name='url'
label='URL'
onChangeCallback={(value) => setUrl(value)}
value={url}
fieldClass={styles['input-field']}
placeholder={`URL original : ${link.url}`}
/>
<Selector
name='category'
label='Catégorie'
value={categoryId}
onChangeCallback={(value: number) => setCategoryId(value)}
options={categories.map(({ id, name }) => ({ label: name, value: id }))}
/>
<Checkbox
name='favorite'
isChecked={favorite}
onChangeCallback={(value) => setFavorite(value)}
label='Favoris'
/>
</FormLayout>
</>);
}
EditLink.authRequired = true;
export default EditLink;
export async function getServerSideProps({ query }) {
const { lid } = query;
const categoriesDB = await prisma.category.findMany();
const categories = categoriesDB.map((categoryDB) => BuildCategory(categoryDB));
const linkDB = await prisma.link.findFirst({
where: { id: Number(lid) },
include: { category: true }
});
if (!linkDB) {
return {
redirect: {
destination: '/'
}
}
}
const link = BuildLink(linkDB, { categoryId: linkDB.categoryId, categoryName: linkDB.category.name });
return {
props: {
link: JSON.parse(JSON.stringify(link)),
categories: JSON.parse(JSON.stringify(categories))
}
}
}

122
pages/link/remove/[lid].tsx Normal file
View File

@@ -0,0 +1,122 @@
import { useState } from 'react';
import axios, { AxiosResponse } from 'axios';
import nProgress from 'nprogress';
import { confirmAlert } from 'react-confirm-alert';
import 'react-confirm-alert/src/react-confirm-alert.css';
import FormLayout from '../../../components/FormLayout';
import TextBox from '../../../components/TextBox';
import Checkbox from '../../../components/Checkbox';
import styles from '../../../styles/create.module.scss';
import { Link } from '../../../types';
import { BuildLink, HandleAxiosError } from '../../../utils/front';
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
function RemoveLink({ link }: { link: Link; }) {
const [canSubmit, setCanSubmit] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState<string | null>(null);
const handleSubmit = (event) => {
event.preventDefault();
confirmAlert({
message: `Confirmer la suppression du lien "${link.name}"`,
buttons: [{
label: 'Confirmer',
onClick: async () => {
setSuccess(null);
setError(null);
setCanSubmit(false);
nProgress.start();
try {
const { data }: AxiosResponse<any> = await axios.delete(`/api/link/remove/${link.id}`);
setSuccess(data?.success || 'Lien supprimé avec succès');
setCanSubmit(false);
} catch (error) {
setError(HandleAxiosError(error));
setCanSubmit(true);
} finally {
nProgress.done();
}
}
}, {
label: 'Annuler',
onClick: () => { }
}]
});
}
return (<>
<FormLayout
title='Supprimer un lien'
errorMessage={error}
successMessage={success}
canSubmit={canSubmit}
handleSubmit={handleSubmit}
classBtnConfirm='red-btn'
textBtnConfirm='Supprimer'
>
<TextBox
name='name'
label='Nom'
value={link.name}
fieldClass={styles['input-field']}
disabled={true}
/>
<TextBox
name='url'
label='URL'
value={link.url}
fieldClass={styles['input-field']}
disabled={true}
/>
<TextBox
name='category'
label='Catégorie'
value={link.category.name}
fieldClass={styles['input-field']}
disabled={true}
/>
<Checkbox
name='favorite'
label='Favoris'
isChecked={link.favorite}
disabled={true}
/>
</FormLayout>
</>);
}
RemoveLink.authRequired = true;
export default RemoveLink;
export async function getServerSideProps({ query }) {
const { lid } = query;
const linkDB = await prisma.link.findFirst({
where: { id: Number(lid) },
include: { category: true }
});
if (!linkDB) {
return {
redirect: {
destination: '/'
}
}
}
const link = BuildLink(linkDB, { categoryId: linkDB.categoryId, categoryName: linkDB.category.name });
return {
props: {
link: JSON.parse(JSON.stringify(link))
}
}
}

55
pages/signin.tsx Normal file
View File

@@ -0,0 +1,55 @@
import { getProviders, signIn, useSession } from 'next-auth/react';
import { useRouter } from 'next/router';
import Link from 'next/link';
import Head from 'next/head';
import styles from '../styles/login.module.scss';
import MessageManager from '../components/MessageManager';
export default function SignIn({ providers }) {
const { data: session, status } = useSession();
const info = useRouter().query?.info as string;
const error = useRouter().query?.error as string;
if (status === 'loading') {
return (
<div className='App' style={{ alignItems: 'center' }}>
<p style={{ height: 'fit-content' }}>Chargement de la session en cours</p>
</div>
);
}
return (<>
<Head>
<title>Superpipo Authentification</title>
</Head>
<div className='App'>
<div className={styles['wrapper']}>
<h2>Se connecter</h2>
<MessageManager
error={error}
info={info}
/>
{session !== null && (<MessageManager info='Vous êtes déjà connecté' />)}
<div className={styles['providers']}>
{Object.values(providers).map(({ name, id }) => (
<button key={id} onClick={() => signIn(id, { callbackUrl: '/' })} disabled={session !== null}>
Continuer avec {name}
</button>
))}
</div>
<Link href='/'>
<a> Revenir à l'accueil</a>
</Link>
</div>
</div>
</>);
}
export async function getServerSideProps(context) {
const providers = await getProviders();
return {
props: { providers }
}
}