Initial commit

This commit is contained in:
Fergal Moran
2023-02-23 11:35:19 +00:00
committed by GitHub
commit e6ccc6b8f0
130 changed files with 12022 additions and 0 deletions

86
.eslintrc.json Normal file
View File

@@ -0,0 +1,86 @@
{
"env": {
"browser": true,
"es2021": true,
"node": true,
"cypress/globals": true
},
"extends": [
"next",
"next/core-web-vitals",
"plugin:@typescript-eslint/recommended-requiring-type-checking",
"plugin:sonarjs/recommended"
],
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module",
"project": [
"./tsconfig.json"
]
},
"parser": "@typescript-eslint/parser",
"ignorePatterns": [
"node_modules/*",
".next/*",
".out/*",
"!.prettierrc.js",
"types/*",
"next-env.d.ts"
],
"plugins": [
"@typescript-eslint",
"cypress",
"simple-import-sort",
"prettier",
"sonarjs"
],
"rules": {
"no-console": "off",
"react/no-unescaped-entities": "off",
"prettier/prettier": [
"error",
{
"semi": true,
"trailingComma": "es5",
"singleQuote": true,
"tabWidth": 2,
"useTabs": false,
"endOfLine": "auto",
"printWidth": 80,
"bracketSameLine": true,
"bracketSpacing": true
}
],
"react/prop-types": "off",
"react/react-in-jsx-scope": "off",
"react/jsx-filename-extension": [
2,
{
"extensions": [
".js",
".jsx",
".ts",
".tsx"
]
}
],
"@typescript-eslint/explicit-function-return-type": [
"warn",
{
"allowExpressions": true,
"allowTypedFunctionExpressions": true,
"allowHigherOrderFunctions": true,
"allowConciseArrowFunctionExpressionsStartingWithVoid": true
}
]
},
"settings": {
"import/resolver": {
"node": {
"paths": [
"."
]
}
}
}
}

4
.estlintignore Normal file
View File

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

38
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,38 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.

View File

@@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

7
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,7 @@
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "daily"

25
.github/workflows/ci.yml vendored Normal file
View File

@@ -0,0 +1,25 @@
name: SupaNexTail-CI
# https://docs.github.com/en/actions/reference/events-that-trigger-workflows
on: [deployment_status]
jobs:
e2e:
# only runs this job on successful deploy
if: github.event.deployment_status.state == 'success'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: pnpm/action-setup@v2.0.1
with:
version: 6.0.2
- uses: actions/setup-node@v2
with:
node-version: '14.x'
- name: Install dependencies
run: pnpm install
- name: Install Playwright
run: pnpx playwright install --with-deps
- name: Run Playwright tests
run: pnpm run test
env:
# This might depend on your test-runner/language binding
PLAYWRIGHT_TEST_BASE_URL: ${{ github.event.deployment_status.target_url }}

35
.gitignore vendored Normal file
View File

@@ -0,0 +1,35 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env.local
.env.development.local
.env.test.local
.env.production.local
.env
# vercel
.vercel

11
.prettierrc.json Normal file
View File

@@ -0,0 +1,11 @@
{
"semi": true,
"trailingComma": "es5",
"singleQuote": true,
"tabWidth": 2,
"useTabs": false,
"endOfLine": "auto",
"printWidth": 80,
"bracketSameLine": true,
"bracketSpacing": true
}

92
README.md Normal file
View File

