Merge pull request #177 from Etheonor/feature/clean

Eslint massive clean
This commit is contained in:
Michael B
2022-01-15 23:45:41 +01:00
committed by GitHub
47 changed files with 1281 additions and 437 deletions

View File

@@ -8,20 +8,30 @@
"extends": [ "extends": [
"next", "next",
"next/core-web-vitals", "next/core-web-vitals",
"plugin:@typescript-eslint/recommended-requiring-type-checking" "plugin:@typescript-eslint/recommended-requiring-type-checking",
"plugin:sonarjs/recommended",
"plugin:unicorn/recommended"
], ],
"parserOptions": { "parserOptions": {
"ecmaFeatures": { "project": [
"jsx": true "./tsconfig.json"
}, ]
"ecmaVersion": 12,
"sourceType": "module"
}, },
"parser": "@typescript-eslint/parser",
"ignorePatterns": [
"node_modules/*",
".next/*",
".out/*",
"!.prettierrc.js",
"types/*",
"next-env.d.ts"
],
"plugins": [ "plugins": [
"@typescript-eslint", "@typescript-eslint",
"cypress", "cypress",
"simple-import-sort", "simple-import-sort",
"prettier" "prettier",
"sonarjs"
], ],
"rules": { "rules": {
"no-console": "off", "no-console": "off",
@@ -61,6 +71,16 @@
"allowHigherOrderFunctions": true, "allowHigherOrderFunctions": true,
"allowConciseArrowFunctionExpressionsStartingWithVoid": true "allowConciseArrowFunctionExpressionsStartingWithVoid": true
} }
],
"unicorn/no-null": "off",
"unicorn/filename-case": [
"error",
{
"cases": {
"camelCase": true,
"pascalCase": true
}
}
] ]
}, },
"settings": { "settings": {

View File

@@ -1,3 +1,4 @@
.next .next
dist dist
node_modules/ node_modules/
types/

View File

@@ -1,8 +1,8 @@
## Welcome to SupaNexTail! ## Welcome to SupaNexTail!
## Documentation 2.0 ## Documentation 2.0
A new documentation is available here : https://doc.supanextail.dev/
A new documentation is available here : https://doc.supanextail.dev/
## ![](https://lh4.googleusercontent.com/0qrns6BGMEh95de3BAE12YRRJceEACWdH09Yj6r7J5MswKG_R6zv7jcHEOUWFiWa7_2Yr6n6m0gSHg7iLa4lb-E0jEqZH6uJHJg3aNjbYO9LGWtCVV4dIi6BKKYUAMiFfvEOtefl) ## ![](https://lh4.googleusercontent.com/0qrns6BGMEh95de3BAE12YRRJceEACWdH09Yj6r7J5MswKG_R6zv7jcHEOUWFiWa7_2Yr6n6m0gSHg7iLa4lb-E0jEqZH6uJHJg3aNjbYO9LGWtCVV4dIi6BKKYUAMiFfvEOtefl)
@@ -18,7 +18,7 @@ Simply follow the installation process. You need to have some knowledge with Rea
#### SupaNexTail project #### SupaNexTail project
You'll need to fork this repository. I suggest to keep your repository sync with SupaNexTail, in order to get all future updates. You'll need to fork this repository. I suggest to keep your repository sync with SupaNexTail, in order to get all future updates.
To do that, you'll have extended information on this page: https://docs.github.com/en/github/collaborating-with-pull-requests/working-with-forks/syncing-a-fork To do that, you'll have extended information on this page: https://docs.github.com/en/github/collaborating-with-pull-requests/working-with-forks/syncing-a-fork
@@ -79,7 +79,7 @@ Notes:
If you want to use Stripe, be sure to set up your webhooks in the dashboard. If you want to test it locally, install Stripe CLI and use this command line: If you want to use Stripe, be sure to set up your webhooks in the dashboard. If you want to test it locally, install Stripe CLI and use this command line:
``` ```
stripe listen --forward-to localhost:3000/api/stripe/stripe-webhook stripe listen --forward-to localhost:3000/api/stripe/webhook
``` ```
The two event needed are: The two event needed are:

View File

@@ -9,25 +9,25 @@ import React, { useEffect, useState } from 'react';
import Image from 'next/image'; import Image from 'next/image';
import { supabase } from 'utils/supabaseClient'; import { supabase } from 'utils/supabaseClient';
type AvatarProps = { type AvatarProperties = {
url: string; url: string;
size: number; size: number;
onUpload: (filePath: string) => void; onUpload: (filePath: string) => void;
}; };
const Avatar = ({ url, size, onUpload }: AvatarProps): JSX.Element => { const customImgLoader = ({ src }: { src: string }): string => {
return `${src}`;
};
const Avatar = ({ url, size, onUpload }: AvatarProperties): JSX.Element => {
const [avatarUrl, setAvatarUrl] = useState(''); const [avatarUrl, setAvatarUrl] = useState('');
const [uploading, setUploading] = useState(false); const [uploading, setUploading] = useState(false);
const customImgLoader = ({ src }: { src: string }) => {
return `${src}`;
};
useEffect(() => { useEffect(() => {
if (url) downloadImage(url); if (url) void downloadImage(url);
}, [url]); }, [url]);
async function downloadImage(path: string) { async function downloadImage(path: string): Promise<void> {
try { try {
const { data, error } = await supabase.storage const { data, error } = await supabase.storage
.from('avatars') .from('avatars')
@@ -41,12 +41,14 @@ const Avatar = ({ url, size, onUpload }: AvatarProps): JSX.Element => {
} }
} catch (error: unknown) { } catch (error: unknown) {
if (error instanceof Error) { if (error instanceof Error) {
console.log('Error downloading image: ', error.message); console.log('Error downloading image:', error.message);
} }
} }
} }
async function uploadAvatar(event: React.ChangeEvent<HTMLInputElement>) { async function uploadAvatar(
event: React.ChangeEvent<HTMLInputElement>
): Promise<void> {
try { try {
setUploading(true); setUploading(true);
@@ -55,11 +57,11 @@ const Avatar = ({ url, size, onUpload }: AvatarProps): JSX.Element => {
} }
const file = event.target.files[0]; const file = event.target.files[0];
const fileExt = file.name.split('.').pop(); const fileExtension = file.name.split('.').pop();
const fileName = `${Math.random()}.${fileExt}`; const fileName = fileExtension ? `${Math.random()}.${fileExtension}` : '';
const filePath = `${fileName}`; const filePath = `${fileName}`;
if (event.target.files[0].size > 150000) { if (event.target.files[0].size > 150_000) {
alert('File is too big!'); alert('File is too big!');
event.target.value = ''; event.target.value = '';
setUploading(false); setUploading(false);

View File

@@ -19,37 +19,37 @@ const CardsLanding = (): JSX.Element => (
</p> </p>
<div className="flex flex-wrap justify-center mt-10"> <div className="flex flex-wrap justify-center mt-10">
<CardLanding <CardLanding
image={cardPage} image={cardPage as string}
text="7 pages fully designed and easily customizable" text="7 pages fully designed and easily customizable"
title="Templates" title="Templates"
/> />
<CardLanding <CardLanding
image={cardServer} image={cardServer as string}
text="Integrated backend already setup with Next.js API Routes" text="Integrated backend already setup with Next.js API Routes"
title="Backend" title="Backend"
/> />
<CardLanding <CardLanding
image={cardAuth} image={cardAuth as string}
text="Auth and user management with Supabase" text="Auth and user management with Supabase"
title="Auth" title="Auth"
/> />
<CardLanding <CardLanding
image={cardResponsive} image={cardResponsive as string}
text="Mobile ready, fully responsive and customizable with Tailwind CSS" text="Mobile ready, fully responsive and customizable with Tailwind CSS"
title="Responsive" title="Responsive"
/> />
<CardLanding <CardLanding
image={cardTheme} image={cardTheme as string}
text="Custom themes available and easily switch to dark mode" text="Custom themes available and easily switch to dark mode"
title="Themes" title="Themes"
/> />
<CardLanding <CardLanding
image={cardStripe} image={cardStripe as string}
text="Stripe integration. Fully functional subscription system" text="Stripe integration. Fully functional subscription system"
title="Payment" title="Payment"
/> />
<CardLanding <CardLanding
image={cardFee} image={cardFee as string}
text="One-time fee. No subscription, youll have access to all the updates" text="One-time fee. No subscription, youll have access to all the updates"
title="Lifetime access" title="Lifetime access"
/> />

View File

@@ -9,39 +9,47 @@ the axios.post here, line 18.
import axios from 'axios'; import axios from 'axios';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
const Contact = (): JSX.Element => { const sendEmail = (): void => {
const sendEmail = () => { const name = (document.querySelector('#name') as HTMLInputElement).value;
const name = (document.getElementById('name') as HTMLInputElement).value; const email = (document.querySelector('#email') as HTMLInputElement).value;
const email = (document.getElementById('email') as HTMLInputElement).value; const message = (document.querySelector('#message') as HTMLInputElement)
const message = (document.getElementById('message') as HTMLInputElement) .value;
.value;
if (name && email && message) { if (name && email && message) {
axios axios
.post('/api/sendgrid', { email, name, message }) .post('/api/sendgrid', { email, name, message })
.then((result) => { .then(
(result: {
data: {
success: boolean;
message: string;
};
}) => {
if (result.data.success === true) { if (result.data.success === true) {
toast.success(result.data.message); toast.success(result.data.message);
(document.getElementById('name') as HTMLInputElement).value = ''; (document.querySelector('#name') as HTMLInputElement).value = '';
(document.getElementById('email') as HTMLInputElement).value = ''; (document.querySelector('#email') as HTMLInputElement).value = '';
(document.getElementById('message') as HTMLInputElement).value = ''; (document.querySelector('#message') as HTMLInputElement).value = '';
} }
}) }
.catch((err) => { )
console.log(err); .catch((error) => {
}); console.log(error);
} else {
toast.info('Please fill all the fields ', {
position: 'top-center',
autoClose: 2000,
hideProgressBar: true,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
progress: undefined,
}); });
} } else {
}; toast.info('Please fill all the fields ', {
position: 'top-center',
autoClose: 2000,
hideProgressBar: true,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
progress: undefined,
});
}
};
const Contact = (): JSX.Element => {
return ( return (
<div className="max-w-xl px-5 py-10 m-auto"> <div className="max-w-xl px-5 py-10 m-auto">
<div> <div>
@@ -87,8 +95,8 @@ const Contact = (): JSX.Element => {
<button <button
type="button" type="button"
className="btn btn-primary btn-sm" className="btn btn-primary btn-sm"
onClick={(e) => { onClick={(event) => {
e.preventDefault(); event.preventDefault();
sendEmail(); sendEmail();
}}> }}>
Submit{' '} Submit{' '}

View File

@@ -16,7 +16,7 @@ import { supabase } from '../utils/supabaseClient';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
type DashboardProps = { type DashboardProperties = {
profile: { username: string; website: string; avatar_url: string }; profile: { username: string; website: string; avatar_url: string };
session: Session; session: Session;
planName: string; planName: string;
@@ -26,7 +26,7 @@ const Dashboard = ({
profile, profile,
session, session,
planName, planName,
}: DashboardProps): JSX.Element => { }: DashboardProperties): JSX.Element => {
const router = useRouter(); const router = useRouter();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [username, setUsername] = useState(profile?.username || ''); const [username, setUsername] = useState(profile?.username || '');
@@ -48,7 +48,7 @@ const Dashboard = ({
username: string; username: string;
website: string; website: string;
avatar_url: string; avatar_url: string;
}) { }): Promise<void> {
try { try {
setLoading(true); setLoading(true);
const user = supabase.auth.user(); const user = supabase.auth.user();
@@ -89,7 +89,7 @@ const Dashboard = ({
size={150} size={150}
onUpload={(url) => { onUpload={(url) => {
setAvatarUrl(url); setAvatarUrl(url);
updateProfile({ username, website, avatar_url: url }); void updateProfile({ username, website, avatar_url: url });
}} }}
/> />
<div className="flex flex-col mb-5"> <div className="flex flex-col mb-5">
@@ -113,7 +113,7 @@ const Dashboard = ({
id="username" id="username"
type="text" type="text"
value={username || ''} value={username || ''}
onChange={(e) => setUsername(e.target.value)} onChange={(event) => setUsername(event.target.value)}
/> />
</div> </div>
<div className="flex flex-col mb-5"> <div className="flex flex-col mb-5">
@@ -125,7 +125,7 @@ const Dashboard = ({
id="website" id="website"
type="website" type="website"
value={website || ''} value={website || ''}
onChange={(e) => setWebsite(e.target.value)} onChange={(event) => setWebsite(event.target.value)}
/> />
</div> </div>
@@ -140,7 +140,7 @@ const Dashboard = ({
</div> </div>
<div className="flex flex-row flex-wrap w-full max-w-xl p-5 m-auto my-5 border-2 shadow-lg bordered border-primary"> <div className="flex flex-row flex-wrap w-full max-w-xl p-5 m-auto my-5 border-2 shadow-lg bordered border-primary">
<Image src={Plan} alt="credit card" /> <Image src={Plan as string} alt="credit card" />
<div className="flex flex-col m-auto"> <div className="flex flex-col m-auto">
<h2>Your current plan</h2> <h2>Your current plan</h2>
<p className="">{planName}</p> <p className="">{planName}</p>

View File

@@ -20,7 +20,7 @@ const Landing = (): JSX.Element => (
</div> </div>
<div className="max-w-xl"> <div className="max-w-xl">
<Image <Image
src={landTop} src={landTop as string}
height={417} height={417}
width={583} width={583}
alt="Construction of a website" alt="Construction of a website"
@@ -40,7 +40,7 @@ const Landing = (): JSX.Element => (
</p> </p>
</div> </div>
<div className="max-w-xl"> <div className="max-w-xl">
<Image src={start} alt="screenshot of the website" /> <Image src={start as string} alt="screenshot of the website" />
</div> </div>
</div> </div>
<div className="flex flex-wrap justify-around max-w-6xl m-auto mt-24"> <div className="flex flex-wrap justify-around max-w-6xl m-auto mt-24">
@@ -60,7 +60,10 @@ const Landing = (): JSX.Element => (
</p> </p>
</div> </div>
<div className="flex order-2 max-w-md lg:order-1"> <div className="flex order-2 max-w-md lg:order-1">
<Image src={supabaseImage} alt="screenshot of the Supabase website" /> <Image
src={supabaseImage as string}
alt="screenshot of the Supabase website"
/>
</div> </div>
</div> </div>
<MailingList /> <MailingList />

View File

@@ -18,11 +18,11 @@ import Nav from './Nav';
import { ToastContainer } from 'react-toastify'; import { ToastContainer } from 'react-toastify';
import { useAuth } from 'utils/AuthContext'; import { useAuth } from 'utils/AuthContext';
type LayoutProps = { type LayoutProperties = {
children: JSX.Element; children: JSX.Element;
}; };
const Layout = ({ children }: LayoutProps): JSX.Element => { const Layout = ({ children }: LayoutProperties): JSX.Element => {
const { user, signOut } = useAuth(); const { user, signOut } = useAuth();
return ( return (

View File

@@ -14,10 +14,10 @@ const MailingList = (): JSX.Element => {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [valid, setValid] = useState(true); const [valid, setValid] = useState(true);
const validateEmail = () => { const validateEmail = (): void => {
// Regex patern for email validation // Regex patern for email validation
const regex = const regex =
/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; /^(([^\s"(),.:;<>@[\\\]]+(\.[^\s"(),.:;<>@[\\\]]+)*)|(".+"))@((\[(?:\d{1,3}\.){3}\d{1,3}])|(([\dA-Za-z\-]+\.)+[A-Za-z]{2,}))$/;
if (regex.test(mail)) { if (regex.test(mail)) {
// this is a valid email address // this is a valid email address
@@ -30,7 +30,7 @@ const MailingList = (): JSX.Element => {
} }
}; };
const subscribe = () => { const subscribe = (): void => {
setLoading(true); setLoading(true);
axios axios
.put('api/mailingList', { .put('api/mailingList', {
@@ -38,12 +38,14 @@ const MailingList = (): JSX.Element => {
}) })
.then((result) => { .then((result) => {
if (result.status === 200) { if (result.status === 200) {
toast.success(result.data.message); toast.success(
'Your email has been succesfully added to the mailing list. Welcome 👋'
);
setLoading(false); setLoading(false);
} }
}) })
.catch((err) => { .catch((error) => {
console.log(err); console.log(error);
setLoading(false); setLoading(false);
}); });
}; };
@@ -52,7 +54,7 @@ const MailingList = (): JSX.Element => {
<h2 className="text-3xl font-bold text-center uppercase md:text-4xl font-title"> <h2 className="text-3xl font-bold text-center uppercase md:text-4xl font-title">
Stay Tuned Stay Tuned
</h2> </h2>
<Image src={Mailing} alt="Mail" /> <Image src={Mailing as string} alt="Mail" />
<label className="label"> <label className="label">
<p className="max-w-md m-auto text-center"> <p className="max-w-md m-auto text-center">
Want to be the first to know when SupaNexTail launches and get an Want to be the first to know when SupaNexTail launches and get an
@@ -61,13 +63,13 @@ const MailingList = (): JSX.Element => {
</label> </label>
<div className="m-auto mt-5"> <div className="m-auto mt-5">
<input <input
onChange={(e) => { onChange={(event) => {
setMail(e.target.value); setMail(event.target.value);
}} }}
type="email" type="email"
placeholder="Your email" placeholder="Your email"
className={`input input-primary input-bordered ${ className={`input input-primary input-bordered ${
valid ? null : 'input-error' valid ? '' : 'input-error'
}`} }`}
/> />
<button <button

View File

@@ -7,13 +7,14 @@ import { LogOut, Menu } from 'react-feather';
import Image from 'next/image'; import Image from 'next/image';
import Link from 'next/link'; import Link from 'next/link';
import Logo from 'public/logo.svg'; import Logo from 'public/logo.svg';
import { User } from '@supabase/gotrue-js';
type NavProps = { type NavProperties = {
user: Record<string, unknown>; user: User | null | undefined;
signOut: () => void; signOut: () => void;
}; };
const Nav = ({ user, signOut }: NavProps): JSX.Element => { const Nav = ({ user, signOut }: NavProperties): JSX.Element => {
// Modify you menu directly here // Modify you menu directly here
const NavMenu = ( const NavMenu = (
<> <>
@@ -66,7 +67,7 @@ const Nav = ({ user, signOut }: NavProps): JSX.Element => {
<nav className="w-full mb-2 navbar"> <nav className="w-full mb-2 navbar">
<Link href="/"> <Link href="/">
<a> <a>
<Image src={Logo} alt="SupaNexTail Logo" /> <Image src={Logo as string} alt="SupaNexTail Logo" />
</a> </a>
</Link> </Link>

View File

@@ -2,13 +2,16 @@ import { Dialog, Transition } from '@headlessui/react';
import { Fragment } from 'react'; import { Fragment } from 'react';
type PaymentModalProps = { type PaymentModalProperties = {
open: boolean; open: boolean;
setPayment: (arg0: boolean) => void; setPayment: (argument0: boolean) => void;
}; };
const PaymentModal = ({ open, setPayment }: PaymentModalProps): JSX.Element => { const PaymentModal = ({
function closeModal() { open,
setPayment,
}: PaymentModalProperties): JSX.Element => {
function closeModal(): void {
setPayment(false); setPayment(false);
} }

View File

@@ -10,19 +10,20 @@ import { getSub, supabase } from 'utils/supabaseClient';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import axios from 'axios'; import axios from 'axios';
import { definitions } from 'types/database/index';
import router from 'next/router'; import router from 'next/router';
import { useAuth } from 'utils/AuthContext'; import { useAuth } from 'utils/AuthContext';
const Pricing = (): JSX.Element => { const Pricing = (): JSX.Element => {
const { user, session } = useAuth(); const { user, session } = useAuth();
const [customerId, setCustomerId] = useState<null | string>(null); const [customerId, setCustomerId] = useState<undefined | string>();
const [sub, setSub] = useState(false); const [sub, setSub] = useState<definitions['subscriptions'] | undefined>();
useEffect(() => { useEffect(() => {
if (user) { if (user) {
getSub().then((result) => setSub(result)); void getSub().then((result) => setSub(result));
supabase void supabase
.from('subscriptions') .from<definitions['subscriptions']>('subscriptions')
.select(`customer_id`) .select(`customer_id`)
.eq('id', user.id) .eq('id', user.id)
.single() .single()
@@ -32,31 +33,37 @@ const Pricing = (): JSX.Element => {
} }
}, [user]); }, [user]);
interface Test {
url: string;
}
const handleSubmit = async ( const handleSubmit = async (
e: React.SyntheticEvent<HTMLButtonElement>, event: React.SyntheticEvent<HTMLButtonElement>,
priceId: string priceId: string
) => { ): Promise<void> => {
e.preventDefault(); event.preventDefault();
// Create a Checkout Session. This will redirect the user to the Stripe website for the payment. // Create a Checkout Session. This will redirect the user to the Stripe website for the payment.
if (sub) { if (user && session) {
axios if (sub) {
.post('/api/stripe/customer-portal', { const stripeInfo = await axios.post<Test>(
customerId, '/api/stripe/customerPortal',
}) {
.then((result) => { customerId,
router.push(result.data.url); }
}); );
} else void router.push(stripeInfo.data.url);
axios } else {
.post('/api/stripe/create-checkout-session', { const stripeInfo = await axios.post<Test>('/api/stripe/checkout', {
priceId, priceId,
email: user.email, email: user.email,
customerId, customerId,
userId: user.id, userId: user.id,
tokenId: session.access_token, tokenId: session.access_token,
pay_mode: 'subscription', pay_mode: 'subscription',
}) });
.then((result) => router.push(result.data.url)); void router.push(stripeInfo.data.url);
}
}
}; };
return ( return (
<div> <div>
@@ -102,8 +109,8 @@ const Pricing = (): JSX.Element => {
{user ? ( {user ? (
<button <button
className="btn btn-primary" className="btn btn-primary"
onClick={(e) => { onClick={(event) => {
handleSubmit(e, 'price_1JtHhaDMjD0UnVmM5uCyyrWn'); void handleSubmit(event, 'price_1JtHhaDMjD0UnVmM5uCyyrWn');
}}> }}>
{sub ? 'Handle subscription' : 'Subscribe'} {sub ? 'Handle subscription' : 'Subscribe'}
</button> </button>
@@ -136,8 +143,8 @@ const Pricing = (): JSX.Element => {
{user ? ( {user ? (
<button <button
className="btn btn-primary" className="btn btn-primary"
onClick={(e) => { onClick={(event) => {
handleSubmit(e, 'price_1JtHhaDMjD0UnVmM5uCyyrWn'); void handleSubmit(event, 'price_1JtHhaDMjD0UnVmM5uCyyrWn');
}}> }}>
{sub ? 'Handle subscription' : 'Subscribe'} {sub ? 'Handle subscription' : 'Subscribe'}
</button> </button>

View File

@@ -12,12 +12,12 @@ import { SupabaseClient } from '@supabase/supabase-js';
import { supabase } from 'utils/supabaseClient'; import { supabase } from 'utils/supabaseClient';
import { useAuth } from 'utils/AuthContext'; import { useAuth } from 'utils/AuthContext';
type ContainerProps = { type ContainerProperties = {
children: JSX.Element; children: JSX.Element;
supabaseClient: SupabaseClient; supabaseClient: SupabaseClient;
}; };
const Container = ({ children }: ContainerProps): JSX.Element => { const Container = ({ children }: ContainerProperties): JSX.Element => {
const { user, signOut } = useAuth(); const { user, signOut } = useAuth();
if (user) if (user)
return ( return (

View File

@@ -4,13 +4,17 @@ This card is used on the landing page
import Image from 'next/image'; import Image from 'next/image';
type CardLandingProps = { type CardLandingProperties = {
image: string; image: string;
title: string; title: string;
text: string; text: string;
}; };
const CardLanding = ({ image, title, text }: CardLandingProps): JSX.Element => { const CardLanding = ({
image,
title,
text,
}: CardLandingProperties): JSX.Element => {
return ( return (
<div className="flex h-48 p-5 mb-5 w-80 sm:ml-5 bg-base-100"> <div className="flex h-48 p-5 mb-5 w-80 sm:ml-5 bg-base-100">
<div> <div>

View File

@@ -4,11 +4,11 @@ This card is used on the landing page
import { FiStar } from 'react-icons/fi'; import { FiStar } from 'react-icons/fi';
type KeyFeatureProps = { type KeyFeatureProperties = {
children: JSX.Element; children: JSX.Element;
}; };
const KeyFeature = ({ children }: KeyFeatureProps): JSX.Element => ( const KeyFeature = ({ children }: KeyFeatureProperties): JSX.Element => (
<div className="flex p-5 mb-5 italic shadow-sm bg-base-100"> <div className="flex p-5 mb-5 italic shadow-sm bg-base-100">
<div className="flex w-12 h-12 p-2 my-auto text-white rounded-sm bg-accent-focus"> <div className="flex w-12 h-12 p-2 my-auto text-white rounded-sm bg-accent-focus">
<FiStar className="m-auto text-2xl" /> <FiStar className="m-auto text-2xl" />

View File

@@ -1,49 +1,53 @@
import { ApiError, Session, UserCredentials } from '@supabase/gotrue-js';
import { IoLogoGoogle } from 'react-icons/io'; import { IoLogoGoogle } from 'react-icons/io';
import router from 'next/router'; import router from 'next/router';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { useState } from 'react'; import { useState } from 'react';
type LoginProps = { type LoginProperties = {
resetPassword: (email: string) => Promise<{ error: { message: string } }>; resetPassword: (data: string) => Promise<{
signIn: ({}) => Promise<{ data: {} | null;
data: Record<string, unknown>; error: ApiError | null;
error: { message: string }; }>;
signIn: (data: UserCredentials) => Promise<{
user: Session['user'] | null;
session: Session | null;
error: ApiError | null;
}>; }>;
}; };
const Login = ({ resetPassword, signIn }: LoginProps): JSX.Element => { const Login = ({ resetPassword, signIn }: LoginProperties): JSX.Element => {
const [email, setEmail] = useState(''); const [email, setEmail] = useState('');
const [password, setPassword] = useState(''); const [password, setPassword] = useState('');
const [forgot, setForgot] = useState(false); const [forgot, setForgot] = useState(false);
const resetPasswordLogin = () => { const resetPasswordLogin = async (): Promise<void> => {
resetPassword(email).then((result: { error: { message: string } }) => { await resetPassword(email).then((result: { error: ApiError | null }) => {
if (result.error) { if (result.error) {
toast.error(result.error.message); toast.error(result.error.message);
} else toast.success('Check your email to reset your password!'); } else toast.success('Check your email to reset your password!');
}); });
}; };
const login = (e: React.SyntheticEvent<HTMLButtonElement>) => { const login = async (
e.preventDefault(); event: React.SyntheticEvent<HTMLButtonElement>
): Promise<void> => {
event.preventDefault();
// Handle the login. Go to the homepage if success or display an error. // Handle the login. Go to the homepage if success or display an error.
signIn({ await signIn({
email, email,
password, password,
}).then( }).then((result) => {
(result: { if (result) {
data: Record<string, unknown>; void router.push('/');
error: { message: string };
}) => {
if (result.data) {
router.push('/');
}
if (result.error) {
toast.error(result.error.message);
}
} }
); if (result.error) {
toast.error(result.error.message);
}
});
}; };
return ( return (
@@ -97,7 +101,7 @@ const Login = ({ resetPassword, signIn }: LoginProps): JSX.Element => {
<button <button
className="w-full btn btn-primary" className="w-full btn btn-primary"
onClick={(event) => { onClick={(event) => {
login(event); void login(event);
}}> }}>
Log in Log in
</button> </button>
@@ -113,7 +117,7 @@ const Login = ({ resetPassword, signIn }: LoginProps): JSX.Element => {
className="flex items-center justify-center px-4 py-2 space-x-2 transition-colors duration-300 border rounded-md focus:outline-none border-base-200 group hover:bg-base-300" className="flex items-center justify-center px-4 py-2 space-x-2 transition-colors duration-300 border rounded-md focus:outline-none border-base-200 group hover:bg-base-300"
onClick={(event) => { onClick={(event) => {
event.preventDefault(); event.preventDefault();
signIn({ provider: 'google' }); void signIn({ provider: 'google' });
}}> }}>
<div className="text-base-content"> <div className="text-base-content">
<IoLogoGoogle /> <IoLogoGoogle />
@@ -154,7 +158,7 @@ const Login = ({ resetPassword, signIn }: LoginProps): JSX.Element => {
className="w-full btn btn-primary btn-sm" className="w-full btn btn-primary btn-sm"
onClick={(event) => { onClick={(event) => {
event.preventDefault(); event.preventDefault();
resetPasswordLogin(); void resetPasswordLogin();
}}> }}>
Recover my password Recover my password
</button> </button>

View File

@@ -1,41 +1,50 @@
import { ApiError, Session, UserCredentials } from '@supabase/gotrue-js';
import { IoLogoGoogle } from 'react-icons/io'; import { IoLogoGoogle } from 'react-icons/io';
import router from 'next/router'; import router from 'next/router';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { useState } from 'react'; import { useState } from 'react';
type SignUpPanelProps = { type SignUpPanelProperties = {
signIn: ({}) => Promise<{ signUp: (data: UserCredentials) => Promise<{
data: Record<string, unknown>; user: Session['user'] | null;
error: { message: string }; session: Session | null;
error: ApiError | null;
}>; }>;
signUp: ({}) => Promise<{
data: Record<string, unknown>; signIn: (data: UserCredentials) => Promise<{
error: { message: string }; user: Session['user'] | null;
session: Session | null;
error: ApiError | null;
}>; }>;
}; };
const SignUpPanel = ({ signIn, signUp }: SignUpPanelProps): JSX.Element => { const SignUpPanel = ({
signIn,
signUp,
}: SignUpPanelProperties): JSX.Element => {
const [email, setEmail] = useState(''); const [email, setEmail] = useState('');
const [password, setPassword] = useState(''); const [password, setPassword] = useState('');
const signup = (e: React.SyntheticEvent<HTMLButtonElement>) => { const signup = async (
e.preventDefault(); event: React.SyntheticEvent<HTMLButtonElement>
): Promise<void> => {
event.preventDefault();
// Handle the login. Go to the homepage if success or display an error. // Handle the login. Go to the homepage if success or display an error.
signUp({ await signUp({
email, email,
password, password,
}).then((result) => { }).then((result) => {
console.log(result); console.log(result);
if (result.error) { if (result.error) {
toast.error(result.error.message); toast.error(result.error.message);
} else if (result.data?.confirmation_sent_at) { } else if (result.user?.confirmation_sent_at) {
console.log(result.data.confirmation_sent_at);
toast.success( toast.success(
'A confirmation email has been sent to you, watch your mailbox!' 'A confirmation email has been sent to you, watch your mailbox!'
); );
} else if (result.data) { } else if (result.user) {
router.push('/'); void router.push('/');
} }
}); });
}; };
@@ -78,7 +87,7 @@ const SignUpPanel = ({ signIn, signUp }: SignUpPanelProps): JSX.Element => {
id="loginBtn" id="loginBtn"
className="w-full btn btn-primary" className="w-full btn btn-primary"
onClick={(event) => { onClick={(event) => {
signup(event); void signup(event);
}}> }}>
Sign Up Sign Up
</button> </button>
@@ -94,7 +103,7 @@ const SignUpPanel = ({ signIn, signUp }: SignUpPanelProps): JSX.Element => {
className="flex items-center justify-center px-4 py-2 space-x-2 transition-colors duration-300 border rounded-md focus:outline-none border-base-200 group hover:bg-base-300" className="flex items-center justify-center px-4 py-2 space-x-2 transition-colors duration-300 border rounded-md focus:outline-none border-base-200 group hover:bg-base-300"
onClick={(event) => { onClick={(event) => {
event.preventDefault(); event.preventDefault();
signIn({ provider: 'google' }); void signIn({ provider: 'google' });
}}> }}>
<div className="text-base-content"> <div className="text-base-content">
<IoLogoGoogle /> <IoLogoGoogle />

View File

@@ -7,7 +7,8 @@
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
"eslint": "eslint . --fix", "eslint": "eslint . --fix",
"test": "playwright test" "test": "playwright test",
"update-types": "npx openapi-typescript https://vwkxoczhqygxvqzbhkmj.supabase.co/rest/v1/?apikey=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlhdCI6MTYzMDQwNzE1NCwiZXhwIjoxOTQ1OTgzMTU0fQ.VsWbay8ZrhDe3-elrlh7EGtJfW1INXlUywjeh6h2xgc --output types/database/index.ts"
}, },
"dependencies": { "dependencies": {
"@headlessui/react": "^1.4.2", "@headlessui/react": "^1.4.2",
@@ -16,7 +17,6 @@
"@supabase/gotrue-js": "^1.21.7", "@supabase/gotrue-js": "^1.21.7",
"@supabase/supabase-js": "^1.29.1", "@supabase/supabase-js": "^1.29.1",
"@types/node": "^17.0.4", "@types/node": "^17.0.4",
"@types/react": "^17.0.38",
"@types/react-dom": "^17.0.11", "@types/react-dom": "^17.0.11",
"axios": "^0.24.0", "axios": "^0.24.0",
"cors": "^2.8.5", "cors": "^2.8.5",
@@ -30,8 +30,7 @@
"react-feather": "^2.0.9", "react-feather": "^2.0.9",
"react-icons": "^4.3.1", "react-icons": "^4.3.1",
"react-toastify": "^8.1.0", "react-toastify": "^8.1.0",
"stripe": "^8.195.0", "stripe": "^8.195.0"
"typescript": "^4.5.4"
}, },
"devDependencies": { "devDependencies": {
"@next/eslint-plugin-next": "^12.0.7", "@next/eslint-plugin-next": "^12.0.7",
@@ -39,6 +38,7 @@
"@types/cors": "^2.8.12", "@types/cors": "^2.8.12",
"@types/express-rate-limit": "^5.1.3", "@types/express-rate-limit": "^5.1.3",
"@types/micro": "^7.3.6", "@types/micro": "^7.3.6",
"@types/react": "^17.0.38",
"@typescript-eslint/eslint-plugin": "^5.8.0", "@typescript-eslint/eslint-plugin": "^5.8.0",
"autoprefixer": "^10.4.0", "autoprefixer": "^10.4.0",
"eslint": "^8.5.0", "eslint": "^8.5.0",
@@ -53,8 +53,11 @@
"eslint-plugin-react": "^7.28.0", "eslint-plugin-react": "^7.28.0",
"eslint-plugin-react-hooks": "^4.3.0", "eslint-plugin-react-hooks": "^4.3.0",
"eslint-plugin-simple-import-sort": "^7.0.0", "eslint-plugin-simple-import-sort": "^7.0.0",
"eslint-plugin-sonarjs": "^0.11.0",
"eslint-plugin-unicorn": "^40.0.0",
"postcss": "^8.4.5", "postcss": "^8.4.5",
"prettier": "^2.5.1", "prettier": "^2.5.1",
"tailwindcss": "^3.0.7" "tailwindcss": "^3.0.7",
"typescript": "^4.5.4"
} }
} }

View File

@@ -9,11 +9,9 @@ import Document, {
class MyDocument extends Document { class MyDocument extends Document {
static async getInitialProps( static async getInitialProps(
ctx: DocumentContext context_: DocumentContext
): Promise<DocumentInitialProps> { ): Promise<DocumentInitialProps> {
const initialProps = await Document.getInitialProps(ctx); return await Document.getInitialProps(context_);
return initialProps;
} }
render(): JSX.Element { render(): JSX.Element {

View File

@@ -7,9 +7,9 @@ import type { NextApiRequest, NextApiResponse } from 'next';
import { supabase } from 'utils/supabaseClient'; import { supabase } from 'utils/supabaseClient';
export default async function handler( export default function handler(
req: NextApiRequest, request: NextApiRequest,
res: NextApiResponse response: NextApiResponse
): Promise<void> { ): void {
supabase.auth.api.setAuthCookie(req, res); supabase.auth.api.setAuthCookie(request, response);
} }

View File

@@ -4,20 +4,20 @@ import { supabase } from 'utils/supabaseClient';
// Example of how to verify and get user data server-side. // Example of how to verify and get user data server-side.
const getUser = async ( const getUser = async (
req: NextApiRequest, request: NextApiRequest,
res: NextApiResponse response: NextApiResponse
): Promise<void> => { ): Promise<void> => {
const token = req.headers.token; const token = request.headers.token;
if (typeof token !== 'string') { if (typeof token !== 'string') {
return res.status(401).json({ error: 'Missing auth token.' }); return response.status(401).json({ error: 'Missing auth token.' });
} }
if (token) { if (token) {
const { data: user, error } = await supabase.auth.api.getUser(token); const { data: user, error } = await supabase.auth.api.getUser(token);
if (error) return res.status(401).json({ error: error.message }); if (error) return response.status(401).json({ error: error.message });
return res.status(200).json(user); return response.status(200).json(user);
} }
}; };

View File

@@ -2,9 +2,15 @@ import type { NextApiRequest, NextApiResponse } from 'next';
import Cors from 'cors'; import Cors from 'cors';
import axios from 'axios'; import axios from 'axios';
import initMiddleware from 'utils/init-middleware'; import initMiddleware from 'utils/initMiddleware';
import rateLimit from 'express-rate-limit'; import rateLimit from 'express-rate-limit';
interface Request extends NextApiRequest {
body: {
mail: string;
};
}
export const config = { export const config = {
api: { api: {
externalResolver: true, externalResolver: true,
@@ -19,44 +25,44 @@ const cors = initMiddleware(
const limiter = initMiddleware( const limiter = initMiddleware(
rateLimit({ rateLimit({
windowMs: 30000, // 30sec windowMs: 30_000, // 30sec
max: 2, // Max 2 request per 30 sec max: 2, // Max 2 request per 30 sec
}) })
); );
export default async function handler( export default async function handler(
req: NextApiRequest, request: Request,
res: NextApiResponse response: NextApiResponse
): Promise<void> { ): Promise<void> {
await cors(req, res); await cors(request, response);
await limiter(req, res); await limiter(request, response);
if (req.method === 'PUT') { if (request.method === 'PUT') {
axios axios
.put( .put(
'https://api.sendgrid.com/v3/marketing/contacts', 'https://api.sendgrid.com/v3/marketing/contacts',
{ {
contacts: [{ email: `${req.body.mail}` }], contacts: [{ email: `${request.body.mail}` }],
list_ids: [process.env.SENDGRID_MAILING_ID], list_ids: [process.env.SENDGRID_MAILING_ID],
}, },
{ {
headers: { headers: {
'content-type': 'application/json', 'content-type': 'application/json',
Authorization: `Bearer ${process.env.SENDGRID_SECRET}`, Authorization: `Bearer ${process.env.SENDGRID_SECRET || ''}`,
}, },
} }
) )
.then((result) => { .then((result) => {
console.log(result); console.log(result);
res.status(200).send({ response.status(200).send({
message: message:
'Your email has been succesfully added to the mailing list. Welcome 👋', 'Your email has been succesfully added to the mailing list. Welcome 👋',
}); });
}) })
.catch((err) => { .catch((error) => {
res.status(500).send({ response.status(500).send({
message: message:
'Oups, there was a problem with your subscription, please try again or contact us', 'Oups, there was a problem with your subscription, please try again or contact us',
error: err, error: error as string,
}); });
}); });
} }

View File

@@ -7,31 +7,38 @@ import type { NextApiRequest, NextApiResponse } from 'next';
import sgMail from '@sendgrid/mail'; import sgMail from '@sendgrid/mail';
const sendGrid = async ( interface Request extends NextApiRequest {
req: NextApiRequest, body: {
res: NextApiResponse name: string;
): Promise<void> => { email: string;
if (req.method === 'POST') { message: string;
};
}
const sendGrid = (request: Request, response: NextApiResponse): void => {
if (request.method === 'POST') {
sgMail.setApiKey(process.env.SENDGRID_SECRET || ''); sgMail.setApiKey(process.env.SENDGRID_SECRET || '');
const msg = { const message = {
to: process.env.SENDGRID_MAILTO || '', // Change to your recipient to: process.env.SENDGRID_MAILTO || '', // Change to your recipient
from: process.env.SENDGRID_MAILFROM || '', // Change to your verified sender from: process.env.SENDGRID_MAILFROM || '', // Change to your verified sender
subject: `[${process.env.NEXT_PUBLIC_TITLE}] New message from ${req.body.name}`, subject: `[${process.env.NEXT_PUBLIC_TITLE || ''}] New message from ${
text: req.body.message, request.body.name
reply_to: req.body.email, }`,
text: request.body.message,
reply_to: request.body.email,
}; };
sgMail sgMail
.send(msg) .send(message)
.then(() => { .then(() => {
res response
.status(200) .status(200)
.send({ message: 'Your email has been sent', success: true }); .send({ message: 'Your email has been sent', success: true });
}) })
.catch((error) => { .catch((error: string) => {
console.error(error); console.error(error);
res.status(500).send({ response.status(500).send({
message: 'There was an issue with your email... please retry', message: 'There was an issue with your email... please retry',
error, error,
}); });

View File

@@ -2,7 +2,7 @@ import type { NextApiRequest, NextApiResponse } from 'next';
import Cors from 'cors'; import Cors from 'cors';
import Stripe from 'stripe'; import Stripe from 'stripe';
import initMiddleware from 'utils/init-middleware'; import initMiddleware from 'utils/initMiddleware';
import rateLimit from 'express-rate-limit'; import rateLimit from 'express-rate-limit';
const cors = initMiddleware( const cors = initMiddleware(
@@ -13,10 +13,23 @@ const cors = initMiddleware(
const limiter = initMiddleware( const limiter = initMiddleware(
rateLimit({ rateLimit({
windowMs: 30000, // 30sec windowMs: 30_000, // 30sec
max: 4, // Max 4 request per 30 sec max: 4, // Max 4 request per 30 sec
}) })
); );
interface Request extends NextApiRequest {
body: {
customerId: string;
email: string;
pay_mode: Stripe.Checkout.SessionCreateParams.Mode;
userId: string;
priceId: string;
};
headers: {
origin: string;
};
}
// Set your secret key. Remember to switch to your live secret key in production. // Set your secret key. Remember to switch to your live secret key in production.
// See your keys here: https://dashboard.stripe.com/apikeys // See your keys here: https://dashboard.stripe.com/apikeys
const stripe = new Stripe(process.env.STRIPE_SECRET || '', { const stripe = new Stripe(process.env.STRIPE_SECRET || '', {
@@ -24,26 +37,26 @@ const stripe = new Stripe(process.env.STRIPE_SECRET || '', {
}); });
export default async function handler( export default async function handler(
req: NextApiRequest, request: Request,
res: NextApiResponse response: NextApiResponse
): Promise<void> { ): Promise<void> {
await cors(req, res); await cors(request, response);
await limiter(req, res); await limiter(request, response);
if (req.method === 'POST') { if (request.method === 'POST') {
const { priceId } = req.body; const { priceId, customerId, pay_mode, userId, email } = request.body;
// See https://stripe.com/docs/api/checkout/sessions/create // See https://stripe.com/docs/api/checkout/sessions/create
// for additional parameters to pass. // for additional parameters to pass.
try { try {
const session = req.body.customerId const session = customerId
? await stripe.checkout.sessions.create({ ? await stripe.checkout.sessions.create({
mode: req.body.pay_mode, mode: pay_mode,
payment_method_types: ['card'], payment_method_types: ['card'],
client_reference_id: req.body.userId, client_reference_id: userId,
metadata: { metadata: {
priceId: req.body.priceId, priceId: priceId,
}, },
customer: req.body.customerId, customer: customerId,
line_items: [ line_items: [
{ {
price: priceId, price: priceId,
@@ -54,16 +67,16 @@ export default async function handler(
// {CHECKOUT_SESSION_ID} is a string literal; do not change it! // {CHECKOUT_SESSION_ID} is a string literal; do not change it!
// the actual Session ID is returned in the query parameter when your customer // the actual Session ID is returned in the query parameter when your customer
// is redirected to the success page. // is redirected to the success page.
success_url: `${req.headers.origin}/dashboard?session_id={CHECKOUT_SESSION_ID}`, success_url: `${request.headers.origin}/dashboard?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${req.headers.origin}/pricing`, cancel_url: `${request.headers.origin}/pricing`,
}) })
: await stripe.checkout.sessions.create({ : await stripe.checkout.sessions.create({
mode: 'subscription', mode: 'subscription',
payment_method_types: ['card'], payment_method_types: ['card'],
customer_email: req.body.email, customer_email: email,
client_reference_id: req.body.userId, client_reference_id: userId,
metadata: { metadata: {
priceId: req.body.priceId, priceId: priceId,
}, },
line_items: [ line_items: [
{ {
@@ -75,16 +88,16 @@ export default async function handler(
// {CHECKOUT_SESSION_ID} is a string literal; do not change it! // {CHECKOUT_SESSION_ID} is a string literal; do not change it!
// the actual Session ID is returned in the query parameter when your customer // the actual Session ID is returned in the query parameter when your customer
// is redirected to the success page. // is redirected to the success page.
success_url: `${req.headers.origin}/dashboard?session_id={CHECKOUT_SESSION_ID}`, success_url: `${request.headers.origin}/dashboard?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${req.headers.origin}/pricing`, cancel_url: `${request.headers.origin}/pricing`,
}); });
res.status(200).send({ url: session.url }); response.status(200).send({ url: session.url });
} catch (e: unknown) { } catch (error: unknown) {
res.status(400); response.status(400);
if (e instanceof Error) { if (error instanceof Error) {
return res.send({ return response.send({
error: { error: {
message: e.message, message: error.message,
}, },
}); });
} }

View File

@@ -5,9 +5,15 @@ import type { NextApiRequest, NextApiResponse } from 'next';
import Cors from 'cors'; import Cors from 'cors';
import Stripe from 'stripe'; import Stripe from 'stripe';
import initMiddleware from 'utils/init-middleware'; import initMiddleware from 'utils/initMiddleware';
import rateLimit from 'express-rate-limit'; import rateLimit from 'express-rate-limit';
interface Request extends NextApiRequest {
body: {
customerId: string;
};
}
const cors = initMiddleware( const cors = initMiddleware(
Cors({ Cors({
methods: ['POST', 'PUT'], methods: ['POST', 'PUT'],
@@ -16,7 +22,7 @@ const cors = initMiddleware(
const limiter = initMiddleware( const limiter = initMiddleware(
rateLimit({ rateLimit({
windowMs: 30000, // 30sec windowMs: 30_000, // 30sec
max: 150, // Max 4 request per 30 sec max: 150, // Max 4 request per 30 sec
}) })
); );
@@ -27,18 +33,20 @@ const stripe = new Stripe(process.env.STRIPE_SECRET || '', {
}); });
export default async function handler( export default async function handler(
req: NextApiRequest, request: Request,
res: NextApiResponse response: NextApiResponse
): Promise<void> { ): Promise<void> {
await cors(req, res); await cors(request, response);
await limiter(req, res); await limiter(request, response);
if (req.method === 'POST') { if (request.method === 'POST') {
const returnUrl = `${req.headers.origin}/dashboard`; // Stripe will return to the dashboard, you can change it const returnUrl = `${
request.headers.origin ? request.headers.origin : '/'
}/dashboard`; // Stripe will return to the dashboard, you can change it
const portalsession = await stripe.billingPortal.sessions.create({ const portalsession = await stripe.billingPortal.sessions.create({
customer: req.body.customerId, customer: request.body.customerId,
return_url: returnUrl, return_url: returnUrl,
}); });
res.status(200).send({ url: portalsession.url }); response.status(200).send({ url: portalsession.url });
} }
} }

View File

@@ -3,7 +3,7 @@ SupaNexTail use only 2 webhooks. Stripe have a lot more,
you can check it here https://stripe.com/docs/webhooks you can check it here https://stripe.com/docs/webhooks
BE SURE TO SETUP YOUR WEBHOOKS IN YOUR DASHBOARD! BE SURE TO SETUP YOUR WEBHOOKS IN YOUR DASHBOARD!
If you want to test it locally, you'll need the stripe CLI and use this command line: If you want to test it locally, you'll need the stripe CLI and use this command line:
stripe listen --forward-to localhost:3000/api/stripe/stripe-webhook stripe listen --forward-to localhost:3000/api/stripe/webhook
*/ */
import type { NextApiRequest, NextApiResponse } from 'next'; import type { NextApiRequest, NextApiResponse } from 'next';
@@ -12,7 +12,7 @@ import Cors from 'cors';
import Stripe from 'stripe'; import Stripe from 'stripe';
import { buffer } from 'micro'; import { buffer } from 'micro';
import { createClient } from '@supabase/supabase-js'; import { createClient } from '@supabase/supabase-js';
import initMiddleware from 'utils/init-middleware'; import initMiddleware from 'utils/initMiddleware';
import rateLimit from 'express-rate-limit'; import rateLimit from 'express-rate-limit';
export const config = { export const config = {
@@ -39,7 +39,7 @@ const supabase = createClient(
const limiter = initMiddleware( const limiter = initMiddleware(
rateLimit({ rateLimit({
windowMs: 30000, // 30sec windowMs: 30_000, // 30sec
max: 150, // Max 150 request per 30 sec max: 150, // Max 150 request per 30 sec
}) })
); );
@@ -52,18 +52,18 @@ const stripe = new Stripe(process.env.STRIPE_SECRET || '', {
}); });
export default async function handler( export default async function handler(
req: NextApiRequest, request: NextApiRequest,
res: NextApiResponse response: NextApiResponse
): Promise<void> { ): Promise<void> {
await cors(req, res); await cors(request, response);
await limiter(req, res); await limiter(request, response);
if (req.method === 'POST') { if (request.method === 'POST') {
// Retrieve the event by verifying the signature using the raw body and secret. // Retrieve the event by verifying the signature using the raw body and secret.
let event: Stripe.Event; let event: Stripe.Event;
const buf = await buffer(req); const buf = await buffer(request);
const sig = req.headers['stripe-signature'] as string; const sig = request.headers['stripe-signature'] as string;
try { try {
event = stripe.webhooks.constructEvent( event = stripe.webhooks.constructEvent(
@@ -71,13 +71,13 @@ export default async function handler(
sig, sig,
process.env.STRIPE_WEBHOOK || '' process.env.STRIPE_WEBHOOK || ''
); );
} catch (err) { } catch (error) {
console.log(err); console.log(error);
console.log(`⚠️ Webhook signature verification failed.`); console.log(`⚠️ Webhook signature verification failed.`);
console.log( console.log(
`⚠️ Check the env file and enter the correct webhook secret.` `⚠️ Check the env file and enter the correct webhook secret.`
); );
return res.send(400); return response.send(400);
} }
// Extract the object from the event. // Extract the object from the event.
const dataObject = event.data.object as { const dataObject = event.data.object as {
@@ -118,7 +118,7 @@ export default async function handler(
}, },
]) ])
.then() .then()
.then(null, (err) => console.log('err: ', err)); // catch .then(undefined, (error) => console.log('err:', error)); // catch
} else if (subscriptions?.length && subscriptions?.length > 0) { } else if (subscriptions?.length && subscriptions?.length > 0) {
await supabase await supabase
.from('subscriptions') .from('subscriptions')
@@ -130,7 +130,7 @@ export default async function handler(
}) })
.eq('id', dataObject.client_reference_id) .eq('id', dataObject.client_reference_id)
.then() .then()
.then(null, (err) => console.log('err: ', err)); // catch .then(undefined, (error) => console.log('err:', error)); // catch
} }
break; break;
case 'customer.subscription.deleted': case 'customer.subscription.deleted':
@@ -139,7 +139,7 @@ export default async function handler(
.update({ paid_user: false }) .update({ paid_user: false })
.eq('customer_id', dataObject.customer) .eq('customer_id', dataObject.customer)
.then() .then()
.then(null, (err) => console.log('err: ', err)); // catch .then(undefined, (error) => console.log('err:', error)); // catch
break; break;
case 'invoice.payment_failed': case 'invoice.payment_failed':
// If the payment fails or the customer does not have a valid payment method, // If the payment fails or the customer does not have a valid payment method,
@@ -157,6 +157,6 @@ export default async function handler(
default: default:
// Unexpected event type // Unexpected event type
} }
res.send(200); response.send(200);
} }
} }

View File

@@ -5,8 +5,12 @@ import { NextSeo } from 'next-seo';
const ContactPage = (): JSX.Element => ( const ContactPage = (): JSX.Element => (
<> <>
<NextSeo <NextSeo
title={`${process.env.NEXT_PUBLIC_TITLE} | Contact`} title={`${
description={`This is the contact page for ${process.env.NEXT_PUBLIC_TITLE}`} process.env.NEXT_PUBLIC_TITLE ? process.env.NEXT_PUBLIC_TITLE : ''
} | Contact`}
description={`This is the contact page for ${
process.env.NEXT_PUBLIC_TITLE ? process.env.NEXT_PUBLIC_TITLE : ''
}`}
/> />
<Layout> <Layout>

View File

@@ -1,3 +1,4 @@
/* eslint-disable unicorn/no-nested-ternary */
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import Dashboard from '../components/Dashboard'; import Dashboard from '../components/Dashboard';
@@ -6,6 +7,7 @@ import Layout from 'components/Layout';
import type { NextPageContext } from 'next'; import type { NextPageContext } from 'next';
import Stripe from 'stripe'; import Stripe from 'stripe';
import { createClient } from '@supabase/supabase-js'; import { createClient } from '@supabase/supabase-js';
import { definitions } from 'types/database/index';
import { supabase } from '../utils/supabaseClient'; import { supabase } from '../utils/supabaseClient';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
@@ -29,7 +31,7 @@ const DashboardPage = ({
useEffect(() => { useEffect(() => {
// If a user is not logged in, return to the homepage // If a user is not logged in, return to the homepage
if (!user) { if (!user) {
router.push('/'); void router.push('/');
} }
}, [router, user]); }, [router, user]);
@@ -66,7 +68,10 @@ const DashboardPage = ({
</div> </div>
); );
}; };
export async function getServerSideProps(context: NextPageContext) { // eslint-disable-next-line unicorn/prevent-abbreviations
export async function getServerSideProps(
context: NextPageContext
): Promise<any> {
const supabaseAdmin = createClient( const supabaseAdmin = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL || '', process.env.NEXT_PUBLIC_SUPABASE_URL || '',
process.env.SUPABASE_ADMIN_KEY || '' process.env.SUPABASE_ADMIN_KEY || ''
@@ -81,7 +86,7 @@ export async function getServerSideProps(context: NextPageContext) {
// If the user exist, you will retrieve the user profile and if he/she's a paid user // If the user exist, you will retrieve the user profile and if he/she's a paid user
if (user) { if (user) {
const { data: plan } = await supabaseAdmin const { data: plan } = await supabaseAdmin
.from('subscriptions') .from<definitions['subscriptions']>('subscriptions')
.select('subscription, paid_user') .select('subscription, paid_user')
.eq('id', user.id) .eq('id', user.id)
.single(); .single();
@@ -92,7 +97,7 @@ export async function getServerSideProps(context: NextPageContext) {
: null; : null;
const { data: profile } = await supabaseAdmin const { data: profile } = await supabaseAdmin
.from('profiles') .from<definitions['profiles']>('profiles')
.select(`username, website, avatar_url`) .select(`username, website, avatar_url`)
.eq('id', user.id) .eq('id', user.id)
.single(); .single();

View File

@@ -10,7 +10,9 @@ import Layout from 'components/Layout';
const Home = (): JSX.Element => ( const Home = (): JSX.Element => (
<> <>
<Head> <Head>
<title>{`Welcome to ${process.env.NEXT_PUBLIC_TITLE} 👋`}</title> <title>{`Welcome to ${
process.env.NEXT_PUBLIC_TITLE ? process.env.NEXT_PUBLIC_TITLE : ''
} 👋`}</title>
<meta <meta
name="description" name="description"
content="SupaNexTail is a boilerplate for your SaaS, based on Next.js, Supabase, and TailwindCSS" content="SupaNexTail is a boilerplate for your SaaS, based on Next.js, Supabase, and TailwindCSS"
@@ -20,7 +22,9 @@ const Home = (): JSX.Element => (
<meta property="og:type" content="website" /> <meta property="og:type" content="website" />
<meta <meta
property="og:title" property="og:title"
content={`Welcome to ${process.env.NEXT_PUBLIC_TITLE} 👋`} content={`Welcome to ${
process.env.NEXT_PUBLIC_TITLE ? process.env.NEXT_PUBLIC_TITLE : ''
} 👋`}
/> />
<meta <meta
property="og:description" property="og:description"
@@ -36,7 +40,9 @@ const Home = (): JSX.Element => (
/> />
<meta <meta
name="twitter:title" name="twitter:title"
content={`Welcome to ${process.env.NEXT_PUBLIC_TITLE} 👋`} content={`Welcome to ${
process.env.NEXT_PUBLIC_TITLE ? process.env.NEXT_PUBLIC_TITLE : ''
} 👋`}
/> />
<meta <meta
name="twitter:description" name="twitter:description"

View File

@@ -14,8 +14,10 @@ const LoginPage = (): JSX.Element => {
return ( return (
<> <>
<NextSeo <NextSeo
title={`${process.env.NEXT_PUBLIC_TITLE} | Auth`} title={`${process.env.NEXT_PUBLIC_TITLE || ''} | Auth`}
description={`This is the auth page for ${process.env.NEXT_PUBLIC_TITLE}`} description={`This is the auth page for ${
process.env.NEXT_PUBLIC_TITLE || ''
}`}
/> />
<Layout> <Layout>

View File

@@ -7,7 +7,9 @@ import Pricing from 'components/Pricing';
const PricingPage = (): JSX.Element => ( const PricingPage = (): JSX.Element => (
<> <>
<NextSeo <NextSeo
title={`${process.env.NEXT_PUBLIC_TITLE} | Pricing`} title={`${
process.env.NEXT_PUBLIC_TITLE ? process.env.NEXT_PUBLIC_TITLE : ''
} | Pricing`}
description="SupaNexTail is a boilerplate for your website, based on Next.js, Supabase, and TailwindCSS" description="SupaNexTail is a boilerplate for your website, based on Next.js, Supabase, and TailwindCSS"
/> />
<Layout> <Layout>

View File

@@ -7,7 +7,9 @@ import PrivacyPolicy from 'components/PrivacyPolicy';
const PrivacyPage = (): JSX.Element => ( const PrivacyPage = (): JSX.Element => (
<> <>
<NextSeo <NextSeo
title={`${process.env.NEXT_PUBLIC_TITLE} | Privacy Policy`} title={`${
process.env.NEXT_PUBLIC_TITLE ? process.env.NEXT_PUBLIC_TITLE : ''
} | Privacy Policy`}
description="SupaNexTail is a boilerplate for your website, based on Next.js, Supabase, and TailwindCSS" description="SupaNexTail is a boilerplate for your website, based on Next.js, Supabase, and TailwindCSS"
/> />
<Layout> <Layout>

View File

@@ -12,8 +12,12 @@ import { NextSeo } from 'next-seo';
const SignUpPage = (): JSX.Element => ( const SignUpPage = (): JSX.Element => (
<> <>
<NextSeo <NextSeo
title={`${process.env.NEXT_PUBLIC_TITLE} | Auth`} title={`${
description={`This is the auth page for ${process.env.NEXT_PUBLIC_TITLE}`} process.env.NEXT_PUBLIC_TITLE ? process.env.NEXT_PUBLIC_TITLE : ''
} | Auth`}
description={`This is the auth page for ${
process.env.NEXT_PUBLIC_TITLE ? process.env.NEXT_PUBLIC_TITLE : ''
}`}
/> />
<Layout> <Layout>

View File

@@ -7,7 +7,9 @@ import Terms from 'components/Terms';
const TermsPage = (): JSX.Element => ( const TermsPage = (): JSX.Element => (
<> <>
<NextSeo <NextSeo
title={`${process.env.NEXT_PUBLIC_TITLE} | Terms and conditions`} title={`${
process.env.NEXT_PUBLIC_TITLE ? process.env.NEXT_PUBLIC_TITLE : ''
} | Terms and conditions`}
description="SupaNexTail is a boilerplate for your website, based on Next.js, Supabase, and TailwindCSS" description="SupaNexTail is a boilerplate for your website, based on Next.js, Supabase, and TailwindCSS"
/> />
<Layout> <Layout>

192
pnpm-lock.yaml generated
View File

@@ -31,6 +31,8 @@ specifiers:
eslint-plugin-react: ^7.28.0 eslint-plugin-react: ^7.28.0
eslint-plugin-react-hooks: ^4.3.0 eslint-plugin-react-hooks: ^4.3.0
eslint-plugin-simple-import-sort: ^7.0.0 eslint-plugin-simple-import-sort: ^7.0.0
eslint-plugin-sonarjs: ^0.11.0
eslint-plugin-unicorn: ^40.0.0
express-rate-limit: ^6.0.1 express-rate-limit: ^6.0.1
micro: ^9.3.4 micro: ^9.3.4
next: '>=12.0.7' next: '>=12.0.7'
@@ -53,7 +55,6 @@ dependencies:
'@supabase/gotrue-js': 1.21.7 '@supabase/gotrue-js': 1.21.7
'@supabase/supabase-js': 1.29.1 '@supabase/supabase-js': 1.29.1
'@types/node': 17.0.4 '@types/node': 17.0.4
'@types/react': 17.0.38
'@types/react-dom': 17.0.11 '@types/react-dom': 17.0.11
axios: 0.24.0 axios: 0.24.0
cors: 2.8.5 cors: 2.8.5
@@ -68,7 +69,6 @@ dependencies:
react-icons: 4.3.1_react@17.0.2 react-icons: 4.3.1_react@17.0.2
react-toastify: 8.1.0_react-dom@17.0.2+react@17.0.2 react-toastify: 8.1.0_react-dom@17.0.2+react@17.0.2
stripe: 8.195.0 stripe: 8.195.0
typescript: 4.5.4
devDependencies: devDependencies:
'@next/eslint-plugin-next': 12.0.7 '@next/eslint-plugin-next': 12.0.7
@@ -76,6 +76,7 @@ devDependencies:
'@types/cors': 2.8.12 '@types/cors': 2.8.12
'@types/express-rate-limit': 5.1.3 '@types/express-rate-limit': 5.1.3
'@types/micro': 7.3.6 '@types/micro': 7.3.6
'@types/react': 17.0.38
'@typescript-eslint/eslint-plugin': 5.8.0_eslint@8.5.0+typescript@4.5.4 '@typescript-eslint/eslint-plugin': 5.8.0_eslint@8.5.0+typescript@4.5.4
autoprefixer: 10.4.0_postcss@8.4.5 autoprefixer: 10.4.0_postcss@8.4.5
eslint: 8.5.0 eslint: 8.5.0
@@ -90,9 +91,12 @@ devDependencies:
eslint-plugin-react: 7.28.0_eslint@8.5.0 eslint-plugin-react: 7.28.0_eslint@8.5.0
eslint-plugin-react-hooks: 4.3.0_eslint@8.5.0 eslint-plugin-react-hooks: 4.3.0_eslint@8.5.0
eslint-plugin-simple-import-sort: 7.0.0_eslint@8.5.0 eslint-plugin-simple-import-sort: 7.0.0_eslint@8.5.0
eslint-plugin-sonarjs: 0.11.0_eslint@8.5.0
eslint-plugin-unicorn: 40.0.0_eslint@8.5.0
postcss: 8.4.5 postcss: 8.4.5
prettier: 2.5.1 prettier: 2.5.1
tailwindcss: 3.0.7_16a290f6d0e3717bf6d2667234aebd30 tailwindcss: 3.0.7_16a290f6d0e3717bf6d2667234aebd30
typescript: 4.5.4
packages: packages:
@@ -1099,13 +1103,16 @@ packages:
/@types/node/17.0.4: /@types/node/17.0.4:
resolution: {integrity: sha512-6xwbrW4JJiJLgF+zNypN5wr2ykM9/jHcL7rQ8fZe2vuftggjzZeRSM4OwRc6Xk8qWjwJ99qVHo/JgOGmomWRog==} resolution: {integrity: sha512-6xwbrW4JJiJLgF+zNypN5wr2ykM9/jHcL7rQ8fZe2vuftggjzZeRSM4OwRc6Xk8qWjwJ99qVHo/JgOGmomWRog==}
/@types/normalize-package-data/2.4.1:
resolution: {integrity: sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==}
dev: true
/@types/parse-json/4.0.0: /@types/parse-json/4.0.0:
resolution: {integrity: sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==} resolution: {integrity: sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==}
dev: true dev: true
/@types/prop-types/15.7.4: /@types/prop-types/15.7.4:
resolution: {integrity: sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ==} resolution: {integrity: sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ==}
dev: false
/@types/qs/6.9.7: /@types/qs/6.9.7:
resolution: {integrity: sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==} resolution: {integrity: sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==}
@@ -1127,11 +1134,9 @@ packages:
'@types/prop-types': 15.7.4 '@types/prop-types': 15.7.4
'@types/scheduler': 0.16.2 '@types/scheduler': 0.16.2
csstype: 3.0.9 csstype: 3.0.9
dev: false
/@types/scheduler/0.16.2: /@types/scheduler/0.16.2:
resolution: {integrity: sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==} resolution: {integrity: sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==}
dev: false
/@types/serve-static/1.13.10: /@types/serve-static/1.13.10:
resolution: {integrity: sha512-nCkHGI4w7ZgAdNkrEu0bv+4xNV/XDqW+DydknebMOQwkpDGx8G+HTlj7R7ABI8i8nKxVw0wtKPi1D+lPOkh4YQ==} resolution: {integrity: sha512-nCkHGI4w7ZgAdNkrEu0bv+4xNV/XDqW+DydknebMOQwkpDGx8G+HTlj7R7ABI8i8nKxVw0wtKPi1D+lPOkh4YQ==}
@@ -1699,6 +1704,11 @@ packages:
node-gyp-build: 4.3.0 node-gyp-build: 4.3.0
dev: false dev: false
/builtin-modules/3.2.0:
resolution: {integrity: sha512-lGzLKcioL90C7wMczpkY0n/oART3MbBa8R9OFGE1rJxoVI86u4WAGfEk8Wjv10eKSyTHVGkSo3bvBylCEtk7LA==}
engines: {node: '>=6'}
dev: true
/builtin-status-codes/3.0.0: /builtin-status-codes/3.0.0:
resolution: {integrity: sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=} resolution: {integrity: sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=}
dev: false dev: false
@@ -1786,6 +1796,10 @@ packages:
fsevents: 2.3.2 fsevents: 2.3.2
dev: true dev: true
/ci-info/3.3.0:
resolution: {integrity: sha512-riT/3vI5YpVH6/qomlDnJow6TBee2PBKSEpx3O32EGPYbWGIRsIlGRms3Sm74wYE1JMo8RnO04Hb12+v1J5ICw==}
dev: true
/cipher-base/1.0.4: /cipher-base/1.0.4:
resolution: {integrity: sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==} resolution: {integrity: sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==}
dependencies: dependencies:
@@ -1797,6 +1811,13 @@ packages:
resolution: {integrity: sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q==} resolution: {integrity: sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q==}
dev: false dev: false
/clean-regexp/1.0.0:
resolution: {integrity: sha1-jffHquUf02h06PjQW5GAvBGj/tc=}
engines: {node: '>=4'}
dependencies:
escape-string-regexp: 1.0.5
dev: true
/clsx/1.1.1: /clsx/1.1.1:
resolution: {integrity: sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA==} resolution: {integrity: sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA==}
engines: {node: '>=6'} engines: {node: '>=6'}
@@ -1982,7 +2003,6 @@ packages:
/csstype/3.0.9: /csstype/3.0.9:
resolution: {integrity: sha512-rpw6JPxK6Rfg1zLOYCSwle2GFOOsnjmDYDaBwEcwoOg4qlsIVCN789VkBZDJAGi4T07gI4YSutR43t9Zz4Lzuw==} resolution: {integrity: sha512-rpw6JPxK6Rfg1zLOYCSwle2GFOOsnjmDYDaBwEcwoOg4qlsIVCN789VkBZDJAGi4T07gI4YSutR43t9Zz4Lzuw==}
dev: false
/d/1.0.1: /d/1.0.1:
resolution: {integrity: sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==} resolution: {integrity: sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==}
@@ -2474,6 +2494,38 @@ packages:
eslint: 8.5.0 eslint: 8.5.0
dev: true dev: true
/eslint-plugin-sonarjs/0.11.0_eslint@8.5.0:
resolution: {integrity: sha512-ei/WuZiL0wP+qx2KrxKyZs3+eDbxiGAhFSm3GKCOOAUkg+G2ny6TSXDB2j67tvyqHefi+eoQsAgGQvz+nEtIBw==}
engines: {node: '>=12'}
peerDependencies:
eslint: ^5.0.0 || ^6.0.0 || ^7.0.0|| ^8.0.0
dependencies:
eslint: 8.5.0
dev: true
/eslint-plugin-unicorn/40.0.0_eslint@8.5.0:
resolution: {integrity: sha512-5GRXISfBk8jMmYk1eeNDw8zSRnWTxBjWkzx2Prre6E2/yLu2twozZ3EomLWCBu9nWms/ZE361BItyMQwfnG1qA==}
engines: {node: '>=12'}
peerDependencies:
eslint: '>=7.32.0'
dependencies:
'@babel/helper-validator-identifier': 7.15.7
ci-info: 3.3.0
clean-regexp: 1.0.0
eslint: 8.5.0
eslint-utils: 3.0.0_eslint@8.5.0
esquery: 1.4.0
indent-string: 4.0.0
is-builtin-module: 3.1.0
lodash: 4.17.21
pluralize: 8.0.0
read-pkg-up: 7.0.1
regexp-tree: 0.1.24
safe-regex: 2.1.1
semver: 7.3.5
strip-indent: 3.0.0
dev: true
/eslint-scope/5.1.1: /eslint-scope/5.1.1:
resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==}
engines: {node: '>=8.0.0'} engines: {node: '>=8.0.0'}
@@ -2725,7 +2777,6 @@ packages:
dependencies: dependencies:
locate-path: 5.0.0 locate-path: 5.0.0
path-exists: 4.0.0 path-exists: 4.0.0
dev: false
/flat-cache/3.0.4: /flat-cache/3.0.4:
resolution: {integrity: sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==} resolution: {integrity: sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==}
@@ -2929,6 +2980,10 @@ packages:
minimalistic-crypto-utils: 1.0.1 minimalistic-crypto-utils: 1.0.1
dev: false dev: false
/hosted-git-info/2.8.9:
resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==}
dev: true
/http-errors/1.6.2: /http-errors/1.6.2:
resolution: {integrity: sha1-CgAsyFcHGSp+eUbO7cERVfYOxzY=} resolution: {integrity: sha1-CgAsyFcHGSp+eUbO7cERVfYOxzY=}
engines: {node: '>= 0.6'} engines: {node: '>= 0.6'}
@@ -3032,6 +3087,11 @@ packages:
engines: {node: '>=0.8.19'} engines: {node: '>=0.8.19'}
dev: true dev: true
/indent-string/4.0.0:
resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==}
engines: {node: '>=8'}
dev: true
/inflight/1.0.6: /inflight/1.0.6:
resolution: {integrity: sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=} resolution: {integrity: sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=}
dependencies: dependencies:
@@ -3088,6 +3148,13 @@ packages:
call-bind: 1.0.2 call-bind: 1.0.2
has-tostringtag: 1.0.0 has-tostringtag: 1.0.0
/is-builtin-module/3.1.0:
resolution: {integrity: sha512-OV7JjAgOTfAFJmHZLvpSTb4qi0nIILDV1gWPYDnDJUTNFM5aGlRAhk4QcT8i7TuAleeEV5Fdkqn3t4mS+Q11fg==}
engines: {node: '>=6'}
dependencies:
builtin-modules: 3.2.0
dev: true
/is-callable/1.2.4: /is-callable/1.2.4:
resolution: {integrity: sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==} resolution: {integrity: sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@@ -3364,7 +3431,6 @@ packages:
engines: {node: '>=8'} engines: {node: '>=8'}
dependencies: dependencies:
p-locate: 4.1.0 p-locate: 4.1.0
dev: false
/lodash.merge/4.6.2: /lodash.merge/4.6.2:
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
@@ -3374,6 +3440,10 @@ packages:
resolution: {integrity: sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=} resolution: {integrity: sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=}
dev: false dev: false
/lodash/4.17.21:
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
dev: true
/loose-envify/1.4.0: /loose-envify/1.4.0:
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
hasBin: true hasBin: true
@@ -3444,6 +3514,11 @@ packages:
hasBin: true hasBin: true
dev: true dev: true
/min-indent/1.0.1:
resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==}
engines: {node: '>=4'}
dev: true
/minimalistic-assert/1.0.1: /minimalistic-assert/1.0.1:
resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==} resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==}
dev: false dev: false
@@ -3612,6 +3687,15 @@ packages:
resolution: {integrity: sha512-CqyzN6z7Q6aMeF/ktcMVTzhAHCEpf8SOarwpzpf8pNBY2k5/oM34UHldUwp8VKI7uxct2HxSRdJjBaZeESzcxA==} resolution: {integrity: sha512-CqyzN6z7Q6aMeF/ktcMVTzhAHCEpf8SOarwpzpf8pNBY2k5/oM34UHldUwp8VKI7uxct2HxSRdJjBaZeESzcxA==}
dev: true dev: true
/normalize-package-data/2.5.0:
resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==}
dependencies:
hosted-git-info: 2.8.9
resolve: 1.20.0
semver: 5.7.1
validate-npm-package-license: 3.0.4
dev: true
/normalize-path/3.0.0: /normalize-path/3.0.0:
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@@ -3731,7 +3815,6 @@ packages:
engines: {node: '>=6'} engines: {node: '>=6'}
dependencies: dependencies:
p-try: 2.2.0 p-try: 2.2.0
dev: false
/p-limit/3.1.0: /p-limit/3.1.0:
resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
@@ -3752,7 +3835,6 @@ packages:
engines: {node: '>=8'} engines: {node: '>=8'}
dependencies: dependencies:
p-limit: 2.3.0 p-limit: 2.3.0
dev: false
/p-try/1.0.0: /p-try/1.0.0:
resolution: {integrity: sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=} resolution: {integrity: sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=}
@@ -3762,7 +3844,6 @@ packages:
/p-try/2.2.0: /p-try/2.2.0:
resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==}
engines: {node: '>=6'} engines: {node: '>=6'}
dev: false
/pako/1.0.11: /pako/1.0.11:
resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==}
@@ -3807,7 +3888,6 @@ packages:
/path-exists/4.0.0: /path-exists/4.0.0:
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
engines: {node: '>=8'} engines: {node: '>=8'}
dev: false
/path-is-absolute/1.0.1: /path-is-absolute/1.0.1:
resolution: {integrity: sha1-F0uSaHNVNP+8es5r9TpanhtcX18=} resolution: {integrity: sha1-F0uSaHNVNP+8es5r9TpanhtcX18=}
@@ -3910,6 +3990,11 @@ packages:
- utf-8-validate - utf-8-validate
dev: true dev: true
/pluralize/8.0.0:
resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==}
engines: {node: '>=4'}
dev: true
/pngjs/4.0.1: /pngjs/4.0.1:
resolution: {integrity: sha512-rf5+2/ioHeQxR6IxuYNYGFytUyG3lma/WW1nsmjeHlWwtb2aByla6dkVc8pmJ9nplzkTA0q2xx7mMWrOTqT4Gg==} resolution: {integrity: sha512-rf5+2/ioHeQxR6IxuYNYGFytUyG3lma/WW1nsmjeHlWwtb2aByla6dkVc8pmJ9nplzkTA0q2xx7mMWrOTqT4Gg==}
engines: {node: '>=8.0.0'} engines: {node: '>=8.0.0'}
@@ -4183,6 +4268,25 @@ packages:
object-assign: 4.1.1 object-assign: 4.1.1
dev: false dev: false
/read-pkg-up/7.0.1:
resolution: {integrity: sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==}
engines: {node: '>=8'}
dependencies:
find-up: 4.1.0
read-pkg: 5.2.0
type-fest: 0.8.1
dev: true
/read-pkg/5.2.0:
resolution: {integrity: sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==}
engines: {node: '>=8'}
dependencies:
'@types/normalize-package-data': 2.4.1
normalize-package-data: 2.5.0
parse-json: 5.2.0
type-fest: 0.6.0
dev: true
/readable-stream/3.6.0: /readable-stream/3.6.0:
resolution: {integrity: sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==} resolution: {integrity: sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==}
engines: {node: '>= 6'} engines: {node: '>= 6'}
@@ -4213,6 +4317,11 @@ packages:
/regenerator-runtime/0.13.9: /regenerator-runtime/0.13.9:
resolution: {integrity: sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==} resolution: {integrity: sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==}
/regexp-tree/0.1.24:
resolution: {integrity: sha512-s2aEVuLhvnVJW6s/iPgEGK6R+/xngd2jNQ+xy4bXNDKxZKJH6jpPHY6kVeVv1IeLCHgswRj+Kl3ELaDjG6V1iw==}
hasBin: true
dev: true
/regexp.prototype.flags/1.3.1: /regexp.prototype.flags/1.3.1:
resolution: {integrity: sha512-JiBdRBq91WlY7uRJ0ds7R+dU02i6LKi8r3BuQhNXn+kmeLN+EfHhfjqMRis1zJxnlu88hq/4dx0P2OP3APRTOA==} resolution: {integrity: sha512-JiBdRBq91WlY7uRJ0ds7R+dU02i6LKi8r3BuQhNXn+kmeLN+EfHhfjqMRis1zJxnlu88hq/4dx0P2OP3APRTOA==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@@ -4287,6 +4396,12 @@ packages:
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
dev: false dev: false
/safe-regex/2.1.1:
resolution: {integrity: sha512-rx+x8AMzKb5Q5lQ95Zoi6ZbJqwCLkqi3XuJXp5P3rT8OEc6sZCJG5AE5dU3lsgRr/F4Bs31jSlVN+j5KrsGu9A==}
dependencies:
regexp-tree: 0.1.24
dev: true
/safer-buffer/2.1.2: /safer-buffer/2.1.2:
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
dev: false dev: false
@@ -4298,6 +4413,11 @@ packages:
object-assign: 4.1.1 object-assign: 4.1.1
dev: false dev: false
/semver/5.7.1:
resolution: {integrity: sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==}
hasBin: true
dev: true
/semver/6.3.0: /semver/6.3.0:
resolution: {integrity: sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==} resolution: {integrity: sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==}
hasBin: true hasBin: true
@@ -4429,6 +4549,28 @@ packages:
whatwg-url: 7.1.0 whatwg-url: 7.1.0
dev: false dev: false
/spdx-correct/3.1.1:
resolution: {integrity: sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==}
dependencies:
spdx-expression-parse: 3.0.1
spdx-license-ids: 3.0.11
dev: true
/spdx-exceptions/2.3.0:
resolution: {integrity: sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==}
dev: true
/spdx-expression-parse/3.0.1:
resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==}
dependencies:
spdx-exceptions: 2.3.0
spdx-license-ids: 3.0.11
dev: true
/spdx-license-ids/3.0.11:
resolution: {integrity: sha512-Ctl2BrFiM0X3MANYgj3CkygxhRmr9mi6xhejbdO960nF6EDJApTYpn0BQnDKlnNBULKiCN1n3w9EBkHK8ZWg+g==}
dev: true
/stack-utils/2.0.5: /stack-utils/2.0.5:
resolution: {integrity: sha512-xrQcmYhOsn/1kX+Vraq+7j4oE2j/6BFscZ0etmYg81xuM8Gq0022Pxb8+IqgOFUIaxHs0KaSb7T1+OegiNrNFA==} resolution: {integrity: sha512-xrQcmYhOsn/1kX+Vraq+7j4oE2j/6BFscZ0etmYg81xuM8Gq0022Pxb8+IqgOFUIaxHs0KaSb7T1+OegiNrNFA==}
engines: {node: '>=10'} engines: {node: '>=10'}
@@ -4516,6 +4658,13 @@ packages:
engines: {node: '>=4'} engines: {node: '>=4'}
dev: true dev: true
/strip-indent/3.0.0:
resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==}
engines: {node: '>=8'}
dependencies:
min-indent: 1.0.1
dev: true
/strip-json-comments/3.1.1: /strip-json-comments/3.1.1:
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
engines: {node: '>=8'} engines: {node: '>=8'}
@@ -4694,11 +4843,21 @@ packages:
engines: {node: '>=10'} engines: {node: '>=10'}
dev: true dev: true
/type-fest/0.6.0:
resolution: {integrity: sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==}
engines: {node: '>=8'}
dev: true
/type-fest/0.7.1: /type-fest/0.7.1:
resolution: {integrity: sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==} resolution: {integrity: sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==}
engines: {node: '>=8'} engines: {node: '>=8'}
dev: false dev: false
/type-fest/0.8.1:
resolution: {integrity: sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==}
engines: {node: '>=8'}
dev: true
/type/1.2.0: /type/1.2.0:
resolution: {integrity: sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==} resolution: {integrity: sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==}
dev: false dev: false
@@ -4717,7 +4876,7 @@ packages:
resolution: {integrity: sha512-VgYs2A2QIRuGphtzFV7aQJduJ2gyfTljngLzjpfW9FoYZF6xuw1W0vW9ghCKLfcWrCFxK81CSGRAvS1pn4fIUg==} resolution: {integrity: sha512-VgYs2A2QIRuGphtzFV7aQJduJ2gyfTljngLzjpfW9FoYZF6xuw1W0vW9ghCKLfcWrCFxK81CSGRAvS1pn4fIUg==}
engines: {node: '>=4.2.0'} engines: {node: '>=4.2.0'}
hasBin: true hasBin: true
dev: false dev: true
/unbox-primitive/1.0.1: /unbox-primitive/1.0.1:
resolution: {integrity: sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw==} resolution: {integrity: sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw==}
@@ -4773,6 +4932,13 @@ packages:
resolution: {integrity: sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==} resolution: {integrity: sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==}
dev: true dev: true
/validate-npm-package-license/3.0.4:
resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==}
dependencies:
spdx-correct: 3.1.1
spdx-expression-parse: 3.0.1
dev: true
/vary/1.1.2: /vary/1.1.2:
resolution: {integrity: sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=} resolution: {integrity: sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=}
engines: {node: '>= 0.8'} engines: {node: '>= 0.8'}

View File

@@ -1,8 +1,9 @@
/* eslint-disable unicorn/prefer-module */
// If you want to use other PostCSS plugins, see the following: // If you want to use other PostCSS plugins, see the following:
// https://tailwindcss.com/docs/using-with-preprocessors // https://tailwindcss.com/docs/using-with-preprocessors
module.exports = { module.exports = {
plugins: { plugins: {
tailwindcss: {}, tailwindcss: {},
autoprefixer: {}, autoprefixer: {},
}, },
}; };

View File

@@ -1,76 +1,77 @@
/* eslint-disable unicorn/prefer-module */
module.exports = { module.exports = {
mode: 'jit', mode: 'jit',
purge: ['./pages/**/*.{js,ts,jsx,tsx}', './components/**/*.{js,ts,jsx,tsx}'], purge: ['./pages/**/*.{js,ts,jsx,tsx}', './components/**/*.{js,ts,jsx,tsx}'],
darkMode: false, // or 'media' or 'class' darkMode: false, // or 'media' or 'class'
theme: { theme: {
fontFamily: { fontFamily: {
title: ['Poppins'], title: ['Poppins'],
body: ['Inter'], body: ['Inter'],
}, },
extend: {}, extend: {},
}, },
variants: { variants: {
extend: {}, extend: {},
}, },
plugins: [require('daisyui')], plugins: [require('daisyui')],
daisyui: { daisyui: {
themes: [ themes: [
{ {
supaTheme: { supaTheme: {
primary: '#00B8F0', primary: '#00B8F0',
'primary-focus': '#009de0', 'primary-focus': '#009de0',
'primary-content': '#ffffff', 'primary-content': '#ffffff',
secondary: '#f03800', secondary: '#f03800',
'secondary-focus': '#e22f00', 'secondary-focus': '#e22f00',
'secondary-content': '#ffffff', 'secondary-content': '#ffffff',
accent: '#00f0b0', accent: '#00f0b0',
'accent-focus': '#00e28a', 'accent-focus': '#00e28a',
'accent-content': '#ffffff', 'accent-content': '#ffffff',
neutral: '#3d4451', neutral: '#3d4451',
'neutral-focus': '#2a2e37', 'neutral-focus': '#2a2e37',
'neutral-content': '#ffffff', 'neutral-content': '#ffffff',
'base-100': '#ffffff', 'base-100': '#ffffff',
'base-200': '#767676', 'base-200': '#767676',
'base-300': '#d1d5db', 'base-300': '#d1d5db',
'base-content': '#1f2937', 'base-content': '#1f2937',
info: '#2094f3' /* Info */, info: '#2094f3' /* Info */,
success: '#009485' /* Success */, success: '#009485' /* Success */,
warning: '#ff9900' /* Warning */, warning: '#ff9900' /* Warning */,
error: '#ff5724' /* Error */, error: '#ff5724' /* Error */,
}, },
dark: { dark: {
primary: '#00B8F0', primary: '#00B8F0',
'primary-focus': '#009de0', 'primary-focus': '#009de0',
'primary-content': '#ffffff', 'primary-content': '#ffffff',
secondary: '#f03800', secondary: '#f03800',
'secondary-focus': '#e22f00', 'secondary-focus': '#e22f00',
'secondary-content': '#ffffff', 'secondary-content': '#ffffff',
accent: '#00f0b0', accent: '#00f0b0',
'accent-focus': '#00e28a', 'accent-focus': '#00e28a',
'accent-content': '#ffffff', 'accent-content': '#ffffff',
neutral: '#3d4451', neutral: '#3d4451',
'neutral-focus': '#2a2e37', 'neutral-focus': '#2a2e37',
'neutral-content': '#ffffff', 'neutral-content': '#ffffff',
'base-100': '#2A2E37', 'base-100': '#2A2E37',
'base-200': '#EBECF0', 'base-200': '#EBECF0',
'base-300': '#16181D', 'base-300': '#16181D',
'base-content': '#EBECF0', 'base-content': '#EBECF0',
info: '#2094f3', info: '#2094f3',
success: '#009485', success: '#009485',
warning: '#ff9900', warning: '#ff9900',
error: '#ff5724', error: '#ff5724',
}, },
}, },
], ],
}, },
}; };

View File

@@ -36,7 +36,7 @@
// "noPropertyAccessFromIndexSignature": true, /* Require undeclared properties from index signatures to use element accesses. */ // "noPropertyAccessFromIndexSignature": true, /* Require undeclared properties from index signatures to use element accesses. */
/* Module Resolution Options */ /* Module Resolution Options */
"moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
"baseUrl": "./", /* Base directory to resolve non-absolute module names. */ /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
// "typeRoots": [], /* List of folders to include type definitions from. */ // "typeRoots": [], /* List of folders to include type definitions from. */
// "types": [], /* Type declaration files to be included in compilation. */ // "types": [], /* Type declaration files to be included in compilation. */
"allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
@@ -46,7 +46,7 @@
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
"inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
"inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ /* Experimental Options */// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ /* Experimental Options */ // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
/* Advanced Options */ /* Advanced Options */
"skipLibCheck": true, /* Skip type checking of declaration files. */ "skipLibCheck": true, /* Skip type checking of declaration files. */
@@ -65,9 +65,16 @@
"include": [ "include": [
"next-env.d.ts", "next-env.d.ts",
"**/*.ts", "**/*.ts",
"**/*.tsx" "**/*.tsx",
"utils/AuthContext.tsx",
"**/*.js",
"*.js",
], ],
"exclude": [ "exclude": [
"node_modules" "node_modules",
"dist",
".next",
"out",
"types/*",
] ]
} }

File diff suppressed because one or more lines are too long

441
types/database/index.ts Normal file
View File

@@ -0,0 +1,441 @@
/**
* This file was auto-generated by openapi-typescript.
* Do not make direct changes to the file.
*/
export interface paths {
'/': {
get: {
responses: {
/** OK */
200: unknown;
};
};
};
'/admin_list': {
get: {
parameters: {
query: {
id?: parameters['rowFilter.admin_list.id'];
isadmin?: parameters['rowFilter.admin_list.isadmin'];
/** Filtering Columns */
select?: parameters['select'];
/** Ordering */
order?: parameters['order'];
/** Limiting and Pagination */
offset?: parameters['offset'];
/** Limiting and Pagination */
limit?: parameters['limit'];
};
header: {
/** Limiting and Pagination */
Range?: parameters['range'];
/** Limiting and Pagination */
'Range-Unit'?: parameters['rangeUnit'];
/** Preference */
Prefer?: parameters['preferCount'];
};
};
responses: {
/** OK */
200: {
schema: definitions['admin_list'][];
};
/** Partial Content */
206: unknown;
};
};
post: {
parameters: {
body: {
/** admin_list */
admin_list?: definitions['admin_list'];
};
query: {
/** Filtering Columns */
select?: parameters['select'];
};
header: {
/** Preference */
Prefer?: parameters['preferReturn'];
};
};
responses: {
/** Created */
201: unknown;
};
};
delete: {
parameters: {
query: {
id?: parameters['rowFilter.admin_list.id'];
isadmin?: parameters['rowFilter.admin_list.isadmin'];
};
header: {
/** Preference */
Prefer?: parameters['preferReturn'];
};
};
responses: {
/** No Content */
204: never;
};
};
patch: {
parameters: {
query: {
id?: parameters['rowFilter.admin_list.id'];
isadmin?: parameters['rowFilter.admin_list.isadmin'];
};
body: {
/** admin_list */
admin_list?: definitions['admin_list'];
};
header: {
/** Preference */
Prefer?: parameters['preferReturn'];
};
};
responses: {
/** No Content */
204: never;
};
};
};
'/profiles': {
get: {
parameters: {
query: {
id?: parameters['rowFilter.profiles.id'];
updated_at?: parameters['rowFilter.profiles.updated_at'];
username?: parameters['rowFilter.profiles.username'];
avatar_url?: parameters['rowFilter.profiles.avatar_url'];
website?: parameters['rowFilter.profiles.website'];
customerId?: parameters['rowFilter.profiles.customerId'];
/** Filtering Columns */
select?: parameters['select'];
/** Ordering */
order?: parameters['order'];
/** Limiting and Pagination */
offset?: parameters['offset'];
/** Limiting and Pagination */
limit?: parameters['limit'];
};
header: {
/** Limiting and Pagination */
Range?: parameters['range'];
/** Limiting and Pagination */
'Range-Unit'?: parameters['rangeUnit'];
/** Preference */
Prefer?: parameters['preferCount'];
};
};
responses: {
/** OK */
200: {
schema: definitions['profiles'][];
};
/** Partial Content */
206: unknown;
};
};
post: {
parameters: {
body: {
/** profiles */
profiles?: definitions['profiles'];
};
query: {
/** Filtering Columns */
select?: parameters['select'];
};
header: {
/** Preference */
Prefer?: parameters['preferReturn'];
};
};
responses: {
/** Created */
201: unknown;
};
};
delete: {
parameters: {
query: {
id?: parameters['rowFilter.profiles.id'];
updated_at?: parameters['rowFilter.profiles.updated_at'];
username?: parameters['rowFilter.profiles.username'];
avatar_url?: parameters['rowFilter.profiles.avatar_url'];
website?: parameters['rowFilter.profiles.website'];
customerId?: parameters['rowFilter.profiles.customerId'];
};
header: {
/** Preference */
Prefer?: parameters['preferReturn'];
};
};
responses: {
/** No Content */
204: never;
};
};
patch: {
parameters: {
query: {
id?: parameters['rowFilter.profiles.id'];
updated_at?: parameters['rowFilter.profiles.updated_at'];
username?: parameters['rowFilter.profiles.username'];
avatar_url?: parameters['rowFilter.profiles.avatar_url'];
website?: parameters['rowFilter.profiles.website'];
customerId?: parameters['rowFilter.profiles.customerId'];
};
body: {
/** profiles */
profiles?: definitions['profiles'];
};
header: {
/** Preference */
Prefer?: parameters['preferReturn'];
};
};
responses: {
/** No Content */
204: never;
};
};
};
'/subscriptions': {
get: {
parameters: {
query: {
id?: parameters['rowFilter.subscriptions.id'];
customer_id?: parameters['rowFilter.subscriptions.customer_id'];
paid_user?: parameters['rowFilter.subscriptions.paid_user'];
plan?: parameters['rowFilter.subscriptions.plan'];
subscription?: parameters['rowFilter.subscriptions.subscription'];
/** Filtering Columns */
select?: parameters['select'];
/** Ordering */
order?: parameters['order'];
/** Limiting and Pagination */
offset?: parameters['offset'];
/** Limiting and Pagination */
limit?: parameters['limit'];
};
header: {
/** Limiting and Pagination */
Range?: parameters['range'];
/** Limiting and Pagination */
'Range-Unit'?: parameters['rangeUnit'];
/** Preference */
Prefer?: parameters['preferCount'];
};
};
responses: {
/** OK */
200: {
schema: definitions['subscriptions'][];
};
/** Partial Content */
206: unknown;
};
};
post: {
parameters: {
body: {
/** subscriptions */
subscriptions?: definitions['subscriptions'];
};
query: {
/** Filtering Columns */
select?: parameters['select'];
};
header: {
/** Preference */
Prefer?: parameters['preferReturn'];
};
};
responses: {
/** Created */
201: unknown;
};
};
delete: {
parameters: {
query: {
id?: parameters['rowFilter.subscriptions.id'];
customer_id?: parameters['rowFilter.subscriptions.customer_id'];
paid_user?: parameters['rowFilter.subscriptions.paid_user'];
plan?: parameters['rowFilter.subscriptions.plan'];
subscription?: parameters['rowFilter.subscriptions.subscription'];
};
header: {
/** Preference */
Prefer?: parameters['preferReturn'];
};
};
responses: {
/** No Content */
204: never;
};
};
patch: {
parameters: {
query: {
id?: parameters['rowFilter.subscriptions.id'];
customer_id?: parameters['rowFilter.subscriptions.customer_id'];
paid_user?: parameters['rowFilter.subscriptions.paid_user'];
plan?: parameters['rowFilter.subscriptions.plan'];
subscription?: parameters['rowFilter.subscriptions.subscription'];
};
body: {
/** subscriptions */
subscriptions?: definitions['subscriptions'];
};
header: {
/** Preference */
Prefer?: parameters['preferReturn'];
};
};
responses: {
/** No Content */
204: never;
};
};
};
'/rpc/handle_new_user': {
post: {
parameters: {
body: {
args: { [key: string]: unknown };
};
header: {
/** Preference */
Prefer?: parameters['preferParams'];
};
};
responses: {
/** OK */
200: unknown;
};
};
};
}
export interface definitions {
admin_list: {
/**
* Format: uuid
* @description Note:
* This is a Primary Key.<pk/>
*/
id: string;
/** Format: boolean */
isadmin?: boolean;
};
profiles: {
/**
* Format: uuid
* @description Note:
* This is a Primary Key.<pk/>
*/
id: string;
/** Format: timestamp with time zone */
updated_at?: string;
/** Format: text */
username?: string;
/** Format: text */
avatar_url?: string;
/** Format: text */
website?: string;
/** Format: text */
customerId?: string;
};
subscriptions: {
/**
* Format: uuid
* @description Note:
* This is a Primary Key.<pk/>
*/
id: string;
/** Format: text */
customer_id?: string;
/** Format: boolean */
paid_user?: boolean;
/** Format: text */
plan?: string;
/** Format: text */
subscription?: string;
};
}
export interface parameters {
/**
* @description Preference
* @enum {string}
*/
preferParams: 'params=single-object';
/**
* @description Preference
* @enum {string}
*/
preferReturn: 'return=representation' | 'return=minimal' | 'return=none';
/**
* @description Preference
* @enum {string}
*/
preferCount: 'count=none';
/** @description Filtering Columns */
select: string;
/** @description On Conflict */
on_conflict: string;
/** @description Ordering */
order: string;
/** @description Limiting and Pagination */
range: string;
/**
* @description Limiting and Pagination
* @default items
*/
rangeUnit: string;
/** @description Limiting and Pagination */
offset: string;
/** @description Limiting and Pagination */
limit: string;
/** @description admin_list */
'body.admin_list': definitions['admin_list'];
/** Format: uuid */
'rowFilter.admin_list.id': string;
/** Format: boolean */
'rowFilter.admin_list.isadmin': string;
/** @description profiles */
'body.profiles': definitions['profiles'];
/** Format: uuid */
'rowFilter.profiles.id': string;
/** Format: timestamp with time zone */
'rowFilter.profiles.updated_at': string;
/** Format: text */
'rowFilter.profiles.username': string;
/** Format: text */
'rowFilter.profiles.avatar_url': string;
/** Format: text */
'rowFilter.profiles.website': string;
/** Format: text */
'rowFilter.profiles.customerId': string;
/** @description subscriptions */
'body.subscriptions': definitions['subscriptions'];
/** Format: uuid */
'rowFilter.subscriptions.id': string;
/** Format: text */
'rowFilter.subscriptions.customer_id': string;
/** Format: boolean */
'rowFilter.subscriptions.paid_user': string;
/** Format: text */
'rowFilter.subscriptions.plan': string;
/** Format: text */
'rowFilter.subscriptions.subscription': string;
}
export interface operations {}
export interface external {}

54
types/supabase.ts Normal file
View File

@@ -0,0 +1,54 @@
/**
* This file was auto-generated by openapi-typescript.
* Do not make direct changes to the file.
*/
export interface paths {
'/': {
get: {
responses: {
/** OK */
200: unknown;
};
};
};
}
export interface parameters {
/**
* @description Preference
* @enum {string}
*/
preferParams: 'params=single-object';
/**
* @description Preference
* @enum {string}
*/
preferReturn: 'return=representation' | 'return=minimal' | 'return=none';
/**
* @description Preference
* @enum {string}
*/
preferCount: 'count=none';
/** @description Filtering Columns */
select: string;
/** @description On Conflict */
on_conflict: string;
/** @description Ordering */
order: string;
/** @description Limiting and Pagination */
range: string;
/**
* @description Limiting and Pagination
* @default items
*/
rangeUnit: string;
/** @description Limiting and Pagination */
offset: string;
/** @description Limiting and Pagination */
limit: string;
}
export interface operations {}
export interface external {}

View File

@@ -1,63 +0,0 @@
import React, { createContext, useContext, useEffect, useState } from 'react';
import { supabase } from 'utils/supabaseClient';
// create a context for authentication
const AuthContext = createContext();
export const AuthProvider = ({ children }) => {
const [user, setUser] = useState();
const [session, setSession] = useState();
const [loading, setLoading] = useState(true);
useEffect(() => {
// Check active sessions and sets the user
const session = supabase.auth.session();
setUser(session?.user ?? null);
setSession(session ?? null);
setLoading(false);
// Listen for changes on auth state (logged in, signed out, etc.)
const { data: listener } = supabase.auth.onAuthStateChange(
async (event, session) => {
if ((event === 'SIGNED_OUT') | (event === 'SIGNED_IN')) {
fetch('/api/auth', {
method: 'POST',
headers: new Headers({ 'Content-Type': 'application/json' }),
credentials: 'same-origin',
body: JSON.stringify({ event, session }),
}).then((res) => res.json());
}
if (event === 'USER_UPDATED') {
}
setUser(session?.user ?? null);
setSession(session ?? null);
setLoading(false);
}
);
return () => {
listener?.unsubscribe();
};
}, []);
// Will be passed down to Signup, Login and Dashboard components
const value = {
signUp: (data) => supabase.auth.signUp(data),
signIn: (data) => supabase.auth.signIn(data),
signOut: () => supabase.auth.signOut(),
resetPassword: (data) => supabase.auth.api.resetPasswordForEmail(data),
user,
session,
};
return (
<AuthContext.Provider value={value}>
{!loading && children}
</AuthContext.Provider>
);
};
// export the useAuth hook
export const useAuth = () => useContext(AuthContext);

108
utils/AuthContext.tsx Normal file
View File

@@ -0,0 +1,108 @@
import { ApiError, Session, UserCredentials } from '@supabase/gotrue-js';
import {
ReactNode,
createContext,
useContext,
useEffect,
useState,
} from 'react';
import { supabase } from 'utils/supabaseClient';
type authContextType = {
user: Session['user'] | undefined;
session: Session | null;
signOut: () => void;
signUp: (data: UserCredentials) => Promise<{
user: Session['user'] | null;
session: Session | null;
error: ApiError | null;
}>;
resetPassword: (data: string) => Promise<{
data: {} | null;
error: ApiError | null;
}>;
signIn: (data: UserCredentials) => Promise<{
user: Session['user'] | null;
session: Session | null;
error: ApiError | null;
}>;
};
type Properties = {
children: ReactNode;
};
const authContextDefaultValues: authContextType = {
user: undefined,
session: null,
signOut: () => {},
signUp: () => {
return Promise.resolve({ user: null, session: null, error: null });
},
signIn: () => {
return Promise.resolve({ user: null, session: null, error: null });
},
resetPassword: () => {
return Promise.resolve({ data: null, error: null });
},
};
// create a context for authentication
const AuthContext = createContext(authContextDefaultValues);
export const AuthProvider = ({ children }: Properties): JSX.Element => {
const [user, setUser] = useState(supabase.auth.session()?.user);
const [session, setSession] = useState(supabase.auth.session());
const [loading, setLoading] = useState(true);
useEffect(() => {
// Check active sessions and sets the user
const session = supabase.auth.session();
setUser(session?.user);
setSession(session);
setLoading(false);
// Listen for changes on auth state (logged in, signed out, etc.)
const { data: listener } = supabase.auth.onAuthStateChange(
(event, session): void => {
if (event === 'SIGNED_OUT' || event === 'SIGNED_IN') {
void fetch('/api/auth', {
method: 'POST',
headers: new Headers({ 'Content-Type': 'application/json' }),
credentials: 'same-origin',
body: JSON.stringify({ event, session }),
}).then((response) => response.json());
}
setUser(session?.user);
setSession(session);
setLoading(false);
}
);
return () => {
listener?.unsubscribe();
};
}, []);
// Will be passed down to Signup, Login and Dashboard components
const value = {
signUp: (data: UserCredentials) => supabase.auth.signUp(data),
signIn: (data: UserCredentials) => supabase.auth.signIn(data),
signOut: () => supabase.auth.signOut(),
resetPassword: (data: string) =>
supabase.auth.api.resetPasswordForEmail(data),
user,
session,
};
return (
<AuthContext.Provider value={value}>
{!loading && children}
</AuthContext.Provider>
);
};
// export the useAuth hook
export const useAuth = (): authContextType => useContext(AuthContext);

View File

@@ -4,9 +4,10 @@
import type { NextApiRequest, NextApiResponse } from 'next'; import type { NextApiRequest, NextApiResponse } from 'next';
export default function initMiddleware(middleware: any) { export default function initMiddleware(middleware: any) {
return (req: NextApiRequest, res: NextApiResponse) => return (request: NextApiRequest, response: NextApiResponse) =>
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
middleware(req, res, (result: any) => { // eslint-disable-next-line @typescript-eslint/no-unsafe-call
middleware(request, response, (result: any) => {
if (result instanceof Error) { if (result instanceof Error) {
return reject(result); return reject(result);
} }

View File

@@ -6,6 +6,7 @@ import { Stripe, loadStripe } from '@stripe/stripe-js';
let stripePromise: Promise<Stripe | null>; let stripePromise: Promise<Stripe | null>;
const getStripe = (): Promise<Stripe | null> => { const getStripe = (): Promise<Stripe | null> => {
// eslint-disable-next-line @typescript-eslint/no-misused-promises
if (!stripePromise) { if (!stripePromise) {
stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLIC_KEY || ''); stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLIC_KEY || '');
} }

View File

@@ -1,4 +1,5 @@
import { createClient } from '@supabase/supabase-js'; import { createClient } from '@supabase/supabase-js';
import { definitions } from 'types/database/index';
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY; const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
@@ -6,12 +7,12 @@ const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
export const supabase = createClient(supabaseUrl || '', supabaseAnonKey || ''); export const supabase = createClient(supabaseUrl || '', supabaseAnonKey || '');
// Check if a user has a paid plan // Check if a user has a paid plan
export const getSub = async () => { export const getSub = async (): Promise<
undefined | definitions['subscriptions']
> => {
const { data: subscriptions } = await supabase const { data: subscriptions } = await supabase
.from('subscriptions') .from<definitions['subscriptions']>('subscriptions')
.select('paid_user, plan') .select('paid_user, plan')
.single(); .single();
if (subscriptions) { return subscriptions ? subscriptions : undefined;
return subscriptions;
}
}; };