mirror of
https://github.com/fergalmoran/supanextail.git
synced 2025-12-22 09:17:54 +00:00
Typescript first integration + fix all eslint errors (97 warnings)
This commit is contained in:
@@ -6,8 +6,9 @@
|
|||||||
"cypress/globals": true
|
"cypress/globals": true
|
||||||
},
|
},
|
||||||
"extends": [
|
"extends": [
|
||||||
"plugin:react/recommended",
|
"next",
|
||||||
"airbnb",
|
"next/core-web-vitals",
|
||||||
|
"plugin:@typescript-eslint/recommended",
|
||||||
"prettier"
|
"prettier"
|
||||||
],
|
],
|
||||||
"parserOptions": {
|
"parserOptions": {
|
||||||
@@ -18,7 +19,7 @@
|
|||||||
"sourceType": "module"
|
"sourceType": "module"
|
||||||
},
|
},
|
||||||
"plugins": [
|
"plugins": [
|
||||||
"react",
|
"@typescript-eslint",
|
||||||
"cypress",
|
"cypress",
|
||||||
"simple-import-sort",
|
"simple-import-sort",
|
||||||
"prettier"
|
"prettier"
|
||||||
@@ -26,7 +27,12 @@
|
|||||||
"rules": {
|
"rules": {
|
||||||
"no-console": "off",
|
"no-console": "off",
|
||||||
"react/no-unescaped-entities": "off",
|
"react/no-unescaped-entities": "off",
|
||||||
"prettier/prettier": "error",
|
"prettier/prettier": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"endOfLine": "auto"
|
||||||
|
}
|
||||||
|
],
|
||||||
"react/react-in-jsx-scope": "off",
|
"react/react-in-jsx-scope": "off",
|
||||||
"react/jsx-filename-extension": [
|
"react/jsx-filename-extension": [
|
||||||
2,
|
2,
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
{
|
{
|
||||||
|
"semi": true,
|
||||||
|
"trailingComma": "es5",
|
||||||
"singleQuote": true,
|
"singleQuote": true,
|
||||||
"tabWidth": 2,
|
"tabWidth": 2,
|
||||||
"semi": true
|
"useTabs": true,
|
||||||
|
"endOfLine": "auto",
|
||||||
|
"printWidth": 100
|
||||||
}
|
}
|
||||||
@@ -2,22 +2,18 @@ import Image from 'next/image';
|
|||||||
import authImage from 'public/auth.png';
|
import authImage from 'public/auth.png';
|
||||||
|
|
||||||
const AuthText = () => (
|
const AuthText = () => (
|
||||||
<div className="lg:mt-0 max-w-lg flex flex-col text-xl">
|
<div className="lg:mt-0 max-w-lg flex flex-col text-xl">
|
||||||
<div className="mt-10 mb-3 m-auto">
|
<div className="mt-10 mb-3 m-auto">
|
||||||
<Image
|
<Image src={authImage} width={authImage.width / 1.5} height={authImage.height / 1.5} />
|
||||||
src={authImage}
|
</div>
|
||||||
width={authImage.width / 1.5}
|
<h2 className="text-4xl font-title font-semibold text-center">
|
||||||
height={authImage.height / 1.5}
|
Join SupaNexTail for <span className="text-primary">free</span>!
|
||||||
/>
|
</h2>
|
||||||
</div>
|
<p className="mb-5 mt-8 leading-9">
|
||||||
<h2 className="text-4xl font-title font-semibold text-center">
|
Create your website in a few minutes with our boilerplate. You can use the login system, this
|
||||||
Join SupaNexTail for <span className="text-primary">free</span>!
|
will allow you to discover the sample dashboard page.
|
||||||
</h2>
|
</p>
|
||||||
<p className="mb-5 mt-8 leading-9">
|
</div>
|
||||||
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;
|
export default AuthText;
|
||||||
|
|||||||
@@ -6,99 +6,105 @@ You can tweak the max size, line 47
|
|||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import Image from 'next/image';
|
||||||
import { supabase } from 'utils/supabaseClient';
|
import { supabase } from 'utils/supabaseClient';
|
||||||
|
|
||||||
const Avatar = ({ url, size, onUpload }) => {
|
const Avatar = ({ url, size, onUpload }) => {
|
||||||
const [avatarUrl, setAvatarUrl] = useState(null);
|
const [avatarUrl, setAvatarUrl] = useState(null);
|
||||||
const [uploading, setUploading] = useState(false);
|
const [uploading, setUploading] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
const customImgLoader = ({ src }) => {
|
||||||
if (url) downloadImage(url);
|
return `${src}`;
|
||||||
}, [url]);
|
};
|
||||||
|
|
||||||
async function downloadImage(path) {
|
useEffect(() => {
|
||||||
try {
|
if (url) downloadImage(url);
|
||||||
const { data, error } = await supabase.storage
|
}, [url]);
|
||||||
.from('avatars')
|
|
||||||
.download(path);
|
|
||||||
if (error) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
const url = URL.createObjectURL(data);
|
|
||||||
setAvatarUrl(url);
|
|
||||||
} catch (error) {
|
|
||||||
console.log('Error downloading image: ', error.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function uploadAvatar(event) {
|
async function downloadImage(path) {
|
||||||
try {
|
try {
|
||||||
setUploading(true);
|
const { data, error } = await supabase.storage.from('avatars').download(path);
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
const url = URL.createObjectURL(data);
|
||||||
|
setAvatarUrl(url);
|
||||||
|
} catch (error) {
|
||||||
|
console.log('Error downloading image: ', error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!event.target.files || event.target.files.length === 0) {
|
async function uploadAvatar(event) {
|
||||||
throw new Error('You must select an image to upload.');
|
try {
|
||||||
}
|
setUploading(true);
|
||||||
|
|
||||||
const file = event.target.files[0];
|
if (!event.target.files || event.target.files.length === 0) {
|
||||||
const fileExt = file.name.split('.').pop();
|
throw new Error('You must select an image to upload.');
|
||||||
const fileName = `${Math.random()}.${fileExt}`;
|
}
|
||||||
const filePath = `${fileName}`;
|
|
||||||
|
|
||||||
if (event.target.files[0].size > 150000) {
|
const file = event.target.files[0];
|
||||||
alert('File is too big!');
|
const fileExt = file.name.split('.').pop();
|
||||||
event.target.value = '';
|
const fileName = `${Math.random()}.${fileExt}`;
|
||||||
setUploading(false);
|
const filePath = `${fileName}`;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { error: uploadError } = await supabase.storage
|
if (event.target.files[0].size > 150000) {
|
||||||
.from('avatars')
|
alert('File is too big!');
|
||||||
.upload(filePath, file);
|
event.target.value = '';
|
||||||
|
setUploading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (uploadError) {
|
const { error: uploadError } = await supabase.storage.from('avatars').upload(filePath, file);
|
||||||
throw uploadError;
|
|
||||||
}
|
|
||||||
|
|
||||||
onUpload(filePath);
|
if (uploadError) {
|
||||||
} catch (error) {
|
throw uploadError;
|
||||||
alert(error.message);
|
}
|
||||||
} finally {
|
|
||||||
setUploading(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
onUpload(filePath);
|
||||||
<div className="m-auto mb-5">
|
} catch (error) {
|
||||||
{avatarUrl ? (
|
alert(error.message);
|
||||||
<img
|
} finally {
|
||||||
src={avatarUrl}
|
setUploading(false);
|
||||||
alt="Avatar"
|
}
|
||||||
className="avatar rounded-full w-28 h-28 flex m-auto"
|
}
|
||||||
/>
|
|
||||||
) : (
|
return (
|
||||||
<div className="avatar rounded-full w-28 h-28" />
|
<div className="m-auto mb-5">
|
||||||
)}
|
{avatarUrl ? (
|
||||||
<div style={{ width: size }}>
|
<div className="w-full flex justify-center">
|
||||||
<label
|
<Image
|
||||||
className="mt-2 btn btn-primary text-center cursor-pointer text-xs btn-sm"
|
loader={customImgLoader} //Using custom loader because of this issue https://github.com/vercel/next.js/discussions/19732
|
||||||
htmlFor="single"
|
src={avatarUrl}
|
||||||
>
|
height={100}
|
||||||
{uploading ? 'Uploading ...' : 'Update my avatar'}
|
width={100}
|
||||||
</label>
|
alt="Avatar"
|
||||||
<input
|
className="avatar rounded-full w-28 h-28"
|
||||||
style={{
|
/>
|
||||||
visibility: 'hidden',
|
</div>
|
||||||
position: 'absolute',
|
) : (
|
||||||
}}
|
<div className="avatar rounded-full w-28 h-28" />
|
||||||
type="file"
|
)}
|
||||||
id="single"
|
<div style={{ width: size }}>
|
||||||
accept="image/*"
|
<label
|
||||||
onChange={uploadAvatar}
|
className="mt-2 btn btn-primary text-center cursor-pointer text-xs btn-sm"
|
||||||
disabled={uploading}
|
htmlFor="single"
|
||||||
/>
|
>
|
||||||
</div>
|
{uploading ? 'Uploading ...' : 'Update my avatar'}
|
||||||
</div>
|
</label>
|
||||||
);
|
<input
|
||||||
|
style={{
|
||||||
|
visibility: 'hidden',
|
||||||
|
position: 'absolute',
|
||||||
|
}}
|
||||||
|
type="file"
|
||||||
|
id="single"
|
||||||
|
accept="image/*"
|
||||||
|
onChange={uploadAvatar}
|
||||||
|
disabled={uploading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Avatar;
|
export default Avatar;
|
||||||
|
|||||||
@@ -8,53 +8,48 @@ import cardStripe from 'public/landing/stripe.svg';
|
|||||||
import cardTheme from 'public/landing/theme.svg';
|
import cardTheme from 'public/landing/theme.svg';
|
||||||
|
|
||||||
const CardsLanding = () => (
|
const CardsLanding = () => (
|
||||||
<div className="mt-14">
|
<div className="mt-14">
|
||||||
<h2 className="uppercase font-bold text-4xl tracking-wide text-center">
|
<h2 className="uppercase font-bold text-4xl tracking-wide text-center">
|
||||||
We've got you covered
|
We've got you covered
|
||||||
</h2>
|
</h2>
|
||||||
<p className="max-w-md m-auto text-center">
|
<p className="max-w-md m-auto text-center">
|
||||||
Don’t waste your time and reinvent the wheel, we have provided you with a
|
Don’t waste your time and reinvent the wheel, we have provided you with a maximum of features
|
||||||
maximum of features so that you only have one goal, to make your SaaS a
|
so that you only have one goal, to make your SaaS a reality.
|
||||||
reality.
|
</p>
|
||||||
</p>
|
<div className="flex flex-wrap justify-center mt-10">
|
||||||
<div className="flex flex-wrap justify-center mt-10">
|
<CardLanding
|
||||||
<CardLanding
|
image={cardPage}
|
||||||
image={cardPage}
|
text="7 pages fully designed and easily customizable"
|
||||||
text="7 pages fully designed and easily customizable"
|
title="Templates"
|
||||||
title="Templates"
|
/>
|
||||||
/>
|
<CardLanding
|
||||||
<CardLanding
|
image={cardServer}
|
||||||
image={cardServer}
|
text="Integrated backend already setup with Next.js API Routes"
|
||||||
text="Integrated backend already setup with Next.js API Routes"
|
title="Backend"
|
||||||
title="Backend"
|
/>
|
||||||
/>
|
<CardLanding image={cardAuth} text="Auth and user management with Supabase" title="Auth" />
|
||||||
<CardLanding
|
<CardLanding
|
||||||
image={cardAuth}
|
image={cardResponsive}
|
||||||
text="Auth and user management with Supabase"
|
text="Mobile ready, fully responsive and customizable with Tailwind CSS"
|
||||||
title="Auth"
|
title="Responsive"
|
||||||
/>
|
/>
|
||||||
<CardLanding
|
<CardLanding
|
||||||
image={cardResponsive}
|
image={cardTheme}
|
||||||
text="Mobile ready, fully responsive and customizable with Tailwind CSS"
|
text="Custom themes available and easily switch to dark mode"
|
||||||
title="Responsive"
|
title="Themes"
|
||||||
/>
|
/>
|
||||||
<CardLanding
|
<CardLanding
|
||||||
image={cardTheme}
|
image={cardStripe}
|
||||||
text="Custom themes available and easily switch to dark mode"
|
text="Stripe integration. Fully functional subscription system"
|
||||||
title="Themes"
|
title="Payment"
|
||||||
/>
|
/>
|
||||||
<CardLanding
|
<CardLanding
|
||||||
image={cardStripe}
|
image={cardFee}
|
||||||
text="Stripe integration. Fully functional subscription system"
|
text="One-time fee. No subscription, you’ll have access to all the updates"
|
||||||
title="Payment"
|
title="Lifetime access"
|
||||||
/>
|
/>
|
||||||
<CardLanding
|
</div>
|
||||||
image={cardFee}
|
</div>
|
||||||
text="One-time fee. No subscription, you’ll have access to all the updates"
|
|
||||||
title="Lifetime access"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
export default CardsLanding;
|
export default CardsLanding;
|
||||||
|
|||||||
@@ -10,92 +10,92 @@ import axios from 'axios';
|
|||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
const Contact = () => {
|
const Contact = () => {
|
||||||
const sendEmail = () => {
|
const sendEmail = () => {
|
||||||
const name = document.getElementById('name').value;
|
const name = document.getElementById('name').value;
|
||||||
const email = document.getElementById('email').value;
|
const email = document.getElementById('email').value;
|
||||||
const message = document.getElementById('message').value;
|
const message = document.getElementById('message').value;
|
||||||
|
|
||||||
if (name && email && message) {
|
if (name && email && message) {
|
||||||
axios
|
axios
|
||||||
.post('/api/sendgrid', { email, name, message })
|
.post('/api/sendgrid', { email, name, message })
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
if (result.data.success === true) {
|
if (result.data.success === true) {
|
||||||
toast.success(result.data.message);
|
toast.success(result.data.message);
|
||||||
document.getElementById('name').value = '';
|
document.getElementById('name').value = '';
|
||||||
document.getElementById('email').value = '';
|
document.getElementById('email').value = '';
|
||||||
document.getElementById('message').value = '';
|
document.getElementById('message').value = '';
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.log(err);
|
console.log(err);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
toast.info('Please enter at least one URL', {
|
toast.info('Please enter at least one URL', {
|
||||||
position: 'top-center',
|
position: 'top-center',
|
||||||
autoClose: 2000,
|
autoClose: 2000,
|
||||||
hideProgressBar: true,
|
hideProgressBar: true,
|
||||||
closeOnClick: true,
|
closeOnClick: true,
|
||||||
pauseOnHover: true,
|
pauseOnHover: true,
|
||||||
draggable: true,
|
draggable: true,
|
||||||
progress: undefined,
|
progress: undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<div className="max-w-xl m-auto px-5 py-10">
|
<div className="max-w-xl m-auto px-5 py-10">
|
||||||
<div>
|
<div>
|
||||||
<div className="flex justify-center">
|
<div className="flex justify-center">
|
||||||
<h2 className="text-3xl sm:text-4xl text-center mb-5 mt-0 font-bold font-title">
|
<h2 className="text-3xl sm:text-4xl text-center mb-5 mt-0 font-bold font-title">
|
||||||
Contact
|
Contact
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<p className="m-auto text-center">
|
<p className="m-auto text-center">
|
||||||
Do you have a question about SupaNexTail? A cool feature you'd like us
|
Do you have a question about SupaNexTail? A cool feature you'd like us to integrate? A bug
|
||||||
to integrate? A bug to report? Don't hesitate!
|
to report? Don't hesitate!
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<form className="m-auto mt-5 grid grid-cols-1 gap-4 md:grid-cols-2 p-5">
|
<form className="m-auto mt-5 grid grid-cols-1 gap-4 md:grid-cols-2 p-5">
|
||||||
<div className="flex flex-col max-w-xs">
|
<div className="flex flex-col max-w-xs">
|
||||||
<p className="font-light mb-4 text-left">Your Name</p>
|
<p className="font-light mb-4 text-left">Your Name</p>
|
||||||
<input
|
<input
|
||||||
id="name"
|
id="name"
|
||||||
name="name"
|
name="name"
|
||||||
placeholder="Enter your name"
|
placeholder="Enter your name"
|
||||||
className="input input-primary input-bordered"
|
className="input input-primary input-bordered"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col max-w-xs mb-3">
|
<div className="flex flex-col max-w-xs mb-3">
|
||||||
<p className="font-light mb-4 text-left">Your email</p>
|
<p className="font-light mb-4 text-left">Your email</p>
|
||||||
<input
|
<input
|
||||||
id="email"
|
id="email"
|
||||||
name="email"
|
name="email"
|
||||||
placeholder="Enter your email adress"
|
placeholder="Enter your email adress"
|
||||||
className="input input-primary input-bordered"
|
className="input input-primary input-bordered"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col col-span-full w-fulll">
|
<div className="flex flex-col col-span-full w-fulll">
|
||||||
<p className="font-light mb-4 text-left">Message</p>
|
<p className="font-light mb-4 text-left">Message</p>
|
||||||
<textarea
|
<textarea
|
||||||
id="message"
|
id="message"
|
||||||
name="message"
|
name="message"
|
||||||
placeholder="Enter your message here..."
|
placeholder="Enter your message here..."
|
||||||
rows="5"
|
rows="5"
|
||||||
className="input input-primary input-bordered resize-none w-full h-32 pt-2"
|
className="input input-primary input-bordered resize-none w-full h-32 pt-2"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="btn btn-primary btn-sm"
|
className="btn btn-primary btn-sm"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
sendEmail();
|
sendEmail();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Submit{' '}
|
Submit{' '}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Contact;
|
export default Contact;
|
||||||
|
|||||||
@@ -17,117 +17,115 @@ import PaymentModal from './PaymentModal';
|
|||||||
import Avatar from './Avatar';
|
import Avatar from './Avatar';
|
||||||
|
|
||||||
export default function Dashboard(props) {
|
export default function Dashboard(props) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [username, setUsername] = useState(props.profile.username);
|
const [username, setUsername] = useState(props.profile.username);
|
||||||
const [website, setWebsite] = useState(props.profile.website);
|
const [website, setWebsite] = useState(props.profile.website);
|
||||||
const [avatar_url, setAvatarUrl] = useState(props.profile.avatar_url);
|
const [avatar_url, setAvatarUrl] = useState(props.profile.avatar_url);
|
||||||
const [payment, setPayment] = useState(false);
|
const [payment, setPayment] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (router.query.session_id && router.query.session_id !== 'canceled') {
|
if (router.query.session_id && router.query.session_id !== 'canceled') {
|
||||||
setPayment(true);
|
setPayment(true);
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
async function updateProfile({ username, website, avatar_url }) {
|
async function updateProfile({ username, website, avatar_url }) {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const user = supabase.auth.user();
|
const user = supabase.auth.user();
|
||||||
|
|
||||||
const updates = {
|
const updates = {
|
||||||
id: user.id,
|
id: user.id,
|
||||||
username,
|
username,
|
||||||
website,
|
website,
|
||||||
avatar_url,
|
avatar_url,
|
||||||
updated_at: new Date(),
|
updated_at: new Date(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const { error } = await supabase.from('profiles').upsert(updates, {
|
const { error } = await supabase.from('profiles').upsert(updates, {
|
||||||
returning: 'minimal', // Don't return the value after inserting
|
returning: 'minimal', // Don't return the value after inserting
|
||||||
});
|
});
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
alert(error.message);
|
alert(error.message);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
toast.success('Your profile has been updated');
|
toast.success('Your profile has been updated');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col text-left w-full max-w-xl m-auto px-5 py-10">
|
<div className="flex flex-col text-left w-full max-w-xl m-auto px-5 py-10">
|
||||||
<div className="max-w-sm flex flex-col justify-center m-auto w-full p-5">
|
<div className="max-w-sm flex flex-col justify-center m-auto w-full p-5">
|
||||||
<h1 className="text-4xl font-bold md:text-5xl font-title text-center mb-10">
|
<h1 className="text-4xl font-bold md:text-5xl font-title text-center mb-10">Dashboard</h1>
|
||||||
Dashboard
|
<Avatar
|
||||||
</h1>
|
url={avatar_url}
|
||||||
<Avatar
|
size={150}
|
||||||
url={avatar_url}
|
onUpload={(url) => {
|
||||||
size={150}
|
setAvatarUrl(url);
|
||||||
onUpload={(url) => {
|
updateProfile({ username, website, avatar_url: url });
|
||||||
setAvatarUrl(url);
|
}}
|
||||||
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">
|
Email
|
||||||
<label htmlFor="email" className="my-auto text-sm mb-2">
|
</label>
|
||||||
Email
|
<input
|
||||||
</label>
|
className="input input-primary input-bordered input-sm flex-1 text-base-100"
|
||||||
<input
|
id="email"
|
||||||
className="input input-primary input-bordered input-sm flex-1 text-base-100"
|
type="text"
|
||||||
id="email"
|
value={props.session.user.email}
|
||||||
type="text"
|
disabled
|
||||||
value={props.session.user.email}
|
/>
|
||||||
disabled
|
</div>
|
||||||
/>
|
<div className="mb-5 flex flex-col">
|
||||||
</div>
|
<label htmlFor="username" className="my-auto text-sm mb-2">
|
||||||
<div className="mb-5 flex flex-col">
|
Name
|
||||||
<label htmlFor="username" className="my-auto text-sm mb-2">
|
</label>
|
||||||
Name
|
<input
|
||||||
</label>
|
className="input input-primary input-bordered input-sm flex-1"
|
||||||
<input
|
id="username"
|
||||||
className="input input-primary input-bordered input-sm flex-1"
|
type="text"
|
||||||
id="username"
|
value={username || ''}
|
||||||
type="text"
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
value={username || ''}
|
/>
|
||||||
onChange={(e) => setUsername(e.target.value)}
|
</div>
|
||||||
/>
|
<div className="mb-5 flex flex-col">
|
||||||
</div>
|
<label htmlFor="website" className="my-auto text-sm mb-2">
|
||||||
<div className="mb-5 flex flex-col">
|
Website
|
||||||
<label htmlFor="website" className="my-auto text-sm mb-2">
|
</label>
|
||||||
Website
|
<input
|
||||||
</label>
|
className="input input-primary input-bordered input-sm flex-1"
|
||||||
<input
|
id="website"
|
||||||
className="input input-primary input-bordered input-sm flex-1"
|
type="website"
|
||||||
id="website"
|
value={website || ''}
|
||||||
type="website"
|
onChange={(e) => setWebsite(e.target.value)}
|
||||||
value={website || ''}
|
/>
|
||||||
onChange={(e) => setWebsite(e.target.value)}
|
</div>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="m-auto">
|
<div className="m-auto">
|
||||||
<button
|
<button
|
||||||
className="btn btn-primary btn-sm"
|
className="btn btn-primary btn-sm"
|
||||||
onClick={() => updateProfile({ username, website, avatar_url })}
|
onClick={() => updateProfile({ username, website, avatar_url })}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
>
|
>
|
||||||
{loading ? 'Loading ...' : 'Update My Profile'}
|
{loading ? 'Loading ...' : 'Update My Profile'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="max-w-xl flex flex-row flex-wrap m-auto w-full p-5 bordered border-2 border-primary shadow-lg my-5">
|
<div className="max-w-xl flex flex-row flex-wrap m-auto w-full p-5 bordered border-2 border-primary shadow-lg my-5">
|
||||||
<Image src={Plan} />
|
<Image src={Plan} />
|
||||||
<div className="flex flex-col m-auto">
|
<div className="flex flex-col m-auto">
|
||||||
<h2>Your current plan</h2>
|
<h2>Your current plan</h2>
|
||||||
<p className="">{props.plan ? PriceIds[props.plan] : 'Free tier'}</p>
|
<p className="">{props.plan ? PriceIds[props.plan] : 'Free tier'}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<PaymentModal open={payment} setPayment={setPayment} />
|
<PaymentModal open={payment} setPayment={setPayment} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,27 +2,27 @@ import Link from 'next/link';
|
|||||||
import dynamic from 'next/dynamic';
|
import dynamic from 'next/dynamic';
|
||||||
|
|
||||||
const Footer = () => {
|
const Footer = () => {
|
||||||
const ThemeToggle = dynamic(() => import('components/UI/ThemeToggle.js'), {
|
const ThemeToggle = dynamic(() => import('components/UI/ThemeToggle.js'), {
|
||||||
ssr: false,
|
ssr: false,
|
||||||
});
|
});
|
||||||
return (
|
return (
|
||||||
<footer className="w-full flex">
|
<footer className="w-full flex">
|
||||||
<nav className=" mr-auto">
|
<nav className=" mr-auto">
|
||||||
<div className="flex flex-col sm:flex-row justify-evenly w-full sm:space-x-10">
|
<div className="flex flex-col sm:flex-row justify-evenly w-full sm:space-x-10">
|
||||||
<div className="">© {process.env.NEXT_PUBLIC_TITLE}</div>
|
<div className="">© {process.env.NEXT_PUBLIC_TITLE}</div>
|
||||||
<Link href="/privacy">
|
<Link href="/privacy">
|
||||||
<a>Privacy Policy</a>
|
<a>Privacy Policy</a>
|
||||||
</Link>
|
</Link>
|
||||||
<Link href="/terms">
|
<Link href="/terms">
|
||||||
<a>Terms of service</a>
|
<a>Terms of service</a>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
<div className="mr-5 my-auto">
|
<div className="mr-5 my-auto">
|
||||||
<ThemeToggle />
|
<ThemeToggle />
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Footer;
|
export default Footer;
|
||||||
|
|||||||
@@ -6,60 +6,57 @@ import supabaseImage from 'public/landing/supabase.svg';
|
|||||||
import MailingList from './MailingList';
|
import MailingList from './MailingList';
|
||||||
|
|
||||||
const Landing = () => (
|
const Landing = () => (
|
||||||
<div className="mt-10 mb-20 text-base-content w-full">
|
<div className="mt-10 mb-20 text-base-content w-full">
|
||||||
<div className="flex max-w-6xl m-auto justify-around">
|
<div className="flex max-w-6xl m-auto justify-around">
|
||||||
<div className="max-w-sm mr-16 my-auto">
|
<div className="max-w-sm mr-16 my-auto">
|
||||||
<h2 className="text-4xl font-bold font-title text-left leading-normal">
|
<h2 className="text-4xl font-bold font-title text-left leading-normal">
|
||||||
Build your <span className="text-primary">SaaS</span> in the blink of
|
Build your <span className="text-primary">SaaS</span> in the blink of an eye!
|
||||||
an eye!
|
</h2>
|
||||||
</h2>
|
<p>
|
||||||
<p>
|
SupaNexTail got your back, and takes care of the initial setup, sometimes time consuming,
|
||||||
SupaNexTail got your back, and takes care of the initial setup,
|
but essential to your success.
|
||||||
sometimes time consuming, but essential to your success.
|
</p>
|
||||||
</p>
|
</div>
|
||||||
</div>
|
<div className="max-w-xl">
|
||||||
<div className="max-w-xl">
|
<Image src={landTop} height={417} width={583} />
|
||||||
<Image src={landTop} height={417} width={583} />
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<CardsLanding />
|
<CardsLanding />
|
||||||
<div className="flex max-w-6xl m-auto justify-around mt-14 flex-wrap">
|
<div className="flex max-w-6xl m-auto justify-around mt-14 flex-wrap">
|
||||||
<div className="max-w-sm mr-16 my-auto">
|
<div className="max-w-sm mr-16 my-auto">
|
||||||
<h2 className="text-4xl font-bold font-title text-left leading-normal">
|
<h2 className="text-4xl font-bold font-title text-left leading-normal">
|
||||||
All you need to start <span className="text-primary">now</span>
|
All you need to start <span className="text-primary">now</span>
|
||||||
</h2>
|
</h2>
|
||||||
<p>
|
<p>
|
||||||
SupaNexTail got your back, and takes care of the initial setup,
|
SupaNexTail got your back, and takes care of the initial setup, sometimes time consuming,
|
||||||
sometimes time consuming, but essential to your success.
|
but essential to your success.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="max-w-xl">
|
<div className="max-w-xl">
|
||||||
<Image src={start} />
|
<Image src={start} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex max-w-6xl m-auto justify-around mt-24 flex-wrap">
|
<div className="flex max-w-6xl m-auto justify-around mt-24 flex-wrap">
|
||||||
<div className="max-w-md my-auto order-1 lg:order-2">
|
<div className="max-w-md my-auto order-1 lg:order-2">
|
||||||
<h2 className="text-4xl font-bold font-title text-left leading-normal">
|
<h2 className="text-4xl font-bold font-title text-left leading-normal">
|
||||||
Leverage the power of <span className="text-primary">Supabase</span>
|
Leverage the power of <span className="text-primary">Supabase</span>
|
||||||
</h2>
|
</h2>
|
||||||
<p>
|
<p>
|
||||||
Supabase is an open source Firebase alternative. You’ll have a
|
Supabase is an open source Firebase alternative. You’ll have a database, an auth system, a
|
||||||
database, an auth system, a storage system, and much more in one
|
storage system, and much more in one product.
|
||||||
product.
|
</p>
|
||||||
</p>
|
<p>
|
||||||
<p>
|
SupaNexTail uses Supabase at its core, and preconfigures all the useful elements for your
|
||||||
SupaNexTail uses Supabase at its core, and preconfigures all the
|
site. User registration, synchronization with Stripe, we’ve got you covered!
|
||||||
useful elements for your site. User registration, synchronization with
|
</p>
|
||||||
Stripe, we’ve got you covered!
|
</div>
|
||||||
</p>
|
<div className="max-w-md order-2 lg:order-1 flex">
|
||||||
</div>
|
<Image src={supabaseImage} />
|
||||||
<div className="max-w-md order-2 lg:order-1 flex">
|
</div>
|
||||||
<Image src={supabaseImage} />
|
</div>
|
||||||
</div>
|
<MailingList />
|
||||||
</div>
|
</div>
|
||||||
<MailingList />
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
export default Landing;
|
export default Landing;
|
||||||
|
|||||||
@@ -19,57 +19,43 @@ import Nav from './Nav';
|
|||||||
import Footer from './Footer';
|
import Footer from './Footer';
|
||||||
|
|
||||||
const Layout = (props) => {
|
const Layout = (props) => {
|
||||||
const { user, signOut } = useAuth();
|
const { user, signOut } = useAuth();
|
||||||
|
|
||||||
const toastStyle = {
|
const toastStyle = {
|
||||||
// Style your toast elements here
|
// Style your toast elements here
|
||||||
success: 'bg-accent',
|
success: 'bg-accent',
|
||||||
error: 'bg-red-600',
|
error: 'bg-red-600',
|
||||||
info: 'bg-gray-600',
|
info: 'bg-gray-600',
|
||||||
warning: 'bg-orange-400',
|
warning: 'bg-orange-400',
|
||||||
default: 'bg-primary',
|
default: 'bg-primary',
|
||||||
dark: 'bg-white-600 font-gray-300',
|
dark: 'bg-white-600 font-gray-300',
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen w-full bg-base-100 text-base-content m-auto font-body">
|
<div className="min-h-screen w-full bg-base-100 text-base-content m-auto font-body">
|
||||||
<Head>
|
<Head>
|
||||||
<link
|
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||||
rel="apple-touch-icon"
|
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
|
||||||
sizes="180x180"
|
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
|
||||||
href="/apple-touch-icon.png"
|
<link rel="manifest" href="/site.webmanifest" />
|
||||||
/>
|
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5" />
|
||||||
<link
|
<meta name="msapplication-TileColor" content="#da532c" />
|
||||||
rel="icon"
|
<meta name="theme-color" content="#ffffff" />
|
||||||
type="image/png"
|
</Head>
|
||||||
sizes="32x32"
|
<div className="max-w-7xl flex flex-col min-h-screen mx-auto p-5">
|
||||||
href="/favicon-32x32.png"
|
<Nav user={user} signOut={signOut} />
|
||||||
/>
|
<main className="flex-1">{props.children}</main>
|
||||||
<link
|
<ToastContainer
|
||||||
rel="icon"
|
position="bottom-center"
|
||||||
type="image/png"
|
toastClassName={({ type }) =>
|
||||||
sizes="16x16"
|
`${
|
||||||
href="/favicon-16x16.png"
|
toastStyle[type || 'default']
|
||||||
/>
|
} flex p-5 my-5 min-h-10 rounded-md justify-between overflow-hidden cursor-pointer `
|
||||||
<link rel="manifest" href="/site.webmanifest" />
|
}
|
||||||
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5" />
|
/>
|
||||||
<meta name="msapplication-TileColor" content="#da532c" />
|
<Footer />
|
||||||
<meta name="theme-color" content="#ffffff" />
|
</div>
|
||||||
</Head>
|
</div>
|
||||||
<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>
|
|
||||||
<ToastContainer
|
|
||||||
position="bottom-center"
|
|
||||||
toastClassName={({ type }) =>
|
|
||||||
`${
|
|
||||||
toastStyle[type || 'default']
|
|
||||||
} flex p-5 my-5 min-h-10 rounded-md justify-between overflow-hidden cursor-pointer `
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Footer />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Layout;
|
export default Layout;
|
||||||
|
|||||||
@@ -10,77 +10,73 @@ import { toast } from 'react-toastify';
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
const MailingList = () => {
|
const MailingList = () => {
|
||||||
const [mail, setMail] = useState(null);
|
const [mail, setMail] = useState(null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [valid, setValid] = useState(true);
|
const [valid, setValid] = useState(true);
|
||||||
|
|
||||||
const validateEmail = () => {
|
const validateEmail = () => {
|
||||||
// Regex patern for email validation
|
// Regex patern for email validation
|
||||||
const regex =
|
const regex =
|
||||||
/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
|
/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
|
||||||
|
|
||||||
if (regex.test(mail)) {
|
if (regex.test(mail)) {
|
||||||
// this is a valid email address
|
// this is a valid email address
|
||||||
subscribe();
|
subscribe();
|
||||||
setValid(true);
|
setValid(true);
|
||||||
} else {
|
} else {
|
||||||
// invalid email.
|
// invalid email.
|
||||||
toast.error('Your email is invalid');
|
toast.error('Your email is invalid');
|
||||||
setValid(false);
|
setValid(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const subscribe = () => {
|
const subscribe = () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
axios
|
axios
|
||||||
.put('api/mailingList', {
|
.put('api/mailingList', {
|
||||||
mail,
|
mail,
|
||||||
})
|
})
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
if (result.status === 200) {
|
if (result.status === 200) {
|
||||||
toast.success(result.data.message);
|
toast.success(result.data.message);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.log(err);
|
console.log(err);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<div className="my-10 mt-24 m-auto flex flex-col">
|
<div className="my-10 mt-24 m-auto flex flex-col">
|
||||||
<h2 className="text-3xl md:text-4xl font-bold font-title uppercase text-center">
|
<h2 className="text-3xl md:text-4xl font-bold font-title uppercase text-center">
|
||||||
Stay Tuned
|
Stay Tuned
|
||||||
</h2>
|
</h2>
|
||||||
<Image src={Mailing} />
|
<Image src={Mailing} />
|
||||||
<label className="label">
|
<label className="label">
|
||||||
<p className="text-center max-w-md m-auto">
|
<p className="text-center max-w-md m-auto">
|
||||||
Want to be the first to know when SupaNexTail launches and get an
|
Want to be the first to know when SupaNexTail launches and get an exclusive discount? Sign
|
||||||
exclusive discount? Sign up for the newsletter!
|
up for the newsletter!
|
||||||
</p>
|
</p>
|
||||||
</label>
|
</label>
|
||||||
<div className="mt-5 m-auto">
|
<div className="mt-5 m-auto">
|
||||||
<input
|
<input
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setMail(e.target.value);
|
setMail(e.target.value);
|
||||||
}}
|
}}
|
||||||
type="email"
|
type="email"
|
||||||
placeholder="Your email"
|
placeholder="Your email"
|
||||||
className={`input input-primary input-bordered ${
|
className={`input input-primary input-bordered ${valid ? null : 'input-error'}`}
|
||||||
valid ? null : 'input-error'
|
/>
|
||||||
}`}
|
<button
|
||||||
/>
|
onClick={validateEmail}
|
||||||
<button
|
className={`btn ml-3 ${loading ? 'btn-disabled loading' : 'btn-primary'}`}
|
||||||
onClick={validateEmail}
|
>
|
||||||
className={`btn ml-3 ${
|
I'm in!
|
||||||
loading ? 'btn-disabled loading' : 'btn-primary'
|
</button>
|
||||||
}`}
|
</div>
|
||||||
>
|
</div>
|
||||||
I'm in!
|
);
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default MailingList;
|
export default MailingList;
|
||||||
|
|||||||
@@ -1,74 +0,0 @@
|
|||||||
/*
|
|
||||||
This is your Nav component. It contain a responsive navbar
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { LogOut, Menu } from 'react-feather';
|
|
||||||
|
|
||||||
import Image from 'next/image';
|
|
||||||
import Link from 'next/link';
|
|
||||||
import Logo from 'public/logo.svg';
|
|
||||||
|
|
||||||
const Nav = (props) => {
|
|
||||||
// Modify you menu directly here
|
|
||||||
const NavMenu = (
|
|
||||||
<>
|
|
||||||
{props.user && (
|
|
||||||
<Link href="/dashboard">
|
|
||||||
<a className="nav-btn">Dashboard</a>
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Link href="/pricing">
|
|
||||||
<a className="nav-btn">Pricing</a>
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
<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" />
|
|
||||||
Logout
|
|
||||||
</button>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<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">
|
|
||||||
Sign Up
|
|
||||||
</a>
|
|
||||||
</Link>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<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">
|
|
||||||
{NavMenu}
|
|
||||||
</div>
|
|
||||||
<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">
|
|
||||||
{NavMenu}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Nav;
|
|
||||||
70
components/Nav.tsx
Normal file
70
components/Nav.tsx
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
/*
|
||||||
|
This is your Nav component. It contain a responsive navbar
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { LogOut, Menu } from 'react-feather';
|
||||||
|
|
||||||
|
import Image from 'next/image';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import Logo from 'public/logo.svg';
|
||||||
|
|
||||||
|
const Nav = (props: any) => {
|
||||||
|
// Modify you menu directly here
|
||||||
|
const NavMenu = (
|
||||||
|
<>
|
||||||
|
{props.user && (
|
||||||
|
<Link href="/dashboard">
|
||||||
|
<a className="nav-btn">Dashboard</a>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Link href="/pricing">
|
||||||
|
<a className="nav-btn">Pricing</a>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<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" />
|
||||||
|
Logout
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<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">Sign Up</a>
|
||||||
|
</Link>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<nav className="navbar mb-2 w-full">
|
||||||
|
<Link href="/">
|
||||||
|
<a>
|
||||||
|
<Image src={Logo} alt="SupaNexTail 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">
|
||||||
|
{NavMenu}
|
||||||
|
</div>
|
||||||
|
<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">{NavMenu}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Nav;
|
||||||
@@ -3,77 +3,64 @@ import { Dialog, Transition } from '@headlessui/react';
|
|||||||
import { Fragment } from 'react';
|
import { Fragment } from 'react';
|
||||||
|
|
||||||
const PaymentModal = (props) => {
|
const PaymentModal = (props) => {
|
||||||
function closeModal() {
|
function closeModal() {
|
||||||
props.setPayment(false);
|
props.setPayment(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Transition appear show={props.open} as={Fragment}>
|
<Transition appear show={props.open} as={Fragment}>
|
||||||
<Dialog
|
<Dialog
|
||||||
as="div"
|
as="div"
|
||||||
className="fixed inset-0 z-10 overflow-y-auto bg-gray-500 bg-opacity-50"
|
className="fixed inset-0 z-10 overflow-y-auto bg-gray-500 bg-opacity-50"
|
||||||
onClose={closeModal}
|
onClose={closeModal}
|
||||||
>
|
>
|
||||||
<div className="min-h-screen px-4 text-center">
|
<div className="min-h-screen px-4 text-center">
|
||||||
<Transition.Child
|
<Transition.Child
|
||||||
as={Fragment}
|
as={Fragment}
|
||||||
enter="ease-out duration-300"
|
enter="ease-out duration-300"
|
||||||
enterFrom="opacity-0"
|
enterFrom="opacity-0"
|
||||||
enterTo="opacity-100"
|
enterTo="opacity-100"
|
||||||
leave="ease-in duration-200"
|
leave="ease-in duration-200"
|
||||||
leaveFrom="opacity-100"
|
leaveFrom="opacity-100"
|
||||||
leaveTo="opacity-0"
|
leaveTo="opacity-0"
|
||||||
>
|
>
|
||||||
<Dialog.Overlay className="fixed inset-0" />
|
<Dialog.Overlay className="fixed inset-0" />
|
||||||
</Transition.Child>
|
</Transition.Child>
|
||||||
|
|
||||||
{/* This element is to trick the browser into centering the modal contents. */}
|
{/* This element is to trick the browser into centering the modal contents. */}
|
||||||
<span
|
<span className="inline-block h-screen align-middle" aria-hidden="true">
|
||||||
className="inline-block h-screen align-middle"
|
​
|
||||||
aria-hidden="true"
|
</span>
|
||||||
>
|
<Transition.Child
|
||||||
​
|
as={Fragment}
|
||||||
</span>
|
enter="ease-out duration-300"
|
||||||
<Transition.Child
|
enterFrom="opacity-0 scale-95"
|
||||||
as={Fragment}
|
enterTo="opacity-100 scale-100"
|
||||||
enter="ease-out duration-300"
|
leave="ease-in duration-200"
|
||||||
enterFrom="opacity-0 scale-95"
|
leaveFrom="opacity-100 scale-100"
|
||||||
enterTo="opacity-100 scale-100"
|
leaveTo="opacity-0 scale-95"
|
||||||
leave="ease-in duration-200"
|
>
|
||||||
leaveFrom="opacity-100 scale-100"
|
<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">
|
||||||
leaveTo="opacity-0 scale-95"
|
<Dialog.Title as="h3" className="text-2xl font-bold leading-6 mb-5 text-center">
|
||||||
>
|
Payment successful 🎉
|
||||||
<div className="inline-block w-full max-w-lg p-8 my-8 overflow-hidden text-left align-middle transition-all transform shadow-xl rounded-2xl bg-base-100 text-base-content border-2 border-accent-focus">
|
</Dialog.Title>
|
||||||
<Dialog.Title
|
<div className="mt-2">
|
||||||
as="h3"
|
<p>Your payment has been successfully submitted. Thank you for your support!</p>
|
||||||
className="text-2xl font-bold leading-6 mb-5 text-center"
|
</div>
|
||||||
>
|
|
||||||
Payment successful 🎉
|
|
||||||
</Dialog.Title>
|
|
||||||
<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
|
<button type="button" className="btn btn-accent flex m-auto" onClick={closeModal}>
|
||||||
type="button"
|
Got it, thanks!
|
||||||
className="btn btn-accent flex m-auto"
|
</button>
|
||||||
onClick={closeModal}
|
</div>
|
||||||
>
|
</div>
|
||||||
Got it, thanks!
|
</Transition.Child>
|
||||||
</button>
|
</div>
|
||||||
</div>
|
</Dialog>
|
||||||
</div>
|
</Transition>
|
||||||
</Transition.Child>
|
</>
|
||||||
</div>
|
);
|
||||||
</Dialog>
|
|
||||||
</Transition>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default PaymentModal;
|
export default PaymentModal;
|
||||||
|
|||||||
@@ -16,264 +16,237 @@ import router from 'next/router';
|
|||||||
import { useAuth } from 'utils/AuthContext';
|
import { useAuth } from 'utils/AuthContext';
|
||||||
|
|
||||||
const Pricing = () => {
|
const Pricing = () => {
|
||||||
const [enabled, setEnabled] = useState(false);
|
const [enabled, setEnabled] = useState(false);
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const [customerId, setCustomerId] = useState(null);
|
const [customerId, setCustomerId] = useState(null);
|
||||||
const [sub, setSub] = useState(false);
|
const [sub, setSub] = useState(false);
|
||||||
const flat = false; // Switch between subscription system or flat prices
|
const flat = false; // Switch between subscription system or flat prices
|
||||||
|
|
||||||
const portal = () => {
|
const portal = () => {
|
||||||
axios
|
axios
|
||||||
.post('/api/stripe/customer-portal', {
|
.post('/api/stripe/customer-portal', {
|
||||||
customerId,
|
customerId,
|
||||||
})
|
})
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
router.push(result.data.url);
|
router.push(result.data.url);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (user) {
|
if (user) {
|
||||||
getSub().then((result) => setSub(result));
|
getSub().then((result) => setSub(result));
|
||||||
supabase
|
supabase
|
||||||
.from('subscriptions')
|
.from('subscriptions')
|
||||||
.select(`customer_id`)
|
.select(`customer_id`)
|
||||||
.eq('id', user.id)
|
.eq('id', user.id)
|
||||||
.single()
|
.single()
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
setCustomerId(result.data?.customer_id);
|
setCustomerId(result.data?.customer_id);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [user]);
|
}, [user]);
|
||||||
|
|
||||||
const pricing = {
|
const pricing = {
|
||||||
monthly: {
|
monthly: {
|
||||||
personal: '$5/mo',
|
personal: '$5/mo',
|
||||||
team: '$15/mo',
|
team: '$15/mo',
|
||||||
pro: '$35/mo',
|
pro: '$35/mo',
|
||||||
},
|
},
|
||||||
yearly: {
|
yearly: {
|
||||||
personal: '$50/yr',
|
personal: '$50/yr',
|
||||||
team: '$150/yr',
|
team: '$150/yr',
|
||||||
pro: '$350/yr',
|
pro: '$350/yr',
|
||||||
},
|
},
|
||||||
flat: {
|
flat: {
|
||||||
personal: '€49',
|
personal: '€49',
|
||||||
team: '€99',
|
team: '€99',
|
||||||
pro: '€149',
|
pro: '€149',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async (e, priceId) => {
|
const handleSubmit = async (e, priceId) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
// Create a Checkout Session. This will redirect the user to the Stripe website for the payment.
|
// Create a Checkout Session. This will redirect the user to the Stripe website for the payment.
|
||||||
axios
|
axios
|
||||||
.post('/api/stripe/create-checkout-session', {
|
.post('/api/stripe/create-checkout-session', {
|
||||||
priceId,
|
priceId,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
customerId,
|
customerId,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
tokenId: session.access_token,
|
tokenId: session.access_token,
|
||||||
pay_mode: flat ? 'payment' : 'subscription',
|
pay_mode: flat ? 'payment' : 'subscription',
|
||||||
})
|
})
|
||||||
.then((result) => router.push(result.data.url));
|
.then((result) => router.push(result.data.url));
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<div className="w-full mx-auto px-5 py-10 mb-10">
|
<div className="w-full mx-auto px-5 py-10 mb-10">
|
||||||
<div className="text-center max-w-xl mx-auto">
|
<div className="text-center max-w-xl mx-auto">
|
||||||
<h1 className="text-3xl sm:text-5xl font-bold font-title mb-5">
|
<h1 className="text-3xl sm:text-5xl font-bold font-title mb-5">Pricing</h1>
|
||||||
Pricing
|
<h3 className="text-lg font-light leading-8 p-3 mb-5">
|
||||||
</h1>
|
This is an example of a pricing page. You can choose a payment method, monthly or yearly.
|
||||||
<h3 className="text-lg font-light leading-8 p-3 mb-5">
|
</h3>
|
||||||
This is an example of a pricing page. You can choose a payment method,
|
</div>
|
||||||
monthly or yearly.
|
{!flat && (
|
||||||
</h3>
|
<div className="flex justify-between max-w-xs m-auto mb-3">
|
||||||
</div>
|
<div>
|
||||||
{!flat && (
|
<p className={`${enabled ? 'text-gray-500' : null}`}>Billed monthly</p>
|
||||||
<div className="flex justify-between max-w-xs m-auto mb-3">
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className={`${enabled ? 'text-gray-500' : null}`}>
|
<Switch
|
||||||
Billed monthly
|
checked={enabled}
|
||||||
</p>
|
onChange={setEnabled}
|
||||||
</div>
|
className={`bg-primary relative inline-flex flex-shrink-0 h-[38px] w-[74px]
|
||||||
<div>
|
|
||||||
<Switch
|
|
||||||
checked={enabled}
|
|
||||||
onChange={setEnabled}
|
|
||||||
className={`bg-primary relative inline-flex flex-shrink-0 h-[38px] w-[74px]
|
|
||||||
border-2 border-transparent rounded-full cursor-pointer transition-colors
|
border-2 border-transparent rounded-full cursor-pointer transition-colors
|
||||||
ease-in-out duration-200 focus:outline-none focus-visible:ring-2
|
ease-in-out duration-200 focus:outline-none focus-visible:ring-2
|
||||||
focus-visible:ring-white focus-visible:ring-opacity-75`}
|
focus-visible:ring-white focus-visible:ring-opacity-75`}
|
||||||
>
|
>
|
||||||
<span className="sr-only">Switch bill</span>
|
<span className="sr-only">Switch bill</span>
|
||||||
<span
|
<span
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
className={`${enabled ? 'translate-x-9' : 'translate-x-0'}
|
className={`${enabled ? 'translate-x-9' : 'translate-x-0'}
|
||||||
pointer-events-none inline-block h-[34px] w-[34px] rounded-full bg-white shadow-lg transform ring-0 transition ease-in-out duration-200`}
|
pointer-events-none inline-block h-[34px] w-[34px] rounded-full bg-white shadow-lg transform ring-0 transition ease-in-out duration-200`}
|
||||||
/>
|
/>
|
||||||
</Switch>
|
</Switch>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className={`${!enabled ? 'text-gray-500' : null}`}>
|
<p className={`${!enabled ? 'text-gray-500' : null}`}>Billed annually</p>
|
||||||
Billed annually
|
</div>
|
||||||
</p>
|
</div>
|
||||||
</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="max-w-4xl mx-auto md:flex space-x-4">
|
<div className="w-full flex-grow">
|
||||||
<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">
|
<h2 className="text-center font-bold uppercase mb-4">Personal</h2>
|
||||||
<div className="w-full flex-grow">
|
<h3 className="text-center font-bold text-4xl mb-5">
|
||||||
<h2 className="text-center font-bold uppercase mb-4">Personal</h2>
|
{flat
|
||||||
<h3 className="text-center font-bold text-4xl mb-5">
|
? pricing.flat.personal
|
||||||
{flat
|
: enabled
|
||||||
? pricing.flat.personal
|
? pricing.yearly.personal
|
||||||
: enabled
|
: pricing.monthly.personal}
|
||||||
? pricing.yearly.personal
|
</h3>
|
||||||
: pricing.monthly.personal}
|
<ul className="text-sm px-5 mb-8 text-left">
|
||||||
</h3>
|
<li className="leading-tight">
|
||||||
<ul className="text-sm px-5 mb-8 text-left">
|
<i className="mdi mdi-check-bold text-lg" /> A cool feature
|
||||||
<li className="leading-tight">
|
</li>
|
||||||
<i className="mdi mdi-check-bold text-lg" /> A cool feature
|
<li className="leading-tight">
|
||||||
</li>
|
<i className="mdi mdi-check-bold text-lg" /> Another feature
|
||||||
<li className="leading-tight">
|
</li>
|
||||||
<i className="mdi mdi-check-bold text-lg" /> Another feature
|
</ul>
|
||||||
</li>
|
</div>
|
||||||
</ul>
|
<div className="w-full">
|
||||||
</div>
|
<button
|
||||||
<div className="w-full">
|
className="btn btn-primary w-full"
|
||||||
<button
|
onClick={
|
||||||
className="btn btn-primary w-full"
|
user
|
||||||
onClick={
|
? sub
|
||||||
user
|
? () => {
|
||||||
? sub
|
portal();
|
||||||
? () => {
|
}
|
||||||
portal();
|
: (e) =>
|
||||||
}
|
handleSubmit(
|
||||||
: (e) =>
|
e,
|
||||||
handleSubmit(
|
enabled ? Prices.personal.annually.id : Prices.personal.monthly.id
|
||||||
e,
|
)
|
||||||
enabled
|
: () => {
|
||||||
? Prices.personal.annually.id
|
router.push('/auth');
|
||||||
: Prices.personal.monthly.id
|
}
|
||||||
)
|
}
|
||||||
: () => {
|
>
|
||||||
router.push('/auth');
|
{user ? (sub ? 'Upgrade' : 'Buy Now') : 'Register'}
|
||||||
}
|
</button>
|
||||||
}
|
</div>
|
||||||
>
|
</div>
|
||||||
{user ? (sub ? 'Upgrade' : 'Buy Now') : 'Register'}
|
<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">
|
||||||
</button>
|
<div className="w-full flex-grow">
|
||||||
</div>
|
<h2 className="text-center font-bold uppercase mb-4">Team</h2>
|
||||||
</div>
|
<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">
|
{flat ? pricing.flat.team : enabled ? pricing.yearly.team : pricing.monthly.team}
|
||||||
<div className="w-full flex-grow">
|
</h3>
|
||||||
<h2 className="text-center font-bold uppercase mb-4">Team</h2>
|
<ul className="text-sm px-5 mb-8 text-left">
|
||||||
<h3 className="text-center font-bold text-4xl mb-5">
|
<li className="leading-tight">
|
||||||
{flat
|
<i className="mdi mdi-check-bold text-lg" /> All basic features
|
||||||
? pricing.flat.team
|
</li>
|
||||||
: enabled
|
<li className="leading-tight">
|
||||||
? pricing.yearly.team
|
<i className="mdi mdi-check-bold text-lg" /> Dolor sit amet
|
||||||
: pricing.monthly.team}
|
</li>
|
||||||
</h3>
|
<li className="leading-tight">
|
||||||
<ul className="text-sm px-5 mb-8 text-left">
|
<i className="mdi mdi-check-bold text-lg" /> Consectetur
|
||||||
<li className="leading-tight">
|
</li>
|
||||||
<i className="mdi mdi-check-bold text-lg" /> All basic features
|
<li className="leading-tight">
|
||||||
</li>
|
<i className="mdi mdi-check-bold text-lg" /> Adipisicing
|
||||||
<li className="leading-tight">
|
</li>
|
||||||
<i className="mdi mdi-check-bold text-lg" /> Dolor sit amet
|
<li className="leading-tight">
|
||||||
</li>
|
<i className="mdi mdi-check-bold text-lg" /> Elit repellat
|
||||||
<li className="leading-tight">
|
</li>
|
||||||
<i className="mdi mdi-check-bold text-lg" /> Consectetur
|
</ul>
|
||||||
</li>
|
</div>
|
||||||
<li className="leading-tight">
|
<div className="w-full">
|
||||||
<i className="mdi mdi-check-bold text-lg" /> Adipisicing
|
<button
|
||||||
</li>
|
className="btn btn-primary w-full"
|
||||||
<li className="leading-tight">
|
onClick={
|
||||||
<i className="mdi mdi-check-bold text-lg" /> Elit repellat
|
user
|
||||||
</li>
|
? sub
|
||||||
</ul>
|
? () => {
|
||||||
</div>
|
portal();
|
||||||
<div className="w-full">
|
}
|
||||||
<button
|
: (e) =>
|
||||||
className="btn btn-primary w-full"
|
handleSubmit(e, enabled ? Prices.team.annually.id : Prices.team.monthly.id)
|
||||||
onClick={
|
: () => {
|
||||||
user
|
router.push('/auth');
|
||||||
? sub
|
}
|
||||||
? () => {
|
}
|
||||||
portal();
|
>
|
||||||
}
|
{user ? (sub ? 'Upgrade' : 'Buy Now') : 'Register'}
|
||||||
: (e) =>
|
</button>
|
||||||
handleSubmit(
|
</div>
|
||||||
e,
|
</div>
|
||||||
enabled
|
<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">
|
||||||
? Prices.team.annually.id
|
<div className="w-full flex-grow">
|
||||||
: Prices.team.monthly.id
|
<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}
|
||||||
router.push('/auth');
|
</h3>
|
||||||
}
|
<ul className="text-sm px-5 mb-8 text-left">
|
||||||
}
|
<li className="leading-tight">
|
||||||
>
|
<i className="mdi mdi-check-bold text-lg" /> Lorem ipsum
|
||||||
{user ? (sub ? 'Upgrade' : 'Buy Now') : 'Register'}
|
</li>
|
||||||
</button>
|
<li className="leading-tight">
|
||||||
</div>
|
<i className="mdi mdi-check-bold text-lg" /> Dolor sit amet
|
||||||
</div>
|
</li>
|
||||||
<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">
|
<li className="leading-tight">
|
||||||
<div className="w-full flex-grow">
|
<i className="mdi mdi-check-bold text-lg" /> Consectetur
|
||||||
<h2 className="text-center font-bold uppercase mb-4">Pro</h2>
|
</li>
|
||||||
<h3 className="text-center font-bold text-4xl mb-5">
|
<li className="leading-tight">
|
||||||
{flat
|
<i className="mdi mdi-check-bold text-lg" /> Adipisicing
|
||||||
? pricing.flat.pro
|
</li>
|
||||||
: enabled
|
<li className="leading-tight">
|
||||||
? pricing.yearly.pro
|
<i className="mdi mdi-check-bold text-lg" /> Much more...
|
||||||
: pricing.monthly.pro}
|
</li>
|
||||||
</h3>
|
</ul>
|
||||||
<ul className="text-sm px-5 mb-8 text-left">
|
</div>
|
||||||
<li className="leading-tight">
|
<div className="w-full">
|
||||||
<i className="mdi mdi-check-bold text-lg" /> Lorem ipsum
|
<button
|
||||||
</li>
|
className="btn btn-primary w-full"
|
||||||
<li className="leading-tight">
|
onClick={
|
||||||
<i className="mdi mdi-check-bold text-lg" /> Dolor sit amet
|
user
|
||||||
</li>
|
? sub
|
||||||
<li className="leading-tight">
|
? () => {
|
||||||
<i className="mdi mdi-check-bold text-lg" /> Consectetur
|
portal();
|
||||||
</li>
|
}
|
||||||
<li className="leading-tight">
|
: (e) =>
|
||||||
<i className="mdi mdi-check-bold text-lg" /> Adipisicing
|
handleSubmit(e, enabled ? Prices.pro.annually.id : Prices.pro.monthly.id)
|
||||||
</li>
|
: () => {
|
||||||
<li className="leading-tight">
|
router.push('/auth');
|
||||||
<i className="mdi mdi-check-bold text-lg" /> Much more...
|
}
|
||||||
</li>
|
}
|
||||||
</ul>
|
>
|
||||||
</div>
|
{user ? (sub ? 'Upgrade' : 'Buy Now') : 'Register'}
|
||||||
<div className="w-full">
|
</button>
|
||||||
<button
|
</div>
|
||||||
className="btn btn-primary w-full"
|
</div>
|
||||||
onClick={
|
</div>
|
||||||
user
|
</div>
|
||||||
? sub
|
);
|
||||||
? () => {
|
|
||||||
portal();
|
|
||||||
}
|
|
||||||
: (e) =>
|
|
||||||
handleSubmit(
|
|
||||||
e,
|
|
||||||
enabled
|
|
||||||
? Prices.pro.annually.id
|
|
||||||
: Prices.pro.monthly.id
|
|
||||||
)
|
|
||||||
: () => {
|
|
||||||
router.push('/auth');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{user ? (sub ? 'Upgrade' : 'Buy Now') : 'Register'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Pricing;
|
export default Pricing;
|
||||||
|
|||||||
@@ -1,159 +1,135 @@
|
|||||||
const PrivacyPolicy = () => (
|
const PrivacyPolicy = () => (
|
||||||
<div className="max-w-xl text-left m-auto py-10">
|
<div className="max-w-xl text-left m-auto py-10">
|
||||||
<h1 className="text-center">
|
<h1 className="text-center">Privacy Policy for {process.env.NEXT_PUBLIC_TITLE}</h1>
|
||||||
Privacy Policy for {process.env.NEXT_PUBLIC_TITLE}
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
At {process.env.NEXT_PUBLIC_TITLE}, accessible from
|
At {process.env.NEXT_PUBLIC_TITLE}, accessible from https://www.supanextail.dev, one of our
|
||||||
https://www.supanextail.dev, one of our main priorities is the privacy of
|
main priorities is the privacy of our visitors. This Privacy Policy document contains types of
|
||||||
our visitors. This Privacy Policy document contains types of information
|
information that is collected and recorded by
|
||||||
that is collected and recorded by
|
{process.env.NEXT_PUBLIC_TITLE} and how we use it.
|
||||||
{process.env.NEXT_PUBLIC_TITLE} and how we use it.
|
</p>
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
If you have additional questions or require more information about our
|
If you have additional questions or require more information about our Privacy Policy, do not
|
||||||
Privacy Policy, do not hesitate to contact us.
|
hesitate to contact us.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h2>General Data Protection Regulation (GDPR)</h2>
|
<h2>General Data Protection Regulation (GDPR)</h2>
|
||||||
<p>We are a Data Controller of your information.</p>
|
<p>We are a Data Controller of your information.</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
{process.env.NEXT_PUBLIC_TITLE} legal basis for collecting and using the
|
{process.env.NEXT_PUBLIC_TITLE} legal basis for collecting and using the personal information
|
||||||
personal information described in this Privacy Policy depends on the
|
described in this Privacy Policy depends on the Personal Information we collect and the
|
||||||
Personal Information we collect and the specific context in which we
|
specific context in which we collect the information:
|
||||||
collect the information:
|
</p>
|
||||||
</p>
|
<ul>
|
||||||
<ul>
|
<li>{process.env.NEXT_PUBLIC_TITLE} needs to perform a contract with you</li>
|
||||||
<li>
|
<li>You have given {process.env.NEXT_PUBLIC_TITLE} permission to do so</li>
|
||||||
{process.env.NEXT_PUBLIC_TITLE} needs to perform a contract with you
|
<li>
|
||||||
</li>
|
Processing your personal information is in {process.env.NEXT_PUBLIC_TITLE} legitimate
|
||||||
<li>
|
interests
|
||||||
You have given {process.env.NEXT_PUBLIC_TITLE} permission to do so
|
</li>
|
||||||
</li>
|
<li>{process.env.NEXT_PUBLIC_TITLE} needs to comply with the law</li>
|
||||||
<li>
|
</ul>
|
||||||
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>
|
<p>
|
||||||
{process.env.NEXT_PUBLIC_TITLE} will retain your personal information only
|
{process.env.NEXT_PUBLIC_TITLE} will retain your personal information only for as long as is
|
||||||
for as long as is necessary for the purposes set out in this Privacy
|
necessary for the purposes set out in this Privacy Policy. We will retain and use your
|
||||||
Policy. We will retain and use your information to the extent necessary to
|
information to the extent necessary to comply with our legal obligations, resolve disputes,
|
||||||
comply with our legal obligations, resolve disputes, and enforce our
|
and enforce our policies.
|
||||||
policies.
|
</p>
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
If you are a resident of the European Economic Area (EEA), you have
|
If you are a resident of the European Economic Area (EEA), you have certain data protection
|
||||||
certain data protection rights. If you wish to be informed what Personal
|
rights. If you wish to be informed what Personal Information we hold about you and if you want
|
||||||
Information we hold about you and if you want it to be removed from our
|
it to be removed from our systems, please contact us.
|
||||||
systems, please contact us.
|
</p>
|
||||||
</p>
|
<p>In certain circumstances, you have the following data protection rights:</p>
|
||||||
<p>
|
<ul>
|
||||||
In certain circumstances, you have the following data protection rights:
|
<li>The right to access, update or to delete the information we have on you.</li>
|
||||||
</p>
|
<li>The right of rectification.</li>
|
||||||
<ul>
|
<li>The right to object.</li>
|
||||||
<li>
|
<li>The right of restriction.</li>
|
||||||
The right to access, update or to delete the information we have on you.
|
<li>The right to data portability</li>
|
||||||
</li>
|
<li>The right to withdraw consent</li>
|
||||||
<li>The right of rectification.</li>
|
</ul>
|
||||||
<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>
|
<p>
|
||||||
{process.env.NEXT_PUBLIC_TITLE} follows a standard procedure of using log
|
{process.env.NEXT_PUBLIC_TITLE} follows a standard procedure of using log files. These files
|
||||||
files. These files log visitors when they visit websites. All hosting
|
log visitors when they visit websites. All hosting companies do this and a part of hosting
|
||||||
companies do this and a part of hosting services' analytics. The
|
services' analytics. The information collected by log files include internet protocol (IP)
|
||||||
information collected by log files include internet protocol (IP)
|
addresses, browser type, Internet Service Provider (ISP), date and time stamp, referring/exit
|
||||||
addresses, browser type, Internet Service Provider (ISP), date and time
|
pages, and possibly the number of clicks. These are not linked to any information that is
|
||||||
stamp, referring/exit pages, and possibly the number of clicks. These are
|
personally identifiable. The purpose of the information is for analyzing trends, administering
|
||||||
not linked to any information that is personally identifiable. The purpose
|
the site, tracking users' movement on the website, and gathering demographic information.
|
||||||
of the information is for analyzing trends, administering the site,
|
</p>
|
||||||
tracking users' movement on the website, and gathering demographic
|
|
||||||
information.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h2>Privacy Policies</h2>
|
<h2>Privacy Policies</h2>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
You may consult this list to find the Privacy Policy for each of the
|
You may consult this list to find the Privacy Policy for each of the advertising partners of{' '}
|
||||||
advertising partners of {process.env.NEXT_PUBLIC_TITLE}.
|
{process.env.NEXT_PUBLIC_TITLE}.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
Third-party ad servers or ad networks uses technologies like cookies,
|
Third-party ad servers or ad networks uses technologies like cookies, JavaScript, or Web
|
||||||
JavaScript, or Web Beacons that are used in their respective
|
Beacons that are used in their respective advertisements and links that appear on{' '}
|
||||||
advertisements and links that appear on {process.env.NEXT_PUBLIC_TITLE},
|
{process.env.NEXT_PUBLIC_TITLE}, which are sent directly to users' browser. They automatically
|
||||||
which are sent directly to users' browser. They automatically receive your
|
receive your IP address when this occurs. These technologies are used to measure the
|
||||||
IP address when this occurs. These technologies are used to measure the
|
effectiveness of their advertising campaigns and/or to personalize the advertising content
|
||||||
effectiveness of their advertising campaigns and/or to personalize the
|
that you see on websites that you visit.
|
||||||
advertising content that you see on websites that you visit.
|
</p>
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
Note that {process.env.NEXT_PUBLIC_TITLE} has no access to or control over
|
Note that {process.env.NEXT_PUBLIC_TITLE} has no access to or control over these cookies that
|
||||||
these cookies that are used by third-party advertisers.
|
are used by third-party advertisers.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h2>Third Party Privacy Policies</h2>
|
<h2>Third Party Privacy Policies</h2>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
{process.env.NEXT_PUBLIC_TITLE}'s Privacy Policy does not apply to other
|
{process.env.NEXT_PUBLIC_TITLE}'s Privacy Policy does not apply to other advertisers or
|
||||||
advertisers or websites. Thus, we are advising you to consult the
|
websites. Thus, we are advising you to consult the respective Privacy Policies of these
|
||||||
respective Privacy Policies of these third-party ad servers for more
|
third-party ad servers for more detailed information. It may include their practices and
|
||||||
detailed information. It may include their practices and instructions
|
instructions about how to opt-out of certain options.{' '}
|
||||||
about how to opt-out of certain options.{' '}
|
</p>
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
You can choose to disable cookies through your individual browser options.
|
You can choose to disable cookies through your individual browser options. To know more
|
||||||
To know more detailed information about cookie management with specific
|
detailed information about cookie management with specific web browsers, it can be found at
|
||||||
web browsers, it can be found at the browsers' respective websites.
|
the browsers' respective websites.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h2>Children's Information</h2>
|
<h2>Children's Information</h2>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
Another part of our priority is adding protection for children while using
|
Another part of our priority is adding protection for children while using the internet. We
|
||||||
the internet. We encourage parents and guardians to observe, participate
|
encourage parents and guardians to observe, participate in, and/or monitor and guide their
|
||||||
in, and/or monitor and guide their online activity.
|
online activity.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
{process.env.NEXT_PUBLIC_TITLE} does not knowingly collect any Personal
|
{process.env.NEXT_PUBLIC_TITLE} does not knowingly collect any Personal Identifiable
|
||||||
Identifiable Information from children under the age of 13. If you think
|
Information from children under the age of 13. If you think that your child provided this kind
|
||||||
that your child provided this kind of information on our website, we
|
of information on our website, we strongly encourage you to contact us immediately and we will
|
||||||
strongly encourage you to contact us immediately and we will do our best
|
do our best efforts to promptly remove such information from our records.
|
||||||
efforts to promptly remove such information from our records.
|
</p>
|
||||||
</p>
|
|
||||||
|
|
||||||
<h2>Online Privacy Policy Only</h2>
|
<h2>Online Privacy Policy Only</h2>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
Our Privacy Policy applies only to our online activities and is valid for
|
Our Privacy Policy applies only to our online activities and is valid for visitors to our
|
||||||
visitors to our website with regards to the information that they shared
|
website with regards to the information that they shared and/or collect in{' '}
|
||||||
and/or collect in {process.env.NEXT_PUBLIC_TITLE}. This policy is not
|
{process.env.NEXT_PUBLIC_TITLE}. This policy is not applicable to any information collected
|
||||||
applicable to any information collected offline or via channels other than
|
offline or via channels other than this website.
|
||||||
this website.
|
</p>
|
||||||
</p>
|
|
||||||
|
|
||||||
<h2>Consent</h2>
|
<h2>Consent</h2>
|
||||||
|
|
||||||
<p>
|
<p>By using our website, you hereby consent to our Privacy Policy and agree to its terms.</p>
|
||||||
By using our website, you hereby consent to our Privacy Policy and agree
|
</div>
|
||||||
to its terms.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
export default PrivacyPolicy;
|
export default PrivacyPolicy;
|
||||||
|
|||||||
@@ -12,31 +12,31 @@ import { useAuth } from 'utils/AuthContext';
|
|||||||
import SignUpPanel from './UI/SignUpPanel';
|
import SignUpPanel from './UI/SignUpPanel';
|
||||||
|
|
||||||
const Container = (props) => {
|
const Container = (props) => {
|
||||||
const { user, signOut } = useAuth();
|
const { user, signOut } = useAuth();
|
||||||
if (user)
|
if (user)
|
||||||
return (
|
return (
|
||||||
<div className="w-80 md:w-96 order-first lg:order-last">
|
<div className="w-80 md:w-96 order-first lg:order-last">
|
||||||
<p>Hello {user.email}! 👋 You are already logged in</p>
|
<p>Hello {user.email}! 👋 You are already logged in</p>
|
||||||
<button className="btn btn-primary" onClick={() => signOut()}>
|
<button className="btn btn-primary" onClick={() => signOut()}>
|
||||||
Sign out
|
Sign out
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
return props.children;
|
return props.children;
|
||||||
};
|
};
|
||||||
|
|
||||||
const AuthComponent = () => {
|
const AuthComponent = () => {
|
||||||
const { signUp, signIn, signOut, resetPassword } = useAuth();
|
const { signUp, signIn, signOut, resetPassword } = useAuth();
|
||||||
return (
|
return (
|
||||||
<Container supabaseClient={supabase}>
|
<Container supabaseClient={supabase}>
|
||||||
<SignUpPanel
|
<SignUpPanel
|
||||||
signUp={signUp}
|
signUp={signUp}
|
||||||
signIn={signIn}
|
signIn={signIn}
|
||||||
signOut={signOut}
|
signOut={signOut}
|
||||||
resetPassword={resetPassword}
|
resetPassword={resetPassword}
|
||||||
/>
|
/>
|
||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default AuthComponent;
|
export default AuthComponent;
|
||||||
|
|||||||
@@ -1,235 +1,201 @@
|
|||||||
const Terms = () => (
|
const Terms = () => (
|
||||||
<div className="max-w-xl text-left m-auto py-10">
|
<div className="max-w-xl text-left m-auto py-10">
|
||||||
<h1>Terms and Conditions</h1>
|
<h1>Terms and Conditions</h1>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
The following terms and conditions (collectively, these "Terms and
|
The following terms and conditions (collectively, these "Terms and Conditions") apply to your
|
||||||
Conditions") apply to your use of{' '}
|
use of <span className="website_url">https://www.supanextail.dev</span>, including any
|
||||||
<span className="website_url">https://www.supanextail.dev</span>,
|
content, functionality and services offered on or via{' '}
|
||||||
including any content, functionality and services offered on or via{' '}
|
<span className="website_url">https://www.supanextail.dev</span> (the "Website").
|
||||||
<span className="website_url">https://www.supanextail.dev</span> (the
|
</p>
|
||||||
"Website").
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
Please read the Terms and Conditions carefully before you start using{' '}
|
Please read the Terms and Conditions carefully before you start using{' '}
|
||||||
<span className="website_name">{process.env.NEXT_PUBLIC_TITLE}</span>,
|
<span className="website_name">{process.env.NEXT_PUBLIC_TITLE}</span>, because by using the
|
||||||
because by using the Website you accept and agree to be bound and abide by
|
Website you accept and agree to be bound and abide by these Terms and Conditions.
|
||||||
these Terms and Conditions.
|
</p>
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
These Terms and Conditions are effective as of{' '}
|
These Terms and Conditions are effective as of <span className="date">06/22/2021</span>. We
|
||||||
<span className="date">06/22/2021</span>. We expressly reserve the right
|
expressly reserve the right to change these Terms and Conditions from time to time without
|
||||||
to change these Terms and Conditions from time to time without notice to
|
notice to you. You acknowledge and agree that it is your responsibility to review this Website
|
||||||
you. You acknowledge and agree that it is your responsibility to review
|
and these Terms and Conditions from time to time and to familiarize yourself with any
|
||||||
this Website and these Terms and Conditions from time to time and to
|
modifications. Your continued use of this Website after such modifications will constitute
|
||||||
familiarize yourself with any modifications. Your continued use of this
|
acknowledgement of the modified Terms and Conditions and agreement to abide and be bound by
|
||||||
Website after such modifications will constitute acknowledgement of the
|
the modified Terms and Conditions.
|
||||||
modified Terms and Conditions and agreement to abide and be bound by the
|
</p>
|
||||||
modified Terms and Conditions.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h2>Conduct on Website</h2>
|
<h2>Conduct on Website</h2>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
Your use of the Website is subject to all applicable laws and regulations,
|
Your use of the Website is subject to all applicable laws and regulations, and you are solely
|
||||||
and you are solely responsible for the substance of your communications
|
responsible for the substance of your communications through the Website.
|
||||||
through the Website.
|
</p>
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
By posting information in or otherwise using any communications service,
|
By posting information in or otherwise using any communications service, chat room, message
|
||||||
chat room, message board, newsgroup, software library, or other
|
board, newsgroup, software library, or other interactive service that may be available to you
|
||||||
interactive service that may be available to you on or through this
|
on or through this Website, you agree that you will not upload, share, post, or otherwise
|
||||||
Website, you agree that you will not upload, share, post, or otherwise
|
distribute or facilitate distribution of any content — including text, communications,
|
||||||
distribute or facilitate distribution of any content — including text,
|
software, images, sounds, data, or other information — that:
|
||||||
communications, software, images, sounds, data, or other information —
|
</p>
|
||||||
that:
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
Is unlawful, threatening, abusive, harassing, defamatory, libelous,
|
Is unlawful, threatening, abusive, harassing, defamatory, libelous, deceptive, fraudulent,
|
||||||
deceptive, fraudulent, invasive of another's privacy, tortious, contains
|
invasive of another's privacy, tortious, contains explicit or graphic descriptions or
|
||||||
explicit or graphic descriptions or accounts of sexual acts (including
|
accounts of sexual acts (including but not limited to sexual language of a violent or
|
||||||
but not limited to sexual language of a violent or threatening nature
|
threatening nature directed at another individual or group of individuals), or otherwise
|
||||||
directed at another individual or group of individuals), or otherwise
|
violates our rules or policies
|
||||||
violates our rules or policies
|
</li>
|
||||||
</li>
|
<li>
|
||||||
<li>
|
Victimizes, harasses, degrades, or intimidates an individual or group of individuals on the
|
||||||
Victimizes, harasses, degrades, or intimidates an individual or group of
|
basis of religion, gender, sexual orientation, race, ethnicity, age, or disability
|
||||||
individuals on the basis of religion, gender, sexual orientation, race,
|
</li>
|
||||||
ethnicity, age, or disability
|
<li>
|
||||||
</li>
|
Infringes on any patent, trademark, trade secret, copyright, right of publicity, or other
|
||||||
<li>
|
proprietary right of any party
|
||||||
Infringes on any patent, trademark, trade secret, copyright, right of
|
</li>
|
||||||
publicity, or other proprietary right of any party
|
<li>
|
||||||
</li>
|
Constitutes unauthorized or unsolicited advertising, junk or bulk email (also known as
|
||||||
<li>
|
"spamming"), chain letters, any other form of unauthorized solicitation, or any form of
|
||||||
Constitutes unauthorized or unsolicited advertising, junk or bulk email
|
lottery or gambling
|
||||||
(also known as "spamming"), chain letters, any other form of
|
</li>
|
||||||
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
|
||||||
<li>
|
or intended to disrupt, damage, or limit the functioning of any software, hardware, or
|
||||||
Contains software viruses or any other computer code, files, or programs
|
telecommunications equipment or to damage or obtain unauthorized access to any data or other
|
||||||
that are designed or intended to disrupt, damage, or limit the
|
information of any third party
|
||||||
functioning of any software, hardware, or telecommunications equipment
|
</li>
|
||||||
or to damage or obtain unauthorized access to any data or other
|
<li>Impersonates any person or entity, including any of our employees or representatives</li>
|
||||||
information of any third party
|
</ul>
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
Impersonates any person or entity, including any of our employees or
|
|
||||||
representatives
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
We neither endorse nor assume any liability for the contents of any
|
We neither endorse nor assume any liability for the contents of any material uploaded or
|
||||||
material uploaded or submitted by third party users of the Website. We
|
submitted by third party users of the Website. We generally do not pre-screen, monitor, or
|
||||||
generally do not pre-screen, monitor, or edit the content posted by users
|
edit the content posted by users of communications services, chat rooms, message boards,
|
||||||
of communications services, chat rooms, message boards, newsgroups,
|
newsgroups, software libraries, or other interactive services that may be available on or
|
||||||
software libraries, or other interactive services that may be available on
|
through this Website.
|
||||||
or through this Website.
|
</p>
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
However, we and our agents have the right at their sole discretion to
|
However, we and our agents have the right at their sole discretion to remove any content that,
|
||||||
remove any content that, in our judgment, does not comply with these Terms
|
in our judgment, does not comply with these Terms of Use and any other rules of user conduct
|
||||||
of Use and any other rules of user conduct for our Website, or is
|
for our Website, or is otherwise harmful, objectionable, or inaccurate. We are not responsible
|
||||||
otherwise harmful, objectionable, or inaccurate. We are not responsible
|
for any failure or delay in removing such content. You hereby consent to such removal and
|
||||||
for any failure or delay in removing such content. You hereby consent to
|
waive any claim against us arising out of such removal of content.
|
||||||
such removal and waive any claim against us arising out of such removal of
|
</p>
|
||||||
content.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
You agree that we may at any time, and at our sole discretion, terminate
|
You agree that we may at any time, and at our sole discretion, terminate your membership,
|
||||||
your membership, account, or other affiliation with our site without prior
|
account, or other affiliation with our site without prior notice to you for violating any of
|
||||||
notice to you for violating any of the above provisions.
|
the above provisions.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
In addition, you acknowledge that we will cooperate fully with
|
In addition, you acknowledge that we will cooperate fully with investigations of violations of
|
||||||
investigations of violations of systems or network security at other
|
systems or network security at other sites, including cooperating with law enforcement
|
||||||
sites, including cooperating with law enforcement authorities in
|
authorities in investigating suspected criminal violations.
|
||||||
investigating suspected criminal violations.
|
</p>
|
||||||
</p>
|
|
||||||
|
|
||||||
<h2>Intellectual Property</h2>
|
<h2>Intellectual Property</h2>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
By accepting these Terms and Conditions, you acknowledge and agree that
|
By accepting these Terms and Conditions, you acknowledge and agree that all content presented
|
||||||
all content presented to you on this Website is protected by copyrights,
|
to you on this Website is protected by copyrights, trademarks, service marks, patents or other
|
||||||
trademarks, service marks, patents or other proprietary rights and laws,
|
proprietary rights and laws, and is the sole property of{' '}
|
||||||
and is the sole property of{' '}
|
<span className="website_name">{process.env.NEXT_PUBLIC_TITLE}</span>.
|
||||||
<span className="website_name">{process.env.NEXT_PUBLIC_TITLE}</span>.
|
</p>
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
You are only permitted to use the content as expressly authorized by us or
|
You are only permitted to use the content as expressly authorized by us or the specific
|
||||||
the specific content provider. Except for a single copy made for personal
|
content provider. Except for a single copy made for personal use only, you may not copy,
|
||||||
use only, you may not copy, reproduce, modify, republish, upload, post,
|
reproduce, modify, republish, upload, post, transmit, or distribute any documents or
|
||||||
transmit, or distribute any documents or information from this Website in
|
information from this Website in any form or by any means without prior written permission
|
||||||
any form or by any means without prior written permission from us or the
|
from us or the specific content provider, and you are solely responsible for obtaining
|
||||||
specific content provider, and you are solely responsible for obtaining
|
permission before reusing any copyrighted material that is available on this Website.
|
||||||
permission before reusing any copyrighted material that is available on
|
</p>
|
||||||
this Website.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h2>Third Party Websites</h2>
|
<h2>Third Party Websites</h2>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
This Website may link you to other sites on the Internet or otherwise
|
This Website may link you to other sites on the Internet or otherwise include references to
|
||||||
include references to information, documents, software, materials and/or
|
information, documents, software, materials and/or services provided by other parties. These
|
||||||
services provided by other parties. These websites may contain information
|
websites may contain information or material that some people may find inappropriate or
|
||||||
or material that some people may find inappropriate or offensive.
|
offensive.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
These other websites and parties are not under our control, and you
|
These other websites and parties are not under our control, and you acknowledge that we are
|
||||||
acknowledge that we are not responsible for the accuracy, copyright
|
not responsible for the accuracy, copyright compliance, legality, decency, or any other aspect
|
||||||
compliance, legality, decency, or any other aspect of the content of such
|
of the content of such sites, nor are we responsible for errors or omissions in any references
|
||||||
sites, nor are we responsible for errors or omissions in any references to
|
to other parties or their products and services. The inclusion of such a link or reference is
|
||||||
other parties or their products and services. The inclusion of such a link
|
provided merely as a convenience and does not imply endorsement of, or association with, the
|
||||||
or reference is provided merely as a convenience and does not imply
|
Website or party by us, or any warranty of any kind, either express or implied.
|
||||||
endorsement of, or association with, the Website or party by us, or any
|
</p>
|
||||||
warranty of any kind, either express or implied.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h2>
|
<h2>Disclaimer of Warranties, Limitations of Liability and Indemnification</h2>
|
||||||
Disclaimer of Warranties, Limitations of Liability and Indemnification
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
Your use of{' '}
|
Your use of <span className="website_name">{process.env.NEXT_PUBLIC_TITLE}</span> is at your
|
||||||
<span className="website_name">{process.env.NEXT_PUBLIC_TITLE}</span> is
|
sole risk. The Website is provided "as is" and "as available". We disclaim all warranties of
|
||||||
at your sole risk. The Website is provided "as is" and "as available". We
|
any kind, express or implied, including, without limitation, the warranties of
|
||||||
disclaim all warranties of any kind, express or implied, including,
|
merchantability, fitness for a particular purpose and non-infringement.
|
||||||
without limitation, the warranties of merchantability, fitness for a
|
</p>
|
||||||
particular purpose and non-infringement.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
We are not liable for damages, direct or consequential, resulting from
|
We are not liable for damages, direct or consequential, resulting from your use of the
|
||||||
your use of the Website, and you agree to defend, indemnify and hold us
|
Website, and you agree to defend, indemnify and hold us harmless from any claims, losses,
|
||||||
harmless from any claims, losses, liability costs and expenses (including
|
liability costs and expenses (including but not limites to attorney's fees) arising from your
|
||||||
but not limites to attorney's fees) arising from your violation of any
|
violation of any third-party's rights. You acknowledge that you have only a limited,
|
||||||
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
|
||||||
non-exclusive, nontransferable license to use the Website. Because the
|
bug free, you agree that you will use it carefully and avoid using it ways which might result
|
||||||
Website is not error or bug free, you agree that you will use it carefully
|
in any loss of your or any third party's property or information.
|
||||||
and avoid using it ways which might result in any loss of your or any
|
</p>
|
||||||
third party's property or information.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h2>Term and termination</h2>
|
<h2>Term and termination</h2>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
This Terms and Conditions will become effective in relation to you when
|
This Terms and Conditions will become effective in relation to you when you create a{' '}
|
||||||
you create a{' '}
|
<span className="website_name">{process.env.NEXT_PUBLIC_TITLE}</span> account or when you
|
||||||
<span className="website_name">{process.env.NEXT_PUBLIC_TITLE}</span>{' '}
|
start using the <span className="website_name">{process.env.NEXT_PUBLIC_TITLE}</span> and will
|
||||||
account or when you start using the{' '}
|
remain effective until terminated by you or by us.{' '}
|
||||||
<span className="website_name">{process.env.NEXT_PUBLIC_TITLE}</span> and
|
</p>
|
||||||
will remain effective until terminated by you or by us.{' '}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<span className="website_name">{process.env.NEXT_PUBLIC_TITLE}</span>{' '}
|
<span className="website_name">{process.env.NEXT_PUBLIC_TITLE}</span> reserves the right to
|
||||||
reserves the right to terminate this Terms and Conditions or suspend your
|
terminate this Terms and Conditions or suspend your account at any time in case of
|
||||||
account at any time in case of unauthorized, or suspected unauthorized use
|
unauthorized, or suspected unauthorized use of the Website whether in contravention of this
|
||||||
of the Website whether in contravention of this Terms and Conditions or
|
Terms and Conditions or otherwise. If{' '}
|
||||||
otherwise. If{' '}
|
<span className="website_name">{process.env.NEXT_PUBLIC_TITLE}</span> terminates this Terms
|
||||||
<span className="website_name">{process.env.NEXT_PUBLIC_TITLE}</span>{' '}
|
and Conditions, or suspends your account for any of the reasons set out in this section,{' '}
|
||||||
terminates this Terms and Conditions, or suspends your account for any of
|
<span className="website_name">{process.env.NEXT_PUBLIC_TITLE}</span> shall have no liability
|
||||||
the reasons set out in this section,{' '}
|
or responsibility to you.
|
||||||
<span className="website_name">{process.env.NEXT_PUBLIC_TITLE}</span>{' '}
|
</p>
|
||||||
shall have no liability or responsibility to you.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h2>Assignment</h2>
|
<h2>Assignment</h2>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<span className="website_name">{process.env.NEXT_PUBLIC_TITLE}</span> may
|
<span className="website_name">{process.env.NEXT_PUBLIC_TITLE}</span> may assign this Terms
|
||||||
assign this Terms and Conditions or any part of it without restrictions.
|
and Conditions or any part of it without restrictions. You may not assign this Terms and
|
||||||
You may not assign this Terms and Conditions or any part of it to any
|
Conditions or any part of it to any third party.
|
||||||
third party.
|
</p>
|
||||||
</p>
|
|
||||||
|
|
||||||
<h2>Governing Law</h2>
|
<h2>Governing Law</h2>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
These Terms and Conditions and any dispute or claim arising out of, or
|
These Terms and Conditions and any dispute or claim arising out of, or related to them, shall
|
||||||
related to them, shall be governed by and construed in accordance with the
|
be governed by and construed in accordance with the internal laws of the{' '}
|
||||||
internal laws of the <span className="country">fr</span> without giving
|
<span className="country">fr</span> without giving effect to any choice or conflict of law
|
||||||
effect to any choice or conflict of law provision or rule.
|
provision or rule.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
Any legal suit, action or proceeding arising out of, or related to, these
|
Any legal suit, action or proceeding arising out of, or related to, these Terms of Service or
|
||||||
Terms of Service or the Website shall be instituted exclusively in the
|
the Website shall be instituted exclusively in the federal courts of{' '}
|
||||||
federal courts of <span className="country">fr</span>.
|
<span className="country">fr</span>.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
export default Terms;
|
export default Terms;
|
||||||
|
|||||||
@@ -5,22 +5,22 @@ This card is used on the landing page
|
|||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
|
|
||||||
const CardLanding = (props) => {
|
const CardLanding = (props) => {
|
||||||
const { image, title, text } = props;
|
const { image, title, text } = props;
|
||||||
return (
|
return (
|
||||||
<div className="w-80 h-48 p-5 sm:ml-5 mb-5 bg-base-100 flex">
|
<div className="w-80 h-48 p-5 sm:ml-5 mb-5 bg-base-100 flex">
|
||||||
<div>
|
<div>
|
||||||
<div className="rounded-full w-12 h-12 border flex bg-neutral-content">
|
<div className="rounded-full w-12 h-12 border flex bg-neutral-content">
|
||||||
<div className="m-auto flex">
|
<div className="m-auto flex">
|
||||||
<Image src={image} width={24} height={24} />
|
<Image src={image} width={24} height={24} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-8">
|
<div className="ml-8">
|
||||||
<p className="font-semibold font-title text-lg">{title}</p>
|
<p className="font-semibold font-title text-lg">{title}</p>
|
||||||
<p className="mt-3">{text}</p>
|
<p className="mt-3">{text}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default CardLanding;
|
export default CardLanding;
|
||||||
|
|||||||
@@ -5,12 +5,12 @@ This card is used on the landing page
|
|||||||
import { FiStar } from 'react-icons/fi';
|
import { FiStar } from 'react-icons/fi';
|
||||||
|
|
||||||
const KeyFeature = (props) => (
|
const KeyFeature = (props) => (
|
||||||
<div className="shadow-sm p-5 mb-5 bg-base-100 flex italic">
|
<div className="shadow-sm p-5 mb-5 bg-base-100 flex italic">
|
||||||
<div className="p-2 bg-accent-focus w-12 h-12 text-white rounded-sm my-auto flex">
|
<div className="p-2 bg-accent-focus w-12 h-12 text-white rounded-sm my-auto flex">
|
||||||
<FiStar className="text-2xl m-auto" />
|
<FiStar className="text-2xl m-auto" />
|
||||||
</div>
|
</div>
|
||||||
<div className="m-auto ml-3">{props.children}</div>
|
<div className="m-auto ml-3">{props.children}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
export default KeyFeature;
|
export default KeyFeature;
|
||||||
|
|||||||
@@ -4,170 +4,163 @@ import { toast } from 'react-toastify';
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
const Login = (props) => {
|
const Login = (props) => {
|
||||||
const [email, setEmail] = useState('');
|
const [email, setEmail] = useState('');
|
||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState('');
|
||||||
const [forgot, setForgot] = useState(false);
|
const [forgot, setForgot] = useState(false);
|
||||||
|
|
||||||
const resetPassword = () => {
|
const resetPassword = () => {
|
||||||
props.resetPassword(email).then((result) => {
|
props.resetPassword(email).then((result) => {
|
||||||
if (result.error) {
|
if (result.error) {
|
||||||
toast.error(result.error.message);
|
toast.error(result.error.message);
|
||||||
} else toast.success('Check your email to reset your password!');
|
} else toast.success('Check your email to reset your password!');
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const login = (e) => {
|
const login = (e) => {
|
||||||
e.preventDefault();
|
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
|
props
|
||||||
.signIn({
|
.signIn({
|
||||||
email,
|
email,
|
||||||
password,
|
password,
|
||||||
})
|
})
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
if (result.data) {
|
if (result.data) {
|
||||||
router.push('/');
|
router.push('/');
|
||||||
}
|
}
|
||||||
if (result.error) {
|
if (result.error) {
|
||||||
toast.error(result.error.message);
|
toast.error(result.error.message);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-10 bg-base-100 md:flex-1 rounded-md text-base-content shadow-md max-w-sm font-body">
|
<div className="p-10 bg-base-100 md:flex-1 rounded-md text-base-content shadow-md max-w-sm font-body">
|
||||||
{!forgot && (
|
{!forgot && (
|
||||||
<>
|
<>
|
||||||
<h3 className="my-4 text-2xl font-semibold font-title">
|
<h3 className="my-4 text-2xl font-semibold font-title">Account Login</h3>
|
||||||
Account Login
|
<form action="#" className="flex flex-col space-y-5">
|
||||||
</h3>
|
<div className="flex flex-col space-y-1">
|
||||||
<form action="#" className="flex flex-col space-y-5">
|
<label htmlFor="email" className="text-sm">
|
||||||
<div className="flex flex-col space-y-1">
|
Email address
|
||||||
<label htmlFor="email" className="text-sm">
|
</label>
|
||||||
Email address
|
<input
|
||||||
</label>
|
type="email"
|
||||||
<input
|
id="email"
|
||||||
type="email"
|
autoFocus
|
||||||
id="email"
|
className="input input-primary input-bordered input-sm"
|
||||||
autoFocus
|
value={email}
|
||||||
className="input input-primary input-bordered input-sm"
|
onChange={(event) => {
|
||||||
value={email}
|
setEmail(event.target.value);
|
||||||
onChange={(event) => {
|
}}
|
||||||
setEmail(event.target.value);
|
/>
|
||||||
}}
|
</div>
|
||||||
/>
|
<div className="flex flex-col space-y-1">
|
||||||
</div>
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex flex-col space-y-1">
|
<label htmlFor="password" className="text-sm">
|
||||||
<div className="flex items-center justify-between">
|
Password
|
||||||
<label htmlFor="password" className="text-sm">
|
</label>
|
||||||
Password
|
<button
|
||||||
</label>
|
onClick={() => {
|
||||||
<button
|
setForgot(true);
|
||||||
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>
|
||||||
Forgot Password?
|
</div>
|
||||||
</button>
|
<input
|
||||||
</div>
|
type="password"
|
||||||
<input
|
id="password"
|
||||||
type="password"
|
className="input input-primary input-bordered input-sm"
|
||||||
id="password"
|
value={password}
|
||||||
className="input input-primary input-bordered input-sm"
|
onChange={(event) => {
|
||||||
value={password}
|
setPassword(event.target.value);
|
||||||
onChange={(event) => {
|
}}
|
||||||
setPassword(event.target.value);
|
/>
|
||||||
}}
|
</div>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<button
|
<button
|
||||||
className="btn btn-primary w-full"
|
className="btn btn-primary w-full"
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
login(event);
|
login(event);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Log in
|
Log in
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col space-y-5">
|
<div className="flex flex-col space-y-5">
|
||||||
<span className="flex items-center justify-center space-x-2">
|
<span className="flex items-center justify-center space-x-2">
|
||||||
<span className="h-px bg-gray-400 w-14" />
|
<span className="h-px bg-gray-400 w-14" />
|
||||||
<span className="font-normal text-gray-500">or login with</span>
|
<span className="font-normal text-gray-500">or login with</span>
|
||||||
<span className="h-px bg-gray-400 w-14" />
|
<span className="h-px bg-gray-400 w-14" />
|
||||||
</span>
|
</span>
|
||||||
<div className="flex flex-col space-y-4">
|
<div className="flex flex-col space-y-4">
|
||||||
<button
|
<button
|
||||||
href="#"
|
href="#"
|
||||||
className="flex items-center justify-center px-4 py-2 space-x-2 transition-colors duration-300 border border-base-200 rounded-md group hover:bg-base-300 focus:outline-none "
|
className="flex items-center justify-center px-4 py-2 space-x-2 transition-colors duration-300 border border-base-200 rounded-md group hover:bg-base-300 focus:outline-none "
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
props.signIn({ provider: 'google' });
|
props.signIn({ provider: 'google' });
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="text-base-content">
|
<div className="text-base-content">
|
||||||
<IoLogoGoogle />
|
<IoLogoGoogle />
|
||||||
</div>
|
</div>
|
||||||
<span className="text-sm font-medium text-base-content">
|
<span className="text-sm font-medium text-base-content">Gmail</span>
|
||||||
Gmail
|
</button>
|
||||||
</span>
|
</div>
|
||||||
</button>
|
</div>
|
||||||
</div>
|
</form>
|
||||||
</div>
|
</>
|
||||||
</form>
|
)}
|
||||||
</>
|
{forgot && (
|
||||||
)}
|
<>
|
||||||
{forgot && (
|
<h3 className="my-4 text-2xl font-semibold">Password recovery</h3>
|
||||||
<>
|
<form action="#" className="flex flex-col space-y-5">
|
||||||
<h3 className="my-4 text-2xl font-semibold">Password recovery</h3>
|
<div className="flex flex-col space-y-1">
|
||||||
<form action="#" className="flex flex-col space-y-5">
|
<label htmlFor="email" className="text-sm font-semibold text-gray-500">
|
||||||
<div className="flex flex-col space-y-1">
|
Email address
|
||||||
<label
|
</label>
|
||||||
htmlFor="email"
|
<input
|
||||||
className="text-sm font-semibold text-gray-500"
|
type="email"
|
||||||
>
|
id="email"
|
||||||
Email address
|
autoFocus
|
||||||
</label>
|
className="input input-primary input-bordered input-sm"
|
||||||
<input
|
value={email}
|
||||||
type="email"
|
onChange={(event) => {
|
||||||
id="email"
|
setEmail(event.target.value);
|
||||||
autoFocus
|
}}
|
||||||
className="input input-primary input-bordered input-sm"
|
/>
|
||||||
value={email}
|
</div>
|
||||||
onChange={(event) => {
|
|
||||||
setEmail(event.target.value);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<button
|
<button
|
||||||
className="btn btn-primary w-full btn-sm"
|
className="btn btn-primary w-full btn-sm"
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
resetPassword();
|
resetPassword();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Recover my password
|
Recover my password
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<hr />
|
<hr />
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setForgot(false);
|
setForgot(false);
|
||||||
}}
|
}}
|
||||||
className="text-sm text-blue-600 hover:underline focus:text-blue-800"
|
className="text-sm text-blue-600 hover:underline focus:text-blue-800"
|
||||||
>
|
>
|
||||||
Go back to sign in
|
Go back to sign in
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Login;
|
export default Login;
|
||||||
|
|||||||
@@ -4,113 +4,107 @@ import { toast } from 'react-toastify';
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
const SignUpPanel = (props) => {
|
const SignUpPanel = (props) => {
|
||||||
const [email, setEmail] = useState('');
|
const [email, setEmail] = useState('');
|
||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState('');
|
||||||
const [forgot, setForgot] = useState(false);
|
const [forgot, setForgot] = useState(false);
|
||||||
|
|
||||||
const resetPassword = () => {
|
const resetPassword = () => {
|
||||||
props.resetPassword(email).then((result) => {
|
props.resetPassword(email).then((result) => {
|
||||||
if (result.error) {
|
if (result.error) {
|
||||||
toast.error(result.error.message);
|
toast.error(result.error.message);
|
||||||
} else toast.success('Check your email to reset your password!');
|
} else toast.success('Check your email to reset your password!');
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const signup = (e) => {
|
const signup = (e) => {
|
||||||
e.preventDefault();
|
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
|
props
|
||||||
.signUp({
|
.signUp({
|
||||||
email,
|
email,
|
||||||
password,
|
password,
|
||||||
})
|
})
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
if (result.data) {
|
if (result.data) {
|
||||||
router.push('/');
|
router.push('/');
|
||||||
}
|
}
|
||||||
if (result.error) {
|
if (result.error) {
|
||||||
toast.error(result.error.message);
|
toast.error(result.error.message);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-10 bg-base-100 md:flex-1 rounded-md text-base-content shadow-md max-w-sm font-body">
|
<div className="p-10 bg-base-100 md:flex-1 rounded-md text-base-content shadow-md max-w-sm font-body">
|
||||||
{!forgot && (
|
{!forgot && (
|
||||||
<>
|
<>
|
||||||
<h3 className="my-4 text-2xl font-semibold font-title">
|
<h3 className="my-4 text-2xl font-semibold font-title">Account Sign Up</h3>
|
||||||
Account Sign Up
|
<form action="#" className="flex flex-col space-y-5">
|
||||||
</h3>
|
<div className="flex flex-col space-y-1">
|
||||||
<form action="#" className="flex flex-col space-y-5">
|
<label htmlFor="email" className="text-sm">
|
||||||
<div className="flex flex-col space-y-1">
|
Email address
|
||||||
<label htmlFor="email" className="text-sm">
|
</label>
|
||||||
Email address
|
<input
|
||||||
</label>
|
type="email"
|
||||||
<input
|
id="email"
|
||||||
type="email"
|
autoFocus
|
||||||
id="email"
|
className="input input-primary input-bordered input-sm"
|
||||||
autoFocus
|
value={email}
|
||||||
className="input input-primary input-bordered input-sm"
|
onChange={(event) => {
|
||||||
value={email}
|
setEmail(event.target.value);
|
||||||
onChange={(event) => {
|
}}
|
||||||
setEmail(event.target.value);
|
/>
|
||||||
}}
|
</div>
|
||||||
/>
|
<div className="flex flex-col space-y-1">
|
||||||
</div>
|
<input
|
||||||
<div className="flex flex-col space-y-1">
|
type="password"
|
||||||
<input
|
id="password"
|
||||||
type="password"
|
className="input input-primary input-bordered input-sm"
|
||||||
id="password"
|
value={password}
|
||||||
className="input input-primary input-bordered input-sm"
|
onChange={(event) => {
|
||||||
value={password}
|
setPassword(event.target.value);
|
||||||
onChange={(event) => {
|
}}
|
||||||
setPassword(event.target.value);
|
/>
|
||||||
}}
|
</div>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<button
|
<button
|
||||||
className="btn btn-primary w-full"
|
className="btn btn-primary w-full"
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
signup(event);
|
signup(event);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Sign Up
|
Sign Up
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col space-y-5">
|
<div className="flex flex-col space-y-5">
|
||||||
<span className="flex items-center justify-center space-x-2">
|
<span className="flex items-center justify-center space-x-2">
|
||||||
<span className="h-px bg-gray-400 w-14" />
|
<span className="h-px bg-gray-400 w-14" />
|
||||||
<span className="font-normal text-gray-500">
|
<span className="font-normal text-gray-500">or sign up with</span>
|
||||||
or sign up with
|
<span className="h-px bg-gray-400 w-14" />
|
||||||
</span>
|
</span>
|
||||||
<span className="h-px bg-gray-400 w-14" />
|
<div className="flex flex-col space-y-4">
|
||||||
</span>
|
<button
|
||||||
<div className="flex flex-col space-y-4">
|
href="#"
|
||||||
<button
|
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="#"
|
onClick={(event) => {
|
||||||
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 "
|
event.preventDefault();
|
||||||
onClick={(event) => {
|
props.signIn({ provider: 'google' });
|
||||||
event.preventDefault();
|
}}
|
||||||
props.signIn({ provider: 'google' });
|
>
|
||||||
}}
|
<div className="text-base-content">
|
||||||
>
|
<IoLogoGoogle />
|
||||||
<div className="text-base-content">
|
</div>
|
||||||
<IoLogoGoogle />
|
<span className="text-sm font-medium text-base-content">Gmail</span>
|
||||||
</div>
|
</button>
|
||||||
<span className="text-sm font-medium text-base-content">
|
</div>
|
||||||
Gmail
|
</div>
|
||||||
</span>
|
</form>
|
||||||
</button>
|
</>
|
||||||
</div>
|
)}
|
||||||
</div>
|
</div>
|
||||||
</form>
|
);
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default SignUpPanel;
|
export default SignUpPanel;
|
||||||
|
|||||||
@@ -7,28 +7,28 @@ import { HiOutlineMoon, HiOutlineSun } from 'react-icons/hi';
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
const theme = {
|
const theme = {
|
||||||
primary: 'supaTheme',
|
primary: 'supaTheme',
|
||||||
secondary: 'dark',
|
secondary: 'dark',
|
||||||
};
|
};
|
||||||
|
|
||||||
const ThemeToggle = () => {
|
const ThemeToggle = () => {
|
||||||
const [activeTheme, setActiveTheme] = useState(document.body.dataset.theme);
|
const [activeTheme, setActiveTheme] = useState(document.body.dataset.theme);
|
||||||
const inactiveTheme = activeTheme === 'supaTheme' ? 'dark' : 'supaTheme';
|
const inactiveTheme = activeTheme === 'supaTheme' ? 'dark' : 'supaTheme';
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.body.dataset.theme = activeTheme;
|
document.body.dataset.theme = activeTheme;
|
||||||
window.localStorage.setItem('theme', activeTheme);
|
window.localStorage.setItem('theme', activeTheme);
|
||||||
}, [activeTheme]);
|
}, [activeTheme]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button className="flex ml-3" onClick={() => setActiveTheme(inactiveTheme)}>
|
<button className="flex ml-3" onClick={() => setActiveTheme(inactiveTheme)}>
|
||||||
{activeTheme === theme.secondary ? (
|
{activeTheme === theme.secondary ? (
|
||||||
<HiOutlineSun className="m-auto text-xl hover:text-accent" />
|
<HiOutlineSun className="m-auto text-xl hover:text-accent" />
|
||||||
) : (
|
) : (
|
||||||
<HiOutlineMoon className="m-auto text-xl hover:text-accent" />
|
<HiOutlineMoon className="m-auto text-xl hover:text-accent" />
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ThemeToggle;
|
export default ThemeToggle;
|
||||||
|
|||||||
@@ -5,45 +5,45 @@ You can see that it will do this twice, with 2 different resolution, to test the
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
describe('Basic Test', () => {
|
describe('Basic Test', () => {
|
||||||
context('Desktop resolution', () => {
|
context('Desktop resolution', () => {
|
||||||
it('Visits the homepage and nav links (Desktop)', () => {
|
it('Visits the homepage and nav links (Desktop)', () => {
|
||||||
cy.viewport(1280, 720);
|
cy.viewport(1280, 720);
|
||||||
cy.visit('');
|
cy.visit('');
|
||||||
|
|
||||||
cy.get('nav').contains('Pricing').click();
|
cy.get('nav').contains('Pricing').click();
|
||||||
cy.url().should('include', '/pricing');
|
cy.url().should('include', '/pricing');
|
||||||
|
|
||||||
cy.get('nav').contains('Contact').click();
|
cy.get('nav').contains('Contact').click();
|
||||||
cy.url().should('include', '/contact');
|
cy.url().should('include', '/contact');
|
||||||
|
|
||||||
cy.get('nav').contains('Login').click();
|
cy.get('nav').contains('Login').click();
|
||||||
cy.url().should('include', '/login');
|
cy.url().should('include', '/login');
|
||||||
|
|
||||||
cy.get('nav').contains('Sign Up').click();
|
cy.get('nav').contains('Sign Up').click();
|
||||||
cy.url().should('include', '/signup');
|
cy.url().should('include', '/signup');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
context('Mobile resolution', () => {
|
context('Mobile resolution', () => {
|
||||||
it('Visits the homepage and nav links (Mobile)', () => {
|
it('Visits the homepage and nav links (Mobile)', () => {
|
||||||
cy.viewport(680, 720);
|
cy.viewport(680, 720);
|
||||||
cy.visit('');
|
cy.visit('');
|
||||||
|
|
||||||
cy.get('[data-cy=dropdown]').click();
|
cy.get('[data-cy=dropdown]').click();
|
||||||
cy.get('[data-cy=dropdown]').contains('Pricing').click();
|
cy.get('[data-cy=dropdown]').contains('Pricing').click();
|
||||||
cy.url().should('include', '/pricing');
|
cy.url().should('include', '/pricing');
|
||||||
|
|
||||||
cy.get('[data-cy=dropdown]').click();
|
cy.get('[data-cy=dropdown]').click();
|
||||||
cy.get('[data-cy=dropdown]').contains('Login').click();
|
cy.get('[data-cy=dropdown]').contains('Login').click();
|
||||||
cy.url().should('include', '/login');
|
cy.url().should('include', '/login');
|
||||||
|
|
||||||
cy.get('[data-cy=dropdown]').click();
|
cy.get('[data-cy=dropdown]').click();
|
||||||
cy.get('[data-cy=dropdown]').contains('Sign Up').click();
|
cy.get('[data-cy=dropdown]').contains('Sign Up').click();
|
||||||
cy.url().should('include', '/signup');
|
cy.url().should('include', '/signup');
|
||||||
|
|
||||||
cy.get('[data-cy=dropdown]').click();
|
cy.get('[data-cy=dropdown]').click();
|
||||||
cy.get('[data-cy=dropdown]').contains('Contact').click();
|
cy.get('[data-cy=dropdown]').contains('Contact').click();
|
||||||
cy.url().should('include', '/contact');
|
cy.url().should('include', '/contact');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -17,6 +17,6 @@
|
|||||||
*/
|
*/
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
module.exports = (on, config) => {
|
module.exports = (on, config) => {
|
||||||
// `on` is used to hook into various events Cypress emits
|
// `on` is used to hook into various events Cypress emits
|
||||||
// `config` is the resolved Cypress config
|
// `config` is the resolved Cypress config
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"baseUrl": "."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
3
next-env.d.ts
vendored
Normal file
3
next-env.d.ts
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
/// <reference types="next" />
|
||||||
|
/// <reference types="next/types/global" />
|
||||||
|
/// <reference types="next/image-types/global" />
|
||||||
8294
package-lock.json
generated
8294
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
10
package.json
10
package.json
@@ -16,6 +16,10 @@
|
|||||||
"@stripe/stripe-js": "^1.15.1",
|
"@stripe/stripe-js": "^1.15.1",
|
||||||
"@supabase/grid": "1.15.0",
|
"@supabase/grid": "1.15.0",
|
||||||
"@supabase/supabase-js": "^1.21.0",
|
"@supabase/supabase-js": "^1.21.0",
|
||||||
|
"@types/node": "^16.4.13",
|
||||||
|
"@types/react": "^17.0.16",
|
||||||
|
"@types/react-dom": "^17.0.9",
|
||||||
|
"@types/stripe": "^8.0.417",
|
||||||
"axios": "^0.21.1",
|
"axios": "^0.21.1",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"daisyui": "^1.11.0",
|
"daisyui": "^1.11.0",
|
||||||
@@ -28,10 +32,14 @@
|
|||||||
"react-feather": "^2.0.9",
|
"react-feather": "^2.0.9",
|
||||||
"react-icons": "^4.2.0",
|
"react-icons": "^4.2.0",
|
||||||
"react-toastify": "^7.0.4",
|
"react-toastify": "^7.0.4",
|
||||||
"stripe": "^8.168.0"
|
"stripe": "^8.168.0",
|
||||||
|
"typescript": "^4.3.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@next/eslint-plugin-next": "^11.0.1",
|
"@next/eslint-plugin-next": "^11.0.1",
|
||||||
|
"@types/cors": "^2.8.12",
|
||||||
|
"@types/express-rate-limit": "^5.1.3",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^4.29.1",
|
||||||
"autoprefixer": "^10.3.1",
|
"autoprefixer": "^10.3.1",
|
||||||
"cypress": "^8.2.0",
|
"cypress": "^8.2.0",
|
||||||
"eslint": "^7.32.0",
|
"eslint": "^7.32.0",
|
||||||
|
|||||||
@@ -9,25 +9,25 @@ setup more elements, visit their Github page https://github.com/garmeeh/next-seo
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
function MyApp({ Component, pageProps }) {
|
function MyApp({ Component, pageProps }) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
<DefaultSeo
|
<DefaultSeo
|
||||||
openGraph={{
|
openGraph={{
|
||||||
type: 'website',
|
type: 'website',
|
||||||
locale: 'en_IE',
|
locale: 'en_IE',
|
||||||
url: '',
|
url: '',
|
||||||
site_name: 'Supanextail',
|
site_name: 'Supanextail',
|
||||||
}}
|
}}
|
||||||
twitter={{
|
twitter={{
|
||||||
handle: '@michael_webdev',
|
handle: '@michael_webdev',
|
||||||
site: '@michael_webdev',
|
site: '@michael_webdev',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Component {...pageProps} />
|
<Component {...pageProps} />
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default MyApp;
|
export default MyApp;
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import Document, { Head, Html, Main, NextScript } from 'next/document';
|
import Document, { Head, Html, Main, NextScript } from 'next/document';
|
||||||
|
|
||||||
class MyDocument extends Document {
|
class MyDocument extends Document {
|
||||||
static async getInitialProps(ctx) {
|
static async getInitialProps(ctx) {
|
||||||
const initialProps = await Document.getInitialProps(ctx);
|
const initialProps = await Document.getInitialProps(ctx);
|
||||||
return { ...initialProps };
|
return { ...initialProps };
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
// This will set the initial theme, saved in localstorage
|
// This will set the initial theme, saved in localstorage
|
||||||
const setInitialTheme = `
|
const setInitialTheme = `
|
||||||
function getUserPreference() {
|
function getUserPreference() {
|
||||||
if(window.localStorage.getItem('theme')) {
|
if(window.localStorage.getItem('theme')) {
|
||||||
return window.localStorage.getItem('theme')
|
return window.localStorage.getItem('theme')
|
||||||
@@ -19,17 +19,17 @@ class MyDocument extends Document {
|
|||||||
}
|
}
|
||||||
document.body.dataset.theme = getUserPreference();
|
document.body.dataset.theme = getUserPreference();
|
||||||
`;
|
`;
|
||||||
return (
|
return (
|
||||||
<Html>
|
<Html>
|
||||||
<Head />
|
<Head />
|
||||||
<body>
|
<body>
|
||||||
<script dangerouslySetInnerHTML={{ __html: setInitialTheme }} />
|
<script dangerouslySetInnerHTML={{ __html: setInitialTheme }} />
|
||||||
<Main />
|
<Main />
|
||||||
<NextScript />
|
<NextScript />
|
||||||
</body>
|
</body>
|
||||||
</Html>
|
</Html>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default MyDocument;
|
export default MyDocument;
|
||||||
|
|||||||
@@ -6,5 +6,5 @@
|
|||||||
import { supabase } from 'utils/supabaseClient';
|
import { supabase } from 'utils/supabaseClient';
|
||||||
|
|
||||||
export default function handler(req, res) {
|
export default function handler(req, res) {
|
||||||
supabase.auth.api.setAuthCookie(req, res);
|
supabase.auth.api.setAuthCookie(req, res);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,12 +2,12 @@ import { supabase } from 'utils/supabaseClient';
|
|||||||
|
|
||||||
// Example of how to verify and get user data server-side.
|
// Example of how to verify and get user data server-side.
|
||||||
const getUser = async (req, res) => {
|
const getUser = async (req, res) => {
|
||||||
const { token } = req.headers;
|
const { token } = req.headers;
|
||||||
|
|
||||||
const { data: user, error } = await supabase.auth.api.getUser(token);
|
const { data: user, error } = await supabase.auth.api.getUser(token);
|
||||||
|
|
||||||
if (error) return res.status(401).json({ error: error.message });
|
if (error) return res.status(401).json({ error: error.message });
|
||||||
return res.status(200).json(user);
|
return res.status(200).json(user);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default getUser;
|
export default getUser;
|
||||||
|
|||||||
@@ -1,58 +0,0 @@
|
|||||||
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',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
56
pages/api/mailingList.ts
Normal file
56
pages/api/mailingList.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import Cors from 'cors';
|
||||||
|
import axios from 'axios';
|
||||||
|
import initMiddleware from 'utils/init-middleware';
|
||||||
|
import rateLimit from '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: any, res: any) {
|
||||||
|
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,31 +2,30 @@
|
|||||||
This is a simple contact form for SupaNexTail
|
This is a simple contact form for SupaNexTail
|
||||||
Using Sendgrid.
|
Using Sendgrid.
|
||||||
*/
|
*/
|
||||||
const sgMail = require('@sendgrid/mail');
|
|
||||||
|
import sgMail from '@sendgrid/mail';
|
||||||
|
|
||||||
export default async function handler(req, res) {
|
export default async function handler(req, res) {
|
||||||
if (req.method === 'POST') {
|
if (req.method === 'POST') {
|
||||||
sgMail.setApiKey(process.env.SENDGRID_SECRET);
|
sgMail.setApiKey(process.env.SENDGRID_SECRET);
|
||||||
const msg = {
|
const msg = {
|
||||||
to: process.env.SENDGRID_MAILTO, // Change to your recipient
|
to: process.env.SENDGRID_MAILTO, // Change to your recipient
|
||||||
from: process.env.SENDGRID_MAILFROM, // Change to your verified sender
|
from: process.env.SENDGRID_MAILFROM, // Change to your verified sender
|
||||||
subject: `[${process.env.NEXT_PUBLIC_TITLE}] New message from ${req.body.name}`,
|
subject: `[${process.env.NEXT_PUBLIC_TITLE}] New message from ${req.body.name}`,
|
||||||
text: req.body.message,
|
text: req.body.message,
|
||||||
reply_to: req.body.email,
|
reply_to: req.body.email,
|
||||||
};
|
};
|
||||||
sgMail
|
sgMail
|
||||||
.send(msg)
|
.send(msg)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
res
|
res.status(200).send({ message: 'Your email has been sent', success: true });
|
||||||
.status(200)
|
})
|
||||||
.send({ message: 'Your email has been sent', success: true });
|
.catch((error) => {
|
||||||
})
|
console.error(error);
|
||||||
.catch((error) => {
|
res.status(500).send({
|
||||||
console.error(error);
|
message: 'There was an issue with your email... please retry',
|
||||||
res.status(500).send({
|
err,
|
||||||
message: 'There was an issue with your email... please retry',
|
});
|
||||||
err,
|
});
|
||||||
});
|
}
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,80 +1,87 @@
|
|||||||
|
import * as Stripe from 'stripe';
|
||||||
|
|
||||||
import Cors from 'cors';
|
import Cors from 'cors';
|
||||||
import initMiddleware from 'utils/init-middleware';
|
import initMiddleware from 'utils/init-middleware';
|
||||||
|
import rateLimit from 'express-rate-limit';
|
||||||
const rateLimit = require('express-rate-limit');
|
|
||||||
|
|
||||||
const cors = initMiddleware(
|
const cors = initMiddleware(
|
||||||
Cors({
|
Cors({
|
||||||
methods: ['POST'],
|
methods: ['POST'],
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const limiter = initMiddleware(
|
const limiter = initMiddleware(
|
||||||
rateLimit({
|
rateLimit({
|
||||||
windowMs: 30000, // 30sec
|
windowMs: 30000, // 30sec
|
||||||
max: 4, // Max 4 request per 30 sec
|
max: 4, // Max 4 request per 30 sec
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
// Set your secret key. Remember to switch to your live secret key in production.
|
// Set your secret key. Remember to switch to your live secret key in production.
|
||||||
// See your keys here: https://dashboard.stripe.com/apikeys
|
// See your keys here: https://dashboard.stripe.com/apikeys
|
||||||
const stripe = require('stripe')(process.env.STRIPE_SECRET);
|
const stripe = new Stripe(process.env.STRIPE_SECRET);
|
||||||
|
|
||||||
export default async function handler(req, res) {
|
export default async function handler(req, res) {
|
||||||
await cors(req, res);
|
await cors(req, res);
|
||||||
await limiter(req, res);
|
await limiter(req, res);
|
||||||
if (req.method === 'POST') {
|
if (req.method === 'POST') {
|
||||||
const { priceId } = req.body;
|
const { priceId } = req.body;
|
||||||
|
|
||||||
// See https://stripe.com/docs/api/checkout/sessions/create
|
// See https://stripe.com/docs/api/checkout/sessions/create
|
||||||
// for additional parameters to pass.
|
// for additional parameters to pass.
|
||||||
try {
|
try {
|
||||||
const session = req.body.customerId
|
const session = req.body.customerId
|
||||||
? await stripe.checkout.sessions.create({
|
? await stripe.checkout.sessions.create({
|
||||||
mode: req.body.pay_mode,
|
mode: req.body.pay_mode,
|
||||||
payment_method_types: ['card'],
|
payment_method_types: ['card'],
|
||||||
client_reference_id: req.body.userId,
|
client_reference_id: req.body.userId,
|
||||||
metadata: { token: req.body.tokenId, priceId: req.body.priceId },
|
metadata: {
|
||||||
customer: req.body.customerId,
|
token: req.body.tokenId,
|
||||||
line_items: [
|
priceId: req.body.priceId,
|
||||||
{
|
},
|
||||||
price: priceId,
|
customer: req.body.customerId,
|
||||||
// For metered billing, do not pass quantity
|
line_items: [
|
||||||
quantity: 1,
|
{
|
||||||
},
|
price: priceId,
|
||||||
],
|
// For metered billing, do not pass quantity
|
||||||
// {CHECKOUT_SESSION_ID} is a string literal; do not change it!
|
quantity: 1,
|
||||||
// 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}`,
|
// {CHECKOUT_SESSION_ID} is a string literal; do not change it!
|
||||||
cancel_url: `${req.headers.origin}/pricing`,
|
// the actual Session ID is returned in the query parameter when your customer
|
||||||
})
|
// is redirected to the success page.
|
||||||
: await stripe.checkout.sessions.create({
|
success_url: `${req.headers.origin}/dashboard?session_id={CHECKOUT_SESSION_ID}`,
|
||||||
mode: 'subscription',
|
cancel_url: `${req.headers.origin}/pricing`,
|
||||||
payment_method_types: ['card'],
|
})
|
||||||
customer_email: req.body.email,
|
: await stripe.checkout.sessions.create({
|
||||||
client_reference_id: req.body.userId,
|
mode: 'subscription',
|
||||||
metadata: { token: req.body.tokenId, priceId: req.body.priceId },
|
payment_method_types: ['card'],
|
||||||
line_items: [
|
customer_email: req.body.email,
|
||||||
{
|
client_reference_id: req.body.userId,
|
||||||
price: priceId,
|
metadata: {
|
||||||
// For metered billing, do not pass quantity
|
token: req.body.tokenId,
|
||||||
quantity: 1,
|
priceId: req.body.priceId,
|
||||||
},
|
},
|
||||||
],
|
line_items: [
|
||||||
// {CHECKOUT_SESSION_ID} is a string literal; do not change it!
|
{
|
||||||
// the actual Session ID is returned in the query parameter when your customer
|
price: priceId,
|
||||||
// is redirected to the success page.
|
// For metered billing, do not pass quantity
|
||||||
success_url: `${req.headers.origin}/dashboard?session_id={CHECKOUT_SESSION_ID}`,
|
quantity: 1,
|
||||||
cancel_url: `${req.headers.origin}/pricing`,
|
},
|
||||||
});
|
],
|
||||||
res.status(200).send({ url: session.url });
|
// {CHECKOUT_SESSION_ID} is a string literal; do not change it!
|
||||||
} catch (e) {
|
// the actual Session ID is returned in the query parameter when your customer
|
||||||
res.status(400);
|
// is redirected to the success page.
|
||||||
return res.send({
|
success_url: `${req.headers.origin}/dashboard?session_id={CHECKOUT_SESSION_ID}`,
|
||||||
error: {
|
cancel_url: `${req.headers.origin}/pricing`,
|
||||||
message: e.message,
|
});
|
||||||
},
|
res.status(200).send({ url: session.url });
|
||||||
});
|
} catch (e) {
|
||||||
}
|
res.status(400);
|
||||||
}
|
return res.send({
|
||||||
|
error: {
|
||||||
|
message: e.message,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,37 +1,38 @@
|
|||||||
/* Dont forget to create your customer portal on Stripe
|
/* Dont forget to create your customer portal on Stripe
|
||||||
https://dashboard.stripe.com/test/settings/billing/portal */
|
https://dashboard.stripe.com/test/settings/billing/portal */
|
||||||
|
|
||||||
|
import * as Stripe from 'stripe';
|
||||||
|
|
||||||
import Cors from 'cors';
|
import Cors from 'cors';
|
||||||
import initMiddleware from 'utils/init-middleware';
|
import initMiddleware from 'utils/init-middleware';
|
||||||
|
import rateLimit from 'express-rate-limit';
|
||||||
const rateLimit = require('express-rate-limit');
|
|
||||||
|
|
||||||
const cors = initMiddleware(
|
const cors = initMiddleware(
|
||||||
Cors({
|
Cors({
|
||||||
methods: ['POST', 'PUT'],
|
methods: ['POST', 'PUT'],
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const limiter = initMiddleware(
|
const limiter = initMiddleware(
|
||||||
rateLimit({
|
rateLimit({
|
||||||
windowMs: 30000, // 30sec
|
windowMs: 30000, // 30sec
|
||||||
max: 150, // Max 4 request per 30 sec
|
max: 150, // Max 4 request per 30 sec
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
// Set your secret key. Remember to switch to your live secret key in production.
|
// Set your secret key. Remember to switch to your live secret key in production.
|
||||||
// See your keys here: https://dashboard.stripe.com/apikeys
|
// See your keys here: https://dashboard.stripe.com/apikeys
|
||||||
const stripe = require('stripe')(process.env.STRIPE_SECRET);
|
const stripe = new Stripe(process.env.STRIPE_SECRET);
|
||||||
|
|
||||||
export default async function handler(req, res) {
|
export default async function handler(req, res) {
|
||||||
await cors(req, res);
|
await cors(req, res);
|
||||||
await limiter(req, res);
|
await limiter(req, res);
|
||||||
if (req.method === 'POST') {
|
if (req.method === 'POST') {
|
||||||
const returnUrl = `${req.headers.origin}/dashboard`; // Stripe will return to the dashboard, you can change it
|
const returnUrl = `${req.headers.origin}/dashboard`; // Stripe will return to the dashboard, you can change it
|
||||||
|
|
||||||
const portalsession = await stripe.billingPortal.sessions.create({
|
const portalsession = await stripe.billingPortal.sessions.create({
|
||||||
customer: req.body.customerId,
|
customer: req.body.customerId,
|
||||||
return_url: returnUrl,
|
return_url: returnUrl,
|
||||||
});
|
});
|
||||||
res.status(200).send({ url: portalsession.url });
|
res.status(200).send({ url: portalsession.url });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,142 +6,139 @@ If you want to test it locally, you'll need the stripe CLI and use this command
|
|||||||
stripe listen --forward-to localhost:3000/api/stripe/stripe-webhook
|
stripe listen --forward-to localhost:3000/api/stripe/stripe-webhook
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import * as Stripe from 'stripe';
|
||||||
|
|
||||||
import Cors from 'cors';
|
import Cors from 'cors';
|
||||||
import { buffer } from 'micro';
|
import { buffer } from 'micro';
|
||||||
import { createClient } from '@supabase/supabase-js';
|
import { createClient } from '@supabase/supabase-js';
|
||||||
import initMiddleware from 'utils/init-middleware';
|
import initMiddleware from 'utils/init-middleware';
|
||||||
|
import rateLimit from 'express-rate-limit';
|
||||||
const rateLimit = require('express-rate-limit');
|
|
||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
api: {
|
api: {
|
||||||
bodyParser: false,
|
bodyParser: false,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Initialize the cors middleware -> Allow the browser extension to create lists
|
// Initialize the cors middleware -> Allow the browser extension to create lists
|
||||||
const cors = initMiddleware(
|
const cors = initMiddleware(
|
||||||
Cors({
|
Cors({
|
||||||
methods: ['POST', 'HEAD'],
|
methods: ['POST', 'HEAD'],
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// Init Supabase Admin
|
// Init Supabase Admin
|
||||||
|
|
||||||
const supabase = createClient(
|
const supabase = createClient(process.env.NEXT_PUBLIC_SUPABASE_URL, process.env.SUPABASE_ADMIN_KEY);
|
||||||
process.env.NEXT_PUBLIC_SUPABASE_URL,
|
|
||||||
process.env.SUPABASE_ADMIN_KEY
|
|
||||||
);
|
|
||||||
|
|
||||||
// Rate limiter : The user can only create one list every 20 seconds (avoid spam)
|
// Rate limiter : The user can only create one list every 20 seconds (avoid spam)
|
||||||
|
|
||||||
const limiter = initMiddleware(
|
const limiter = initMiddleware(
|
||||||
rateLimit({
|
rateLimit({
|
||||||
windowMs: 30000, // 30sec
|
windowMs: 30000, // 30sec
|
||||||
max: 150, // Max 150 request per 30 sec
|
max: 150, // Max 150 request per 30 sec
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
// Set your secret key. Remember to switch to your live secret key in production.
|
// Set your secret key. Remember to switch to your live secret key in production.
|
||||||
// See your keys here: https://dashboard.stripe.com/apikeys
|
// See your keys here: https://dashboard.stripe.com/apikeys
|
||||||
const stripe = require('stripe')(process.env.STRIPE_SECRET);
|
|
||||||
|
const stripe = new Stripe(process.env.STRIPE_SECRET);
|
||||||
|
|
||||||
export default async function handler(req, res) {
|
export default async function handler(req, res) {
|
||||||
await cors(req, res);
|
await cors(req, res);
|
||||||
await limiter(req, res);
|
await limiter(req, res);
|
||||||
stripe.setMaxNetworkRetries(2);
|
stripe.setMaxNetworkRetries(2);
|
||||||
|
|
||||||
if (req.method === 'POST') {
|
if (req.method === 'POST') {
|
||||||
// Retrieve the event by verifying the signature using the raw body and secret.
|
// Retrieve the event by verifying the signature using the raw body and secret.
|
||||||
let event;
|
let event;
|
||||||
const buf = await buffer(req);
|
const buf = await buffer(req);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
event = stripe.webhooks.constructEvent(
|
event = stripe.webhooks.constructEvent(
|
||||||
buf,
|
buf,
|
||||||
req.headers['stripe-signature'],
|
req.headers['stripe-signature'],
|
||||||
process.env.STRIPE_WEBHOOK
|
process.env.STRIPE_WEBHOOK
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err);
|
console.log(err);
|
||||||
console.log(`⚠️ Webhook signature verification failed.`);
|
console.log(`⚠️ Webhook signature verification failed.`);
|
||||||
console.log(
|
console.log(`⚠️ Check the env file and enter the correct webhook secret.`);
|
||||||
`⚠️ Check the env file and enter the correct webhook secret.`
|
return res.send(400);
|
||||||
);
|
}
|
||||||
return res.send(400);
|
// Extract the object from the event.
|
||||||
}
|
const dataObject = event.data.object;
|
||||||
// Extract the object from the event.
|
|
||||||
const dataObject = event.data.object;
|
|
||||||
|
|
||||||
// Handle the event
|
// Handle the event
|
||||||
// Review important events for Billing webhooks
|
// Review important events for Billing webhooks
|
||||||
// https://stripe.com/docs/billing/webhooks
|
// https://stripe.com/docs/billing/webhooks
|
||||||
// Remove comment to see the various objects sent for this sample
|
// Remove comment to see the various objects sent for this sample
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case 'checkout.session.completed':
|
case 'checkout.session.completed':
|
||||||
const { data: subscriptions, error } = await supabase
|
const { data: subscriptions, error } = await supabase
|
||||||
.from('subscriptions')
|
.from('subscriptions')
|
||||||
.select('*')
|
.select('*')
|
||||||
.eq('id', dataObject.client_reference_id);
|
.eq('id', dataObject.client_reference_id);
|
||||||
console.log(dataObject);
|
console.log(dataObject);
|
||||||
|
|
||||||
if (subscriptions.length == 0) {
|
if (subscriptions.length == 0) {
|
||||||
const { data, error } = await supabase
|
const { data, error } = await supabase
|
||||||
.from('profiles')
|
.from('profiles')
|
||||||
.update({ customerId: dataObject.customer })
|
.update({ customerId: dataObject.customer })
|
||||||
.eq('id', dataObject.client_reference_id);
|
.eq('id', dataObject.client_reference_id);
|
||||||
if (error) console.log(error);
|
if (error) console.log(error);
|
||||||
|
|
||||||
await supabase
|
await supabase
|
||||||
.from('subscriptions')
|
.from('subscriptions')
|
||||||
.insert([
|
.insert([
|
||||||
{
|
{
|
||||||
id: dataObject.client_reference_id,
|
id: dataObject.client_reference_id,
|
||||||
customer_id: dataObject.customer,
|
customer_id: dataObject.customer,
|
||||||
paid_user: true,
|
paid_user: true,
|
||||||
plan: dataObject.metadata.priceId,
|
plan: dataObject.metadata.priceId,
|
||||||
subscription: dataObject.subscription,
|
subscription: dataObject.subscription,
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
.then()
|
.then()
|
||||||
.catch((err) => console.log(err));
|
.catch((err) => console.log(err));
|
||||||
} else if (subscriptions.length > 0) {
|
} else if (subscriptions.length > 0) {
|
||||||
await supabase
|
await supabase
|
||||||
.from('subscriptions')
|
.from('subscriptions')
|
||||||
.update({
|
.update({
|
||||||
customer_id: dataObject.customer,
|
customer_id: dataObject.customer,
|
||||||
paid_user: true,
|
paid_user: true,
|
||||||
plan: dataObject.metadata.priceId,
|
plan: dataObject.metadata.priceId,
|
||||||
subscription: dataObject.subscription,
|
subscription: dataObject.subscription,
|
||||||
})
|
})
|
||||||
.eq('id', dataObject.client_reference_id)
|
.eq('id', dataObject.client_reference_id)
|
||||||
.then()
|
.then()
|
||||||
.catch((err) => console.log(err));
|
.catch((err) => console.log(err));
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'customer.subscription.deleted':
|
case 'customer.subscription.deleted':
|
||||||
await supabase
|
await supabase
|
||||||
.from('subscriptions')
|
.from('subscriptions')
|
||||||
.update({ paid_user: false })
|
.update({ paid_user: false })
|
||||||
.eq('customer_id', dataObject.customer)
|
.eq('customer_id', dataObject.customer)
|
||||||
.then()
|
.then()
|
||||||
.catch((err) => console.log(err));
|
.catch((err) => console.log(err));
|
||||||
break;
|
break;
|
||||||
case 'invoice.payment_failed':
|
case 'invoice.payment_failed':
|
||||||
// If the payment fails or the customer does not have a valid payment method,
|
// If the payment fails or the customer does not have a valid payment method,
|
||||||
// an invoice.payment_failed event is sent, the subscription becomes past_due.
|
// an invoice.payment_failed event is sent, the subscription becomes past_due.
|
||||||
// Use this webhook to notify your user that their payment has
|
// Use this webhook to notify your user that their payment has
|
||||||
// failed and to retrieve new card details.
|
// failed and to retrieve new card details.
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'invoice.paid':
|
case 'invoice.paid':
|
||||||
// Used to provision services after the trial has ended.
|
// Used to provision services after the trial has ended.
|
||||||
// The status of the invoice will show up as paid. Store the status in your
|
// The status of the invoice will show up as paid. Store the status in your
|
||||||
// database to reference when a user accesses your service to avoid hitting rate limits.
|
// database to reference when a user accesses your service to avoid hitting rate limits.
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
// Unexpected event type
|
// Unexpected event type
|
||||||
}
|
}
|
||||||
res.send(200);
|
res.send(200);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,15 +3,15 @@ import Layout from 'components/Layout';
|
|||||||
import { NextSeo } from 'next-seo';
|
import { NextSeo } from 'next-seo';
|
||||||
|
|
||||||
const ContactPage = () => (
|
const ContactPage = () => (
|
||||||
<>
|
<>
|
||||||
<NextSeo
|
<NextSeo
|
||||||
title={`${process.env.NEXT_PUBLIC_TITLE} | Contact`}
|
title={`${process.env.NEXT_PUBLIC_TITLE} | Contact`}
|
||||||
description={`This is the contact page for ${process.env.NEXT_PUBLIC_TITLE}`}
|
description={`This is the contact page for ${process.env.NEXT_PUBLIC_TITLE}`}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Layout>
|
<Layout>
|
||||||
<Contact />
|
<Contact />
|
||||||
</Layout>
|
</Layout>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
export default ContactPage;
|
export default ContactPage;
|
||||||
|
|||||||
@@ -1,95 +1,90 @@
|
|||||||
|
import * as Stripe from 'stripe';
|
||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import Dashboard from '../components/Dashboard';
|
||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
import Layout from 'components/Layout';
|
import Layout from 'components/Layout';
|
||||||
import { createClient } from '@supabase/supabase-js';
|
import { createClient } from '@supabase/supabase-js';
|
||||||
import { useRouter } from 'next/router';
|
|
||||||
import { supabase } from '../utils/supabaseClient';
|
import { supabase } from '../utils/supabaseClient';
|
||||||
import Dashboard from '../components/Dashboard';
|
import { useRouter } from 'next/router';
|
||||||
|
|
||||||
const DashboardPage = ({ user, plan, profile }) => {
|
const DashboardPage = ({ user, plan, profile }) => {
|
||||||
const [session, setSession] = useState(null);
|
const [session, setSession] = useState(null);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// If a user is not logged in, return to the homepage
|
// If a user is not logged in, return to the homepage
|
||||||
if (!user) {
|
if (!user) {
|
||||||
router.push('/');
|
router.push('/');
|
||||||
}
|
}
|
||||||
}, [user]);
|
}, [user]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setSession(supabase.auth.session());
|
setSession(supabase.auth.session());
|
||||||
|
|
||||||
supabase.auth.onAuthStateChange((_event, session) => {
|
supabase.auth.onAuthStateChange((_event, session) => {
|
||||||
setSession(session);
|
setSession(session);
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Head>
|
<Head>
|
||||||
<title>{process.env.NEXT_PUBLIC_TITLE} | Dashboard</title>
|
<title>{process.env.NEXT_PUBLIC_TITLE} | Dashboard</title>
|
||||||
</Head>
|
</Head>
|
||||||
|
|
||||||
<Layout>
|
<Layout>
|
||||||
{!session ? (
|
{!session ? (
|
||||||
<div className="text-center">You are not logged in</div>
|
<div className="text-center">You are not logged in</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Dashboard
|
<Dashboard key={user.id} session={session} plan={plan} profile={profile} />
|
||||||
key={user.id}
|
</>
|
||||||
session={session}
|
)}
|
||||||
plan={plan}
|
</Layout>
|
||||||
profile={profile}
|
</div>
|
||||||
/>
|
);
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Layout>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
export async function getServerSideProps({ req }) {
|
export async function getServerSideProps({ req }) {
|
||||||
const supabaseAdmin = createClient(
|
const supabaseAdmin = createClient(
|
||||||
process.env.NEXT_PUBLIC_SUPABASE_URL,
|
process.env.NEXT_PUBLIC_SUPABASE_URL,
|
||||||
process.env.SUPABASE_ADMIN_KEY
|
process.env.SUPABASE_ADMIN_KEY
|
||||||
);
|
);
|
||||||
const { user } = await supabaseAdmin.auth.api.getUserByCookie(req);
|
const { user } = await supabaseAdmin.auth.api.getUserByCookie(req);
|
||||||
const stripe = require('stripe')(process.env.STRIPE_SECRET);
|
const stripe = new Stripe(process.env.STRIPE_SECRET);
|
||||||
|
|
||||||
// If the user exist, you will retrieve the user profile and if he/she's a paid user
|
// If the user exist, you will retrieve the user profile and if he/she's a paid user
|
||||||
if (user) {
|
if (user) {
|
||||||
const { data: plan, error } = await supabaseAdmin
|
const { data: plan, error } = await supabaseAdmin
|
||||||
.from('subscriptions')
|
.from('subscriptions')
|
||||||
.select('plan')
|
.select('plan')
|
||||||
.eq('id', user.id)
|
.eq('id', user.id)
|
||||||
.single();
|
.single();
|
||||||
|
|
||||||
// Check the subscription plan. If it doesnt exist, return null
|
// Check the subscription plan. If it doesnt exist, return null
|
||||||
const subscription = plan?.plan
|
const subscription = plan?.plan ? await stripe.subscriptions.retrieve(plan.plan) : null;
|
||||||
? await stripe.subscriptions.retrieve(plan.plan)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
const { data: profile, errorProfile } = await supabaseAdmin
|
const { data: profile, errorProfile } = await supabaseAdmin
|
||||||
.from('profiles')
|
.from('profiles')
|
||||||
.select(`username, website, avatar_url`)
|
.select(`username, website, avatar_url`)
|
||||||
.eq('id', user.id)
|
.eq('id', user.id)
|
||||||
.single();
|
.single();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
user,
|
user,
|
||||||
plan: subscription?.plan?.id ? subscription.plan.id : null,
|
plan: subscription?.plan?.id ? subscription.plan.id : null,
|
||||||
profile,
|
profile,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
// If no user, redirect to index.
|
// If no user, redirect to index.
|
||||||
return { props: {}, redirect: { destination: '/', permanent: false } };
|
return { props: {}, redirect: { destination: '/', permanent: false } };
|
||||||
}
|
}
|
||||||
|
|
||||||
// If there is a user, return it.
|
// If there is a user, return it.
|
||||||
}
|
}
|
||||||
export default DashboardPage;
|
export default DashboardPage;
|
||||||
|
|||||||
@@ -8,50 +8,38 @@ import Landing from 'components/Landing';
|
|||||||
import Layout from 'components/Layout';
|
import Layout from 'components/Layout';
|
||||||
|
|
||||||
const Home = () => (
|
const Home = () => (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
<title>{`Welcome to ${process.env.NEXT_PUBLIC_TITLE} 👋`}</title>
|
<title>{`Welcome to ${process.env.NEXT_PUBLIC_TITLE} 👋`}</title>
|
||||||
<meta
|
<meta
|
||||||
name="description"
|
name="description"
|
||||||
content="SupaNexTail is a boilerplate for your SaaS, based on Next.js, Supabase, and TailwindCSS"
|
content="SupaNexTail is a boilerplate for your SaaS, based on Next.js, Supabase, and TailwindCSS"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<meta property="og:url" content="https://supanextail.dev/" />
|
<meta property="og:url" content="https://supanextail.dev/" />
|
||||||
<meta property="og:type" content="website" />
|
<meta property="og:type" content="website" />
|
||||||
<meta
|
<meta property="og:title" content={`Welcome to ${process.env.NEXT_PUBLIC_TITLE} 👋`} />
|
||||||
property="og:title"
|
<meta
|
||||||
content={`Welcome to ${process.env.NEXT_PUBLIC_TITLE} 👋`}
|
property="og:description"
|
||||||
/>
|
content="SupaNexTail is a boilerplate for your SaaS, based on Next.js, Supabase, and TailwindCSS"
|
||||||
<meta
|
/>
|
||||||
property="og:description"
|
<meta property="og:image" content="https://supanextail.dev/ogimage.png" />
|
||||||
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 name="twitter:card" content="summary_large_image" />
|
||||||
<meta property="twitter:domain" content="supanextail.dev" />
|
<meta property="twitter:domain" content="supanextail.dev" />
|
||||||
<meta
|
<meta property="twitter:url" content="https://supanextail.dev/ogimage.png" />
|
||||||
property="twitter:url"
|
<meta name="twitter:title" content={`Welcome to ${process.env.NEXT_PUBLIC_TITLE} 👋`} />
|
||||||
content="https://supanextail.dev/ogimage.png"
|
<meta
|
||||||
/>
|
name="twitter:description"
|
||||||
<meta
|
content="SupaNexTail is a boilerplate for your SaaS, based on Next.js, Supabase, and TailwindCSS"
|
||||||
name="twitter:title"
|
/>
|
||||||
content={`Welcome to ${process.env.NEXT_PUBLIC_TITLE} 👋`}
|
<meta name="twitter:image" content="https://supanextail.dev/ogimage.png" />
|
||||||
/>
|
<meta charSet="UTF-8" />
|
||||||
<meta
|
</Head>
|
||||||
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>
|
<Layout>
|
||||||
<Landing />
|
<Landing />
|
||||||
</Layout>
|
</Layout>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
export default Home;
|
export default Home;
|
||||||
|
|||||||
@@ -10,26 +10,21 @@ import { NextSeo } from 'next-seo';
|
|||||||
import { useAuth } from 'utils/AuthContext';
|
import { useAuth } from 'utils/AuthContext';
|
||||||
|
|
||||||
const LoginPage = () => {
|
const LoginPage = () => {
|
||||||
const { signUp, signIn, signOut, resetPassword } = useAuth();
|
const { signUp, signIn, signOut, resetPassword } = useAuth();
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<NextSeo
|
<NextSeo
|
||||||
title={`${process.env.NEXT_PUBLIC_TITLE} | Auth`}
|
title={`${process.env.NEXT_PUBLIC_TITLE} | Auth`}
|
||||||
description={`This is the auth page for ${process.env.NEXT_PUBLIC_TITLE}`}
|
description={`This is the auth page for ${process.env.NEXT_PUBLIC_TITLE}`}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Layout>
|
<Layout>
|
||||||
<div className="flex flex-wrap justify-evenly w-full mt-20">
|
<div className="flex flex-wrap justify-evenly w-full mt-20">
|
||||||
<Login
|
<Login signUp={signUp} signIn={signIn} signOut={signOut} resetPassword={resetPassword} />
|
||||||
signUp={signUp}
|
</div>
|
||||||
signIn={signIn}
|
</Layout>
|
||||||
signOut={signOut}
|
</>
|
||||||
resetPassword={resetPassword}
|
);
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Layout>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default LoginPage;
|
export default LoginPage;
|
||||||
|
|||||||
@@ -5,14 +5,14 @@ import { NextSeo } from 'next-seo';
|
|||||||
import Pricing from 'components/Pricing';
|
import Pricing from 'components/Pricing';
|
||||||
|
|
||||||
const PricingPage = () => (
|
const PricingPage = () => (
|
||||||
<>
|
<>
|
||||||
<NextSeo
|
<NextSeo
|
||||||
title={`${process.env.NEXT_PUBLIC_TITLE} | Pricing`}
|
title={`${process.env.NEXT_PUBLIC_TITLE} | Pricing`}
|
||||||
description="SupaNexTail is a boilerplate for your website, based on Next.js, Supabase, and TailwindCSS"
|
description="SupaNexTail is a boilerplate for your website, based on Next.js, Supabase, and TailwindCSS"
|
||||||
/>
|
/>
|
||||||
<Layout>
|
<Layout>
|
||||||
<Pricing />
|
<Pricing />
|
||||||
</Layout>
|
</Layout>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
export default PricingPage;
|
export default PricingPage;
|
||||||
|
|||||||
@@ -5,14 +5,14 @@ import { NextSeo } from 'next-seo';
|
|||||||
import PrivacyPolicy from 'components/PrivacyPolicy';
|
import PrivacyPolicy from 'components/PrivacyPolicy';
|
||||||
|
|
||||||
const PrivacyPage = () => (
|
const PrivacyPage = () => (
|
||||||
<>
|
<>
|
||||||
<NextSeo
|
<NextSeo
|
||||||
title={`${process.env.NEXT_PUBLIC_TITLE} | Privacy Policy`}
|
title={`${process.env.NEXT_PUBLIC_TITLE} | Privacy Policy`}
|
||||||
description="SupaNexTail is a boilerplate for your website, based on Next.js, Supabase, and TailwindCSS"
|
description="SupaNexTail is a boilerplate for your website, based on Next.js, Supabase, and TailwindCSS"
|
||||||
/>
|
/>
|
||||||
<Layout>
|
<Layout>
|
||||||
<PrivacyPolicy />
|
<PrivacyPolicy />
|
||||||
</Layout>
|
</Layout>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
export default PrivacyPage;
|
export default PrivacyPage;
|
||||||
|
|||||||
@@ -10,19 +10,19 @@ import Layout from 'components/Layout';
|
|||||||
import { NextSeo } from 'next-seo';
|
import { NextSeo } from 'next-seo';
|
||||||
|
|
||||||
const SignUpPage = () => (
|
const SignUpPage = () => (
|
||||||
<>
|
<>
|
||||||
<NextSeo
|
<NextSeo
|
||||||
title={`${process.env.NEXT_PUBLIC_TITLE} | Auth`}
|
title={`${process.env.NEXT_PUBLIC_TITLE} | Auth`}
|
||||||
description={`This is the auth page for ${process.env.NEXT_PUBLIC_TITLE}`}
|
description={`This is the auth page for ${process.env.NEXT_PUBLIC_TITLE}`}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Layout>
|
<Layout>
|
||||||
<div className="flex flex-wrap justify-evenly w-full mt-20">
|
<div className="flex flex-wrap justify-evenly w-full mt-20">
|
||||||
<AuthText />
|
<AuthText />
|
||||||
<AuthComponent />
|
<AuthComponent />
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</Layout>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
export default SignUpPage;
|
export default SignUpPage;
|
||||||
|
|||||||
@@ -5,14 +5,14 @@ import { NextSeo } from 'next-seo';
|
|||||||
import Terms from 'components/Terms';
|
import Terms from 'components/Terms';
|
||||||
|
|
||||||
const TermsPage = () => (
|
const TermsPage = () => (
|
||||||
<>
|
<>
|
||||||
<NextSeo
|
<NextSeo
|
||||||
title={`${process.env.NEXT_PUBLIC_TITLE} | Terms and conditions`}
|
title={`${process.env.NEXT_PUBLIC_TITLE} | Terms and conditions`}
|
||||||
description="SupaNexTail is a boilerplate for your website, based on Next.js, Supabase, and TailwindCSS"
|
description="SupaNexTail is a boilerplate for your website, based on Next.js, Supabase, and TailwindCSS"
|
||||||
/>
|
/>
|
||||||
<Layout>
|
<Layout>
|
||||||
<Terms />
|
<Terms />
|
||||||
</Layout>
|
</Layout>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
export default TermsPage;
|
export default TermsPage;
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
// If you want to use other PostCSS plugins, see the following:
|
// If you want to use other PostCSS plugins, see the following:
|
||||||
// https://tailwindcss.com/docs/using-with-preprocessors
|
// https://tailwindcss.com/docs/using-with-preprocessors
|
||||||
module.exports = {
|
module.exports = {
|
||||||
plugins: {
|
plugins: {
|
||||||
tailwindcss: {},
|
tailwindcss: {},
|
||||||
autoprefixer: {},
|
autoprefixer: {},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,76 +1,76 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
mode: 'jit',
|
mode: 'jit',
|
||||||
purge: ['./pages/**/*.{js,ts,jsx,tsx}', './components/**/*.{js,ts,jsx,tsx}'],
|
purge: ['./pages/**/*.{js,ts,jsx,tsx}', './components/**/*.{js,ts,jsx,tsx}'],
|
||||||
darkMode: false, // or 'media' or 'class'
|
darkMode: false, // or 'media' or 'class'
|
||||||
theme: {
|
theme: {
|
||||||
fontFamily: {
|
fontFamily: {
|
||||||
title: ['Poppins'],
|
title: ['Poppins'],
|
||||||
body: ['Inter'],
|
body: ['Inter'],
|
||||||
},
|
},
|
||||||
extend: {},
|
extend: {},
|
||||||
},
|
},
|
||||||
variants: {
|
variants: {
|
||||||
extend: {},
|
extend: {},
|
||||||
},
|
},
|
||||||
plugins: [require('daisyui')],
|
plugins: [require('daisyui')],
|
||||||
daisyui: {
|
daisyui: {
|
||||||
themes: [
|
themes: [
|
||||||
{
|
{
|
||||||
supaTheme: {
|
supaTheme: {
|
||||||
primary: '#00B8F0',
|
primary: '#00B8F0',
|
||||||
'primary-focus': '#009de0',
|
'primary-focus': '#009de0',
|
||||||
'primary-content': '#ffffff',
|
'primary-content': '#ffffff',
|
||||||
|
|
||||||
secondary: '#f03800',
|
secondary: '#f03800',
|
||||||
'secondary-focus': '#e22f00',
|
'secondary-focus': '#e22f00',
|
||||||
'secondary-content': '#ffffff',
|
'secondary-content': '#ffffff',
|
||||||
|
|
||||||
accent: '#00f0b0',
|
accent: '#00f0b0',
|
||||||
'accent-focus': '#00e28a',
|
'accent-focus': '#00e28a',
|
||||||
'accent-content': '#ffffff',
|
'accent-content': '#ffffff',
|
||||||
|
|
||||||
neutral: '#3d4451',
|
neutral: '#3d4451',
|
||||||
'neutral-focus': '#2a2e37',
|
'neutral-focus': '#2a2e37',
|
||||||
'neutral-content': '#ffffff',
|
'neutral-content': '#ffffff',
|
||||||
|
|
||||||
'base-100': '#ffffff',
|
'base-100': '#ffffff',
|
||||||
'base-200': '#767676',
|
'base-200': '#767676',
|
||||||
'base-300': '#d1d5db',
|
'base-300': '#d1d5db',
|
||||||
'base-content': '#1f2937',
|
'base-content': '#1f2937',
|
||||||
|
|
||||||
info: '#2094f3' /* Info */,
|
info: '#2094f3' /* Info */,
|
||||||
success: '#009485' /* Success */,
|
success: '#009485' /* Success */,
|
||||||
warning: '#ff9900' /* Warning */,
|
warning: '#ff9900' /* Warning */,
|
||||||
error: '#ff5724' /* Error */,
|
error: '#ff5724' /* Error */,
|
||||||
},
|
},
|
||||||
dark: {
|
dark: {
|
||||||
primary: '#00B8F0',
|
primary: '#00B8F0',
|
||||||
'primary-focus': '#009de0',
|
'primary-focus': '#009de0',
|
||||||
'primary-content': '#ffffff',
|
'primary-content': '#ffffff',
|
||||||
|
|
||||||
secondary: '#f03800',
|
secondary: '#f03800',
|
||||||
'secondary-focus': '#e22f00',
|
'secondary-focus': '#e22f00',
|
||||||
'secondary-content': '#ffffff',
|
'secondary-content': '#ffffff',
|
||||||
|
|
||||||
accent: '#00f0b0',
|
accent: '#00f0b0',
|
||||||
'accent-focus': '#00e28a',
|
'accent-focus': '#00e28a',
|
||||||
'accent-content': '#ffffff',
|
'accent-content': '#ffffff',
|
||||||
|
|
||||||
neutral: '#3d4451',
|
neutral: '#3d4451',
|
||||||
'neutral-focus': '#2a2e37',
|
'neutral-focus': '#2a2e37',
|
||||||
'neutral-content': '#ffffff',
|
'neutral-content': '#ffffff',
|
||||||
|
|
||||||
'base-100': '#2A2E37',
|
'base-100': '#2A2E37',
|
||||||
'base-200': '#EBECF0',
|
'base-200': '#EBECF0',
|
||||||
'base-300': '#16181D',
|
'base-300': '#16181D',
|
||||||
'base-content': '#EBECF0',
|
'base-content': '#EBECF0',
|
||||||
|
|
||||||
info: '#2094f3',
|
info: '#2094f3',
|
||||||
success: '#009485',
|
success: '#009485',
|
||||||
warning: '#ff9900',
|
warning: '#ff9900',
|
||||||
error: '#ff5724',
|
error: '#ff5724',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
74
tsconfig.json
Normal file
74
tsconfig.json
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
/* Visit https://aka.ms/tsconfig.json to read more about this file */
|
||||||
|
/* Basic Options */
|
||||||
|
// "incremental": true, /* Enable incremental compilation */
|
||||||
|
"target": "esnext", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', 'ES2021', or 'ESNEXT'. */
|
||||||
|
"module": "esnext", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */// "lib": [], /* Specify library files to be included in the compilation. */
|
||||||
|
// "allowJs": true, /* Allow javascript files to be compiled. */
|
||||||
|
// "checkJs": true, /* Report errors in .js files. */
|
||||||
|
"jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', 'react', 'react-jsx' or 'react-jsxdev'. */// "declaration": true, /* Generates corresponding '.d.ts' file. */
|
||||||
|
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
|
||||||
|
// "sourceMap": true, /* Generates corresponding '.map' file. */
|
||||||
|
// "outFile": "./", /* Concatenate and emit output to single file. */
|
||||||
|
// "outDir": "./", /* Redirect output structure to the directory. */
|
||||||
|
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
|
||||||
|
// "composite": true, /* Enable project compilation */
|
||||||
|
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
|
||||||
|
"removeComments": false, /* Do not emit comments to output. */// "noEmit": true, /* Do not emit outputs. */
|
||||||
|
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
|
||||||
|
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
|
||||||
|
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
|
||||||
|
/* Strict Type-Checking Options */
|
||||||
|
"strict": true, /* Enable all strict type-checking options. */
|
||||||
|
"noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
|
||||||
|
"strictNullChecks": true, /* Enable strict null checks. */
|
||||||
|
"strictFunctionTypes": true, /* Enable strict checking of function types. */
|
||||||
|
"strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
|
||||||
|
"strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
|
||||||
|
"noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
|
||||||
|
"alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. *//* Additional Checks */
|
||||||
|
"noUnusedLocals": true, /* Report errors on unused locals. */
|
||||||
|
"noUnusedParameters": true, /* Report errors on unused parameters. */
|
||||||
|
"noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
|
||||||
|
"noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */// "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */
|
||||||
|
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an 'override' modifier. */
|
||||||
|
// "noPropertyAccessFromIndexSignature": true, /* Require undeclared properties from index signatures to use element accesses. */
|
||||||
|
/* Module Resolution Options */
|
||||||
|
"moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
|
||||||
|
"baseUrl": "./", /* Base directory to resolve non-absolute module names. *//* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
|
||||||
|
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
|
||||||
|
// "typeRoots": [], /* List of folders to include type definitions from. */
|
||||||
|
// "types": [], /* Type declaration files to be included in compilation. */
|
||||||
|
"allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
|
||||||
|
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
|
||||||
|
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
||||||
|
/* Source Map Options */
|
||||||
|
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
|
||||||
|
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
||||||
|
"inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
|
||||||
|
"inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. *//* Experimental Options */
|
||||||
|
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
|
||||||
|
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
|
||||||
|
/* Advanced Options */
|
||||||
|
"skipLibCheck": true, /* Skip type checking of declaration files. */
|
||||||
|
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */,
|
||||||
|
"lib": [
|
||||||
|
"dom",
|
||||||
|
"dom.iterable",
|
||||||
|
"esnext"
|
||||||
|
],
|
||||||
|
"allowJs": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"next-env.d.ts",
|
||||||
|
"**/*.ts",
|
||||||
|
"**/*.tsx"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -6,53 +6,47 @@ import { supabase } from 'utils/supabaseClient';
|
|||||||
const AuthContext = createContext();
|
const AuthContext = createContext();
|
||||||
|
|
||||||
export const AuthProvider = ({ children }) => {
|
export const AuthProvider = ({ children }) => {
|
||||||
const [user, setUser] = useState();
|
const [user, setUser] = useState();
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Check active sessions and sets the user
|
// Check active sessions and sets the user
|
||||||
const session = supabase.auth.session();
|
const session = supabase.auth.session();
|
||||||
|
|
||||||
setUser(session?.user ?? null);
|
setUser(session?.user ?? null);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|
||||||
// Listen for changes on auth state (logged in, signed out, etc.)
|
// Listen for changes on auth state (logged in, signed out, etc.)
|
||||||
const { data: listener } = supabase.auth.onAuthStateChange(
|
const { data: listener } = supabase.auth.onAuthStateChange(async (event, session) => {
|
||||||
async (event, session) => {
|
if ((event === 'SIGNED_OUT') | (event === 'SIGNED_IN')) {
|
||||||
if ((event === 'SIGNED_OUT') | (event === 'SIGNED_IN')) {
|
fetch('/api/auth', {
|
||||||
fetch('/api/auth', {
|
method: 'POST',
|
||||||
method: 'POST',
|
headers: new Headers({ 'Content-Type': 'application/json' }),
|
||||||
headers: new Headers({ 'Content-Type': 'application/json' }),
|
credentials: 'same-origin',
|
||||||
credentials: 'same-origin',
|
body: JSON.stringify({ event, session }),
|
||||||
body: JSON.stringify({ event, session }),
|
}).then((res) => res.json());
|
||||||
}).then((res) => res.json());
|
}
|
||||||
}
|
if (event === 'USER_UPDATED') {
|
||||||
if (event === 'USER_UPDATED') {
|
}
|
||||||
}
|
setUser(session?.user ?? null);
|
||||||
setUser(session?.user ?? null);
|
setLoading(false);
|
||||||
setLoading(false);
|
});
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
listener?.unsubscribe();
|
listener?.unsubscribe();
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Will be passed down to Signup, Login and Dashboard components
|
// Will be passed down to Signup, Login and Dashboard components
|
||||||
const value = {
|
const value = {
|
||||||
signUp: (data) => supabase.auth.signUp(data),
|
signUp: (data) => supabase.auth.signUp(data),
|
||||||
signIn: (data) => supabase.auth.signIn(data),
|
signIn: (data) => supabase.auth.signIn(data),
|
||||||
signOut: () => supabase.auth.signOut(),
|
signOut: () => supabase.auth.signOut(),
|
||||||
resetPassword: (data) => supabase.auth.api.resetPasswordForEmail(data),
|
resetPassword: (data) => supabase.auth.api.resetPasswordForEmail(data),
|
||||||
user,
|
user,
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return <AuthContext.Provider value={value}>{!loading && children}</AuthContext.Provider>;
|
||||||
<AuthContext.Provider value={value}>
|
|
||||||
{!loading && children}
|
|
||||||
</AuthContext.Provider>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// export the useAuth hook
|
// export the useAuth hook
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
// Helper method to wait for a middleware to execute before continuing
|
// Helper method to wait for a middleware to execute before continuing
|
||||||
// And to throw an error when an error happens in a middleware
|
// And to throw an error when an error happens in a middleware
|
||||||
export default function initMiddleware(middleware) {
|
export default function initMiddleware(middleware) {
|
||||||
return (req, res) =>
|
return (req, res) =>
|
||||||
new Promise((resolve, reject) => {
|
new Promise((resolve, reject) => {
|
||||||
middleware(req, res, (result) => {
|
middleware(req, res, (result) => {
|
||||||
if (result instanceof Error) {
|
if (result instanceof Error) {
|
||||||
return reject(result);
|
return reject(result);
|
||||||
}
|
}
|
||||||
return resolve(result);
|
return resolve(result);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,45 +1,45 @@
|
|||||||
// You can store your price IDs from Stripe here
|
// You can store your price IDs from Stripe here
|
||||||
|
|
||||||
const Prices = {
|
const Prices = {
|
||||||
personal: {
|
personal: {
|
||||||
monthly: {
|
monthly: {
|
||||||
id: 'price_1J5q2yDMjD0UnVmMXzEWYDnl',
|
id: 'price_1J5q2yDMjD0UnVmMXzEWYDnl',
|
||||||
desc: 'Personal plan (monthly)',
|
desc: 'Personal plan (monthly)',
|
||||||
},
|
},
|
||||||
annually: {
|
annually: {
|
||||||
id: 'price_1J5q45DMjD0UnVmMQxXHKGAv',
|
id: 'price_1J5q45DMjD0UnVmMQxXHKGAv',
|
||||||
desc: 'Personal plan (annually)',
|
desc: 'Personal plan (annually)',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
team: {
|
team: {
|
||||||
monthly: {
|
monthly: {
|
||||||
id: 'price_1J5q3GDMjD0UnVmMlHc5Eedq',
|
id: 'price_1J5q3GDMjD0UnVmMlHc5Eedq',
|
||||||
desc: 'Team plan (monthly)',
|
desc: 'Team plan (monthly)',
|
||||||
},
|
},
|
||||||
annually: {
|
annually: {
|
||||||
id: 'price_1J5q8zDMjD0UnVmMqsngM91X',
|
id: 'price_1J5q8zDMjD0UnVmMqsngM91X',
|
||||||
desc: 'Team plan (annually)',
|
desc: 'Team plan (annually)',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
pro: {
|
pro: {
|
||||||
monthly: {
|
monthly: {
|
||||||
id: 'price_1J6KRuDMjD0UnVmMIItaOdT3',
|
id: 'price_1J6KRuDMjD0UnVmMIItaOdT3',
|
||||||
desc: 'Pro plan (monthly)',
|
desc: 'Pro plan (monthly)',
|
||||||
},
|
},
|
||||||
annually: {
|
annually: {
|
||||||
id: 'price_1J5q9VDMjD0UnVmMIQtVDSZ9',
|
id: 'price_1J5q9VDMjD0UnVmMIQtVDSZ9',
|
||||||
desc: 'Pro plan (annually)',
|
desc: 'Pro plan (annually)',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const PriceIds = {
|
const PriceIds = {
|
||||||
price_1J5q2yDMjD0UnVmMXzEWYDnl: 'Personal plan (monthly)',
|
price_1J5q2yDMjD0UnVmMXzEWYDnl: 'Personal plan (monthly)',
|
||||||
price_1J5q45DMjD0UnVmMQxXHKGAv: 'Personal plan (annually)',
|
price_1J5q45DMjD0UnVmMQxXHKGAv: 'Personal plan (annually)',
|
||||||
price_1J5q3GDMjD0UnVmMlHc5Eedq: 'Team plan (monthly)',
|
price_1J5q3GDMjD0UnVmMlHc5Eedq: 'Team plan (monthly)',
|
||||||
price_1J5q8zDMjD0UnVmMqsngM91X: 'Team plan (annually)',
|
price_1J5q8zDMjD0UnVmMqsngM91X: 'Team plan (annually)',
|
||||||
price_1J6KRuDMjD0UnVmMIItaOdT3: 'Pro plan (monthly)',
|
price_1J6KRuDMjD0UnVmMIItaOdT3: 'Pro plan (monthly)',
|
||||||
price_1J5q9VDMjD0UnVmMIQtVDSZ9: 'Pro plan (annually)',
|
price_1J5q9VDMjD0UnVmMIQtVDSZ9: 'Pro plan (annually)',
|
||||||
};
|
};
|
||||||
|
|
||||||
export { Prices, PriceIds };
|
export { Prices, PriceIds };
|
||||||
|
|||||||
@@ -5,10 +5,10 @@ import { loadStripe } from '@stripe/stripe-js';
|
|||||||
|
|
||||||
let stripePromise = null;
|
let stripePromise = null;
|
||||||
const getStripe = () => {
|
const getStripe = () => {
|
||||||
if (!stripePromise) {
|
if (!stripePromise) {
|
||||||
stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLIC_KEY);
|
stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLIC_KEY);
|
||||||
}
|
}
|
||||||
return stripePromise;
|
return stripePromise;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default getStripe;
|
export default getStripe;
|
||||||
|
|||||||
@@ -7,11 +7,11 @@ export const supabase = createClient(supabaseUrl, supabaseAnonKey);
|
|||||||
|
|
||||||
// Check if a user has a paid plan
|
// Check if a user has a paid plan
|
||||||
export const getSub = async () => {
|
export const getSub = async () => {
|
||||||
const { data: subscriptions, error } = await supabase
|
const { data: subscriptions, error } = await supabase
|
||||||
.from('subscriptions')
|
.from('subscriptions')
|
||||||
.select('paid_user, plan')
|
.select('paid_user, plan')
|
||||||
.single();
|
.single();
|
||||||
if (subscriptions) {
|
if (subscriptions) {
|
||||||
return subscriptions;
|
return subscriptions;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user