Initial commit
86
.eslintrc.json
Normal 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
@@ -0,0 +1,4 @@
|
|||||||
|
.next
|
||||||
|
dist
|
||||||
|
node_modules/
|
||||||
|
types/
|
||||||
38
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal 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.
|
||||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal 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
@@ -0,0 +1,7 @@
|
|||||||
|
|
||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: "npm"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "daily"
|
||||||
25
.github/workflows/ci.yml
vendored
Normal 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
@@ -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
@@ -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
@@ -0,0 +1,92 @@
|
|||||||
|
## Welcome to SupaNexTail!
|
||||||
|
|
||||||
|
## Documentation 2.0
|
||||||
|
|
||||||
|
A new documentation is available here : https://doc.supanextail.dev/
|
||||||
|
|
||||||
|
## 
|
||||||
|
|
||||||
|
### What is SupaNexTail?
|
||||||
|
|
||||||
|
SupaNexTail is a boilerplate to quickly create a MVP for a SaaS. It’s 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
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
3. When your project is created, go to the SQL tab and create a new query
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
4. Paste the content of setupSupabaseSQL.sql and run the query
|
||||||
|
5. Your Supabase account is ready! Don’t forget to retrieve your env variables in settings -> API
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
#### 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.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
3. You’ll 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. You’ll have to update the Pricing component too if you have a different amount of price ids.
|
||||||
|
4. Don’t forget to retrieve the variables from Stripe
|
||||||
|
|
||||||
|
 5. You also need to configure the webhook section. Two events are needed
|
||||||
|
|
||||||
|
- customer.subscription.deleted
|
||||||
|
- checkout.session.completed
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
#### Sendgrid
|
||||||
|
|
||||||
|
Sendgrid is optional but you’ll need to configure it if you want to use the contact form.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
If you want to use the mailing list system, you’ll 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 it’s just for the contact form. Stripe variables are needed if you want the subscription system.
|
||||||
|
- Don’t 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 don’t 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.
|
||||||
44
_posts/blog-post-example.mdx
Normal 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 don’t 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>
|
||||||
|
|
||||||
|

|
||||||
25
_posts/example-post.mdx
Normal 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
@@ -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
@@ -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
@@ -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
@@ -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>;
|
||||||
|
}
|
||||||
72
components/CardsLanding.tsx
Normal 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">
|
||||||
|
Don’t waste your time and reinvent the wheel, we have provided you with a
|
||||||
|
maximum of features so that you only have one goal, to make your SaaS a
|
||||||
|
reality.
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-wrap justify-center mt-10">
|
||||||
|
<CardLanding
|
||||||
|
image={cardPage 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, you’ll have access to all the updates"
|
||||||
|
title="Lifetime access"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default CardsLanding;
|
||||||
119
components/Contact.tsx
Normal 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
@@ -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
@@ -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
@@ -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. You’ll have a
|
||||||
|
database, an auth system, a storage system, and much more in one
|
||||||
|
product.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
SupaNexTail uses Supabase at its core, and preconfigures all the
|
||||||
|
useful elements for your site. User registration, synchronization with
|
||||||
|
Stripe, we’ve got you covered!
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="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
@@ -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;
|
||||||
98
components/MailingList.tsx
Normal 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
@@ -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;
|
||||||
81
components/PaymentModal.tsx
Normal 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">
|
||||||
|
​
|
||||||
|
</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
@@ -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;
|
||||||
159
components/PrivacyPolicy.tsx
Normal 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
@@ -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
@@ -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;
|
||||||
35
components/UI/CardLanding.tsx
Normal 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;
|
||||||
20
components/UI/KeyFeature.tsx
Normal 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
@@ -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;
|
||||||
121
components/UI/SignUpPanel.tsx
Normal 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;
|
||||||
36
components/UI/ThemeToggle.tsx
Normal 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;
|
||||||
23
components/blog/avatar.tsx
Normal 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;
|
||||||
11
components/blog/container.tsx
Normal 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;
|
||||||
34
components/blog/cover-image.tsx
Normal 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;
|
||||||
12
components/blog/date-formatter.tsx
Normal 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;
|
||||||
13
components/blog/header.tsx
Normal 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;
|
||||||
49
components/blog/hero-post.tsx
Normal 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;
|
||||||
34
components/blog/more-stories.tsx
Normal 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;
|
||||||
18
components/blog/post-body.tsx
Normal 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;
|
||||||
41
components/blog/post-header.tsx
Normal 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;
|
||||||
43
components/blog/post-preview.tsx
Normal 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;
|
||||||
15
components/blog/post-title.tsx
Normal 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;
|
||||||
5
components/blog/section-separator.tsx
Normal 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||||
106
pages/api/stripe/checkout.ts
Normal 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,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
52
pages/api/stripe/customerPortal.ts
Normal 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
8
postcss.config.js
Normal 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: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
BIN
public/android-chrome-192x192.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
public/android-chrome-512x512.png
Normal file
|
After Width: | Height: | Size: 57 KiB |
BIN
public/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
public/auth.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
1
public/blog/author/Avatar1.svg
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
1
public/blog/author/Avatar2.svg
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
BIN
public/blog/coverdefault.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
public/blog/covers/smallexample.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
public/blog/covers/writeblog.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
9
public/browserconfig.xml
Normal 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
After Width: | Height: | Size: 1.1 KiB |
BIN
public/favicon-32x32.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
public/favicon.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
1
public/landing/TS.svg
Normal 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
@@ -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 |
6
public/landing/backend.svg
Normal 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
@@ -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
|
After Width: | Height: | Size: 118 KiB |
3
public/landing/lifetime.svg
Normal 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 |
69
public/landing/mailing.svg
Normal 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
@@ -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 |