@@ -0,0 +1,92 @@
## Welcome to SupaNexTail!
## Documentation 2.0
A new documentation is available here : https://doc.supanextail.dev/
## ![](https://lh4.googleusercontent.com/0qrns6BGMEh95de3BAE12YRRJceEACWdH09Yj6r7J5MswKG_R6zv7jcHEOUWFiWa7_2Yr6n6m0gSHg7iLa4lb-E0jEqZH6uJHJg3aNjbYO9LGWtCVV4dIi6BKKYUAMiFfvEOtefl)
### What is SupaNexTail?
SupaNexTail is a boilerplate to quickly create a MVP for a SaaS. Its built with Next.js, Supabase, TailwindCSS, and Stripe.
### How can I use it?
Simply follow the installation process. You need to have some knowledge with React and know how to set up a database on Supabase.
### Installation
#### SupaNexTail project
You'll need to fork this repository. I suggest to keep your repository sync with SupaNexTail, in order to get all future updates.
To do that, you'll have extended information on this page: https://docs.github.com/en/github/collaborating-with-pull-requests/working-with-forks/syncing-a-fork
#### Supabase
You need to create a Supabase project and make a SQL Query with the « SetupSupabaseSQL.sql » (You can copy the content and create a query in the Supabase dashboard)
1. Go to https://supabase.io/
2. Create an account and go to your dashboard
![](https://lh5.googleusercontent.com/8Ry_yqGbMp7-8obVn_62kE4pcyNf5u0FkWe_-Mhec1bHMoGJtCG18HUH2j8DwOyuplOpKoCgoMSOtFvTA3G4kkpDAITo_xI-RgkHo5Brh2aSgcqJjs21ZDsqXD9GxQORw4tn3sPH)
3. When your project is created, go to the SQL tab and create a new query
![](https://lh6.googleusercontent.com/kg7pBNhb9P49vYOMMVhsD4JiMxXSqRSLFnU_BEDTUH19CYUVEPRmaxg5WC3Ef_M2e5Y23DhV6__h9xFKn2GgXkltWBV4su-h8s8qdsP1GaAGkL1Q7cjqQ-TN57VfnGLD1HZOiCDp)
4. Paste the content of setupSupabaseSQL.sql and run the query
5. Your Supabase account is ready! Dont forget to retrieve your env variables in settings -> API
![](https://lh3.googleusercontent.com/FVmq_BSn4TB6ISx8B7WLa8biEm8kvcexqqzBMLmBtZt30NDz58Q7MV5umD0G_VccZ8LYmE_33z46Z-eLcR4Smg_mnKsU0ybC__tV__Jaet6T_YSJAcebbijvvyFUDLpBOTRty4pV)
#### Stripe
1. Create a Stripe account and a new project
2. Create Products (as many as you want). For example SupaNexTail have 6 prices, 3 monthly plans and 3 annually plans.
![](https://lh3.googleusercontent.com/G_MYkYXRoGJb2VhWf9GIP6J5Iis0F2gg1OMdHa6BY-3Rb3VUVGg-fUUOZX6wG1AjFLu-AvgOEml6MkivEZ_8WWaBSrp3OW8lDp7c00o1-TFAa-Z0vCcuL4YTUQcTCuVYQkBbA_Wx)
3. Youll need to retrieve the price ID from each product on Stripe and paste them on utils/priceListjs. Of course you can handle the prices differently if you want. Youll have to update the Pricing component too if you have a different amount of price ids.
4. Dont forget to retrieve the variables from Stripe
![](https://lh4.googleusercontent.com/ASiVfOBvKvD_vnKL7rOiVFlyiG6kR_95e6kQHyv7H3grlNt5PRGBhv_pmszrZeJmdF5sWRq41IV4QdwzcoMW0esb9l5pR_aVCCym5I5ksipGhmSCVVaB4gGNa17GUfFD-0DL7HuP) 5. You also need to configure the webhook section. Two events are needed
- customer.subscription.deleted
- checkout.session.completed
![](https://lh3.googleusercontent.com/zYnWdnmHFX2uIpi_UzSIvDvqOP_cO8WWfsL-iRwifqHbiGcUy1322Jj8hMAqfId5oXdHpY26lNg154ASTa5qkoEUtCTnN3JfKVA4WZWAboZVPiaPCp9i4ydV0yuWIfEmtu4NJkhP)
#### Sendgrid
Sendgrid is optional but youll need to configure it if you want to use the contact form.
![](https://lh4.googleusercontent.com/9EZ6EcWyc2EEILZJBs2xIEt_eesh2yTMz4WZsm2y8qYgQt-QdiODJfMriwkiBILM3S0iLAGNoN9JETgNp6DOpTIfKgChuY5yaoTBCEzIQwhSflYYJS6EGQrR5s9jRXMHOidTFXf8)
If you want to use the mailing list system, youll need to do a little bit more configuration. I wrote an article about it [here](https://dev.to/michael_webdev/create-a-mailing-list-with-sendgrid-and-next-js-41f7)
The backend is ready to use. You just have to add your SENDGRID_MAILING_ID env variables (more explanation about it in the article).
#### Misc.
- Supabase variables are mandatory, you can skip sendgrid as its just for the contact form. Stripe variables are needed if you want the subscription system.
- Dont forget to do an `npm install` locally
- You can launch the website locally with `npm run dev `
- If you want to setup the website with Vercel, you can install the Vercel CLI and simply enter the command `vercel`
Notes:
If you want to use Stripe, be sure to set up your webhooks in the dashboard. If you want to test it locally, install Stripe CLI and use this command line:
```
stripe listen --forward-to localhost:3000/api/stripe/webhook
```
The two event needed are:
- customer.subscription.deleted
- checkout.session.completed
### Known issues
- When a user sign up and you have the confirmation email enabled on Supabase, you dont have a message that tells you to check your email. It will be fixed with a new version of Supabase UI in a few days.

View File

@@ -0,0 +1,44 @@
---
title: Blog Post Example
description: A very small example of a blog post with a title and a description.
excerpt: Want to easily integrate a blog into your SaaS? Nothing could be simpler, with SupaNexTail, everything is already configured, you just have to write!
date: '2022-01-17'
coverImage: '/blog/covers/writeblog.png'
author:
name: 'Michael B'
picture: '/blog/author/Avatar1.svg'
ogImage:
url: '/blog/covers/writeblog.png'
---
import Button from '../components/ButtonMdx';
## A MDX Blog
This is a small example of a blog post.
Can you make it bigger?
I'm sure you can!
In this article, you can see a small button at the bottom. It's a React component!
This is possible thanks to the support of the .mdx format. Want to know more?
You can go [on this site](https://mdxjs.com/).
## Tailwind Typography
The blog posts are formatted with the Tailwind Typography plugin.
Here is the description from the official documentation:
_The official Tailwind CSS Typography plugin provides a set of prose classes you can use to add beautiful typographic defaults to any vanilla HTML you dont control, like HTML rendered from Markdown, or pulled from a CMS._
If you want to customize the typography,
you can change the classes in the post-body.tsx file.
To know more about Tailwind Typography,
you can [check the documentation](https://tailwindcss.com/docs/typography-plugin).
## And this is a MDX example
This button is a React component inside a MDX file. Crazy, right?
<Button>Click me</Button>
![alt text](/logo.svg 'Logo Website')

25
_posts/example-post.mdx Normal file
View File

@@ -0,0 +1,25 @@
---
title: A small example
description: A very small example
date: '2022-01-17'
author:
name: Michael B
picture: '/blog/author/Avatar1.svg'
coverImage: '/blog/covers/smallexample.png'
ogImage:
url: '/blog/Blog.png'
---
## A header
This is a small example of a blog post.
Can you make it bigger?
I'm sure you can!
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum id pulvinar velit, ut dignissim nibh. Nullam commodo pharetra sem. Donec in erat diam. Donec efficitur arcu sagittis felis vulputate feugiat in in enim. Sed ultricies tincidunt eros, et mattis erat hendrerit nec. Cras congue nec augue non imperdiet. Proin tincidunt luctus erat sit amet lobortis. Sed urna erat, ornare vel suscipit id, pharetra vel elit.
Suspendisse fringilla sit amet lorem nec semper. Curabitur aliquet ultrices rhoncus. Morbi facilisis, nibh eu dapibus maximus, lectus ante porttitor ante, quis dictum ipsum dui in velit. Pellentesque sapien enim, laoreet eu aliquam non, pellentesque in dui. Curabitur tristique magna lacus, ac pulvinar ex aliquam luctus. Etiam volutpat faucibus purus. Integer non euismod diam. Nam tincidunt vulputate nulla, a porta est accumsan non. Morbi et suscipit arcu. Cras placerat turpis eu mauris placerat, quis commodo ipsum iaculis. Vivamus turpis diam, posuere id ligula a, rhoncus volutpat risus. Fusce accumsan magna vitae odio molestie ultrices. Donec efficitur tellus ut leo posuere, vitae porttitor ipsum consectetur. Mauris tempor odio non laoreet luctus. Curabitur finibus vestibulum justo nec auctor.
## A second header
Integer egestas nulla non ultricies ultricies. Nulla et ultrices odio. Etiam eget tempor ipsum. Pellentesque sit amet commodo est. Praesent tincidunt faucibus orci. Proin at aliquet orci. Sed eget ultrices ligula. Maecenas bibendum, leo quis suscipit posuere, tellus libero maximus nibh, vel bibendum eros leo non nulla. Proin quis gravida leo. Cras efficitur mi congue lacus viverra placerat quis in leo. Nam faucibus est sollicitudin, suscipit erat ac, ultrices metus. Fusce sagittis vulputate turpis eget laoreet.

25
_posts/old-example.mdx Normal file
View File

@@ -0,0 +1,25 @@
---
title: An old blog post
description: A very small example
date: '2022-01-17'
author:
name: Michael B
picture: '/blog/author/Avatar1.svg'
coverImage: '/cardServer.svg'
ogImage:
url: '/blog/Blog.png'
---
## A header
This is a small example of a blog post.
Can you make it bigger?
I'm sure you can!
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum id pulvinar velit, ut dignissim nibh. Nullam commodo pharetra sem. Donec in erat diam. Donec efficitur arcu sagittis felis vulputate feugiat in in enim. Sed ultricies tincidunt eros, et mattis erat hendrerit nec. Cras congue nec augue non imperdiet. Proin tincidunt luctus erat sit amet lobortis. Sed urna erat, ornare vel suscipit id, pharetra vel elit.
Suspendisse fringilla sit amet lorem nec semper. Curabitur aliquet ultrices rhoncus. Morbi facilisis, nibh eu dapibus maximus, lectus ante porttitor ante, quis dictum ipsum dui in velit. Pellentesque sapien enim, laoreet eu aliquam non, pellentesque in dui. Curabitur tristique magna lacus, ac pulvinar ex aliquam luctus. Etiam volutpat faucibus purus. Integer non euismod diam. Nam tincidunt vulputate nulla, a porta est accumsan non. Morbi et suscipit arcu. Cras placerat turpis eu mauris placerat, quis commodo ipsum iaculis. Vivamus turpis diam, posuere id ligula a, rhoncus volutpat risus. Fusce accumsan magna vitae odio molestie ultrices. Donec efficitur tellus ut leo posuere, vitae porttitor ipsum consectetur. Mauris tempor odio non laoreet luctus. Curabitur finibus vestibulum justo nec auctor.
## A second header
Integer egestas nulla non ultricies ultricies. Nulla et ultrices odio. Etiam eget tempor ipsum. Pellentesque sit amet commodo est. Praesent tincidunt faucibus orci. Proin at aliquet orci. Sed eget ultrices ligula. Maecenas bibendum, leo quis suscipit posuere, tellus libero maximus nibh, vel bibendum eros leo non nulla. Proin quis gravida leo. Cras efficitur mi congue lacus viverra placerat quis in leo. Nam faucibus est sollicitudin, suscipit erat ac, ultrices metus. Fusce sagittis vulputate turpis eget laoreet.

24
components/AuthText.tsx Normal file
View File

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

127
components/Avatar.tsx Normal file
View File

@@ -0,0 +1,127 @@
/*
As a user, you can upload an avatar in your dashboard. This component will handle
the upload.
You can tweak the max size, line 47
*/
import React, { useEffect, useState } from 'react';
import Image from 'next/image';
import { supabase } from 'utils/supabaseClient';
type AvatarProperties = {
url: string;
size: number;
onUpload: (filePath: string) => void;
};
const customImgLoader = ({ src }: { src: string }): string => {
return `${src}`;
};
const Avatar = ({ url, size, onUpload }: AvatarProperties): JSX.Element => {
const [avatarUrl, setAvatarUrl] = useState('');
const [uploading, setUploading] = useState(false);
useEffect(() => {
if (url) void downloadImage(url);
}, [url]);
async function downloadImage(path: string): Promise<void> {
try {
const { data, error } = await supabase.storage
.from('avatars')
.download(path);
if (error) {
throw error;
}
if (data) {
const url = URL.createObjectURL(data);
setAvatarUrl(url);
}
} catch (error: unknown) {
if (error instanceof Error) {
console.log('Error downloading image:', error.message);
}
}
}
async function uploadAvatar(
event: React.ChangeEvent<HTMLInputElement>
): Promise<void> {
try {
setUploading(true);
if (!event.target.files || event.target.files.length === 0) {
throw new Error('You must select an image to upload.');
}
const file = event.target.files[0];
const fileExtension = file.name.split('.').pop();
const fileName = fileExtension ? `${Math.random()}.${fileExtension}` : '';
const filePath = `${fileName}`;
if (event.target.files[0].size > 150_000) {
alert('File is too big!');
event.target.value = '';
setUploading(false);
return;
}
const { error: uploadError } = await supabase.storage
.from('avatars')
.upload(filePath, file);
if (uploadError) {
throw uploadError;
}
onUpload(filePath);
} catch (error: unknown) {
if (error instanceof Error) {
alert(error.message);
}
} finally {
setUploading(false);
}
}
return (
<div className="m-auto mb-5">
{avatarUrl ? (
<div className="flex justify-center w-full">
<Image
loader={customImgLoader} //Using custom loader because of this issue https://github.com/vercel/next.js/discussions/19732
src={avatarUrl}
height={100}
width={100}
alt="Avatar"
className="rounded-full w-28 h-28 avatar"
/>
</div>
) : (
<div className="rounded-full w-28 h-28 avatar" />
)}
<div style={{ width: size }}>
<label
className="mt-2 text-xs text-center cursor-pointer btn btn-primary btn-sm"
htmlFor="single">
{uploading ? 'Uploading ...' : 'Update my avatar'}
</label>
<input
style={{
visibility: 'hidden',
position: 'absolute',
}}
type="file"
id="single"
accept="image/*"
onChange={uploadAvatar}
disabled={uploading}
/>
</div>
</div>
);
};
export default Avatar;

5
components/ButtonMdx.tsx Normal file
View File

@@ -0,0 +1,5 @@
// This button is used as an example of how to use the MDX button component
export default function Button({ children }: any): JSX.Element {
return <button className="btn-primary btn">{children}</button>;
}

View File

@@ -0,0 +1,72 @@
import CardLanding from 'components/UI/CardLanding';
import cardAuth from 'public/landing/auth.svg';
import cardBlog from 'public/landing/blog.svg';
import cardFee from 'public/landing/lifetime.svg';
import cardPage from 'public/landing/page.svg';
import cardResponsive from 'public/landing/responsive.svg';
import cardServer from 'public/landing/backend.svg';
import cardStripe from 'public/landing/stripe.svg';
import cardTS from 'public/landing/TS.svg';
import cardTheme from 'public/landing/theme.svg';
const CardsLanding = (): JSX.Element => (
<div className="mt-14">
<h2 className="text-4xl font-bold tracking-wide text-center uppercase">
We've got you covered
</h2>
<p className="max-w-md m-auto text-center">
Dont waste your time and reinvent the wheel, we have provided you with a
maximum of features so that you only have one goal, to make your SaaS a
reality.
</p>
<div className="flex flex-wrap justify-center mt-10">
<CardLanding
image={cardPage as string}
text="7 pages fully designed and easily customizable"
title="Templates"
/>
<CardLanding
image={cardServer as string}
text="Integrated backend already setup with Next.js API Routes"
title="Backend"
/>
<CardLanding
image={cardAuth as string}
text="Auth and user management with Supabase"
title="Auth"
/>
<CardLanding
image={cardBlog as string}
text="An easy to use blog system with MDX support"
title="Blog"
/>
<CardLanding
image={cardResponsive as string}
text="Mobile ready, fully responsive and customizable with Tailwind CSS"
title="Responsive"
/>
<CardLanding
image={cardTheme as string}
text="Custom themes available and easily switch to dark mode"
title="Themes"
/>
<CardLanding
image={cardTS as string}
text="The entire code base is in Typescript, with ESLint and Prettier already configured."
title="Typescript"
/>
<CardLanding
image={cardStripe as string}
text="Stripe integration. Fully functional subscription system"
title="Payment"
/>
<CardLanding
image={cardFee as string}
text="One-time fee. No subscription, youll have access to all the updates"
title="Lifetime access"
/>
</div>
</div>
);
export default CardsLanding;

119
components/Contact.tsx Normal file
View File

@@ -0,0 +1,119 @@
/*
This is the contact component. It will allow your user to send you an email.
We use Sendgrid by default and you'll need to check /api/sendgrid.js and don't forget to add
the environment variables.
If you want to change the email provider, don't hesitate to create a new api route and change
the axios.post here, line 18.
*/
import axios, { AxiosError } from 'axios';
import { toast } from 'react-toastify';
const sendEmail = async (): Promise<void> => {
const name = (document.querySelector('#name') as HTMLInputElement).value;
const email = (document.querySelector('#email') as HTMLInputElement).value;
const message = (document.querySelector('#message') as HTMLInputElement)
.value;
interface EmailData {
success: boolean;
message: string;
}
interface ErrorAxios {
data: {
error: string;
success: boolean;
};
}
if (name && email && message) {
try {
const mail = await axios.post<EmailData>('/api/sendgrid', {
email,
name,
message,
});
if (mail.data.success === true) {
toast.success(mail.data.message);
(document.querySelector('#name') as HTMLInputElement).value = '';
(document.querySelector('#email') as HTMLInputElement).value = '';
(document.querySelector('#message') as HTMLInputElement).value = '';
}
} catch (error: unknown) {
const errorChecked = error as AxiosError;
const errorMessage = errorChecked.response as ErrorAxios;
toast.error(errorMessage.data.error);
}
} else {
toast.info('Please fill all the fields ', {
position: 'top-center',
autoClose: 2000,
hideProgressBar: true,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
progress: undefined,
});
}
};
const Contact = (): JSX.Element => {
return (
<div className="max-w-xl px-5 py-10 m-auto">
<div>
<div className="flex justify-center">
<h2 className="mt-0 mb-5 text-3xl font-bold text-center sm:text-4xl font-title">
Contact
</h2>
</div>
<p className="m-auto text-center">
Do you have a question about SupaNexTail? A cool feature you'd like us
to integrate? A bug to report? Don't hesitate!
</p>
</div>
<form className="grid grid-cols-1 gap-4 p-5 m-auto mt-5 md:grid-cols-2">
<div className="flex flex-col max-w-xs">
<p className="mb-4 font-light text-left">Your Name</p>
<input
id="name"
name="name"
placeholder="Enter your name"
className="input input-primary input-bordered"
/>
</div>
<div className="flex flex-col max-w-xs mb-3">
<p className="mb-4 font-light text-left">Your email</p>
<input
id="email"
name="email"
placeholder="Enter your email adress"
className="input input-primary input-bordered"
/>
</div>
<div className="flex flex-col col-span-full w-fulll">
<p className="mb-4 font-light text-left">Message</p>
<textarea
id="message"
name="message"
placeholder="Enter your message here..."
rows={5}
className="w-full h-32 pt-2 resize-none input input-primary input-bordered"
/>
</div>
<button
type="button"
className="btn btn-primary btn-sm"
onClick={(event) => {
event.preventDefault();
void sendEmail();
}}>
Submit{' '}
</button>
</form>
</div>
);
};
export default Contact;

154
components/Dashboard.tsx Normal file
View File

@@ -0,0 +1,154 @@
/*
This is the Dashboard component. If a user is logged in, he/she can update his/her name and website.
You can add as many elements as you want. Don't forget to update your getProfile() and updateProfile()
function with your new elements.
It also show you the current subscription plan
*/
import { useEffect, useState } from 'react';
import Avatar from './Avatar';
import Image from 'next/image';
import PaymentModal from './PaymentModal';
import Plan from 'public/plan.svg';
import { Session } from '@supabase/gotrue-js';
import { supabase } from '../utils/supabaseClient';
import { toast } from 'react-toastify';
import { useRouter } from 'next/router';
type DashboardProperties = {
profile: { username: string; website: string; avatar_url: string };
session: Session;
planName: string;
};
const Dashboard = ({
profile,
session,
planName,
}: DashboardProperties): JSX.Element => {
const router = useRouter();
const [loading, setLoading] = useState(false);
const [username, setUsername] = useState(profile?.username || '');
const [website, setWebsite] = useState(profile?.website || '');
const [avatar_url, setAvatarUrl] = useState(profile?.avatar_url || '');
const [payment, setPayment] = useState(false);
useEffect(() => {
if (router.query.session_id && router.query.session_id !== 'canceled') {
setPayment(true);
}
}, [router.query.session_id]);
async function updateProfile({
username,
website,
avatar_url,
}: {
username: string;
website: string;
avatar_url: string;
}): Promise<void> {
try {
setLoading(true);
const user = supabase.auth.user();
const updates = {
id: user?.id,
username,
website,
avatar_url,
updated_at: new Date(),
};
const { error } = await supabase.from('profiles').upsert(updates, {
returning: 'minimal', // Don't return the value after inserting
});
if (error) {
throw error;
}
} catch (error: unknown) {
if (error instanceof Error) {
alert(error.message);
}
} finally {
setLoading(false);
toast.success('Your profile has been updated');
}
}
return (
<div className="flex flex-col w-full max-w-xl px-5 py-10 m-auto text-left">
<div className="flex flex-col justify-center w-full max-w-sm p-5 m-auto">
<h1 className="mb-10 text-4xl font-bold text-center md:text-5xl font-title">
Dashboard
</h1>
<Avatar
url={avatar_url}
size={150}
onUpload={(url) => {
setAvatarUrl(url);
void updateProfile({ username, website, avatar_url: url });
}}
/>
<div className="flex flex-col mb-5">
<label htmlFor="email" className="my-auto mb-2 text-sm">
Email
</label>
<input
className="flex-1 input input-primary input-bordered input-sm text-base-100"
id="email"
type="text"
value={session.user?.email}
disabled
/>
</div>
<div className="flex flex-col mb-5">
<label htmlFor="username" className="my-auto mb-2 text-sm">
Name
</label>
<input
className="flex-1 input input-primary input-bordered input-sm"
id="username"
type="text"
value={username || ''}
onChange={(event) => setUsername(event.target.value)}
/>
</div>
<div className="flex flex-col mb-5">
<label htmlFor="website" className="my-auto mb-2 text-sm">
Website
</label>
<input
className="flex-1 input input-primary input-bordered input-sm"
id="website"
type="website"
value={website || ''}
onChange={(event) => setWebsite(event.target.value)}
/>
</div>
<div className="m-auto">
<button
className="btn btn-primary btn-sm"
onClick={() => updateProfile({ username, website, avatar_url })}
disabled={loading}>
{loading ? 'Loading ...' : 'Update My Profile'}
</button>
</div>
</div>
<div className="flex flex-row flex-wrap w-full max-w-xl p-5 m-auto my-5 border-2 shadow-lg bordered border-primary">
<Image src={Plan as string} alt="credit card" />
<div className="flex flex-col m-auto">
<h2>Your current plan</h2>
<p className="">{planName}</p>
</div>
</div>
<PaymentModal open={payment} setPayment={setPayment} />
</div>
);
};
export default Dashboard;

28
components/Footer.tsx Normal file
View File

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

73
components/Landing.tsx Normal file
View File

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

63
components/Layout.tsx Normal file
View File

@@ -0,0 +1,63 @@
/* This is your Layout. It will wrap your content on every page of the website.
You'll have:
* The NavBar
* The Content, generally a component
* The Footer
You can change it as you want with new components.
You also have the head component containing all the favicon for different platforms.
The images are in the public folder.
*/
import 'react-toastify/dist/ReactToastify.css';
import Footer from './Footer';
import Head from 'next/head';
import Nav from './Nav';
import { ToastContainer } from 'react-toastify';
import { useAuth } from 'utils/AuthContext';
type LayoutProperties = {
children: JSX.Element;
};
const Layout = ({ children }: LayoutProperties): JSX.Element => {
const { user, signOut } = useAuth();
return (
<div className="w-full min-h-screen m-auto bg-base-100 text-base-content font-body">
<Head>
<link
rel="apple-touch-icon"
sizes="180x180"
href="/apple-touch-icon.png"
/>
<link
rel="icon"
type="image/png"
sizes="32x32"
href="/favicon-32x32.png"
/>
<link
rel="icon"
type="image/png"
sizes="16x16"
href="/favicon-16x16.png"
/>
<link rel="manifest" href="/site.webmanifest" />
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5" />
<meta name="msapplication-TileColor" content="#da532c" />
<meta name="theme-color" content="#ffffff" />
</Head>
<div className="flex flex-col min-h-screen p-5 mx-auto max-w-7xl">
<Nav user={user} signOut={signOut} />
<main className="flex-1">{children}</main>
<ToastContainer position="bottom-center" />
<Footer />
</div>
</div>
);
};
export default Layout;

View File

@@ -0,0 +1,98 @@
/*
This is the form component to register an email adress to your mailing list.
This is just the frontend, and the email will be send to our backend API (/api/mailingList)
*/
import axios, { AxiosError } from 'axios';
import Image from 'next/image';
import Mailing from 'public/landing/mailing.svg';
import { toast } from 'react-toastify';
import { useState } from 'react';
const MailingList = (): JSX.Element => {
const [mail, setMail] = useState('');
const [loading, setLoading] = useState(false);
const [valid, setValid] = useState(true);
interface ErrorAxios {
data: {
error: string;
success: boolean;
};
}
const validateEmail = (): void => {
// Regex patern for email validation
const regex =
/^(([^\s"(),.:;<>@[\\\]]+(\.[^\s"(),.:;<>@[\\\]]+)*)|(".+"))@((\[(?:\d{1,3}\.){3}\d{1,3}])|(([\dA-Za-z\-]+\.)+[A-Za-z]{2,}))$/;
if (regex.test(mail)) {
// this is a valid email address
void subscribe();
setValid(true);
} else {
// invalid email.
toast.error('Your email is invalid');
setValid(false);
}
};
const subscribe = async (): Promise<void> => {
setLoading(true);
try {
const data = await axios.put('api/mailingList', {
mail,
});
if (data.status === 200) {
toast.success('You have been added to our mailing list. Welcome 👋');
setLoading(false);
setMail('');
}
} catch (error: unknown) {
const errorChecked = error as AxiosError;
const errorMessage = errorChecked.response as ErrorAxios;
if (errorMessage.data.error) {
toast.error(errorMessage.data.error);
setLoading(false);
}
}
};
return (
<div className="flex flex-col m-auto my-10 mt-24">
<h2 className="text-3xl font-bold text-center uppercase md:text-4xl font-title">
Stay Tuned
</h2>
<Image src={Mailing as string} alt="Mail" />
<label className="label">
<p className="max-w-md m-auto text-center">
Want to be the first to know when SupaNexTail launches and get an
exclusive discount? Sign up for the newsletter!
</p>
</label>
<div className="m-auto mt-5">
<input
onChange={(event) => {
setMail(event.target.value);
}}
type="email"
placeholder="Your email"
className={`input input-primary input-bordered ${
valid ? '' : 'input-error'
}`}
/>
<button
onClick={validateEmail}
className={`btn ml-3 ${
loading ? 'btn-disabled loading' : 'btn-primary'
}`}>
I'm in!
</button>
</div>
</div>
);
};
export default MailingList;

102
components/Nav.tsx Normal file
View File

@@ -0,0 +1,102 @@
/*
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';
import { User } from '@supabase/gotrue-js';
type NavProperties = {
user: User | null | undefined;
signOut: () => void;
};
const Nav = ({ user, signOut }: NavProperties): JSX.Element => {
// Modify you menu directly here
const NavMenu = (
<>
{user && (
<>
<Link href="/dashboard">
<a className="nav-btn">Dashboard</a>
</Link>
<Link href="/members-only">
<a className="nav-btn">Premium user </a>
</Link>
</>
)}
<Link href="/blog">
<a id="blog" className="nav-btn">
Blog
</a>
</Link>
<Link href="/pricing">
<a id="pricing" className="nav-btn">
Pricing
</a>
</Link>
<Link href="/contact">
<a id="contact" className="nav-btn">
Contact Us
</a>
</Link>
{user ? (
<button
id="logOutBtn"
className="text-xs btn btn-xs"
onClick={() => signOut()}>
<LogOut size={12} className="mr-2" />
Logout
</button>
) : (
<>
<Link href="/login">
<a id="login" className="nav-btn">
Login
</a>
</Link>
<Link href="/signup">
<a
id="signup"
className="font-normal normal-case btn btn-sm btn-primary font-body">
Sign Up
</a>
</Link>
</>
)}
</>
);
return (
<nav className="w-full mb-2 navbar">
<Link href="/">
<a>
<Image src={Logo as string} alt="SupaNexTail Logo" />
</a>
</Link>
<div className="flex-col hidden ml-auto text-sm text-center lg:flex lg:flex-row lg:space-x-10 font-body">
{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="w-24 mt-3 space-y-3 text-center menu dropdown-content">
{NavMenu}
</div>
</div>
</div>
</nav>
);
};
export default Nav;

View File

@@ -0,0 +1,81 @@
import { Dialog, Transition } from '@headlessui/react';
import { Fragment } from 'react';
type PaymentModalProperties = {
open: boolean;
setPayment: (argument0: boolean) => void;
};
const PaymentModal = ({
open,
setPayment,
}: PaymentModalProperties): JSX.Element => {
function closeModal(): void {
setPayment(false);
}
return (
<>
<Transition appear show={open} as={Fragment}>
<Dialog
as="div"
className="fixed inset-0 z-10 overflow-y-auto bg-gray-500 bg-opacity-50"
onClose={closeModal}>
<div className="min-h-screen px-4 text-center">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0">
<Dialog.Overlay className="fixed inset-0" />
</Transition.Child>
{/* This element is to trick the browser into centering the modal contents. */}
<span
className="inline-block h-screen align-middle"
aria-hidden="true">
&#8203;
</span>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95">
<div className="inline-block w-full max-w-lg p-8 my-8 overflow-hidden text-left align-middle transition-all transform border-2 shadow-xl rounded-2xl bg-base-100 text-base-content border-accent-focus">
<Dialog.Title
as="h3"
className="mb-5 text-2xl font-bold leading-6 text-center">
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">
<button
type="button"
className="flex m-auto btn btn-accent"
onClick={closeModal}>
Got it, thanks!
</button>
</div>
</div>
</Transition.Child>
</div>
</Dialog>
</Transition>
</>
);
};
export default PaymentModal;

170
components/Pricing.tsx Normal file
View File

@@ -0,0 +1,170 @@
/*
This is the pricing component.
You can switch between flat payment or subscription by setting the flat variable.
----------
Dont forget to create your customer portal on Stripe
https://dashboard.stripe.com/test/settings/billing/portal
*/
import { getSub, supabase } from 'utils/supabaseClient';
import { useEffect, useState } from 'react';
import axios from 'axios';
import { definitions } from 'types/database/index';
import router from 'next/router';
import { useAuth } from 'utils/AuthContext';
const Pricing = (): JSX.Element => {
const { user, session } = useAuth();
const [customerId, setCustomerId] = useState<undefined | string>();
const [sub, setSub] = useState<definitions['subscriptions'] | undefined>();
useEffect(() => {
if (user) {
const subFunction = async (): Promise<void> => {
const sub = await getSub();
if (sub) {
setSub(sub);
}
const subSupa = await supabase
.from<definitions['subscriptions']>('subscriptions')
.select(`customer_id`)
.eq('id', user.id)
.single();
setCustomerId(subSupa.data?.customer_id);
};
void subFunction();
}
}, [user]);
interface Test {
url: string;
}
const handleSubmit = async (
event: React.SyntheticEvent<HTMLButtonElement>,
priceId: string
): Promise<void> => {
event.preventDefault();
// Create a Checkout Session. This will redirect the user to the Stripe website for the payment.
if (user && session) {
if (sub) {
const stripeInfo = await axios.post<Test>(
'/api/stripe/customerPortal',
{
customerId,
}
);
void router.push(stripeInfo.data.url);
} else {
const stripeInfo = await axios.post<Test>('/api/stripe/checkout', {
priceId,
email: user.email,
customerId,
userId: user.id,
tokenId: session.access_token,
pay_mode: 'subscription',
});
void router.push(stripeInfo.data.url);
}
}
};
return (
<div>
<div className="container px-6 py-8 mx-auto text-base-100">
<h2 className="mt-0 mb-5 text-3xl font-bold text-center sm:text-4xl font-title text-base-content">
Pricing
</h2>
<div className="flex flex-col items-center justify-center mt-16 space-y-8 lg:flex-row lg:items-stretch lg:-mx-4 lg:space-y-0">
<div className="flex flex-col w-full max-w-sm p-8 space-y-8 text-center rounded-lg shadow-lg lg:mx-4 bg-base-100 text-base-content">
<div className="flex-shrink-0">
<h3 className="inline-flex items-center badge-neutral badge badge-lg bg-base-content text-base-100">
Casual
</h3>
</div>
<div className="flex-shrink-0">
<span className="pt-2 text-4xl font-bold uppercase">FREE</span>
</div>
<ul className="flex-1 space-y-4 text-base-content">
<li>Up to 10 projects</li>
<li>Up to 20 collaborators</li>
<li>10Gb of storage</li>
</ul>
<button className="btn btn-primary">Start for free</button>
</div>
<div className="flex flex-col w-full max-w-sm p-8 space-y-8 text-center rounded-lg shadow-lg lg:mx-4 bg-base-100 text-base-content">
<div className="flex-shrink-0">
<h3 className="inline-flex items-center badge-neutral badge badge-lg bg-base-content text-base-100">
Professional
</h3>
</div>
<div className="flex-shrink-0">
<span className="pt-2 text-4xl font-bold uppercase">$4.90</span>
<span>/month</span>
</div>
<ul className="flex-1 space-y-4 text-base-content">
<li>Up to 30 projects</li>
<li>Up to 25 collaborators</li>
<li>100Gb of storage</li>
<li>Real-time collaborations</li>
</ul>
{user ? (
<button
className="btn btn-primary"
onClick={(event) => {
void handleSubmit(event, 'price_1JtHhaDMjD0UnVmM5uCyyrWn');
}}>
{sub ? 'Handle subscription' : 'Subscribe'}
</button>
) : (
<button
className="btn btn-primary"
onClick={() => router.push('/login')}>
Log in
</button>
)}
</div>
<div className="flex flex-col w-full max-w-sm p-8 space-y-8 text-center rounded-lg shadow-lg lg:mx-4 bg-base-100 text-base-content">
<div className="flex-shrink-0">
<h3 className="inline-flex items-center badge-neutral badge badge-lg bg-base-content text-base-100">
Business
</h3>
</div>
<div className="flex-shrink-0">
<span className="pt-2 text-4xl font-bold uppercase">$24.90</span>
<span>/month</span>
</div>
<ul className="flex-1 space-y-4 text-base-content">
<li>Up to 60 projects</li>
<li>Up to 200 collaborators</li>
<li>1Tb of storage</li>
<li>Real-time collaborations</li>
</ul>
{user ? (
<button
className="btn btn-primary"
onClick={(event) => {
void handleSubmit(event, 'price_1JtHhaDMjD0UnVmM5uCyyrWn');
}}>
{sub ? 'Handle subscription' : 'Subscribe'}
</button>
) : (
<button
className="btn btn-primary"
onClick={() => router.push('/login')}>
Log in
</button>
)}
</div>
</div>
</div>
</div>
);
};
export default Pricing;

View File

@@ -0,0 +1,159 @@
const PrivacyPolicy = (): JSX.Element => (
<div className="max-w-xl py-10 m-auto text-left">
<h1 className="text-center">
Privacy Policy for {process.env.NEXT_PUBLIC_TITLE}
</h1>
<p>
At {process.env.NEXT_PUBLIC_TITLE}, accessible from
https://www.supanextail.dev, one of our main priorities is the privacy of
our visitors. This Privacy Policy document contains types of information
that is collected and recorded by
{process.env.NEXT_PUBLIC_TITLE} and how we use it.
</p>
<p>
If you have additional questions or require more information about our
Privacy Policy, do not hesitate to contact us.
</p>
<h2>General Data Protection Regulation (GDPR)</h2>
<p>We are a Data Controller of your information.</p>
<p>
{process.env.NEXT_PUBLIC_TITLE} legal basis for collecting and using the
personal information described in this Privacy Policy depends on the
Personal Information we collect and the specific context in which we
collect the information:
</p>
<ul>
<li>
{process.env.NEXT_PUBLIC_TITLE} needs to perform a contract with you
</li>
<li>
You have given {process.env.NEXT_PUBLIC_TITLE} permission to do so
</li>
<li>
Processing your personal information is in{' '}
{process.env.NEXT_PUBLIC_TITLE} legitimate interests
</li>
<li>{process.env.NEXT_PUBLIC_TITLE} needs to comply with the law</li>
</ul>
<p>
{process.env.NEXT_PUBLIC_TITLE} will retain your personal information only
for as long as is necessary for the purposes set out in this Privacy
Policy. We will retain and use your information to the extent necessary to
comply with our legal obligations, resolve disputes, and enforce our
policies.
</p>
<p>
If you are a resident of the European Economic Area (EEA), you have
certain data protection rights. If you wish to be informed what Personal
Information we hold about you and if you want it to be removed from our
systems, please contact us.
</p>
<p>
In certain circumstances, you have the following data protection rights:
</p>
<ul>
<li>
The right to access, update or to delete the information we have on you.
</li>
<li>The right of rectification.</li>
<li>The right to object.</li>
<li>The right of restriction.</li>
<li>The right to data portability</li>
<li>The right to withdraw consent</li>
</ul>
<h2>Log Files</h2>
<p>
{process.env.NEXT_PUBLIC_TITLE} follows a standard procedure of using log
files. These files log visitors when they visit websites. All hosting
companies do this and a part of hosting services' analytics. The
information collected by log files include internet protocol (IP)
addresses, browser type, Internet Service Provider (ISP), date and time
stamp, referring/exit pages, and possibly the number of clicks. These are
not linked to any information that is personally identifiable. The purpose
of the information is for analyzing trends, administering the site,
tracking users' movement on the website, and gathering demographic
information.
</p>
<h2>Privacy Policies</h2>
<p>
You may consult this list to find the Privacy Policy for each of the
advertising partners of {process.env.NEXT_PUBLIC_TITLE}.
</p>
<p>
Third-party ad servers or ad networks uses technologies like cookies,
JavaScript, or Web Beacons that are used in their respective
advertisements and links that appear on {process.env.NEXT_PUBLIC_TITLE},
which are sent directly to users' browser. They automatically receive your
IP address when this occurs. These technologies are used to measure the
effectiveness of their advertising campaigns and/or to personalize the
advertising content that you see on websites that you visit.
</p>
<p>
Note that {process.env.NEXT_PUBLIC_TITLE} has no access to or control over
these cookies that are used by third-party advertisers.
</p>
<h2>Third Party Privacy Policies</h2>
<p>
{process.env.NEXT_PUBLIC_TITLE}'s Privacy Policy does not apply to other
advertisers or websites. Thus, we are advising you to consult the
respective Privacy Policies of these third-party ad servers for more
detailed information. It may include their practices and instructions
about how to opt-out of certain options.{' '}
</p>
<p>
You can choose to disable cookies through your individual browser options.
To know more detailed information about cookie management with specific
web browsers, it can be found at the browsers' respective websites.
</p>
<h2>Children's Information</h2>
<p>
Another part of our priority is adding protection for children while using
the internet. We encourage parents and guardians to observe, participate
in, and/or monitor and guide their online activity.
</p>
<p>
{process.env.NEXT_PUBLIC_TITLE} does not knowingly collect any Personal
Identifiable Information from children under the age of 13. If you think
that your child provided this kind of information on our website, we
strongly encourage you to contact us immediately and we will do our best
efforts to promptly remove such information from our records.
</p>
<h2>Online Privacy Policy Only</h2>
<p>
Our Privacy Policy applies only to our online activities and is valid for
visitors to our website with regards to the information that they shared
and/or collect in {process.env.NEXT_PUBLIC_TITLE}. This policy is not
applicable to any information collected offline or via channels other than
this website.
</p>
<h2>Consent</h2>
<p>
By using our website, you hereby consent to our Privacy Policy and agree
to its terms.
</p>
</div>
);
export default PrivacyPolicy;

43
components/SignUp.tsx Normal file
View File

@@ -0,0 +1,43 @@
/*
This is the Auth component. It will allow your user to login.
By default, it is available with the auth.js page, but you can use it everywhere you want!
CONFIGURE THE AUTH COMPONENT LINE 30
You can select your auth providers, or just keep the email/password. You can
check the providers available here: https://supabase.io/docs/guides/auth
*/
import SignUpPanel from './UI/SignUpPanel';
import { SupabaseClient } from '@supabase/supabase-js';
import { supabase } from 'utils/supabaseClient';
import { useAuth } from 'utils/AuthContext';
type ContainerProperties = {
children: JSX.Element;
supabaseClient: SupabaseClient;
};
const Container = ({ children }: ContainerProperties): JSX.Element => {
const { user, signOut } = useAuth();
if (user)
return (
<div className="order-first w-80 md:w-96 lg:order-last">
<p>Hello {user.email}! 👋 You are already logged in</p>
<button className="btn btn-primary" onClick={() => signOut()}>
Sign out
</button>
</div>
);
return children;
};
const AuthComponent = (): JSX.Element => {
const { signUp, signIn } = useAuth();
return (
<Container supabaseClient={supabase}>
<SignUpPanel signUp={signUp} signIn={signIn} />
</Container>
);
};
export default AuthComponent;

235
components/Terms.tsx Normal file
View File

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

View File

@@ -0,0 +1,35 @@
/*
This card is used on the landing page
*/
import Image from 'next/image';
type CardLandingProperties = {
image: string;
title: string;
text: string;
};
const CardLanding = ({
image,
title,
text,
}: CardLandingProperties): JSX.Element => {
return (
<div className="flex h-48 p-5 mb-5 w-80 sm:ml-5 bg-base-100">
<div>
<div className="flex w-12 h-12 border rounded-full bg-neutral-content">
<div className="flex m-auto">
<Image src={image} width={24} height={24} alt={`${title} logo`} />
</div>
</div>
</div>
<div className="ml-8">
<p className="text-lg font-semibold font-title">{title}</p>
<p className="mt-3">{text}</p>
</div>
</div>
);
};
export default CardLanding;

View File

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

184
components/UI/Login.tsx Normal file
View File

@@ -0,0 +1,184 @@
import { ApiError, Session, UserCredentials } from '@supabase/gotrue-js';
import { IoLogoGoogle } from 'react-icons/io';
import router from 'next/router';
import { toast } from 'react-toastify';
import { useState } from 'react';
type LoginProperties = {
resetPassword: (data: string) => Promise<{
data: {} | null;
error: ApiError | null;
}>;
signIn: (data: UserCredentials) => Promise<{
user: Session['user'] | null;
session: Session | null;
error: ApiError | null;
}>;
};
const Login = ({ resetPassword, signIn }: LoginProperties): JSX.Element => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [forgot, setForgot] = useState(false);
const resetPasswordLogin = async (): Promise<void> => {
const result = await resetPassword(email);
if (result.error) {
toast.error(result.error.message);
} else if (result.data) {
toast.success(
'A password reset email has been sent to you, watch your mailbox!'
);
}
};
const login = async (
event: React.SyntheticEvent<HTMLButtonElement>
): Promise<void> => {
event.preventDefault();
// Handle the login. Go to the homepage if success or display an error.
const result = await signIn({
email,
password,
});
if (result.error) {
toast.error(result.error.message);
} else if (result.user) {
void router.push('/');
}
};
return (
<div className="max-w-sm p-10 rounded-md shadow-md md:flex-1 bg-base-100 text-base-content font-body">
{!forgot && (
<>
<h3 className="my-4 text-2xl font-semibold font-title">
Account Login
</h3>
<form action="#" className="flex flex-col space-y-5">
<div className="flex flex-col space-y-1">
<label htmlFor="email" className="text-sm">
Email address
</label>
<input
type="email"
id="email"
autoFocus
className="input input-primary input-bordered input-sm"
value={email}
onChange={(event) => {
setEmail(event.target.value);
}}
/>
</div>
<div className="flex flex-col space-y-1">
<div className="flex items-center justify-between">
<label htmlFor="password" className="text-sm">
Password
</label>
<button
onClick={() => {
setForgot(true);
}}
className="text-sm text-blue-600 hover:underline focus:text-blue-800">
Forgot Password?
</button>
</div>
<input
type="password"
id="password"
className="input input-primary input-bordered input-sm"
value={password}
onChange={(event) => {
setPassword(event.target.value);
}}
/>
</div>
<div>
<button
className="w-full btn btn-primary"
onClick={(event) => {
void login(event);
}}>
Log in
</button>
</div>
<div className="flex flex-col space-y-5">
<span className="flex items-center justify-center space-x-2">
<span className="h-px bg-gray-400 w-14" />
<span className="font-normal text-gray-500">or login with</span>
<span className="h-px bg-gray-400 w-14" />
</span>
<div className="flex flex-col space-y-4">
<button
className="flex items-center justify-center px-4 py-2 space-x-2 transition-colors duration-300 border rounded-md focus:outline-none border-base-200 group hover:bg-base-300"
onClick={(event) => {
event.preventDefault();
void signIn({ provider: 'google' });
}}>
<div className="text-base-content">
<IoLogoGoogle />
</div>
<span className="text-sm font-medium text-base-content">
Gmail
</span>
</button>
</div>
</div>
</form>
</>
)}
{forgot && (
<>
<h3 className="my-4 text-2xl font-semibold">Password recovery</h3>
<form action="#" className="flex flex-col space-y-5">
<div className="flex flex-col space-y-1">
<label
htmlFor="email"
className="text-sm font-semibold text-gray-500">
Email address
</label>
<input
type="email"
id="email"
autoFocus
className="input input-primary input-bordered input-sm"
value={email}
onChange={(event) => {
setEmail(event.target.value);
}}
/>
</div>
<div>
<button
className="w-full btn btn-primary btn-sm"
onClick={(event) => {
event.preventDefault();
void resetPasswordLogin();
}}>
Recover my password
</button>
</div>
<hr />
<button
onClick={() => {
setForgot(false);
}}
className="text-sm text-blue-600 hover:underline focus:text-blue-800">
Go back to sign in
</button>
</form>
</>
)}
</div>
);
};
export default Login;

View File

@@ -0,0 +1,121 @@
import { ApiError, Session, UserCredentials } from '@supabase/gotrue-js';
import { IoLogoGoogle } from 'react-icons/io';
import router from 'next/router';
import { toast } from 'react-toastify';
import { useState } from 'react';
type SignUpPanelProperties = {
signUp: (data: UserCredentials) => Promise<{
user: Session['user'] | null;
session: Session | null;
error: ApiError | null;
}>;
signIn: (data: UserCredentials) => Promise<{
user: Session['user'] | null;
session: Session | null;
error: ApiError | null;
}>;
};
const SignUpPanel = ({
signIn,
signUp,
}: SignUpPanelProperties): JSX.Element => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const signup = async (
event: React.SyntheticEvent<HTMLButtonElement>
): Promise<void> => {
event.preventDefault();
// Handle the login. Go to the homepage if success or display an error.
const result = await signUp({
email,
password,
});
if (result.error) {
toast.error(result.error.message);
} else if (result.user?.confirmation_sent_at) {
toast.success(
'A confirmation email has been sent to you, watch your mailbox!'
);
} else if (result.user) {
void router.push('/');
}
};
return (
<div className="max-w-sm p-10 rounded-md shadow-md md:flex-1 bg-base-100 text-base-content font-body">
<h3 className="my-4 text-2xl font-semibold font-title">
Account Sign Up
</h3>
<form action="#" className="flex flex-col space-y-5">
<div className="flex flex-col space-y-1">
<label htmlFor="email" className="text-sm">
Email address
</label>
<input
type="email"
id="email"
autoFocus
className="input input-primary input-bordered input-sm"
value={email}
onChange={(event) => {
setEmail(event.target.value);
}}
/>
</div>
<div className="flex flex-col space-y-1">
<input
type="password"
id="password"
className="input input-primary input-bordered input-sm"
value={password}
onChange={(event) => {
setPassword(event.target.value);
}}
/>
</div>
<div>
<button
id="loginBtn"
className="w-full btn btn-primary"
onClick={(event) => {
void signup(event);
}}>
Sign Up
</button>
</div>
<div className="flex flex-col space-y-5">
<span className="flex items-center justify-center space-x-2">
<span className="h-px bg-gray-400 w-14" />
<span className="font-normal text-gray-500">or sign up with</span>
<span className="h-px bg-gray-400 w-14" />
</span>
<div className="flex flex-col space-y-4">
<button
className="flex items-center justify-center px-4 py-2 space-x-2 transition-colors duration-300 border rounded-md focus:outline-none border-base-200 group hover:bg-base-300"
onClick={(event) => {
event.preventDefault();
void signIn({ provider: 'google' });
}}>
<div className="text-base-content">
<IoLogoGoogle />
</div>
<span className="text-sm font-medium text-base-content">
Gmail
</span>
</button>
</div>
</div>
</form>
</div>
);
};
export default SignUpPanel;

View File

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

View File

@@ -0,0 +1,23 @@
import Image from 'next/image';
type Properties = {
name: string;
picture: string;
};
const Avatar = ({ name, picture }: Properties): JSX.Element => {
return (
<div className="flex items-center">
<Image
src={picture}
className="w-12 h-12 rounded-full"
alt={name}
width={48}
height={48}
/>
<div className="ml-3 font-medium">{name}</div>
</div>
);
};
export default Avatar;

View File

@@ -0,0 +1,11 @@
import { FunctionComponent, ReactNode } from 'react';
type Properties = {
children?: ReactNode;
};
const Container: FunctionComponent = ({ children }: Properties) => {
return <div className="container mx-auto p-10">{children}</div>;
};
export default Container;

View File

@@ -0,0 +1,34 @@
import Image from 'next/image';
import Link from 'next/link';
type Properties = {
title: string;
src: string;
slug?: string;
};
const CoverImage = ({ title, src, slug }: Properties): JSX.Element => {
const image = (
<Image
src={src}
alt={`Cover Image for ${title}`}
layout="fill"
objectFit="contain"
objectPosition={'center top'}
quality={100}
/>
);
return (
<div className="sm:mx-0 flex justify-center relative max-w-full h-48">
{slug ? (
<Link as={`/blog/${slug}`} href="/blog/[slug]">
<a aria-label={title}>{image}</a>
</Link>
) : (
image
)}
</div>
);
};
export default CoverImage;

View File

@@ -0,0 +1,12 @@
import { format, parseISO } from 'date-fns';
type Properties = {
dateString: string;
};
const DateFormatter = ({ dateString }: Properties): JSX.Element => {
const date = parseISO(dateString);
return <time dateTime={dateString}>{format(date, 'LLLL d, yyyy')}</time>;
};
export default DateFormatter;

View File

@@ -0,0 +1,13 @@
import Link from 'next/link';
const Header = (): JSX.Element => {
return (
<h2 className="text-sm md:text-xl font-semibold tracking-tight md:tracking-tighter leading-tight mb-16 mt-8">
<Link href="/blog">
<a className="hover:underline">Return to the blog</a>
</Link>
</h2>
);
};
export default Header;

View File

@@ -0,0 +1,49 @@
import Author from 'types/author';
import Avatar from './avatar';
import CoverImage from './cover-image';
import DateFormatter from './date-formatter';
import Link from 'next/link';
type Properties = {
title: string;
coverImage: string;
date: string;
excerpt: string;
author: Author;
slug: string;
};
const HeroPost = ({
title,
coverImage,
date,
excerpt,
author,
slug,
}: Properties): JSX.Element => {
return (
<section>
<div className="mb-8 md:mb-16">
<CoverImage title={title} src={coverImage} slug={slug} />
</div>
<div className="md:grid md:grid-cols-2 md:gap-x-16 lg:gap-x-8 mb-20 md:mb-28">
<div>
<h3 className="mb-4 text-4xl lg:text-5xl leading-tight font-semibold">
<Link as={`/blog/${slug}`} href="/blog/[slug]">
<a className="hover:underline">{title}</a>
</Link>
</h3>
<div className="mb-4 md:mb-0 text-lg">
<DateFormatter dateString={date} />
</div>
</div>
<div>
<p className="text-lg leading-relaxed mb-4">{excerpt}</p>
<Avatar name={author.name} picture={author.picture} />
</div>
</div>
</section>
);
};
export default HeroPost;

View File

@@ -0,0 +1,34 @@
import Post from 'types/post';
import PostPreview from './post-preview';
type Properties = {
posts: Post[];
};
const MoreStories = ({ posts }: Properties): JSX.Element => {
return (
<section>
<h2 className="mb-8 text-2xl md:text-4xl font-semibold tracking-tighter leading-tight">
More Stories
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 md:gap-x-16 lg:gap-x-32 gap-y-20 md:gap-y-32 mb-32">
{posts.map(
(post) =>
post && (
<PostPreview
key={post.slug}
title={post.title}
coverImage={post.coverImage}
date={post.date}
author={post.author}
slug={post.slug}
excerpt={post.excerpt}
/>
)
)}
</div>
</section>
);
};
export default MoreStories;

View File

@@ -0,0 +1,18 @@
import React from 'react';
import { getMDXComponent } from 'mdx-bundler/client';
import { useMemo } from 'react';
type Properties = {
code: string;
};
const PostBody = ({ code }: Properties): JSX.Element => {
const BlogPost = useMemo(() => getMDXComponent(code), [code]);
return (
<div className="max-w-2xl mx-auto prose lg:prose-xl">
<BlogPost />
</div>
);
};
export default PostBody;

View File

@@ -0,0 +1,41 @@
import Author from 'types/author';
import Avatar from './avatar';
import CoverImage from './cover-image';
import DateFormatter from './date-formatter';
import PostTitle from './post-title';
type Properties = {
title: string;
coverImage: string;
date: string;
author: Author;
};
const PostHeader = ({
title,
coverImage,
date,
author,
}: Properties): JSX.Element => {
return (
<>
<PostTitle>{title}</PostTitle>
<div className="hidden md:block md:mb-12">
<Avatar name={author.name} picture={author.picture} />
</div>
<div className="mb-8 md:mb-16 sm:mx-0">
<CoverImage title={title} src={coverImage} />
</div>
<div className="max-w-2xl mx-auto">
<div className="block md:hidden mb-6">
<Avatar name={author.name} picture={author.picture} />
</div>
<div className="mb-6 text-lg">
<DateFormatter dateString={date} />
</div>
</div>
</>
);
};
export default PostHeader;

View File

@@ -0,0 +1,43 @@
import Author from 'types/author';
import Avatar from './avatar';
import CoverImage from './cover-image';
import DateFormatter from './date-formatter';
import Link from 'next/link';
type Properties = {
title: string;
coverImage: string;
date: string;
excerpt: string;
author: Author;
slug: string;
};
const PostPreview = ({
title,
coverImage,
date,
excerpt,
author,
slug,
}: Properties): JSX.Element => {
return (
<div>
<div className="mb-5">
<CoverImage slug={slug} title={title} src={coverImage} />
</div>
<h3 className="text-3xl mb-3 leading-snug font-medium">
<Link as={`/blog/${slug}`} href="/blog/[slug]">
<a className="hover:underline">{title}</a>
</Link>
</h3>
<div className="text-lg mb-4">
<DateFormatter dateString={date} />
</div>
<p className="text-lg leading-relaxed mb-4">{excerpt}</p>
<Avatar name={author.name} picture={author.picture} />
</div>
);
};
export default PostPreview;

View File

@@ -0,0 +1,15 @@
import { ReactNode } from 'react';
type Properties = {
children?: ReactNode;
};
const PostTitle = ({ children }: Properties): JSX.Element => {
return (
<h1 className="text-4xl md:text-6xl lg:text-7xl font-semibold tracking-tighter leading-tight md:leading-none mb-5 text-center md:text-left">
{children}
</h1>
);
};
export default PostTitle;

View File

@@ -0,0 +1,5 @@
const SectionSeparator = (): JSX.Element => {
return <hr className="border-neutral-200 mt-16 mb-12" />;
};
export default SectionSeparator;

18
env.local.example Normal file
View File

@@ -0,0 +1,18 @@
NEXT_PUBLIC_TITLE="SupaNexTail"
# SENDGRID
SENDGRID_MAILTO=YOUR_RECIPIENT
SENDGRID_MAILFROM=YOUR_VERIFIED_SENDER
SENDGRID_SECRET=YOUR_SENGRID_SECRET
SENDGRID_MAILING_ID=YOUR_MAILING_LIST_ID
# STRIPE
STRIPE_WEBHOOK=YOUR_STRIPE_WEBHOOK
STRIPE_SECRET=YOUR_SECRET_STRIPE_KEY
NEXT_PUBLIC_STRIPE_PUBLIC_KEY=YOUR_PUBLIC_STRIPE_KEY
#SUPABASE
NEXT_PUBLIC_SUPABASE_URL=YOUR_SUPABASE_URL
NEXT_PUBLIC_SUPABASE_ANON_KEY=YOUR_SUPABASE_ANON_KEY
SUPABASE_ADMIN_KEY=YOUR_SUPABASE_ADMIN_KEY

75
lib/blogApi.ts Normal file
View File

@@ -0,0 +1,75 @@
// This is a set of functions for the Blog system.
import { bundleMDX } from 'mdx-bundler';
import fs from 'node:fs';
import { join } from 'node:path';
import matter from 'gray-matter';
const postsDirectory = join(process.cwd(), '_posts');
export function getPostSlugs(): string[] {
return fs.readdirSync(postsDirectory);
}
type Items = {
[key: string]: string;
};
export function getPostBySlug(slug: string, fields: string[] = []): Items {
const realSlug = slug.replace(/\.mdx$/, '');
const fullPath = join(postsDirectory, `${realSlug}.mdx`);
const fileContents = fs.readFileSync(fullPath, 'utf8');
const {
data,
content,
}: {
data: Items;
content: string;
} = matter(fileContents);
const items: Items = {};
// Ensure only the minimal needed data is exposed
for (const field of fields) {
if (field === 'slug') {
items[field] = realSlug;
}
if (field === 'content') {
items[field] = content;
}
if (typeof data[field] !== 'undefined') {
items[field] = data[field];
}
}
return items;
}
export function getAllPosts(fields: string[] = []): Items[] {
const slugs = getPostSlugs();
return (
slugs
.map((slug) => getPostBySlug(slug, fields))
// sort posts by date in descending order
.sort((post1, post2) => (post1.date > post2.date ? -1 : 1))
);
}
type Parameters_ = {
slug: string;
frontmatter: { [key: string]: any };
code: string;
};
export async function getPostData(slug: string): Promise<Parameters_> {
const realSlug = slug.replace(/\.mdx$/, '');
const fullPath = join(postsDirectory, `${realSlug}.mdx`);
const source = fs.readFileSync(fullPath, 'utf8');
const { code, frontmatter } = await bundleMDX({ source: source });
return {
slug,
frontmatter,
code,
};
}

5
next-env.d.ts vendored Normal file
View File

@@ -0,0 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.

67
package.json Normal file
View File

@@ -0,0 +1,67 @@
{
"name": "supanextail",
"version": "1.5.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"eslint": "eslint . --fix",
"test": "playwright test",
"update-types": "npx openapi-typescript https://vwkxoczhqygxvqzbhkmj.supabase.co/rest/v1/?apikey=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlhdCI6MTYzMDQwNzE1NCwiZXhwIjoxOTQ1OTgzMTU0fQ.VsWbay8ZrhDe3-elrlh7EGtJfW1INXlUywjeh6h2xgc --output types/database/index.ts"
},
"dependencies": {
"@headlessui/react": "^1.4.3",
"@sendgrid/mail": "^7.6.1",
"@stripe/stripe-js": "^1.23.0",
"@supabase/gotrue-js": "^1.22.1",
"@supabase/supabase-js": "^1.30.3",
"@types/node": "^17.0.18",
"@types/react-dom": "^17.0.11",
"axios": "^0.26.0",
"cors": "^2.8.5",
"daisyui": "^2.0.6",
"date-fns": "^2.28.0",
"esbuild": "^0.14.22",
"express-rate-limit": "^6.2.1",
"gray-matter": "^4.0.3",
"mdx-bundler": "^8.0.1",
"micro": "^9.3.4",
"next": ">=12.0.10",
"next-seo": "^5.1.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-feather": "^2.0.9",
"react-icons": "^4.3.1",
"react-toastify": "^8.2.0",
"stripe": "^8.203.0"
},
"devDependencies": {
"@next/eslint-plugin-next": "^12.0.10",
"@playwright/test": "^1.19.1",
"@tailwindcss/typography": "^0.5.2",
"@types/cors": "^2.8.12",
"@types/express-rate-limit": "^5.1.3",
"@types/micro": "^7.3.6",
"@types/react": "^17.0.39",
"@typescript-eslint/eslint-plugin": "^5.12.0",
"autoprefixer": "^10.4.2",
"eslint": "^8.9.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-config-next": "^12.0.10",
"eslint-config-prettier": "^8.3.0",
"eslint-import-resolver-alias": "^1.1.2",
"eslint-plugin-cypress": "^2.12.1",
"eslint-plugin-import": "^2.25.4",
"eslint-plugin-jsx-a11y": "^6.5.1",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-react": "^7.28.0",
"eslint-plugin-react-hooks": "^4.3.0",
"eslint-plugin-simple-import-sort": "^7.0.0",
"eslint-plugin-sonarjs": "^0.11.0",
"postcss": "^8.4.6",
"prettier": "^2.5.1",
"tailwindcss": "^3.0.23",
"typescript": "^4.5.5"
}
}

34
pages/_app.tsx Normal file
View File

@@ -0,0 +1,34 @@
import './global.css';
import { AppProps } from 'next/app';
import { AuthProvider } from 'utils/AuthContext';
import { DefaultSeo } from 'next-seo';
/*
Next-seo is integrated by default, if you want more information and how to
setup more elements, visit their Github page https://github.com/garmeeh/next-seo
*/
function MyApp({ Component, pageProps }: AppProps): JSX.Element {
return (
<>
<AuthProvider>
<DefaultSeo
openGraph={{
type: 'website',
locale: 'en_IE',
url: '',
site_name: 'Supanextail',
}}
twitter={{
handle: '@michael_webdev',
site: '@michael_webdev',
}}
/>
<Component {...pageProps} />
</AuthProvider>
</>
);
}
export default MyApp;

43
pages/_document.tsx Normal file
View File

@@ -0,0 +1,43 @@
import Document, {
DocumentContext,
DocumentInitialProps,
Head,
Html,
Main,
NextScript,
} from 'next/document';
class MyDocument extends Document {
static async getInitialProps(
context_: DocumentContext
): Promise<DocumentInitialProps> {
return await Document.getInitialProps(context_);
}
render(): JSX.Element {
// This will set the initial theme, saved in localstorage
const setInitialTheme = `
function getUserPreference() {
if(window.localStorage.getItem('theme')) {
return window.localStorage.getItem('theme')
}
return window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
: 'supaTheme'
}
document.body.dataset.theme = getUserPreference();
`;
return (
<Html>
<Head />
<body>
<script dangerouslySetInnerHTML={{ __html: setInitialTheme }} />
<Main />
<NextScript />
</body>
</Html>
);
}
}
export default MyDocument;

15
pages/api/auth.ts Normal file
View File

@@ -0,0 +1,15 @@
/**
* NOTE: this file is only needed if you're doing SSR (getServerSideProps)!
* With SupaNexTail, we use SSR with the Dashboard page (pages/dashboard.js)
*/
import type { NextApiRequest, NextApiResponse } from 'next';
import { supabase } from 'utils/supabaseClient';
export default function handler(
request: NextApiRequest,
response: NextApiResponse
): void {
supabase.auth.api.setAuthCookie(request, response);
}

24
pages/api/getUser.ts Normal file
View File

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

66
pages/api/mailingList.ts Normal file
View File

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

56
pages/api/sendgrid.ts Normal file
View File

@@ -0,0 +1,56 @@
/*
This is a simple contact form for SupaNexTail
Using Sendgrid.
*/
import type { NextApiRequest, NextApiResponse } from 'next';
import sgMail from '@sendgrid/mail';
interface Request extends NextApiRequest {
body: {
name: string;
email: string;
message: string;
};
}
const sendGrid = async (
request: Request,
response: NextApiResponse
): Promise<void> => {
if (request.method === 'POST') {
sgMail.setApiKey(process.env.SENDGRID_SECRET || '');
const message = {
to: process.env.SENDGRID_MAILTO || '', // Change to your recipient
from: process.env.SENDGRID_MAILFROM || '', // Change to your verified sender
subject: `[${process.env.NEXT_PUBLIC_TITLE || ''}] New message from ${
request.body.name
}`,
text: request.body.message,
reply_to: request.body.email,
};
try {
const result = await sgMail.send(message);
console.log(result);
if (result) {
response.status(200).json({
message:
'Your message has been succesfully sent. Thank you for your feedback.',
success: true,
});
}
} catch (error: unknown) {
if (error) {
response.status(500).json({
error:
'Oups, there was a problem with your email, please try again or contact us',
success: false,
});
}
}
}
};
export default sendGrid;

View File

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

View File

@@ -0,0 +1,52 @@
/* Dont forget to create your customer portal on Stripe
https://dashboard.stripe.com/test/settings/billing/portal */
import type { NextApiRequest, NextApiResponse } from 'next';
import Cors from 'cors';
import Stripe from 'stripe';
import initMiddleware from 'utils/initMiddleware';
import rateLimit from 'express-rate-limit';
interface Request extends NextApiRequest {
body: {
customerId: string;
};
}
const cors = initMiddleware(
Cors({
methods: ['POST', 'PUT'],
})
);
const limiter = initMiddleware(
rateLimit({
windowMs: 30_000, // 30sec
max: 150, // Max 4 request per 30 sec
})
);
// Set your secret key. Remember to switch to your live secret key in production.
// See your keys here: https://dashboard.stripe.com/apikeys
const stripe = new Stripe(process.env.STRIPE_SECRET || '', {
apiVersion: '2020-08-27',
});
export default async function handler(
request: Request,
response: NextApiResponse
): Promise<void> {
await cors(request, response);
await limiter(request, response);
if (request.method === 'POST') {
const returnUrl = `${
request.headers.origin ? request.headers.origin : '/'
}/dashboard`; // Stripe will return to the dashboard, you can change it
const portalsession = await stripe.billingPortal.sessions.create({
customer: request.body.customerId,
return_url: returnUrl,
});
response.status(200).send({ url: portalsession.url });
}
}

162
pages/api/stripe/webhook.ts Normal file
View File

@@ -0,0 +1,162 @@
/*
SupaNexTail use only 2 webhooks. Stripe have a lot more,
you can check it here https://stripe.com/docs/webhooks
BE SURE TO SETUP YOUR WEBHOOKS IN YOUR DASHBOARD!
If you want to test it locally, you'll need the stripe CLI and use this command line:
stripe listen --forward-to localhost:3000/api/stripe/webhook
*/
import type { NextApiRequest, NextApiResponse } from 'next';
import Cors from 'cors';
import Stripe from 'stripe';
import { buffer } from 'micro';
import { createClient } from '@supabase/supabase-js';
import initMiddleware from 'utils/initMiddleware';
import rateLimit from 'express-rate-limit';
export const config = {
api: {
bodyParser: false,
},
};
// Initialize the cors middleware -> Allow the browser extension to create lists
const cors = initMiddleware(
Cors({
methods: ['POST', 'HEAD'],
})
);
// Init Supabase Admin
const supabase = createClient(
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)
const limiter = initMiddleware(
rateLimit({
windowMs: 30_000, // 30sec
max: 150, // Max 150 request per 30 sec
})
);
// Set your secret key. Remember to switch to your live secret key in production.
// See your keys here: https://dashboard.stripe.com/apikeys
const stripe = new Stripe(process.env.STRIPE_SECRET || '', {
apiVersion: '2020-08-27',
maxNetworkRetries: 2,
});
export default async function handler(
request: NextApiRequest,
response: NextApiResponse
): Promise<void> {
await cors(request, response);
await limiter(request, response);
if (request.method === 'POST') {
// Retrieve the event by verifying the signature using the raw body and secret.
let event: Stripe.Event;
const buf = await buffer(request);
const sig = request.headers['stripe-signature'] as string;
try {
event = stripe.webhooks.constructEvent(
buf,
sig,
process.env.STRIPE_WEBHOOK || ''
);
} catch (error) {
console.log(error);
console.log(`⚠️ Webhook signature verification failed.`);
console.log(
`⚠️ Check the env file and enter the correct webhook secret.`
);
return response.send(400);
}
// Extract the object from the event.
const dataObject = event.data.object as {
client_reference_id: string;
customer: string;
metadata: {
priceId: string;
};
subscription: string;
};
// Handle the event
// Review important events for Billing webhooks
// https://stripe.com/docs/billing/webhooks
// Remove comment to see the various objects sent for this sample
switch (event.type) {
case 'checkout.session.completed':
const { data: subscriptions } = await supabase
.from('subscriptions')
.select('*')
.eq('id', dataObject.client_reference_id);
if (subscriptions?.length == 0) {
await supabase
.from('profiles')
.update({ customerId: dataObject.customer })
.eq('id', dataObject.client_reference_id);
await supabase
.from('subscriptions')
.insert([
{
id: dataObject.client_reference_id,
customer_id: dataObject.customer,
paid_user: true,
plan: dataObject.metadata.priceId,
subscription: dataObject.subscription,
},
])
.then()
.then(undefined, (error) => console.log('err:', error)); // catch
} else if (subscriptions?.length && subscriptions?.length > 0) {
await supabase
.from('subscriptions')
.update({
customer_id: dataObject.customer,
paid_user: true,
plan: dataObject.metadata.priceId,
subscription: dataObject.subscription,
})
.eq('id', dataObject.client_reference_id)
.then()
.then(undefined, (error) => console.log('err:', error)); // catch
}
break;
case 'customer.subscription.deleted':
await supabase
.from('subscriptions')
.update({ paid_user: false })
.eq('customer_id', dataObject.customer)
.then()
.then(undefined, (error) => console.log('err:', error)); // catch
break;
case 'invoice.payment_failed':
// If the payment fails or the customer does not have a valid payment method,
// an invoice.payment_failed event is sent, the subscription becomes past_due.
// Use this webhook to notify your user that their payment has
// failed and to retrieve new card details.
break;
case 'invoice.paid':
// Used to provision services after the trial has ended.
// The status of the invoice will show up as paid. Store the status in your
// database to reference when a user accesses your service to avoid hitting rate limits.
break;
default:
// Unexpected event type
}
response.send(200);
}
}

67
pages/blog.tsx Normal file
View File

@@ -0,0 +1,67 @@
import Container from 'components/blog/container';
import { GetStaticPropsResult } from 'next/types';
import Head from 'next/head';
import HeroPost from 'components/blog/hero-post';
import Layout from 'components/Layout';
import MoreStories from 'components/blog/more-stories';
import Post from '../types/post';
import SectionSeparator from 'components/blog/section-separator';
import { getAllPosts } from '../lib/blogApi';
type Properties = {
allPosts: Post[];
};
type Items = {
[key: string]: string;
};
const Blog = ({ allPosts }: Properties): JSX.Element => {
const heroPost = allPosts[0];
const morePosts = allPosts.slice(1);
return (
<Layout>
<>
<Head>
<title>{`${
process.env.NEXT_PUBLIC_TITLE ? process.env.NEXT_PUBLIC_TITLE : ''
} | Blog`}</title>
</Head>
<Container>
{heroPost && (
<HeroPost
title={heroPost.title}
coverImage={heroPost.coverImage}
date={heroPost.date}
author={heroPost.author}
slug={heroPost.slug}
excerpt={heroPost.excerpt}
/>
)}
<SectionSeparator />
{morePosts.length > 0 && <MoreStories posts={morePosts} />}
</Container>
</>
</Layout>
);
};
export default Blog;
type Parameters_ = {
allPosts: Items[];
};
export const getStaticProps = (): GetStaticPropsResult<Parameters_> => {
const allPosts = getAllPosts([
'title',
'date',
'slug',
'author',
'coverImage',
'excerpt',
]);
return {
props: { allPosts },
};
};

85
pages/blog/[slug].tsx Normal file
View File

@@ -0,0 +1,85 @@
import { GetStaticPathsResult, GetStaticPropsResult } from 'next';
import { getAllPosts, getPostData } from 'lib/blogApi';
import Container from 'components/blog/container';
import Head from 'next/head';
import Header from 'components/blog/header';
import Layout from 'components/Layout';
import PostBody from 'components/blog/post-body';
import PostHeader from 'components/blog/post-header';
import PostType from 'types/post';
type Properties = {
preview?: boolean;
code: string;
frontmatter: PostType;
};
const Post = ({ code, frontmatter }: Properties): JSX.Element => {
return (
<Layout>
<Container>
<>
<article className="mb-32">
<Head>
<title>
{frontmatter.title} | {process.env.NEXT_PUBLIC_TITLE}
</title>
<meta property="og:image" content={frontmatter.ogImage.url} />
</Head>
<Header />
<PostHeader
title={frontmatter.title}
coverImage={frontmatter.coverImage}
date={frontmatter.date}
author={frontmatter.author}
/>
<PostBody code={code} />
</article>
</>
</Container>
</Layout>
);
};
export default Post;
type Parameters_ = {
params: {
slug: string;
};
};
type StaticResult = {
slug: string;
frontmatter: {
[key: string]: any;
};
code: string;
};
export async function getStaticProps({
params,
}: Parameters_): Promise<GetStaticPropsResult<StaticResult>> {
const postData = await getPostData(params.slug);
return {
props: {
...postData,
},
};
}
export function getStaticPaths(): GetStaticPathsResult {
const posts = getAllPosts(['slug']);
return {
paths: posts.map((post) => {
return {
params: {
slug: post.slug,
},
};
}),
fallback: false,
};
}

21
pages/contact.tsx Normal file
View File

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

129
pages/dashboard.tsx Normal file
View File

@@ -0,0 +1,129 @@
import { useEffect, useState } from 'react';
import Dashboard from '../components/Dashboard';
import Head from 'next/head';
import Layout from 'components/Layout';
import type { NextPageContext } from 'next';
import Stripe from 'stripe';
import { createClient } from '@supabase/supabase-js';
import { definitions } from 'types/database/index';
import { supabase } from '../utils/supabaseClient';
import { useRouter } from 'next/router';
const DashboardPage = ({
user,
profile,
planName,
}: {
user: {
id: string;
};
profile: {
username: string;
website: string;
avatar_url: string;
};
planName: string;
}): JSX.Element => {
const [session, setSession] = useState(supabase.auth.session());
const router = useRouter();
useEffect(() => {
// If a user is not logged in, return to the homepage
if (!user) {
void router.push('/');
}
}, [router, user]);
useEffect(() => {
setSession(supabase.auth.session());
supabase.auth.onAuthStateChange((_event, session) => {
setSession(session);
});
}, []);
return (
<div>
<Head>
<title>{process.env.NEXT_PUBLIC_TITLE} | Dashboard</title>
</Head>
<Layout>
{!session ? (
<div className="text-center">You are not logged in</div>
) : (
<>
{session && (
<Dashboard
key={user.id || undefined}
session={session}
profile={profile}
planName={planName}
/>
)}
</>
)}
</Layout>
</div>
);
};
export async function getServerSideProps(
context: NextPageContext
): Promise<any> {
const supabaseAdmin = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL || '',
process.env.SUPABASE_ADMIN_KEY || ''
);
const { user } = await supabaseAdmin.auth.api.getUserByCookie(context.req);
const stripe = new Stripe(process.env.STRIPE_SECRET || '', {
apiVersion: '2020-08-27',
maxNetworkRetries: 2,
});
// If the user exist, you will retrieve the user profile and if he/she's a paid user
if (user) {
const { data: plan } = await supabaseAdmin
.from<definitions['subscriptions']>('subscriptions')
.select('subscription, paid_user')
.eq('id', user.id)
.single();
// Check the subscription plan. If it doesnt exist, return null
const subscription = plan?.subscription
? await stripe.subscriptions.retrieve(plan.subscription)
: null;
const { data: profile } = await supabaseAdmin
.from<definitions['profiles']>('profiles')
.select(`username, website, avatar_url`)
.eq('id', user.id)
.single();
return {
props: {
user,
plan: subscription?.items.data[0].price.id
? subscription?.items.data[0].price.id
: null,
profile,
// Retrieve the name of the subscription plan (Don't forget to add nickname to your prices)
planName: plan?.paid_user
? subscription?.items.data[0].plan.nickname
? subscription?.items.data[0].plan.nickname
: '[DEV] Please add a description for your prices (Edit your pricing in the Stripe dashboard)'
: 'Free Tier',
},
};
}
if (!user) {
// If no user, redirect to index.
return { props: {}, redirect: { destination: '/', permanent: false } };
}
// If there is a user, return it.
return null;
}
export default DashboardPage;

51
pages/global.css Normal file
View File

@@ -0,0 +1,51 @@
@import 'tailwindcss/tailwind.css';
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500&family=Poppins:wght@400;600;700;800&display=swap');
@tailwind base;
@tailwind components;
@tailwind utilities;
/*
You can setup basic rules for your headers/text here
*/
@layer base {
h1 {
@apply mb-5 text-5xl font-bold font-title;
}
h2 {
@apply mt-2 mb-3 text-2xl font-title;
}
p {
@apply mb-5 text-sm leading-loose text-left font-body text-base-200;
}
ul {
@apply mb-3 ml-10 text-sm leading-loose list-disc font-body text-base-200;
}
}
.nav-btn {
position: relative;
text-decoration: none;
@apply my-auto text-base-200;
}
.nav-btn::before {
content: '';
position: absolute;
display: block;
width: 100%;
height: 2px;
bottom: 0;
left: 0;
transform: scaleX(0);
transition: transform 0.3s ease;
@apply bg-primary;
}
.nav-btn:hover::before {
transform: scaleX(1);
}
.btn {
@apply font-normal;
}

63
pages/index.tsx Normal file
View File

@@ -0,0 +1,63 @@
/*
Don't forget to modify the Head component with your website informations
You can also update the content on the Landing.js component
*/
import Head from 'next/head';
import Landing from 'components/Landing';
import Layout from 'components/Layout';
const Home = (): JSX.Element => (
<>
<Head>
<title>{`Welcome to ${
process.env.NEXT_PUBLIC_TITLE ? process.env.NEXT_PUBLIC_TITLE : ''
} 👋`}</title>
<meta
name="description"
content="SupaNexTail is a boilerplate for your SaaS, based on Next.js, Supabase, and TailwindCSS"
/>
<meta property="og:url" content="https://supanextail.dev/" />
<meta property="og:type" content="website" />
<meta
property="og:title"
content={`Welcome to ${
process.env.NEXT_PUBLIC_TITLE ? process.env.NEXT_PUBLIC_TITLE : ''
} 👋`}
/>
<meta
property="og:description"
content="SupaNexTail is a boilerplate for your SaaS, based on Next.js, Supabase, and TailwindCSS"
/>
<meta property="og:image" content="https://supanextail.dev/ogimage.png" />
<meta name="twitter:card" content="summary_large_image" />
<meta property="twitter:domain" content="supanextail.dev" />
<meta
property="twitter:url"
content="https://supanextail.dev/ogimage.png"
/>
<meta
name="twitter:title"
content={`Welcome to ${
process.env.NEXT_PUBLIC_TITLE ? process.env.NEXT_PUBLIC_TITLE : ''
} 👋`}
/>
<meta
name="twitter:description"
content="SupaNexTail is a boilerplate for your SaaS, based on Next.js, Supabase, and TailwindCSS"
/>
<meta
name="twitter:image"
content="https://supanextail.dev/ogimage.png"
/>
<meta charSet="UTF-8" />
</Head>
<Layout>
<Landing />
</Layout>
</>
);
export default Home;

32
pages/login.tsx Normal file
View File

@@ -0,0 +1,32 @@
/*
This is the login/register page.
You have 2 components, the "AuthComponent" that handle the logic,
and the "AuthText" that will show the description on the left of the screen
*/
import Layout from 'components/Layout';
import Login from 'components/UI/Login';
import { NextSeo } from 'next-seo';
import { useAuth } from 'utils/AuthContext';
const LoginPage = (): JSX.Element => {
const { signIn, resetPassword } = useAuth();
return (
<>
<NextSeo
title={`${process.env.NEXT_PUBLIC_TITLE || ''} | Auth`}
description={`This is the auth page for ${
process.env.NEXT_PUBLIC_TITLE || ''
}`}
/>
<Layout>
<div className="flex flex-wrap w-full mt-20 justify-evenly">
<Login signIn={signIn} resetPassword={resetPassword} />
</div>
</Layout>
</>
);
};
export default LoginPage;

61
pages/members-only.tsx Normal file
View File

@@ -0,0 +1,61 @@
// SSR Example to handle premium users only
import Layout from 'components/Layout';
import { NextPageContext } from 'next';
import { NextSeo } from 'next-seo';
import { createClient } from '@supabase/supabase-js';
import { definitions } from 'types/database/index';
const MembersOnly = (): JSX.Element => (
<>
<NextSeo
title={`${
process.env.NEXT_PUBLIC_TITLE ? process.env.NEXT_PUBLIC_TITLE : ''
} | Premium`}
description={`This is the premium page for ${
process.env.NEXT_PUBLIC_TITLE ? process.env.NEXT_PUBLIC_TITLE : ''
}`}
/>
<Layout>
<>
<h2 className="text-center mb-10"> Welcome premium user 🎉</h2>
<p className="max-w-md m-auto">
This page check server side if a user is premium. You can use this
example to store your premium app/logic here!
</p>
</>
</Layout>
</>
);
export default MembersOnly;
export async function getServerSideProps(context: NextPageContext) {
const supabaseAdmin = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL || '',
process.env.SUPABASE_ADMIN_KEY || ''
);
const { user } = await supabaseAdmin.auth.api.getUserByCookie(context.req);
console.log(user);
const { data: subscriptions } = await supabaseAdmin
.from<definitions['subscriptions']>('subscriptions')
.select('paid_user, plan')
.eq('id', user?.id)
.single();
console.log(subscriptions);
if (!subscriptions?.paid_user) {
return {
redirect: {
destination: '/',
permanent: false,
},
};
}
return {
props: {
sub: subscriptions?.paid_user,
},
};
}

20
pages/pricing.tsx Normal file
View File

@@ -0,0 +1,20 @@
// To modify the content of the pricing page, check the Pricing.js component
import Layout from 'components/Layout';
import { NextSeo } from 'next-seo';
import Pricing from 'components/Pricing';
const PricingPage = (): JSX.Element => (
<>
<NextSeo
title={`${
process.env.NEXT_PUBLIC_TITLE ? process.env.NEXT_PUBLIC_TITLE : ''
} | Pricing`}
description="SupaNexTail is a boilerplate for your website, based on Next.js, Supabase, and TailwindCSS"
/>
<Layout>
<Pricing />
</Layout>
</>
);
export default PricingPage;

20
pages/privacy.tsx Normal file
View File

@@ -0,0 +1,20 @@
// To modify the privacy policy, check the PrivacyPolicy.js component
import Layout from 'components/Layout';
import { NextSeo } from 'next-seo';
import PrivacyPolicy from 'components/PrivacyPolicy';
const PrivacyPage = (): JSX.Element => (
<>
<NextSeo
title={`${
process.env.NEXT_PUBLIC_TITLE ? process.env.NEXT_PUBLIC_TITLE : ''
} | Privacy Policy`}
description="SupaNexTail is a boilerplate for your website, based on Next.js, Supabase, and TailwindCSS"
/>
<Layout>
<PrivacyPolicy />
</Layout>
</>
);
export default PrivacyPage;

32
pages/signup.tsx Normal file
View File

@@ -0,0 +1,32 @@
/*
This is the login/register page.
You have 2 components, the "AuthComponent" that handle the logic,
and the "AuthText" that will show the description on the left of the screen
*/
import AuthComponent from 'components/SignUp';
import AuthText from 'components/AuthText';
import Layout from 'components/Layout';
import { NextSeo } from 'next-seo';
const SignUpPage = (): JSX.Element => (
<>
<NextSeo
title={`${
process.env.NEXT_PUBLIC_TITLE ? process.env.NEXT_PUBLIC_TITLE : ''
} | Auth`}
description={`This is the auth page for ${
process.env.NEXT_PUBLIC_TITLE ? process.env.NEXT_PUBLIC_TITLE : ''
}`}
/>
<Layout>
<div className="flex flex-wrap w-full mt-20 justify-evenly">
<AuthText />
<AuthComponent />
</div>
</Layout>
</>
);
export default SignUpPage;

20
pages/terms.tsx Normal file
View File

@@ -0,0 +1,20 @@
// To modify the terms & conditions, check the Terms.js component
import Layout from 'components/Layout';
import { NextSeo } from 'next-seo';
import Terms from 'components/Terms';
const TermsPage = (): JSX.Element => (
<>
<NextSeo
title={`${
process.env.NEXT_PUBLIC_TITLE ? process.env.NEXT_PUBLIC_TITLE : ''
} | Terms and conditions`}
description="SupaNexTail is a boilerplate for your website, based on Next.js, Supabase, and TailwindCSS"
/>
<Layout>
<Terms />
</Layout>
</>
);
export default TermsPage;

5375
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

8
postcss.config.js Normal file
View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

BIN
public/apple-touch-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
public/auth.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.8 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

9
public/browserconfig.xml Normal file
View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<browserconfig>
<msapplication>
<tile>
<square150x150logo src="/mstile-150x150.png"/>
<TileColor>#da532c</TileColor>
</tile>
</msapplication>
</browserconfig>

27
public/cardAuth.svg Normal file
View File

@@ -0,0 +1,27 @@
<svg xmlns="http://www.w3.org/2000/svg" width="81" height="81" viewBox="0 0 81 81">
<g id="cardAuth" transform="translate(-9.5 -9.5)">
<path id="Tracé_596" data-name="Tracé 596" d="M10,50A40,40,0,1,0,50,10,40,40,0,0,0,10,50Z" fill="#e8f4fa" stroke="#daedf7" stroke-width="1"/>
<path id="Tracé_597" data-name="Tracé 597" d="M47.023,36.011A15.862,15.862,0,0,1,28,51.54" fill="none" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_598" data-name="Tracé 598" d="M28,43.62V36.011A19.018,19.018,0,0,1,47.023,16.993h0A19.017,19.017,0,0,1,66.041,36.012a34.7,34.7,0,0,1-4.119,15.819" fill="none" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_599" data-name="Tracé 599" d="M54.8,49.65a15.911,15.911,0,0,0,11.253,1.891" fill="none" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_600" data-name="Tracé 600" d="M52.688,52.822a19.562,19.562,0,0,0,10.184,2.841,19.853,19.853,0,0,0,2.829-.2" fill="none" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_601" data-name="Tracé 601" d="M50.1,55.69a23.344,23.344,0,0,0,12.768,3.777q.811,0,1.611-.055" fill="none" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_602" data-name="Tracé 602" d="M47.023,62.625A31.894,31.894,0,0,1,34.6,66.264" fill="none" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_603" data-name="Tracé 603" d="M47.023,62.625a31.894,31.894,0,0,0,12.419,3.639" fill="none" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_604" data-name="Tracé 604" d="M54.565,69.324a31,31,0,0,1-7.542-2.265,31,31,0,0,1-7.541,2.265" fill="none" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_605" data-name="Tracé 605" d="M44.487,70.877h5.072" fill="none" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_606" data-name="Tracé 606" d="M31.809,44.039V36.011A15.213,15.213,0,0,1,47.02,20.8h0A15.215,15.215,0,0,1,62.238,36.011a30.91,30.91,0,0,1-4.261,15.078" fill="none" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_607" data-name="Tracé 607" d="M35.612,42.957V36.011A11.411,11.411,0,0,1,47.023,24.6h0A11.411,11.411,0,0,1,58.434,36.011C58.1,50.59,47.915,62.341,31.786,63.241" fill="none" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_608" data-name="Tracé 608" d="M47.012,58.219a29.167,29.167,0,0,0,15.226,5.022" fill="none" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_609" data-name="Tracé 609" d="M28,43.62a8.245,8.245,0,0,0,11.411-7.609A7.607,7.607,0,0,1,47.023,28.4h0a7.607,7.607,0,0,1,7.607,7.607,23.457,23.457,0,0,1-23.459,23.46c-.538,0-1.075-.018-1.607-.055" fill="none" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_610" data-name="Tracé 610" d="M28.057,47.648A12.053,12.053,0,0,0,43.219,36.011a3.8,3.8,0,0,1,3.8-3.8h0a3.8,3.8,0,0,1,3.8,3.8,19.669,19.669,0,0,1-22.481,19.45" fill="none" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_611" data-name="Tracé 611" d="M34.559,81.02c0,.948,6.148,1.716,13.731,1.716s13.731-.768,13.731-1.716S55.873,79.3,48.29,79.3s-13.731.768-13.731,1.716Z" fill="#45413c"/>
<path id="Tracé_612" data-name="Tracé 612" d="M82.839,53.761V66.44H78.4V53.761a6.34,6.34,0,1,0-12.68,0V66.44H61.285V53.761a10.777,10.777,0,1,1,21.554,0Z" fill="#daedf7"/>
<path id="Tracé_613" data-name="Tracé 613" d="M72.062,42.984A10.785,10.785,0,0,0,61.285,53.761V56.3a10.777,10.777,0,1,1,21.554,0V53.761A10.785,10.785,0,0,0,72.062,42.984Z" fill="#e8f4fa"/>
<path id="Tracé_614" data-name="Tracé 614" d="M82.839,53.761V66.44H78.4V53.761a6.34,6.34,0,1,0-12.68,0V66.44H61.285V53.761a10.777,10.777,0,1,1,21.554,0Z" fill="none" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_615" data-name="Tracé 615" d="M58.75,58.2H85.375V76.583H58.75Z" fill="#ffe500"/>
<path id="Tracé_616" data-name="Tracé 616" d="M74.6,65.806a2.536,2.536,0,1,0-3.8,2.184v2.887a1.268,1.268,0,1,0,2.536,0V67.99A2.524,2.524,0,0,0,74.6,65.806Z" fill="#656769" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_617" data-name="Tracé 617" d="M82.839,58.2H61.285a2.535,2.535,0,0,0-2.535,2.535v3.8A2.536,2.536,0,0,1,61.285,62H82.839a2.536,2.536,0,0,1,2.536,2.536v-3.8A2.535,2.535,0,0,0,82.839,58.2Z" fill="#fff48c"/>
<path id="Tracé_618" data-name="Tracé 618" d="M58.75,58.2H85.375V76.583H58.75Z" fill="none" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.0 KiB

27
public/cardFee.svg Normal file
View File

@@ -0,0 +1,27 @@
<svg xmlns="http://www.w3.org/2000/svg" width="81" height="81" viewBox="0 0 81 81">
<g id="cardFee" transform="translate(-9.5 -9.5)">
<path id="Tracé_400" data-name="Tracé 400" d="M10,50A40,40,0,1,0,50,10,40,40,0,0,0,10,50Z" fill="#e8f4fa" stroke="#daedf7" stroke-width="1"/>
<path id="Tracé_401" data-name="Tracé 401" d="M22.621,39.151H63.413a5.508,5.508,0,0,1,5.508,5.508V66.878a5.508,5.508,0,0,1-5.508,5.508H22.62a5.507,5.507,0,0,1-5.507-5.507V44.659A5.508,5.508,0,0,1,22.621,39.151Z" fill="#ebcb00"/>
<path id="Tracé_402" data-name="Tracé 402" d="M42.37,39.151H22.62a5.509,5.509,0,0,0-5.507,5.508V64.408Z" fill="#ffe500"/>
<path id="Tracé_403" data-name="Tracé 403" d="M23.956,64.566h9.775" fill="none" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_404" data-name="Tracé 404" d="M23.956,50.881h9.775v6.842H23.956Z" fill="#ffef9e" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_405" data-name="Tracé 405" d="M22.621,39.151H63.413a5.508,5.508,0,0,1,5.508,5.508V66.878a5.508,5.508,0,0,1-5.508,5.508H22.62a5.507,5.507,0,0,1-5.507-5.507V44.659A5.508,5.508,0,0,1,22.621,39.151Z" fill="none" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_406" data-name="Tracé 406" d="M45.461,61.634a5.865,5.865,0,1,0,5.865-5.865A5.865,5.865,0,0,0,45.461,61.634Z" fill="#ff6242"/>
<path id="Tracé_407" data-name="Tracé 407" d="M52.3,61.634a5.865,5.865,0,1,0,5.865-5.865A5.865,5.865,0,0,0,52.3,61.634Z" fill="#ffef9e"/>
<path id="Tracé_408" data-name="Tracé 408" d="M54.747,56.869a5.868,5.868,0,0,0,0,9.529,5.866,5.866,0,0,0,0-9.529Z" fill="#ffaa54"/>
<path id="Tracé_409" data-name="Tracé 409" d="M45.461,61.634a5.865,5.865,0,1,0,5.865-5.865A5.865,5.865,0,0,0,45.461,61.634Z" fill="none" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_410" data-name="Tracé 410" d="M52.3,61.634a5.865,5.865,0,1,0,5.865-5.865A5.865,5.865,0,0,0,52.3,61.634Z" fill="none" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_411" data-name="Tracé 411" d="M55.722,81.308c0-.956-5.688-1.732-12.7-1.732s-12.706.776-12.706,1.732,5.684,1.733,12.7,1.733S55.722,82.265,55.722,81.308Z" fill="#45413c" opacity="0.15"/>
<path id="Tracé_412" data-name="Tracé 412" d="M48.385,30.319A20.213,20.213,0,1,0,68.6,10.106,20.213,20.213,0,0,0,48.385,30.319Z" fill="#f0f0f0"/>
<path id="Tracé_413" data-name="Tracé 413" d="M88.45,34.142a20.2,20.2,0,0,0-39.7,0,20.213,20.213,0,1,1,39.7,0Z" fill="#fff"/>
<path id="Tracé_414" data-name="Tracé 414" d="M48.385,30.319A20.213,20.213,0,1,0,68.6,10.106,20.213,20.213,0,0,0,48.385,30.319Z" fill="none" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_415" data-name="Tracé 415" d="M74.8,29.688l-5.7-11.4a.565.565,0,0,0-1.009,0l-5.7,11.4L55.17,23.426a.564.564,0,0,0-.923.534l3.074,15.878s0,2.82,11.277,2.82,11.278-2.82,11.278-2.82L82.949,23.96a.564.564,0,0,0-.923-.534Z" fill="#ffe500"/>
<path id="Tracé_416" data-name="Tracé 416" d="M74.8,29.688l-5.7-11.4a.565.565,0,0,0-1.009,0l-5.7,11.4L55.17,23.426a.564.564,0,0,0-.923.534l3.074,15.878s0,2.82,11.277,2.82,11.278-2.82,11.278-2.82L82.949,23.96a.564.564,0,0,0-.923-.534Z" fill="#ffe500"/>
<path id="Tracé_417" data-name="Tracé 417" d="M82.678,23.364a.566.566,0,0,0-.652.062L74.8,29.688l-5.7-11.4a.565.565,0,0,0-1.009,0l-5.7,11.4L55.17,23.426a.564.564,0,0,0-.923.533l3.074,15.879s0,2.82,11.277,2.82,11.278-2.82,11.278-2.82l3.073-15.879a.563.563,0,0,0-.271-.595Zm-4.51,15.99c-.342.3-2.212,1.534-9.57,1.534s-9.228-1.239-9.569-1.534L56.89,28.3a.442.442,0,0,1,.724-.418l4.914,4.259A.443.443,0,0,0,63.214,32L68.2,22.03a.443.443,0,0,1,.791,0l4.989,9.978a.442.442,0,0,0,.685.136l4.914-4.258a.442.442,0,0,1,.724.418Z" fill="#fff48c"/>
<path id="Tracé_418" data-name="Tracé 418" d="M56.837,37.339l.484,2.5s0,2.82,11.277,2.82,11.278-2.82,11.278-2.82l.484-2.5c-1.083.827-3.984,1.935-11.762,1.935S57.919,38.166,56.837,37.339Z" fill="#ebcb00"/>
<path id="Tracé_419" data-name="Tracé 419" d="M58.818,38.265a6.314,6.314,0,0,1-1.981-.926l.484,2.5s0,.983,2.322,1.789Z" fill="#ffe500"/>
<path id="Tracé_420" data-name="Tracé 420" d="M77.907,41.5c1.969-.777,1.969-1.658,1.969-1.658l.484-2.5a6.085,6.085,0,0,1-1.817.874Z" fill="#ffe500"/>
<path id="Tracé_421" data-name="Tracé 421" d="M56.837,37.339c1.082.827,3.984,1.935,11.761,1.935s10.679-1.108,11.762-1.935" fill="none" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_422" data-name="Tracé 422" d="M74.8,29.688l-5.7-11.4a.565.565,0,0,0-1.009,0l-5.7,11.4L55.17,23.426a.564.564,0,0,0-.923.534l3.074,15.878s0,2.82,11.277,2.82,11.278-2.82,11.278-2.82L82.949,23.96a.564.564,0,0,0-.923-.534Z" fill="none" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.9 KiB

41
public/cardPage.svg Normal file
View File

@@ -0,0 +1,41 @@
<svg xmlns="http://www.w3.org/2000/svg" width="90.615" height="80.002" viewBox="0 0 90.615 80.002">
<g id="cardPage" transform="translate(-4.443 -9.993)">
<path id="Tracé_553" data-name="Tracé 553" d="M88.443,50A40,40,0,1,0,12.007,66.5H84.879A39.844,39.844,0,0,0,88.443,50Z" fill="#ace5fa"/>
<path id="Tracé_554" data-name="Tracé 554" d="M12.007,66.5a40,40,0,0,0,72.872,0Z" fill="#ccf7ad"/>
<path id="Tracé_555" data-name="Tracé 555" d="M9.443,66.5h78m-82.5,0h2.5m82,0h2.5" fill="none" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_556" data-name="Tracé 556" d="M65.872,69.7a2,2,0,0,1-2,2H11.491a2,2,0,0,1-2-2V38.532H65.872Z" fill="#fff" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_557" data-name="Tracé 557" d="M65.872,38.532H9.491v-5.96a2,2,0,0,1,2-2H63.872a2,2,0,0,1,2,2Z" fill="#f0f0f0"/>
<path id="Tracé_558" data-name="Tracé 558" d="M65.872,32.572a2,2,0,0,0-2-2H11.491a2,2,0,0,0-2,2V36.66a2.771,2.771,0,0,1,2.771-2.771H63.1a2.771,2.771,0,0,1,2.771,2.771Z" fill="#fff"/>
<path id="Tracé_559" data-name="Tracé 559" d="M65.872,38.532H9.491v-5.96a2,2,0,0,1,2-2H63.872a2,2,0,0,1,2,2Z" fill="none" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<circle id="Ellipse_6" data-name="Ellipse 6" cx="1.327" cy="1.327" r="1.327" transform="translate(12.144 33.225)" fill="#ff6242" stroke="#e04122" stroke-miterlimit="10" stroke-width="1"/>
<circle id="Ellipse_7" data-name="Ellipse 7" cx="1.327" cy="1.327" r="1.327" transform="translate(17.45 33.225)" fill="#ffe500" stroke="#ebcb00" stroke-miterlimit="10" stroke-width="1"/>
<circle id="Ellipse_8" data-name="Ellipse 8" cx="1.327" cy="1.327" r="1.327" transform="translate(22.757 33.225)" fill="#6dd627" stroke="#46b000" stroke-miterlimit="10" stroke-width="1"/>
<rect id="Rectangle_10" data-name="Rectangle 10" width="2.653" height="17.756" rx="0.5" transform="translate(13.551 68.394) rotate(-45)" fill="#daedf7" stroke="#45413c" stroke-linejoin="round" stroke-width="1"/>
<rect id="Rectangle_11" data-name="Rectangle 11" width="2.653" height="17.756" rx="0.5" transform="translate(26.463 68.395) rotate(-45)" fill="#daedf7" stroke="#45413c" stroke-linejoin="round" stroke-width="1"/>
<rect id="Rectangle_12" data-name="Rectangle 12" width="17.756" height="2.653" rx="0.5" transform="translate(13.552 79.075) rotate(-45)" fill="#daedf7" stroke="#45413c" stroke-linejoin="round" stroke-width="1"/>
<rect id="Rectangle_13" data-name="Rectangle 13" width="2.653" height="18.572" rx="0.354" transform="translate(26.074 62.411)" fill="#daedf7" stroke="#45413c" stroke-linejoin="round" stroke-width="1"/>
<rect id="Rectangle_14" data-name="Rectangle 14" width="2.653" height="18.572" rx="0.354" transform="translate(12.808 62.411)" fill="#daedf7" stroke="#45413c" stroke-linejoin="round" stroke-width="1"/>
<rect id="Rectangle_15" data-name="Rectangle 15" width="10.613" height="2.653" rx="0.5" transform="translate(15.461 64.401)" fill="#daedf7" stroke="#45413c" stroke-linejoin="round" stroke-width="1"/>
<rect id="Rectangle_16" data-name="Rectangle 16" width="13.929" height="15.919" rx="1" transform="translate(14.134 43.175)" fill="#ff6242" stroke="#45413c" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_560" data-name="Tracé 560" d="M58.239,63.074H38.35a1,1,0,0,0-1,1V71.7H59.239V64.074a1,1,0,0,0-1-1Z" fill="#ffaa54" stroke="#45413c" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_561" data-name="Tracé 561" d="M37.35,56.441H59.239v3.317H37.35Z" fill="#ffe500" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_562" data-name="Tracé 562" d="M70.163,23.835l-1.58,5.529H79.64V23.835Z" fill="#00b8f0"/>
<path id="Tracé_563" data-name="Tracé 563" d="M73.117,23.835H79.64v5.528h-2.5l-4.021-5.528Z" fill="#4acfff"/>
<rect id="Rectangle_17" data-name="Rectangle 17" width="52.569" height="3.159" rx="1" transform="translate(40.498 20.676)" fill="#c0dceb" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_564" data-name="Tracé 564" d="M71.742,32.523H78.06v48.2H71.742Z" fill="#adc4d9" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_565" data-name="Tracé 565" d="M78.061,20.676H71.742l2.526-8.208a.663.663,0,0,1,1.267,0Z" fill="#c0dceb" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_566" data-name="Tracé 566" d="M41.825,20.676,74.99,12.4m14.128,8.278L74.99,12.4" fill="none" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_567" data-name="Tracé 567" d="M69.246,32.523h9.731a.663.663,0,0,0,.663-.663v-2.5H68.583v2.5A.663.663,0,0,0,69.246,32.523Z" fill="#f0f0f0" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_568" data-name="Tracé 568" d="M70.163,23.835l-1.58,5.529H79.64V23.835Z" fill="none" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_569" data-name="Tracé 569" d="M45.237,23.835h6.319v3.159H45.237Zm4.8,19.593a1.58,1.58,0,1,1-1.579-1.579,1.58,1.58,0,0,1,1.579,1.579Z" fill="#f0f0f0" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_570" data-name="Tracé 570" d="M71.742,47.53l6.319,6.318m0-6.318-6.319,6.318m0,.79,6.319,6.319m0-6.319-6.319,6.319m0,.176,6.319,6.319m0-6.319-6.319,6.319m0,.314,6.319,6.319m0-6.319-6.319,6.319m0-33.664,6.319,6.319m0-6.319L71.742,46.74m0-13.427,6.319,6.318m0-6.318-6.319,6.318" fill="none" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_571" data-name="Tracé 571" d="M67.53,78.065H89.746a.5.5,0,0,1,.5.5v2.153H67.03V78.565A.5.5,0,0,1,67.53,78.065Z" fill="#adc4d9" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_572" data-name="Tracé 572" d="M70.847,75.412H78.47a.5.5,0,0,1,.5.5v2.153H70.347V75.912A.5.5,0,0,1,70.847,75.412Z" fill="#adc4d9" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_573" data-name="Tracé 573" d="M82.286,75.412h6.633v2.653H82.286Zm0-2.654h6.633v2.653H82.286Zm0-2.653h6.633v2.653H82.286Z" fill="#e0e0e0" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_574" data-name="Tracé 574" d="M48.458,45.008,53.1,49.145H43.815l4.643-4.137Z" fill="none" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_575" data-name="Tracé 575" d="M39.666,49.145H57.249v3.317H39.666Z" fill="#ffe500" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_576" data-name="Tracé 576" d="M48.458,41.849V26.994" fill="none" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<ellipse id="Ellipse_9" data-name="Ellipse 9" cx="16.583" cy="1.99" rx="16.583" ry="1.99" transform="translate(61.892 80.32)" fill="#45413c" opacity="0.15"/>
<ellipse id="Ellipse_10" data-name="Ellipse 10" cx="16.583" cy="1.99" rx="16.583" ry="1.99" transform="translate(8.828 80.32)" fill="#45413c" opacity="0.15"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 7.3 KiB

44
public/cardResponsive.svg Normal file
View File

@@ -0,0 +1,44 @@
<svg xmlns="http://www.w3.org/2000/svg" width="81" height="82.858" viewBox="0 0 81 82.858">
<g id="cardResponsive" transform="translate(-9.5 -8.571)">
<path id="Tracé_474" data-name="Tracé 474" d="M10,50.929a40,40,0,1,0,40-40,40,40,0,0,0-40,40Z" fill="#e8f4fa" stroke="#daedf7" stroke-width="1"/>
<path id="Tracé_475" data-name="Tracé 475" d="M25.881,85.354c0,.957,11.753,1.733,26.25,1.733s26.25-.776,26.25-1.733-11.752-1.733-26.25-1.733-26.25.776-26.25,1.733Z" fill="#525252" opacity="0.15"/>
<path id="Tracé_476" data-name="Tracé 476" d="M45.831,63.4a1.325,1.325,0,0,0,1.325,1.325H85.581A1.325,1.325,0,0,0,86.906,63.4V20.334L75.643,9.071H47.156A1.325,1.325,0,0,0,45.831,10.4Z" fill="#fff"/>
<path id="Tracé_477" data-name="Tracé 477" d="M45.831,63.4a1.325,1.325,0,0,0,1.325,1.325H85.581A1.325,1.325,0,0,0,86.906,63.4V20.334L75.643,9.071H47.156A1.325,1.325,0,0,0,45.831,10.4Z" fill="none" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_478" data-name="Tracé 478" d="M86.906,20.334H75.643V9.071Z" fill="#e8f4fa" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_479" data-name="Tracé 479" d="M69.981,45.821h9.45" fill="#b8ecff" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_480" data-name="Tracé 480" d="M69.981,51.071h9.45" fill="#b8ecff" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_481" data-name="Tracé 481" d="M69.981,56.321h5.25" fill="#b8ecff" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_482" data-name="Tracé 482" d="M51.083,22.721H76.9v17.85H51.083Z" fill="#b8ecff" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_483" data-name="Tracé 483" d="M54.026,26.5a1.243,1.243,0,0,0-.453.088,2.1,2.1,0,0,0-2.066-1.768,2.07,2.07,0,0,0-.42.046V29.02h2.939a1.26,1.26,0,1,0,0-2.52Z" fill="#fff" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_484" data-name="Tracé 484" d="M76.9,35.32,66.386,26.13a4.2,4.2,0,0,0-5.453-.064l-9.85,8.208v5.247a1.05,1.05,0,0,0,1.05,1.05H75.854a1.05,1.05,0,0,0,1.05-1.05v-4.2" fill="#627b8c"/>
<path id="Tracé_485" data-name="Tracé 485" d="M55.062,30.958,60.041,29.2a1.047,1.047,0,0,1,.933.116l2.13,1.42a1.051,1.051,0,0,0,1.165,0L66.4,29.314a1.052,1.052,0,0,1,.937-.115l4.343,1.555L66.386,26.13a4.2,4.2,0,0,0-5.453-.064Z" fill="#fff"/>
<path id="Tracé_486" data-name="Tracé 486" d="M76.9,35.32,66.386,26.13a4.2,4.2,0,0,0-5.453-.064l-9.85,8.208v5.247a1.05,1.05,0,0,0,1.05,1.05H75.854a1.05,1.05,0,0,0,1.05-1.05v-4.2" fill="none" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_487" data-name="Tracé 487" d="M72.366,67.871a3.15,3.15,0,0,1-3.15,3.15H26.166a3.15,3.15,0,0,1-3.15-3.15V43.516h49.35Z" fill="#b8ecff"/>
<path id="Tracé_488" data-name="Tracé 488" d="M23.014,43.511h49.35v3.58H23.014Z" fill="#80ddff"/>
<path id="Tracé_489" data-name="Tracé 489" d="M72.366,37.216a3.15,3.15,0,0,0-3.15-3.15H26.166a3.15,3.15,0,0,0-3.15,3.15v6.3h49.35Z" fill="#fff" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_490" data-name="Tracé 490" d="M26.166,38.791a1.575,1.575,0,1,0,1.575-1.575,1.575,1.575,0,0,0-1.575,1.575Z" fill="#ff6242"/>
<path id="Tracé_491" data-name="Tracé 491" d="M31.416,38.791a1.575,1.575,0,1,0,1.575-1.575A1.575,1.575,0,0,0,31.416,38.791Z" fill="#ffe500"/>
<path id="Tracé_492" data-name="Tracé 492" d="M36.666,38.791a1.575,1.575,0,1,0,1.575-1.575A1.575,1.575,0,0,0,36.666,38.791Z" fill="#6dd627"/>
<path id="Tracé_493" data-name="Tracé 493" d="M72.366,67.871a3.15,3.15,0,0,1-3.15,3.15H26.166a3.15,3.15,0,0,1-3.15-3.15V43.516h49.35Z" fill="none" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_494" data-name="Tracé 494" d="M59.31,48.723h9.061" fill="none" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_495" data-name="Tracé 495" d="M57.381,54.221h10.99" fill="none" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_496" data-name="Tracé 496" d="M57.381,59.72h6.3" fill="none" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_497" data-name="Tracé 497" d="M27.359,46.871H53.181v17.85H27.359Z" fill="#e5f8ff" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_498" data-name="Tracé 498" d="M30.3,50.651a1.244,1.244,0,0,0-.454.088,2.1,2.1,0,0,0-2.066-1.768,2.07,2.07,0,0,0-.42.046v4.154H30.3a1.26,1.26,0,1,0,0-2.52Z" fill="#fff" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_499" data-name="Tracé 499" d="M53.179,59.47,42.662,50.28a4.2,4.2,0,0,0-5.453-.064l-9.85,8.208v5.247a1.05,1.05,0,0,0,1.05,1.05H52.131a1.051,1.051,0,0,0,1.05-1.05v-4.2" fill="#627b8c"/>
<path id="Tracé_500" data-name="Tracé 500" d="M31.339,55.108l4.978-1.761a1.047,1.047,0,0,1,.933.116l2.13,1.42a1.051,1.051,0,0,0,1.165,0l2.129-1.419a1.05,1.05,0,0,1,.936-.115L47.953,54.9l-5.291-4.62a4.2,4.2,0,0,0-5.453-.064Z" fill="#fff"/>
<path id="Tracé_501" data-name="Tracé 501" d="M53.179,59.47,42.662,50.28a4.2,4.2,0,0,0-5.453-.064l-9.85,8.208v5.247a1.05,1.05,0,0,0,1.05,1.05H52.131a1.051,1.051,0,0,0,1.05-1.05v-4.2" fill="none" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_502" data-name="Tracé 502" d="M15.233,79.707a3.363,3.363,0,0,1-3.362-3.361V61.782a3.359,3.359,0,0,1,3.361-3.36H47.72a3.36,3.36,0,0,1,3.36,3.361V76.346a3.361,3.361,0,0,1-3.361,3.36Z" fill="#fff"/>
<path id="Tracé_503" data-name="Tracé 503" d="M18.592,58.422H44.347V79.707H18.592Z" fill="#b8ecff"/>
<path id="Tracé_504" data-name="Tracé 504" d="M15.233,79.707a3.363,3.363,0,0,1-3.362-3.361V61.782a3.359,3.359,0,0,1,3.361-3.36H47.72a3.36,3.36,0,0,1,3.36,3.361V76.346a3.361,3.361,0,0,1-3.361,3.36Z" fill="none" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_505" data-name="Tracé 505" d="M18.592,58.422H44.347V79.707H18.592Z" fill="none" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_506" data-name="Tracé 506" d="M47.719,67.944v5.6" fill="none" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_507" data-name="Tracé 507" d="M47.719,64.355a.788.788,0,0,0,0,1.576h0a.788.788,0,1,0,0-1.576Z" fill="#45413c"/>
<path id="Tracé_508" data-name="Tracé 508" d="M15.232,70.324a1.26,1.26,0,1,0-1.26-1.26,1.26,1.26,0,0,0,1.26,1.26Z" fill="#fff" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_509" data-name="Tracé 509" d="M18.594,73.42l9.823-8.185a4.188,4.188,0,0,1,5.438.063l10.493,9.169V79.7H18.594Z" fill="#627b8c"/>
<path id="Tracé_510" data-name="Tracé 510" d="M39.134,69.906,34.8,68.355a1.049,1.049,0,0,0-.934.115l-2.123,1.415a1.046,1.046,0,0,1-1.162,0l-2.125-1.416a1.047,1.047,0,0,0-.93-.116l-4.953,1.752,5.845-4.87a4.176,4.176,0,0,1,5.436.063Z" fill="#fff"/>
<path id="Tracé_511" data-name="Tracé 511" d="M18.594,73.42l9.823-8.185a4.188,4.188,0,0,1,5.438.063l10.493,9.169V79.7H18.594Z" fill="none" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_512" data-name="Tracé 512" d="M18.592,58.427H44.348V79.7H18.592Z" fill="none" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_513" data-name="Tracé 513" d="M21.527,63.531a1.243,1.243,0,0,0-.453.088,2.074,2.074,0,0,0-2.48-1.718v4.144h2.933a1.257,1.257,0,1,0,0-2.514Z" fill="#fff" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 8.0 KiB

43
public/cardServer.svg Normal file
View File

@@ -0,0 +1,43 @@
<svg xmlns="http://www.w3.org/2000/svg" width="94.38" height="84.83" viewBox="0 0 94.38 84.83">
<g id="cardServer" transform="translate(-2.81 -7.585)">
<path id="Tracé_514" data-name="Tracé 514" d="M9.875,50a40,40,0,1,0,40-40,40,40,0,0,0-40,40Z" fill="#e8f4fa" stroke="#daedf7" stroke-width="1"/>
<path id="Tracé_515" data-name="Tracé 515" d="M55.408,13.82a37,37,0,0,0-11.092,0" fill="none" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_516" data-name="Tracé 516" d="M15.921,36.286a36.539,36.539,0,0,1,4.723-8.328" fill="none" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_517" data-name="Tracé 517" d="M55.408,86.18a37,37,0,0,1-11.092,0" fill="none" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_518" data-name="Tracé 518" d="M15.921,63.714a36.539,36.539,0,0,0,4.723,8.328" fill="none" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_519" data-name="Tracé 519" d="M83.8,36.286a36.581,36.581,0,0,0-4.724-8.328" fill="none" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_520" data-name="Tracé 520" d="M83.8,63.714a36.58,36.58,0,0,1-4.724,8.328" fill="none" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_521" data-name="Tracé 521" d="M25.786,50h6.976" fill="none" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_522" data-name="Tracé 522" d="M36.08,27.049l4.213,7.016" fill="none" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_523" data-name="Tracé 523" d="M36.08,72.951l4.213-7.016" fill="none" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_524" data-name="Tracé 524" d="M73.938,50H66.962" fill="none" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_525" data-name="Tracé 525" d="M63.644,27.049l-4.213,7.016" fill="none" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_526" data-name="Tracé 526" d="M63.644,72.951l-4.213-7.016" fill="none" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_527" data-name="Tracé 527" d="M23.155,56.747a1.373,1.373,0,0,1-1.369,1.369H4.678A1.373,1.373,0,0,1,3.31,56.747V45.114H23.155Z" fill="#4acfff" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_528" data-name="Tracé 528" d="M23.155,45.114H3.31V43.253a1.373,1.373,0,0,1,1.368-1.369H21.786a1.373,1.373,0,0,1,1.369,1.369Z" fill="#fff" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_529" data-name="Tracé 529" d="M40.885,22.948a1.373,1.373,0,0,1-1.369,1.369H22.408a1.373,1.373,0,0,1-1.368-1.369V11.315H40.885Z" fill="#4acfff" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_530" data-name="Tracé 530" d="M40.885,11.315H21.04V9.454a1.373,1.373,0,0,1,1.368-1.369H39.516a1.373,1.373,0,0,1,1.369,1.369Z" fill="#fff" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_531" data-name="Tracé 531" d="M39.782,90.546a1.373,1.373,0,0,1-1.368,1.369H21.306a1.373,1.373,0,0,1-1.369-1.369V78.913H39.782Z" fill="#4acfff" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_532" data-name="Tracé 532" d="M39.782,78.913H19.937V77.052a1.373,1.373,0,0,1,1.369-1.369H38.414a1.373,1.373,0,0,1,1.368,1.369Z" fill="#fff" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_533" data-name="Tracé 533" d="M76.845,56.747a1.373,1.373,0,0,0,1.369,1.369H95.322a1.373,1.373,0,0,0,1.368-1.369V45.114H76.845Z" fill="#4acfff" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_534" data-name="Tracé 534" d="M76.845,45.114H96.69V43.253a1.373,1.373,0,0,0-1.368-1.369H78.214a1.373,1.373,0,0,0-1.369,1.369Z" fill="#fff" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_535" data-name="Tracé 535" d="M58.84,22.948a1.372,1.372,0,0,0,1.368,1.369H77.316a1.373,1.373,0,0,0,1.369-1.369V11.315H58.84Z" fill="#4acfff" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_536" data-name="Tracé 536" d="M58.84,11.315H78.685V9.454a1.373,1.373,0,0,0-1.369-1.369H60.208A1.372,1.372,0,0,0,58.84,9.454Z" fill="#fff" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_537" data-name="Tracé 537" d="M59.942,90.546a1.373,1.373,0,0,0,1.369,1.369H78.418a1.373,1.373,0,0,0,1.369-1.369V78.913H59.942Z" fill="#4acfff" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_538" data-name="Tracé 538" d="M59.942,78.913H79.787V77.052a1.373,1.373,0,0,0-1.369-1.369H61.311a1.373,1.373,0,0,0-1.369,1.369Z" fill="#fff" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_539" data-name="Tracé 539" d="M34.978,36.77H64.745V63.23H34.978Z" fill="#656769"/>
<path id="Tracé_540" data-name="Tracé 540" d="M64.746,39.526H34.978v-.551a2.2,2.2,0,0,1,2.2-2.2H62.541a2.2,2.2,0,0,1,2.2,2.2Z" fill="#87898c"/>
<path id="Tracé_541" data-name="Tracé 541" d="M38.286,58.82H44.9" fill="none" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_542" data-name="Tracé 542" d="M38.286,50H44.9" fill="none" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_543" data-name="Tracé 543" d="M38.286,41.18H44.9" fill="none" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_544" data-name="Tracé 544" d="M59.233,41.18a1.654,1.654,0,1,0,1.654-1.654,1.654,1.654,0,0,0-1.654,1.654Z" fill="#4acfff" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_545" data-name="Tracé 545" d="M53.721,41.18a1.654,1.654,0,1,0,1.654-1.654,1.654,1.654,0,0,0-1.654,1.654Z" fill="#00f5bc" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_546" data-name="Tracé 546" d="M59.233,50a1.654,1.654,0,1,0,1.654-1.654A1.654,1.654,0,0,0,59.233,50Z" fill="#4acfff" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_547" data-name="Tracé 547" d="M53.721,50a1.654,1.654,0,1,0,1.654-1.654A1.654,1.654,0,0,0,53.721,50Z" fill="#00f5bc" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_548" data-name="Tracé 548" d="M59.233,58.82a1.654,1.654,0,1,0,1.654-1.654A1.654,1.654,0,0,0,59.233,58.82Z" fill="#4acfff" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_549" data-name="Tracé 549" d="M53.721,58.82a1.654,1.654,0,1,0,1.654-1.654A1.654,1.654,0,0,0,53.721,58.82Z" fill="#00f5bc" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_550" data-name="Tracé 550" d="M62.541,63.23H37.183a2.2,2.2,0,0,1-2.2-2.2V54.41H64.746v6.615a2.2,2.2,0,0,1-2.205,2.2Z" fill="none" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_551" data-name="Tracé 551" d="M34.978,45.59H64.745v8.82H34.978Z" fill="none" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_552" data-name="Tracé 552" d="M64.746,45.59H34.978V38.975a2.2,2.2,0,0,1,2.2-2.2H62.541a2.2,2.2,0,0,1,2.2,2.2Z" fill="none" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 8.0 KiB

29
public/cardStripe.svg Normal file
View File

@@ -0,0 +1,29 @@
<svg xmlns="http://www.w3.org/2000/svg" width="81" height="81" viewBox="0 0 81 81">
<g id="cardStripe" transform="translate(-9.5 -9.5)">
<path id="Tracé_423" data-name="Tracé 423" d="M10,50A40,40,0,1,0,50,10,40,40,0,0,0,10,50Z" fill="#e8f4fa" stroke="#daedf7" stroke-width="1"/>
<path id="Tracé_424" data-name="Tracé 424" d="M80.87,71.606a3.308,3.308,0,0,1-3.308,3.308H22.44a3.308,3.308,0,0,1-3.308-3.308V32.8H80.87Z" fill="#4acfff"/>
<path id="Tracé_425" data-name="Tracé 425" d="M19.13,32.8H80.868v3.759H19.13Z" fill="#00b8f0"/>
<path id="Tracé_426" data-name="Tracé 426" d="M80.87,26.188a3.308,3.308,0,0,0-3.308-3.307H22.44a3.308,3.308,0,0,0-3.308,3.307V32.8H80.87Z" fill="#fff" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_427" data-name="Tracé 427" d="M22.439,27.842a1.654,1.654,0,1,0,1.654-1.654,1.654,1.654,0,0,0-1.654,1.654Z" fill="#ff6242"/>
<path id="Tracé_428" data-name="Tracé 428" d="M27.952,27.842a1.654,1.654,0,1,0,1.654-1.654A1.654,1.654,0,0,0,27.952,27.842Z" fill="#ffe500"/>
<path id="Tracé_429" data-name="Tracé 429" d="M33.464,27.842a1.654,1.654,0,1,0,1.654-1.654,1.654,1.654,0,0,0-1.654,1.654Z" fill="#6dd627"/>
<path id="Tracé_430" data-name="Tracé 430" d="M80.87,71.606a3.308,3.308,0,0,1-3.308,3.308H22.44a3.308,3.308,0,0,1-3.308-3.308V32.8H80.87Z" fill="none" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_431" data-name="Tracé 431" d="M31.258,52.864A17.64,17.64,0,1,0,48.9,35.224a17.64,17.64,0,0,0-17.64,17.64Z" fill="#00b8f0"/>
<path id="Tracé_432" data-name="Tracé 432" d="M37.009,84.125c0,.913,5.43,1.654,12.128,1.654s12.128-.741,12.128-1.654-5.43-1.654-12.128-1.654-12.128.741-12.128,1.654Z" fill="#525252" opacity="0.15"/>
<path id="Tracé_433" data-name="Tracé 433" d="M34.014,52.864A14.884,14.884,0,1,0,48.9,37.98,14.884,14.884,0,0,0,34.014,52.864Z" fill="#ffe500"/>
<path id="Tracé_434" data-name="Tracé 434" d="M48.9,37.98A14.884,14.884,0,0,0,38.373,63.388L59.422,42.34A14.835,14.835,0,0,0,48.9,37.98Z" fill="#fff48c"/>
<path id="Tracé_435" data-name="Tracé 435" d="M34.014,52.864A14.884,14.884,0,1,0,48.9,37.98,14.884,14.884,0,0,0,34.014,52.864Z" fill="none" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_436" data-name="Tracé 436" d="M34.014,52.864A14.884,14.884,0,1,0,48.9,37.98,14.884,14.884,0,0,0,34.014,52.864Z" fill="none" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_437" data-name="Tracé 437" d="M37.114,52.864A11.783,11.783,0,1,0,48.9,41.081,11.783,11.783,0,0,0,37.114,52.864Z" fill="none" stroke="#ebcb00" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_438" data-name="Tracé 438" d="M52.618,49.142a3.721,3.721,0,0,0-3.721-3.72h-.271a3.45,3.45,0,0,0-1.543,6.535l3.629,1.813A3.45,3.45,0,0,1,49.169,60.3H48.9a3.721,3.721,0,0,1-3.72-3.721" fill="none" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_439" data-name="Tracé 439" d="M48.9,45.422v-1.86" fill="none" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_440" data-name="Tracé 440" d="M48.9,62.165v-1.86" fill="none" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_441" data-name="Tracé 441" d="M51.151,31.058c0,1.444-1.17,4.712-2.614,4.712s-2.614-3.268-2.614-4.712a2.614,2.614,0,1,1,5.228,0Z" fill="#fff" stroke="#00658a" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_442" data-name="Tracé 442" d="M31.046,39.846c1.251.722,3.5,3.37,2.774,4.62s-4.138.63-5.388-.092a2.614,2.614,0,1,1,2.614-4.528Z" fill="#fff" stroke="#00658a" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_443" data-name="Tracé 443" d="M28.6,61.652c1.251-.722,4.666-1.343,5.388-.092s-1.523,3.9-2.774,4.619A2.614,2.614,0,1,1,28.6,61.652Z" fill="#fff" stroke="#00658a" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_444" data-name="Tracé 444" d="M46.267,74.669c0-1.444,1.171-4.712,2.615-4.712S51.5,73.225,51.5,74.669a2.615,2.615,0,1,1-5.23,0Z" fill="#fff" stroke="#00658a" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_445" data-name="Tracé 445" d="M68.815,44.076c-1.251.722-4.666,1.342-5.388.092s1.523-3.9,2.773-4.62a2.614,2.614,0,0,1,2.615,4.528Z" fill="#fff" stroke="#00658a" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_446" data-name="Tracé 446" d="M55.1,56.541a.55.55,0,0,0-.671.671l3.4,12.92a.549.549,0,0,0,.92.249l3.206-3.207,4.272,4.271a.547.547,0,0,0,.776,0l2.33-2.329a.549.549,0,0,0,0-.777l-4.271-4.271,3.883-3.883Z" fill="#525252" opacity="0.3"/>
<path id="Tracé_447" data-name="Tracé 447" d="M57.03,51.966a1.1,1.1,0,0,0-1.346,1.346L58.6,64.384a1.1,1.1,0,0,0,1.846.5l2.526-2.526,3.88,3.88a1.1,1.1,0,0,0,1.559,0l1.547-1.547a1.1,1.1,0,0,0,0-1.559l-3.88-3.88L68.6,56.725a1.1,1.1,0,0,0-.5-1.846Z" fill="#fff" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.2 KiB

30
public/cardTheme.svg Normal file
View File

@@ -0,0 +1,30 @@
<svg xmlns="http://www.w3.org/2000/svg" width="81" height="81" viewBox="0 0 81 81">
<g id="cardTheme" transform="translate(-9.5 -9.5)">
<path id="Tracé_448" data-name="Tracé 448" d="M10,50A40,40,0,1,0,50,10,40,40,0,0,0,10,50Z" fill="#e8f4fa" stroke="#daedf7" stroke-width="1"/>
<path id="Tracé_449" data-name="Tracé 449" d="M77.714,52.286a2,2,0,0,1,1.98,2.284,29.7,29.7,0,0,1-5.676,13.676A2,2,0,0,1,71,68.46l-5.016-5.016a2,2,0,0,1-.24-2.535,18.854,18.854,0,0,0,2.9-7,2,2,0,0,1,1.964-1.619Z" fill="#00dfeb" stroke="#45413c" stroke-width="1"/>
<path id="Tracé_450" data-name="Tracé 450" d="M68.174,71.29a2,2,0,0,1-.214,3.01,29.693,29.693,0,0,1-13.676,5.68A2,2,0,0,1,52,78V70.9a2,2,0,0,1,1.622-1.961,18.86,18.86,0,0,0,7-2.9,2,2,0,0,1,2.534.243Z" fill="#6dd627" stroke="#45413c" stroke-width="1"/>
<path id="Tracé_451" data-name="Tracé 451" d="M46.381,68.936A2,2,0,0,1,48,70.9V78a2,2,0,0,1-2.284,1.98A29.693,29.693,0,0,1,32.04,74.3a2,2,0,0,1-.214-3.014l5.015-5.015a2,2,0,0,1,2.534-.243,18.85,18.85,0,0,0,7.006,2.908Z" fill="#ffe500" stroke="#45413c" stroke-width="1"/>
<path id="Tracé_452" data-name="Tracé 452" d="M45.716,20.592A2,2,0,0,1,48,22.572v7.1a2,2,0,0,1-1.622,1.961,18.843,18.843,0,0,0-7,2.9,2,2,0,0,1-2.534-.244l-5.015-5.014a2,2,0,0,1,.214-3.014,29.693,29.693,0,0,1,13.673-5.669Z" fill="#ff6196" stroke="#45413c" stroke-width="1"/>
<path id="Tracé_453" data-name="Tracé 453" d="M34.011,37.127a2,2,0,0,1,.243,2.534,18.85,18.85,0,0,0-2.9,7.006,2,2,0,0,1-1.964,1.619h-7.1A2,2,0,0,1,20.306,46a29.693,29.693,0,0,1,5.676-13.676A2,2,0,0,1,29,32.112Z" fill="#ff6242" stroke="#45413c" stroke-width="1"/>
<path id="Tracé_454" data-name="Tracé 454" d="M34.254,60.911a2,2,0,0,1-.243,2.534L29,68.46a2,2,0,0,1-3.014-.214,29.7,29.7,0,0,1-5.68-13.676,2,2,0,0,1,1.98-2.284h7.1A2,2,0,0,1,31.35,53.9,18.85,18.85,0,0,0,34.254,60.911Z" fill="#ff8a14" stroke="#45413c" stroke-width="1"/>
<path id="Tracé_455" data-name="Tracé 455" d="M39.75,85.5c0,.828,4.925,1.5,11,1.5s11-.672,11-1.5-4.925-1.5-11-1.5S39.75,84.672,39.75,85.5Z" fill="#45413c" opacity="0.15"/>
<path id="Tracé_456" data-name="Tracé 456" d="M46.774,66.859a3.45,3.45,0,0,1-6.9,0c0-1.5,2.143-4.494,3.051-5.694a.5.5,0,0,1,.8,0C44.631,62.365,46.774,65.357,46.774,66.859Z" fill="#00b8f0"/>
<path id="Tracé_457" data-name="Tracé 457" d="M43.324,68.054A3.451,3.451,0,0,1,40.1,65.826a3.159,3.159,0,0,0-.223,1.033,3.45,3.45,0,0,0,6.9,0,3.159,3.159,0,0,0-.223-1.033,3.451,3.451,0,0,1-3.23,2.228Z" fill="#009fd9"/>
<path id="Tracé_458" data-name="Tracé 458" d="M46.774,66.859a3.45,3.45,0,0,1-6.9,0c0-1.5,2.143-4.494,3.051-5.694a.5.5,0,0,1,.8,0C44.631,62.365,46.774,65.357,46.774,66.859Z" fill="none" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_459" data-name="Tracé 459" d="M46.625,57.219a1.15,1.15,0,0,1-1.326-.227l-.773-.787a1.151,1.151,0,0,1-.2-1.329l1.44-2.815a2.3,2.3,0,0,1,.437-.594L60.729,37.238,64.6,41.177,50.067,55.406a2.265,2.265,0,0,1-.6.425Z" fill="#e5f8ff"/>
<path id="Tracé_460" data-name="Tracé 460" d="M50.144,47.6,46.2,51.467a2.26,2.26,0,0,0-.436.6l-1.441,2.813a1.152,1.152,0,0,0,.2,1.33l.773.787a1.148,1.148,0,0,0,1.325.227l2.842-1.388a2.276,2.276,0,0,0,.6-.425l7.967-7.8Z" fill="#00b8f0"/>
<path id="Tracé_461" data-name="Tracé 461" d="M47.7,50h7.889l2.447-2.4h-7.89Z" fill="#4acfff"/>
<path id="Tracé_462" data-name="Tracé 462" d="M75.433,34.426a11.042,11.042,0,0,0-2.373,3.436L72.427,39.3l-9.66-9.839,1.453-.608a11.009,11.009,0,0,0,3.48-2.3l2.915-2.861a5.52,5.52,0,1,1,7.734,7.878Z" fill="#525252"/>
<path id="Tracé_463" data-name="Tracé 463" d="M70.251,45.3a1.148,1.148,0,0,1-1.626-.016L56.7,33.135a1.149,1.149,0,0,1,.015-1.627L60,28.285a1.15,1.15,0,0,1,1.626.016L73.548,40.446a1.15,1.15,0,0,1-.014,1.626Z" fill="#656769"/>
<path id="Tracé_464" data-name="Tracé 464" d="M58.546,39.376l3.867,3.939L64.6,41.177l-3.868-3.939Z" fill="#b8ecff"/>
<path id="Tracé_465" data-name="Tracé 465" d="M70.251,45.3a1.148,1.148,0,0,1-1.626-.016L56.7,33.135a1.149,1.149,0,0,1,.015-1.627L60,28.285a1.15,1.15,0,0,1,1.626.016L73.548,40.446a1.15,1.15,0,0,1-.014,1.626Z" fill="none" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_466" data-name="Tracé 466" d="M63.237,29.942l-2.955,2.9" fill="none" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_467" data-name="Tracé 467" d="M66.136,32.9l-2.954,2.9" fill="none" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_468" data-name="Tracé 468" d="M69.037,35.851l-2.954,2.9" fill="none" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_469" data-name="Tracé 469" d="M71.937,38.8l-2.954,2.9" fill="none" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_470" data-name="Tracé 470" d="M79.324,24.976a5.525,5.525,0,0,0-8.711-1.289L67.7,26.548a11.009,11.009,0,0,1-3.478,2.309l-1.453.608,2.04,2.078,1.453-.607a11.03,11.03,0,0,0,3.479-2.309l2.914-2.861a5.506,5.506,0,0,1,6.669-.79Z" fill="#87898c"/>
<path id="Tracé_471" data-name="Tracé 471" d="M75.433,34.426a11.042,11.042,0,0,0-2.373,3.436L72.427,39.3l-9.66-9.839,1.453-.608a11.009,11.009,0,0,0,3.48-2.3l2.915-2.861a5.52,5.52,0,1,1,7.734,7.878Z" fill="none" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_472" data-name="Tracé 472" d="M46.625,57.219a1.15,1.15,0,0,1-1.326-.227l-.773-.787a1.151,1.151,0,0,1-.2-1.329l1.44-2.815a2.3,2.3,0,0,1,.437-.594L60.729,37.238,64.6,41.177,50.067,55.406a2.265,2.265,0,0,1-.6.425Z" fill="none" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Tracé_473" data-name="Tracé 473" d="M50.144,47.6,46.2,51.467a2.26,2.26,0,0,0-.436.6l-1.441,2.813a1.152,1.152,0,0,0,.2,1.33l.773.787a1.148,1.148,0,0,0,1.325.227l2.842-1.388a2.276,2.276,0,0,0,.6-.425l7.967-7.8Z" fill="none" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.0 KiB

BIN
public/favicon-16x16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
public/favicon-32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

1
public/landing/TS.svg Normal file
View File

@@ -0,0 +1 @@
<svg id="SvgjsSvg1006" width="288" height="288" xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.com/svgjs"><defs id="SvgjsDefs1007"></defs><g id="SvgjsG1008" transform="matrix(1,0,0,1,0,0)"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="288" height="288"><path d="M1.125 0C.502 0 0 .502 0 1.125v21.75C0 23.498.502 24 1.125 24h21.75c.623 0 1.125-.502 1.125-1.125V1.125C24 .502 23.498 0 22.875 0zm17.363 9.75c.612 0 1.154.037 1.627.111a6.38 6.38 0 0 1 1.306.34v2.458a3.95 3.95 0 0 0-.643-.361 5.093 5.093 0 0 0-.717-.26 5.453 5.453 0 0 0-1.426-.2c-.3 0-.573.028-.819.086a2.1 2.1 0 0 0-.623.242c-.17.104-.3.229-.393.374a.888.888 0 0 0-.14.49c0 .196.053.373.156.529.104.156.252.304.443.444s.423.276.696.41c.273.135.582.274.926.416.47.197.892.407 1.266.628.374.222.695.473.963.753.268.279.472.598.614.957.142.359.214.776.214 1.253 0 .657-.125 1.21-.373 1.656a3.033 3.033 0 0 1-1.012 1.085 4.38 4.38 0 0 1-1.487.596c-.566.12-1.163.18-1.79.18a9.916 9.916 0 0 1-1.84-.164 5.544 5.544 0 0 1-1.512-.493v-2.63a5.033 5.033 0 0 0 3.237 1.2c.333 0 .624-.03.872-.09.249-.06.456-.144.623-.25.166-.108.29-.234.373-.38a1.023 1.023 0 0 0-.074-1.089 2.12 2.12 0 0 0-.537-.5 5.597 5.597 0 0 0-.807-.444 27.72 27.72 0 0 0-1.007-.436c-.918-.383-1.602-.852-2.053-1.405-.45-.553-.676-1.222-.676-2.005 0-.614.123-1.141.369-1.582.246-.441.58-.804 1.004-1.089a4.494 4.494 0 0 1 1.47-.629 7.536 7.536 0 0 1 1.77-.201zm-15.113.188h9.563v2.166H9.506v9.646H6.789v-9.646H3.375z" fill="#35558a" class="color000 svgShape"></path></svg></g></svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

3
public/landing/auth.svg Normal file
View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="25" viewBox="0 0 32 25">
<path id="Icon_awesome-user-lock" data-name="Icon awesome-user-lock" d="M10.625,12.5c3.452,0,7-2.548,7-6s-3.548-6-7-6a5.832,5.832,0,0,0-6,6A5.832,5.832,0,0,0,10.625,12.5Zm5,3c0-.522-.256-.545,0-1-.234-.024.244,0,0,0h-1a8.231,8.231,0,0,1-7,0h-1c-3.623,0-7,2.376-7,6v2a3.621,3.621,0,0,0,3,3h13c-.281-.473,0-1.45,0-2Zm14-1h-1v-4a4,4,0,0,0-8,0v4h-2c-.863,0-1,.137-1,1v8c0,.863.137,2,1,2h11a2.414,2.414,0,0,0,2-2v-8C31.625,14.637,30.488,14.5,29.625,14.5Zm-5,7a2.414,2.414,0,0,1-2-2,2.414,2.414,0,0,1,2-2c.863,0,1,1.137,1,2S25.488,21.5,24.625,21.5Zm1-7h-3v-4a2.414,2.414,0,0,1,2-2c.863,0,1,1.137,1,2Z" transform="translate(0.375 -0.5)" fill="#35558a"/>
</svg>

After

Width:  |  Height:  |  Size: 740 B

View File

@@ -0,0 +1,6 @@
<svg id="Icon_feather-server" data-name="Icon feather-server" xmlns="http://www.w3.org/2000/svg" width="34.5" height="34.5" viewBox="0 0 34.5 34.5">
<path id="Tracé_695" data-name="Tracé 695" d="M6.5,2.5h24a4,4,0,0,1,4,4v6a4,4,0,0,1-4,4H6.5a4,4,0,0,1-4-4v-6A4,4,0,0,1,6.5,2.5Zm24,12a2,2,0,0,0,2-2v-6a2,2,0,0,0-2-2H6.5a2,2,0,0,0-2,2v6a2,2,0,0,0,2,2Z" fill="#35558a"/>
<path id="Tracé_696" data-name="Tracé 696" d="M6.5,20.5h24a4,4,0,0,1,4,4v6a4,4,0,0,1-4,4H6.5a4,4,0,0,1-4-4v-6A4,4,0,0,1,6.5,20.5Zm24,12a2,2,0,0,0,2-2v-6a2,2,0,0,0-2-2H6.5a2,2,0,0,0-2,2v6a2,2,0,0,0,2,2Z" fill="#35558a"/>
<path id="Tracé_697" data-name="Tracé 697" fill="#35558a"/>
<path id="Tracé_698" data-name="Tracé 698" fill="#35558a"/>
</svg>

After

Width:  |  Height:  |  Size: 730 B

8
public/landing/blog.svg Normal file
View File

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" width="27" height="27" viewBox="0 0 27 27">
<g id="Icon_feather-pen-tool" data-name="Icon feather-pen-tool" transform="translate(-1.701 -1.701)">
<path id="Tracé_699" data-name="Tracé 699" d="M21.5,31.5l-.071,0a1,1,0,0,1-.729-.4l-3-4a1,1,0,0,1,.093-1.307l8-8A1,1,0,0,1,27.1,17.7l4,3a1,1,0,0,1,.107,1.507l-9,9A1,1,0,0,1,21.5,31.5Zm-1.68-4.906,1.788,2.384,7.369-7.369L26.594,19.82Z" transform="translate(-2.799 -2.799)" fill="#35558a"/>
<path id="Tracé_700" data-name="Tracé 700" d="M16.7,23.7a1,1,0,0,1-.217-.024l-9-2a1,1,0,0,1-.747-.709l-5-18A1,1,0,0,1,2.968,1.737l18,5a1,1,0,0,1,.709.747l2,9a1,1,0,0,1-.269.924l-6,6A1,1,0,0,1,16.7,23.7ZM8.5,19.855l7.878,1.751,5.224-5.224L19.855,8.5,4.138,4.138Z" fill="#35558a"/>
<path id="Tracé_701" data-name="Tracé 701" d="M12.7,13.7a1,1,0,0,1-.707-.293l-10-10A1,1,0,0,1,3.408,1.993l10,10A1,1,0,0,1,12.7,13.7Z" fill="#35558a"/>
<path id="Tracé_702" data-name="Tracé 702" d="M15.66,12.66a5.194,5.194,0,0,1,2.5.626,2.7,2.7,0,0,1,1.5,2.374,4.462,4.462,0,0,1-4,4,2.7,2.7,0,0,1-2.374-1.5,5.194,5.194,0,0,1-.626-2.5A2.731,2.731,0,0,1,15.66,12.66Zm0,5a2.169,2.169,0,0,0,1.3-.7,2.169,2.169,0,0,0,.7-1.3c0-.572-1.056-1-2-1a1.191,1.191,0,0,0-.8.2,1.191,1.191,0,0,0-.2.8C14.66,16.6,15.088,17.66,15.66,17.66Z" transform="translate(-1.96 -1.96)" fill="#35558a"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

1132
public/landing/land-top.svg Normal file

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 118 KiB

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="14" viewBox="0 0 32 14">
<path id="Icon_ionic-ios-infinite" data-name="Icon ionic-ios-infinite" d="M31.993,13a6.73,6.73,0,0,0-5-2,6.719,6.719,0,0,0-5,2l-3,2c-.112.113-.112-.105,0,0l2,2c.105.105-.112.105,0,0l2-2a6.874,6.874,0,0,1,4-2c1.336,0,2.051,1.079,3,2s2,1.7,2,3-1.065,2.093-2,3-1.664,2-3,2a6.874,6.874,0,0,1-4-2l-9-8a6.763,6.763,0,0,0-5-2,6.719,6.719,0,0,0-5,2,6.609,6.609,0,0,0-2,5,6.619,6.619,0,0,0,2,5,6.73,6.73,0,0,0,5,2,6.719,6.719,0,0,0,5-2l3-2c.112-.112.112.105,0,0l-2-2c-.105-.105.113-.105,0,0l-2,2a6.874,6.874,0,0,1-4,2c-1.336,0-2.051-1.079-3-2s-2-1.7-2-3,1.065-2.086,2-3,1.664-2,3-2a6.874,6.874,0,0,1,4,2l9,8a6.763,6.763,0,0,0,5,2,6.719,6.719,0,0,0,5-2,6.583,6.583,0,0,0,2-5A6.558,6.558,0,0,0,31.993,13Z" transform="translate(-1.993 -11.004)" fill="#35558a"/>
</svg>

After

Width:  |  Height:  |  Size: 843 B

View File

@@ -0,0 +1,69 @@
<svg xmlns="http://www.w3.org/2000/svg" width="184.341" height="176.087" viewBox="0 0 184.341 176.087">
<g id="Groupe_43" data-name="Groupe 43" transform="translate(-160.118 -102.301)">
<g id="Groupe_41" data-name="Groupe 41">
<g id="Groupe_30" data-name="Groupe 30">
<circle id="Ellipse_18" data-name="Ellipse 18" cx="62.256" cy="62.256" r="62.256" transform="translate(163.573 190.345) rotate(-45)" fill="#baedf4"/>
<path id="Tracé_627" data-name="Tracé 627" d="M302.459,205.3v21a64.644,64.644,0,0,1-8,9v-30h0a47.529,47.529,0,0,0-6-6v-1l3-1a5.79,5.79,0,0,1,4,0c.606.262,1.37.8,2,1v3c2.888,2.365,4.965,3.845,5,4Z" fill="#7fd6cb" opacity="0.73"/>
</g>
<g id="Groupe_40" data-name="Groupe 40">
<g id="Groupe_31" data-name="Groupe 31">
<path id="Tracé_628" data-name="Tracé 628" d="M163.459,110.3l18,43,13-9,8,11,1-19,16-10Z" fill="#ffb031"/>
<path id="Tracé_629" data-name="Tracé 629" d="M197.459,142.3l-16,11,13-9,8,11,1-19-25-15Z" fill="#ff9433"/>
<path id="Tracé_630" data-name="Tracé 630" d="M203.459,141.3l-25-20,25,15Z" fill="#f73"/>
</g>
<g id="Groupe_32" data-name="Groupe 32">
<path id="Tracé_631" data-name="Tracé 631" d="M202.459,160.3c-.144-.835.216-1.191,0-2h1c.226.846.849,1.127,1,2Z" fill="#00c8b7"/>
<path id="Tracé_632" data-name="Tracé 632" d="M187.459,190.3a10.3,10.3,0,0,1-5-2l1-1c1.336.886,2.343.889,4,1Zm5-1v-2a13.225,13.225,0,0,0,4-3l1,1A19.928,19.928,0,0,1,192.459,189.3Zm-14-5a11.817,11.817,0,0,1-1-5c0-.131-.005.131,0,0v-1h1v1c0,.117,0-.113,0,0a8.829,8.829,0,0,0,1,4Zm50-1v-1c1.689-.008,4.261.128,6,0v1C232.691,183.432,230.178,183.293,228.459,183.3Zm-5,0h-1c-1.594-.133-2.623.239-4,0v-2a37.491,37.491,0,0,0,4,1h1Zm16,0v-2c1.668-.227,3.277.332,5,0v1C242.714,182.638,241.151,183.071,239.459,183.3Zm-26-2a22.842,22.842,0,0,1-4-2c-.2-.114-.8.121-1,0l1-1c.189.114-.193-.108,0,0a24.049,24.049,0,0,0,4,2Zm-13,0-1-1a22.4,22.4,0,0,0,2-4h1A31.418,31.418,0,0,1,200.459,181.3Zm49,0v-2c1.622-.394,3.294-.512,5-1v1C252.735,179.794,251.1,180.9,249.459,181.3Zm10-3v-1c1.648-.534,3.369-1.42,5-2v1C262.817,176.884,261.119,177.763,259.459,178.3Zm-55-2a36.027,36.027,0,0,0-4-2l-1-1,1-1c.155.079-.153-.083,0,0a38.628,38.628,0,0,1,4,3Zm-24-2-1-1c1.142-1.118,2.93-1.4,5-2l1,1C183.553,172.859,181.432,173.348,180.459,174.3Zm15-2a26.3,26.3,0,0,0-5-1h0v-1c1.785-.044,3.251-.393,5,0Zm8-1h-1c.3-1.676.991-3.307,1-5h1C204.45,168.068,203.769,169.552,203.459,171.3Z" fill="#00c8b7"/>
<path id="Tracé_633" data-name="Tracé 633" d="M269.459,174.3v-1c.807-.308,1.2-.685,2-1l1,1C271.652,173.616,270.271,173.992,269.459,174.3Z" fill="#00c8b7"/>
</g>
<path id="Tracé_634" data-name="Tracé 634" d="M310.459,206.3c-.66,0-.935-.329-1-1-.069-.712.287-1.931,1-2a29.483,29.483,0,0,0,20-10,1.414,1.414,0,0,1,2,2,32.868,32.868,0,0,1-22,11C310.416,206.305,310.5,206.3,310.459,206.3Z" fill="#00c8b7"/>
<g id="Groupe_36" data-name="Groupe 36">
<g id="Groupe_35" data-name="Groupe 35">
<path id="Tracé_635" data-name="Tracé 635" d="M294.459,205.3l-45,27-45-27c0-.022-.007.022,0,0,.037-.169,2.2-1.91,6-5h0c12.475-10.129,39-31,39-31s26.525,20.871,39,31c3.862,3.134,5.963,4.823,6,5C294.466,205.323,294.459,205.279,294.459,205.3Z" fill="#7a4c08"/>
<g id="Groupe_33" data-name="Groupe 33">
<path id="Tracé_636" data-name="Tracé 636" d="M268.459,147.3h-49c-4.181,0-8,2.819-8,7v74c0,4.181,3.819,7,8,7h61c4.181,0,8-2.819,8-7v-62Z" fill="#fff"/>
<path id="Tracé_637" data-name="Tracé 637" d="M268.459,159.3a6.633,6.633,0,0,0,7,7h13l-20-19Z" fill="#e6e8e8"/>
<rect id="Rectangle_27" data-name="Rectangle 27" width="22" height="22" rx="2.697" transform="translate(223.459 159.301)" fill="#82ebe3"/>
<path id="Tracé_638" data-name="Tracé 638" d="M265.459,187.3h-40c-1.2,0-2,1.8-2,3h0a1.89,1.89,0,0,0,2,2h40c1.2,0,3-.8,3-2h0A3.854,3.854,0,0,0,265.459,187.3Z" fill="#d9dbdb"/>
<path id="Tracé_639" data-name="Tracé 639" d="M265.459,198.3h-40a1.89,1.89,0,0,0-2,2h0c0,1.2.8,3,2,3h40a3.854,3.854,0,0,0,3-3h0C268.459,199.1,266.659,198.3,265.459,198.3Z" fill="#d9dbdb"/>
<path id="Tracé_640" data-name="Tracé 640" d="M247.459,209.3h-22a1.89,1.89,0,0,0-2,2h0a1.89,1.89,0,0,0,2,2h22a1.89,1.89,0,0,0,2-2h0A1.89,1.89,0,0,0,247.459,209.3Z" fill="#d9dbdb"/>
</g>
<g id="Groupe_34" data-name="Groupe 34">
<path id="Tracé_641" data-name="Tracé 641" d="M294.459,205.3v43c0,3.029-1.97,6-5,6h-80c-3.03,0-5-2.971-5-6v-43l45,27Z" fill="#ffb031"/>
<path id="Tracé_642" data-name="Tracé 642" d="M293.459,252.3l-29-29-15,9-14-9-30,29a5.789,5.789,0,0,0,4,2h80A5.8,5.8,0,0,0,293.459,252.3Z" fill="#ff9433"/>
</g>
</g>
</g>
<g id="Groupe_39" data-name="Groupe 39">
<g id="Groupe_38" data-name="Groupe 38">
<path id="Tracé_643" data-name="Tracé 643" d="M303.459,146.3a26.912,26.912,0,0,0-24,25,26.011,26.011,0,0,0,5,17c1.275,1.836.857,3.935,0,6l-2,5,9-2a5.746,5.746,0,0,1,4,0c4.065,1.763,9.19,3.466,14,3,12.766-1.236,22.767-12.234,24-25C335.113,158.175,320.591,144.62,303.459,146.3Z" fill="#00c8b7"/>
<g id="Groupe_37" data-name="Groupe 37">
<path id="Tracé_644" data-name="Tracé 644" d="M307.459,186.3l1-1s-.968-3.292,4-8c2.956-2.8,3.241-4.782,3-7-.382-3.532-2.469-6.617-6-7a9.083,9.083,0,0,0-8,3c-4.709,4.967-8,4-8,4l-1,1Z" fill="#fff"/>
<path id="Tracé_645" data-name="Tracé 645" d="M295.459,177.3c-1.18,1.61-.455,3.545,1,5s3.39,2.18,5,1Z" fill="#fff"/>
<path id="Tracé_646" data-name="Tracé 646" d="M317.459,165.3h-1c-.724-1.15-1.843-1.284-3-2l1-1a2.121,2.121,0,0,1,3,3Z" fill="#fff"/>
</g>
</g>
<path id="Tracé_647" data-name="Tracé 647" d="M305.459,145.3a24.375,24.375,0,0,1,17,23,22.689,22.689,0,0,1-23,23c-6.823,0-13.715-2.349-18-7,.675,1.477,2.082,2.679,3,4,1.275,1.836.857,3.935,0,6l-2,5,9-2a5.746,5.746,0,0,1,4,0c4.065,1.763,9.19,3.465,14,3,12.766-1.236,22.767-12.234,24-25A27.481,27.481,0,0,0,305.459,145.3Z" fill="#08b29d"/>
</g>
<path id="Tracé_648" data-name="Tracé 648" d="M164.459,123.3l6,13a1.762,1.762,0,0,1-1,2h0c-.7.3-1.705.7-2,0l-5-14c-.3-.717-.731-1.73,0-2h0A1.862,1.862,0,0,1,164.459,123.3Z" fill="#ffb031"/>
</g>
</g>
<g id="Groupe_42" data-name="Groupe 42">
<path id="Tracé_649" data-name="Tracé 649" d="M187.459,219.3a4,4,0,0,0,0,8c2.406,0,5-1.594,5-4S189.865,219.3,187.459,219.3Zm0,6c-.974,0-1-1.026-1-2s.026-2,1-2a2,2,0,0,1,0,4Z" fill="#baf4ee"/>
<path id="Tracé_650" data-name="Tracé 650" d="M304.459,236.3c-2.407,0-4,2.594-4,5a4,4,0,0,0,8,0C308.459,238.9,306.865,236.3,304.459,236.3Zm0,6c-.974,0-2-.026-2-1a2,2,0,0,1,4,0C306.459,242.275,305.433,242.3,304.459,242.3Z" fill="#baf4ee"/>
<path id="Tracé_651" data-name="Tracé 651" d="M340.459,169.3c-1.842,0-4,1.158-4,3s2.158,3,4,3a3,3,0,0,0,0-6Zm0,4c-.745,0-2-.254-2-1s1.255-1,2-1,1,.254,1,1S341.2,173.3,340.459,173.3Z" fill="#baf4ee"/>
<path id="Tracé_652" data-name="Tracé 652" d="M281.459,115.3a4.388,4.388,0,0,0-4,4c0,1.948,2.052,3,4,3a2.7,2.7,0,0,0,3-3C284.459,117.353,283.406,115.3,281.459,115.3Zm0,5c-.789,0-1-.211-1-1s.211-2,1-2,1,1.212,1,2S282.247,120.3,281.459,120.3Z" fill="#baf4ee"/>
<path id="Tracé_653" data-name="Tracé 653" d="M168.459,197.3a4,4,0,1,0,4,4A4.226,4.226,0,0,0,168.459,197.3Z" fill="#baf4ee"/>
<path id="Tracé_654" data-name="Tracé 654" d="M317.459,210.3c-1.49,0-3,.511-3,2a3,3,0,0,0,6,0C320.459,210.812,318.948,210.3,317.459,210.3Z" fill="#baf4ee"/>
<path id="Tracé_655" data-name="Tracé 655" d="M228.459,116.3a4,4,0,1,0,4,4A4.042,4.042,0,0,0,228.459,116.3Z" fill="#baf4ee"/>
<path id="Tracé_656" data-name="Tracé 656" d="M249.459,259.3a3,3,0,1,0,3,3A3.094,3.094,0,0,0,249.459,259.3Z" fill="#baf4ee"/>
<path id="Tracé_657" data-name="Tracé 657" d="M304.459,131.3c-1.862,0-3,2.138-3,4a2.772,2.772,0,0,0,3,3c1.862,0,4-1.138,4-3A4.544,4.544,0,0,0,304.459,131.3Z" fill="#baf4ee"/>
<circle id="Ellipse_19" data-name="Ellipse 19" cx="2" cy="2" r="2" transform="translate(183.459 158.301)" fill="#baf4ee"/>
<path id="Tracé_658" data-name="Tracé 658" d="M341.459,152.3a3,3,0,1,0,3,3A2.825,2.825,0,0,0,341.459,152.3Z" fill="#baf4ee"/>
<circle id="Ellipse_20" data-name="Ellipse 20" cx="2" cy="2" r="2" transform="translate(164.459 169.301)" fill="#baf4ee"/>
<path id="Tracé_659" data-name="Tracé 659" d="M254.459,109.3l1-1a1.636,1.636,0,0,0,0-2c-.454-.455-.546-.455-1,0l-2,2-1-2c-.455-.455-.546-.455-1,0a1.636,1.636,0,0,0,0,2l1,1-1,2c-.455.455-.455.545,0,1s.545.455,1,0l1-1,2,1c.454.455.546.455,1,0s.455-.545,0-1Z" fill="#baf4ee"/>
<path id="Tracé_660" data-name="Tracé 660" d="M165.459,149.3l1-1a1.414,1.414,0,0,0-2-2l-1,1-1-1a1.414,1.414,0,0,0-2,2l2,1-2,1a1.414,1.414,0,0,0,2,2l1-1,1,1a1.414,1.414,0,0,0,2-2Z" fill="#baf4ee"/>
<path id="Tracé_661" data-name="Tracé 661" d="M320.459,229.3l1-1c.385-.386.385-.615,0-1a1.876,1.876,0,0,0-2,0l-1,1-1-1c-.386-.385-.615-.385-1,0s-.386.614,0,1l1,1-1,1a1.872,1.872,0,0,0,0,2c.385.385.614.385,1,0l1-2,1,2a1.414,1.414,0,0,0,2-2Z" fill="#baf4ee"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 9.1 KiB

3
public/landing/page.svg Normal file
View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="27" height="22" viewBox="0 0 27 22">
<path id="Icon_material-web" data-name="Icon material-web" d="M27.5,6H5.5c-1.513,0-2,1.487-2,3V25c0,1.513.487,3,2,3h22a3.206,3.206,0,0,0,3-3V9A3.206,3.206,0,0,0,27.5,6Zm-7,19H5.5V20h15Zm0-7H5.5V13h15Zm7,7h-5V13h5Z" transform="translate(-3.5 -6)" fill="#35558a"/>
</svg>

After

Width:  |  Height:  |  Size: 356 B

Some files were not shown because too many files have changed in this diff Show More