mirror of
https://github.com/Sonny93/my-links.git
synced 2025-12-08 22:53:25 +00:00
migration vers typescript + refonte total de l'app
This commit is contained in:
@@ -1,3 +1,6 @@
|
||||
{
|
||||
"extends": "next/core-web-vitals"
|
||||
"extends": "next",
|
||||
"rules": {
|
||||
"react/no-unescaped-entities": "off"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
import { signIn, signOut } from "next-auth/react"
|
||||
|
||||
import ModalAddCategory from './ModalAddCategory';
|
||||
|
||||
import styles from '../../styles/categories.module.scss';
|
||||
|
||||
export default function Categories({
|
||||
categories,
|
||||
favorites,
|
||||
handleSelectCategory,
|
||||
categoryActive,
|
||||
session
|
||||
}) {
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
|
||||
return (<div className={styles['categories-wrapper']}>
|
||||
<div className={`${styles['block-wrapper']} ${styles['favorites']}`}>
|
||||
<h4>Favoris</h4>
|
||||
<ul className={styles['items']}>
|
||||
{favorites.map((link, key) => {
|
||||
const { name, url, categoryName } = link;
|
||||
return <li key={key} className={styles['item']}>
|
||||
<a href={url} target={'_blank'} rel={'noreferrer'}>
|
||||
{name} <span className={styles['category']}>- {categoryName}</span>
|
||||
</a>
|
||||
</li>;
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
<div className={`${styles['block-wrapper']} ${styles['categories']}`}>
|
||||
<h4>Catégories</h4>
|
||||
<ul className={styles['items']}>
|
||||
{categories.map(({ id, name }, key) => {
|
||||
const className = `${styles['item']} ${id === categoryActive ? styles['active'] : ''}`;
|
||||
const onClick = () => handleSelectCategory(id);
|
||||
|
||||
return (
|
||||
<li key={key} className={className} onClick={onClick}>
|
||||
{name}
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
{session !== null ?
|
||||
<button onClick={() => setModalOpen((state) => !state)}>
|
||||
Ajouter une catégorie
|
||||
</button> :
|
||||
<div className={`${styles['block-wrapper']} ${styles['controls']}`} onClick={signIn}>
|
||||
<button>Se connecter</button>
|
||||
</div>}
|
||||
|
||||
<ModalAddCategory
|
||||
categories={categories}
|
||||
isOpen={modalOpen}
|
||||
closeModal={() => setModalOpen(false)}
|
||||
/>
|
||||
</div>);
|
||||
}
|
||||
98
components/Categories/Categories.tsx
Normal file
98
components/Categories/Categories.tsx
Normal file
@@ -0,0 +1,98 @@
|
||||
import { signOut } from "next-auth/react"
|
||||
import { Session } from 'next-auth';
|
||||
import LinkTag from "next/link";
|
||||
import Image from "next/image";
|
||||
|
||||
import styles from '../../styles/home/categories.module.scss';
|
||||
import { Category, Link } from '../../types';
|
||||
|
||||
interface CategoryProps {
|
||||
categories: Category[];
|
||||
favorites: Link[];
|
||||
handleSelectCategory: (category: Category) => void;
|
||||
categoryActive: Category;
|
||||
session: Session;
|
||||
}
|
||||
export default function Categories({
|
||||
categories,
|
||||
favorites,
|
||||
handleSelectCategory,
|
||||
categoryActive,
|
||||
session
|
||||
}: CategoryProps) {
|
||||
return (<div className={styles['categories-wrapper']}>
|
||||
<div className={`${styles['block-wrapper']} ${styles['favorites']}`}>
|
||||
<h4>Favoris</h4>
|
||||
<ul className={styles['items']}>
|
||||
{favorites.map((link, key) => (
|
||||
<LinkFavorite
|
||||
link={link}
|
||||
key={key}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div className={`${styles['block-wrapper']} ${styles['categories']}`}>
|
||||
<h4>Catégories</h4>
|
||||
<ul className={styles['items']}>
|
||||
{categories.map((category, key) => (
|
||||
<CategoryItem
|
||||
category={category}
|
||||
categoryActive={categoryActive}
|
||||
handleSelectCategory={handleSelectCategory}
|
||||
key={key}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div className={styles['controls']}>
|
||||
<LinkTag href={'/category/create'}>
|
||||
<a>Créer categorie</a>
|
||||
</LinkTag>
|
||||
<LinkTag href={'/link/create'}>
|
||||
<a>Créer lien</a>
|
||||
</LinkTag>
|
||||
</div>
|
||||
<div className={styles['user-card-wrapper']}>
|
||||
<div className={styles['user-card']}>
|
||||
<Image
|
||||
src={session.user.image}
|
||||
width={28}
|
||||
height={28}
|
||||
alt={`${session.user.name}'s avatar`}
|
||||
/>
|
||||
{session.user.name}
|
||||
</div>
|
||||
<button onClick={() => signOut()} className={styles['disconnect-btn']}>
|
||||
Se déconnecter
|
||||
</button>
|
||||
</div>
|
||||
</div>);
|
||||
}
|
||||
|
||||
function LinkFavorite({ link }: { link: Link; }): JSX.Element {
|
||||
const { name, url, category } = link;
|
||||
return (
|
||||
<li className={styles['item']}>
|
||||
<a href={url} target={'_blank'} rel={'noreferrer'}>
|
||||
{name} <span className={styles['category']}>- {category.name}</span>
|
||||
</a>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
interface CategoryItemProps {
|
||||
category: Category;
|
||||
categoryActive: Category;
|
||||
handleSelectCategory: (category: Category) => void;
|
||||
}
|
||||
function CategoryItem({ category, categoryActive, handleSelectCategory }: CategoryItemProps): JSX.Element {
|
||||
const className = `${styles['item']} ${category.id === categoryActive.id ? styles['active'] : ''}`;
|
||||
const onClick = () => handleSelectCategory(category);
|
||||
|
||||
return (
|
||||
<li className={className} onClick={onClick}>
|
||||
{category.name}<span className={styles['links-count']}> — {category.links.length}</span>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
import styles from '../../styles/links.module.scss';
|
||||
|
||||
export default function Link({ link }) {
|
||||
return (
|
||||
<li className={styles['link']}>
|
||||
<a href={link?.url} target={'_blank'} rel={'noreferrer'}>
|
||||
<span className={styles['link-name']}>{link?.name} <span className={styles['link-category']}>— {link?.categoryName}</span></span> <span className={styles['link-url']}>{link?.url}</span>
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
import { useEffect, useCallback } from 'react';
|
||||
import { useInView } from 'react-intersection-observer';
|
||||
|
||||
import Link from './Link';
|
||||
|
||||
import styles from '../../styles/links.module.scss';
|
||||
|
||||
export default function LinkBlock({ category, setCategoryActive, refCategoryActive }) {
|
||||
const { ref, inView } = useInView({ threshold: .5 });
|
||||
const { id, name, links, ref: refCategory } = category;
|
||||
|
||||
useEffect(() => inView ? setCategoryActive(id) : null, [id, inView, setCategoryActive]);
|
||||
|
||||
const setRefs = useCallback((node) => {
|
||||
refCategory.current = node;
|
||||
refCategoryActive.current = node;
|
||||
ref(node);
|
||||
}, [ref, refCategoryActive, refCategory]);
|
||||
|
||||
return (
|
||||
<div className={styles['link-block']} ref={setRefs}>
|
||||
<h2>{name}</h2>
|
||||
<ul className={styles['links']}>
|
||||
{links.map((link, key2) => (
|
||||
<Link key={key2} link={link} />
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import LinkBlock from './LinkBlock';
|
||||
|
||||
import styles from '../../styles/links.module.scss';
|
||||
|
||||
export default function Links({ categories, setCategoryActive, refCategoryActive }) {
|
||||
return (<div className={styles['links-wrapper']}>
|
||||
{categories.map((category, key) => (
|
||||
<LinkBlock
|
||||
key={key}
|
||||
category={category}
|
||||
setCategoryActive={setCategoryActive}
|
||||
refCategoryActive={refCategoryActive}
|
||||
/>
|
||||
))}
|
||||
</div>);
|
||||
}
|
||||
56
components/Links/Links.tsx
Normal file
56
components/Links/Links.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import styles from '../../styles/home/links.module.scss';
|
||||
import { Category, Link } from '../../types';
|
||||
|
||||
export default function Links({ category }: { category: Category; }) {
|
||||
const { name, links } = category;
|
||||
|
||||
return (<div className={styles['links-wrapper']}>
|
||||
<h2>{name}<span className={styles['links-count']}> — {links.length}</span></h2>
|
||||
<ul className={styles['links']}>
|
||||
{links.map((link, key) => (
|
||||
<LinkItem key={key} link={link} />
|
||||
))}
|
||||
</ul>
|
||||
</div>);
|
||||
}
|
||||
|
||||
function LinkItem({ link }: { link: Link; }) {
|
||||
const { name, url, category } = link;
|
||||
const { origin, pathname, search } = new URL(url);
|
||||
|
||||
return (
|
||||
<li className={styles['link']}>
|
||||
<a href={url} target={'_blank'} rel={'noreferrer'}>
|
||||
<span className={styles['link-name']}>
|
||||
{name}<span className={styles['link-category']}> — {category.name}</span>
|
||||
</span>
|
||||
<LinkItemURL
|
||||
origin={origin}
|
||||
pathname={pathname}
|
||||
search={search}
|
||||
/>
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
function LinkItemURL({ origin, pathname, search }) {
|
||||
let text = '';
|
||||
|
||||
if (pathname !== '/') {
|
||||
text += pathname;
|
||||
}
|
||||
|
||||
if (search !== '') {
|
||||
if (text === '') {
|
||||
text += '/';
|
||||
}
|
||||
text += search;
|
||||
}
|
||||
|
||||
return (
|
||||
<span className={styles['link-url']}>
|
||||
{origin}<span className={styles['url-pathname']}>{text}</span>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
57
components/input.tsx
Normal file
57
components/input.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import { MutableRefObject, useState } from "react";
|
||||
|
||||
interface InputProps {
|
||||
name: string;
|
||||
label?: string;
|
||||
labelComponent?: JSX.Element;
|
||||
type?: string;
|
||||
multiple?: boolean;
|
||||
innerRef?: MutableRefObject<any>;
|
||||
placeholder?: string;
|
||||
fieldClass?: string;
|
||||
value?: string;
|
||||
onChangeCallback: ({ target }, value) => void;
|
||||
}
|
||||
|
||||
export default function Input({
|
||||
name,
|
||||
label,
|
||||
labelComponent,
|
||||
type = 'text',
|
||||
multiple = false,
|
||||
innerRef = null,
|
||||
placeholder = 'Type something...',
|
||||
fieldClass = '',
|
||||
value,
|
||||
onChangeCallback
|
||||
}: InputProps): JSX.Element {
|
||||
const [inputValue, setInputValue] = useState<string>(value);
|
||||
|
||||
function onChange({ target }) {
|
||||
setInputValue(target.value);
|
||||
onChangeCallback({ target }, target.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>
|
||||
)}
|
||||
<input
|
||||
id={name}
|
||||
name={name}
|
||||
type={type}
|
||||
onChange={onChange}
|
||||
value={inputValue}
|
||||
multiple={multiple}
|
||||
placeholder={placeholder}
|
||||
ref={innerRef}
|
||||
/>
|
||||
</div>);
|
||||
}
|
||||
52
components/selector.tsx
Normal file
52
components/selector.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import { MutableRefObject, useState } from "react";
|
||||
|
||||
interface SelectorProps {
|
||||
name: string;
|
||||
label?: string;
|
||||
labelComponent?: JSX.Element;
|
||||
innerRef?: MutableRefObject<any>;
|
||||
fieldClass?: string;
|
||||
value?: string | number;
|
||||
onChangeCallback: ({ target }, value) => void;
|
||||
children?: any;
|
||||
}
|
||||
|
||||
export default function Selector({
|
||||
name,
|
||||
label,
|
||||
labelComponent,
|
||||
innerRef = null,
|
||||
fieldClass = '',
|
||||
value,
|
||||
onChangeCallback,
|
||||
children
|
||||
}: SelectorProps): JSX.Element {
|
||||
const [inputValue, setInputValue] = useState<string | number>(value);
|
||||
|
||||
function onChange({ target }) {
|
||||
setInputValue(target.value);
|
||||
onChangeCallback({ target }, target.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
|
||||
id={name}
|
||||
name={name}
|
||||
onChange={onChange}
|
||||
value={inputValue}
|
||||
ref={innerRef}
|
||||
>
|
||||
{children}
|
||||
</select>
|
||||
</div>);
|
||||
}
|
||||
7
example.env.local
Normal file
7
example.env.local
Normal file
@@ -0,0 +1,7 @@
|
||||
NEXTAUTH_URL=http://localhost:3000
|
||||
NEXTAUTH_URL_INTERNAL=http://localhost:3000
|
||||
|
||||
NEXTAUTH_SECRET=
|
||||
|
||||
GOOGLE_CLIENT_ID=
|
||||
GOOGLE_CLIENT_SECRET=
|
||||
5
next-env.d.ts
vendored
Normal file
5
next-env.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
||||
@@ -1,3 +1,6 @@
|
||||
module.exports = {
|
||||
reactStrictMode: true,
|
||||
images: {
|
||||
domains: ['lh3.googleusercontent.com']
|
||||
}
|
||||
}
|
||||
|
||||
5920
package-lock.json
generated
5920
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
17
package.json
17
package.json
@@ -8,19 +8,24 @@
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@prisma/client": "3.6.0",
|
||||
"@prisma/client": "^3.13.0",
|
||||
"@reduxjs/toolkit": "^1.8.1",
|
||||
"bcrypt": "^5.0.1",
|
||||
"next": "12.0.7",
|
||||
"next": "^12.1.5",
|
||||
"next-auth": "^4.0.6",
|
||||
"react": "17.0.2",
|
||||
"react-dom": "17.0.2",
|
||||
"react-intersection-observer": "^8.33.1",
|
||||
"nprogress": "^0.2.0",
|
||||
"react": "^18.1.0",
|
||||
"react-dom": "^18.1.0",
|
||||
"react-modal": "^3.14.4",
|
||||
"react-redux": "^8.0.1",
|
||||
"sass": "^1.46.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^17.0.29",
|
||||
"@types/nprogress": "^0.2.0",
|
||||
"@types/react": "^18.0.8",
|
||||
"eslint": "7",
|
||||
"eslint-config-next": "12.0.7",
|
||||
"prisma": "3.6.0"
|
||||
"prisma": "^3.13.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
import '../styles/globals.scss';
|
||||
import { SessionProvider } from 'next-auth/react';
|
||||
|
||||
function MyApp({
|
||||
Component,
|
||||
pageProps: { session, ...pageProps }
|
||||
}) {
|
||||
return (
|
||||
<SessionProvider session={session}>
|
||||
<Component {...pageProps} />
|
||||
</SessionProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default MyApp;
|
||||
34
pages/_app.tsx
Normal file
34
pages/_app.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import { SessionProvider } from 'next-auth/react';
|
||||
import nProgress from "nprogress";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import '../styles/globals.scss';
|
||||
import "nprogress/nprogress.css"
|
||||
import { useEffect } from 'react';
|
||||
|
||||
function MyApp({
|
||||
Component,
|
||||
pageProps: { session, ...pageProps }
|
||||
}) {
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => { // Chargement pages
|
||||
router.events.on('routeChangeStart', nProgress.start);
|
||||
router.events.on('routeChangeComplete', nProgress.done);
|
||||
router.events.on('routeChangeError', nProgress.done);
|
||||
|
||||
return () => {
|
||||
router.events.off('routeChangeStart', nProgress.start);
|
||||
router.events.off('routeChangeComplete', nProgress.done);
|
||||
router.events.off('routeChangeError', nProgress.done);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<SessionProvider session={session}>
|
||||
<Component {...pageProps} />
|
||||
</SessionProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default MyApp;
|
||||
17
pages/_document.tsx
Normal file
17
pages/_document.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import { Html, Head, Main, NextScript } from 'next/document';
|
||||
|
||||
const Document = () => (
|
||||
<Html lang='fr'>
|
||||
<Head />
|
||||
<title>Superpipo</title>
|
||||
<body>
|
||||
<noscript>
|
||||
Vous devez activer JavaScript pour utiliser ce site
|
||||
</noscript>
|
||||
<Main />
|
||||
<NextScript />
|
||||
</body>
|
||||
</Html>
|
||||
)
|
||||
|
||||
export default Document;
|
||||
@@ -1,48 +1,30 @@
|
||||
import NextAuth from 'next-auth'
|
||||
import CredentialsProvider from 'next-auth/providers/credentials';
|
||||
import bcrypt from 'bcrypt';
|
||||
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
const prisma = new PrismaClient();
|
||||
import NextAuth from 'next-auth';
|
||||
import GoogleProvider from 'next-auth/providers/google';
|
||||
|
||||
export default NextAuth({
|
||||
providers: [
|
||||
CredentialsProvider({
|
||||
name: 'Credentials',
|
||||
credentials: {
|
||||
username: { label: 'Email', type: 'text', placeholder: 'user@example.com' },
|
||||
password: { label: 'Mot de passe', type: 'password', placeholder: '********' }
|
||||
},
|
||||
async authorize(credentials, req) {
|
||||
return { email: 'user@example.com' };
|
||||
|
||||
const email = credentials?.email;
|
||||
const password = credentials?.password;
|
||||
|
||||
if (!email || !password)
|
||||
return null;
|
||||
|
||||
let user;
|
||||
try {
|
||||
user = await prisma.user.findUnique({
|
||||
where: { email }
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`Impossible de récupérer l'utilisateur avec les identifiants : ${credentials}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!user)
|
||||
return null;
|
||||
|
||||
const passwordMatch = await bcrypt.compare(password, user.password);
|
||||
if (!passwordMatch)
|
||||
return null;
|
||||
|
||||
return {
|
||||
email: user.email
|
||||
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;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
45
pages/category/create.tsx
Normal file
45
pages/category/create.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import { useState } from 'react';
|
||||
import { useSession } from 'next-auth/react';
|
||||
import Link from 'next/link';
|
||||
|
||||
import Input from '../../components/input';
|
||||
import styles from '../../styles/create.module.scss';
|
||||
import Head from 'next/head';
|
||||
|
||||
export default function CreateCategory() {
|
||||
const { data: session, status } = useSession({ required: true });
|
||||
const [name, setName] = useState<string>('');
|
||||
|
||||
if (status === 'loading') {
|
||||
return (<p>Chargement de la session en cours</p>)
|
||||
}
|
||||
|
||||
const handleSubmit = (event) => {
|
||||
event.preventDefault();
|
||||
console.log('On peut envoyer la requête pour créer une catégorie');
|
||||
}
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
import { createRef, useRef, useState } from 'react';
|
||||
import { useSession } from 'next-auth/react';
|
||||
|
||||
import Categories from '../components/Categories/Categories';
|
||||
import Links from '../components/Links/Links';
|
||||
|
||||
import styles from '../styles/Home.module.scss';
|
||||
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
export default function Home({ categories, favorites }) {
|
||||
const { data: session, status } = useSession();
|
||||
const [categoryActive, setCategoryActive] = useState(categories?.[0]?.id);
|
||||
const refCategoryActive = useRef();
|
||||
|
||||
const handleSelectCategory = (id) => {
|
||||
if (!refCategoryActive?.current) return;
|
||||
|
||||
const { ref } = categories.find(c => c.id === id);
|
||||
ref?.current?.scrollIntoView({
|
||||
block: 'end',
|
||||
behavior: 'smooth'
|
||||
});
|
||||
}
|
||||
console.log(session, session?.user);
|
||||
|
||||
return (
|
||||
<div className={styles.App}>
|
||||
<Categories
|
||||
categories={categories}
|
||||
favorites={favorites}
|
||||
handleSelectCategory={handleSelectCategory}
|
||||
categoryActive={categoryActive}
|
||||
session={session}
|
||||
/>
|
||||
<Links
|
||||
categories={categories}
|
||||
setCategoryActive={setCategoryActive}
|
||||
refCategoryActive={refCategoryActive}
|
||||
session={session}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export async function getStaticProps(context) {
|
||||
const categories = await prisma.category.findMany({
|
||||
include: {
|
||||
links: true
|
||||
}
|
||||
});
|
||||
|
||||
const favorites = [];
|
||||
categories.map((category) => {
|
||||
category['ref'] = createRef();
|
||||
category['links'] = category.links.map((link) => {
|
||||
if (link.favorite)
|
||||
favorites.push(link);
|
||||
|
||||
link['categoryName'] = category.name;
|
||||
return link;
|
||||
});
|
||||
|
||||
return category;
|
||||
});
|
||||
|
||||
return {
|
||||
props: {
|
||||
categories: JSON.parse(JSON.stringify(categories)),
|
||||
favorites: JSON.parse(JSON.stringify(favorites))
|
||||
}
|
||||
}
|
||||
}
|
||||
93
pages/index.tsx
Normal file
93
pages/index.tsx
Normal file
@@ -0,0 +1,93 @@
|
||||
import { createRef, useRef, useState } from 'react';
|
||||
import { useSession } from 'next-auth/react';
|
||||
import { Provider } from 'react-redux';
|
||||
import Head from 'next/head'
|
||||
|
||||
import Categories from '../components/Categories/Categories';
|
||||
import Links from '../components/Links/Links';
|
||||
|
||||
import { Category, Link } from '../types';
|
||||
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
import { store } from '../redux';
|
||||
|
||||
interface HomeProps {
|
||||
categories: Category[];
|
||||
favorites: Link[];
|
||||
}
|
||||
|
||||
export default function Home({ categories, favorites }: HomeProps) {
|
||||
const { data: session, status } = 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'>
|
||||
<Categories
|
||||
categories={categories}
|
||||
favorites={favorites}
|
||||
handleSelectCategory={handleSelectCategory}
|
||||
categoryActive={categoryActive}
|
||||
session={session}
|
||||
/>
|
||||
<Links category={categoryActive} />
|
||||
</div>
|
||||
</Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export async function getStaticProps() {
|
||||
const categoriesDB = await prisma.category.findMany({ include: { links: true } });
|
||||
|
||||
const favorites = [] as Link[];
|
||||
const categories = categoriesDB.map((categoryDB) => {
|
||||
const category = BuildCategory(categoryDB);
|
||||
category.links.map((link) => link.favorite ? favorites.push(link) : null);
|
||||
return category;
|
||||
});
|
||||
|
||||
return {
|
||||
props: {
|
||||
categories: JSON.parse(JSON.stringify(categories)),
|
||||
favorites: JSON.parse(JSON.stringify(favorites)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function BuildCategory({ id, name, order, links = [], createdAt, updatedAt }): Category {
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
links: links.map((link) => BuildLink(link, { categoryId: id, categoryName: name })),
|
||||
order,
|
||||
createdAt,
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
|
||||
export function BuildLink({ id, name, url, order, favorite, createdAt, updatedAt }, { categoryId, categoryName }): Link {
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
url,
|
||||
category: {
|
||||
id: categoryId,
|
||||
name: categoryName
|
||||
},
|
||||
order,
|
||||
favorite,
|
||||
createdAt,
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
94
pages/link/create.tsx
Normal file
94
pages/link/create.tsx
Normal file
@@ -0,0 +1,94 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useSession } from 'next-auth/react';
|
||||
import Link from 'next/link';
|
||||
|
||||
import Input from '../../components/input';
|
||||
import styles from '../../styles/create.module.scss';
|
||||
|
||||
import { Category } from '../../types';
|
||||
import { BuildCategory } from '..';
|
||||
|
||||
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 });
|
||||
const [name, setName] = useState<string>('');
|
||||
const [url, setUrl] = useState<string>('');
|
||||
const [categoryId, setCategoryId] = useState<number | null>(categories?.[0].id || 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 {
|
||||
setCanSubmit(true);
|
||||
}
|
||||
}, [name, url, categoryId]);
|
||||
|
||||
if (status === 'loading') {
|
||||
return (<p>Chargement de la session en cours</p>)
|
||||
}
|
||||
|
||||
const handleSubmit = (event) => {
|
||||
event.preventDefault();
|
||||
console.log('On peut envoyer la requête pour créer un lien');
|
||||
}
|
||||
|
||||
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>
|
||||
</>);
|
||||
}
|
||||
|
||||
export async function getStaticProps() {
|
||||
const categoriesDB = await prisma.category.findMany();
|
||||
const categories = categoriesDB.map((categoryDB) => BuildCategory(categoryDB));
|
||||
|
||||
return {
|
||||
props: {
|
||||
categories: JSON.parse(JSON.stringify(categories))
|
||||
}
|
||||
}
|
||||
}
|
||||
89
redux.ts
Normal file
89
redux.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import { configureStore, createSlice } from "@reduxjs/toolkit";
|
||||
import { Category, Link } from "./types";
|
||||
|
||||
const categoriesSlice = createSlice({
|
||||
name: 'categories',
|
||||
initialState: [] as Category[],
|
||||
reducers: {
|
||||
addCategory: (state: Category[], { payload }: { payload: Category }) => {
|
||||
const categories = [...state];
|
||||
categories.push(payload);
|
||||
return categories;
|
||||
},
|
||||
editCategory: (state: Category[], { payload }: { payload: Category }) => {
|
||||
const categories = [...state];
|
||||
const categoryIndex = categories.findIndex(category => category.id === payload.id);
|
||||
if (categoryIndex === -1) {
|
||||
return categories
|
||||
}
|
||||
|
||||
categories[categoryIndex] = payload;
|
||||
return categories;
|
||||
},
|
||||
removeCategory: (state: Category[], { payload }: { payload: Category }) => {
|
||||
const categories = [...state];
|
||||
const categoryIndex = categories.findIndex(category => category.id === payload.id);
|
||||
if (categoryIndex === -1) {
|
||||
return categories
|
||||
}
|
||||
|
||||
categories.slice(categoryIndex, 1);
|
||||
return categories;
|
||||
},
|
||||
|
||||
addLink: (state: Category[], { payload }: { payload: Link }) => {
|
||||
const categories = [...state];
|
||||
const categoryIndex = state.findIndex(link => link.id === payload.id);
|
||||
if (categoryIndex === -1) {
|
||||
return categories;
|
||||
}
|
||||
|
||||
categories[categoryIndex].links.push(payload);
|
||||
return categories;
|
||||
},
|
||||
editLink: (state: Category[], { payload }: { payload: Link }) => {
|
||||
const categories = [...state];
|
||||
const categoryIndex = state.findIndex(link => link.id === payload.id);
|
||||
if (categoryIndex === -1) {
|
||||
return categories;
|
||||
}
|
||||
|
||||
const linkIndex = categories[categoryIndex].links.findIndex(link => link.id === payload.id);
|
||||
if (linkIndex === -1) {
|
||||
return categories;
|
||||
}
|
||||
|
||||
categories[categoryIndex].links[linkIndex] = payload;
|
||||
return categories;
|
||||
},
|
||||
removeLink: (state: Category[], { payload }: { payload: Link }) => {
|
||||
const categories = [...state];
|
||||
const categoryIndex = state.findIndex(link => link.id === payload.id);
|
||||
if (categoryIndex === -1) {
|
||||
return categories;
|
||||
}
|
||||
|
||||
const linkIndex = categories[categoryIndex].links.findIndex(link => link.id === payload.id);
|
||||
if (linkIndex === -1) {
|
||||
return categories;
|
||||
}
|
||||
|
||||
categories[categoryIndex].links.splice(linkIndex, 1);
|
||||
return categories;
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
export const store = configureStore({
|
||||
reducer: {
|
||||
categories: categoriesSlice.reducer
|
||||
},
|
||||
middleware: (getDefaultMiddleware) => getDefaultMiddleware({ serializableCheck: false })
|
||||
});
|
||||
|
||||
export const {
|
||||
addCategory,
|
||||
addLink,
|
||||
editLink,
|
||||
removeLink
|
||||
} = categoriesSlice.actions;
|
||||
@@ -1,13 +0,0 @@
|
||||
.App {
|
||||
height: 100%;
|
||||
width: 1280px;
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
@media (max-width: 1280px) {
|
||||
.App {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
27
styles/create.module.scss
Normal file
27
styles/create.module.scss
Normal file
@@ -0,0 +1,27 @@
|
||||
.create-app {
|
||||
width: 680px;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
|
||||
& h2 {
|
||||
color: #3f88c5;
|
||||
}
|
||||
|
||||
& form {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
& .input-field {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 680px) {
|
||||
.create-app {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ body {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
color: #111;
|
||||
background-color: #f0eef6;
|
||||
font-family: "Poppins", sans-serif;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
@@ -27,6 +28,28 @@ body {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.App {
|
||||
height: 100%;
|
||||
width: 1280px;
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
a {
|
||||
width: fit-content;
|
||||
color: #3f88c5;
|
||||
border-bottom: 1px solid transparent;
|
||||
text-decoration: none;
|
||||
transition: 0.15s;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
color: #3d7bab;
|
||||
border-bottom: 1px solid #3d7bab;
|
||||
}
|
||||
}
|
||||
|
||||
ul,
|
||||
li {
|
||||
list-style: none;
|
||||
@@ -61,9 +84,61 @@ button {
|
||||
padding: 10px;
|
||||
border: 1px solid #3f88c5;
|
||||
border-radius: 3px;
|
||||
transition: .15s;
|
||||
transition: 0.15s;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.01);
|
||||
&:disabled {
|
||||
cursor: default;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
&:not(:disabled):hover {
|
||||
box-shadow: #105b97 0 0 3px 1px;
|
||||
background: #105b97;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
input:not(.nostyle) {
|
||||
color: #333;
|
||||
background: #fff;
|
||||
padding: 10px;
|
||||
border: 1px solid #dadce0;
|
||||
border-bottom: 3px solid #dadce0;
|
||||
transition: 0.15s;
|
||||
|
||||
&:focus {
|
||||
border-bottom: 3px solid #3f88c5;
|
||||
}
|
||||
}
|
||||
|
||||
input::placeholder {
|
||||
font-style: italic;
|
||||
color: #dadce0;
|
||||
}
|
||||
|
||||
.input-field {
|
||||
& label,
|
||||
& input,
|
||||
& select {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
select:not(.nostyle) {
|
||||
color: #333;
|
||||
background: #fff;
|
||||
padding: 10px;
|
||||
border: 1px solid #dadce0;
|
||||
border-bottom: 3px solid #dadce0;
|
||||
transition: 0.15s;
|
||||
|
||||
&:focus {
|
||||
border-bottom: 3px solid #3f88c5;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1280px) {
|
||||
.App {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
@@ -12,12 +12,11 @@
|
||||
& .block-wrapper {
|
||||
height: auto;
|
||||
width: 100%;
|
||||
margin-bottom: 15px;
|
||||
|
||||
& h4 {
|
||||
user-select: none;
|
||||
text-transform: uppercase;
|
||||
font-size: .85em;
|
||||
font-size: 0.85em;
|
||||
color: #bbb;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
@@ -26,12 +25,21 @@
|
||||
& .items .item {
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
padding: 5px 10px;
|
||||
background-color: #fff;
|
||||
padding: 7px 12px;
|
||||
border: 1px solid #dadce0;
|
||||
border-bottom: 2px solid #dadce0;
|
||||
border-radius: 3px;
|
||||
margin-bottom: 5px;
|
||||
transition: .15s;
|
||||
transition: 0.15s;
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
& .links-count {
|
||||
color: #bbb;
|
||||
font-size: 0.85em;
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: #fff;
|
||||
@@ -41,7 +49,7 @@
|
||||
|
||||
&:hover:not(.active) {
|
||||
color: #3f88c5;
|
||||
background: #eee;
|
||||
background: #f0eef6;
|
||||
border-bottom: 2px solid #3f88c5;
|
||||
}
|
||||
}
|
||||
@@ -49,6 +57,8 @@
|
||||
|
||||
// Favorites
|
||||
& .block-wrapper.favorites {
|
||||
margin-bottom: 15px;
|
||||
|
||||
& .items .item a {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
@@ -56,7 +66,7 @@
|
||||
|
||||
& .items .item .category {
|
||||
color: #bbb;
|
||||
font-size: .85em;
|
||||
font-size: 0.85em;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,16 +87,50 @@
|
||||
}
|
||||
|
||||
// Controls
|
||||
& .block-wrapper.controls {
|
||||
margin-bottom: 0;
|
||||
.controls {
|
||||
margin: 10px 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.modal {
|
||||
height: 500px;
|
||||
width: 500px;
|
||||
// User Card
|
||||
& .user-card-wrapper {
|
||||
position: relative;
|
||||
height: fit-content;
|
||||
width: 100%;
|
||||
color: #333;
|
||||
background-color: #fff;
|
||||
|
||||
& .user-card {
|
||||
border-right: 1px solid #dadce0;
|
||||
padding: 7px 12px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
|
||||
& img {
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
& .disconnect-btn {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
color: #fff;
|
||||
background-color: red;
|
||||
border: 1px solid red;
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:hover .disconnect-btn {
|
||||
display: block;
|
||||
border: 1px solid darkred;
|
||||
box-shadow: red 0 0 3px 1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
72
styles/home/links.module.scss
Normal file
72
styles/home/links.module.scss
Normal file
@@ -0,0 +1,72 @@
|
||||
.links-wrapper {
|
||||
height: 100%;
|
||||
padding: 10px;
|
||||
flex: 1;
|
||||
overflow-x: auto;
|
||||
|
||||
& h2 {
|
||||
color: #3f88c5;
|
||||
margin-bottom: 15px;
|
||||
|
||||
& .links-count {
|
||||
color: #bbb;
|
||||
font-weight: 300;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
}
|
||||
|
||||
& .links {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
& .links .link {
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
|
||||
& > a {
|
||||
height: fit-content;
|
||||
width: 100%;
|
||||
color: #3f88c5;
|
||||
background-color: #fff;
|
||||
text-decoration: none;
|
||||
padding: 10px 15px;
|
||||
border: 1px solid #dadce0;
|
||||
border-bottom: 2px solid #dadce0;
|
||||
border-radius: 3px;
|
||||
margin-bottom: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: 0.1s;
|
||||
|
||||
&:hover {
|
||||
border-bottom-color: #3f88c5;
|
||||
background: #f0eef6;
|
||||
|
||||
& .url-pathname {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
}
|
||||
|
||||
& .link-name .link-category {
|
||||
color: #bbb;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
& .link-url {
|
||||
width: 100%;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
color: #bbb;
|
||||
font-size: 0.8em;
|
||||
|
||||
& .url-pathname {
|
||||
opacity: 0;
|
||||
transition: 0.1s;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
.links-wrapper {
|
||||
height: 100%;
|
||||
padding: 10px;
|
||||
flex: 1;
|
||||
overflow-x: auto;
|
||||
scroll-snap-type: y mandatory;
|
||||
|
||||
& .link-block {
|
||||
min-height: 100%;
|
||||
width: 100%;
|
||||
margin-bottom: 10px;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
scroll-snap-align: center;
|
||||
|
||||
& h2 {
|
||||
color: #3f88c5;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
& .links {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
& .links .link {
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
|
||||
& > a {
|
||||
color: #3f88c5;
|
||||
text-decoration: none;
|
||||
padding: 10px 15px;
|
||||
border: 1px solid #dadce0;
|
||||
border-bottom: 2px solid #dadce0;
|
||||
border-radius: 3px;
|
||||
margin-bottom: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: .05s;
|
||||
|
||||
&:hover {
|
||||
border-bottom-color: #3f88c5;
|
||||
background: #eee;
|
||||
margin: 0 5px 10px;
|
||||
}
|
||||
|
||||
& .link-name .link-category {
|
||||
color: #bbb;
|
||||
font-size: .9em;
|
||||
}
|
||||
|
||||
& .link-url {
|
||||
width: 100%;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
color: #bbb;
|
||||
font-size: .8em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
31
tsconfig.json
Normal file
31
tsconfig.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": false,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
|
||||
28
types.d.ts
vendored
Normal file
28
types.d.ts
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
export interface Category {
|
||||
id: number;
|
||||
name: string;
|
||||
|
||||
links: Link[];
|
||||
order: number;
|
||||
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export interface Link {
|
||||
id: number;
|
||||
|
||||
name: string;
|
||||
url: string;
|
||||
|
||||
category: {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
order: number;
|
||||
favorite: boolean;
|
||||
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
Reference in New Issue
Block a user