mirror of
https://github.com/fergalmoran/supanextail.git
synced 2025-12-22 09:17:54 +00:00
First implementation of eslint/prettier
This commit is contained in:
52
.eslintrc.json
Normal file
52
.eslintrc.json
Normal 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
3
.estlintignore
Normal file
@@ -0,0 +1,3 @@
|
||||
.next
|
||||
dist
|
||||
node_modules/
|
||||
5
.prettierrc.json
Normal file
5
.prettierrc.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"tabWidth": 2,
|
||||
"semi": true
|
||||
}
|
||||
@@ -1,26 +1,23 @@
|
||||
import Image from "next/image";
|
||||
import authImage from "public/auth.png";
|
||||
import Image from 'next/image';
|
||||
import authImage from 'public/auth.png';
|
||||
|
||||
const AuthText = () => {
|
||||
return (
|
||||
<div className='lg:mt-0 max-w-lg flex flex-col text-xl'>
|
||||
<div className='mt-10 mb-3 m-auto'>
|
||||
<Image
|
||||
src={authImage}
|
||||
width={authImage.width / 1.5}
|
||||
height={authImage.height / 1.5}
|
||||
/>
|
||||
</div>
|
||||
<h2 className='text-4xl font-title font-semibold text-center'>
|
||||
Join SupaNexTail for <span className='text-primary'>free</span>!
|
||||
</h2>
|
||||
<p className='mb-5 mt-8 leading-9'>
|
||||
Create your website in a few minutes with our boilerplate. You can use
|
||||
the login system, this will allow you to discover the sample dashboard
|
||||
page.
|
||||
</p>
|
||||
const AuthText = () => (
|
||||
<div className="lg:mt-0 max-w-lg flex flex-col text-xl">
|
||||
<div className="mt-10 mb-3 m-auto">
|
||||
<Image
|
||||
src={authImage}
|
||||
width={authImage.width / 1.5}
|
||||
height={authImage.height / 1.5}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
<h2 className="text-4xl font-title font-semibold text-center">
|
||||
Join SupaNexTail for <span className="text-primary">free</span>!
|
||||
</h2>
|
||||
<p className="mb-5 mt-8 leading-9">
|
||||
Create your website in a few minutes with our boilerplate. You can use the
|
||||
login system, this will allow you to discover the sample dashboard page.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default AuthText;
|
||||
|
||||
@@ -4,9 +4,9 @@ the upload.
|
||||
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 [avatarUrl, setAvatarUrl] = useState(null);
|
||||
@@ -19,7 +19,7 @@ const Avatar = ({ url, size, onUpload }) => {
|
||||
async function downloadImage(path) {
|
||||
try {
|
||||
const { data, error } = await supabase.storage
|
||||
.from("avatars")
|
||||
.from('avatars')
|
||||
.download(path);
|
||||
if (error) {
|
||||
throw error;
|
||||
@@ -27,7 +27,7 @@ const Avatar = ({ url, size, onUpload }) => {
|
||||
const url = URL.createObjectURL(data);
|
||||
setAvatarUrl(url);
|
||||
} 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);
|
||||
|
||||
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 fileExt = file.name.split(".").pop();
|
||||
const fileExt = file.name.split('.').pop();
|
||||
const fileName = `${Math.random()}.${fileExt}`;
|
||||
const filePath = `${fileName}`;
|
||||
|
||||
if (event.target.files[0].size > 150000) {
|
||||
alert("File is too big!");
|
||||
event.target.value = "";
|
||||
setUploading(false)
|
||||
alert('File is too big!');
|
||||
event.target.value = '';
|
||||
setUploading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
let { error: uploadError } = await supabase.storage
|
||||
.from("avatars")
|
||||
const { error: uploadError } = await supabase.storage
|
||||
.from('avatars')
|
||||
.upload(filePath, file);
|
||||
|
||||
if (uploadError) {
|
||||
@@ -68,30 +68,31 @@ const Avatar = ({ url, size, onUpload }) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='m-auto mb-5'>
|
||||
<div className="m-auto mb-5">
|
||||
{avatarUrl ? (
|
||||
<img
|
||||
src={avatarUrl}
|
||||
alt='Avatar'
|
||||
className='avatar rounded-full w-28 h-28 flex m-auto'
|
||||
alt="Avatar"
|
||||
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 }}>
|
||||
<label
|
||||
className='mt-2 btn btn-primary text-center cursor-pointer text-xs btn-sm'
|
||||
htmlFor='single'>
|
||||
{uploading ? "Uploading ..." : "Update my avatar"}
|
||||
className="mt-2 btn btn-primary text-center cursor-pointer text-xs btn-sm"
|
||||
htmlFor="single"
|
||||
>
|
||||
{uploading ? 'Uploading ...' : 'Update my avatar'}
|
||||
</label>
|
||||
<input
|
||||
style={{
|
||||
visibility: "hidden",
|
||||
position: "absolute",
|
||||
visibility: 'hidden',
|
||||
position: 'absolute',
|
||||
}}
|
||||
type='file'
|
||||
id='single'
|
||||
accept='image/*'
|
||||
type="file"
|
||||
id="single"
|
||||
accept="image/*"
|
||||
onChange={uploadAvatar}
|
||||
disabled={uploading}
|
||||
/>
|
||||
|
||||
@@ -1,62 +1,60 @@
|
||||
import CardLanding from "components/UI/CardLanding";
|
||||
import cardAuth from "public/landing/auth.svg";
|
||||
import cardFee from "public/landing/lifetime.svg";
|
||||
import cardPage from "public/landing/page.svg";
|
||||
import cardResponsive from "public/landing/responsive.svg";
|
||||
import cardServer from "public/landing/backend.svg";
|
||||
import cardStripe from "public/landing/stripe.svg";
|
||||
import cardTheme from "public/landing/theme.svg";
|
||||
import CardLanding from 'components/UI/CardLanding';
|
||||
import cardAuth from 'public/landing/auth.svg';
|
||||
import cardFee from 'public/landing/lifetime.svg';
|
||||
import cardPage from 'public/landing/page.svg';
|
||||
import cardResponsive from 'public/landing/responsive.svg';
|
||||
import cardServer from 'public/landing/backend.svg';
|
||||
import cardStripe from 'public/landing/stripe.svg';
|
||||
import cardTheme from 'public/landing/theme.svg';
|
||||
|
||||
const CardsLanding = () => {
|
||||
return (
|
||||
<div className='mt-14'>
|
||||
<h2 className='uppercase font-bold text-4xl tracking-wide text-center'>
|
||||
We've got you covered
|
||||
</h2>
|
||||
<p className='max-w-md m-auto text-center'>
|
||||
Don’t waste your time and reinvent the wheel, we have provided you with
|
||||
a maximum of features so that you only have one goal, to make your SaaS
|
||||
a reality.
|
||||
</p>
|
||||
<div className='flex flex-wrap justify-center mt-10'>
|
||||
<CardLanding
|
||||
image={cardPage}
|
||||
text='7 pages fully designed and easily customizable'
|
||||
title='Templates'
|
||||
/>
|
||||
<CardLanding
|
||||
image={cardServer}
|
||||
text='Integrated backend already setup with Next.js API Routes'
|
||||
title='Backend'
|
||||
/>
|
||||
<CardLanding
|
||||
image={cardAuth}
|
||||
text='Auth and user management with Supabase'
|
||||
title='Auth'
|
||||
/>
|
||||
<CardLanding
|
||||
image={cardResponsive}
|
||||
text='Mobile ready, fully responsive and customizable with Tailwind CSS'
|
||||
title='Responsive'
|
||||
/>
|
||||
<CardLanding
|
||||
image={cardTheme}
|
||||
text='Custom themes available and easily switch to dark mode'
|
||||
title='Themes'
|
||||
/>
|
||||
<CardLanding
|
||||
image={cardStripe}
|
||||
text='Stripe integration. Fully functional subscription system'
|
||||
title='Payment'
|
||||
/>
|
||||
<CardLanding
|
||||
image={cardFee}
|
||||
text='One-time fee. No subscription, you’ll have access to all the updates'
|
||||
title='Lifetime access'
|
||||
/>
|
||||
</div>
|
||||
const CardsLanding = () => (
|
||||
<div className="mt-14">
|
||||
<h2 className="uppercase font-bold text-4xl tracking-wide text-center">
|
||||
We've got you covered
|
||||
</h2>
|
||||
<p className="max-w-md m-auto text-center">
|
||||
Don’t waste your time and reinvent the wheel, we have provided you with a
|
||||
maximum of features so that you only have one goal, to make your SaaS a
|
||||
reality.
|
||||
</p>
|
||||
<div className="flex flex-wrap justify-center mt-10">
|
||||
<CardLanding
|
||||
image={cardPage}
|
||||
text="7 pages fully designed and easily customizable"
|
||||
title="Templates"
|
||||
/>
|
||||
<CardLanding
|
||||
image={cardServer}
|
||||
text="Integrated backend already setup with Next.js API Routes"
|
||||
title="Backend"
|
||||
/>
|
||||
<CardLanding
|
||||
image={cardAuth}
|
||||
text="Auth and user management with Supabase"
|
||||
title="Auth"
|
||||
/>
|
||||
<CardLanding
|
||||
image={cardResponsive}
|
||||
text="Mobile ready, fully responsive and customizable with Tailwind CSS"
|
||||
title="Responsive"
|
||||
/>
|
||||
<CardLanding
|
||||
image={cardTheme}
|
||||
text="Custom themes available and easily switch to dark mode"
|
||||
title="Themes"
|
||||
/>
|
||||
<CardLanding
|
||||
image={cardStripe}
|
||||
text="Stripe integration. Fully functional subscription system"
|
||||
title="Payment"
|
||||
/>
|
||||
<CardLanding
|
||||
image={cardFee}
|
||||
text="One-time fee. No subscription, you’ll have access to all the updates"
|
||||
title="Lifetime access"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
</div>
|
||||
);
|
||||
|
||||
export default CardsLanding;
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
import axios from "axios";
|
||||
import { toast } from "react-toastify";
|
||||
import axios from 'axios';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
const Contact = () => {
|
||||
const sendEmail = () => {
|
||||
const name = document.getElementById("name").value;
|
||||
const email = document.getElementById("email").value;
|
||||
const message = document.getElementById("message").value;
|
||||
const name = document.getElementById('name').value;
|
||||
const email = document.getElementById('email').value;
|
||||
const message = document.getElementById('message').value;
|
||||
|
||||
if (name && email && message) {
|
||||
axios
|
||||
.post("/api/sendgrid", { email, name, message })
|
||||
.post('/api/sendgrid', { email, name, message })
|
||||
.then((result) => {
|
||||
if (result.data.success === true) {
|
||||
toast.success(result.data.message);
|
||||
document.getElementById("name").value = "";
|
||||
document.getElementById("email").value = "";
|
||||
document.getElementById("message").value = "";
|
||||
document.getElementById('name').value = '';
|
||||
document.getElementById('email').value = '';
|
||||
document.getElementById('message').value = '';
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
});
|
||||
} else {
|
||||
toast.info("Please enter at least one URL", {
|
||||
position: "top-center",
|
||||
toast.info('Please enter at least one URL', {
|
||||
position: 'top-center',
|
||||
autoClose: 2000,
|
||||
hideProgressBar: true,
|
||||
closeOnClick: true,
|
||||
@@ -42,51 +42,56 @@ const Contact = () => {
|
||||
}
|
||||
};
|
||||
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 className='flex justify-center'>
|
||||
<h2 className='text-3xl sm:text-4xl text-center mb-5 mt-0 font-bold font-title'>
|
||||
<div className="flex justify-center">
|
||||
<h2 className="text-3xl sm:text-4xl text-center mb-5 mt-0 font-bold font-title">
|
||||
Contact
|
||||
</h2>
|
||||
</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
|
||||
to integrate? A bug to report? Don't hesitate!
|
||||
</p>
|
||||
</div>
|
||||
<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'>
|
||||
<label className='font-light mb-4 text-left'>Your Name</label>
|
||||
<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">
|
||||
<p className="font-light mb-4 text-left">Your Name</p>
|
||||
<input
|
||||
id='name'
|
||||
name='name'
|
||||
placeholder='Enter your name'
|
||||
className='input input-primary input-bordered'></input>
|
||||
id="name"
|
||||
name="name"
|
||||
placeholder="Enter your name"
|
||||
className="input input-primary input-bordered"
|
||||
/>
|
||||
</div>
|
||||
<div className='flex flex-col max-w-xs mb-3'>
|
||||
<label className='font-light mb-4 text-left'>Your email</label>
|
||||
<div className="flex flex-col max-w-xs mb-3">
|
||||
<p className="font-light mb-4 text-left">Your email</p>
|
||||
<input
|
||||
id='email'
|
||||
name='email'
|
||||
placeholder='Enter your email adress'
|
||||
className='input input-primary input-bordered'></input>
|
||||
id="email"
|
||||
name="email"
|
||||
placeholder="Enter your email adress"
|
||||
className="input input-primary input-bordered"
|
||||
/>
|
||||
</div>
|
||||
<div className='flex flex-col col-span-full w-fulll'>
|
||||
<label className='font-light mb-4 text-left'>Message</label>
|
||||
<div className="flex flex-col col-span-full w-fulll">
|
||||
<p className="font-light mb-4 text-left">Message</p>
|
||||
<textarea
|
||||
id='message'
|
||||
name='message'
|
||||
placeholder='Enter your message here...'
|
||||
rows='5'
|
||||
className='input input-primary input-bordered resize-none w-full h-32 pt-2'></textarea>
|
||||
id="message"
|
||||
name="message"
|
||||
placeholder="Enter your message here..."
|
||||
rows="5"
|
||||
className="input input-primary input-bordered resize-none w-full h-32 pt-2"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
className='btn btn-primary btn-sm'
|
||||
type="button"
|
||||
className="btn btn-primary btn-sm"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
sendEmail();
|
||||
}}>
|
||||
Submit{" "}
|
||||
}}
|
||||
>
|
||||
Submit{' '}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -5,16 +5,16 @@ function with your new elements.
|
||||
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 PaymentModal from "./PaymentModal";
|
||||
import Plan from "public/plan.svg";
|
||||
import { PriceIds } from "utils/priceList";
|
||||
import { supabase } from "../utils/supabaseClient";
|
||||
import { toast } from "react-toastify";
|
||||
import { useRouter } from "next/router";
|
||||
import Image from 'next/image';
|
||||
import Plan from 'public/plan.svg';
|
||||
import { PriceIds } from 'utils/priceList';
|
||||
import { toast } from 'react-toastify';
|
||||
import { useRouter } from 'next/router';
|
||||
import { supabase } from '../utils/supabaseClient';
|
||||
import PaymentModal from './PaymentModal';
|
||||
import Avatar from './Avatar';
|
||||
|
||||
export default function Dashboard(props) {
|
||||
const router = useRouter();
|
||||
@@ -25,7 +25,7 @@ export default function Dashboard(props) {
|
||||
const [payment, setPayment] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (router.query.session_id && router.query.session_id !== "canceled") {
|
||||
if (router.query.session_id && router.query.session_id !== 'canceled') {
|
||||
setPayment(true);
|
||||
}
|
||||
}, []);
|
||||
@@ -43,8 +43,8 @@ export default function Dashboard(props) {
|
||||
updated_at: new Date(),
|
||||
};
|
||||
|
||||
let { error } = await supabase.from("profiles").upsert(updates, {
|
||||
returning: "minimal", // Don't return the value after inserting
|
||||
const { error } = await supabase.from('profiles').upsert(updates, {
|
||||
returning: 'minimal', // Don't return the value after inserting
|
||||
});
|
||||
|
||||
if (error) {
|
||||
@@ -54,14 +54,14 @@ export default function Dashboard(props) {
|
||||
alert(error.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
toast.success("Your profile has been updated");
|
||||
toast.success('Your profile has been updated');
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<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'>
|
||||
<h1 className='text-4xl font-bold md:text-5xl font-title text-center mb-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">
|
||||
<h1 className="text-4xl font-bold md:text-5xl font-title text-center mb-10">
|
||||
Dashboard
|
||||
</h1>
|
||||
<Avatar
|
||||
@@ -72,60 +72,59 @@ export default function Dashboard(props) {
|
||||
updateProfile({ username, website, avatar_url: url });
|
||||
}}
|
||||
/>
|
||||
<div className='mb-5 flex flex-col'>
|
||||
<label htmlFor='email' className='my-auto text-sm mb-2'>
|
||||
<div className="mb-5 flex flex-col">
|
||||
<label htmlFor="email" className="my-auto text-sm mb-2">
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
className='input input-primary input-bordered input-sm flex-1 text-base-100'
|
||||
id='email'
|
||||
type='text'
|
||||
className="input input-primary input-bordered input-sm flex-1 text-base-100"
|
||||
id="email"
|
||||
type="text"
|
||||
value={props.session.user.email}
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
<div className='mb-5 flex flex-col'>
|
||||
<label htmlFor='username' className='my-auto text-sm mb-2'>
|
||||
<div className="mb-5 flex flex-col">
|
||||
<label htmlFor="username" className="my-auto text-sm mb-2">
|
||||
Name
|
||||
</label>
|
||||
<input
|
||||
className='input input-primary input-bordered input-sm flex-1'
|
||||
id='username'
|
||||
type='text'
|
||||
value={username || ""}
|
||||
className="input input-primary input-bordered input-sm flex-1"
|
||||
id="username"
|
||||
type="text"
|
||||
value={username || ''}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className='mb-5 flex flex-col'>
|
||||
<label htmlFor='website' className='my-auto text-sm mb-2'>
|
||||
<div className="mb-5 flex flex-col">
|
||||
<label htmlFor="website" className="my-auto text-sm mb-2">
|
||||
Website
|
||||
</label>
|
||||
<input
|
||||
className='input input-primary input-bordered input-sm flex-1'
|
||||
id='website'
|
||||
type='website'
|
||||
value={website || ""}
|
||||
className="input input-primary input-bordered input-sm flex-1"
|
||||
id="website"
|
||||
type="website"
|
||||
value={website || ''}
|
||||
onChange={(e) => setWebsite(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='m-auto'>
|
||||
<div className="m-auto">
|
||||
<button
|
||||
className='btn btn-primary btn-sm'
|
||||
className="btn btn-primary btn-sm"
|
||||
onClick={() => updateProfile({ username, website, avatar_url })}
|
||||
disabled={loading}>
|
||||
{loading ? "Loading ..." : "Update My Profile"}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? 'Loading ...' : 'Update My Profile'}
|
||||
</button>
|
||||
</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} />
|
||||
<div className='flex flex-col m-auto'>
|
||||
<div className="flex flex-col m-auto">
|
||||
<h2>Your current plan</h2>
|
||||
<p className=''>
|
||||
{props.plan ? PriceIds[props.plan] : "Free tier"}
|
||||
</p>
|
||||
<p className="">{props.plan ? PriceIds[props.plan] : 'Free tier'}</p>
|
||||
</div>
|
||||
</div>
|
||||
<PaymentModal open={payment} setPayment={setPayment} />
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
import Link from "next/link";
|
||||
import dynamic from "next/dynamic";
|
||||
import Link from 'next/link';
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
const Footer = () => {
|
||||
const ThemeToggle = dynamic(() => import("components/UI/ThemeToggle.js"), {
|
||||
const ThemeToggle = dynamic(() => import('components/UI/ThemeToggle.js'), {
|
||||
ssr: false,
|
||||
});
|
||||
return (
|
||||
<footer className='w-full flex'>
|
||||
<nav className=' mr-auto'>
|
||||
<div className='flex flex-col sm:flex-row justify-evenly w-full sm:space-x-10'>
|
||||
<div className=''>© {process.env.NEXT_PUBLIC_TITLE}</div>
|
||||
<Link href='/privacy'>
|
||||
<footer className="w-full flex">
|
||||
<nav className=" mr-auto">
|
||||
<div className="flex flex-col sm:flex-row justify-evenly w-full sm:space-x-10">
|
||||
<div className="">© {process.env.NEXT_PUBLIC_TITLE}</div>
|
||||
<Link href="/privacy">
|
||||
<a>Privacy Policy</a>
|
||||
</Link>
|
||||
<Link href='/terms'>
|
||||
<Link href="/terms">
|
||||
<a>Terms of service</a>
|
||||
</Link>
|
||||
</div>
|
||||
</nav>
|
||||
<div className='mr-5 my-auto'>
|
||||
<div className="mr-5 my-auto">
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
@@ -1,67 +1,65 @@
|
||||
import CardsLanding from "components/CardsLanding";
|
||||
import Image from "next/image";
|
||||
import MailingList from "./MailingList";
|
||||
import landTop from "public/landing/land-top.svg";
|
||||
import start from "public/landing/start.svg";
|
||||
import supabaseImage from "public/landing/supabase.svg";
|
||||
import CardsLanding from 'components/CardsLanding';
|
||||
import Image from 'next/image';
|
||||
import landTop from 'public/landing/land-top.svg';
|
||||
import start from 'public/landing/start.svg';
|
||||
import supabaseImage from 'public/landing/supabase.svg';
|
||||
import MailingList from './MailingList';
|
||||
|
||||
const Landing = () => {
|
||||
return (
|
||||
<div className='mt-10 mb-20 text-base-content w-full'>
|
||||
<div className='flex max-w-6xl m-auto justify-around'>
|
||||
<div className='max-w-sm mr-16 my-auto'>
|
||||
<h2 className='text-4xl font-bold font-title text-left leading-normal'>
|
||||
Build your <span className='text-primary'>SaaS</span> in the blink
|
||||
of an eye!
|
||||
</h2>
|
||||
<p>
|
||||
SupaNexTail got your back, and takes care of the initial setup,
|
||||
sometimes time consuming, but essential to your success.
|
||||
</p>
|
||||
</div>
|
||||
<div className='max-w-xl'>
|
||||
<Image src={landTop} height={417} width={583} />
|
||||
</div>
|
||||
const Landing = () => (
|
||||
<div className="mt-10 mb-20 text-base-content w-full">
|
||||
<div className="flex max-w-6xl m-auto justify-around">
|
||||
<div className="max-w-sm mr-16 my-auto">
|
||||
<h2 className="text-4xl font-bold font-title text-left leading-normal">
|
||||
Build your <span className="text-primary">SaaS</span> in the blink of
|
||||
an eye!
|
||||
</h2>
|
||||
<p>
|
||||
SupaNexTail got your back, and takes care of the initial setup,
|
||||
sometimes time consuming, but essential to your success.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<CardsLanding />
|
||||
<div className='flex max-w-6xl m-auto justify-around mt-14 flex-wrap'>
|
||||
<div className='max-w-sm mr-16 my-auto'>
|
||||
<h2 className='text-4xl font-bold font-title text-left leading-normal'>
|
||||
All you need to start <span className='text-primary'>now</span>
|
||||
</h2>
|
||||
<p>
|
||||
SupaNexTail got your back, and takes care of the initial setup,
|
||||
sometimes time consuming, but essential to your success.
|
||||
</p>
|
||||
</div>
|
||||
<div className='max-w-xl'>
|
||||
<Image src={start} />
|
||||
</div>
|
||||
<div className="max-w-xl">
|
||||
<Image src={landTop} height={417} width={583} />
|
||||
</div>
|
||||
<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'>
|
||||
<h2 className='text-4xl font-bold font-title text-left leading-normal'>
|
||||
Leverage the power of <span className='text-primary'>Supabase</span>
|
||||
</h2>
|
||||
<p>
|
||||
Supabase is an open source Firebase alternative. You’ll have a
|
||||
database, an auth system, a storage system, and much more in one
|
||||
product.
|
||||
</p>
|
||||
<p>
|
||||
SupaNexTail uses Supabase at its core, and preconfigures all the
|
||||
useful elements for your site. User registration, synchronization
|
||||
with Stripe, we’ve got you covered!
|
||||
</p>
|
||||
</div>
|
||||
<div className='max-w-md order-2 lg:order-1 flex'>
|
||||
<Image src={supabaseImage} />
|
||||
</div>
|
||||
</div>
|
||||
<MailingList />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
<CardsLanding />
|
||||
<div className="flex max-w-6xl m-auto justify-around mt-14 flex-wrap">
|
||||
<div className="max-w-sm mr-16 my-auto">
|
||||
<h2 className="text-4xl font-bold font-title text-left leading-normal">
|
||||
All you need to start <span className="text-primary">now</span>
|
||||
</h2>
|
||||
<p>
|
||||
SupaNexTail got your back, and takes care of the initial setup,
|
||||
sometimes time consuming, but essential to your success.
|
||||
</p>
|
||||
</div>
|
||||
<div className="max-w-xl">
|
||||
<Image src={start} />
|
||||
</div>
|
||||
</div>
|
||||
<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">
|
||||
<h2 className="text-4xl font-bold font-title text-left leading-normal">
|
||||
Leverage the power of <span className="text-primary">Supabase</span>
|
||||
</h2>
|
||||
<p>
|
||||
Supabase is an open source Firebase alternative. You’ll have a
|
||||
database, an auth system, a storage system, and much more in one
|
||||
product.
|
||||
</p>
|
||||
<p>
|
||||
SupaNexTail uses Supabase at its core, and preconfigures all the
|
||||
useful elements for your site. User registration, synchronization with
|
||||
Stripe, we’ve got you covered!
|
||||
</p>
|
||||
</div>
|
||||
<div className="max-w-md order-2 lg:order-1 flex">
|
||||
<Image src={supabaseImage} />
|
||||
</div>
|
||||
</div>
|
||||
<MailingList />
|
||||
</div>
|
||||
);
|
||||
|
||||
export default Landing;
|
||||
|
||||
@@ -10,59 +10,60 @@ You also have the head component containing all the favicon for different platfo
|
||||
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 Nav from "./Nav";
|
||||
import { ToastContainer } from "react-toastify";
|
||||
import { useAuth } from "utils/AuthContext";
|
||||
import Head from 'next/head';
|
||||
import { ToastContainer } from 'react-toastify';
|
||||
import { useAuth } from 'utils/AuthContext';
|
||||
import Nav from './Nav';
|
||||
import Footer from './Footer';
|
||||
|
||||
const Layout = (props) => {
|
||||
const { user, signOut } = useAuth();
|
||||
|
||||
const toastStyle = {
|
||||
//Style your toast elements here
|
||||
success: "bg-accent",
|
||||
error: "bg-red-600",
|
||||
info: "bg-gray-600",
|
||||
warning: "bg-orange-400",
|
||||
default: "bg-primary",
|
||||
dark: "bg-white-600 font-gray-300",
|
||||
// Style your toast elements here
|
||||
success: 'bg-accent',
|
||||
error: 'bg-red-600',
|
||||
info: 'bg-gray-600',
|
||||
warning: 'bg-orange-400',
|
||||
default: 'bg-primary',
|
||||
dark: 'bg-white-600 font-gray-300',
|
||||
};
|
||||
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>
|
||||
<link
|
||||
rel='apple-touch-icon'
|
||||
sizes='180x180'
|
||||
href='/apple-touch-icon.png'
|
||||
rel="apple-touch-icon"
|
||||
sizes="180x180"
|
||||
href="/apple-touch-icon.png"
|
||||
/>
|
||||
<link
|
||||
rel='icon'
|
||||
type='image/png'
|
||||
sizes='32x32'
|
||||
href='/favicon-32x32.png'
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="32x32"
|
||||
href="/favicon-32x32.png"
|
||||
/>
|
||||
<link
|
||||
rel='icon'
|
||||
type='image/png'
|
||||
sizes='16x16'
|
||||
href='/favicon-16x16.png'
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="16x16"
|
||||
href="/favicon-16x16.png"
|
||||
/>
|
||||
<link rel='manifest' href='/site.webmanifest' />
|
||||
<link rel='mask-icon' href='/safari-pinned-tab.svg' color='#5bbad5' />
|
||||
<meta name='msapplication-TileColor' content='#da532c' />
|
||||
<meta name='theme-color' content='#ffffff' />
|
||||
<link rel="manifest" href="/site.webmanifest" />
|
||||
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5" />
|
||||
<meta name="msapplication-TileColor" content="#da532c" />
|
||||
<meta name="theme-color" content="#ffffff" />
|
||||
</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} />
|
||||
<main className='flex-1'>{props.children}</main>
|
||||
<main className="flex-1">{props.children}</main>
|
||||
<ToastContainer
|
||||
position='bottom-center'
|
||||
position="bottom-center"
|
||||
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 />
|
||||
|
||||
@@ -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)
|
||||
*/
|
||||
|
||||
import Image from "next/image";
|
||||
import Mailing from "public/landing/mailing.svg";
|
||||
import axios from "axios";
|
||||
import { toast } from "react-toastify";
|
||||
import { useState } from "react";
|
||||
import Image from 'next/image';
|
||||
import Mailing from 'public/landing/mailing.svg';
|
||||
import axios from 'axios';
|
||||
import { toast } from 'react-toastify';
|
||||
import { useState } from 'react';
|
||||
|
||||
const MailingList = () => {
|
||||
const [mail, setMail] = useState(null);
|
||||
@@ -16,7 +16,7 @@ const MailingList = () => {
|
||||
|
||||
const validateEmail = () => {
|
||||
// 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,}))$/;
|
||||
|
||||
if (regex.test(mail)) {
|
||||
@@ -25,7 +25,7 @@ const MailingList = () => {
|
||||
setValid(true);
|
||||
} else {
|
||||
// invalid email.
|
||||
toast.error("Your email is invalid");
|
||||
toast.error('Your email is invalid');
|
||||
setValid(false);
|
||||
}
|
||||
};
|
||||
@@ -33,7 +33,7 @@ const MailingList = () => {
|
||||
const subscribe = () => {
|
||||
setLoading(true);
|
||||
axios
|
||||
.put("api/mailingList", {
|
||||
.put('api/mailingList', {
|
||||
mail,
|
||||
})
|
||||
.then((result) => {
|
||||
@@ -48,32 +48,34 @@ const MailingList = () => {
|
||||
});
|
||||
};
|
||||
return (
|
||||
<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'>
|
||||
<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">
|
||||
Stay Tuned
|
||||
</h2>
|
||||
<Image src={Mailing}/>
|
||||
<label className='label'>
|
||||
<p className='text-center max-w-md m-auto'>
|
||||
<Image src={Mailing} />
|
||||
<label className="label">
|
||||
<p className="text-center max-w-md m-auto">
|
||||
Want to be the first to know when SupaNexTail launches and get an
|
||||
exclusive discount? Sign up for the newsletter!
|
||||
</p>
|
||||
</label>
|
||||
<div className='mt-5 m-auto'>
|
||||
<div className="mt-5 m-auto">
|
||||
<input
|
||||
onChange={(e) => {
|
||||
setMail(e.target.value);
|
||||
}}
|
||||
type='email'
|
||||
placeholder='Your email'
|
||||
type="email"
|
||||
placeholder="Your email"
|
||||
className={`input input-primary input-bordered ${
|
||||
valid ? null : "input-error"
|
||||
}`}></input>
|
||||
valid ? null : 'input-error'
|
||||
}`}
|
||||
/>
|
||||
<button
|
||||
onClick={validateEmail}
|
||||
className={`btn ml-3 ${
|
||||
loading ? "btn-disabled loading" : "btn-primary"
|
||||
}`}>
|
||||
loading ? 'btn-disabled loading' : 'btn-primary'
|
||||
}`}
|
||||
>
|
||||
I'm in!
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -2,42 +2,42 @@
|
||||
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 Link from "next/link";
|
||||
import Logo from "public/logo.svg";
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import Logo from 'public/logo.svg';
|
||||
|
||||
const Nav = (props) => {
|
||||
//Modify you menu directly here
|
||||
// Modify you menu directly here
|
||||
const NavMenu = (
|
||||
<>
|
||||
{props.user && (
|
||||
<Link href='/dashboard'>
|
||||
<a className='nav-btn'>Dashboard</a>
|
||||
<Link href="/dashboard">
|
||||
<a className="nav-btn">Dashboard</a>
|
||||
</Link>
|
||||
)}
|
||||
|
||||
<Link href='/pricing'>
|
||||
<a className='nav-btn'>Pricing</a>
|
||||
<Link href="/pricing">
|
||||
<a className="nav-btn">Pricing</a>
|
||||
</Link>
|
||||
|
||||
<Link href='/contact'>
|
||||
<a className='nav-btn'>Contact Us</a>
|
||||
<Link href="/contact">
|
||||
<a className="nav-btn">Contact Us</a>
|
||||
</Link>
|
||||
|
||||
{props.user ? (
|
||||
<button className='btn btn-xs text-xs' onClick={() => props.signOut()}>
|
||||
<LogOut size={12} className='mr-2' />
|
||||
<button className="btn btn-xs text-xs" onClick={() => props.signOut()}>
|
||||
<LogOut size={12} className="mr-2" />
|
||||
Logout
|
||||
</button>
|
||||
) : (
|
||||
<>
|
||||
<Link href='/login'>
|
||||
<a className='nav-btn'>Login</a>
|
||||
<Link href="/login">
|
||||
<a className="nav-btn">Login</a>
|
||||
</Link>
|
||||
<Link href='/signup'>
|
||||
<a className='btn btn-sm btn-primary font-body normal-case font-normal'>
|
||||
<Link href="/signup">
|
||||
<a className="btn btn-sm btn-primary font-body normal-case font-normal">
|
||||
Sign Up
|
||||
</a>
|
||||
</Link>
|
||||
@@ -47,22 +47,22 @@ const Nav = (props) => {
|
||||
);
|
||||
|
||||
return (
|
||||
<nav className='navbar mb-2 w-full'>
|
||||
<Link href='/'>
|
||||
<nav className="navbar mb-2 w-full">
|
||||
<Link href="/">
|
||||
<a>
|
||||
<Image src={Logo} />
|
||||
</a>
|
||||
</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}
|
||||
</div>
|
||||
<div className='ml-auto lg:hidden'>
|
||||
<div className='dropdown dropdown-end' data-cy='dropdown'>
|
||||
<div tabIndex='0' className='m-1 cursor-pointer'>
|
||||
<div className="ml-auto lg:hidden">
|
||||
<div className="dropdown dropdown-end" data-cy="dropdown">
|
||||
<div tabIndex="0" className="m-1 cursor-pointer">
|
||||
<Menu />
|
||||
</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}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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) => {
|
||||
function closeModal() {
|
||||
@@ -11,53 +11,59 @@ const PaymentModal = (props) => {
|
||||
<>
|
||||
<Transition appear show={props.open} as={Fragment}>
|
||||
<Dialog
|
||||
as='div'
|
||||
className='fixed inset-0 z-10 overflow-y-auto bg-gray-500 bg-opacity-50'
|
||||
onClose={closeModal}>
|
||||
<div className='min-h-screen px-4 text-center'>
|
||||
as="div"
|
||||
className="fixed inset-0 z-10 overflow-y-auto bg-gray-500 bg-opacity-50"
|
||||
onClose={closeModal}
|
||||
>
|
||||
<div className="min-h-screen px-4 text-center">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter='ease-out duration-300'
|
||||
enterFrom='opacity-0'
|
||||
enterTo='opacity-100'
|
||||
leave='ease-in duration-200'
|
||||
leaveFrom='opacity-100'
|
||||
leaveTo='opacity-0'>
|
||||
<Dialog.Overlay className='fixed inset-0' />
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<Dialog.Overlay className="fixed inset-0" />
|
||||
</Transition.Child>
|
||||
|
||||
{/* This element is to trick the browser into centering the modal contents. */}
|
||||
<span
|
||||
className='inline-block h-screen align-middle'
|
||||
aria-hidden='true'>
|
||||
className="inline-block h-screen align-middle"
|
||||
aria-hidden="true"
|
||||
>
|
||||
​
|
||||
</span>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter='ease-out duration-300'
|
||||
enterFrom='opacity-0 scale-95'
|
||||
enterTo='opacity-100 scale-100'
|
||||
leave='ease-in duration-200'
|
||||
leaveFrom='opacity-100 scale-100'
|
||||
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'>
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 scale-95"
|
||||
enterTo="opacity-100 scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
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">
|
||||
<Dialog.Title
|
||||
as='h3'
|
||||
className='text-2xl font-bold leading-6 mb-5 text-center'>
|
||||
as="h3"
|
||||
className="text-2xl font-bold leading-6 mb-5 text-center"
|
||||
>
|
||||
Payment successful 🎉
|
||||
</Dialog.Title>
|
||||
<div className='mt-2'>
|
||||
<div className="mt-2">
|
||||
<p>
|
||||
Your payment has been successfully submitted. Thank you for
|
||||
your support!
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className='mt-4'>
|
||||
<div className="mt-4">
|
||||
<button
|
||||
type='button'
|
||||
className='btn btn-accent flex m-auto'
|
||||
onClick={closeModal}>
|
||||
type="button"
|
||||
className="btn btn-accent flex m-auto"
|
||||
onClick={closeModal}
|
||||
>
|
||||
Got it, thanks!
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -6,14 +6,14 @@ Dont forget to create your customer portal on Stripe
|
||||
https://dashboard.stripe.com/test/settings/billing/portal
|
||||
*/
|
||||
|
||||
import { getSub, supabase } from "utils/supabaseClient";
|
||||
import { useEffect, useState } from "react";
|
||||
import { getSub, supabase } from 'utils/supabaseClient';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { Prices } from "utils/priceList";
|
||||
import { Switch } from "@headlessui/react";
|
||||
import axios from "axios";
|
||||
import router from "next/router";
|
||||
import { useAuth } from "utils/AuthContext";
|
||||
import { Prices } from 'utils/priceList';
|
||||
import { Switch } from '@headlessui/react';
|
||||
import axios from 'axios';
|
||||
import router from 'next/router';
|
||||
import { useAuth } from 'utils/AuthContext';
|
||||
|
||||
const Pricing = () => {
|
||||
const [enabled, setEnabled] = useState(false);
|
||||
@@ -24,8 +24,8 @@ const Pricing = () => {
|
||||
|
||||
const portal = () => {
|
||||
axios
|
||||
.post("/api/stripe/customer-portal", {
|
||||
customerId: customerId,
|
||||
.post('/api/stripe/customer-portal', {
|
||||
customerId,
|
||||
})
|
||||
.then((result) => {
|
||||
router.push(result.data.url);
|
||||
@@ -36,9 +36,9 @@ const Pricing = () => {
|
||||
if (user) {
|
||||
getSub().then((result) => setSub(result));
|
||||
supabase
|
||||
.from("subscriptions")
|
||||
.from('subscriptions')
|
||||
.select(`customer_id`)
|
||||
.eq("id", user.id)
|
||||
.eq('id', user.id)
|
||||
.single()
|
||||
.then((result) => {
|
||||
setCustomerId(result.data?.customer_id);
|
||||
@@ -48,19 +48,19 @@ const Pricing = () => {
|
||||
|
||||
const pricing = {
|
||||
monthly: {
|
||||
personal: "$5/mo",
|
||||
team: "$15/mo",
|
||||
pro: "$35/mo",
|
||||
personal: '$5/mo',
|
||||
team: '$15/mo',
|
||||
pro: '$35/mo',
|
||||
},
|
||||
yearly: {
|
||||
personal: "$50/yr",
|
||||
team: "$150/yr",
|
||||
pro: "$350/yr",
|
||||
personal: '$50/yr',
|
||||
team: '$150/yr',
|
||||
pro: '$350/yr',
|
||||
},
|
||||
flat: {
|
||||
personal: "€49",
|
||||
team: "€99",
|
||||
pro: "€149",
|
||||
personal: '€49',
|
||||
team: '€99',
|
||||
pro: '€149',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -68,31 +68,31 @@ const Pricing = () => {
|
||||
e.preventDefault();
|
||||
// Create a Checkout Session. This will redirect the user to the Stripe website for the payment.
|
||||
axios
|
||||
.post("/api/stripe/create-checkout-session", {
|
||||
priceId: priceId,
|
||||
.post('/api/stripe/create-checkout-session', {
|
||||
priceId,
|
||||
email: user.email,
|
||||
customerId: customerId,
|
||||
customerId,
|
||||
userId: user.id,
|
||||
tokenId: session.access_token,
|
||||
pay_mode: flat ? "payment" : "subscription",
|
||||
pay_mode: flat ? 'payment' : 'subscription',
|
||||
})
|
||||
.then((result) => router.push(result.data.url));
|
||||
};
|
||||
return (
|
||||
<div className='w-full mx-auto px-5 py-10 mb-10'>
|
||||
<div className='text-center max-w-xl mx-auto'>
|
||||
<h1 className='text-3xl sm:text-5xl font-bold font-title mb-5'>
|
||||
<div className="w-full mx-auto px-5 py-10 mb-10">
|
||||
<div className="text-center max-w-xl mx-auto">
|
||||
<h1 className="text-3xl sm:text-5xl font-bold font-title mb-5">
|
||||
Pricing
|
||||
</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,
|
||||
monthly or yearly.
|
||||
</h3>
|
||||
</div>
|
||||
{!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>
|
||||
<p className={`${enabled ? "text-gray-500" : null}`}>
|
||||
<p className={`${enabled ? 'text-gray-500' : null}`}>
|
||||
Billed monthly
|
||||
</p>
|
||||
</div>
|
||||
@@ -103,45 +103,46 @@ const Pricing = () => {
|
||||
className={`bg-primary relative inline-flex flex-shrink-0 h-[38px] w-[74px]
|
||||
border-2 border-transparent rounded-full cursor-pointer transition-colors
|
||||
ease-in-out duration-200 focus:outline-none focus-visible:ring-2
|
||||
focus-visible:ring-white focus-visible:ring-opacity-75`}>
|
||||
<span className='sr-only'>Switch bill</span>
|
||||
focus-visible:ring-white focus-visible:ring-opacity-75`}
|
||||
>
|
||||
<span className="sr-only">Switch bill</span>
|
||||
<span
|
||||
aria-hidden='true'
|
||||
className={`${enabled ? "translate-x-9" : "translate-x-0"}
|
||||
aria-hidden="true"
|
||||
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`}
|
||||
/>
|
||||
</Switch>
|
||||
</div>
|
||||
<div>
|
||||
<p className={`${!enabled ? "text-gray-500" : null}`}>
|
||||
<p className={`${!enabled ? 'text-gray-500' : null}`}>
|
||||
Billed annually
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<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 flex-grow'>
|
||||
<h2 className='text-center font-bold uppercase mb-4'>Personal</h2>
|
||||
<h3 className='text-center font-bold text-4xl mb-5'>
|
||||
<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 flex-grow">
|
||||
<h2 className="text-center font-bold uppercase mb-4">Personal</h2>
|
||||
<h3 className="text-center font-bold text-4xl mb-5">
|
||||
{flat
|
||||
? pricing.flat.personal
|
||||
: enabled
|
||||
? pricing.yearly.personal
|
||||
: pricing.monthly.personal}
|
||||
</h3>
|
||||
<ul className='text-sm px-5 mb-8 text-left'>
|
||||
<li className='leading-tight'>
|
||||
<i className='mdi mdi-check-bold text-lg'></i> A cool feature
|
||||
<ul className="text-sm px-5 mb-8 text-left">
|
||||
<li className="leading-tight">
|
||||
<i className="mdi mdi-check-bold text-lg" /> A cool feature
|
||||
</li>
|
||||
<li className='leading-tight'>
|
||||
<i className='mdi mdi-check-bold text-lg'></i> Another feature
|
||||
<li className="leading-tight">
|
||||
<i className="mdi mdi-check-bold text-lg" /> Another feature
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className='w-full'>
|
||||
<div className="w-full">
|
||||
<button
|
||||
className='btn btn-primary w-full'
|
||||
className="btn btn-primary w-full"
|
||||
onClick={
|
||||
user
|
||||
? sub
|
||||
@@ -156,45 +157,45 @@ const Pricing = () => {
|
||||
: Prices.personal.monthly.id
|
||||
)
|
||||
: () => {
|
||||
router.push("/auth");
|
||||
router.push('/auth');
|
||||
}
|
||||
}>
|
||||
{user ? (sub ? "Upgrade" : "Buy Now") : "Register"}
|
||||
}
|
||||
>
|
||||
{user ? (sub ? 'Upgrade' : 'Buy Now') : 'Register'}
|
||||
</button>
|
||||
</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 flex-grow'>
|
||||
<h2 className='text-center font-bold uppercase mb-4'>Team</h2>
|
||||
<h3 className='text-center font-bold text-4xl mb-5'>
|
||||
<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">
|
||||
<h2 className="text-center font-bold uppercase mb-4">Team</h2>
|
||||
<h3 className="text-center font-bold text-4xl mb-5">
|
||||
{flat
|
||||
? pricing.flat.team
|
||||
: enabled
|
||||
? pricing.yearly.team
|
||||
: pricing.monthly.team}
|
||||
</h3>
|
||||
<ul className='text-sm px-5 mb-8 text-left'>
|
||||
<li className='leading-tight'>
|
||||
<i className='mdi mdi-check-bold text-lg'></i> All basic
|
||||
features
|
||||
<ul className="text-sm px-5 mb-8 text-left">
|
||||
<li className="leading-tight">
|
||||
<i className="mdi mdi-check-bold text-lg" /> All basic features
|
||||
</li>
|
||||
<li className='leading-tight'>
|
||||
<i className='mdi mdi-check-bold text-lg'></i> Dolor sit amet
|
||||
<li className="leading-tight">
|
||||
<i className="mdi mdi-check-bold text-lg" /> Dolor sit amet
|
||||
</li>
|
||||
<li className='leading-tight'>
|
||||
<i className='mdi mdi-check-bold text-lg'></i> Consectetur
|
||||
<li className="leading-tight">
|
||||
<i className="mdi mdi-check-bold text-lg" /> Consectetur
|
||||
</li>
|
||||
<li className='leading-tight'>
|
||||
<i className='mdi mdi-check-bold text-lg'></i> Adipisicing
|
||||
<li className="leading-tight">
|
||||
<i className="mdi mdi-check-bold text-lg" /> Adipisicing
|
||||
</li>
|
||||
<li className='leading-tight'>
|
||||
<i className='mdi mdi-check-bold text-lg'></i> Elit repellat
|
||||
<li className="leading-tight">
|
||||
<i className="mdi mdi-check-bold text-lg" /> Elit repellat
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className='w-full'>
|
||||
<div className="w-full">
|
||||
<button
|
||||
className='btn btn-primary w-full'
|
||||
className="btn btn-primary w-full"
|
||||
onClick={
|
||||
user
|
||||
? sub
|
||||
@@ -209,44 +210,45 @@ const Pricing = () => {
|
||||
: Prices.team.monthly.id
|
||||
)
|
||||
: () => {
|
||||
router.push("/auth");
|
||||
router.push('/auth');
|
||||
}
|
||||
}>
|
||||
{user ? (sub ? "Upgrade" : "Buy Now") : "Register"}
|
||||
}
|
||||
>
|
||||
{user ? (sub ? 'Upgrade' : 'Buy Now') : 'Register'}
|
||||
</button>
|
||||
</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 flex-grow'>
|
||||
<h2 className='text-center font-bold uppercase mb-4'>Pro</h2>
|
||||
<h3 className='text-center font-bold text-4xl mb-5'>
|
||||
<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">
|
||||
<h2 className="text-center font-bold uppercase mb-4">Pro</h2>
|
||||
<h3 className="text-center font-bold text-4xl mb-5">
|
||||
{flat
|
||||
? pricing.flat.pro
|
||||
: enabled
|
||||
? pricing.yearly.pro
|
||||
: pricing.monthly.pro}
|
||||
</h3>
|
||||
<ul className='text-sm px-5 mb-8 text-left'>
|
||||
<li className='leading-tight'>
|
||||
<i className='mdi mdi-check-bold text-lg'></i> Lorem ipsum
|
||||
<ul className="text-sm px-5 mb-8 text-left">
|
||||
<li className="leading-tight">
|
||||
<i className="mdi mdi-check-bold text-lg" /> Lorem ipsum
|
||||
</li>
|
||||
<li className='leading-tight'>
|
||||
<i className='mdi mdi-check-bold text-lg'></i> Dolor sit amet
|
||||
<li className="leading-tight">
|
||||
<i className="mdi mdi-check-bold text-lg" /> Dolor sit amet
|
||||
</li>
|
||||
<li className='leading-tight'>
|
||||
<i className='mdi mdi-check-bold text-lg'></i> Consectetur
|
||||
<li className="leading-tight">
|
||||
<i className="mdi mdi-check-bold text-lg" /> Consectetur
|
||||
</li>
|
||||
<li className='leading-tight'>
|
||||
<i className='mdi mdi-check-bold text-lg'></i> Adipisicing
|
||||
<li className="leading-tight">
|
||||
<i className="mdi mdi-check-bold text-lg" /> Adipisicing
|
||||
</li>
|
||||
<li className='leading-tight'>
|
||||
<i className='mdi mdi-check-bold text-lg'></i> Much more...
|
||||
<li className="leading-tight">
|
||||
<i className="mdi mdi-check-bold text-lg" /> Much more...
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className='w-full'>
|
||||
<div className="w-full">
|
||||
<button
|
||||
className='btn btn-primary w-full'
|
||||
className="btn btn-primary w-full"
|
||||
onClick={
|
||||
user
|
||||
? sub
|
||||
@@ -261,10 +263,11 @@ const Pricing = () => {
|
||||
: Prices.pro.monthly.id
|
||||
)
|
||||
: () => {
|
||||
router.push("/auth");
|
||||
router.push('/auth');
|
||||
}
|
||||
}>
|
||||
{user ? (sub ? "Upgrade" : "Buy Now") : "Register"}
|
||||
}
|
||||
>
|
||||
{user ? (sub ? 'Upgrade' : 'Buy Now') : 'Register'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,163 +1,159 @@
|
||||
const PrivacyPolicy = () => {
|
||||
return (
|
||||
<div className='max-w-xl text-left m-auto py-10'>
|
||||
<h1 className='text-center'>
|
||||
Privacy Policy for {process.env.NEXT_PUBLIC_TITLE}
|
||||
</h1>
|
||||
const PrivacyPolicy = () => (
|
||||
<div className="max-w-xl text-left m-auto py-10">
|
||||
<h1 className="text-center">
|
||||
Privacy Policy for {process.env.NEXT_PUBLIC_TITLE}
|
||||
</h1>
|
||||
|
||||
<p>
|
||||
At {process.env.NEXT_PUBLIC_TITLE}, accessible from
|
||||
https://www.supanextail.dev, one of our main priorities is the privacy
|
||||
of our visitors. This Privacy Policy document contains types of
|
||||
information that is collected and recorded by
|
||||
{process.env.NEXT_PUBLIC_TITLE} and how we use it.
|
||||
</p>
|
||||
<p>
|
||||
At {process.env.NEXT_PUBLIC_TITLE}, accessible from
|
||||
https://www.supanextail.dev, one of our main priorities is the privacy of
|
||||
our visitors. This Privacy Policy document contains types of information
|
||||
that is collected and recorded by
|
||||
{process.env.NEXT_PUBLIC_TITLE} and how we use it.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
If you have additional questions or require more information about our
|
||||
Privacy Policy, do not hesitate to contact us.
|
||||
</p>
|
||||
<p>
|
||||
If you have additional questions or require more information about our
|
||||
Privacy Policy, do not hesitate to contact us.
|
||||
</p>
|
||||
|
||||
<h2>General Data Protection Regulation (GDPR)</h2>
|
||||
<p>We are a Data Controller of your information.</p>
|
||||
<h2>General Data Protection Regulation (GDPR)</h2>
|
||||
<p>We are a Data Controller of your information.</p>
|
||||
|
||||
<p>
|
||||
{process.env.NEXT_PUBLIC_TITLE} legal basis for collecting and using the
|
||||
personal information described in this Privacy Policy depends on the
|
||||
Personal Information we collect and the specific context in which we
|
||||
collect the information:
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
{process.env.NEXT_PUBLIC_TITLE} needs to perform a contract with you
|
||||
</li>
|
||||
<li>
|
||||
You have given {process.env.NEXT_PUBLIC_TITLE} permission to do so
|
||||
</li>
|
||||
<li>
|
||||
Processing your personal information is in{" "}
|
||||
{process.env.NEXT_PUBLIC_TITLE} legitimate interests
|
||||
</li>
|
||||
<li>{process.env.NEXT_PUBLIC_TITLE} needs to comply with the law</li>
|
||||
</ul>
|
||||
<p>
|
||||
{process.env.NEXT_PUBLIC_TITLE} legal basis for collecting and using the
|
||||
personal information described in this Privacy Policy depends on the
|
||||
Personal Information we collect and the specific context in which we
|
||||
collect the information:
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
{process.env.NEXT_PUBLIC_TITLE} needs to perform a contract with you
|
||||
</li>
|
||||
<li>
|
||||
You have given {process.env.NEXT_PUBLIC_TITLE} permission to do so
|
||||
</li>
|
||||
<li>
|
||||
Processing your personal information is in{' '}
|
||||
{process.env.NEXT_PUBLIC_TITLE} legitimate interests
|
||||
</li>
|
||||
<li>{process.env.NEXT_PUBLIC_TITLE} needs to comply with the law</li>
|
||||
</ul>
|
||||
|
||||
<p>
|
||||
{process.env.NEXT_PUBLIC_TITLE} will retain your personal information
|
||||
only for as long as is necessary for the purposes set out in this
|
||||
Privacy Policy. We will retain and use your information to the extent
|
||||
necessary to comply with our legal obligations, resolve disputes, and
|
||||
enforce our policies.
|
||||
</p>
|
||||
<p>
|
||||
{process.env.NEXT_PUBLIC_TITLE} will retain your personal information only
|
||||
for as long as is necessary for the purposes set out in this Privacy
|
||||
Policy. We will retain and use your information to the extent necessary to
|
||||
comply with our legal obligations, resolve disputes, and enforce our
|
||||
policies.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
If you are a resident of the European Economic Area (EEA), you have
|
||||
certain data protection rights. If you wish to be informed what Personal
|
||||
Information we hold about you and if you want it to be removed from our
|
||||
systems, please contact us.
|
||||
</p>
|
||||
<p>
|
||||
In certain circumstances, you have the following data protection rights:
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
The right to access, update or to delete the information we have on
|
||||
you.
|
||||
</li>
|
||||
<li>The right of rectification.</li>
|
||||
<li>The right to object.</li>
|
||||
<li>The right of restriction.</li>
|
||||
<li>The right to data portability</li>
|
||||
<li>The right to withdraw consent</li>
|
||||
</ul>
|
||||
<p>
|
||||
If you are a resident of the European Economic Area (EEA), you have
|
||||
certain data protection rights. If you wish to be informed what Personal
|
||||
Information we hold about you and if you want it to be removed from our
|
||||
systems, please contact us.
|
||||
</p>
|
||||
<p>
|
||||
In certain circumstances, you have the following data protection rights:
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
The right to access, update or to delete the information we have on you.
|
||||
</li>
|
||||
<li>The right of rectification.</li>
|
||||
<li>The right to object.</li>
|
||||
<li>The right of restriction.</li>
|
||||
<li>The right to data portability</li>
|
||||
<li>The right to withdraw consent</li>
|
||||
</ul>
|
||||
|
||||
<h2>Log Files</h2>
|
||||
<h2>Log Files</h2>
|
||||
|
||||
<p>
|
||||
{process.env.NEXT_PUBLIC_TITLE} follows a standard procedure of using
|
||||
log files. These files log visitors when they visit websites. All
|
||||
hosting companies do this and a part of hosting services' analytics. The
|
||||
information collected by log files include internet protocol (IP)
|
||||
addresses, browser type, Internet Service Provider (ISP), date and time
|
||||
stamp, referring/exit pages, and possibly the number of clicks. These
|
||||
are not linked to any information that is personally identifiable. The
|
||||
purpose of the information is for analyzing trends, administering the
|
||||
site, tracking users' movement on the website, and gathering demographic
|
||||
information.
|
||||
</p>
|
||||
<p>
|
||||
{process.env.NEXT_PUBLIC_TITLE} follows a standard procedure of using log
|
||||
files. These files log visitors when they visit websites. All hosting
|
||||
companies do this and a part of hosting services' analytics. The
|
||||
information collected by log files include internet protocol (IP)
|
||||
addresses, browser type, Internet Service Provider (ISP), date and time
|
||||
stamp, referring/exit pages, and possibly the number of clicks. These are
|
||||
not linked to any information that is personally identifiable. The purpose
|
||||
of the information is for analyzing trends, administering the site,
|
||||
tracking users' movement on the website, and gathering demographic
|
||||
information.
|
||||
</p>
|
||||
|
||||
<h2>Privacy Policies</h2>
|
||||
<h2>Privacy Policies</h2>
|
||||
|
||||
<p>
|
||||
You may consult this list to find the Privacy Policy for each of the
|
||||
advertising partners of {process.env.NEXT_PUBLIC_TITLE}.
|
||||
</p>
|
||||
<p>
|
||||
You may consult this list to find the Privacy Policy for each of the
|
||||
advertising partners of {process.env.NEXT_PUBLIC_TITLE}.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Third-party ad servers or ad networks uses technologies like cookies,
|
||||
JavaScript, or Web Beacons that are used in their respective
|
||||
advertisements and links that appear on {process.env.NEXT_PUBLIC_TITLE},
|
||||
which are sent directly to users' browser. They automatically receive
|
||||
your IP address when this occurs. These technologies are used to measure
|
||||
the effectiveness of their advertising campaigns and/or to personalize
|
||||
the advertising content that you see on websites that you visit.
|
||||
</p>
|
||||
<p>
|
||||
Third-party ad servers or ad networks uses technologies like cookies,
|
||||
JavaScript, or Web Beacons that are used in their respective
|
||||
advertisements and links that appear on {process.env.NEXT_PUBLIC_TITLE},
|
||||
which are sent directly to users' browser. They automatically receive your
|
||||
IP address when this occurs. These technologies are used to measure the
|
||||
effectiveness of their advertising campaigns and/or to personalize the
|
||||
advertising content that you see on websites that you visit.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Note that {process.env.NEXT_PUBLIC_TITLE} has no access to or control
|
||||
over these cookies that are used by third-party advertisers.
|
||||
</p>
|
||||
<p>
|
||||
Note that {process.env.NEXT_PUBLIC_TITLE} has no access to or control over
|
||||
these cookies that are used by third-party advertisers.
|
||||
</p>
|
||||
|
||||
<h2>Third Party Privacy Policies</h2>
|
||||
<h2>Third Party Privacy Policies</h2>
|
||||
|
||||
<p>
|
||||
{process.env.NEXT_PUBLIC_TITLE}'s Privacy Policy does not apply to other
|
||||
advertisers or websites. Thus, we are advising you to consult the
|
||||
respective Privacy Policies of these third-party ad servers for more
|
||||
detailed information. It may include their practices and instructions
|
||||
about how to opt-out of certain options.{" "}
|
||||
</p>
|
||||
<p>
|
||||
{process.env.NEXT_PUBLIC_TITLE}'s Privacy Policy does not apply to other
|
||||
advertisers or websites. Thus, we are advising you to consult the
|
||||
respective Privacy Policies of these third-party ad servers for more
|
||||
detailed information. It may include their practices and instructions
|
||||
about how to opt-out of certain options.{' '}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
You can choose to disable cookies through your individual browser
|
||||
options. To know more detailed information about cookie management with
|
||||
specific web browsers, it can be found at the browsers' respective
|
||||
websites.
|
||||
</p>
|
||||
<p>
|
||||
You can choose to disable cookies through your individual browser options.
|
||||
To know more detailed information about cookie management with specific
|
||||
web browsers, it can be found at the browsers' respective websites.
|
||||
</p>
|
||||
|
||||
<h2>Children's Information</h2>
|
||||
<h2>Children's Information</h2>
|
||||
|
||||
<p>
|
||||
Another part of our priority is adding protection for children while
|
||||
using the internet. We encourage parents and guardians to observe,
|
||||
participate in, and/or monitor and guide their online activity.
|
||||
</p>
|
||||
<p>
|
||||
Another part of our priority is adding protection for children while using
|
||||
the internet. We encourage parents and guardians to observe, participate
|
||||
in, and/or monitor and guide their online activity.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
{process.env.NEXT_PUBLIC_TITLE} does not knowingly collect any Personal
|
||||
Identifiable Information from children under the age of 13. If you think
|
||||
that your child provided this kind of information on our website, we
|
||||
strongly encourage you to contact us immediately and we will do our best
|
||||
efforts to promptly remove such information from our records.
|
||||
</p>
|
||||
<p>
|
||||
{process.env.NEXT_PUBLIC_TITLE} does not knowingly collect any Personal
|
||||
Identifiable Information from children under the age of 13. If you think
|
||||
that your child provided this kind of information on our website, we
|
||||
strongly encourage you to contact us immediately and we will do our best
|
||||
efforts to promptly remove such information from our records.
|
||||
</p>
|
||||
|
||||
<h2>Online Privacy Policy Only</h2>
|
||||
<h2>Online Privacy Policy Only</h2>
|
||||
|
||||
<p>
|
||||
Our Privacy Policy applies only to our online activities and is valid
|
||||
for visitors to our website with regards to the information that they
|
||||
shared and/or collect in {process.env.NEXT_PUBLIC_TITLE}. This policy is
|
||||
not applicable to any information collected offline or via channels
|
||||
other than this website.
|
||||
</p>
|
||||
<p>
|
||||
Our Privacy Policy applies only to our online activities and is valid for
|
||||
visitors to our website with regards to the information that they shared
|
||||
and/or collect in {process.env.NEXT_PUBLIC_TITLE}. This policy is not
|
||||
applicable to any information collected offline or via channels other than
|
||||
this website.
|
||||
</p>
|
||||
|
||||
<h2>Consent</h2>
|
||||
<h2>Consent</h2>
|
||||
|
||||
<p>
|
||||
By using our website, you hereby consent to our Privacy Policy and agree
|
||||
to its terms.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
<p>
|
||||
By using our website, you hereby consent to our Privacy Policy and agree
|
||||
to its terms.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default PrivacyPolicy;
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
import SignUpPanel from "./UI/SignUpPanel";
|
||||
import { supabase } from "utils/supabaseClient";
|
||||
import { useAuth } from "utils/AuthContext";
|
||||
import { supabase } from 'utils/supabaseClient';
|
||||
import { useAuth } from 'utils/AuthContext';
|
||||
import SignUpPanel from './UI/SignUpPanel';
|
||||
|
||||
const Container = (props) => {
|
||||
const { user, signOut } = useAuth();
|
||||
if (user)
|
||||
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>
|
||||
<button className='btn btn-primary' onClick={() => signOut()}>
|
||||
<button className="btn btn-primary" onClick={() => signOut()}>
|
||||
Sign out
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -1,239 +1,235 @@
|
||||
const Terms = () => {
|
||||
return (
|
||||
<div className='max-w-xl text-left m-auto py-10'>
|
||||
<h1>Terms and Conditions</h1>
|
||||
const Terms = () => (
|
||||
<div className="max-w-xl text-left m-auto py-10">
|
||||
<h1>Terms and Conditions</h1>
|
||||
|
||||
<p>
|
||||
The following terms and conditions (collectively, these "Terms and
|
||||
Conditions") apply to your use of{" "}
|
||||
<span className='website_url'>https://www.supanextail.dev</span>,
|
||||
including any content, functionality and services offered on or via{" "}
|
||||
<span className='website_url'>https://www.supanextail.dev</span> (the
|
||||
"Website").
|
||||
</p>
|
||||
<p>
|
||||
The following terms and conditions (collectively, these "Terms and
|
||||
Conditions") apply to your use of{' '}
|
||||
<span className="website_url">https://www.supanextail.dev</span>,
|
||||
including any content, functionality and services offered on or via{' '}
|
||||
<span className="website_url">https://www.supanextail.dev</span> (the
|
||||
"Website").
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Please read the Terms and Conditions carefully before you start using{" "}
|
||||
<span className='website_name'>{process.env.NEXT_PUBLIC_TITLE}</span>,
|
||||
because by using the Website you accept and agree to be bound and abide
|
||||
by these Terms and Conditions.
|
||||
</p>
|
||||
<p>
|
||||
Please read the Terms and Conditions carefully before you start using{' '}
|
||||
<span className="website_name">{process.env.NEXT_PUBLIC_TITLE}</span>,
|
||||
because by using the Website you accept and agree to be bound and abide by
|
||||
these Terms and Conditions.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
These Terms and Conditions are effective as of{" "}
|
||||
<span className='date'>06/22/2021</span>. We expressly reserve the right 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
|
||||
this Website and these Terms and Conditions from time to time and to
|
||||
familiarize yourself with any modifications. Your continued use of this
|
||||
Website after such modifications will constitute acknowledgement of the
|
||||
modified Terms and Conditions and agreement to abide and be bound by the
|
||||
modified Terms and Conditions.
|
||||
</p>
|
||||
<p>
|
||||
These Terms and Conditions are effective as of{' '}
|
||||
<span className="date">06/22/2021</span>. We expressly reserve the right
|
||||
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
|
||||
this Website and these Terms and Conditions from time to time and to
|
||||
familiarize yourself with any modifications. Your continued use of this
|
||||
Website after such modifications will constitute acknowledgement of the
|
||||
modified Terms and Conditions and agreement to abide and be bound by the
|
||||
modified Terms and Conditions.
|
||||
</p>
|
||||
|
||||
<h2>Conduct on Website</h2>
|
||||
<h2>Conduct on Website</h2>
|
||||
|
||||
<p>
|
||||
Your use of the Website is subject to all applicable laws and
|
||||
regulations, and you are solely responsible for the substance of your
|
||||
communications through the Website.
|
||||
</p>
|
||||
<p>
|
||||
Your use of the Website is subject to all applicable laws and regulations,
|
||||
and you are solely responsible for the substance of your communications
|
||||
through the Website.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
By posting information in or otherwise using any communications service,
|
||||
chat room, message board, newsgroup, software library, or other
|
||||
interactive service that may be available to you on or through this
|
||||
Website, you agree that you will not upload, share, post, or otherwise
|
||||
distribute or facilitate distribution of any content — including text,
|
||||
communications, software, images, sounds, data, or other information —
|
||||
that:
|
||||
</p>
|
||||
<p>
|
||||
By posting information in or otherwise using any communications service,
|
||||
chat room, message board, newsgroup, software library, or other
|
||||
interactive service that may be available to you on or through this
|
||||
Website, you agree that you will not upload, share, post, or otherwise
|
||||
distribute or facilitate distribution of any content — including text,
|
||||
communications, software, images, sounds, data, or other information —
|
||||
that:
|
||||
</p>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
Is unlawful, threatening, abusive, harassing, defamatory, libelous,
|
||||
deceptive, fraudulent, invasive of another's privacy, tortious,
|
||||
contains explicit or graphic descriptions or accounts of sexual acts
|
||||
(including but not limited to sexual language of a violent or
|
||||
threatening nature directed at another individual or group of
|
||||
individuals), or otherwise violates our rules or policies
|
||||
</li>
|
||||
<li>
|
||||
Victimizes, harasses, degrades, or intimidates an individual or group
|
||||
of individuals on the basis of religion, gender, sexual orientation,
|
||||
race, ethnicity, age, or disability
|
||||
</li>
|
||||
<li>
|
||||
Infringes on any patent, trademark, trade secret, copyright, right of
|
||||
publicity, or other proprietary right of any party
|
||||
</li>
|
||||
<li>
|
||||
Constitutes unauthorized or unsolicited advertising, junk or bulk
|
||||
email (also known as "spamming"), chain letters, any other form of
|
||||
unauthorized solicitation, or any form of lottery or gambling
|
||||
</li>
|
||||
<li>
|
||||
Contains software viruses or any other computer code, files, or
|
||||
programs that are designed or intended to disrupt, damage, or limit
|
||||
the functioning of any software, hardware, or telecommunications
|
||||
equipment or to damage or obtain unauthorized access to any data or
|
||||
other information of any third party
|
||||
</li>
|
||||
<li>
|
||||
Impersonates any person or entity, including any of our employees or
|
||||
representatives
|
||||
</li>
|
||||
</ul>
|
||||
<ul>
|
||||
<li>
|
||||
Is unlawful, threatening, abusive, harassing, defamatory, libelous,
|
||||
deceptive, fraudulent, invasive of another's privacy, tortious, contains
|
||||
explicit or graphic descriptions or accounts of sexual acts (including
|
||||
but not limited to sexual language of a violent or threatening nature
|
||||
directed at another individual or group of individuals), or otherwise
|
||||
violates our rules or policies
|
||||
</li>
|
||||
<li>
|
||||
Victimizes, harasses, degrades, or intimidates an individual or group of
|
||||
individuals on the basis of religion, gender, sexual orientation, race,
|
||||
ethnicity, age, or disability
|
||||
</li>
|
||||
<li>
|
||||
Infringes on any patent, trademark, trade secret, copyright, right of
|
||||
publicity, or other proprietary right of any party
|
||||
</li>
|
||||
<li>
|
||||
Constitutes unauthorized or unsolicited advertising, junk or bulk email
|
||||
(also known as "spamming"), chain letters, any other form of
|
||||
unauthorized solicitation, or any form of lottery or gambling
|
||||
</li>
|
||||
<li>
|
||||
Contains software viruses or any other computer code, files, or programs
|
||||
that are designed or intended to disrupt, damage, or limit the
|
||||
functioning of any software, hardware, or telecommunications equipment
|
||||
or to damage or obtain unauthorized access to any data or other
|
||||
information of any third party
|
||||
</li>
|
||||
<li>
|
||||
Impersonates any person or entity, including any of our employees or
|
||||
representatives
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p>
|
||||
We neither endorse nor assume any liability for the contents of any
|
||||
material uploaded or submitted by third party users of the Website. We
|
||||
generally do not pre-screen, monitor, or edit the content posted by
|
||||
users of communications services, chat rooms, message boards,
|
||||
newsgroups, software libraries, or other interactive services that may
|
||||
be available on or through this Website.
|
||||
</p>
|
||||
<p>
|
||||
We neither endorse nor assume any liability for the contents of any
|
||||
material uploaded or submitted by third party users of the Website. We
|
||||
generally do not pre-screen, monitor, or edit the content posted by users
|
||||
of communications services, chat rooms, message boards, newsgroups,
|
||||
software libraries, or other interactive services that may be available on
|
||||
or through this Website.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
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
|
||||
Terms of Use and any other rules of user conduct for our Website, or is
|
||||
otherwise harmful, objectionable, or inaccurate. We are not responsible
|
||||
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
|
||||
of content.
|
||||
</p>
|
||||
<p>
|
||||
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 Terms
|
||||
of Use and any other rules of user conduct for our Website, or is
|
||||
otherwise harmful, objectionable, or inaccurate. We are not responsible
|
||||
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 of
|
||||
content.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
You agree that we may at any time, and at our sole discretion, terminate
|
||||
your membership, account, or other affiliation with our site without
|
||||
prior notice to you for violating any of the above provisions.
|
||||
</p>
|
||||
<p>
|
||||
You agree that we may at any time, and at our sole discretion, terminate
|
||||
your membership, account, or other affiliation with our site without prior
|
||||
notice to you for violating any of the above provisions.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
In addition, you acknowledge that we will cooperate fully with
|
||||
investigations of violations of systems or network security at other
|
||||
sites, including cooperating with law enforcement authorities in
|
||||
investigating suspected criminal violations.
|
||||
</p>
|
||||
<p>
|
||||
In addition, you acknowledge that we will cooperate fully with
|
||||
investigations of violations of systems or network security at other
|
||||
sites, including cooperating with law enforcement authorities in
|
||||
investigating suspected criminal violations.
|
||||
</p>
|
||||
|
||||
<h2>Intellectual Property</h2>
|
||||
<h2>Intellectual Property</h2>
|
||||
|
||||
<p>
|
||||
By accepting these Terms and Conditions, you acknowledge and agree that
|
||||
all content presented to you on this Website is protected by copyrights,
|
||||
trademarks, service marks, patents or other proprietary rights and laws,
|
||||
and is the sole property of{" "}
|
||||
<span className='website_name'>{process.env.NEXT_PUBLIC_TITLE}</span>.
|
||||
</p>
|
||||
<p>
|
||||
By accepting these Terms and Conditions, you acknowledge and agree that
|
||||
all content presented to you on this Website is protected by copyrights,
|
||||
trademarks, service marks, patents or other proprietary rights and laws,
|
||||
and is the sole property of{' '}
|
||||
<span className="website_name">{process.env.NEXT_PUBLIC_TITLE}</span>.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
You are only permitted to use the content as expressly authorized by us
|
||||
or the specific content provider. Except for a single copy made for
|
||||
personal use only, you may not copy, reproduce, modify, republish,
|
||||
upload, post, transmit, or distribute any documents or information from
|
||||
this Website in any form or by any means without prior written
|
||||
permission from us or the specific content provider, and you are solely
|
||||
responsible for obtaining permission before reusing any copyrighted
|
||||
material that is available on this Website.
|
||||
</p>
|
||||
<p>
|
||||
You are only permitted to use the content as expressly authorized by us or
|
||||
the specific content provider. Except for a single copy made for personal
|
||||
use only, you may not copy, reproduce, modify, republish, upload, post,
|
||||
transmit, or distribute any documents or information from this Website in
|
||||
any form or by any means without prior written permission from us or the
|
||||
specific content provider, and you are solely responsible for obtaining
|
||||
permission before reusing any copyrighted material that is available on
|
||||
this Website.
|
||||
</p>
|
||||
|
||||
<h2>Third Party Websites</h2>
|
||||
<h2>Third Party Websites</h2>
|
||||
|
||||
<p>
|
||||
This Website may link you to other sites on the Internet or otherwise
|
||||
include references to information, documents, software, materials and/or
|
||||
services provided by other parties. These websites may contain
|
||||
information or material that some people may find inappropriate or
|
||||
offensive.
|
||||
</p>
|
||||
<p>
|
||||
This Website may link you to other sites on the Internet or otherwise
|
||||
include references to information, documents, software, materials and/or
|
||||
services provided by other parties. These websites may contain information
|
||||
or material that some people may find inappropriate or offensive.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
These other websites and parties are not under our control, and you
|
||||
acknowledge that we are not responsible for the accuracy, copyright
|
||||
compliance, legality, decency, or any other aspect of the content of
|
||||
such sites, nor are we responsible for errors or omissions in any
|
||||
references to other parties or their products and services. The
|
||||
inclusion of such a link or reference is provided merely as a
|
||||
convenience and does not imply endorsement of, or association with, the
|
||||
Website or party by us, or any warranty of any kind, either express or
|
||||
implied.
|
||||
</p>
|
||||
<p>
|
||||
These other websites and parties are not under our control, and you
|
||||
acknowledge that we are not responsible for the accuracy, copyright
|
||||
compliance, legality, decency, or any other aspect of the content of such
|
||||
sites, nor are we responsible for errors or omissions in any references to
|
||||
other parties or their products and services. The inclusion of such a link
|
||||
or reference is provided merely as a convenience and does not imply
|
||||
endorsement of, or association with, the Website or party by us, or any
|
||||
warranty of any kind, either express or implied.
|
||||
</p>
|
||||
|
||||
<h2>
|
||||
Disclaimer of Warranties, Limitations of Liability and Indemnification
|
||||
</h2>
|
||||
<h2>
|
||||
Disclaimer of Warranties, Limitations of Liability and Indemnification
|
||||
</h2>
|
||||
|
||||
<p>
|
||||
Your use of{" "}
|
||||
<span className='website_name'>{process.env.NEXT_PUBLIC_TITLE}</span> is
|
||||
at your sole risk. The Website is provided "as is" and "as available".
|
||||
We disclaim all warranties of any kind, express or implied, including,
|
||||
without limitation, the warranties of merchantability, fitness for a
|
||||
particular purpose and non-infringement.
|
||||
</p>
|
||||
<p>
|
||||
Your use of{' '}
|
||||
<span className="website_name">{process.env.NEXT_PUBLIC_TITLE}</span> is
|
||||
at your sole risk. The Website is provided "as is" and "as available". We
|
||||
disclaim all warranties of any kind, express or implied, including,
|
||||
without limitation, the warranties of merchantability, fitness for a
|
||||
particular purpose and non-infringement.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
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
|
||||
harmless from any claims, losses, liability costs and expenses
|
||||
(including but not limites to attorney's fees) arising from your
|
||||
violation of any third-party's rights. You acknowledge that you have
|
||||
only a limited, non-exclusive, nontransferable license to use the
|
||||
Website. Because the Website is not error or bug free, you agree that
|
||||
you will use it carefully and avoid using it ways which might result in
|
||||
any loss of your or any third party's property or information.
|
||||
</p>
|
||||
<p>
|
||||
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
|
||||
harmless from any claims, losses, liability costs and expenses (including
|
||||
but not limites to attorney's fees) arising from your violation of any
|
||||
third-party's rights. You acknowledge that you have only a limited,
|
||||
non-exclusive, nontransferable license to use the Website. Because the
|
||||
Website is not error or bug free, you agree that you will use it carefully
|
||||
and avoid using it ways which might result in any loss of your or any
|
||||
third party's property or information.
|
||||
</p>
|
||||
|
||||
<h2>Term and termination</h2>
|
||||
<h2>Term and termination</h2>
|
||||
|
||||
<p>
|
||||
This Terms and Conditions will become effective in relation to you when
|
||||
you create a{" "}
|
||||
<span className='website_name'>{process.env.NEXT_PUBLIC_TITLE}</span>{" "}
|
||||
account or when you start using the{" "}
|
||||
<span className='website_name'>{process.env.NEXT_PUBLIC_TITLE}</span>{" "}
|
||||
and will remain effective until terminated by you or by us.{" "}
|
||||
</p>
|
||||
<p>
|
||||
This Terms and Conditions will become effective in relation to you when
|
||||
you create a{' '}
|
||||
<span className="website_name">{process.env.NEXT_PUBLIC_TITLE}</span>{' '}
|
||||
account or when you start using the{' '}
|
||||
<span className="website_name">{process.env.NEXT_PUBLIC_TITLE}</span> and
|
||||
will remain effective until terminated by you or by us.{' '}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<span className='website_name'>{process.env.NEXT_PUBLIC_TITLE}</span>{" "}
|
||||
reserves the right to terminate this Terms and Conditions or suspend
|
||||
your account at any time in case of unauthorized, or suspected
|
||||
unauthorized use of the Website whether in contravention of this Terms
|
||||
and Conditions or otherwise. If{" "}
|
||||
<span className='website_name'>{process.env.NEXT_PUBLIC_TITLE}</span>{" "}
|
||||
terminates this Terms and Conditions, or suspends your account for any
|
||||
of the reasons set out in this section,{" "}
|
||||
<span className='website_name'>{process.env.NEXT_PUBLIC_TITLE}</span>{" "}
|
||||
shall have no liability or responsibility to you.
|
||||
</p>
|
||||
<p>
|
||||
<span className="website_name">{process.env.NEXT_PUBLIC_TITLE}</span>{' '}
|
||||
reserves the right to terminate this Terms and Conditions or suspend your
|
||||
account at any time in case of unauthorized, or suspected unauthorized use
|
||||
of the Website whether in contravention of this Terms and Conditions or
|
||||
otherwise. If{' '}
|
||||
<span className="website_name">{process.env.NEXT_PUBLIC_TITLE}</span>{' '}
|
||||
terminates this Terms and Conditions, or suspends your account for any of
|
||||
the reasons set out in this section,{' '}
|
||||
<span className="website_name">{process.env.NEXT_PUBLIC_TITLE}</span>{' '}
|
||||
shall have no liability or responsibility to you.
|
||||
</p>
|
||||
|
||||
<h2>Assignment</h2>
|
||||
<h2>Assignment</h2>
|
||||
|
||||
<p>
|
||||
<span className='website_name'>{process.env.NEXT_PUBLIC_TITLE}</span>{" "}
|
||||
may assign this Terms and Conditions or any part of it without
|
||||
restrictions. You may not assign this Terms and Conditions or any part
|
||||
of it to any third party.
|
||||
</p>
|
||||
<p>
|
||||
<span className="website_name">{process.env.NEXT_PUBLIC_TITLE}</span> may
|
||||
assign this Terms and Conditions or any part of it without restrictions.
|
||||
You may not assign this Terms and Conditions or any part of it to any
|
||||
third party.
|
||||
</p>
|
||||
|
||||
<h2>Governing Law</h2>
|
||||
<h2>Governing Law</h2>
|
||||
|
||||
<p>
|
||||
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
|
||||
the internal laws of the <span className='country'>fr</span> without
|
||||
giving effect to any choice or conflict of law provision or rule.
|
||||
</p>
|
||||
<p>
|
||||
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 the
|
||||
internal laws of the <span className="country">fr</span> without giving
|
||||
effect to any choice or conflict of law provision or rule.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Any legal suit, action or proceeding arising out of, or related to,
|
||||
these Terms of Service or the Website shall be instituted exclusively in
|
||||
the federal courts of <span className='country'>fr</span>.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
<p>
|
||||
Any legal suit, action or proceeding arising out of, or related to, these
|
||||
Terms of Service or the Website shall be instituted exclusively in the
|
||||
federal courts of <span className="country">fr</span>.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default Terms;
|
||||
|
||||
@@ -1,22 +1,23 @@
|
||||
/*
|
||||
/*
|
||||
This card is used on the landing page
|
||||
*/
|
||||
|
||||
import Image from "next/image";
|
||||
import Image from 'next/image';
|
||||
|
||||
const CardLanding = (props) => {
|
||||
const { image, title, text } = props;
|
||||
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 className='rounded-full w-12 h-12 border flex bg-neutral-content'>
|
||||
<div className='m-auto flex'>
|
||||
<Image src={props.image} width={24} height={24} />
|
||||
<div className="rounded-full w-12 h-12 border flex bg-neutral-content">
|
||||
<div className="m-auto flex">
|
||||
<Image src={image} width={24} height={24} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='ml-8'>
|
||||
<p className='font-semibold font-title text-lg'>{props.title}</p>
|
||||
<p className='mt-3'>{props.text}</p>
|
||||
<div className="ml-8">
|
||||
<p className="font-semibold font-title text-lg">{title}</p>
|
||||
<p className="mt-3">{text}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -2,17 +2,15 @@
|
||||
This card is used on the landing page
|
||||
*/
|
||||
|
||||
import { FiStar } from "react-icons/fi";
|
||||
import { FiStar } from 'react-icons/fi';
|
||||
|
||||
const KeyFeature = (props) => {
|
||||
return (
|
||||
<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'>
|
||||
<FiStar className="text-2xl m-auto"/>
|
||||
</div>
|
||||
<div className='m-auto ml-3'>{props.children}</div>
|
||||
const KeyFeature = (props) => (
|
||||
<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">
|
||||
<FiStar className="text-2xl m-auto" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
<div className="m-auto ml-3">{props.children}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default KeyFeature;
|
||||
|
||||
@@ -1,33 +1,33 @@
|
||||
import { IoLogoGoogle } from "react-icons/io";
|
||||
import router from "next/router";
|
||||
import { toast } from "react-toastify";
|
||||
import { useState } from "react";
|
||||
import { IoLogoGoogle } from 'react-icons/io';
|
||||
import router from 'next/router';
|
||||
import { toast } from 'react-toastify';
|
||||
import { useState } from 'react';
|
||||
|
||||
const Login = (props) => {
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [forgot, setForgot] = useState(false);
|
||||
|
||||
const resetPassword = () => {
|
||||
props.resetPassword(email).then((result) => {
|
||||
if (result.error) {
|
||||
toast.error(result.error.message);
|
||||
} else toast.success("Check your email to reset your password!");
|
||||
} else toast.success('Check your email to reset your password!');
|
||||
});
|
||||
};
|
||||
|
||||
const login = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
//Handle the login. Go to the homepage if success or display an error.
|
||||
// Handle the login. Go to the homepage if success or display an error.
|
||||
props
|
||||
.signIn({
|
||||
email: email,
|
||||
password: password,
|
||||
email,
|
||||
password,
|
||||
})
|
||||
.then((result) => {
|
||||
if (result.data) {
|
||||
router.push("/");
|
||||
router.push('/');
|
||||
}
|
||||
if (result.error) {
|
||||
toast.error(result.error.message);
|
||||
@@ -36,45 +36,46 @@ const Login = (props) => {
|
||||
};
|
||||
|
||||
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 && (
|
||||
<>
|
||||
<h3 className='my-4 text-2xl font-semibold font-title'>
|
||||
<h3 className="my-4 text-2xl font-semibold font-title">
|
||||
Account Login
|
||||
</h3>
|
||||
<form action='#' className='flex flex-col space-y-5'>
|
||||
<div className='flex flex-col space-y-1'>
|
||||
<label htmlFor='email' className='text-sm'>
|
||||
<form action="#" className="flex flex-col space-y-5">
|
||||
<div className="flex flex-col space-y-1">
|
||||
<label htmlFor="email" className="text-sm">
|
||||
Email address
|
||||
</label>
|
||||
<input
|
||||
type='email'
|
||||
id='email'
|
||||
type="email"
|
||||
id="email"
|
||||
autoFocus
|
||||
className='input input-primary input-bordered input-sm'
|
||||
className="input input-primary input-bordered input-sm"
|
||||
value={email}
|
||||
onChange={(event) => {
|
||||
setEmail(event.target.value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className='flex flex-col space-y-1'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<label htmlFor='password' className='text-sm'>
|
||||
<div className="flex flex-col space-y-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<label htmlFor="password" className="text-sm">
|
||||
Password
|
||||
</label>
|
||||
<button
|
||||
onClick={() => {
|
||||
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?
|
||||
</button>
|
||||
</div>
|
||||
<input
|
||||
type='password'
|
||||
id='password'
|
||||
className='input input-primary input-bordered input-sm'
|
||||
type="password"
|
||||
id="password"
|
||||
className="input input-primary input-bordered input-sm"
|
||||
value={password}
|
||||
onChange={(event) => {
|
||||
setPassword(event.target.value);
|
||||
@@ -84,31 +85,33 @@ const Login = (props) => {
|
||||
|
||||
<div>
|
||||
<button
|
||||
className='btn btn-primary w-full'
|
||||
className="btn btn-primary w-full"
|
||||
onClick={(event) => {
|
||||
login(event);
|
||||
}}>
|
||||
}}
|
||||
>
|
||||
Log in
|
||||
</button>
|
||||
</div>
|
||||
<div className='flex flex-col space-y-5'>
|
||||
<span className='flex items-center justify-center space-x-2'>
|
||||
<span className='h-px bg-gray-400 w-14'></span>
|
||||
<span className='font-normal text-gray-500'>or login with</span>
|
||||
<span className='h-px bg-gray-400 w-14'></span>
|
||||
<div className="flex flex-col space-y-5">
|
||||
<span className="flex items-center justify-center space-x-2">
|
||||
<span className="h-px bg-gray-400 w-14" />
|
||||
<span className="font-normal text-gray-500">or login with</span>
|
||||
<span className="h-px bg-gray-400 w-14" />
|
||||
</span>
|
||||
<div className='flex flex-col space-y-4'>
|
||||
<div className="flex flex-col space-y-4">
|
||||
<button
|
||||
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 '
|
||||
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 "
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
props.signIn({ provider: "google" });
|
||||
}}>
|
||||
<div className='text-base-content'>
|
||||
props.signIn({ provider: 'google' });
|
||||
}}
|
||||
>
|
||||
<div className="text-base-content">
|
||||
<IoLogoGoogle />
|
||||
</div>
|
||||
<span className='text-sm font-medium text-base-content'>
|
||||
<span className="text-sm font-medium text-base-content">
|
||||
Gmail
|
||||
</span>
|
||||
</button>
|
||||
@@ -119,19 +122,20 @@ const Login = (props) => {
|
||||
)}
|
||||
{forgot && (
|
||||
<>
|
||||
<h3 className='my-4 text-2xl font-semibold'>Password recovery</h3>
|
||||
<form action='#' className='flex flex-col space-y-5'>
|
||||
<div className='flex flex-col space-y-1'>
|
||||
<h3 className="my-4 text-2xl font-semibold">Password recovery</h3>
|
||||
<form action="#" className="flex flex-col space-y-5">
|
||||
<div className="flex flex-col space-y-1">
|
||||
<label
|
||||
htmlFor='email'
|
||||
className='text-sm font-semibold text-gray-500'>
|
||||
htmlFor="email"
|
||||
className="text-sm font-semibold text-gray-500"
|
||||
>
|
||||
Email address
|
||||
</label>
|
||||
<input
|
||||
type='email'
|
||||
id='email'
|
||||
type="email"
|
||||
id="email"
|
||||
autoFocus
|
||||
className='input input-primary input-bordered input-sm'
|
||||
className="input input-primary input-bordered input-sm"
|
||||
value={email}
|
||||
onChange={(event) => {
|
||||
setEmail(event.target.value);
|
||||
@@ -141,11 +145,12 @@ const Login = (props) => {
|
||||
|
||||
<div>
|
||||
<button
|
||||
className='btn btn-primary w-full btn-sm'
|
||||
className="btn btn-primary w-full btn-sm"
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
resetPassword();
|
||||
}}>
|
||||
}}
|
||||
>
|
||||
Recover my password
|
||||
</button>
|
||||
</div>
|
||||
@@ -154,7 +159,8 @@ const Login = (props) => {
|
||||
onClick={() => {
|
||||
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
|
||||
</button>
|
||||
</form>
|
||||
|
||||
@@ -1,33 +1,33 @@
|
||||
import { IoLogoGoogle } from "react-icons/io";
|
||||
import router from "next/router";
|
||||
import { toast } from "react-toastify";
|
||||
import { useState } from "react";
|
||||
import { IoLogoGoogle } from 'react-icons/io';
|
||||
import router from 'next/router';
|
||||
import { toast } from 'react-toastify';
|
||||
import { useState } from 'react';
|
||||
|
||||
const SignUpPanel = (props) => {
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [forgot, setForgot] = useState(false);
|
||||
|
||||
const resetPassword = () => {
|
||||
props.resetPassword(email).then((result) => {
|
||||
if (result.error) {
|
||||
toast.error(result.error.message);
|
||||
} else toast.success("Check your email to reset your password!");
|
||||
} else toast.success('Check your email to reset your password!');
|
||||
});
|
||||
};
|
||||
|
||||
const signup = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
//Handle the login. Go to the homepage if success or display an error.
|
||||
// Handle the login. Go to the homepage if success or display an error.
|
||||
props
|
||||
.signUp({
|
||||
email: email,
|
||||
password: password,
|
||||
email,
|
||||
password,
|
||||
})
|
||||
.then((result) => {
|
||||
if (result.data) {
|
||||
router.push("/");
|
||||
router.push('/');
|
||||
}
|
||||
if (result.error) {
|
||||
toast.error(result.error.message);
|
||||
@@ -36,33 +36,33 @@ const SignUpPanel = (props) => {
|
||||
};
|
||||
|
||||
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 && (
|
||||
<>
|
||||
<h3 className='my-4 text-2xl font-semibold font-title'>
|
||||
<h3 className="my-4 text-2xl font-semibold font-title">
|
||||
Account Sign Up
|
||||
</h3>
|
||||
<form action='#' className='flex flex-col space-y-5'>
|
||||
<div className='flex flex-col space-y-1'>
|
||||
<label htmlFor='email' className='text-sm'>
|
||||
<form action="#" className="flex flex-col space-y-5">
|
||||
<div className="flex flex-col space-y-1">
|
||||
<label htmlFor="email" className="text-sm">
|
||||
Email address
|
||||
</label>
|
||||
<input
|
||||
type='email'
|
||||
id='email'
|
||||
type="email"
|
||||
id="email"
|
||||
autoFocus
|
||||
className='input input-primary input-bordered input-sm'
|
||||
className="input input-primary input-bordered input-sm"
|
||||
value={email}
|
||||
onChange={(event) => {
|
||||
setEmail(event.target.value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className='flex flex-col space-y-1'>
|
||||
<div className="flex flex-col space-y-1">
|
||||
<input
|
||||
type='password'
|
||||
id='password'
|
||||
className='input input-primary input-bordered input-sm'
|
||||
type="password"
|
||||
id="password"
|
||||
className="input input-primary input-bordered input-sm"
|
||||
value={password}
|
||||
onChange={(event) => {
|
||||
setPassword(event.target.value);
|
||||
@@ -72,33 +72,35 @@ const SignUpPanel = (props) => {
|
||||
|
||||
<div>
|
||||
<button
|
||||
className='btn btn-primary w-full'
|
||||
className="btn btn-primary w-full"
|
||||
onClick={(event) => {
|
||||
signup(event);
|
||||
}}>
|
||||
}}
|
||||
>
|
||||
Sign Up
|
||||
</button>
|
||||
</div>
|
||||
<div className='flex flex-col space-y-5'>
|
||||
<span className='flex items-center justify-center space-x-2'>
|
||||
<span className='h-px bg-gray-400 w-14'></span>
|
||||
<span className='font-normal text-gray-500'>
|
||||
<div className="flex flex-col space-y-5">
|
||||
<span className="flex items-center justify-center space-x-2">
|
||||
<span className="h-px bg-gray-400 w-14" />
|
||||
<span className="font-normal text-gray-500">
|
||||
or sign up with
|
||||
</span>
|
||||
<span className='h-px bg-gray-400 w-14'></span>
|
||||
<span className="h-px bg-gray-400 w-14" />
|
||||
</span>
|
||||
<div className='flex flex-col space-y-4'>
|
||||
<div className="flex flex-col space-y-4">
|
||||
<button
|
||||
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 '
|
||||
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 "
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
props.signIn({ provider: "google" });
|
||||
}}>
|
||||
<div className='text-base-content'>
|
||||
props.signIn({ provider: 'google' });
|
||||
}}
|
||||
>
|
||||
<div className="text-base-content">
|
||||
<IoLogoGoogle />
|
||||
</div>
|
||||
<span className='text-sm font-medium text-base-content'>
|
||||
<span className="text-sm font-medium text-base-content">
|
||||
Gmail
|
||||
</span>
|
||||
</button>
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
import { HiOutlineMoon, HiOutlineSun } from "react-icons/hi";
|
||||
import { useEffect, useState } from "react";
|
||||
import { HiOutlineMoon, HiOutlineSun } from 'react-icons/hi';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
const theme = {
|
||||
primary: "supaTheme",
|
||||
secondary: "dark",
|
||||
primary: 'supaTheme',
|
||||
secondary: 'dark',
|
||||
};
|
||||
|
||||
const ThemeToggle = () => {
|
||||
const [activeTheme, setActiveTheme] = useState(document.body.dataset.theme);
|
||||
const inactiveTheme = activeTheme === "supaTheme" ? "dark" : "supaTheme";
|
||||
const inactiveTheme = activeTheme === 'supaTheme' ? 'dark' : 'supaTheme';
|
||||
|
||||
useEffect(() => {
|
||||
document.body.dataset.theme = activeTheme;
|
||||
window.localStorage.setItem("theme", activeTheme);
|
||||
window.localStorage.setItem('theme', activeTheme);
|
||||
}, [activeTheme]);
|
||||
|
||||
return (
|
||||
<button className='flex ml-3' onClick={() => setActiveTheme(inactiveTheme)}>
|
||||
<button className="flex ml-3" onClick={() => setActiveTheme(inactiveTheme)}>
|
||||
{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>
|
||||
);
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
describe("Basic Test", () => {
|
||||
context("Desktop resolution", () => {
|
||||
it("Visits the homepage and nav links (Desktop)", () => {
|
||||
describe('Basic Test', () => {
|
||||
context('Desktop resolution', () => {
|
||||
it('Visits the homepage and nav links (Desktop)', () => {
|
||||
cy.viewport(1280, 720);
|
||||
cy.visit("");
|
||||
cy.visit('');
|
||||
|
||||
cy.get("nav").contains("Pricing").click();
|
||||
cy.url().should("include", "/pricing");
|
||||
cy.get('nav').contains('Pricing').click();
|
||||
cy.url().should('include', '/pricing');
|
||||
|
||||
cy.get("nav").contains("Contact").click();
|
||||
cy.url().should("include", "/contact");
|
||||
cy.get('nav').contains('Contact').click();
|
||||
cy.url().should('include', '/contact');
|
||||
|
||||
cy.get("nav").contains("Login").click();
|
||||
cy.url().should("include", "/login");
|
||||
cy.get('nav').contains('Login').click();
|
||||
cy.url().should('include', '/login');
|
||||
|
||||
cy.get("nav").contains("Sign Up").click();
|
||||
cy.url().should("include", "/signup");
|
||||
cy.get('nav').contains('Sign Up').click();
|
||||
cy.url().should('include', '/signup');
|
||||
});
|
||||
});
|
||||
|
||||
context("Mobile resolution", () => {
|
||||
it("Visits the homepage and nav links (Mobile)", () => {
|
||||
context('Mobile resolution', () => {
|
||||
it('Visits the homepage and nav links (Mobile)', () => {
|
||||
cy.viewport(680, 720);
|
||||
cy.visit("");
|
||||
cy.visit('');
|
||||
|
||||
cy.get("[data-cy=dropdown]").click();
|
||||
cy.get("[data-cy=dropdown]").contains("Pricing").click();
|
||||
cy.url().should("include", "/pricing");
|
||||
cy.get('[data-cy=dropdown]').click();
|
||||
cy.get('[data-cy=dropdown]').contains('Pricing').click();
|
||||
cy.url().should('include', '/pricing');
|
||||
|
||||
cy.get("[data-cy=dropdown]").click();
|
||||
cy.get("[data-cy=dropdown]").contains("Login").click();
|
||||
cy.url().should("include", "/login");
|
||||
cy.get('[data-cy=dropdown]').click();
|
||||
cy.get('[data-cy=dropdown]').contains('Login').click();
|
||||
cy.url().should('include', '/login');
|
||||
|
||||
cy.get("[data-cy=dropdown]").click();
|
||||
cy.get("[data-cy=dropdown]").contains("Sign Up").click();
|
||||
cy.url().should("include", "/signup");
|
||||
cy.get('[data-cy=dropdown]').click();
|
||||
cy.get('[data-cy=dropdown]').contains('Sign Up').click();
|
||||
cy.url().should('include', '/signup');
|
||||
|
||||
cy.get("[data-cy=dropdown]").click();
|
||||
cy.get("[data-cy=dropdown]").contains("Contact").click();
|
||||
cy.url().should("include", "/contact");
|
||||
cy.get('[data-cy=dropdown]').click();
|
||||
cy.get('[data-cy=dropdown]').contains('Contact').click();
|
||||
cy.url().should('include', '/contact');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,4 +19,4 @@
|
||||
module.exports = (on, config) => {
|
||||
// `on` is used to hook into various events Cypress emits
|
||||
// `config` is the resolved Cypress config
|
||||
}
|
||||
};
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
// ***********************************************************
|
||||
|
||||
// Import commands.js using ES2015 syntax:
|
||||
import './commands'
|
||||
import './commands';
|
||||
|
||||
// Alternatively you can use CommonJS syntax:
|
||||
// require('./commands')
|
||||
|
||||
7462
package-lock.json
generated
7462
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
18
package.json
18
package.json
@@ -7,7 +7,8 @@
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"cypress:open": "cypress open",
|
||||
"cypress:run": "cypress run"
|
||||
"cypress:run": "cypress run",
|
||||
"eslint": "eslint . --fix"
|
||||
},
|
||||
"dependencies": {
|
||||
"@headlessui/react": "^1.4.0",
|
||||
@@ -30,9 +31,22 @@
|
||||
"stripe": "^8.168.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@next/eslint-plugin-next": "^11.0.1",
|
||||
"autoprefixer": "^10.3.1",
|
||||
"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",
|
||||
"prettier": "^2.3.2",
|
||||
"tailwindcss": "^2.2.7"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import "./global.css";
|
||||
import './global.css';
|
||||
|
||||
import { AuthProvider } from "utils/AuthContext";
|
||||
import { DefaultSeo } from "next-seo";
|
||||
import { AuthProvider } from 'utils/AuthContext';
|
||||
import { DefaultSeo } from 'next-seo';
|
||||
|
||||
/*
|
||||
Next-seo is integrated by default, if you want more information and how to
|
||||
@@ -14,14 +14,14 @@ function MyApp({ Component, pageProps }) {
|
||||
<AuthProvider>
|
||||
<DefaultSeo
|
||||
openGraph={{
|
||||
type: "website",
|
||||
locale: "en_IE",
|
||||
url: "",
|
||||
site_name: "Supanextail",
|
||||
type: 'website',
|
||||
locale: 'en_IE',
|
||||
url: '',
|
||||
site_name: 'Supanextail',
|
||||
}}
|
||||
twitter={{
|
||||
handle: "@michael_webdev",
|
||||
site: "@michael_webdev",
|
||||
handle: '@michael_webdev',
|
||||
site: '@michael_webdev',
|
||||
}}
|
||||
/>
|
||||
<Component {...pageProps} />
|
||||
|
||||
@@ -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 {
|
||||
static async getInitialProps(ctx) {
|
||||
|
||||
169
pages/admin.js
169
pages/admin.js
@@ -1,92 +1,87 @@
|
||||
import Head from "next/head";
|
||||
import Layout from "components/Layout";
|
||||
import { createClient } from "@supabase/supabase-js";
|
||||
import dynamic from "next/dynamic";
|
||||
import { useEffect } from "react";
|
||||
import { useState } from "react";
|
||||
// import Head from 'next/head';
|
||||
// import Layout from 'components/Layout';
|
||||
// import { createClient } from '@supabase/supabase-js';
|
||||
// import { useState } from 'react';
|
||||
|
||||
const AdminPage = ({ adminKey }) => {
|
||||
const [currentTable, setCurrentTable] = useState("admin_list");
|
||||
const SupabaseGrid = dynamic(() =>
|
||||
import("@supabase/grid").then((mod) => mod.SupabaseGrid)
|
||||
);
|
||||
// const AdminPage = () => {
|
||||
// const [currentTable, setCurrentTable] = useState('admin_list');
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Head>
|
||||
<title>{process.env.NEXT_PUBLIC_TITLE} | Dashboard</title>
|
||||
</Head>
|
||||
// return (
|
||||
// <div>
|
||||
// <Head>
|
||||
// <title>{process.env.NEXT_PUBLIC_TITLE} | Dashboard</title>
|
||||
// </Head>
|
||||
|
||||
<Layout>
|
||||
<>
|
||||
<h1 className='text-4xl font-bold md:text-5xl font-title'>
|
||||
Admin Dashboard
|
||||
</h1>
|
||||
<p>Hello admin ! Select your table to display the content</p>
|
||||
<div className='flex space-x-3'>
|
||||
<button
|
||||
className='btn btn-primary btn-sm'
|
||||
onClick={() => {
|
||||
setCurrentTable("profiles");
|
||||
}}>
|
||||
User profiles
|
||||
</button><button
|
||||
className='btn btn-primary btn-sm'
|
||||
onClick={() => {
|
||||
setCurrentTable("admin_list");
|
||||
}}>
|
||||
Admin List
|
||||
</button><button
|
||||
className='btn btn-primary btn-sm'
|
||||
onClick={() => {
|
||||
setCurrentTable("subscriptions");
|
||||
}}>
|
||||
Subscriptions
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
<div className='shadow-sm max-w-4xl w-full'>
|
||||
<SupabaseGrid
|
||||
className='w-full'
|
||||
table={`${currentTable}`}
|
||||
clientProps={{
|
||||
supabaseUrl: process.env.NEXT_PUBLIC_SUPABASE_URL,
|
||||
supabaseKey: adminKey,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Layout>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export async function getServerSideProps({ req }) {
|
||||
const supabaseAdmin = createClient(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL,
|
||||
process.env.SUPABASE_ADMIN_KEY
|
||||
);
|
||||
const { user } = await supabaseAdmin.auth.api.getUserByCookie(req);
|
||||
// <Layout>
|
||||
// <>
|
||||
// <h1 className="text-4xl font-bold md:text-5xl font-title">
|
||||
// Admin Dashboard
|
||||
// </h1>
|
||||
// <p>Hello admin ! Select your table to display the content</p>
|
||||
// <div className="flex space-x-3">
|
||||
// <button
|
||||
// type="button"
|
||||
// className="btn btn-primary btn-sm"
|
||||
// onClick={() => {
|
||||
// setCurrentTable('profiles');
|
||||
// }}
|
||||
// >
|
||||
// User profiles
|
||||
// </button>
|
||||
// <button
|
||||
// type="button"
|
||||
// className="btn btn-primary btn-sm"
|
||||
// onClick={() => {
|
||||
// setCurrentTable('admin_list');
|
||||
// }}
|
||||
// >
|
||||
// Admin List
|
||||
// </button>
|
||||
// <button
|
||||
// type="button"
|
||||
// className="btn btn-primary btn-sm"
|
||||
// onClick={() => {
|
||||
// setCurrentTable('subscriptions');
|
||||
// }}
|
||||
// >
|
||||
// Subscriptions
|
||||
// </button>
|
||||
// </div>
|
||||
// </>
|
||||
// </Layout>
|
||||
// </div>
|
||||
// );
|
||||
// };
|
||||
// export async function getServerSideProps({ req }) {
|
||||
// const supabaseAdmin = createClient(
|
||||
// process.env.NEXT_PUBLIC_SUPABASE_URL,
|
||||
// process.env.SUPABASE_ADMIN_KEY
|
||||
// );
|
||||
// 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 (user) {
|
||||
let { data: admincheck, error } = await supabaseAdmin
|
||||
.from("admin_list")
|
||||
.select("isadmin")
|
||||
.eq("id", user.id)
|
||||
.single();
|
||||
// // If the user exist, you will retrieve the user profile and if he/she's an admin
|
||||
// if (user) {
|
||||
// const { data: admincheck, error } = await supabaseAdmin
|
||||
// .from('admin_list')
|
||||
// .select('isadmin')
|
||||
// .eq('id', user.id)
|
||||
// .single();
|
||||
|
||||
if (admincheck.isadmin) {
|
||||
return {
|
||||
props: {
|
||||
admincheck: admincheck.isadmin,
|
||||
adminKey: process.env.SUPABASE_ADMIN_KEY,
|
||||
},
|
||||
};
|
||||
} else
|
||||
return { props: {}, redirect: { destination: "/", permanent: false } };
|
||||
}
|
||||
// If no user, redirect to index.
|
||||
if (!user) {
|
||||
return { props: {}, redirect: { destination: "/", permanent: false } };
|
||||
}
|
||||
}
|
||||
export default AdminPage;
|
||||
// if (admincheck.isadmin) {
|
||||
// return {
|
||||
// props: {
|
||||
// admincheck: admincheck.isadmin,
|
||||
// adminKey: process.env.SUPABASE_ADMIN_KEY,
|
||||
// },
|
||||
// };
|
||||
// }
|
||||
|
||||
// if (error) {
|
||||
// console.log(error);
|
||||
// }
|
||||
// return { props: {}, redirect: { destination: '/', permanent: false } };
|
||||
// }
|
||||
// // If no user, redirect to index.
|
||||
// return { props: {}, redirect: { destination: '/', permanent: false } };
|
||||
// }
|
||||
// export default AdminPage;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* 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) {
|
||||
supabase.auth.api.setAuthCookie(req, res);
|
||||
|
||||
@@ -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.
|
||||
const getUser = async (req, res) => {
|
||||
const token = req.headers.token;
|
||||
const { token } = req.headers;
|
||||
|
||||
const { data: user, error } = await supabase.auth.api.getUser(token);
|
||||
|
||||
|
||||
@@ -1,60 +1,58 @@
|
||||
import Cors from "cors";
|
||||
import axios from "axios";
|
||||
import initMiddleware from "utils/init-middleware";
|
||||
|
||||
const rateLimit = require("express-rate-limit");
|
||||
|
||||
export const config = {
|
||||
api: {
|
||||
externalResolver: true,
|
||||
},
|
||||
};
|
||||
|
||||
const cors = initMiddleware(
|
||||
Cors({
|
||||
methods: ["PUT"],
|
||||
})
|
||||
);
|
||||
|
||||
const limiter = initMiddleware(
|
||||
rateLimit({
|
||||
windowMs: 30000, // 30sec
|
||||
max: 2, // Max 2 request per 30 sec
|
||||
})
|
||||
);
|
||||
|
||||
export default async function handler(req, res) {
|
||||
await cors(req, res);
|
||||
await limiter(req, res);
|
||||
if (req.method === "PUT") {
|
||||
axios
|
||||
.put(
|
||||
"https://api.sendgrid.com/v3/marketing/contacts",
|
||||
{
|
||||
contacts: [{ email: `${req.body.mail}` }],
|
||||
list_ids: [process.env.SENDGRID_MAILING_ID],
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
Authorization: `Bearer ${process.env.SENDGRID_SECRET}`,
|
||||
},
|
||||
}
|
||||
)
|
||||
.then((result) => {
|
||||
console.log(result);
|
||||
res
|
||||
.status(200)
|
||||
.send({
|
||||
message:
|
||||
"Your email has been succesfully added to the mailing list. Welcome 👋",
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
res.status(500).send({
|
||||
message:
|
||||
"Oups, there was a problem with your subscription, please try again or contact us",
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
import Cors from 'cors';
|
||||
import axios from 'axios';
|
||||
import initMiddleware from 'utils/init-middleware';
|
||||
|
||||
const rateLimit = require('express-rate-limit');
|
||||
|
||||
export const config = {
|
||||
api: {
|
||||
externalResolver: true,
|
||||
},
|
||||
};
|
||||
|
||||
const cors = initMiddleware(
|
||||
Cors({
|
||||
methods: ['PUT'],
|
||||
})
|
||||
);
|
||||
|
||||
const limiter = initMiddleware(
|
||||
rateLimit({
|
||||
windowMs: 30000, // 30sec
|
||||
max: 2, // Max 2 request per 30 sec
|
||||
})
|
||||
);
|
||||
|
||||
export default async function handler(req, res) {
|
||||
await cors(req, res);
|
||||
await limiter(req, res);
|
||||
if (req.method === 'PUT') {
|
||||
axios
|
||||
.put(
|
||||
'https://api.sendgrid.com/v3/marketing/contacts',
|
||||
{
|
||||
contacts: [{ email: `${req.body.mail}` }],
|
||||
list_ids: [process.env.SENDGRID_MAILING_ID],
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
Authorization: `Bearer ${process.env.SENDGRID_SECRET}`,
|
||||
},
|
||||
}
|
||||
)
|
||||
.then((result) => {
|
||||
console.log(result);
|
||||
res.status(200).send({
|
||||
message:
|
||||
'Your email has been succesfully added to the mailing list. Welcome 👋',
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
res.status(500).send({
|
||||
message:
|
||||
'Oups, there was a problem with your subscription, please try again or contact us',
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
This is a simple contact form for SupaNexTail
|
||||
Using Sendgrid.
|
||||
*/
|
||||
const sgMail = require("@sendgrid/mail");
|
||||
const sgMail = require('@sendgrid/mail');
|
||||
|
||||
export default async function handler(req, res) {
|
||||
if (req.method === "POST") {
|
||||
if (req.method === 'POST') {
|
||||
sgMail.setApiKey(process.env.SENDGRID_SECRET);
|
||||
const msg = {
|
||||
to: process.env.SENDGRID_MAILTO, // Change to your recipient
|
||||
@@ -19,13 +19,13 @@ export default async function handler(req, res) {
|
||||
.then(() => {
|
||||
res
|
||||
.status(200)
|
||||
.send({ message: "Your email has been sent", success: true });
|
||||
.send({ message: 'Your email has been sent', success: true });
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
res.status(500).send({
|
||||
message: "There was an issue with your email... please retry",
|
||||
err: err,
|
||||
message: 'There was an issue with your email... please retry',
|
||||
err,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,80 +1,80 @@
|
||||
import Cors from "cors";
|
||||
import initMiddleware from "utils/init-middleware";
|
||||
|
||||
const rateLimit = require("express-rate-limit");
|
||||
|
||||
const cors = initMiddleware(
|
||||
Cors({
|
||||
methods: ["POST"],
|
||||
})
|
||||
);
|
||||
|
||||
const limiter = initMiddleware(
|
||||
rateLimit({
|
||||
windowMs: 30000, // 30sec
|
||||
max: 4, // Max 4 request per 30 sec
|
||||
})
|
||||
);
|
||||
// Set your secret key. Remember to switch to your live secret key in production.
|
||||
// See your keys here: https://dashboard.stripe.com/apikeys
|
||||
const stripe = require("stripe")(process.env.STRIPE_SECRET);
|
||||
|
||||
export default async function handler(req, res) {
|
||||
await cors(req, res);
|
||||
await limiter(req, res);
|
||||
if (req.method === "POST") {
|
||||
const priceId = req.body.priceId;
|
||||
|
||||
// See https://stripe.com/docs/api/checkout/sessions/create
|
||||
// for additional parameters to pass.
|
||||
try {
|
||||
const session = req.body.customerId
|
||||
? await stripe.checkout.sessions.create({
|
||||
mode: req.body.pay_mode,
|
||||
payment_method_types: ["card"],
|
||||
client_reference_id: req.body.userId,
|
||||
metadata: { token: req.body.tokenId, priceId: req.body.priceId },
|
||||
customer: req.body.customerId,
|
||||
line_items: [
|
||||
{
|
||||
price: priceId,
|
||||
// For metered billing, do not pass quantity
|
||||
quantity: 1,
|
||||
},
|
||||
],
|
||||
// {CHECKOUT_SESSION_ID} is a string literal; do not change it!
|
||||
// the actual Session ID is returned in the query parameter when your customer
|
||||
// is redirected to the success page.
|
||||
success_url: `${req.headers.origin}/dashboard?session_id={CHECKOUT_SESSION_ID}`,
|
||||
cancel_url: `${req.headers.origin}/pricing`,
|
||||
})
|
||||
: await stripe.checkout.sessions.create({
|
||||
mode: "subscription",
|
||||
payment_method_types: ["card"],
|
||||
customer_email: req.body.email,
|
||||
client_reference_id: req.body.userId,
|
||||
metadata: { token: req.body.tokenId, priceId: req.body.priceId },
|
||||
line_items: [
|
||||
{
|
||||
price: priceId,
|
||||
// For metered billing, do not pass quantity
|
||||
quantity: 1,
|
||||
},
|
||||
],
|
||||
// {CHECKOUT_SESSION_ID} is a string literal; do not change it!
|
||||
// the actual Session ID is returned in the query parameter when your customer
|
||||
// is redirected to the success page.
|
||||
success_url: `${req.headers.origin}/dashboard?session_id={CHECKOUT_SESSION_ID}`,
|
||||
cancel_url: `${req.headers.origin}/pricing`,
|
||||
});
|
||||
res.status(200).send({ url: session.url });
|
||||
} catch (e) {
|
||||
res.status(400);
|
||||
return res.send({
|
||||
error: {
|
||||
message: e.message,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
import Cors from 'cors';
|
||||
import initMiddleware from 'utils/init-middleware';
|
||||
|
||||
const rateLimit = require('express-rate-limit');
|
||||
|
||||
const cors = initMiddleware(
|
||||
Cors({
|
||||
methods: ['POST'],
|
||||
})
|
||||
);
|
||||
|
||||
const limiter = initMiddleware(
|
||||
rateLimit({
|
||||
windowMs: 30000, // 30sec
|
||||
max: 4, // Max 4 request per 30 sec
|
||||
})
|
||||
);
|
||||
// Set your secret key. Remember to switch to your live secret key in production.
|
||||
// See your keys here: https://dashboard.stripe.com/apikeys
|
||||
const stripe = require('stripe')(process.env.STRIPE_SECRET);
|
||||
|
||||
export default async function handler(req, res) {
|
||||
await cors(req, res);
|
||||
await limiter(req, res);
|
||||
if (req.method === 'POST') {
|
||||
const { priceId } = req.body;
|
||||
|
||||
// See https://stripe.com/docs/api/checkout/sessions/create
|
||||
// for additional parameters to pass.
|
||||
try {
|
||||
const session = req.body.customerId
|
||||
? await stripe.checkout.sessions.create({
|
||||
mode: req.body.pay_mode,
|
||||
payment_method_types: ['card'],
|
||||
client_reference_id: req.body.userId,
|
||||
metadata: { token: req.body.tokenId, priceId: req.body.priceId },
|
||||
customer: req.body.customerId,
|
||||
line_items: [
|
||||
{
|
||||
price: priceId,
|
||||
// For metered billing, do not pass quantity
|
||||
quantity: 1,
|
||||
},
|
||||
],
|
||||
// {CHECKOUT_SESSION_ID} is a string literal; do not change it!
|
||||
// the actual Session ID is returned in the query parameter when your customer
|
||||
// is redirected to the success page.
|
||||
success_url: `${req.headers.origin}/dashboard?session_id={CHECKOUT_SESSION_ID}`,
|
||||
cancel_url: `${req.headers.origin}/pricing`,
|
||||
})
|
||||
: await stripe.checkout.sessions.create({
|
||||
mode: 'subscription',
|
||||
payment_method_types: ['card'],
|
||||
customer_email: req.body.email,
|
||||
client_reference_id: req.body.userId,
|
||||
metadata: { token: req.body.tokenId, priceId: req.body.priceId },
|
||||
line_items: [
|
||||
{
|
||||
price: priceId,
|
||||
// For metered billing, do not pass quantity
|
||||
quantity: 1,
|
||||
},
|
||||
],
|
||||
// {CHECKOUT_SESSION_ID} is a string literal; do not change it!
|
||||
// the actual Session ID is returned in the query parameter when your customer
|
||||
// is redirected to the success page.
|
||||
success_url: `${req.headers.origin}/dashboard?session_id={CHECKOUT_SESSION_ID}`,
|
||||
cancel_url: `${req.headers.origin}/pricing`,
|
||||
});
|
||||
res.status(200).send({ url: session.url });
|
||||
} catch (e) {
|
||||
res.status(400);
|
||||
return res.send({
|
||||
error: {
|
||||
message: e.message,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
/* Dont forget to create your customer portal on Stripe
|
||||
https://dashboard.stripe.com/test/settings/billing/portal */
|
||||
|
||||
import Cors from "cors";
|
||||
import initMiddleware from "utils/init-middleware";
|
||||
const rateLimit = require("express-rate-limit");
|
||||
import Cors from 'cors';
|
||||
import initMiddleware from 'utils/init-middleware';
|
||||
|
||||
const rateLimit = require('express-rate-limit');
|
||||
|
||||
const cors = initMiddleware(
|
||||
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.
|
||||
// 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) {
|
||||
await cors(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 portalsession = await stripe.billingPortal.sessions.create({
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
import Cors from "cors";
|
||||
import { buffer } from "micro";
|
||||
import { createClient } from "@supabase/supabase-js";
|
||||
import initMiddleware from "utils/init-middleware";
|
||||
const rateLimit = require("express-rate-limit");
|
||||
import Cors from 'cors';
|
||||
import { buffer } from 'micro';
|
||||
import { createClient } from '@supabase/supabase-js';
|
||||
import initMiddleware from 'utils/init-middleware';
|
||||
|
||||
const rateLimit = require('express-rate-limit');
|
||||
|
||||
export const config = {
|
||||
api: {
|
||||
@@ -21,7 +22,7 @@ export const config = {
|
||||
// Initialize the cors middleware -> Allow the browser extension to create lists
|
||||
const cors = initMiddleware(
|
||||
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.
|
||||
// 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) {
|
||||
await cors(req, res);
|
||||
await limiter(req, res);
|
||||
stripe.setMaxNetworkRetries(2);
|
||||
|
||||
if (req.method === "POST") {
|
||||
if (req.method === 'POST') {
|
||||
// Retrieve the event by verifying the signature using the raw body and secret.
|
||||
let event;
|
||||
const buf = await buffer(req);
|
||||
@@ -57,7 +58,7 @@ export default async function handler(req, res) {
|
||||
try {
|
||||
event = stripe.webhooks.constructEvent(
|
||||
buf,
|
||||
req.headers["stripe-signature"],
|
||||
req.headers['stripe-signature'],
|
||||
process.env.STRIPE_WEBHOOK
|
||||
);
|
||||
} catch (err) {
|
||||
@@ -76,22 +77,22 @@ export default async function handler(req, res) {
|
||||
// https://stripe.com/docs/billing/webhooks
|
||||
// Remove comment to see the various objects sent for this sample
|
||||
switch (event.type) {
|
||||
case "checkout.session.completed":
|
||||
let { data: subscriptions, error } = await supabase
|
||||
.from("subscriptions")
|
||||
.select("*")
|
||||
.eq("id", dataObject.client_reference_id);
|
||||
case 'checkout.session.completed':
|
||||
const { data: subscriptions, error } = await supabase
|
||||
.from('subscriptions')
|
||||
.select('*')
|
||||
.eq('id', dataObject.client_reference_id);
|
||||
console.log(dataObject);
|
||||
|
||||
if (subscriptions.length == 0) {
|
||||
const { data, error } = await supabase
|
||||
.from("profiles")
|
||||
.from('profiles')
|
||||
.update({ customerId: dataObject.customer })
|
||||
.eq("id", dataObject.client_reference_id);
|
||||
.eq('id', dataObject.client_reference_id);
|
||||
if (error) console.log(error);
|
||||
|
||||
await supabase
|
||||
.from("subscriptions")
|
||||
.from('subscriptions')
|
||||
.insert([
|
||||
{
|
||||
id: dataObject.client_reference_id,
|
||||
@@ -105,34 +106,34 @@ export default async function handler(req, res) {
|
||||
.catch((err) => console.log(err));
|
||||
} else if (subscriptions.length > 0) {
|
||||
await supabase
|
||||
.from("subscriptions")
|
||||
.from('subscriptions')
|
||||
.update({
|
||||
customer_id: dataObject.customer,
|
||||
paid_user: true,
|
||||
plan: dataObject.metadata.priceId,
|
||||
subscription: dataObject.subscription,
|
||||
})
|
||||
.eq("id", dataObject.client_reference_id)
|
||||
.eq('id', dataObject.client_reference_id)
|
||||
.then()
|
||||
.catch((err) => console.log(err));
|
||||
}
|
||||
break;
|
||||
case "customer.subscription.deleted":
|
||||
case 'customer.subscription.deleted':
|
||||
await supabase
|
||||
.from("subscriptions")
|
||||
.from('subscriptions')
|
||||
.update({ paid_user: false })
|
||||
.eq("customer_id", dataObject.customer)
|
||||
.eq('customer_id', dataObject.customer)
|
||||
.then()
|
||||
.catch((err) => console.log(err));
|
||||
break;
|
||||
case "invoice.payment_failed":
|
||||
case 'invoice.payment_failed':
|
||||
// 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.
|
||||
// Use this webhook to notify your user that their payment has
|
||||
// failed and to retrieve new card details.
|
||||
break;
|
||||
|
||||
case "invoice.paid":
|
||||
case 'invoice.paid':
|
||||
// Used to provision services after the trial has ended.
|
||||
// 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.
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
import Contact from "components/Contact";
|
||||
import Layout from "components/Layout";
|
||||
import { NextSeo } from "next-seo";
|
||||
import Contact from 'components/Contact';
|
||||
import Layout from 'components/Layout';
|
||||
import { NextSeo } from 'next-seo';
|
||||
|
||||
const ContactPage = () => {
|
||||
return (
|
||||
<>
|
||||
<NextSeo
|
||||
title={`${process.env.NEXT_PUBLIC_TITLE} | Contact`}
|
||||
description={`This is the contact page for ${process.env.NEXT_PUBLIC_TITLE}`}
|
||||
/>
|
||||
const ContactPage = () => (
|
||||
<>
|
||||
<NextSeo
|
||||
title={`${process.env.NEXT_PUBLIC_TITLE} | Contact`}
|
||||
description={`This is the contact page for ${process.env.NEXT_PUBLIC_TITLE}`}
|
||||
/>
|
||||
|
||||
<Layout>
|
||||
<Contact />
|
||||
</Layout>
|
||||
</>
|
||||
);
|
||||
};
|
||||
<Layout>
|
||||
<Contact />
|
||||
</Layout>
|
||||
</>
|
||||
);
|
||||
export default ContactPage;
|
||||
|
||||
@@ -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 Layout from "components/Layout";
|
||||
import { createClient } from "@supabase/supabase-js";
|
||||
import { supabase } from "../utils/supabaseClient";
|
||||
import { useRouter } from "next/router";
|
||||
import Head from 'next/head';
|
||||
import Layout from 'components/Layout';
|
||||
import { createClient } from '@supabase/supabase-js';
|
||||
import { useRouter } from 'next/router';
|
||||
import { supabase } from '../utils/supabaseClient';
|
||||
import Dashboard from '../components/Dashboard';
|
||||
|
||||
const DashboardPage = ({ user, plan, profile }) => {
|
||||
const [session, setSession] = useState(null);
|
||||
@@ -14,7 +14,7 @@ const DashboardPage = ({ user, plan, profile }) => {
|
||||
useEffect(() => {
|
||||
// If a user is not logged in, return to the homepage
|
||||
if (!user) {
|
||||
router.push("/");
|
||||
router.push('/');
|
||||
}
|
||||
}, [user]);
|
||||
|
||||
@@ -34,7 +34,7 @@ const DashboardPage = ({ user, plan, profile }) => {
|
||||
|
||||
<Layout>
|
||||
{!session ? (
|
||||
<div className='text-center'>You are not logged in</div>
|
||||
<div className="text-center">You are not logged in</div>
|
||||
) : (
|
||||
<>
|
||||
<Dashboard
|
||||
@@ -55,14 +55,14 @@ export async function getServerSideProps({ req }) {
|
||||
process.env.SUPABASE_ADMIN_KEY
|
||||
);
|
||||
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 (user) {
|
||||
let { data: plan, error } = await supabaseAdmin
|
||||
.from("subscriptions")
|
||||
.select("plan")
|
||||
.eq("id", user.id)
|
||||
const { data: plan, error } = await supabaseAdmin
|
||||
.from('subscriptions')
|
||||
.select('plan')
|
||||
.eq('id', user.id)
|
||||
.single();
|
||||
|
||||
// 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)
|
||||
: null;
|
||||
|
||||
let { data: profile, errorProfile } = await supabaseAdmin
|
||||
.from("profiles")
|
||||
const { data: profile, errorProfile } = await supabaseAdmin
|
||||
.from('profiles')
|
||||
.select(`username, website, avatar_url`)
|
||||
.eq("id", user.id)
|
||||
.eq('id', user.id)
|
||||
.single();
|
||||
|
||||
return {
|
||||
@@ -87,7 +87,7 @@ export async function getServerSideProps({ req }) {
|
||||
|
||||
if (!user) {
|
||||
// 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.
|
||||
|
||||
@@ -3,60 +3,55 @@ Don't forget to modify the Head component with your website informations
|
||||
You can also update the content on the Landing.js component
|
||||
*/
|
||||
|
||||
import Head from "next/head";
|
||||
import Landing from "components/Landing";
|
||||
import Layout from "components/Layout";
|
||||
import Head from 'next/head';
|
||||
import Landing from 'components/Landing';
|
||||
import Layout from 'components/Layout';
|
||||
|
||||
const Home = () => {
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{`Welcome to ${process.env.NEXT_PUBLIC_TITLE} 👋`}</title>
|
||||
<meta
|
||||
name='description'
|
||||
content='SupaNexTail is a boilerplate for your SaaS, based on Next.js, Supabase, and TailwindCSS'
|
||||
/>
|
||||
const Home = () => (
|
||||
<>
|
||||
<Head>
|
||||
<title>{`Welcome to ${process.env.NEXT_PUBLIC_TITLE} 👋`}</title>
|
||||
<meta
|
||||
name="description"
|
||||
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:type' content='website' />
|
||||
<meta
|
||||
property='og:title'
|
||||
content={`Welcome to ${process.env.NEXT_PUBLIC_TITLE} 👋`}
|
||||
/>
|
||||
<meta
|
||||
property='og:description'
|
||||
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:url" content="https://supanextail.dev/" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta
|
||||
property="og:title"
|
||||
content={`Welcome to ${process.env.NEXT_PUBLIC_TITLE} 👋`}
|
||||
/>
|
||||
<meta
|
||||
property="og:description"
|
||||
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 name='twitter:card' content='summary_large_image' />
|
||||
<meta property='twitter:domain' content='supanextail.dev' />
|
||||
<meta
|
||||
property='twitter:url'
|
||||
content='https://supanextail.dev/ogimage.png'
|
||||
/>
|
||||
<meta
|
||||
name='twitter:title'
|
||||
content={`Welcome to ${process.env.NEXT_PUBLIC_TITLE} 👋`}
|
||||
/>
|
||||
<meta
|
||||
name='twitter:description'
|
||||
content='SupaNexTail is a boilerplate for your SaaS, based on Next.js, Supabase, and TailwindCSS'
|
||||
/>
|
||||
<meta
|
||||
name='twitter:image'
|
||||
content='https://supanextail.dev/ogimage.png'
|
||||
/>
|
||||
<meta charSet='UTF-8' />
|
||||
</Head>
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta property="twitter:domain" content="supanextail.dev" />
|
||||
<meta
|
||||
property="twitter:url"
|
||||
content="https://supanextail.dev/ogimage.png"
|
||||
/>
|
||||
<meta
|
||||
name="twitter:title"
|
||||
content={`Welcome to ${process.env.NEXT_PUBLIC_TITLE} 👋`}
|
||||
/>
|
||||
<meta
|
||||
name="twitter:description"
|
||||
content="SupaNexTail is a boilerplate for your SaaS, based on Next.js, Supabase, and TailwindCSS"
|
||||
/>
|
||||
<meta
|
||||
name="twitter:image"
|
||||
content="https://supanextail.dev/ogimage.png"
|
||||
/>
|
||||
<meta charSet="UTF-8" />
|
||||
</Head>
|
||||
|
||||
<Layout>
|
||||
<Landing />
|
||||
</Layout>
|
||||
</>
|
||||
);
|
||||
};
|
||||
<Layout>
|
||||
<Landing />
|
||||
</Layout>
|
||||
</>
|
||||
);
|
||||
export default Home;
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
import Layout from "components/Layout";
|
||||
import Login from "components/UI/Login";
|
||||
import { NextSeo } from "next-seo";
|
||||
import { useAuth } from "utils/AuthContext";
|
||||
import Layout from 'components/Layout';
|
||||
import Login from 'components/UI/Login';
|
||||
import { NextSeo } from 'next-seo';
|
||||
import { useAuth } from 'utils/AuthContext';
|
||||
|
||||
const LoginPage = () => {
|
||||
const { signUp, signIn, signOut, resetPassword } = useAuth();
|
||||
@@ -19,7 +19,7 @@ const LoginPage = () => {
|
||||
/>
|
||||
|
||||
<Layout>
|
||||
<div className='flex flex-wrap justify-evenly w-full mt-20'>
|
||||
<div className="flex flex-wrap justify-evenly w-full mt-20">
|
||||
<Login
|
||||
signUp={signUp}
|
||||
signIn={signIn}
|
||||
|
||||
@@ -1,20 +1,18 @@
|
||||
// To modify the content of the pricing page, check the Pricing.js component
|
||||
|
||||
import Layout from "components/Layout";
|
||||
import { NextSeo } from "next-seo";
|
||||
import Pricing from "components/Pricing";
|
||||
import Layout from 'components/Layout';
|
||||
import { NextSeo } from 'next-seo';
|
||||
import Pricing from 'components/Pricing';
|
||||
|
||||
const PricingPage = () => {
|
||||
return (
|
||||
<>
|
||||
<NextSeo
|
||||
title={`${process.env.NEXT_PUBLIC_TITLE} | Pricing`}
|
||||
description={`SupaNexTail is a boilerplate for your website, based on Next.js, Supabase, and TailwindCSS`}
|
||||
/>
|
||||
<Layout>
|
||||
<Pricing />
|
||||
</Layout>
|
||||
</>
|
||||
);
|
||||
};
|
||||
const PricingPage = () => (
|
||||
<>
|
||||
<NextSeo
|
||||
title={`${process.env.NEXT_PUBLIC_TITLE} | Pricing`}
|
||||
description="SupaNexTail is a boilerplate for your website, based on Next.js, Supabase, and TailwindCSS"
|
||||
/>
|
||||
<Layout>
|
||||
<Pricing />
|
||||
</Layout>
|
||||
</>
|
||||
);
|
||||
export default PricingPage;
|
||||
|
||||
@@ -1,20 +1,18 @@
|
||||
// To modify the privacy policy, check the PrivacyPolicy.js component
|
||||
|
||||
import Layout from "components/Layout";
|
||||
import { NextSeo } from "next-seo";
|
||||
import PrivacyPolicy from "components/PrivacyPolicy";
|
||||
import Layout from 'components/Layout';
|
||||
import { NextSeo } from 'next-seo';
|
||||
import PrivacyPolicy from 'components/PrivacyPolicy';
|
||||
|
||||
const PrivacyPage = () => {
|
||||
return (
|
||||
<>
|
||||
<NextSeo
|
||||
title={`${process.env.NEXT_PUBLIC_TITLE} | Privacy Policy`}
|
||||
description={`SupaNexTail is a boilerplate for your website, based on Next.js, Supabase, and TailwindCSS`}
|
||||
/>
|
||||
<Layout>
|
||||
<PrivacyPolicy />
|
||||
</Layout>
|
||||
</>
|
||||
);
|
||||
};
|
||||
const PrivacyPage = () => (
|
||||
<>
|
||||
<NextSeo
|
||||
title={`${process.env.NEXT_PUBLIC_TITLE} | Privacy Policy`}
|
||||
description="SupaNexTail is a boilerplate for your website, based on Next.js, Supabase, and TailwindCSS"
|
||||
/>
|
||||
<Layout>
|
||||
<PrivacyPolicy />
|
||||
</Layout>
|
||||
</>
|
||||
);
|
||||
export default PrivacyPage;
|
||||
|
||||
@@ -4,27 +4,25 @@ You have 2 components, the "AuthComponent" that handle the logic,
|
||||
and the "AuthText" that will show the description on the left of the screen
|
||||
*/
|
||||
|
||||
import AuthComponent from "components/SignUp";
|
||||
import AuthText from "components/AuthText";
|
||||
import Layout from "components/Layout";
|
||||
import { NextSeo } from "next-seo";
|
||||
import AuthComponent from 'components/SignUp';
|
||||
import AuthText from 'components/AuthText';
|
||||
import Layout from 'components/Layout';
|
||||
import { NextSeo } from 'next-seo';
|
||||
|
||||
const SignUpPage = () => {
|
||||
return (
|
||||
<>
|
||||
<NextSeo
|
||||
title={`${process.env.NEXT_PUBLIC_TITLE} | Auth`}
|
||||
description={`This is the auth page for ${process.env.NEXT_PUBLIC_TITLE}`}
|
||||
/>
|
||||
const SignUpPage = () => (
|
||||
<>
|
||||
<NextSeo
|
||||
title={`${process.env.NEXT_PUBLIC_TITLE} | Auth`}
|
||||
description={`This is the auth page for ${process.env.NEXT_PUBLIC_TITLE}`}
|
||||
/>
|
||||
|
||||
<Layout>
|
||||
<div className='flex flex-wrap justify-evenly w-full mt-20'>
|
||||
<AuthText />
|
||||
<AuthComponent />
|
||||
</div>
|
||||
</Layout>
|
||||
</>
|
||||
);
|
||||
};
|
||||
<Layout>
|
||||
<div className="flex flex-wrap justify-evenly w-full mt-20">
|
||||
<AuthText />
|
||||
<AuthComponent />
|
||||
</div>
|
||||
</Layout>
|
||||
</>
|
||||
);
|
||||
|
||||
export default SignUpPage;
|
||||
|
||||
@@ -1,20 +1,18 @@
|
||||
// To modify the terms & conditions, check the Terms.js component
|
||||
|
||||
import Layout from "components/Layout";
|
||||
import { NextSeo } from "next-seo";
|
||||
import Terms from "components/Terms";
|
||||
import Layout from 'components/Layout';
|
||||
import { NextSeo } from 'next-seo';
|
||||
import Terms from 'components/Terms';
|
||||
|
||||
const TermsPage = () => {
|
||||
return (
|
||||
<>
|
||||
<NextSeo
|
||||
title={`${process.env.NEXT_PUBLIC_TITLE} | Terms and conditions`}
|
||||
description={`SupaNexTail is a boilerplate for your website, based on Next.js, Supabase, and TailwindCSS`}
|
||||
/>
|
||||
<Layout>
|
||||
<Terms />
|
||||
</Layout>
|
||||
</>
|
||||
);
|
||||
};
|
||||
const TermsPage = () => (
|
||||
<>
|
||||
<NextSeo
|
||||
title={`${process.env.NEXT_PUBLIC_TITLE} | Terms and conditions`}
|
||||
description="SupaNexTail is a boilerplate for your website, based on Next.js, Supabase, and TailwindCSS"
|
||||
/>
|
||||
<Layout>
|
||||
<Terms />
|
||||
</Layout>
|
||||
</>
|
||||
);
|
||||
export default TermsPage;
|
||||
|
||||
@@ -5,4 +5,4 @@ module.exports = {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,74 +1,74 @@
|
||||
module.exports = {
|
||||
mode: "jit",
|
||||
purge: ["./pages/**/*.{js,ts,jsx,tsx}", "./components/**/*.{js,ts,jsx,tsx}"],
|
||||
mode: 'jit',
|
||||
purge: ['./pages/**/*.{js,ts,jsx,tsx}', './components/**/*.{js,ts,jsx,tsx}'],
|
||||
darkMode: false, // or 'media' or 'class'
|
||||
theme: {
|
||||
fontFamily: {
|
||||
title: ["Poppins"],
|
||||
body: ["Inter"],
|
||||
title: ['Poppins'],
|
||||
body: ['Inter'],
|
||||
},
|
||||
extend: {},
|
||||
},
|
||||
variants: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [require("daisyui")],
|
||||
plugins: [require('daisyui')],
|
||||
daisyui: {
|
||||
themes: [
|
||||
{
|
||||
supaTheme: {
|
||||
primary: "#00B8F0",
|
||||
"primary-focus": "#009de0",
|
||||
"primary-content": "#ffffff",
|
||||
primary: '#00B8F0',
|
||||
'primary-focus': '#009de0',
|
||||
'primary-content': '#ffffff',
|
||||
|
||||
secondary: "#f03800",
|
||||
"secondary-focus": "#e22f00",
|
||||
"secondary-content": "#ffffff",
|
||||
secondary: '#f03800',
|
||||
'secondary-focus': '#e22f00',
|
||||
'secondary-content': '#ffffff',
|
||||
|
||||
accent: "#00f0b0",
|
||||
"accent-focus": "#00e28a",
|
||||
"accent-content": "#ffffff",
|
||||
accent: '#00f0b0',
|
||||
'accent-focus': '#00e28a',
|
||||
'accent-content': '#ffffff',
|
||||
|
||||
neutral: "#3d4451",
|
||||
"neutral-focus": "#2a2e37",
|
||||
"neutral-content": "#ffffff",
|
||||
neutral: '#3d4451',
|
||||
'neutral-focus': '#2a2e37',
|
||||
'neutral-content': '#ffffff',
|
||||
|
||||
"base-100": "#ffffff",
|
||||
"base-200": "#767676",
|
||||
"base-300": "#d1d5db",
|
||||
"base-content": "#1f2937",
|
||||
'base-100': '#ffffff',
|
||||
'base-200': '#767676',
|
||||
'base-300': '#d1d5db',
|
||||
'base-content': '#1f2937',
|
||||
|
||||
info: "#2094f3" /* Info */,
|
||||
success: "#009485" /* Success */,
|
||||
warning: "#ff9900" /* Warning */,
|
||||
error: "#ff5724" /* Error */,
|
||||
info: '#2094f3' /* Info */,
|
||||
success: '#009485' /* Success */,
|
||||
warning: '#ff9900' /* Warning */,
|
||||
error: '#ff5724' /* Error */,
|
||||
},
|
||||
dark: {
|
||||
primary: "#00B8F0",
|
||||
"primary-focus": "#009de0",
|
||||
"primary-content": "#ffffff",
|
||||
primary: '#00B8F0',
|
||||
'primary-focus': '#009de0',
|
||||
'primary-content': '#ffffff',
|
||||
|
||||
secondary: "#f03800",
|
||||
"secondary-focus": "#e22f00",
|
||||
"secondary-content": "#ffffff",
|
||||
secondary: '#f03800',
|
||||
'secondary-focus': '#e22f00',
|
||||
'secondary-content': '#ffffff',
|
||||
|
||||
accent: "#00f0b0",
|
||||
"accent-focus": "#00e28a",
|
||||
"accent-content": "#ffffff",
|
||||
accent: '#00f0b0',
|
||||
'accent-focus': '#00e28a',
|
||||
'accent-content': '#ffffff',
|
||||
|
||||
neutral: "#3d4451",
|
||||
"neutral-focus": "#2a2e37",
|
||||
"neutral-content": "#ffffff",
|
||||
neutral: '#3d4451',
|
||||
'neutral-focus': '#2a2e37',
|
||||
'neutral-content': '#ffffff',
|
||||
|
||||
"base-100": "#2A2E37",
|
||||
"base-200": "#EBECF0",
|
||||
"base-300": "#16181D",
|
||||
"base-content": "#EBECF0",
|
||||
'base-100': '#2A2E37',
|
||||
'base-200': '#EBECF0',
|
||||
'base-300': '#16181D',
|
||||
'base-content': '#EBECF0',
|
||||
|
||||
info: "#2094f3",
|
||||
success: "#009485",
|
||||
warning: "#ff9900",
|
||||
error: "#ff5724",
|
||||
info: '#2094f3',
|
||||
success: '#009485',
|
||||
warning: '#ff9900',
|
||||
error: '#ff5724',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -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
|
||||
const AuthContext = createContext();
|
||||
@@ -19,15 +19,15 @@ export const AuthProvider = ({ children }) => {
|
||||
// Listen for changes on auth state (logged in, signed out, etc.)
|
||||
const { data: listener } = supabase.auth.onAuthStateChange(
|
||||
async (event, session) => {
|
||||
if ((event === "SIGNED_OUT") | (event === "SIGNED_IN")) {
|
||||
fetch("/api/auth", {
|
||||
method: "POST",
|
||||
headers: new Headers({ "Content-Type": "application/json" }),
|
||||
credentials: "same-origin",
|
||||
if ((event === 'SIGNED_OUT') | (event === 'SIGNED_IN')) {
|
||||
fetch('/api/auth', {
|
||||
method: 'POST',
|
||||
headers: new Headers({ 'Content-Type': 'application/json' }),
|
||||
credentials: 'same-origin',
|
||||
body: JSON.stringify({ event, session }),
|
||||
}).then((res) => res.json());
|
||||
}
|
||||
if (event === "USER_UPDATED") {
|
||||
if (event === 'USER_UPDATED') {
|
||||
}
|
||||
setUser(session?.user ?? null);
|
||||
setLoading(false);
|
||||
@@ -56,6 +56,4 @@ export const AuthProvider = ({ children }) => {
|
||||
};
|
||||
|
||||
// export the useAuth hook
|
||||
export const useAuth = () => {
|
||||
return useContext(AuthContext);
|
||||
};
|
||||
export const useAuth = () => useContext(AuthContext);
|
||||
|
||||
@@ -3,43 +3,43 @@
|
||||
const Prices = {
|
||||
personal: {
|
||||
monthly: {
|
||||
id: "price_1J5q2yDMjD0UnVmMXzEWYDnl",
|
||||
desc: "Personal plan (monthly)",
|
||||
id: 'price_1J5q2yDMjD0UnVmMXzEWYDnl',
|
||||
desc: 'Personal plan (monthly)',
|
||||
},
|
||||
annually: {
|
||||
id: "price_1J5q45DMjD0UnVmMQxXHKGAv",
|
||||
desc: "Personal plan (annually)",
|
||||
id: 'price_1J5q45DMjD0UnVmMQxXHKGAv',
|
||||
desc: 'Personal plan (annually)',
|
||||
},
|
||||
},
|
||||
team: {
|
||||
monthly: {
|
||||
id: "price_1J5q3GDMjD0UnVmMlHc5Eedq",
|
||||
desc: "Team plan (monthly)",
|
||||
id: 'price_1J5q3GDMjD0UnVmMlHc5Eedq',
|
||||
desc: 'Team plan (monthly)',
|
||||
},
|
||||
annually: {
|
||||
id: "price_1J5q8zDMjD0UnVmMqsngM91X",
|
||||
desc: "Team plan (annually)",
|
||||
id: 'price_1J5q8zDMjD0UnVmMqsngM91X',
|
||||
desc: 'Team plan (annually)',
|
||||
},
|
||||
},
|
||||
pro: {
|
||||
monthly: {
|
||||
id: "price_1J6KRuDMjD0UnVmMIItaOdT3",
|
||||
desc: "Pro plan (monthly)",
|
||||
id: 'price_1J6KRuDMjD0UnVmMIItaOdT3',
|
||||
desc: 'Pro plan (monthly)',
|
||||
},
|
||||
annually: {
|
||||
id: "price_1J5q9VDMjD0UnVmMIQtVDSZ9",
|
||||
desc: "Pro plan (annually)",
|
||||
id: 'price_1J5q9VDMjD0UnVmMIQtVDSZ9',
|
||||
desc: 'Pro plan (annually)',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const PriceIds = {
|
||||
price_1J5q2yDMjD0UnVmMXzEWYDnl: "Personal plan (monthly)",
|
||||
price_1J5q45DMjD0UnVmMQxXHKGAv: "Personal plan (annually)",
|
||||
price_1J5q3GDMjD0UnVmMlHc5Eedq: "Team plan (monthly)",
|
||||
price_1J5q8zDMjD0UnVmMqsngM91X: "Team plan (annually)",
|
||||
price_1J6KRuDMjD0UnVmMIItaOdT3: "Pro plan (monthly)",
|
||||
price_1J5q9VDMjD0UnVmMIQtVDSZ9: "Pro plan (annually)",
|
||||
price_1J5q2yDMjD0UnVmMXzEWYDnl: 'Personal plan (monthly)',
|
||||
price_1J5q45DMjD0UnVmMQxXHKGAv: 'Personal plan (annually)',
|
||||
price_1J5q3GDMjD0UnVmMlHc5Eedq: 'Team plan (monthly)',
|
||||
price_1J5q8zDMjD0UnVmMqsngM91X: 'Team plan (annually)',
|
||||
price_1J6KRuDMjD0UnVmMIItaOdT3: 'Pro plan (monthly)',
|
||||
price_1J5q9VDMjD0UnVmMIQtVDSZ9: 'Pro plan (annually)',
|
||||
};
|
||||
|
||||
export { Prices, PriceIds };
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* 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;
|
||||
const getStripe = () => {
|
||||
|
||||
@@ -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 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
|
||||
export const getSub = async () => {
|
||||
let { data: subscriptions, error } = await supabase
|
||||
.from("subscriptions")
|
||||
.select("paid_user, plan")
|
||||
const { data: subscriptions, error } = await supabase
|
||||
.from('subscriptions')
|
||||
.select('paid_user, plan')
|
||||
.single();
|
||||
if (subscriptions) {
|
||||
return subscriptions;
|
||||
|
||||
Reference in New Issue
Block a user