First implementation of eslint/prettier

This commit is contained in:
Michael
2021-08-10 12:50:47 +02:00
parent fbd57c837c
commit 9bc6fae2c4
52 changed files with 2780 additions and 7675 deletions

52
.eslintrc.json Normal file
View File

@@ -0,0 +1,52 @@
{
"env": {
"browser": true,
"es2021": true,
"node": true,
"cypress/globals": true
},
"extends": [
"plugin:react/recommended",
"airbnb",
"prettier"
],
"parserOptions": {
"ecmaFeatures": {
"jsx": true
},
"ecmaVersion": 12,
"sourceType": "module"
},
"plugins": [
"react",
"cypress",
"simple-import-sort",
"prettier"
],
"rules": {
"no-console": "off",
"react/no-unescaped-entities": "off",
"prettier/prettier": "error",
"react/react-in-jsx-scope": "off",
"react/jsx-filename-extension": [
2,
{
"extensions": [
".js",
".jsx",
".ts",
".tsx"
]
}
]
},
"settings": {
"import/resolver": {
"node": {
"paths": [
"."
]
}
}
}
}

3
.estlintignore Normal file
View File

@@ -0,0 +1,3 @@
.next
dist
node_modules/

5
.prettierrc.json Normal file
View File

@@ -0,0 +1,5 @@
{
"singleQuote": true,
"tabWidth": 2,
"semi": true
}

View File

@@ -1,26 +1,23 @@
import Image from "next/image"; import Image from 'next/image';
import authImage from "public/auth.png"; import authImage from 'public/auth.png';
const AuthText = () => { const AuthText = () => (
return ( <div className="lg:mt-0 max-w-lg flex flex-col text-xl">
<div className='lg:mt-0 max-w-lg flex flex-col text-xl'> <div className="mt-10 mb-3 m-auto">
<div className='mt-10 mb-3 m-auto'>
<Image <Image
src={authImage} src={authImage}
width={authImage.width / 1.5} width={authImage.width / 1.5}
height={authImage.height / 1.5} height={authImage.height / 1.5}
/> />
</div> </div>
<h2 className='text-4xl font-title font-semibold text-center'> <h2 className="text-4xl font-title font-semibold text-center">
Join SupaNexTail for <span className='text-primary'>free</span>! Join SupaNexTail for <span className="text-primary">free</span>!
</h2> </h2>
<p className='mb-5 mt-8 leading-9'> <p className="mb-5 mt-8 leading-9">
Create your website in a few minutes with our boilerplate. You can use Create your website in a few minutes with our boilerplate. You can use the
the login system, this will allow you to discover the sample dashboard login system, this will allow you to discover the sample dashboard page.
page.
</p> </p>
</div> </div>
); );
};
export default AuthText; export default AuthText;

View File

@@ -4,9 +4,9 @@ the upload.
You can tweak the max size, line 47 You can tweak the max size, line 47
*/ */
import { useEffect, useState } from "react"; import { useEffect, useState } from 'react';
import { supabase } from "utils/supabaseClient"; import { supabase } from 'utils/supabaseClient';
const Avatar = ({ url, size, onUpload }) => { const Avatar = ({ url, size, onUpload }) => {
const [avatarUrl, setAvatarUrl] = useState(null); const [avatarUrl, setAvatarUrl] = useState(null);
@@ -19,7 +19,7 @@ const Avatar = ({ url, size, onUpload }) => {
async function downloadImage(path) { async function downloadImage(path) {
try { try {
const { data, error } = await supabase.storage const { data, error } = await supabase.storage
.from("avatars") .from('avatars')
.download(path); .download(path);
if (error) { if (error) {
throw error; throw error;
@@ -27,7 +27,7 @@ const Avatar = ({ url, size, onUpload }) => {
const url = URL.createObjectURL(data); const url = URL.createObjectURL(data);
setAvatarUrl(url); setAvatarUrl(url);
} catch (error) { } catch (error) {
console.log("Error downloading image: ", error.message); console.log('Error downloading image: ', error.message);
} }
} }
@@ -36,23 +36,23 @@ const Avatar = ({ url, size, onUpload }) => {
setUploading(true); setUploading(true);
if (!event.target.files || event.target.files.length === 0) { if (!event.target.files || event.target.files.length === 0) {
throw new Error("You must select an image to upload."); throw new Error('You must select an image to upload.');
} }
const file = event.target.files[0]; const file = event.target.files[0];
const fileExt = file.name.split(".").pop(); const fileExt = file.name.split('.').pop();
const fileName = `${Math.random()}.${fileExt}`; const fileName = `${Math.random()}.${fileExt}`;
const filePath = `${fileName}`; const filePath = `${fileName}`;
if (event.target.files[0].size > 150000) { if (event.target.files[0].size > 150000) {
alert("File is too big!"); alert('File is too big!');
event.target.value = ""; event.target.value = '';
setUploading(false) setUploading(false);
return; return;
} }
let { error: uploadError } = await supabase.storage const { error: uploadError } = await supabase.storage
.from("avatars") .from('avatars')
.upload(filePath, file); .upload(filePath, file);
if (uploadError) { if (uploadError) {
@@ -68,30 +68,31 @@ const Avatar = ({ url, size, onUpload }) => {
} }
return ( return (
<div className='m-auto mb-5'> <div className="m-auto mb-5">
{avatarUrl ? ( {avatarUrl ? (
<img <img
src={avatarUrl} src={avatarUrl}
alt='Avatar' alt="Avatar"
className='avatar rounded-full w-28 h-28 flex m-auto' className="avatar rounded-full w-28 h-28 flex m-auto"
/> />
) : ( ) : (
<div className='avatar rounded-full w-28 h-28' /> <div className="avatar rounded-full w-28 h-28" />
)} )}
<div style={{ width: size }}> <div style={{ width: size }}>
<label <label
className='mt-2 btn btn-primary text-center cursor-pointer text-xs btn-sm' className="mt-2 btn btn-primary text-center cursor-pointer text-xs btn-sm"
htmlFor='single'> htmlFor="single"
{uploading ? "Uploading ..." : "Update my avatar"} >
{uploading ? 'Uploading ...' : 'Update my avatar'}
</label> </label>
<input <input
style={{ style={{
visibility: "hidden", visibility: 'hidden',
position: "absolute", position: 'absolute',
}} }}
type='file' type="file"
id='single' id="single"
accept='image/*' accept="image/*"
onChange={uploadAvatar} onChange={uploadAvatar}
disabled={uploading} disabled={uploading}
/> />

View File

@@ -1,62 +1,60 @@
import CardLanding from "components/UI/CardLanding"; import CardLanding from 'components/UI/CardLanding';
import cardAuth from "public/landing/auth.svg"; import cardAuth from 'public/landing/auth.svg';
import cardFee from "public/landing/lifetime.svg"; import cardFee from 'public/landing/lifetime.svg';
import cardPage from "public/landing/page.svg"; import cardPage from 'public/landing/page.svg';
import cardResponsive from "public/landing/responsive.svg"; import cardResponsive from 'public/landing/responsive.svg';
import cardServer from "public/landing/backend.svg"; import cardServer from 'public/landing/backend.svg';
import cardStripe from "public/landing/stripe.svg"; import cardStripe from 'public/landing/stripe.svg';
import cardTheme from "public/landing/theme.svg"; import cardTheme from 'public/landing/theme.svg';
const CardsLanding = () => { const CardsLanding = () => (
return ( <div className="mt-14">
<div className='mt-14'> <h2 className="uppercase font-bold text-4xl tracking-wide text-center">
<h2 className='uppercase font-bold text-4xl tracking-wide text-center'>
We've got you covered We've got you covered
</h2> </h2>
<p className='max-w-md m-auto text-center'> <p className="max-w-md m-auto text-center">
Dont waste your time and reinvent the wheel, we have provided you with Dont waste your time and reinvent the wheel, we have provided you with a
a maximum of features so that you only have one goal, to make your SaaS maximum of features so that you only have one goal, to make your SaaS a
a reality. reality.
</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}
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}
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}
text='Auth and user management with Supabase' text="Auth and user management with Supabase"
title='Auth' title="Auth"
/> />
<CardLanding <CardLanding
image={cardResponsive} image={cardResponsive}
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}
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}
text='Stripe integration. Fully functional subscription system' text="Stripe integration. Fully functional subscription system"
title='Payment' title="Payment"
/> />
<CardLanding <CardLanding
image={cardFee} image={cardFee}
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"
/> />
</div> </div>
</div> </div>
); );
};
export default CardsLanding; export default CardsLanding;

View File

@@ -6,32 +6,32 @@ If you want to change the email provider, don't hesitate to create a new api rou
the axios.post here, line 18. 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 = () => { const Contact = () => {
const sendEmail = () => { const sendEmail = () => {
const name = document.getElementById("name").value; const name = document.getElementById('name').value;
const email = document.getElementById("email").value; const email = document.getElementById('email').value;
const message = document.getElementById("message").value; const message = document.getElementById('message').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) => {
if (result.data.success === true) { if (result.data.success === true) {
toast.success(result.data.message); toast.success(result.data.message);
document.getElementById("name").value = ""; document.getElementById('name').value = '';
document.getElementById("email").value = ""; document.getElementById('email').value = '';
document.getElementById("message").value = ""; document.getElementById('message').value = '';
} }
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.log(err);
}); });
} else { } else {
toast.info("Please enter at least one URL", { toast.info('Please enter at least one URL', {
position: "top-center", position: 'top-center',
autoClose: 2000, autoClose: 2000,
hideProgressBar: true, hideProgressBar: true,
closeOnClick: true, closeOnClick: true,
@@ -42,51 +42,56 @@ const Contact = () => {
} }
}; };
return ( return (
<div className='max-w-xl m-auto px-5 py-10'> <div className="max-w-xl m-auto px-5 py-10">
<div> <div>
<div className='flex justify-center'> <div className="flex justify-center">
<h2 className='text-3xl sm:text-4xl text-center mb-5 mt-0 font-bold font-title'> <h2 className="text-3xl sm:text-4xl text-center mb-5 mt-0 font-bold font-title">
Contact Contact
</h2> </h2>
</div> </div>
<p className='m-auto text-center'> <p className="m-auto text-center">
Do you have a question about SupaNexTail? A cool feature you'd like us Do you have a question about SupaNexTail? A cool feature you'd like us
to integrate? A bug to report? Don't hesitate! to integrate? A bug to report? Don't hesitate!
</p> </p>
</div> </div>
<form className='m-auto mt-5 grid grid-cols-1 gap-4 md:grid-cols-2 p-5'> <form className="m-auto mt-5 grid grid-cols-1 gap-4 md:grid-cols-2 p-5">
<div className='flex flex-col max-w-xs'> <div className="flex flex-col max-w-xs">
<label className='font-light mb-4 text-left'>Your Name</label> <p className="font-light mb-4 text-left">Your Name</p>
<input <input
id='name' id="name"
name='name' name="name"
placeholder='Enter your name' placeholder="Enter your name"
className='input input-primary input-bordered'></input> className="input input-primary input-bordered"
/>
</div> </div>
<div className='flex flex-col max-w-xs mb-3'> <div className="flex flex-col max-w-xs mb-3">
<label className='font-light mb-4 text-left'>Your email</label> <p className="font-light mb-4 text-left">Your email</p>
<input <input
id='email' id="email"
name='email' name="email"
placeholder='Enter your email adress' placeholder="Enter your email adress"
className='input input-primary input-bordered'></input> className="input input-primary input-bordered"
/>
</div> </div>
<div className='flex flex-col col-span-full w-fulll'> <div className="flex flex-col col-span-full w-fulll">
<label className='font-light mb-4 text-left'>Message</label> <p className="font-light mb-4 text-left">Message</p>
<textarea <textarea
id='message' id="message"
name='message' name="message"
placeholder='Enter your message here...' placeholder="Enter your message here..."
rows='5' rows="5"
className='input input-primary input-bordered resize-none w-full h-32 pt-2'></textarea> className="input input-primary input-bordered resize-none w-full h-32 pt-2"
/>
</div> </div>
<button <button
className='btn btn-primary btn-sm' type="button"
className="btn btn-primary btn-sm"
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.preventDefault();
sendEmail(); sendEmail();
}}> }}
Submit{" "} >
Submit{' '}
</button> </button>
</form> </form>
</div> </div>

View File

@@ -5,16 +5,16 @@ function with your new elements.
It also show you the current subscription plan It also show you the current subscription plan
*/ */
import { useEffect, useState } from "react"; import { useEffect, useState } from 'react';
import Avatar from "./Avatar"; import Image from 'next/image';
import Image from "next/image"; import Plan from 'public/plan.svg';
import PaymentModal from "./PaymentModal"; import { PriceIds } from 'utils/priceList';
import Plan from "public/plan.svg"; import { toast } from 'react-toastify';
import { PriceIds } from "utils/priceList"; import { useRouter } from 'next/router';
import { supabase } from "../utils/supabaseClient"; import { supabase } from '../utils/supabaseClient';
import { toast } from "react-toastify"; import PaymentModal from './PaymentModal';
import { useRouter } from "next/router"; import Avatar from './Avatar';
export default function Dashboard(props) { export default function Dashboard(props) {
const router = useRouter(); const router = useRouter();
@@ -25,7 +25,7 @@ export default function Dashboard(props) {
const [payment, setPayment] = useState(false); const [payment, setPayment] = useState(false);
useEffect(() => { useEffect(() => {
if (router.query.session_id && router.query.session_id !== "canceled") { if (router.query.session_id && router.query.session_id !== 'canceled') {
setPayment(true); setPayment(true);
} }
}, []); }, []);
@@ -43,8 +43,8 @@ export default function Dashboard(props) {
updated_at: new Date(), updated_at: new Date(),
}; };
let { error } = await supabase.from("profiles").upsert(updates, { const { error } = await supabase.from('profiles').upsert(updates, {
returning: "minimal", // Don't return the value after inserting returning: 'minimal', // Don't return the value after inserting
}); });
if (error) { if (error) {
@@ -54,14 +54,14 @@ export default function Dashboard(props) {
alert(error.message); alert(error.message);
} finally { } finally {
setLoading(false); setLoading(false);
toast.success("Your profile has been updated"); toast.success('Your profile has been updated');
} }
} }
return ( return (
<div className='flex flex-col text-left w-full max-w-xl m-auto px-5 py-10'> <div className="flex flex-col text-left w-full max-w-xl m-auto px-5 py-10">
<div className='max-w-sm flex flex-col justify-center m-auto w-full p-5'> <div className="max-w-sm flex flex-col justify-center m-auto w-full p-5">
<h1 className='text-4xl font-bold md:text-5xl font-title text-center mb-10'> <h1 className="text-4xl font-bold md:text-5xl font-title text-center mb-10">
Dashboard Dashboard
</h1> </h1>
<Avatar <Avatar
@@ -72,60 +72,59 @@ export default function Dashboard(props) {
updateProfile({ username, website, avatar_url: url }); updateProfile({ username, website, avatar_url: url });
}} }}
/> />
<div className='mb-5 flex flex-col'> <div className="mb-5 flex flex-col">
<label htmlFor='email' className='my-auto text-sm mb-2'> <label htmlFor="email" className="my-auto text-sm mb-2">
Email Email
</label> </label>
<input <input
className='input input-primary input-bordered input-sm flex-1 text-base-100' className="input input-primary input-bordered input-sm flex-1 text-base-100"
id='email' id="email"
type='text' type="text"
value={props.session.user.email} value={props.session.user.email}
disabled disabled
/> />
</div> </div>
<div className='mb-5 flex flex-col'> <div className="mb-5 flex flex-col">
<label htmlFor='username' className='my-auto text-sm mb-2'> <label htmlFor="username" className="my-auto text-sm mb-2">
Name Name
</label> </label>
<input <input
className='input input-primary input-bordered input-sm flex-1' className="input input-primary input-bordered input-sm flex-1"
id='username' id="username"
type='text' type="text"
value={username || ""} value={username || ''}
onChange={(e) => setUsername(e.target.value)} onChange={(e) => setUsername(e.target.value)}
/> />
</div> </div>
<div className='mb-5 flex flex-col'> <div className="mb-5 flex flex-col">
<label htmlFor='website' className='my-auto text-sm mb-2'> <label htmlFor="website" className="my-auto text-sm mb-2">
Website Website
</label> </label>
<input <input
className='input input-primary input-bordered input-sm flex-1' className="input input-primary input-bordered input-sm flex-1"
id='website' id="website"
type='website' type="website"
value={website || ""} value={website || ''}
onChange={(e) => setWebsite(e.target.value)} onChange={(e) => setWebsite(e.target.value)}
/> />
</div> </div>
<div className='m-auto'> <div className="m-auto">
<button <button
className='btn btn-primary btn-sm' className="btn btn-primary btn-sm"
onClick={() => updateProfile({ username, website, avatar_url })} onClick={() => updateProfile({ username, website, avatar_url })}
disabled={loading}> disabled={loading}
{loading ? "Loading ..." : "Update My Profile"} >
{loading ? 'Loading ...' : 'Update My Profile'}
</button> </button>
</div> </div>
</div> </div>
<div className='max-w-xl flex flex-row flex-wrap m-auto w-full p-5 bordered border-2 border-primary shadow-lg my-5'> <div className="max-w-xl flex flex-row flex-wrap m-auto w-full p-5 bordered border-2 border-primary shadow-lg my-5">
<Image src={Plan} /> <Image src={Plan} />
<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=''> <p className="">{props.plan ? PriceIds[props.plan] : 'Free tier'}</p>
{props.plan ? PriceIds[props.plan] : "Free tier"}
</p>
</div> </div>
</div> </div>
<PaymentModal open={payment} setPayment={setPayment} /> <PaymentModal open={payment} setPayment={setPayment} />

View File

@@ -1,24 +1,24 @@
import Link from "next/link"; import Link from 'next/link';
import dynamic from "next/dynamic"; import dynamic from 'next/dynamic';
const Footer = () => { const Footer = () => {
const ThemeToggle = dynamic(() => import("components/UI/ThemeToggle.js"), { const ThemeToggle = dynamic(() => import('components/UI/ThemeToggle.js'), {
ssr: false, ssr: false,
}); });
return ( return (
<footer className='w-full flex'> <footer className="w-full flex">
<nav className=' mr-auto'> <nav className=" mr-auto">
<div className='flex flex-col sm:flex-row justify-evenly w-full sm:space-x-10'> <div className="flex flex-col sm:flex-row justify-evenly w-full sm:space-x-10">
<div className=''>© {process.env.NEXT_PUBLIC_TITLE}</div> <div className="">© {process.env.NEXT_PUBLIC_TITLE}</div>
<Link href='/privacy'> <Link href="/privacy">
<a>Privacy Policy</a> <a>Privacy Policy</a>
</Link> </Link>
<Link href='/terms'> <Link href="/terms">
<a>Terms of service</a> <a>Terms of service</a>
</Link> </Link>
</div> </div>
</nav> </nav>
<div className='mr-5 my-auto'> <div className="mr-5 my-auto">
<ThemeToggle /> <ThemeToggle />
</div> </div>
</footer> </footer>

View File

@@ -1,48 +1,47 @@
import CardsLanding from "components/CardsLanding"; import CardsLanding from 'components/CardsLanding';
import Image from "next/image"; import Image from 'next/image';
import MailingList from "./MailingList"; import landTop from 'public/landing/land-top.svg';
import landTop from "public/landing/land-top.svg"; import start from 'public/landing/start.svg';
import start from "public/landing/start.svg"; import supabaseImage from 'public/landing/supabase.svg';
import supabaseImage from "public/landing/supabase.svg"; import MailingList from './MailingList';
const Landing = () => { const Landing = () => (
return ( <div className="mt-10 mb-20 text-base-content w-full">
<div className='mt-10 mb-20 text-base-content w-full'> <div className="flex max-w-6xl m-auto justify-around">
<div className='flex max-w-6xl m-auto justify-around'> <div className="max-w-sm mr-16 my-auto">
<div className='max-w-sm mr-16 my-auto'> <h2 className="text-4xl font-bold font-title text-left leading-normal">
<h2 className='text-4xl font-bold font-title text-left leading-normal'> Build your <span className="text-primary">SaaS</span> in the blink of
Build your <span className='text-primary'>SaaS</span> in the blink an eye!
of an eye!
</h2> </h2>
<p> <p>
SupaNexTail got your back, and takes care of the initial setup, SupaNexTail got your back, and takes care of the initial setup,
sometimes time consuming, but essential to your success. sometimes time consuming, but essential to your success.
</p> </p>
</div> </div>
<div className='max-w-xl'> <div className="max-w-xl">
<Image src={landTop} height={417} width={583} /> <Image src={landTop} height={417} width={583} />
</div> </div>
</div> </div>
<CardsLanding /> <CardsLanding />
<div className='flex max-w-6xl m-auto justify-around mt-14 flex-wrap'> <div className="flex max-w-6xl m-auto justify-around mt-14 flex-wrap">
<div className='max-w-sm mr-16 my-auto'> <div className="max-w-sm mr-16 my-auto">
<h2 className='text-4xl font-bold font-title text-left leading-normal'> <h2 className="text-4xl font-bold font-title text-left leading-normal">
All you need to start <span className='text-primary'>now</span> All you need to start <span className="text-primary">now</span>
</h2> </h2>
<p> <p>
SupaNexTail got your back, and takes care of the initial setup, SupaNexTail got your back, and takes care of the initial setup,
sometimes time consuming, but essential to your success. sometimes time consuming, but essential to your success.
</p> </p>
</div> </div>
<div className='max-w-xl'> <div className="max-w-xl">
<Image src={start} /> <Image src={start} />
</div> </div>
</div> </div>
<div className='flex max-w-6xl m-auto justify-around mt-24 flex-wrap'> <div className="flex max-w-6xl m-auto justify-around mt-24 flex-wrap">
<div className='max-w-md my-auto order-1 lg:order-2'> <div className="max-w-md my-auto order-1 lg:order-2">
<h2 className='text-4xl font-bold font-title text-left leading-normal'> <h2 className="text-4xl font-bold font-title text-left leading-normal">
Leverage the power of <span className='text-primary'>Supabase</span> Leverage the power of <span className="text-primary">Supabase</span>
</h2> </h2>
<p> <p>
Supabase is an open source Firebase alternative. Youll have a Supabase is an open source Firebase alternative. Youll have a
@@ -51,17 +50,16 @@ const Landing = () => {
</p> </p>
<p> <p>
SupaNexTail uses Supabase at its core, and preconfigures all the SupaNexTail uses Supabase at its core, and preconfigures all the
useful elements for your site. User registration, synchronization useful elements for your site. User registration, synchronization with
with Stripe, weve got you covered! Stripe, weve got you covered!
</p> </p>
</div> </div>
<div className='max-w-md order-2 lg:order-1 flex'> <div className="max-w-md order-2 lg:order-1 flex">
<Image src={supabaseImage} /> <Image src={supabaseImage} />
</div> </div>
</div> </div>
<MailingList /> <MailingList />
</div> </div>
); );
};
export default Landing; export default Landing;

View File

@@ -10,59 +10,60 @@ You also have the head component containing all the favicon for different platfo
The images are in the public folder. The images are in the public folder.
*/ */
import "react-toastify/dist/ReactToastify.css"; import 'react-toastify/dist/ReactToastify.css';
import Footer from "./Footer"; import Head from 'next/head';
import Head from "next/head"; import { ToastContainer } from 'react-toastify';
import Nav from "./Nav"; import { useAuth } from 'utils/AuthContext';
import { ToastContainer } from "react-toastify"; import Nav from './Nav';
import { useAuth } from "utils/AuthContext"; import Footer from './Footer';
const Layout = (props) => { const Layout = (props) => {
const { user, signOut } = useAuth(); const { user, signOut } = useAuth();
const toastStyle = { const toastStyle = {
// Style your toast elements here // Style your toast elements here
success: "bg-accent", success: 'bg-accent',
error: "bg-red-600", error: 'bg-red-600',
info: "bg-gray-600", info: 'bg-gray-600',
warning: "bg-orange-400", warning: 'bg-orange-400',
default: "bg-primary", default: 'bg-primary',
dark: "bg-white-600 font-gray-300", dark: 'bg-white-600 font-gray-300',
}; };
return ( return (
<div className='min-h-screen w-full bg-base-100 text-base-content m-auto font-body'> <div className="min-h-screen w-full bg-base-100 text-base-content m-auto font-body">
<Head> <Head>
<link <link
rel='apple-touch-icon' rel="apple-touch-icon"
sizes='180x180' sizes="180x180"
href='/apple-touch-icon.png' href="/apple-touch-icon.png"
/> />
<link <link
rel='icon' rel="icon"
type='image/png' type="image/png"
sizes='32x32' sizes="32x32"
href='/favicon-32x32.png' href="/favicon-32x32.png"
/> />
<link <link
rel='icon' rel="icon"
type='image/png' type="image/png"
sizes='16x16' sizes="16x16"
href='/favicon-16x16.png' href="/favicon-16x16.png"
/> />
<link rel='manifest' href='/site.webmanifest' /> <link rel="manifest" href="/site.webmanifest" />
<link rel='mask-icon' href='/safari-pinned-tab.svg' color='#5bbad5' /> <link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5" />
<meta name='msapplication-TileColor' content='#da532c' /> <meta name="msapplication-TileColor" content="#da532c" />
<meta name='theme-color' content='#ffffff' /> <meta name="theme-color" content="#ffffff" />
</Head> </Head>
<div className='max-w-7xl flex flex-col min-h-screen mx-auto p-5'> <div className="max-w-7xl flex flex-col min-h-screen mx-auto p-5">
<Nav user={user} signOut={signOut} /> <Nav user={user} signOut={signOut} />
<main className='flex-1'>{props.children}</main> <main className="flex-1">{props.children}</main>
<ToastContainer <ToastContainer
position='bottom-center' position="bottom-center"
toastClassName={({ type }) => toastClassName={({ type }) =>
toastStyle[type || "default"] + `${
" flex p-5 my-5 min-h-10 rounded-md justify-between overflow-hidden cursor-pointer " toastStyle[type || 'default']
} flex p-5 my-5 min-h-10 rounded-md justify-between overflow-hidden cursor-pointer `
} }
/> />
<Footer /> <Footer />

View File

@@ -3,11 +3,11 @@ This is the form component to register an email adress to your mailing list.
This is just the frontend, and the email will be send to our backend API (/api/mailingList) This is just the frontend, and the email will be send to our backend API (/api/mailingList)
*/ */
import Image from "next/image"; import Image from 'next/image';
import Mailing from "public/landing/mailing.svg"; import Mailing from 'public/landing/mailing.svg';
import axios from "axios"; import axios from 'axios';
import { toast } from "react-toastify"; import { toast } from 'react-toastify';
import { useState } from "react"; import { useState } from 'react';
const MailingList = () => { const MailingList = () => {
const [mail, setMail] = useState(null); const [mail, setMail] = useState(null);
@@ -16,7 +16,7 @@ const MailingList = () => {
const validateEmail = () => { const validateEmail = () => {
// Regex patern for email validation // Regex patern for email validation
let 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@"]+)*)|(".+"))@((\[[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,}))$/;
if (regex.test(mail)) { if (regex.test(mail)) {
@@ -25,7 +25,7 @@ const MailingList = () => {
setValid(true); setValid(true);
} else { } else {
// invalid email. // invalid email.
toast.error("Your email is invalid"); toast.error('Your email is invalid');
setValid(false); setValid(false);
} }
}; };
@@ -33,7 +33,7 @@ const MailingList = () => {
const subscribe = () => { const subscribe = () => {
setLoading(true); setLoading(true);
axios axios
.put("api/mailingList", { .put('api/mailingList', {
mail, mail,
}) })
.then((result) => { .then((result) => {
@@ -48,32 +48,34 @@ const MailingList = () => {
}); });
}; };
return ( return (
<div className='my-10 mt-24 m-auto flex flex-col'> <div className="my-10 mt-24 m-auto flex flex-col">
<h2 className='text-3xl md:text-4xl font-bold font-title uppercase text-center'> <h2 className="text-3xl md:text-4xl font-bold font-title uppercase text-center">
Stay Tuned Stay Tuned
</h2> </h2>
<Image src={Mailing} /> <Image src={Mailing} />
<label className='label'> <label className="label">
<p className='text-center max-w-md m-auto'> <p className="text-center max-w-md m-auto">
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
exclusive discount? Sign up for the newsletter! exclusive discount? Sign up for the newsletter!
</p> </p>
</label> </label>
<div className='mt-5 m-auto'> <div className="mt-5 m-auto">
<input <input
onChange={(e) => { onChange={(e) => {
setMail(e.target.value); setMail(e.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 ? null : 'input-error'
}`}></input> }`}
/>
<button <button
onClick={validateEmail} onClick={validateEmail}
className={`btn ml-3 ${ className={`btn ml-3 ${
loading ? "btn-disabled loading" : "btn-primary" loading ? 'btn-disabled loading' : 'btn-primary'
}`}> }`}
>
I'm in! I'm in!
</button> </button>
</div> </div>

View File

@@ -2,42 +2,42 @@
This is your Nav component. It contain a responsive navbar This is your Nav component. It contain a responsive navbar
*/ */
import { LogOut, Menu } from "react-feather"; 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';
const Nav = (props) => { const Nav = (props) => {
// Modify you menu directly here // Modify you menu directly here
const NavMenu = ( const NavMenu = (
<> <>
{props.user && ( {props.user && (
<Link href='/dashboard'> <Link href="/dashboard">
<a className='nav-btn'>Dashboard</a> <a className="nav-btn">Dashboard</a>
</Link> </Link>
)} )}
<Link href='/pricing'> <Link href="/pricing">
<a className='nav-btn'>Pricing</a> <a className="nav-btn">Pricing</a>
</Link> </Link>
<Link href='/contact'> <Link href="/contact">
<a className='nav-btn'>Contact Us</a> <a className="nav-btn">Contact Us</a>
</Link> </Link>
{props.user ? ( {props.user ? (
<button className='btn btn-xs text-xs' onClick={() => props.signOut()}> <button className="btn btn-xs text-xs" onClick={() => props.signOut()}>
<LogOut size={12} className='mr-2' /> <LogOut size={12} className="mr-2" />
Logout Logout
</button> </button>
) : ( ) : (
<> <>
<Link href='/login'> <Link href="/login">
<a className='nav-btn'>Login</a> <a className="nav-btn">Login</a>
</Link> </Link>
<Link href='/signup'> <Link href="/signup">
<a className='btn btn-sm btn-primary font-body normal-case font-normal'> <a className="btn btn-sm btn-primary font-body normal-case font-normal">
Sign Up Sign Up
</a> </a>
</Link> </Link>
@@ -47,22 +47,22 @@ const Nav = (props) => {
); );
return ( return (
<nav className='navbar mb-2 w-full'> <nav className="navbar mb-2 w-full">
<Link href='/'> <Link href="/">
<a> <a>
<Image src={Logo} /> <Image src={Logo} />
</a> </a>
</Link> </Link>
<div className='hidden lg:flex text-center flex-col lg:flex-row lg:space-x-10 font-body text-sm ml-auto'> <div className="hidden lg:flex text-center flex-col lg:flex-row lg:space-x-10 font-body text-sm ml-auto">
{NavMenu} {NavMenu}
</div> </div>
<div className='ml-auto lg:hidden'> <div className="ml-auto lg:hidden">
<div className='dropdown dropdown-end' data-cy='dropdown'> <div className="dropdown dropdown-end" data-cy="dropdown">
<div tabIndex='0' className='m-1 cursor-pointer'> <div tabIndex="0" className="m-1 cursor-pointer">
<Menu /> <Menu />
</div> </div>
<div className='menu dropdown-content mt-3 text-center space-y-3 w-24'> <div className="menu dropdown-content mt-3 text-center space-y-3 w-24">
{NavMenu} {NavMenu}
</div> </div>
</div> </div>

View File

@@ -1,6 +1,6 @@
import { Dialog, Transition } from "@headlessui/react"; import { Dialog, Transition } from '@headlessui/react';
import { Fragment } from "react"; import { Fragment } from 'react';
const PaymentModal = (props) => { const PaymentModal = (props) => {
function closeModal() { function closeModal() {
@@ -11,53 +11,59 @@ const PaymentModal = (props) => {
<> <>
<Transition appear show={props.open} as={Fragment}> <Transition appear show={props.open} as={Fragment}>
<Dialog <Dialog
as='div' as="div"
className='fixed inset-0 z-10 overflow-y-auto bg-gray-500 bg-opacity-50' className="fixed inset-0 z-10 overflow-y-auto bg-gray-500 bg-opacity-50"
onClose={closeModal}> onClose={closeModal}
<div className='min-h-screen px-4 text-center'> >
<div className="min-h-screen px-4 text-center">
<Transition.Child <Transition.Child
as={Fragment} as={Fragment}
enter='ease-out duration-300' enter="ease-out duration-300"
enterFrom='opacity-0' enterFrom="opacity-0"
enterTo='opacity-100' enterTo="opacity-100"
leave='ease-in duration-200' leave="ease-in duration-200"
leaveFrom='opacity-100' leaveFrom="opacity-100"
leaveTo='opacity-0'> leaveTo="opacity-0"
<Dialog.Overlay className='fixed inset-0' /> >
<Dialog.Overlay className="fixed inset-0" />
</Transition.Child> </Transition.Child>
{/* This element is to trick the browser into centering the modal contents. */} {/* This element is to trick the browser into centering the modal contents. */}
<span <span
className='inline-block h-screen align-middle' className="inline-block h-screen align-middle"
aria-hidden='true'> aria-hidden="true"
>
&#8203; &#8203;
</span> </span>
<Transition.Child <Transition.Child
as={Fragment} as={Fragment}
enter='ease-out duration-300' enter="ease-out duration-300"
enterFrom='opacity-0 scale-95' enterFrom="opacity-0 scale-95"
enterTo='opacity-100 scale-100' enterTo="opacity-100 scale-100"
leave='ease-in duration-200' leave="ease-in duration-200"
leaveFrom='opacity-100 scale-100' leaveFrom="opacity-100 scale-100"
leaveTo='opacity-0 scale-95'> leaveTo="opacity-0 scale-95"
<div className='inline-block w-full max-w-lg p-8 my-8 overflow-hidden text-left align-middle transition-all transform shadow-xl rounded-2xl bg-base-100 text-base-content border-2 border-accent-focus'> >
<div className="inline-block w-full max-w-lg p-8 my-8 overflow-hidden text-left align-middle transition-all transform shadow-xl rounded-2xl bg-base-100 text-base-content border-2 border-accent-focus">
<Dialog.Title <Dialog.Title
as='h3' as="h3"
className='text-2xl font-bold leading-6 mb-5 text-center'> className="text-2xl font-bold leading-6 mb-5 text-center"
>
Payment successful 🎉 Payment successful 🎉
</Dialog.Title> </Dialog.Title>
<div className='mt-2'> <div className="mt-2">
<p> <p>
Your payment has been successfully submitted. Thank you for Your payment has been successfully submitted. Thank you for
your support! your support!
</p> </p>
</div> </div>
<div className='mt-4'> <div className="mt-4">
<button <button
type='button' type="button"
className='btn btn-accent flex m-auto' className="btn btn-accent flex m-auto"
onClick={closeModal}> onClick={closeModal}
>
Got it, thanks! Got it, thanks!
</button> </button>
</div> </div>

View File

@@ -6,14 +6,14 @@ Dont forget to create your customer portal on Stripe
https://dashboard.stripe.com/test/settings/billing/portal https://dashboard.stripe.com/test/settings/billing/portal
*/ */
import { getSub, supabase } from "utils/supabaseClient"; import { getSub, supabase } from 'utils/supabaseClient';
import { useEffect, useState } from "react"; import { useEffect, useState } from 'react';
import { Prices } from "utils/priceList"; import { Prices } from 'utils/priceList';
import { Switch } from "@headlessui/react"; import { Switch } from '@headlessui/react';
import axios from "axios"; import axios from 'axios';
import router from "next/router"; import router from 'next/router';
import { useAuth } from "utils/AuthContext"; import { useAuth } from 'utils/AuthContext';
const Pricing = () => { const Pricing = () => {
const [enabled, setEnabled] = useState(false); const [enabled, setEnabled] = useState(false);
@@ -24,8 +24,8 @@ const Pricing = () => {
const portal = () => { const portal = () => {
axios axios
.post("/api/stripe/customer-portal", { .post('/api/stripe/customer-portal', {
customerId: customerId, customerId,
}) })
.then((result) => { .then((result) => {
router.push(result.data.url); router.push(result.data.url);
@@ -36,9 +36,9 @@ const Pricing = () => {
if (user) { if (user) {
getSub().then((result) => setSub(result)); getSub().then((result) => setSub(result));
supabase supabase
.from("subscriptions") .from('subscriptions')
.select(`customer_id`) .select(`customer_id`)
.eq("id", user.id) .eq('id', user.id)
.single() .single()
.then((result) => { .then((result) => {
setCustomerId(result.data?.customer_id); setCustomerId(result.data?.customer_id);
@@ -48,19 +48,19 @@ const Pricing = () => {
const pricing = { const pricing = {
monthly: { monthly: {
personal: "$5/mo", personal: '$5/mo',
team: "$15/mo", team: '$15/mo',
pro: "$35/mo", pro: '$35/mo',
}, },
yearly: { yearly: {
personal: "$50/yr", personal: '$50/yr',
team: "$150/yr", team: '$150/yr',
pro: "$350/yr", pro: '$350/yr',
}, },
flat: { flat: {
personal: "€49", personal: '€49',
team: "€99", team: '€99',
pro: "€149", pro: '€149',
}, },
}; };
@@ -68,31 +68,31 @@ const Pricing = () => {
e.preventDefault(); e.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.
axios axios
.post("/api/stripe/create-checkout-session", { .post('/api/stripe/create-checkout-session', {
priceId: priceId, priceId,
email: user.email, email: user.email,
customerId: customerId, customerId,
userId: user.id, userId: user.id,
tokenId: session.access_token, tokenId: session.access_token,
pay_mode: flat ? "payment" : "subscription", pay_mode: flat ? 'payment' : 'subscription',
}) })
.then((result) => router.push(result.data.url)); .then((result) => router.push(result.data.url));
}; };
return ( return (
<div className='w-full mx-auto px-5 py-10 mb-10'> <div className="w-full mx-auto px-5 py-10 mb-10">
<div className='text-center max-w-xl mx-auto'> <div className="text-center max-w-xl mx-auto">
<h1 className='text-3xl sm:text-5xl font-bold font-title mb-5'> <h1 className="text-3xl sm:text-5xl font-bold font-title mb-5">
Pricing Pricing
</h1> </h1>
<h3 className='text-lg font-light leading-8 p-3 mb-5'> <h3 className="text-lg font-light leading-8 p-3 mb-5">
This is an example of a pricing page. You can choose a payment method, This is an example of a pricing page. You can choose a payment method,
monthly or yearly. monthly or yearly.
</h3> </h3>
</div> </div>
{!flat && ( {!flat && (
<div className='flex justify-between max-w-xs m-auto mb-3'> <div className="flex justify-between max-w-xs m-auto mb-3">
<div> <div>
<p className={`${enabled ? "text-gray-500" : null}`}> <p className={`${enabled ? 'text-gray-500' : null}`}>
Billed monthly Billed monthly
</p> </p>
</div> </div>
@@ -103,45 +103,46 @@ const Pricing = () => {
className={`bg-primary relative inline-flex flex-shrink-0 h-[38px] w-[74px] className={`bg-primary relative inline-flex flex-shrink-0 h-[38px] w-[74px]
border-2 border-transparent rounded-full cursor-pointer transition-colors border-2 border-transparent rounded-full cursor-pointer transition-colors
ease-in-out duration-200 focus:outline-none focus-visible:ring-2 ease-in-out duration-200 focus:outline-none focus-visible:ring-2
focus-visible:ring-white focus-visible:ring-opacity-75`}> focus-visible:ring-white focus-visible:ring-opacity-75`}
<span className='sr-only'>Switch bill</span> >
<span className="sr-only">Switch bill</span>
<span <span
aria-hidden='true' aria-hidden="true"
className={`${enabled ? "translate-x-9" : "translate-x-0"} className={`${enabled ? 'translate-x-9' : 'translate-x-0'}
pointer-events-none inline-block h-[34px] w-[34px] rounded-full bg-white shadow-lg transform ring-0 transition ease-in-out duration-200`} pointer-events-none inline-block h-[34px] w-[34px] rounded-full bg-white shadow-lg transform ring-0 transition ease-in-out duration-200`}
/> />
</Switch> </Switch>
</div> </div>
<div> <div>
<p className={`${!enabled ? "text-gray-500" : null}`}> <p className={`${!enabled ? 'text-gray-500' : null}`}>
Billed annually Billed annually
</p> </p>
</div> </div>
</div> </div>
)} )}
<div className='max-w-4xl mx-auto md:flex space-x-4'> <div className="max-w-4xl mx-auto md:flex space-x-4">
<div className='w-full md:w-1/3 md:max-w-none px-8 md:px-10 py-8 md:py-10 mb-3 mx-auto md:my-6 rounded-md shadow-lg shadow-gray-600 md:flex md:flex-col'> <div className="w-full md:w-1/3 md:max-w-none px-8 md:px-10 py-8 md:py-10 mb-3 mx-auto md:my-6 rounded-md shadow-lg shadow-gray-600 md:flex md:flex-col">
<div className='w-full flex-grow'> <div className="w-full flex-grow">
<h2 className='text-center font-bold uppercase mb-4'>Personal</h2> <h2 className="text-center font-bold uppercase mb-4">Personal</h2>
<h3 className='text-center font-bold text-4xl mb-5'> <h3 className="text-center font-bold text-4xl mb-5">
{flat {flat
? pricing.flat.personal ? pricing.flat.personal
: enabled : enabled
? pricing.yearly.personal ? pricing.yearly.personal
: pricing.monthly.personal} : pricing.monthly.personal}
</h3> </h3>
<ul className='text-sm px-5 mb-8 text-left'> <ul className="text-sm px-5 mb-8 text-left">
<li className='leading-tight'> <li className="leading-tight">
<i className='mdi mdi-check-bold text-lg'></i> A cool feature <i className="mdi mdi-check-bold text-lg" /> A cool feature
</li> </li>
<li className='leading-tight'> <li className="leading-tight">
<i className='mdi mdi-check-bold text-lg'></i> Another feature <i className="mdi mdi-check-bold text-lg" /> Another feature
</li> </li>
</ul> </ul>
</div> </div>
<div className='w-full'> <div className="w-full">
<button <button
className='btn btn-primary w-full' className="btn btn-primary w-full"
onClick={ onClick={
user user
? sub ? sub
@@ -156,45 +157,45 @@ const Pricing = () => {
: Prices.personal.monthly.id : Prices.personal.monthly.id
) )
: () => { : () => {
router.push("/auth"); router.push('/auth');
} }
}> }
{user ? (sub ? "Upgrade" : "Buy Now") : "Register"} >
{user ? (sub ? 'Upgrade' : 'Buy Now') : 'Register'}
</button> </button>
</div> </div>
</div> </div>
<div className='w-full md:w-1/3 md:max-w-none px-8 md:px-10 py-8 md:py-10 mb-3 mx-auto md:my-6 rounded-md shadow-lg shadow-gray-600 md:flex md:flex-col'> <div className="w-full md:w-1/3 md:max-w-none px-8 md:px-10 py-8 md:py-10 mb-3 mx-auto md:my-6 rounded-md shadow-lg shadow-gray-600 md:flex md:flex-col">
<div className='w-full flex-grow'> <div className="w-full flex-grow">
<h2 className='text-center font-bold uppercase mb-4'>Team</h2> <h2 className="text-center font-bold uppercase mb-4">Team</h2>
<h3 className='text-center font-bold text-4xl mb-5'> <h3 className="text-center font-bold text-4xl mb-5">
{flat {flat
? pricing.flat.team ? pricing.flat.team
: enabled : enabled
? pricing.yearly.team ? pricing.yearly.team
: pricing.monthly.team} : pricing.monthly.team}
</h3> </h3>
<ul className='text-sm px-5 mb-8 text-left'> <ul className="text-sm px-5 mb-8 text-left">
<li className='leading-tight'> <li className="leading-tight">
<i className='mdi mdi-check-bold text-lg'></i> All basic <i className="mdi mdi-check-bold text-lg" /> All basic features
features
</li> </li>
<li className='leading-tight'> <li className="leading-tight">
<i className='mdi mdi-check-bold text-lg'></i> Dolor sit amet <i className="mdi mdi-check-bold text-lg" /> Dolor sit amet
</li> </li>
<li className='leading-tight'> <li className="leading-tight">
<i className='mdi mdi-check-bold text-lg'></i> Consectetur <i className="mdi mdi-check-bold text-lg" /> Consectetur
</li> </li>
<li className='leading-tight'> <li className="leading-tight">
<i className='mdi mdi-check-bold text-lg'></i> Adipisicing <i className="mdi mdi-check-bold text-lg" /> Adipisicing
</li> </li>
<li className='leading-tight'> <li className="leading-tight">
<i className='mdi mdi-check-bold text-lg'></i> Elit repellat <i className="mdi mdi-check-bold text-lg" /> Elit repellat
</li> </li>
</ul> </ul>
</div> </div>
<div className='w-full'> <div className="w-full">
<button <button
className='btn btn-primary w-full' className="btn btn-primary w-full"
onClick={ onClick={
user user
? sub ? sub
@@ -209,44 +210,45 @@ const Pricing = () => {
: Prices.team.monthly.id : Prices.team.monthly.id
) )
: () => { : () => {
router.push("/auth"); router.push('/auth');
} }
}> }
{user ? (sub ? "Upgrade" : "Buy Now") : "Register"} >
{user ? (sub ? 'Upgrade' : 'Buy Now') : 'Register'}
</button> </button>
</div> </div>
</div> </div>
<div className='w-full md:w-1/3 md:max-w-none px-8 md:px-10 py-8 md:py-10 mb-3 mx-auto md:my-6 rounded-md shadow-lg shadow-gray-600 md:flex md:flex-col'> <div className="w-full md:w-1/3 md:max-w-none px-8 md:px-10 py-8 md:py-10 mb-3 mx-auto md:my-6 rounded-md shadow-lg shadow-gray-600 md:flex md:flex-col">
<div className='w-full flex-grow'> <div className="w-full flex-grow">
<h2 className='text-center font-bold uppercase mb-4'>Pro</h2> <h2 className="text-center font-bold uppercase mb-4">Pro</h2>
<h3 className='text-center font-bold text-4xl mb-5'> <h3 className="text-center font-bold text-4xl mb-5">
{flat {flat
? pricing.flat.pro ? pricing.flat.pro
: enabled : enabled
? pricing.yearly.pro ? pricing.yearly.pro
: pricing.monthly.pro} : pricing.monthly.pro}
</h3> </h3>
<ul className='text-sm px-5 mb-8 text-left'> <ul className="text-sm px-5 mb-8 text-left">
<li className='leading-tight'> <li className="leading-tight">
<i className='mdi mdi-check-bold text-lg'></i> Lorem ipsum <i className="mdi mdi-check-bold text-lg" /> Lorem ipsum
</li> </li>
<li className='leading-tight'> <li className="leading-tight">
<i className='mdi mdi-check-bold text-lg'></i> Dolor sit amet <i className="mdi mdi-check-bold text-lg" /> Dolor sit amet
</li> </li>
<li className='leading-tight'> <li className="leading-tight">
<i className='mdi mdi-check-bold text-lg'></i> Consectetur <i className="mdi mdi-check-bold text-lg" /> Consectetur
</li> </li>
<li className='leading-tight'> <li className="leading-tight">
<i className='mdi mdi-check-bold text-lg'></i> Adipisicing <i className="mdi mdi-check-bold text-lg" /> Adipisicing
</li> </li>
<li className='leading-tight'> <li className="leading-tight">
<i className='mdi mdi-check-bold text-lg'></i> Much more... <i className="mdi mdi-check-bold text-lg" /> Much more...
</li> </li>
</ul> </ul>
</div> </div>
<div className='w-full'> <div className="w-full">
<button <button
className='btn btn-primary w-full' className="btn btn-primary w-full"
onClick={ onClick={
user user
? sub ? sub
@@ -261,10 +263,11 @@ const Pricing = () => {
: Prices.pro.monthly.id : Prices.pro.monthly.id
) )
: () => { : () => {
router.push("/auth"); router.push('/auth');
} }
}> }
{user ? (sub ? "Upgrade" : "Buy Now") : "Register"} >
{user ? (sub ? 'Upgrade' : 'Buy Now') : 'Register'}
</button> </button>
</div> </div>
</div> </div>

View File

@@ -1,15 +1,14 @@
const PrivacyPolicy = () => { const PrivacyPolicy = () => (
return ( <div className="max-w-xl text-left m-auto py-10">
<div className='max-w-xl text-left m-auto py-10'> <h1 className="text-center">
<h1 className='text-center'>
Privacy Policy for {process.env.NEXT_PUBLIC_TITLE} Privacy Policy for {process.env.NEXT_PUBLIC_TITLE}
</h1> </h1>
<p> <p>
At {process.env.NEXT_PUBLIC_TITLE}, accessible from At {process.env.NEXT_PUBLIC_TITLE}, accessible from
https://www.supanextail.dev, one of our main priorities is the privacy https://www.supanextail.dev, one of our main priorities is the privacy of
of our visitors. This Privacy Policy document contains types of our visitors. This Privacy Policy document contains types of information
information that is collected and recorded by that is collected and recorded by
{process.env.NEXT_PUBLIC_TITLE} and how we use it. {process.env.NEXT_PUBLIC_TITLE} and how we use it.
</p> </p>
@@ -35,18 +34,18 @@ const PrivacyPolicy = () => {
You have given {process.env.NEXT_PUBLIC_TITLE} permission to do so You have given {process.env.NEXT_PUBLIC_TITLE} permission to do so
</li> </li>
<li> <li>
Processing your personal information is in{" "} Processing your personal information is in{' '}
{process.env.NEXT_PUBLIC_TITLE} legitimate interests {process.env.NEXT_PUBLIC_TITLE} legitimate interests
</li> </li>
<li>{process.env.NEXT_PUBLIC_TITLE} needs to comply with the law</li> <li>{process.env.NEXT_PUBLIC_TITLE} needs to comply with the law</li>
</ul> </ul>
<p> <p>
{process.env.NEXT_PUBLIC_TITLE} will retain your personal information {process.env.NEXT_PUBLIC_TITLE} will retain your personal information only
only for as long as is necessary for the purposes set out in this for as long as is necessary for the purposes set out in this Privacy
Privacy Policy. We will retain and use your information to the extent Policy. We will retain and use your information to the extent necessary to
necessary to comply with our legal obligations, resolve disputes, and comply with our legal obligations, resolve disputes, and enforce our
enforce our policies. policies.
</p> </p>
<p> <p>
@@ -60,8 +59,7 @@ const PrivacyPolicy = () => {
</p> </p>
<ul> <ul>
<li> <li>
The right to access, update or to delete the information we have on The right to access, update or to delete the information we have on you.
you.
</li> </li>
<li>The right of rectification.</li> <li>The right of rectification.</li>
<li>The right to object.</li> <li>The right to object.</li>
@@ -73,15 +71,15 @@ const PrivacyPolicy = () => {
<h2>Log Files</h2> <h2>Log Files</h2>
<p> <p>
{process.env.NEXT_PUBLIC_TITLE} follows a standard procedure of using {process.env.NEXT_PUBLIC_TITLE} follows a standard procedure of using log
log files. These files log visitors when they visit websites. All files. These files log visitors when they visit websites. All hosting
hosting companies do this and a part of hosting services' analytics. The companies do this and a part of hosting services' analytics. The
information collected by log files include internet protocol (IP) information collected by log files include internet protocol (IP)
addresses, browser type, Internet Service Provider (ISP), date and time addresses, browser type, Internet Service Provider (ISP), date and time
stamp, referring/exit pages, and possibly the number of clicks. These stamp, referring/exit pages, and possibly the number of clicks. These are
are not linked to any information that is personally identifiable. The not linked to any information that is personally identifiable. The purpose
purpose of the information is for analyzing trends, administering the of the information is for analyzing trends, administering the site,
site, tracking users' movement on the website, and gathering demographic tracking users' movement on the website, and gathering demographic
information. information.
</p> </p>
@@ -96,15 +94,15 @@ const PrivacyPolicy = () => {
Third-party ad servers or ad networks uses technologies like cookies, Third-party ad servers or ad networks uses technologies like cookies,
JavaScript, or Web Beacons that are used in their respective JavaScript, or Web Beacons that are used in their respective
advertisements and links that appear on {process.env.NEXT_PUBLIC_TITLE}, advertisements and links that appear on {process.env.NEXT_PUBLIC_TITLE},
which are sent directly to users' browser. They automatically receive which are sent directly to users' browser. They automatically receive your
your IP address when this occurs. These technologies are used to measure IP address when this occurs. These technologies are used to measure the
the effectiveness of their advertising campaigns and/or to personalize effectiveness of their advertising campaigns and/or to personalize the
the advertising content that you see on websites that you visit. advertising content that you see on websites that you visit.
</p> </p>
<p> <p>
Note that {process.env.NEXT_PUBLIC_TITLE} has no access to or control Note that {process.env.NEXT_PUBLIC_TITLE} has no access to or control over
over these cookies that are used by third-party advertisers. these cookies that are used by third-party advertisers.
</p> </p>
<h2>Third Party Privacy Policies</h2> <h2>Third Party Privacy Policies</h2>
@@ -114,22 +112,21 @@ const PrivacyPolicy = () => {
advertisers or websites. Thus, we are advising you to consult the advertisers or websites. Thus, we are advising you to consult the
respective Privacy Policies of these third-party ad servers for more respective Privacy Policies of these third-party ad servers for more
detailed information. It may include their practices and instructions detailed information. It may include their practices and instructions
about how to opt-out of certain options.{" "} about how to opt-out of certain options.{' '}
</p> </p>
<p> <p>
You can choose to disable cookies through your individual browser You can choose to disable cookies through your individual browser options.
options. To know more detailed information about cookie management with To know more detailed information about cookie management with specific
specific web browsers, it can be found at the browsers' respective web browsers, it can be found at the browsers' respective websites.
websites.
</p> </p>
<h2>Children's Information</h2> <h2>Children's Information</h2>
<p> <p>
Another part of our priority is adding protection for children while Another part of our priority is adding protection for children while using
using the internet. We encourage parents and guardians to observe, the internet. We encourage parents and guardians to observe, participate
participate in, and/or monitor and guide their online activity. in, and/or monitor and guide their online activity.
</p> </p>
<p> <p>
@@ -143,11 +140,11 @@ const PrivacyPolicy = () => {
<h2>Online Privacy Policy Only</h2> <h2>Online Privacy Policy Only</h2>
<p> <p>
Our Privacy Policy applies only to our online activities and is valid Our Privacy Policy applies only to our online activities and is valid for
for visitors to our website with regards to the information that they visitors to our website with regards to the information that they shared
shared and/or collect in {process.env.NEXT_PUBLIC_TITLE}. This policy is and/or collect in {process.env.NEXT_PUBLIC_TITLE}. This policy is not
not applicable to any information collected offline or via channels applicable to any information collected offline or via channels other than
other than this website. this website.
</p> </p>
<h2>Consent</h2> <h2>Consent</h2>
@@ -158,6 +155,5 @@ const PrivacyPolicy = () => {
</p> </p>
</div> </div>
); );
};
export default PrivacyPolicy; export default PrivacyPolicy;

View File

@@ -7,17 +7,17 @@ You can select your auth providers, or just keep the email/password. You can
check the providers available here: https://supabase.io/docs/guides/auth check the providers available here: https://supabase.io/docs/guides/auth
*/ */
import SignUpPanel from "./UI/SignUpPanel"; import { supabase } from 'utils/supabaseClient';
import { supabase } from "utils/supabaseClient"; import { useAuth } from 'utils/AuthContext';
import { useAuth } from "utils/AuthContext"; import SignUpPanel from './UI/SignUpPanel';
const Container = (props) => { const Container = (props) => {
const { user, signOut } = useAuth(); const { user, signOut } = useAuth();
if (user) if (user)
return ( return (
<div className='w-80 md:w-96 order-first lg:order-last'> <div className="w-80 md:w-96 order-first lg:order-last">
<p>Hello {user.email}! 👋 You are already logged in</p> <p>Hello {user.email}! 👋 You are already logged in</p>
<button className='btn btn-primary' onClick={() => signOut()}> <button className="btn btn-primary" onClick={() => signOut()}>
Sign out Sign out
</button> </button>
</div> </div>

View File

@@ -1,28 +1,27 @@
const Terms = () => { const Terms = () => (
return ( <div className="max-w-xl text-left m-auto py-10">
<div className='max-w-xl text-left m-auto py-10'>
<h1>Terms and Conditions</h1> <h1>Terms and Conditions</h1>
<p> <p>
The following terms and conditions (collectively, these "Terms and The following terms and conditions (collectively, these "Terms and
Conditions") apply to your use of{" "} Conditions") apply to your use of{' '}
<span className='website_url'>https://www.supanextail.dev</span>, <span className="website_url">https://www.supanextail.dev</span>,
including any content, functionality and services offered on or via{" "} including any content, functionality and services offered on or via{' '}
<span className='website_url'>https://www.supanextail.dev</span> (the <span className="website_url">https://www.supanextail.dev</span> (the
"Website"). "Website").
</p> </p>
<p> <p>
Please read the Terms and Conditions carefully before you start using{" "} Please read the Terms and Conditions carefully before you start using{' '}
<span className='website_name'>{process.env.NEXT_PUBLIC_TITLE}</span>, <span className="website_name">{process.env.NEXT_PUBLIC_TITLE}</span>,
because by using the Website you accept and agree to be bound and abide because by using the Website you accept and agree to be bound and abide by
by these Terms and Conditions. these Terms and Conditions.
</p> </p>
<p> <p>
These Terms and Conditions are effective as of{" "} These Terms and Conditions are effective as of{' '}
<span className='date'>06/22/2021</span>. We expressly reserve the right to <span className="date">06/22/2021</span>. We expressly reserve the right
change these Terms and Conditions from time to time without notice to to change these Terms and Conditions from time to time without notice to
you. You acknowledge and agree that it is your responsibility to review you. You acknowledge and agree that it is your responsibility to review
this Website and these Terms and Conditions from time to time and to this Website and these Terms and Conditions from time to time and to
familiarize yourself with any modifications. Your continued use of this familiarize yourself with any modifications. Your continued use of this
@@ -34,9 +33,9 @@ const Terms = () => {
<h2>Conduct on Website</h2> <h2>Conduct on Website</h2>
<p> <p>
Your use of the Website is subject to all applicable laws and Your use of the Website is subject to all applicable laws and regulations,
regulations, and you are solely responsible for the substance of your and you are solely responsible for the substance of your communications
communications through the Website. through the Website.
</p> </p>
<p> <p>
@@ -52,32 +51,32 @@ const Terms = () => {
<ul> <ul>
<li> <li>
Is unlawful, threatening, abusive, harassing, defamatory, libelous, Is unlawful, threatening, abusive, harassing, defamatory, libelous,
deceptive, fraudulent, invasive of another's privacy, tortious, deceptive, fraudulent, invasive of another's privacy, tortious, contains
contains explicit or graphic descriptions or accounts of sexual acts explicit or graphic descriptions or accounts of sexual acts (including
(including but not limited to sexual language of a violent or but not limited to sexual language of a violent or threatening nature
threatening nature directed at another individual or group of directed at another individual or group of individuals), or otherwise
individuals), or otherwise violates our rules or policies violates our rules or policies
</li> </li>
<li> <li>
Victimizes, harasses, degrades, or intimidates an individual or group Victimizes, harasses, degrades, or intimidates an individual or group of
of individuals on the basis of religion, gender, sexual orientation, individuals on the basis of religion, gender, sexual orientation, race,
race, ethnicity, age, or disability ethnicity, age, or disability
</li> </li>
<li> <li>
Infringes on any patent, trademark, trade secret, copyright, right of Infringes on any patent, trademark, trade secret, copyright, right of
publicity, or other proprietary right of any party publicity, or other proprietary right of any party
</li> </li>
<li> <li>
Constitutes unauthorized or unsolicited advertising, junk or bulk Constitutes unauthorized or unsolicited advertising, junk or bulk email
email (also known as "spamming"), chain letters, any other form of (also known as "spamming"), chain letters, any other form of
unauthorized solicitation, or any form of lottery or gambling unauthorized solicitation, or any form of lottery or gambling
</li> </li>
<li> <li>
Contains software viruses or any other computer code, files, or Contains software viruses or any other computer code, files, or programs
programs that are designed or intended to disrupt, damage, or limit that are designed or intended to disrupt, damage, or limit the
the functioning of any software, hardware, or telecommunications functioning of any software, hardware, or telecommunications equipment
equipment or to damage or obtain unauthorized access to any data or or to damage or obtain unauthorized access to any data or other
other information of any third party information of any third party
</li> </li>
<li> <li>
Impersonates any person or entity, including any of our employees or Impersonates any person or entity, including any of our employees or
@@ -88,26 +87,26 @@ const Terms = () => {
<p> <p>
We neither endorse nor assume any liability for the contents of any We neither endorse nor assume any liability for the contents of any
material uploaded or submitted by third party users of the Website. We material uploaded or submitted by third party users of the Website. We
generally do not pre-screen, monitor, or edit the content posted by generally do not pre-screen, monitor, or edit the content posted by users
users of communications services, chat rooms, message boards, of communications services, chat rooms, message boards, newsgroups,
newsgroups, software libraries, or other interactive services that may software libraries, or other interactive services that may be available on
be available on or through this Website. or through this Website.
</p> </p>
<p> <p>
However, we and our agents have the right at their sole discretion to However, we and our agents have the right at their sole discretion to
remove any content that, in our judgment, does not comply with these remove any content that, in our judgment, does not comply with these Terms
Terms of Use and any other rules of user conduct for our Website, or is of Use and any other rules of user conduct for our Website, or is
otherwise harmful, objectionable, or inaccurate. We are not responsible otherwise harmful, objectionable, or inaccurate. We are not responsible
for any failure or delay in removing such content. You hereby consent to for any failure or delay in removing such content. You hereby consent to
such removal and waive any claim against us arising out of such removal such removal and waive any claim against us arising out of such removal of
of content. content.
</p> </p>
<p> <p>
You agree that we may at any time, and at our sole discretion, terminate You agree that we may at any time, and at our sole discretion, terminate
your membership, account, or other affiliation with our site without your membership, account, or other affiliation with our site without prior
prior notice to you for violating any of the above provisions. notice to you for violating any of the above provisions.
</p> </p>
<p> <p>
@@ -123,19 +122,19 @@ const Terms = () => {
By accepting these Terms and Conditions, you acknowledge and agree that By accepting these Terms and Conditions, you acknowledge and agree that
all content presented to you on this Website is protected by copyrights, all content presented to you on this Website is protected by copyrights,
trademarks, service marks, patents or other proprietary rights and laws, trademarks, service marks, patents or other proprietary rights and laws,
and is the sole property of{" "} and is the sole property of{' '}
<span className='website_name'>{process.env.NEXT_PUBLIC_TITLE}</span>. <span className="website_name">{process.env.NEXT_PUBLIC_TITLE}</span>.
</p> </p>
<p> <p>
You are only permitted to use the content as expressly authorized by us You are only permitted to use the content as expressly authorized by us or
or the specific content provider. Except for a single copy made for the specific content provider. Except for a single copy made for personal
personal use only, you may not copy, reproduce, modify, republish, use only, you may not copy, reproduce, modify, republish, upload, post,
upload, post, transmit, or distribute any documents or information from transmit, or distribute any documents or information from this Website in
this Website in any form or by any means without prior written any form or by any means without prior written permission from us or the
permission from us or the specific content provider, and you are solely specific content provider, and you are solely responsible for obtaining
responsible for obtaining permission before reusing any copyrighted permission before reusing any copyrighted material that is available on
material that is available on this Website. this Website.
</p> </p>
<h2>Third Party Websites</h2> <h2>Third Party Websites</h2>
@@ -143,21 +142,19 @@ const Terms = () => {
<p> <p>
This Website may link you to other sites on the Internet or otherwise This Website may link you to other sites on the Internet or otherwise
include references to information, documents, software, materials and/or include references to information, documents, software, materials and/or
services provided by other parties. These websites may contain services provided by other parties. These websites may contain information
information or material that some people may find inappropriate or or material that some people may find inappropriate or offensive.
offensive.
</p> </p>
<p> <p>
These other websites and parties are not under our control, and you These other websites and parties are not under our control, and you
acknowledge that we are not responsible for the accuracy, copyright acknowledge that we are not responsible for the accuracy, copyright
compliance, legality, decency, or any other aspect of the content of compliance, legality, decency, or any other aspect of the content of such
such sites, nor are we responsible for errors or omissions in any sites, nor are we responsible for errors or omissions in any references to
references to other parties or their products and services. The other parties or their products and services. The inclusion of such a link
inclusion of such a link or reference is provided merely as a or reference is provided merely as a convenience and does not imply
convenience and does not imply endorsement of, or association with, the endorsement of, or association with, the Website or party by us, or any
Website or party by us, or any warranty of any kind, either express or warranty of any kind, either express or implied.
implied.
</p> </p>
<h2> <h2>
@@ -165,10 +162,10 @@ const Terms = () => {
</h2> </h2>
<p> <p>
Your use of{" "} Your use of{' '}
<span className='website_name'>{process.env.NEXT_PUBLIC_TITLE}</span> is <span className="website_name">{process.env.NEXT_PUBLIC_TITLE}</span> is
at your sole risk. The Website is provided "as is" and "as available". at your sole risk. The Website is provided "as is" and "as available". We
We disclaim all warranties of any kind, express or implied, including, disclaim all warranties of any kind, express or implied, including,
without limitation, the warranties of merchantability, fitness for a without limitation, the warranties of merchantability, fitness for a
particular purpose and non-infringement. particular purpose and non-infringement.
</p> </p>
@@ -176,64 +173,63 @@ const Terms = () => {
<p> <p>
We are not liable for damages, direct or consequential, resulting from We are not liable for damages, direct or consequential, resulting from
your use of the Website, and you agree to defend, indemnify and hold us your use of the Website, and you agree to defend, indemnify and hold us
harmless from any claims, losses, liability costs and expenses harmless from any claims, losses, liability costs and expenses (including
(including but not limites to attorney's fees) arising from your but not limites to attorney's fees) arising from your violation of any
violation of any third-party's rights. You acknowledge that you have third-party's rights. You acknowledge that you have only a limited,
only a limited, non-exclusive, nontransferable license to use the non-exclusive, nontransferable license to use the Website. Because the
Website. Because the Website is not error or bug free, you agree that Website is not error or bug free, you agree that you will use it carefully
you will use it carefully and avoid using it ways which might result in and avoid using it ways which might result in any loss of your or any
any loss of your or any third party's property or information. third party's property or information.
</p> </p>
<h2>Term and termination</h2> <h2>Term and termination</h2>
<p> <p>
This Terms and Conditions will become effective in relation to you when This Terms and Conditions will become effective in relation to you when
you create a{" "} you create a{' '}
<span className='website_name'>{process.env.NEXT_PUBLIC_TITLE}</span>{" "} <span className="website_name">{process.env.NEXT_PUBLIC_TITLE}</span>{' '}
account or when you start using the{" "} account or when you start using the{' '}
<span className='website_name'>{process.env.NEXT_PUBLIC_TITLE}</span>{" "} <span className="website_name">{process.env.NEXT_PUBLIC_TITLE}</span> and
and will remain effective until terminated by you or by us.{" "} will remain effective until terminated by you or by us.{' '}
</p> </p>
<p> <p>
<span className='website_name'>{process.env.NEXT_PUBLIC_TITLE}</span>{" "} <span className="website_name">{process.env.NEXT_PUBLIC_TITLE}</span>{' '}
reserves the right to terminate this Terms and Conditions or suspend reserves the right to terminate this Terms and Conditions or suspend your
your account at any time in case of unauthorized, or suspected account at any time in case of unauthorized, or suspected unauthorized use
unauthorized use of the Website whether in contravention of this Terms of the Website whether in contravention of this Terms and Conditions or
and Conditions or otherwise. If{" "} otherwise. If{' '}
<span className='website_name'>{process.env.NEXT_PUBLIC_TITLE}</span>{" "} <span className="website_name">{process.env.NEXT_PUBLIC_TITLE}</span>{' '}
terminates this Terms and Conditions, or suspends your account for any terminates this Terms and Conditions, or suspends your account for any of
of the reasons set out in this section,{" "} the reasons set out in this section,{' '}
<span className='website_name'>{process.env.NEXT_PUBLIC_TITLE}</span>{" "} <span className="website_name">{process.env.NEXT_PUBLIC_TITLE}</span>{' '}
shall have no liability or responsibility to you. shall have no liability or responsibility to you.
</p> </p>
<h2>Assignment</h2> <h2>Assignment</h2>
<p> <p>
<span className='website_name'>{process.env.NEXT_PUBLIC_TITLE}</span>{" "} <span className="website_name">{process.env.NEXT_PUBLIC_TITLE}</span> may
may assign this Terms and Conditions or any part of it without assign this Terms and Conditions or any part of it without restrictions.
restrictions. You may not assign this Terms and Conditions or any part You may not assign this Terms and Conditions or any part of it to any
of it to any third party. third party.
</p> </p>
<h2>Governing Law</h2> <h2>Governing Law</h2>
<p> <p>
These Terms and Conditions and any dispute or claim arising out of, or These Terms and Conditions and any dispute or claim arising out of, or
related to them, shall be governed by and construed in accordance with related to them, shall be governed by and construed in accordance with the
the internal laws of the <span className='country'>fr</span> without internal laws of the <span className="country">fr</span> without giving
giving effect to any choice or conflict of law provision or rule. effect to any choice or conflict of law provision or rule.
</p> </p>
<p> <p>
Any legal suit, action or proceeding arising out of, or related to, Any legal suit, action or proceeding arising out of, or related to, these
these Terms of Service or the Website shall be instituted exclusively in Terms of Service or the Website shall be instituted exclusively in the
the federal courts of <span className='country'>fr</span>. federal courts of <span className="country">fr</span>.
</p> </p>
</div> </div>
); );
};
export default Terms; export default Terms;

View File

@@ -2,21 +2,22 @@
This card is used on the landing page This card is used on the landing page
*/ */
import Image from "next/image"; import Image from 'next/image';
const CardLanding = (props) => { const CardLanding = (props) => {
const { image, title, text } = props;
return ( return (
<div className='w-80 h-48 p-5 sm:ml-5 mb-5 bg-base-100 flex'> <div className="w-80 h-48 p-5 sm:ml-5 mb-5 bg-base-100 flex">
<div> <div>
<div className='rounded-full w-12 h-12 border flex bg-neutral-content'> <div className="rounded-full w-12 h-12 border flex bg-neutral-content">
<div className='m-auto flex'> <div className="m-auto flex">
<Image src={props.image} width={24} height={24} /> <Image src={image} width={24} height={24} />
</div> </div>
</div> </div>
</div> </div>
<div className='ml-8'> <div className="ml-8">
<p className='font-semibold font-title text-lg'>{props.title}</p> <p className="font-semibold font-title text-lg">{title}</p>
<p className='mt-3'>{props.text}</p> <p className="mt-3">{text}</p>
</div> </div>
</div> </div>
); );

View File

@@ -2,17 +2,15 @@
This card is used on the landing page This card is used on the landing page
*/ */
import { FiStar } from "react-icons/fi"; import { FiStar } from 'react-icons/fi';
const KeyFeature = (props) => { const KeyFeature = (props) => (
return ( <div className="shadow-sm p-5 mb-5 bg-base-100 flex italic">
<div className='shadow-sm p-5 mb-5 bg-base-100 flex italic'> <div className="p-2 bg-accent-focus w-12 h-12 text-white rounded-sm my-auto flex">
<div className='p-2 bg-accent-focus w-12 h-12 text-white rounded-sm my-auto flex'>
<FiStar className="text-2xl m-auto" /> <FiStar className="text-2xl m-auto" />
</div> </div>
<div className='m-auto ml-3'>{props.children}</div> <div className="m-auto ml-3">{props.children}</div>
</div> </div>
); );
};
export default KeyFeature; export default KeyFeature;

View File

@@ -1,18 +1,18 @@
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';
const Login = (props) => { const Login = (props) => {
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 resetPassword = () => { const resetPassword = () => {
props.resetPassword(email).then((result) => { props.resetPassword(email).then((result) => {
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!');
}); });
}; };
@@ -22,12 +22,12 @@ const Login = (props) => {
// 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.
props props
.signIn({ .signIn({
email: email, email,
password: password, password,
}) })
.then((result) => { .then((result) => {
if (result.data) { if (result.data) {
router.push("/"); router.push('/');
} }
if (result.error) { if (result.error) {
toast.error(result.error.message); toast.error(result.error.message);
@@ -36,45 +36,46 @@ const Login = (props) => {
}; };
return ( return (
<div className='p-10 bg-base-100 md:flex-1 rounded-md text-base-content shadow-md max-w-sm font-body'> <div className="p-10 bg-base-100 md:flex-1 rounded-md text-base-content shadow-md max-w-sm font-body">
{!forgot && ( {!forgot && (
<> <>
<h3 className='my-4 text-2xl font-semibold font-title'> <h3 className="my-4 text-2xl font-semibold font-title">
Account Login Account Login
</h3> </h3>
<form action='#' className='flex flex-col space-y-5'> <form action="#" className="flex flex-col space-y-5">
<div className='flex flex-col space-y-1'> <div className="flex flex-col space-y-1">
<label htmlFor='email' className='text-sm'> <label htmlFor="email" className="text-sm">
Email address Email address
</label> </label>
<input <input
type='email' type="email"
id='email' id="email"
autoFocus autoFocus
className='input input-primary input-bordered input-sm' className="input input-primary input-bordered input-sm"
value={email} value={email}
onChange={(event) => { onChange={(event) => {
setEmail(event.target.value); setEmail(event.target.value);
}} }}
/> />
</div> </div>
<div className='flex flex-col space-y-1'> <div className="flex flex-col space-y-1">
<div className='flex items-center justify-between'> <div className="flex items-center justify-between">
<label htmlFor='password' className='text-sm'> <label htmlFor="password" className="text-sm">
Password Password
</label> </label>
<button <button
onClick={() => { onClick={() => {
setForgot(true); setForgot(true);
}} }}
className='text-sm text-blue-600 hover:underline focus:text-blue-800'> className="text-sm text-blue-600 hover:underline focus:text-blue-800"
>
Forgot Password? Forgot Password?
</button> </button>
</div> </div>
<input <input
type='password' type="password"
id='password' id="password"
className='input input-primary input-bordered input-sm' className="input input-primary input-bordered input-sm"
value={password} value={password}
onChange={(event) => { onChange={(event) => {
setPassword(event.target.value); setPassword(event.target.value);
@@ -84,31 +85,33 @@ const Login = (props) => {
<div> <div>
<button <button
className='btn btn-primary w-full' className="btn btn-primary w-full"
onClick={(event) => { onClick={(event) => {
login(event); login(event);
}}> }}
>
Log in Log in
</button> </button>
</div> </div>
<div className='flex flex-col space-y-5'> <div className="flex flex-col space-y-5">
<span className='flex items-center justify-center space-x-2'> <span className="flex items-center justify-center space-x-2">
<span className='h-px bg-gray-400 w-14'></span> <span className="h-px bg-gray-400 w-14" />
<span className='font-normal text-gray-500'>or login with</span> <span className="font-normal text-gray-500">or login with</span>
<span className='h-px bg-gray-400 w-14'></span> <span className="h-px bg-gray-400 w-14" />
</span> </span>
<div className='flex flex-col space-y-4'> <div className="flex flex-col space-y-4">
<button <button
href='#' href="#"
className='flex items-center justify-center px-4 py-2 space-x-2 transition-colors duration-300 border border-base-200 rounded-md group hover:bg-base-300 focus:outline-none ' className="flex items-center justify-center px-4 py-2 space-x-2 transition-colors duration-300 border border-base-200 rounded-md group hover:bg-base-300 focus:outline-none "
onClick={(event) => { onClick={(event) => {
event.preventDefault(); event.preventDefault();
props.signIn({ provider: "google" }); props.signIn({ provider: 'google' });
}}> }}
<div className='text-base-content'> >
<div className="text-base-content">
<IoLogoGoogle /> <IoLogoGoogle />
</div> </div>
<span className='text-sm font-medium text-base-content'> <span className="text-sm font-medium text-base-content">
Gmail Gmail
</span> </span>
</button> </button>
@@ -119,19 +122,20 @@ const Login = (props) => {
)} )}
{forgot && ( {forgot && (
<> <>
<h3 className='my-4 text-2xl font-semibold'>Password recovery</h3> <h3 className="my-4 text-2xl font-semibold">Password recovery</h3>
<form action='#' className='flex flex-col space-y-5'> <form action="#" className="flex flex-col space-y-5">
<div className='flex flex-col space-y-1'> <div className="flex flex-col space-y-1">
<label <label
htmlFor='email' htmlFor="email"
className='text-sm font-semibold text-gray-500'> className="text-sm font-semibold text-gray-500"
>
Email address Email address
</label> </label>
<input <input
type='email' type="email"
id='email' id="email"
autoFocus autoFocus
className='input input-primary input-bordered input-sm' className="input input-primary input-bordered input-sm"
value={email} value={email}
onChange={(event) => { onChange={(event) => {
setEmail(event.target.value); setEmail(event.target.value);
@@ -141,11 +145,12 @@ const Login = (props) => {
<div> <div>
<button <button
className='btn btn-primary w-full btn-sm' className="btn btn-primary w-full btn-sm"
onClick={(event) => { onClick={(event) => {
event.preventDefault(); event.preventDefault();
resetPassword(); resetPassword();
}}> }}
>
Recover my password Recover my password
</button> </button>
</div> </div>
@@ -154,7 +159,8 @@ const Login = (props) => {
onClick={() => { onClick={() => {
setForgot(false); setForgot(false);
}} }}
className='text-sm text-blue-600 hover:underline focus:text-blue-800'> className="text-sm text-blue-600 hover:underline focus:text-blue-800"
>
Go back to sign in Go back to sign in
</button> </button>
</form> </form>

View File

@@ -1,18 +1,18 @@
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';
const SignUpPanel = (props) => { const SignUpPanel = (props) => {
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 resetPassword = () => { const resetPassword = () => {
props.resetPassword(email).then((result) => { props.resetPassword(email).then((result) => {
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!');
}); });
}; };
@@ -22,12 +22,12 @@ const SignUpPanel = (props) => {
// 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.
props props
.signUp({ .signUp({
email: email, email,
password: password, password,
}) })
.then((result) => { .then((result) => {
if (result.data) { if (result.data) {
router.push("/"); router.push('/');
} }
if (result.error) { if (result.error) {
toast.error(result.error.message); toast.error(result.error.message);
@@ -36,33 +36,33 @@ const SignUpPanel = (props) => {
}; };
return ( return (
<div className='p-10 bg-base-100 md:flex-1 rounded-md text-base-content shadow-md max-w-sm font-body'> <div className="p-10 bg-base-100 md:flex-1 rounded-md text-base-content shadow-md max-w-sm font-body">
{!forgot && ( {!forgot && (
<> <>
<h3 className='my-4 text-2xl font-semibold font-title'> <h3 className="my-4 text-2xl font-semibold font-title">
Account Sign Up Account Sign Up
</h3> </h3>
<form action='#' className='flex flex-col space-y-5'> <form action="#" className="flex flex-col space-y-5">
<div className='flex flex-col space-y-1'> <div className="flex flex-col space-y-1">
<label htmlFor='email' className='text-sm'> <label htmlFor="email" className="text-sm">
Email address Email address
</label> </label>
<input <input
type='email' type="email"
id='email' id="email"
autoFocus autoFocus
className='input input-primary input-bordered input-sm' className="input input-primary input-bordered input-sm"
value={email} value={email}
onChange={(event) => { onChange={(event) => {
setEmail(event.target.value); setEmail(event.target.value);
}} }}
/> />
</div> </div>
<div className='flex flex-col space-y-1'> <div className="flex flex-col space-y-1">
<input <input
type='password' type="password"
id='password' id="password"
className='input input-primary input-bordered input-sm' className="input input-primary input-bordered input-sm"
value={password} value={password}
onChange={(event) => { onChange={(event) => {
setPassword(event.target.value); setPassword(event.target.value);
@@ -72,33 +72,35 @@ const SignUpPanel = (props) => {
<div> <div>
<button <button
className='btn btn-primary w-full' className="btn btn-primary w-full"
onClick={(event) => { onClick={(event) => {
signup(event); signup(event);
}}> }}
>
Sign Up Sign Up
</button> </button>
</div> </div>
<div className='flex flex-col space-y-5'> <div className="flex flex-col space-y-5">
<span className='flex items-center justify-center space-x-2'> <span className="flex items-center justify-center space-x-2">
<span className='h-px bg-gray-400 w-14'></span> <span className="h-px bg-gray-400 w-14" />
<span className='font-normal text-gray-500'> <span className="font-normal text-gray-500">
or sign up with or sign up with
</span> </span>
<span className='h-px bg-gray-400 w-14'></span> <span className="h-px bg-gray-400 w-14" />
</span> </span>
<div className='flex flex-col space-y-4'> <div className="flex flex-col space-y-4">
<button <button
href='#' href="#"
className='flex items-center justify-center px-4 py-2 space-x-2 transition-colors duration-300 border border-base-200 rounded-md group hover:bg-base-300 focus:outline-none ' className="flex items-center justify-center px-4 py-2 space-x-2 transition-colors duration-300 border border-base-200 rounded-md group hover:bg-base-300 focus:outline-none "
onClick={(event) => { onClick={(event) => {
event.preventDefault(); event.preventDefault();
props.signIn({ provider: "google" }); props.signIn({ provider: 'google' });
}}> }}
<div className='text-base-content'> >
<div className="text-base-content">
<IoLogoGoogle /> <IoLogoGoogle />
</div> </div>
<span className='text-sm font-medium text-base-content'> <span className="text-sm font-medium text-base-content">
Gmail Gmail
</span> </span>
</button> </button>

View File

@@ -3,29 +3,29 @@ This component will handle the theme (dark/light). You are able to change the se
DaisyUI have more than 10 themes availables https://daisyui.com/docs/default-themes DaisyUI have more than 10 themes availables https://daisyui.com/docs/default-themes
*/ */
import { HiOutlineMoon, HiOutlineSun } from "react-icons/hi"; import { HiOutlineMoon, HiOutlineSun } from 'react-icons/hi';
import { useEffect, useState } from "react"; import { useEffect, useState } from 'react';
const theme = { const theme = {
primary: "supaTheme", primary: 'supaTheme',
secondary: "dark", secondary: 'dark',
}; };
const ThemeToggle = () => { const ThemeToggle = () => {
const [activeTheme, setActiveTheme] = useState(document.body.dataset.theme); const [activeTheme, setActiveTheme] = useState(document.body.dataset.theme);
const inactiveTheme = activeTheme === "supaTheme" ? "dark" : "supaTheme"; const inactiveTheme = activeTheme === 'supaTheme' ? 'dark' : 'supaTheme';
useEffect(() => { useEffect(() => {
document.body.dataset.theme = activeTheme; document.body.dataset.theme = activeTheme;
window.localStorage.setItem("theme", activeTheme); window.localStorage.setItem('theme', activeTheme);
}, [activeTheme]); }, [activeTheme]);
return ( return (
<button className='flex ml-3' onClick={() => setActiveTheme(inactiveTheme)}> <button className="flex ml-3" onClick={() => setActiveTheme(inactiveTheme)}>
{activeTheme === theme.secondary ? ( {activeTheme === theme.secondary ? (
<HiOutlineSun className='m-auto text-xl hover:text-accent' /> <HiOutlineSun className="m-auto text-xl hover:text-accent" />
) : ( ) : (
<HiOutlineMoon className='m-auto text-xl hover:text-accent' /> <HiOutlineMoon className="m-auto text-xl hover:text-accent" />
)} )}
</button> </button>
); );

View File

@@ -4,46 +4,46 @@ It will launch the homepage and navigate through 2 differents pages (Pricing and
You can see that it will do this twice, with 2 different resolution, to test the mobile version of the site. You can see that it will do this twice, with 2 different resolution, to test the mobile version of the site.
*/ */
describe("Basic Test", () => { describe('Basic Test', () => {
context("Desktop resolution", () => { context('Desktop resolution', () => {
it("Visits the homepage and nav links (Desktop)", () => { it('Visits the homepage and nav links (Desktop)', () => {
cy.viewport(1280, 720); cy.viewport(1280, 720);
cy.visit(""); cy.visit('');
cy.get("nav").contains("Pricing").click(); cy.get('nav').contains('Pricing').click();
cy.url().should("include", "/pricing"); cy.url().should('include', '/pricing');
cy.get("nav").contains("Contact").click(); cy.get('nav').contains('Contact').click();
cy.url().should("include", "/contact"); cy.url().should('include', '/contact');
cy.get("nav").contains("Login").click(); cy.get('nav').contains('Login').click();
cy.url().should("include", "/login"); cy.url().should('include', '/login');
cy.get("nav").contains("Sign Up").click(); cy.get('nav').contains('Sign Up').click();
cy.url().should("include", "/signup"); cy.url().should('include', '/signup');
}); });
}); });
context("Mobile resolution", () => { context('Mobile resolution', () => {
it("Visits the homepage and nav links (Mobile)", () => { it('Visits the homepage and nav links (Mobile)', () => {
cy.viewport(680, 720); cy.viewport(680, 720);
cy.visit(""); cy.visit('');
cy.get("[data-cy=dropdown]").click(); cy.get('[data-cy=dropdown]').click();
cy.get("[data-cy=dropdown]").contains("Pricing").click(); cy.get('[data-cy=dropdown]').contains('Pricing').click();
cy.url().should("include", "/pricing"); cy.url().should('include', '/pricing');
cy.get("[data-cy=dropdown]").click(); cy.get('[data-cy=dropdown]').click();
cy.get("[data-cy=dropdown]").contains("Login").click(); cy.get('[data-cy=dropdown]').contains('Login').click();
cy.url().should("include", "/login"); cy.url().should('include', '/login');
cy.get("[data-cy=dropdown]").click(); cy.get('[data-cy=dropdown]').click();
cy.get("[data-cy=dropdown]").contains("Sign Up").click(); cy.get('[data-cy=dropdown]').contains('Sign Up').click();
cy.url().should("include", "/signup"); cy.url().should('include', '/signup');
cy.get("[data-cy=dropdown]").click(); cy.get('[data-cy=dropdown]').click();
cy.get("[data-cy=dropdown]").contains("Contact").click(); cy.get('[data-cy=dropdown]').contains('Contact').click();
cy.url().should("include", "/contact"); cy.url().should('include', '/contact');
}); });
}); });
}); });

View File

@@ -19,4 +19,4 @@
module.exports = (on, config) => { module.exports = (on, config) => {
// `on` is used to hook into various events Cypress emits // `on` is used to hook into various events Cypress emits
// `config` is the resolved Cypress config // `config` is the resolved Cypress config
} };

View File

@@ -14,7 +14,7 @@
// *********************************************************** // ***********************************************************
// Import commands.js using ES2015 syntax: // Import commands.js using ES2015 syntax:
import './commands' import './commands';
// Alternatively you can use CommonJS syntax: // Alternatively you can use CommonJS syntax:
// require('./commands') // require('./commands')

7454
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,8 @@
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
"cypress:open": "cypress open", "cypress:open": "cypress open",
"cypress:run": "cypress run" "cypress:run": "cypress run",
"eslint": "eslint . --fix"
}, },
"dependencies": { "dependencies": {
"@headlessui/react": "^1.4.0", "@headlessui/react": "^1.4.0",
@@ -30,9 +31,22 @@
"stripe": "^8.168.0" "stripe": "^8.168.0"
}, },
"devDependencies": { "devDependencies": {
"@next/eslint-plugin-next": "^11.0.1",
"autoprefixer": "^10.3.1", "autoprefixer": "^10.3.1",
"cypress": "^8.2.0", "cypress": "^8.2.0",
"eslint": "^7.32.0",
"eslint-config-airbnb": "^18.2.1",
"eslint-config-prettier": "^8.3.0",
"eslint-import-resolver-alias": "^1.1.2",
"eslint-plugin-cypress": "^2.11.3",
"eslint-plugin-import": "^2.24.0",
"eslint-plugin-jsx-a11y": "^6.4.1",
"eslint-plugin-prettier": "^3.4.0",
"eslint-plugin-react": "^7.24.0",
"eslint-plugin-react-hooks": "^4.2.0",
"eslint-plugin-simple-import-sort": "^7.0.0",
"postcss": "^8.3.6", "postcss": "^8.3.6",
"prettier": "^2.3.2",
"tailwindcss": "^2.2.7" "tailwindcss": "^2.2.7"
} }
} }

View File

@@ -1,7 +1,7 @@
import "./global.css"; import './global.css';
import { AuthProvider } from "utils/AuthContext"; import { AuthProvider } from 'utils/AuthContext';
import { DefaultSeo } from "next-seo"; import { DefaultSeo } from 'next-seo';
/* /*
Next-seo is integrated by default, if you want more information and how to Next-seo is integrated by default, if you want more information and how to
@@ -14,14 +14,14 @@ function MyApp({ Component, pageProps }) {
<AuthProvider> <AuthProvider>
<DefaultSeo <DefaultSeo
openGraph={{ openGraph={{
type: "website", type: 'website',
locale: "en_IE", locale: 'en_IE',
url: "", url: '',
site_name: "Supanextail", site_name: 'Supanextail',
}} }}
twitter={{ twitter={{
handle: "@michael_webdev", handle: '@michael_webdev',
site: "@michael_webdev", site: '@michael_webdev',
}} }}
/> />
<Component {...pageProps} /> <Component {...pageProps} />

View File

@@ -1,4 +1,4 @@
import Document, { Head, Html, Main, NextScript } from "next/document"; import Document, { Head, Html, Main, NextScript } from 'next/document';
class MyDocument extends Document { class MyDocument extends Document {
static async getInitialProps(ctx) { static async getInitialProps(ctx) {

View File

@@ -1,92 +1,87 @@
import Head from "next/head"; // import Head from 'next/head';
import Layout from "components/Layout"; // import Layout from 'components/Layout';
import { createClient } from "@supabase/supabase-js"; // import { createClient } from '@supabase/supabase-js';
import dynamic from "next/dynamic"; // import { useState } from 'react';
import { useEffect } from "react";
import { useState } from "react";
const AdminPage = ({ adminKey }) => { // const AdminPage = () => {
const [currentTable, setCurrentTable] = useState("admin_list"); // const [currentTable, setCurrentTable] = useState('admin_list');
const SupabaseGrid = dynamic(() =>
import("@supabase/grid").then((mod) => mod.SupabaseGrid)
);
return ( // return (
<div> // <div>
<Head> // <Head>
<title>{process.env.NEXT_PUBLIC_TITLE} | Dashboard</title> // <title>{process.env.NEXT_PUBLIC_TITLE} | Dashboard</title>
</Head> // </Head>
<Layout> // <Layout>
<> // <>
<h1 className='text-4xl font-bold md:text-5xl font-title'> // <h1 className="text-4xl font-bold md:text-5xl font-title">
Admin Dashboard // Admin Dashboard
</h1> // </h1>
<p>Hello admin ! Select your table to display the content</p> // <p>Hello admin ! Select your table to display the content</p>
<div className='flex space-x-3'> // <div className="flex space-x-3">
<button // <button
className='btn btn-primary btn-sm' // type="button"
onClick={() => { // className="btn btn-primary btn-sm"
setCurrentTable("profiles"); // onClick={() => {
}}> // setCurrentTable('profiles');
User profiles // }}
</button><button // >
className='btn btn-primary btn-sm' // User profiles
onClick={() => { // </button>
setCurrentTable("admin_list"); // <button
}}> // type="button"
Admin List // className="btn btn-primary btn-sm"
</button><button // onClick={() => {
className='btn btn-primary btn-sm' // setCurrentTable('admin_list');
onClick={() => { // }}
setCurrentTable("subscriptions"); // >
}}> // Admin List
Subscriptions // </button>
</button> // <button
</div> // type="button"
</> // className="btn btn-primary btn-sm"
<div className='shadow-sm max-w-4xl w-full'> // onClick={() => {
<SupabaseGrid // setCurrentTable('subscriptions');
className='w-full' // }}
table={`${currentTable}`} // >
clientProps={{ // Subscriptions
supabaseUrl: process.env.NEXT_PUBLIC_SUPABASE_URL, // </button>
supabaseKey: adminKey, // </div>
}} // </>
/> // </Layout>
</div> // </div>
</Layout> // );
</div> // };
); // export async function getServerSideProps({ req }) {
}; // const supabaseAdmin = createClient(
export async function getServerSideProps({ req }) { // process.env.NEXT_PUBLIC_SUPABASE_URL,
const supabaseAdmin = createClient( // process.env.SUPABASE_ADMIN_KEY
process.env.NEXT_PUBLIC_SUPABASE_URL, // );
process.env.SUPABASE_ADMIN_KEY // const { user } = await supabaseAdmin.auth.api.getUserByCookie(req);
);
const { user } = await supabaseAdmin.auth.api.getUserByCookie(req);
// If the user exist, you will retrieve the user profile and if he/she's an admin // // If the user exist, you will retrieve the user profile and if he/she's an admin
if (user) { // if (user) {
let { data: admincheck, error } = await supabaseAdmin // const { data: admincheck, error } = await supabaseAdmin
.from("admin_list") // .from('admin_list')
.select("isadmin") // .select('isadmin')
.eq("id", user.id) // .eq('id', user.id)
.single(); // .single();
if (admincheck.isadmin) { // if (admincheck.isadmin) {
return { // return {
props: { // props: {
admincheck: admincheck.isadmin, // admincheck: admincheck.isadmin,
adminKey: process.env.SUPABASE_ADMIN_KEY, // adminKey: process.env.SUPABASE_ADMIN_KEY,
}, // },
}; // };
} else // }
return { props: {}, redirect: { destination: "/", permanent: false } };
} // if (error) {
// If no user, redirect to index. // console.log(error);
if (!user) { // }
return { props: {}, redirect: { destination: "/", permanent: false } }; // return { props: {}, redirect: { destination: '/', permanent: false } };
} // }
} // // If no user, redirect to index.
export default AdminPage; // return { props: {}, redirect: { destination: '/', permanent: false } };
// }
// export default AdminPage;

View File

@@ -3,7 +3,7 @@
* With SupaNexTail, we use SSR with the Dashboard page (pages/dashboard.js) * With SupaNexTail, we use SSR with the Dashboard page (pages/dashboard.js)
*/ */
import { supabase } from "utils/supabaseClient"; import { supabase } from 'utils/supabaseClient';
export default function handler(req, res) { export default function handler(req, res) {
supabase.auth.api.setAuthCookie(req, res); supabase.auth.api.setAuthCookie(req, res);

View File

@@ -1,8 +1,8 @@
import { supabase } from "utils/supabaseClient"; 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 (req, res) => { const getUser = async (req, res) => {
const token = req.headers.token; const { token } = req.headers;
const { data: user, error } = await supabase.auth.api.getUser(token); const { data: user, error } = await supabase.auth.api.getUser(token);

View File

@@ -1,8 +1,8 @@
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/init-middleware';
const rateLimit = require("express-rate-limit"); const rateLimit = require('express-rate-limit');
export const config = { export const config = {
api: { api: {
@@ -12,7 +12,7 @@ export const config = {
const cors = initMiddleware( const cors = initMiddleware(
Cors({ Cors({
methods: ["PUT"], methods: ['PUT'],
}) })
); );
@@ -26,34 +26,32 @@ const limiter = initMiddleware(
export default async function handler(req, res) { export default async function handler(req, res) {
await cors(req, res); await cors(req, res);
await limiter(req, res); await limiter(req, res);
if (req.method === "PUT") { if (req.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: `${req.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 res.status(200).send({
.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((err) => {
res.status(500).send({ res.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',
}); });
}); });
} }

View File

@@ -2,10 +2,10 @@
This is a simple contact form for SupaNexTail This is a simple contact form for SupaNexTail
Using Sendgrid. Using Sendgrid.
*/ */
const sgMail = require("@sendgrid/mail"); const sgMail = require('@sendgrid/mail');
export default async function handler(req, res) { export default async function handler(req, res) {
if (req.method === "POST") { if (req.method === 'POST') {
sgMail.setApiKey(process.env.SENDGRID_SECRET); sgMail.setApiKey(process.env.SENDGRID_SECRET);
const msg = { const msg = {
to: process.env.SENDGRID_MAILTO, // Change to your recipient to: process.env.SENDGRID_MAILTO, // Change to your recipient
@@ -19,13 +19,13 @@ export default async function handler(req, res) {
.then(() => { .then(() => {
res res
.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) => {
console.error(error); console.error(error);
res.status(500).send({ res.status(500).send({
message: "There was an issue with your email... please retry", message: 'There was an issue with your email... please retry',
err: err, err,
}); });
}); });
} }

View File

@@ -1,11 +1,11 @@
import Cors from "cors"; import Cors from 'cors';
import initMiddleware from "utils/init-middleware"; import initMiddleware from 'utils/init-middleware';
const rateLimit = require("express-rate-limit"); const rateLimit = require('express-rate-limit');
const cors = initMiddleware( const cors = initMiddleware(
Cors({ Cors({
methods: ["POST"], methods: ['POST'],
}) })
); );
@@ -17,13 +17,13 @@ const limiter = initMiddleware(
); );
// 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 = require("stripe")(process.env.STRIPE_SECRET); const stripe = require('stripe')(process.env.STRIPE_SECRET);
export default async function handler(req, res) { export default async function handler(req, res) {
await cors(req, res); await cors(req, res);
await limiter(req, res); await limiter(req, res);
if (req.method === "POST") { if (req.method === 'POST') {
const priceId = req.body.priceId; const { priceId } = req.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.
@@ -31,7 +31,7 @@ export default async function handler(req, res) {
const session = req.body.customerId const session = req.body.customerId
? await stripe.checkout.sessions.create({ ? await stripe.checkout.sessions.create({
mode: req.body.pay_mode, mode: req.body.pay_mode,
payment_method_types: ["card"], payment_method_types: ['card'],
client_reference_id: req.body.userId, client_reference_id: req.body.userId,
metadata: { token: req.body.tokenId, priceId: req.body.priceId }, metadata: { token: req.body.tokenId, priceId: req.body.priceId },
customer: req.body.customerId, customer: req.body.customerId,
@@ -49,8 +49,8 @@ export default async function handler(req, res) {
cancel_url: `${req.headers.origin}/pricing`, cancel_url: `${req.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: req.body.email,
client_reference_id: req.body.userId, client_reference_id: req.body.userId,
metadata: { token: req.body.tokenId, priceId: req.body.priceId }, metadata: { token: req.body.tokenId, priceId: req.body.priceId },

View File

@@ -1,13 +1,14 @@
/* Dont forget to create your customer portal on Stripe /* Dont forget to create your customer portal on Stripe
https://dashboard.stripe.com/test/settings/billing/portal */ https://dashboard.stripe.com/test/settings/billing/portal */
import Cors from "cors"; import Cors from 'cors';
import initMiddleware from "utils/init-middleware"; import initMiddleware from 'utils/init-middleware';
const rateLimit = require("express-rate-limit");
const rateLimit = require('express-rate-limit');
const cors = initMiddleware( const cors = initMiddleware(
Cors({ Cors({
methods: ["POST", "PUT"], methods: ['POST', 'PUT'],
}) })
); );
@@ -19,12 +20,12 @@ const limiter = initMiddleware(
); );
// 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 = require("stripe")(process.env.STRIPE_SECRET); const stripe = require('stripe')(process.env.STRIPE_SECRET);
export default async function handler(req, res) { export default async function handler(req, res) {
await cors(req, res); await cors(req, res);
await limiter(req, res); await limiter(req, res);
if (req.method === "POST") { if (req.method === 'POST') {
const returnUrl = `${req.headers.origin}/dashboard`; // Stripe will return to the dashboard, you can change it const returnUrl = `${req.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({

View File

@@ -6,11 +6,12 @@ If you want to test it locally, you'll need the stripe CLI and use this command
stripe listen --forward-to localhost:3000/api/stripe/stripe-webhook stripe listen --forward-to localhost:3000/api/stripe/stripe-webhook
*/ */
import Cors from "cors"; import Cors from 'cors';
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/init-middleware';
const rateLimit = require("express-rate-limit");
const rateLimit = require('express-rate-limit');
export const config = { export const config = {
api: { api: {
@@ -21,7 +22,7 @@ export const config = {
// Initialize the cors middleware -> Allow the browser extension to create lists // Initialize the cors middleware -> Allow the browser extension to create lists
const cors = initMiddleware( const cors = initMiddleware(
Cors({ Cors({
methods: ["POST", "HEAD"], methods: ['POST', 'HEAD'],
}) })
); );
@@ -42,14 +43,14 @@ const limiter = initMiddleware(
); );
// 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 = require("stripe")(process.env.STRIPE_SECRET); const stripe = require('stripe')(process.env.STRIPE_SECRET);
export default async function handler(req, res) { export default async function handler(req, res) {
await cors(req, res); await cors(req, res);
await limiter(req, res); await limiter(req, res);
stripe.setMaxNetworkRetries(2); stripe.setMaxNetworkRetries(2);
if (req.method === "POST") { if (req.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; let event;
const buf = await buffer(req); const buf = await buffer(req);
@@ -57,7 +58,7 @@ export default async function handler(req, res) {
try { try {
event = stripe.webhooks.constructEvent( event = stripe.webhooks.constructEvent(
buf, buf,
req.headers["stripe-signature"], req.headers['stripe-signature'],
process.env.STRIPE_WEBHOOK process.env.STRIPE_WEBHOOK
); );
} catch (err) { } catch (err) {
@@ -76,22 +77,22 @@ export default async function handler(req, res) {
// https://stripe.com/docs/billing/webhooks // https://stripe.com/docs/billing/webhooks
// Remove comment to see the various objects sent for this sample // Remove comment to see the various objects sent for this sample
switch (event.type) { switch (event.type) {
case "checkout.session.completed": case 'checkout.session.completed':
let { data: subscriptions, error } = await supabase const { data: subscriptions, error } = await supabase
.from("subscriptions") .from('subscriptions')
.select("*") .select('*')
.eq("id", dataObject.client_reference_id); .eq('id', dataObject.client_reference_id);
console.log(dataObject); console.log(dataObject);
if (subscriptions.length == 0) { if (subscriptions.length == 0) {
const { data, error } = await supabase const { data, error } = await supabase
.from("profiles") .from('profiles')
.update({ customerId: dataObject.customer }) .update({ customerId: dataObject.customer })
.eq("id", dataObject.client_reference_id); .eq('id', dataObject.client_reference_id);
if (error) console.log(error); if (error) console.log(error);
await supabase await supabase
.from("subscriptions") .from('subscriptions')
.insert([ .insert([
{ {
id: dataObject.client_reference_id, id: dataObject.client_reference_id,
@@ -105,34 +106,34 @@ export default async function handler(req, res) {
.catch((err) => console.log(err)); .catch((err) => console.log(err));
} else if (subscriptions.length > 0) { } else if (subscriptions.length > 0) {
await supabase await supabase
.from("subscriptions") .from('subscriptions')
.update({ .update({
customer_id: dataObject.customer, customer_id: dataObject.customer,
paid_user: true, paid_user: true,
plan: dataObject.metadata.priceId, plan: dataObject.metadata.priceId,
subscription: dataObject.subscription, subscription: dataObject.subscription,
}) })
.eq("id", dataObject.client_reference_id) .eq('id', dataObject.client_reference_id)
.then() .then()
.catch((err) => console.log(err)); .catch((err) => console.log(err));
} }
break; break;
case "customer.subscription.deleted": case 'customer.subscription.deleted':
await supabase await supabase
.from("subscriptions") .from('subscriptions')
.update({ paid_user: false }) .update({ paid_user: false })
.eq("customer_id", dataObject.customer) .eq('customer_id', dataObject.customer)
.then() .then()
.catch((err) => console.log(err)); .catch((err) => console.log(err));
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,
// an invoice.payment_failed event is sent, the subscription becomes past_due. // an invoice.payment_failed event is sent, the subscription becomes past_due.
// Use this webhook to notify your user that their payment has // Use this webhook to notify your user that their payment has
// failed and to retrieve new card details. // failed and to retrieve new card details.
break; break;
case "invoice.paid": case 'invoice.paid':
// Used to provision services after the trial has ended. // Used to provision services after the trial has ended.
// The status of the invoice will show up as paid. Store the status in your // The status of the invoice will show up as paid. Store the status in your
// database to reference when a user accesses your service to avoid hitting rate limits. // database to reference when a user accesses your service to avoid hitting rate limits.

View File

@@ -1,9 +1,8 @@
import Contact from "components/Contact"; import Contact from 'components/Contact';
import Layout from "components/Layout"; import Layout from 'components/Layout';
import { NextSeo } from "next-seo"; import { NextSeo } from 'next-seo';
const ContactPage = () => { const ContactPage = () => (
return (
<> <>
<NextSeo <NextSeo
title={`${process.env.NEXT_PUBLIC_TITLE} | Contact`} title={`${process.env.NEXT_PUBLIC_TITLE} | Contact`}
@@ -15,5 +14,4 @@ const ContactPage = () => {
</Layout> </Layout>
</> </>
); );
};
export default ContactPage; export default ContactPage;

View File

@@ -1,11 +1,11 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from 'react';
import Dashboard from "../components/Dashboard"; import Head from 'next/head';
import Head from "next/head"; import Layout from 'components/Layout';
import Layout from "components/Layout"; import { createClient } from '@supabase/supabase-js';
import { createClient } from "@supabase/supabase-js"; import { useRouter } from 'next/router';
import { supabase } from "../utils/supabaseClient"; import { supabase } from '../utils/supabaseClient';
import { useRouter } from "next/router"; import Dashboard from '../components/Dashboard';
const DashboardPage = ({ user, plan, profile }) => { const DashboardPage = ({ user, plan, profile }) => {
const [session, setSession] = useState(null); const [session, setSession] = useState(null);
@@ -14,7 +14,7 @@ const DashboardPage = ({ user, plan, profile }) => {
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("/"); router.push('/');
} }
}, [user]); }, [user]);
@@ -34,7 +34,7 @@ const DashboardPage = ({ user, plan, profile }) => {
<Layout> <Layout>
{!session ? ( {!session ? (
<div className='text-center'>You are not logged in</div> <div className="text-center">You are not logged in</div>
) : ( ) : (
<> <>
<Dashboard <Dashboard
@@ -55,14 +55,14 @@ export async function getServerSideProps({ req }) {
process.env.SUPABASE_ADMIN_KEY process.env.SUPABASE_ADMIN_KEY
); );
const { user } = await supabaseAdmin.auth.api.getUserByCookie(req); const { user } = await supabaseAdmin.auth.api.getUserByCookie(req);
const stripe = require("stripe")(process.env.STRIPE_SECRET); const stripe = require('stripe')(process.env.STRIPE_SECRET);
// 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) {
let { data: plan, error } = await supabaseAdmin const { data: plan, error } = await supabaseAdmin
.from("subscriptions") .from('subscriptions')
.select("plan") .select('plan')
.eq("id", user.id) .eq('id', user.id)
.single(); .single();
// Check the subscription plan. If it doesnt exist, return null // Check the subscription plan. If it doesnt exist, return null
@@ -70,10 +70,10 @@ export async function getServerSideProps({ req }) {
? await stripe.subscriptions.retrieve(plan.plan) ? await stripe.subscriptions.retrieve(plan.plan)
: null; : null;
let { data: profile, errorProfile } = await supabaseAdmin const { data: profile, errorProfile } = await supabaseAdmin
.from("profiles") .from('profiles')
.select(`username, website, avatar_url`) .select(`username, website, avatar_url`)
.eq("id", user.id) .eq('id', user.id)
.single(); .single();
return { return {
@@ -87,7 +87,7 @@ export async function getServerSideProps({ req }) {
if (!user) { if (!user) {
// If no user, redirect to index. // If no user, redirect to index.
return { props: {}, redirect: { destination: "/", permanent: false } }; return { props: {}, redirect: { destination: '/', permanent: false } };
} }
// If there is a user, return it. // If there is a user, return it.

View File

@@ -3,54 +3,50 @@ Don't forget to modify the Head component with your website informations
You can also update the content on the Landing.js component You can also update the content on the Landing.js component
*/ */
import Head from "next/head"; import Head from 'next/head';
import Landing from "components/Landing"; import Landing from 'components/Landing';
import Layout from "components/Layout"; import Layout from 'components/Layout';
const Home = () => { const Home = () => (
return (
<> <>
<Head> <Head>
<title>{`Welcome to ${process.env.NEXT_PUBLIC_TITLE} 👋`}</title> <title>{`Welcome to ${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"
/> />
<meta property='og:url' content='https://supanextail.dev/' /> <meta property="og:url" content="https://supanextail.dev/" />
<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} 👋`}
/> />
<meta <meta
property='og:description' property="og: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"
/>
<meta
property='og:image'
content='https://supanextail.dev/ogimage.png'
/> />
<meta property="og:image" content="https://supanextail.dev/ogimage.png" />
<meta name='twitter:card' content='summary_large_image' /> <meta name="twitter:card" content="summary_large_image" />
<meta property='twitter:domain' content='supanextail.dev' /> <meta property="twitter:domain" content="supanextail.dev" />
<meta <meta
property='twitter:url' property="twitter:url"
content='https://supanextail.dev/ogimage.png' content="https://supanextail.dev/ogimage.png"
/> />
<meta <meta
name='twitter:title' name="twitter:title"
content={`Welcome to ${process.env.NEXT_PUBLIC_TITLE} 👋`} content={`Welcome to ${process.env.NEXT_PUBLIC_TITLE} 👋`}
/> />
<meta <meta
name='twitter:description' name="twitter: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"
/> />
<meta <meta
name='twitter:image' name="twitter:image"
content='https://supanextail.dev/ogimage.png' content="https://supanextail.dev/ogimage.png"
/> />
<meta charSet='UTF-8' /> <meta charSet="UTF-8" />
</Head> </Head>
<Layout> <Layout>
@@ -58,5 +54,4 @@ const Home = () => {
</Layout> </Layout>
</> </>
); );
};
export default Home; export default Home;

View File

@@ -4,10 +4,10 @@ You have 2 components, the "AuthComponent" that handle the logic,
and the "AuthText" that will show the description on the left of the screen and the "AuthText" that will show the description on the left of the screen
*/ */
import Layout from "components/Layout"; import Layout from 'components/Layout';
import Login from "components/UI/Login"; import Login from 'components/UI/Login';
import { NextSeo } from "next-seo"; import { NextSeo } from 'next-seo';
import { useAuth } from "utils/AuthContext"; import { useAuth } from 'utils/AuthContext';
const LoginPage = () => { const LoginPage = () => {
const { signUp, signIn, signOut, resetPassword } = useAuth(); const { signUp, signIn, signOut, resetPassword } = useAuth();
@@ -19,7 +19,7 @@ const LoginPage = () => {
/> />
<Layout> <Layout>
<div className='flex flex-wrap justify-evenly w-full mt-20'> <div className="flex flex-wrap justify-evenly w-full mt-20">
<Login <Login
signUp={signUp} signUp={signUp}
signIn={signIn} signIn={signIn}

View File

@@ -1,20 +1,18 @@
// To modify the content of the pricing page, check the Pricing.js component // To modify the content of the pricing page, check the Pricing.js component
import Layout from "components/Layout"; import Layout from 'components/Layout';
import { NextSeo } from "next-seo"; import { NextSeo } from 'next-seo';
import Pricing from "components/Pricing"; import Pricing from 'components/Pricing';
const PricingPage = () => { const PricingPage = () => (
return (
<> <>
<NextSeo <NextSeo
title={`${process.env.NEXT_PUBLIC_TITLE} | Pricing`} 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>
<Pricing /> <Pricing />
</Layout> </Layout>
</> </>
); );
};
export default PricingPage; export default PricingPage;

View File

@@ -1,20 +1,18 @@
// To modify the privacy policy, check the PrivacyPolicy.js component // To modify the privacy policy, check the PrivacyPolicy.js component
import Layout from "components/Layout"; import Layout from 'components/Layout';
import { NextSeo } from "next-seo"; import { NextSeo } from 'next-seo';
import PrivacyPolicy from "components/PrivacyPolicy"; import PrivacyPolicy from 'components/PrivacyPolicy';
const PrivacyPage = () => { const PrivacyPage = () => (
return (
<> <>
<NextSeo <NextSeo
title={`${process.env.NEXT_PUBLIC_TITLE} | Privacy Policy`} 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>
<PrivacyPolicy /> <PrivacyPolicy />
</Layout> </Layout>
</> </>
); );
};
export default PrivacyPage; export default PrivacyPage;

View File

@@ -4,13 +4,12 @@ You have 2 components, the "AuthComponent" that handle the logic,
and the "AuthText" that will show the description on the left of the screen and the "AuthText" that will show the description on the left of the screen
*/ */
import AuthComponent from "components/SignUp"; import AuthComponent from 'components/SignUp';
import AuthText from "components/AuthText"; import AuthText from 'components/AuthText';
import Layout from "components/Layout"; import Layout from 'components/Layout';
import { NextSeo } from "next-seo"; import { NextSeo } from 'next-seo';
const SignUpPage = () => { const SignUpPage = () => (
return (
<> <>
<NextSeo <NextSeo
title={`${process.env.NEXT_PUBLIC_TITLE} | Auth`} title={`${process.env.NEXT_PUBLIC_TITLE} | Auth`}
@@ -18,13 +17,12 @@ const SignUpPage = () => {
/> />
<Layout> <Layout>
<div className='flex flex-wrap justify-evenly w-full mt-20'> <div className="flex flex-wrap justify-evenly w-full mt-20">
<AuthText /> <AuthText />
<AuthComponent /> <AuthComponent />
</div> </div>
</Layout> </Layout>
</> </>
); );
};
export default SignUpPage; export default SignUpPage;

View File

@@ -1,20 +1,18 @@
// To modify the terms & conditions, check the Terms.js component // To modify the terms & conditions, check the Terms.js component
import Layout from "components/Layout"; import Layout from 'components/Layout';
import { NextSeo } from "next-seo"; import { NextSeo } from 'next-seo';
import Terms from "components/Terms"; import Terms from 'components/Terms';
const TermsPage = () => { const TermsPage = () => (
return (
<> <>
<NextSeo <NextSeo
title={`${process.env.NEXT_PUBLIC_TITLE} | Terms and conditions`} 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>
<Terms /> <Terms />
</Layout> </Layout>
</> </>
); );
};
export default TermsPage; export default TermsPage;

View File

@@ -5,4 +5,4 @@ module.exports = {
tailwindcss: {}, tailwindcss: {},
autoprefixer: {}, autoprefixer: {},
}, },
} };

View File

@@ -1,74 +1,74 @@
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

@@ -1,6 +1,6 @@
import React, { createContext, useContext, useEffect, useState } from "react"; import React, { createContext, useContext, useEffect, useState } from 'react';
import { supabase } from "utils/supabaseClient"; import { supabase } from 'utils/supabaseClient';
// create a context for authentication // create a context for authentication
const AuthContext = createContext(); const AuthContext = createContext();
@@ -19,15 +19,15 @@ export const AuthProvider = ({ children }) => {
// Listen for changes on auth state (logged in, signed out, etc.) // Listen for changes on auth state (logged in, signed out, etc.)
const { data: listener } = supabase.auth.onAuthStateChange( const { data: listener } = supabase.auth.onAuthStateChange(
async (event, session) => { async (event, session) => {
if ((event === "SIGNED_OUT") | (event === "SIGNED_IN")) { if ((event === 'SIGNED_OUT') | (event === 'SIGNED_IN')) {
fetch("/api/auth", { fetch('/api/auth', {
method: "POST", method: 'POST',
headers: new Headers({ "Content-Type": "application/json" }), headers: new Headers({ 'Content-Type': 'application/json' }),
credentials: "same-origin", credentials: 'same-origin',
body: JSON.stringify({ event, session }), body: JSON.stringify({ event, session }),
}).then((res) => res.json()); }).then((res) => res.json());
} }
if (event === "USER_UPDATED") { if (event === 'USER_UPDATED') {
} }
setUser(session?.user ?? null); setUser(session?.user ?? null);
setLoading(false); setLoading(false);
@@ -56,6 +56,4 @@ export const AuthProvider = ({ children }) => {
}; };
// export the useAuth hook // export the useAuth hook
export const useAuth = () => { export const useAuth = () => useContext(AuthContext);
return useContext(AuthContext);
};

View File

@@ -3,43 +3,43 @@
const Prices = { const Prices = {
personal: { personal: {
monthly: { monthly: {
id: "price_1J5q2yDMjD0UnVmMXzEWYDnl", id: 'price_1J5q2yDMjD0UnVmMXzEWYDnl',
desc: "Personal plan (monthly)", desc: 'Personal plan (monthly)',
}, },
annually: { annually: {
id: "price_1J5q45DMjD0UnVmMQxXHKGAv", id: 'price_1J5q45DMjD0UnVmMQxXHKGAv',
desc: "Personal plan (annually)", desc: 'Personal plan (annually)',
}, },
}, },
team: { team: {
monthly: { monthly: {
id: "price_1J5q3GDMjD0UnVmMlHc5Eedq", id: 'price_1J5q3GDMjD0UnVmMlHc5Eedq',
desc: "Team plan (monthly)", desc: 'Team plan (monthly)',
}, },
annually: { annually: {
id: "price_1J5q8zDMjD0UnVmMqsngM91X", id: 'price_1J5q8zDMjD0UnVmMqsngM91X',
desc: "Team plan (annually)", desc: 'Team plan (annually)',
}, },
}, },
pro: { pro: {
monthly: { monthly: {
id: "price_1J6KRuDMjD0UnVmMIItaOdT3", id: 'price_1J6KRuDMjD0UnVmMIItaOdT3',
desc: "Pro plan (monthly)", desc: 'Pro plan (monthly)',
}, },
annually: { annually: {
id: "price_1J5q9VDMjD0UnVmMIQtVDSZ9", id: 'price_1J5q9VDMjD0UnVmMIQtVDSZ9',
desc: "Pro plan (annually)", desc: 'Pro plan (annually)',
}, },
}, },
}; };
const PriceIds = { const PriceIds = {
price_1J5q2yDMjD0UnVmMXzEWYDnl: "Personal plan (monthly)", price_1J5q2yDMjD0UnVmMXzEWYDnl: 'Personal plan (monthly)',
price_1J5q45DMjD0UnVmMQxXHKGAv: "Personal plan (annually)", price_1J5q45DMjD0UnVmMQxXHKGAv: 'Personal plan (annually)',
price_1J5q3GDMjD0UnVmMlHc5Eedq: "Team plan (monthly)", price_1J5q3GDMjD0UnVmMlHc5Eedq: 'Team plan (monthly)',
price_1J5q8zDMjD0UnVmMqsngM91X: "Team plan (annually)", price_1J5q8zDMjD0UnVmMqsngM91X: 'Team plan (annually)',
price_1J6KRuDMjD0UnVmMIItaOdT3: "Pro plan (monthly)", price_1J6KRuDMjD0UnVmMIItaOdT3: 'Pro plan (monthly)',
price_1J5q9VDMjD0UnVmMIQtVDSZ9: "Pro plan (annually)", price_1J5q9VDMjD0UnVmMIQtVDSZ9: 'Pro plan (annually)',
}; };
export { Prices, PriceIds }; export { Prices, PriceIds };

View File

@@ -1,7 +1,7 @@
/** /**
* This is a singleton to ensure we only instantiate Stripe once. * This is a singleton to ensure we only instantiate Stripe once.
*/ */
import { loadStripe } from "@stripe/stripe-js"; import { loadStripe } from '@stripe/stripe-js';
let stripePromise = null; let stripePromise = null;
const getStripe = () => { const getStripe = () => {

View File

@@ -1,4 +1,4 @@
import { createClient } from "@supabase/supabase-js"; import { createClient } from '@supabase/supabase-js';
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;
@@ -7,9 +7,9 @@ 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 () => {
let { data: subscriptions, error } = await supabase const { data: subscriptions, error } = await supabase
.from("subscriptions") .from('subscriptions')
.select("paid_user, plan") .select('paid_user, plan')
.single(); .single();
if (subscriptions) { if (subscriptions) {
return subscriptions; return subscriptions;