Merge pull request #10654 from supabase/master

Prod deploy Dashboard
This commit is contained in:
Joshen Lim authored and GitHub committed 2022-12-02 00:04:56 +08:00
commit d571209895
137 files changed
+3446 -897

No files matched your search

@@ -226,8 +226,9 @@ export const menuItems: NavMenu = {
label: 'Edge Functions',
items: [
{ name: 'Overview', url: '/guides/functions', items: [] },
{ name: 'Quickstart', url: '/guides/functions/quickstart', items: [] },
{ name: 'Auth', url: '/guides/functions/auth', items: [] },
{ name: 'Examples', url: '/guides/functions/examples', items: [] },
{ name: 'Best Practices', url: '/guides/functions/best-practices', items: [] },
{ name: 'CI/CD Workflow', url: '/guides/functions/cicd-workflow', items: [] },
],
},
@@ -313,6 +314,7 @@ export const menuItems: NavMenu = {
{ name: 'Sequin', url: '/guides/integrations/sequin', items: [] },
{ name: 'Snaplet', url: '/guides/integrations/snaplet', items: [] },
{ name: 'Vercel', url: '/guides/integrations/vercel', items: [] },
{ name: 'Zuplo', url: '/guides/integrations/zuplo', items: [] },
],
},
{
+1 -1
View File
@@ -28,7 +28,7 @@ const nextConfig = {
async headers() {
return [
{
source: '/(.*)',
source: '/:path*',
headers: [
{
key: 'Strict-Transport-Security',
+1 -1
View File
@@ -71,7 +71,7 @@ For deployments with Netlify, set the `SITE_URL` to your official site URL. Add
For deployments with Vercel, set the `SITE_URL` to your official site URL. Add the following additional redirect URLs for local development and deployment previews:
- `http://localhost:3000/**`
- `https://**.vercel.app/**`
- `https://*-username.vercel.app/**`
Vercel provides an environment variable for the URL of the deployment called `NEXT_PUBLIC_VERCEL_URL`. See the [Vercel docs](https://vercel.com/docs/concepts/projects/environment-variables#system-environment-variables) for more details. You can use this variable to dynamically redirect depending on the environment:
+9 -258
View File
@@ -1,280 +1,31 @@
import Layout from '~/layouts/DefaultGuideLayout'
import {Button} from 'ui'
export const meta = {
id: 'functions',
title: 'Edge Functions',
description: 'Globally distributed TypeScript functions.',
sidebar_label: 'Overview',
hide_table_of_contents: true,
}
Edge Functions are server-side TypeScript functions, distributed globally at the edge—close to your users. They can be used for listening to webhooks or integrating your Supabase project with third-parties [like Stripe](https://github.com/supabase/supabase/tree/master/examples/edge-functions/supabase/functions/stripe-webhooks).
Edge Functions are developed using [Deno](https://deno.com), which offers a few benefits to you as a developer:
Edge Functions are server-side TypeScript functions, distributed globally at the edge—close to your users. They can be used for listening to webhooks or integrating your Supabase project with third-parties [like Stripe](https://github.com/supabase/supabase/tree/master/examples/edge-functions/supabase/functions/stripe-webhooks). Edge Functions are developed using [Deno](https://deno.com), which offers a few benefits to you as a developer:
- It is open source.
- It is portable. Supabase Edge Functions run locally, and on any other Deno-compatible platform (including self-hosted infrastructure).
- It is TypeScript first and supports WASM.
- Edge Functions are globally distributed for low-latency.
## Quickstart
<Button as="a" href="/docs/guides/functions/quickstart" size="medium">Get started</Button>
Learn how to develop Edge Functions in less than 7 minutes:
## Examples
<div className="video-container">
<iframe
src="https://www.youtube-nocookie.com/embed/rzglqRdZUQE"
frameBorder="1"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
></iframe>
Check out the [Edge Function Examples](https://github.com/supabase/supabase/tree/master/examples/edge-functions) in our GitHub repository.
<div className="container" style={{ padding: 0 }}>
<FunctionsExamples />
</div>
## Prerequisites
Follow the steps to prepare your Supabase project on your local machine.
- Install the Supabase CLI. [Docs](/docs/guides/cli).
- Login to the CLI using the command: `supabase login`. [Docs](/docs/reference/cli/usage#supabase-login).
- Initialize Supabase inside your project using the command: `supabase init`. [Docs](/docs/guides/cli/local-development#getting-started).
- Link to your Remote Project using the command `supabase link --project-ref your-project-ref`. [Docs](/docs/reference/cli/usage#supabase-link).
- Optional: Setup your environment: Follow [this setup guide](https://deno.land/manual/getting_started/setup_your_environment) to integrate the Deno language server with your editor.
## Getting Started
Let's build an Edge Function locally, then deploy it to the Supabase Platform.
### Creating a function
Let's create a new Edge Function called `hello-world` inside your project:
```bash
supabase functions new hello-world
```
This creates a function stub in your `supabase` folder at `./functions/hello-world/index.ts`.
### Deploy to production
```bash
supabase functions deploy hello-world
```
This command bundles your Edge Function from `./functions/hello-world/index.ts` and deploys it to the Supabase platform.
The command outputs a URL to the Supabase Dashboard which you can open to find view more details. Let's open the link to find the execution command.
<Admonition type="note">
By default, Edge Functions require a valid JWT in the authorization header. This header is automatically set when invoking your function via a Supabase client library.
If you want to use Edge Functions to handle webhooks (e.g. [Stripe payment webhooks](https://github.com/supabase/supabase/tree/master/examples/edge-functions/supabase/functions/stripe-webhooks) etc.), you need to pass the `--no-verify-jwt` flag when deploying your function.
</Admonition>
### Executing Remote Functions
You can execute Edge Functions using curl. Copy the curl command from the Dashboard. It should look like this:
```bash
curl --request POST 'https://<project_ref>.functions.supabase.co/hello-world' \
--header 'Authorization: Bearer ANON_KEY' \
--header 'Content-Type: application/json' \
--data '{ "name":"Functions" }'
```
If you receive an error `Invalid JWT`, find the `ANON_KEY` of your project in the Dashboard under `Settings > API`.
After invoking your Edge Function you should see the response `{ "message":"Hello Functions!" }`.
## Debugging your Functions
You can debug your deployed Edge Functions using the "Functions" section of the Dashboard. There are two types debugging tools available:
- Invocations: shows the Request and Response for each execution.
- Logs: shows any platform events, including deployments and errors.
![Function invocations.](/docs/img/guides/functions/function-logs.png)
## Developing locally
You can run your Edge Function locally using [`supabase functions serve`](/docs/reference/cli/usage#supabase-functions-serve):
```bash
supabase start # start the supabase stack
supabase functions serve hello-world # start the Function watcher
```
The `functions serve` command has hot-reloading capabilities. It will watch for any changes to your files and restart the Deno server.
### Invoking Functions locally
While serving your local Function, you can execute it using curl:
```bash
curl --request POST 'http://localhost:54321/functions/v1/hello-world' \
--header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24ifQ.625_WdcF3KHqz5amU0x2X5WWHP-OEs_4qj0ssLNHzTs' \
--header 'Content-Type: application/json' \
--data '{ "name":"Functions" }'
```
You should see the response `{ "message":"Hello Functions!" }`.
<details>
<summary>Implementation details</summary>
- All Edge Functions are `POST` requests.
- The `Authorization` header is required. You can use either the `ANON` key, the `SERVICE_ROLE` key, or a logged-in user's JWT.
- The Function is proxied through the local API (`http://localhost:54321`)
</details>
<br />
If you execute Function with a different payload the response will change. <br />
Modify the `--data '{"name":"Functions"}'` line to `--data '{"name":"World"}'` and try invoking the command again!
## Secrets and Environment Variables
It's common that you will need to use sensitive information or environment-specific variables inside your Edge Functions. You can access these using Deno's built-in handler
```js
Deno.env.get(MY_SECRET_NAME)
```
### Default secrets
By default, Edge Functions have access to these secrets:
- `SUPABASE_URL`: The API gateway for your Supabase project.
- `SUPABASE_ANON_KEY`: The `anon` key for your Supabase API. This is safe to use in a browser when you have [Row Level Security](/docs/guides/auth/row-level-security) enabled.
- `SUPABASE_SERVICE_ROLE_KEY`: The `service_role` key for your Supabase API. This is safe to use in Edge Functions, but it should NEVER be used in a browser. This key will bypass [Row Level Security](/docs/guides/auth/row-level-security).
- `SUPABASE_DB_URL`: The URL for your [PostgreSQL database](/docs/guides/database). You can use this to connect directly to your database.
### Local secrets
Let's create a local file for storing our secrets, and inside it we can store a secret `MY_NAME`:
```jsx
echo "MY_NAME=Yoda" >> ./supabase/.env.local
```
This creates a new file `./supabase/.env.local` for storing your local development secrets.
<Admonition type="caution">
Never check your .env files into Git!
</Admonition>
Now let's access this environment variable `MY_NAME` inside our Function. Anywhere in your function, add this line:
```jsx
console.log(Deno.env.get('MY_NAME'))
```
Now we can invoke our function locally, by serving it with our new `.env.local` file:
```bash
supabase functions serve hello-world --env-file ./supabase/.env.local
```
When the function starts you should see the name “Yoda” output to the terminal.
### Production secrets
Let's create a `.env` for production. In this case we'll just use the same as our local secrets:
```bash
cp ./supabase/.env.local ./supabase/.env
```
This creates a new file `./supabase/.env` for storing your production secrets.
<Admonition type="caution">
Never check your `.env` files into Git!
</Admonition>
Let's push all the secrets from the `.env` file to our remote project using [`supabase secrets set`](/docs/reference/cli/usage#supabase-secrets-set):
```bash
supabase secrets set --env-file ./supabase/.env
# You can also set secrets individually using:
supabase secrets set MY_NAME=Chewbacca
```
You don't need to re-deploy after setting your secrets.
To see all the secrets which you have set remotely, use [`supabase secrets list`](/docs/reference/cli/usage#supabase-secrets-list):
```bash
supabase secrets list
```
## Suggestions
### Database Functions vs Edge Functions
For data-intensive operations we recommend using [Database Functions](/docs/guides/database/functions), which are executed within your database
and can be called remotely using the [REST and GraphQL API](/docs/guides/api).
For use-cases which require low-latency we recommend [Edge Functions](/docs/guides/functions), which are globally-distributed and can be written in TypeScript.
### Organizing your Edge Functions
We recommend developing “fat functions”. This means that you should develop few large functions, rather than many small functions. One common pattern when developing Functions is that you need to share code between two or more Functions. To do this, you can store any shared code in a folder prefixed with an underscore (`_`). We recommend this folder structure:
```bash
└── supabase
├── functions
│ ├── _shared
│ | ├── supabaseAdmin.ts # Supabase client with SERVICE_ROLE key
│ │ └── supabaseClient.ts # Supabase client with ANON key
│ ├── function-one # use hyphens to name functions
│ │ └── index.ts
│ └── function-two
│ └── index.ts
├── migrations
└── config.toml
```
### Naming Edge Functions
We recommend using hyphens to name functions because hyphens are the most URL-friendly of all the naming conventions (snake_case, camelCase, PascalCase).
### CORS (Cross-Origin Resource Sharing)
We recommend adding a check to handle [CORS Preflight](https://developer.mozilla.org/en-US/docs/Glossary/Preflight_request) requests in your edge function to be able to invoke the function from browsers.
See the [example on GitHub](https://github.com/supabase/supabase/blob/master/examples/edge-functions/supabase/functions/browser-with-cors/index.ts).
```ts
export const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey',
}
serve(async (req) => {
if (req.method === 'OPTIONS') {
return new Response('ok', { headers: corsHeaders })
}
...
})
```
### Using HTTP Methods
Edge Functions supports `GET`, `POST`, `PUT`, `PATCH`, `DELETE`, and `OPTIONS`. A function can be designed to perform different actions based on a request's HTTP method. See the [example on building a RESTful service](https://github.com/supabase/supabase/tree/master/examples/edge-functions/supabase/functions/restful-tasks) to learn how to handle different HTTP methods in your function.
## Limitations
- Deno Deploy limitations
- Deno does not support outgoing connections to ports `25`, `465`, and `587`.
- Cannot write to File System
- Edge Functions
- Local development - only one function at a time
- Serving of HTML content is not supported (`GET` requests that return `text/html` will be rewritten to `text/plain`).
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
@@ -0,0 +1,64 @@
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'functions-best-practices',
title: 'Edge Functions Best Practices',
description: 'Globally distributed TypeScript functions.',
}
## Database Functions vs Edge Functions
For data-intensive operations we recommend using [Database Functions](/docs/guides/database/functions), which are executed within your database
and can be called remotely using the [REST and GraphQL API](/docs/guides/api).
For use-cases which require low-latency we recommend [Edge Functions](/docs/guides/functions), which are globally-distributed and can be written in TypeScript.
## Organizing your Edge Functions
We recommend developing “fat functions”. This means that you should develop few large functions, rather than many small functions. One common pattern when developing Functions is that you need to share code between two or more Functions. To do this, you can store any shared code in a folder prefixed with an underscore (`_`). We recommend this folder structure:
```bash
└── supabase
├── functions
│ ├── _shared
│ | ├── supabaseAdmin.ts # Supabase client with SERVICE_ROLE key
│ │ └── supabaseClient.ts # Supabase client with ANON key
│ ├── function-one # use hyphens to name functions
│ │ └── index.ts
│ └── function-two
│ └── index.ts
├── migrations
└── config.toml
```
## Naming Edge Functions
We recommend using hyphens to name functions because hyphens are the most URL-friendly of all the naming conventions (snake_case, camelCase, PascalCase).
## CORS (Cross-Origin Resource Sharing)
We recommend adding a check to handle [CORS Preflight](https://developer.mozilla.org/en-US/docs/Glossary/Preflight_request) requests in your edge function to be able to invoke the function from browsers.
See the [example on GitHub](https://github.com/supabase/supabase/blob/master/examples/edge-functions/supabase/functions/browser-with-cors/index.ts).
```ts
export const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey',
}
serve(async (req) => {
if (req.method === 'OPTIONS') {
return new Response('ok', { headers: corsHeaders })
}
...
})
```
## Using HTTP Methods
Edge Functions supports `GET`, `POST`, `PUT`, `PATCH`, `DELETE`, and `OPTIONS`. A function can be designed to perform different actions based on a request's HTTP method. See the [example on building a RESTful service](https://github.com/supabase/supabase/tree/master/examples/edge-functions/supabase/functions/restful-tasks) to learn how to handle different HTTP methods in your function.
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
@@ -0,0 +1,210 @@
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'functions-quickstart',
title: 'Edge Functions Quickstart',
description: 'Globally distributed TypeScript functions.',
sidebar_label: 'Quickstart',
}
Learn how to build an Edge Function locally and deploy it to the Supabase Platform in less than 7 minutes.
<div className="video-container">
<iframe
src="https://www.youtube-nocookie.com/embed/rzglqRdZUQE"
frameBorder="1"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
></iframe>
</div>
## Prerequisites
Follow the steps to prepare your Supabase project on your local machine.
- Install the Supabase CLI. [Docs](/docs/guides/cli).
- Login to the CLI using the command: `supabase login`. [Docs](/docs/reference/cli/usage#supabase-login).
- Initialize Supabase inside your project using the command: `supabase init`. [Docs](/docs/guides/cli/local-development#getting-started).
- Link to your Remote Project using the command `supabase link --project-ref your-project-ref`. [Docs](/docs/reference/cli/usage#supabase-link).
- Optional: Setup your environment: Follow [this setup guide](https://deno.land/manual/getting_started/setup_your_environment) to integrate the Deno language server with your editor.
## Create a function
Let's create a new Edge Function called `hello-world` inside your project:
```bash
supabase functions new hello-world
```
This creates a function stub in your `supabase` folder at `./functions/hello-world/index.ts`.
## Deploy to production
```bash
supabase functions deploy hello-world
```
This command bundles your Edge Function from `./functions/hello-world/index.ts` and deploys it to the Supabase platform.
The command outputs a URL to the Supabase Dashboard which you can open to find view more details. Let's open the link to find the execution command.
<Admonition type="note">
By default, Edge Functions require a valid JWT in the authorization header. This header is automatically set when invoking your function via a Supabase client library.
If you want to use Edge Functions to handle webhooks (e.g. [Stripe payment webhooks](https://github.com/supabase/supabase/tree/master/examples/edge-functions/supabase/functions/stripe-webhooks) etc.), you need to pass the `--no-verify-jwt` flag when deploying your function.
</Admonition>
## Execute remote functions
You can execute Edge Functions using curl. Copy the curl command from the Dashboard. It should look like this:
```bash
curl --request POST 'https://<project_ref>.functions.supabase.co/hello-world' \
--header 'Authorization: Bearer ANON_KEY' \
--header 'Content-Type: application/json' \
--data '{ "name":"Functions" }'
```
If you receive an error `Invalid JWT`, find the `ANON_KEY` of your project in the Dashboard under `Settings > API`.
After invoking your Edge Function you should see the response `{ "message":"Hello Functions!" }`.
## Debug functions
You can debug your deployed Edge Functions using the "Functions" section of the Dashboard. There are two types debugging tools available:
- Invocations: shows the Request and Response for each execution.
- Logs: shows any platform events, including deployments and errors.
![Function invocations.](/docs/img/guides/functions/function-logs.png)
## Develop locally
You can run your Edge Function locally using [`supabase functions serve`](/docs/reference/cli/usage#supabase-functions-serve):
```bash
supabase start # start the supabase stack
supabase functions serve hello-world # start the Function watcher
```
The `functions serve` command has hot-reloading capabilities. It will watch for any changes to your files and restart the Deno server.
### Invoke functions locally
While serving your local Function, you can execute it using curl:
```bash
curl --request POST 'http://localhost:54321/functions/v1/hello-world' \
--header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24ifQ.625_WdcF3KHqz5amU0x2X5WWHP-OEs_4qj0ssLNHzTs' \
--header 'Content-Type: application/json' \
--data '{ "name":"Functions" }'
```
You should see the response `{ "message":"Hello Functions!" }`.
<details>
<summary>Implementation details</summary>
- All Edge Functions are `POST` requests.
- The `Authorization` header is required. You can use either the `ANON` key, the `SERVICE_ROLE` key, or a logged-in user's JWT.
- The Function is proxied through the local API (`http://localhost:54321`)
</details>
<br />
If you execute Function with a different payload the response will change. <br />
Modify the `--data '{"name":"Functions"}'` line to `--data '{"name":"World"}'` and try invoking the command again!
## Secrets and Environment Variables
It's common that you will need to use sensitive information or environment-specific variables inside your Edge Functions. You can access these using Deno's built-in handler
```js
Deno.env.get(MY_SECRET_NAME)
```
### Default secrets
By default, Edge Functions have access to these secrets:
- `SUPABASE_URL`: The API gateway for your Supabase project.
- `SUPABASE_ANON_KEY`: The `anon` key for your Supabase API. This is safe to use in a browser when you have [Row Level Security](/docs/guides/auth/row-level-security) enabled.
- `SUPABASE_SERVICE_ROLE_KEY`: The `service_role` key for your Supabase API. This is safe to use in Edge Functions, but it should NEVER be used in a browser. This key will bypass [Row Level Security](/docs/guides/auth/row-level-security).
- `SUPABASE_DB_URL`: The URL for your [PostgreSQL database](/docs/guides/database). You can use this to connect directly to your database.
### Local secrets
Let's create a local file for storing our secrets, and inside it we can store a secret `MY_NAME`:
```jsx
echo "MY_NAME=Yoda" >> ./supabase/.env.local
```
This creates a new file `./supabase/.env.local` for storing your local development secrets.
<Admonition type="caution">
Never check your .env files into Git!
</Admonition>
Now let's access this environment variable `MY_NAME` inside our Function. Anywhere in your function, add this line:
```jsx
console.log(Deno.env.get('MY_NAME'))
```
Now we can invoke our function locally, by serving it with our new `.env.local` file:
```bash
supabase functions serve hello-world --env-file ./supabase/.env.local
```
When the function starts you should see the name “Yoda” output to the terminal.
### Production secrets
Let's create a `.env` for production. In this case we'll just use the same as our local secrets:
```bash
cp ./supabase/.env.local ./supabase/.env
```
This creates a new file `./supabase/.env` for storing your production secrets.
<Admonition type="caution">
Never check your `.env` files into Git!
</Admonition>
Let's push all the secrets from the `.env` file to our remote project using [`supabase secrets set`](/docs/reference/cli/usage#supabase-secrets-set):
```bash
supabase secrets set --env-file ./supabase/.env
# You can also set secrets individually using:
supabase secrets set MY_NAME=Chewbacca
```
You don't need to re-deploy after setting your secrets.
To see all the secrets which you have set remotely, use [`supabase secrets list`](/docs/reference/cli/usage#supabase-secrets-list):
```bash
supabase secrets list
```
## Limitations
- Deno Deploy limitations
- Deno does not support outgoing connections to ports `25`, `465`, and `587`.
- Cannot write to File System
- Edge Functions
- Local development - only one function at a time
- Serving of HTML content is not supported (`GET` requests that return `text/html` will be rewritten to `text/plain`).
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
+1 -1
View File
@@ -39,7 +39,7 @@ Each system has a number of configuration options which can be found in the rele
- [Realtime](https://github.com/supabase/realtime#server)
- [GoTrue](https://github.com/supabase/gotrue)
- [Storage](https://github.com/supabase/storage-api)
- [Kong](https://docs.konghq.com/install/docker/)
- [Kong](https://docs.konghq.com/gateway/latest/install/docker/)
## Managing your database
@@ -0,0 +1,241 @@
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'zuplo',
title: 'Zuplo',
description: 'Building a public API backed by Supabase.',
}
[Zuplo](https://zuplo.com) is a fully-managed API gateway that offers the easiest way to securely and safely share your API. In this guide we look at how you can combine Zuplo and Supabase to create a public API with rate-limiting, a self-serve developer portal, and API-key authentication. There is an [accompanying video for this article](https://www.youtube.com/watch?v=GJSkbxMnWxE).
![zuplo layout](/docs/img/guides/integrations/zuplo/arch.png)
In this example we're going to work with a simple table that allows people to read and write entries to a Supabase table that contains some reviews of skis. Because this is an API for developers, we have to assume that they may be calling it from another backend service and can't login as a user using the standard Supabase method. In this scenario, API keys are often a better choice - see [Wait, you're not using API keys?](https://zuplo.com/blog/2022/05/03/you-should-be-using-api-keys/).
We'll allow people, with a valid API key, to read data from the ski results table and to create new records. Hopefully it's obvious that there are many ways that you can extend this example to add more behavior like roles based access, with custom policies, custom handlers and more.
## Setting up Supabase
If you haven't already, create a new project in Supabase and create a table called ski-reviews with the following columns:
- id (int8)
- created_at (timestamptz)
- make (varchar)
- model (varchar)
- year (int8)
- rating (int2)
- author (varchar)
Manually enter a couple of rows of data, so that we have something to read from the DB.
## The `Get all` reviews route in Zuplo
Login to Zuplo at [portal.zuplo.com](https://portal.zuplo.com] and create a new project in Zuplo - I went with `supabase-ski-reviews`.
Select the **File** tab and choose **Routes**. Add your first route with the following settings:
- method: `GET`
- path: `/reviews`
- summary: `Get all reviews`
- version: `v1`
- CORS: `Anything goes`
And in the request handler section, paste the `READ ALL ROWS` URL of your Supabase backend (you can get to this in the **API docs** section of Supabase)
- URL Rewrite: `https://YOUR_SUPABASE_URL.supabase.co/rest/v1/ski-reviews?select=*`
- Forward Search: `unchecked`
In order to call the Supabase backend I need to add some authentication headers to the outgoing request.
Expand the **Policies** section of your route. Click **Add policy** on the **Request** pipeline.
We don't want to forward any headers that the client sends us to Supabase, so find the **Clear Headers Policy** and add that to your inbound pipeline. Note, that we will allow the `content-type` header to flow through, so this should be your policy config.
```json
{
"export": "ClearHeadersInboundPolicy",
"module": "$import(@zuplo/runtime)",
"options": {
"exclude": ["content-type"]
}
}
```
Next, we need to add the credentials to the outgoing request. We'll need to get the JWT token from supabase - you'll find it in **Settings** > **API** as shown below:
![secret_role jwt](/docs/img/guides/integrations/zuplo/secret-role.png)
Once you've got your service_role JWT, click **Add Policy** again on the **Request** pipeline and choose the **Add/Set Headers Policy** and configure it as follows:
```json
{
"export": "SetHeadersInboundPolicy",
"module": "$import(@zuplo/runtime)",
"options": {
"headers": [
{
"name": "apikey",
"value": "$env(SUPABASE_API_KEY)",
"overwrite": true
},
{
"name": "authorization",
"value": "$env(SUPABASE_AUTHZ_HEADER)",
"overwrite": true
}
]
}
}
```
Save your changes.
Next, create two secret [environment variables](https://zuplo.com/docs/deployments/environment-variables) as follows:
- SUPABASE_API_KEY: `"YOUR_SUPABASE_SECRET_ROLE_JWT"`
- SUPABASE_AUTHZ_HEADER: `"Bearer YOUR_SUPABASE_SECRET_ROLE_JWT"`
Obviously, in both instances replace `YOUR_SUPABASE_SECRET_ROLE_JWT` with your service_role JWT from Supabase.
You are now ready to invoke your API gateway and see data flow through from your Supabase backend!
Click on the **open in browser** button shown below and you should see the JSON, flowing from Supabase in your browser 👏.
![open in browser](/docs/img/guides/integrations/zuplo/open-in-browser.png)
## Adding authentication
At this point, that route is wide open to the world so we need to secure it. We'll do this using API keys. You can follow this guide [Add API key Authentication](https://zuplo.com/docs/quickstarts/add-api-key-auth). Be sure to drag the API Key authentication policy to the very top of your **Request** pipeline. Come back here when you're done.
Welcome back! You've now learned how to secure your API with API-Keys.
## Adding a Create route
Next we'll add a route that allows somebody to create a review. Add another route with the following settings
- method: `POST`
- path: `/reviews`
- summary: `Create a new review`
- version: `v1`
- CORS: `Anything goes`
And the request handler as follows:
- URL Rewrite: `https://YOUR_SUPABASE_URL.supabase.co/rest/v1/ski-reviews`
- Forward Search: `unchecked`
Expand the policies section and add the same policies (note you can reuse policies by picking from the existing policies at the top of the library)
![existing policies](/docs/img/guides/integrations/zuplo/existing-policies.png)
- api-key-auth-inbound
- clear-headers-inbound
- set-headers-inbound
Now your **create** route is secured and will automatically set the right headers before calling Supabase. That was easy.
You can test this out by using the **API Test Console** to invoke your new endpoint. Go to the **API Test Console** and create a new test called `create-review.json`.
- Method: `POST`
- Path: `/v1/reviews`
- Headers:
- `content-type`: `application/json`
- `authorization`: `Bearer YOUR_ZUPLO_API_KEY`
- Body:
```json
{
"make": "Rossignol",
"model": "Soul HD7",
"rating": 5,
"year": 2019
}
```
![Test console](/docs/img/guides/integrations/zuplo/test-console.png)
If you invoke your API by clicking `Test` you should see that you get a **201 Created** - congratulations!
## Add validation to your post
To make your API more usable and more secure it is good practice to validate incoming requests. In this case we will add a JSON Schema document and use it to validate the incoming body to our POST.
Create a new schema document called `new-review.json`.
![new schema](/docs/img/guides/integrations/zuplo/new-schema.png)
This example fits the ski-reviews table we described above
```json
{
"$id": "http://example.com/example.json",
"type": "object",
"default": {},
"title": "Root Schema",
"required": ["make", "model", "rating", "year"],
"additionalProperties": false,
"properties": {
"make": {
"type": "string",
"default": "",
"title": "The make Schema",
"examples": ["DPS"]
},
"model": {
"type": "string",
"default": "",
"title": "The model Schema",
"examples": ["Pagoda"]
},
"rating": {
"type": "integer",
"default": 0,
"title": "The rating Schema",
"examples": [5]
},
"year": {
"type": "integer",
"default": 0,
"title": "The year Schema",
"examples": [2018]
}
},
"examples": [
{
"make": "DPS",
"model": "Pagoda",
"rating": 5,
"year": 2018,
"author": "Josh"
}
]
}
```
Now add a new policy to **request** pipeline for your `Create new review` route. Choose the **JSON Body Validation** policy and configure it to use your newly created JSON schema document:
```json
{
"export": "ValidateJsonSchemaInbound",
"module": "$import(@zuplo/runtime)",
"options": {
"validator": "$import(./schemas/new-review.json)"
}
}
```
This policy can be dragged to the first position in your pipeline.
Now to test this is working, go back to your API test console and change the body of your `create-review.json` test to be invalid (add a new property for example). You should find that you get a `400 Bad Request` response.
![400 Bad Request](/docs/img/guides/integrations/zuplo/400-bad.png)
Finally, lean back and marvel at your beautiful Developer Portal that took almost zero effort to get this far, wow! Hopefully you already found the link for this when adding API key support :)
![Developer Portal](/docs/img/guides/integrations/zuplo/dev-portal.png)
Zuplo can also be used to handle Supabase JWT tokens for any API, learn more at [API Authentication with Supabase JWT Tokens](https://zuplo.com/blog/2022/11/15/api-authentication-with-supabase-jwt)
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
+44 -25
View File
@@ -40,31 +40,6 @@ To update the screenshots, ensure that at least one log line is selected to disp
[Postgres logs](https://app.supabase.com/project/_/logs/postgres-logs) show queries and activity for your [database](../../guides/database).
By default, query logs are disabled for all new Supabase projects, as they can reveal metadata about the contents of your database (e.g. table and column names). If you'd like to enable query logs, you can enable and configure the [pgAudit extension](https://www.pgaudit.org/):
- [Enable the pgAudit extension for your project](https://app.supabase.com/project/_/database/extensions)
- [Reboot the DB](https://app.supabase.com/project/_/settings/general); a "Fast database reboot" is sufficient, and faster than a full project restart
- Configure `pgaudit.log` to the [appropriate value](https://github.com/pgaudit/pgaudit/blob/master/README.md#pgauditlog)
- You should be able to view query logs in your [log explorer](https://app.supabase.com/project/_/logs/explorer) now
As an example, if you wanted to enable logs for writes and DDL statements for a single session, you could execute within the session:
```sql
set pgaudit.log = 'write, ddl';
```
To _permanently_ enable logs for all statements that relate to roles and privileges, you could execute (followed by a db reboot):
```sql
alter system set pgaudit.log to 'role';
```
To remove a system-wide setting, you can use (followed by a db reboot):
```sql
alter system reset pgaudit.log
```
![Postgres Logs](/docs/img/guides/platform/logs/logs-database.png)
</TabPanel>
@@ -108,6 +83,50 @@ The Logs tab displays logs emitted during function execution.
</TabPanel>
</Tabs>
---
## Logging Postgres Queries
By default, query logs are disabled for new Supabase projects, as they can reveal metadata about the contents of your database (such as table and column names).
To enable query logs:
1. [Enable the pgAudit extension](https://app.supabase.com/project/_/database/extensions).
2. [Restart your project](https://app.supabase.com/project/_/settings/general) using the **Fast database reboot** option.
3. Configure `pgaudit.log` (see below). Perform a fast reboot if needed.
4. View your query logs under [Logs > Postgres Logs](https://app.supabase.com/project/_/logs/postgres-logs).
### Configuring `pgaudit.log`
The stored value under `pgaudit.log` determines the classes of statements that are logged by [pgAudit extension](https://www.pgaudit.org/). Refer to the pgAudit documentation for the [full list of values](https://github.com/pgaudit/pgaudit/blob/master/README.md#pgauditlog).
To enable logging for function calls/do blocks, writes, and DDL statements for a single session, execute the following within the session:
```sql
-- temporary single-session config update
set pgaudit.log = 'function, write, ddl';
```
To _permanently_ set a logging configuration (beyond a single session), execute the following, then perform a fast reboot:
```sql
-- equivalent permanent config update.
alter role postgres set pgaudit.log to 'function, write, ddl';
```
To reset system-wide settings, execute the following, then perform a fast reboot:
```sql
-- resets stored config.
alter role postgres reset pgaudit.log
```
<Admonition type="note">
If any permission errors are encountered when executing `alter role postgres ...`, it is likely that your project has yet to receive the patch to the latest version of [supautils](https://github.com/supabase/supautils), which is currently being rolled out.
</Admonition>
## Logs Explorer
The [Logs Explorer](https://app.supabase.com/project/_/logs-explorer) exposes logs from each part of the Supabase stack as a separate table that can be queried and joined using SQL.
+1 -1
View File
@@ -10,7 +10,7 @@ export const meta = {
Supabase provides a globally distributed cluster of [Realtime](https://github.com/supabase/realtime) servers that enable the following functionality:
- [Broadcast](#broadcast): Send ephemeral messages from client to clients with low latency.
- [Presence](#presence): Track and synchrononize shared state between clients.
- [Presence](#presence): Track and synchronize shared state between clients.
- [Postgres CDC](#postgres-cdc): Listen to Postgres database changes and send them to authorized clients.
A [channel](https://hexdocs.pm/phoenix/channels.html) is the basic building block of Realtime and narrows the scope of data flow to subscribed clients. You can think of a channel as a chatroom where participants are able to see who's online and send and receive messages; similar to a Discord or Slack channel.
Binary file not shown.

After

Width:  |  Height:  |  Size: 250 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 494 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 279 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 KiB

+120 -78
View File
@@ -78,6 +78,24 @@
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/handbook/contributing</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/handbook/introduction</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/handbook/supasquad</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/api</loc>
<changefreq>weekly</changefreq>
@@ -217,19 +235,31 @@
</url>
<url>
<loc>https://supabase.com/docs/handbook/contributing</loc>
<loc>https://supabase.com/docs/learn/auth-deep-dive/auth-deep-dive-jwts</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/handbook/introduction</loc>
<loc>https://supabase.com/docs/learn/auth-deep-dive/auth-google-oauth</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/handbook/supasquad</loc>
<loc>https://supabase.com/docs/learn/auth-deep-dive/auth-gotrue</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/learn/auth-deep-dive/auth-policies</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/learn/auth-deep-dive/auth-row-level-security</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
@@ -252,6 +282,36 @@
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/functions/auth</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/functions/best-practices</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/functions/cicd-workflow</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/functions/examples</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/functions/quickstart</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/auth/auth-apple</loc>
<changefreq>weekly</changefreq>
@@ -420,42 +480,6 @@
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/functions/auth</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/functions/cicd-workflow</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/functions/examples</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/hosting/docker</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/hosting/overview</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/hosting/platform</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/database/arrays</loc>
<changefreq>weekly</changefreq>
@@ -541,25 +565,19 @@
</url>
<url>
<loc>https://supabase.com/docs/guides/migrations/firebase-auth</loc>
<loc>https://supabase.com/docs/guides/hosting/docker</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/migrations/firebase-storage</loc>
<loc>https://supabase.com/docs/guides/hosting/overview</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/migrations/firestore-data</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/migrations/heroku</loc>
<loc>https://supabase.com/docs/guides/hosting/platform</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
@@ -612,6 +630,12 @@
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/integrations/estuary</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/integrations/fezto</loc>
<changefreq>weekly</changefreq>
@@ -690,6 +714,36 @@
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/integrations/zuplo</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/migrations/firebase-auth</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/migrations/firebase-storage</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/migrations/firestore-data</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/migrations/heroku</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/platform/compute-add-ons</loc>
<changefreq>weekly</changefreq>
@@ -768,42 +822,18 @@
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/learn/auth-deep-dive/auth-deep-dive-jwts</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/learn/auth-deep-dive/auth-google-oauth</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/learn/auth-deep-dive/auth-gotrue</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/learn/auth-deep-dive/auth-policies</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/learn/auth-deep-dive/auth-row-level-security</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/auth/auth-helpers/auth-ui</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/auth/auth-helpers/nextjs-server-components</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/auth/auth-helpers/nextjs</loc>
<changefreq>weekly</changefreq>
@@ -1740,6 +1770,12 @@
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/reference/dart/storage-from-createsignedurls</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/reference/dart/storage-from-download</loc>
<changefreq>weekly</changefreq>
@@ -1932,6 +1968,12 @@
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/reference/realtime/config</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/reference/storage/release-notes</loc>
<changefreq>weekly</changefreq>
+1
View File
@@ -9,3 +9,4 @@ NEXT_PUBLIC_DOCS_URL="http://localhost:3005"
NEXT_PUBLIC_STUDIO_URL="https://localhost:8082"
NEXT_PUBLIC_LAUNCHWEEKSITE_URL="https://localhost:3008"
NEXT_PUBLIC_REFERENCE_DOCS_URL="https://localhost:3010"
@@ -43,12 +43,12 @@ At Supabase, we're building some amazing tools that make Postgres as easy to use
#### Simple interface
Why are database interfaces so hard to use? The Supabase team has built products for 70-year-olds, so we're confident we can make something easier for developers:
![Supabase table view](https://dev-to-uploads.s3.amazonaws.com/i/b4o39am95zcl5vl54j75.png)
![Supabase table view](/images/blog/2020/b4o39am95zcl5vl54j75.png)
#### Connectors
Send realtime database changes to other systems, like queues or webhooks (Slack notifications!):
![Supabase connectors](https://dev-to-uploads.s3.amazonaws.com/i/aom5r917s792cc081bbz.png)
![Supabase connectors](/images/blog/2020/aom5r917s792cc081bbz.png)
#### And more
@@ -39,13 +39,13 @@ OK now for the important part - as promised, we will continue to release our upd
We've revamped the UI to fit in some more features that we're building.
![Fresh UI](https://dev-to-uploads.s3.amazonaws.com/i/jzzm7u56ns6ega9uyc4j.png)
![Fresh UI](/images/blog/2020/jzzm7u56ns6ega9uyc4j.png)
### Table View
This one is still very unstable, but we wanted to ship it anyway. It's a little hidden away so you'll have to hunt for it :). Give us a couple of months and we promise this will be as good as (better than?) Airtable.
![Table view](https://dev-to-uploads.s3.amazonaws.com/i/jalcaoz4lsp2b6wegah5.png)
![Table view](/images/blog/2020/jalcaoz4lsp2b6wegah5.png)
#### And more
@@ -94,7 +94,7 @@ $ sudo -su postgres envdir /etc/wal-g.d/env /usr/local/bin/wal-g backup-push /va
At this point, if you were to check the S3 path that you provided, the following two newly created and populated directories would be observed:
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/lai1mxg62kffyd2khmtm.png)
![Alt Text](/images/blog/2020/lai1mxg62kffyd2khmtm.png)
From then on, subsequent physical backups would be found in the directory `basebackups_005` and any WAL archives would be sent to the directory `wal_005`.
@@ -35,11 +35,11 @@ Here's how to start a Supabase + Node.js repl:
Sign up for [replit.com](http://replit.com) and hit new repl in the top left
![Untitled-2](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/u3dljulzsyqu58i75epn.png)
![Untitled-2](/images/blog/2021-march/u3dljulzsyqu58i75epn.png)
Select node.js, give it a name, and click Create repl
![Untitled-1](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7rcfbb12sfabevto571j.png)
![Untitled-1](/images/blog/2021-march/7rcfbb12sfabevto571j.png)
Import supabase's createClient method and hit run to install the required libs:
@@ -56,11 +56,11 @@ const supabase = createClient(
)
```
![Untitled-3](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5j5aqyjdh74qm83slmli.png)
![Untitled-3](/images/blog/2021-march/5j5aqyjdh74qm83slmli.png)
Now that supabase is connected you'll want to add some data to your db, you can grab any SQL dataset on the web, or make your own, but the fasted way to test is to open the SQL tab in the Supabase dashboard and click the Countries sample database and click Run.
![Untitled-4](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/54yykm6h9hqpric87zad.png)
![Untitled-4](/images/blog/2021-march/54yykm6h9hqpric87zad.png)
From within your repl you can now query your countries table like:
@@ -111,7 +111,7 @@ type Mutation {
): AccountDeleteResponse!
```
For a complete example with relationships, check out the [reflection docs](https://supabase.github.io/pg_graphql/reflection/).
For a complete example with relationships, check out the [API docs](https://supabase.github.io/pg_graphql/api/).
## Security
@@ -0,0 +1,263 @@
---
title: Transparent Column Encryption with Postgres
description: Using pgsodium's Transparent Column Encryption to encrypt data and provide your users with row-level encryption.
author: michel
image: transparent-column-encryption-with-postgres.jpg
thumb: transparent-column-encryption-with-postgres.jpg
tags:
- postgres
date: '2022-12-01'
toc_depth: 3
---
One of the more common questions we receive at Supabase is “how do I encrypt sensitive data”?
pgsodium's Transparent Column Encryption (TCE) is one of the safest ways, and can be used to encrypt one or more text columns in any number of tables. Using TCE, you can encrypt data so that it doesn't leak into logs as well as providing your users with row-level encryption.
To understand how TCE works, let's first do a deep-dive into an important encryption topic: key derivation.
<div className="bg-gray-300 rounded-lg p-6 italic">
This post is a sneak peek of a big feature we are shipping on LW6. To not miss a thing, get your ticket for [Supabase Launch Week 6!](https://supabase.com/launch-week).
</div>
## How Key Derivation Works
The current state-of-the-art in encryption libraries is [libsodium](https://doc.libsodium.org/).
**libsodium** offers a range of APIs for authenticated secret and public key encryption, key derivation, encrypted streaming, [AEAD](https://en.wikipedia.org/wiki/Authenticated_encryption), various forms of hashing, and much more.
This powerful API is available to PostgreSQL using the [**pgsodium**](https://github.com/michelp/pgsodium) extension. **pgsodium** provides all the functionality of the full **libsodium** API, but previously it required developers to set up database encryption themselves, which remained a challenge even for those familiar with database administration.
To solve this problem, **pgsodium** now has a full key management API, primarily via the table `pgsodium.key` and the `pgsodium.create_key()` function. This key table contains no raw keys, but instead uses libsodium Key IDs to derive keys that are used internally for encryption. A [key derivation function](https://libsodium.gitbook.io/doc/key_derivation) is used with an internal root key that is unavailable to SQL and not stored in the database, but rather managed by you externally using flexible scripts, or by Supabase automatically as part of our service offering.
The simplest way to use **pgsodium** to encrypt and decrypt data is to first create a ******Key ID******. Valid Key IDs are stored in pgsodium in a special extension table, and they can be created using the `pgsodium.create_key()` function. This function takes a number of arguments depending on how it's used, but the simplest case is to create a new key with no arguments:
```sql
select * from pgsodium.create_key();
-[ RECORD 1 ]---+-------------------------------------
id | eaa20d8c-c77c-4985-9f73-2a5f5d1f1e6d
name |
status | valid
key_type | aead-det
key_id | 2
key_context | \x7067736f6469756d
created | 2022-11-13 21:19:35.765823+00
expires |
associated_data |
```
This key can now be used with pgsodium encryption functions by its UUID (`eaa20d8c-c77c-4985-9f73-2a5f5d1f1e6d`). For example:
```sql
select * from pgsodium.crypto_aead_det_encrypt (
'this is the message', -- a message to encrypt
'this is associated data', -- some authenticated associated data
'eaa20d8c-c77c-4985-9f73-2a5f5d1f1e6d'::uuid -- key ID
);
```
This produces the following encrypted “ciphertext” using the `aead-det` algorithm from the libsodium [XChaCha20-SIV](https://github.com/jedisct1/libsodium-xchacha20-siv) encryption function.
```
-[ RECORD 1 ]-----------+---------------------------------------------------------------------------------------------------------
crypto_aead_det_encrypt | \\x099baa820250d7375ed141f8f1936af384bc229f3de1010a6eff6ffdaf3998baffbae75b5cd83d1c469407ff2d3764a428b742
```
Now to decrypt the ciphertext, pass it to the decryption function *with the same Key ID*:
```sql
select *
from convert_from(pgsodium.crypto_aead_det_decrypt (
'\\x099baa820250d7375ed141f8f1936af384bc229f3de1010a6eff6ffdaf3998baffbae75b5cd83d1c469407ff2d3764a428b742',
'this is associated data',
'eaa20d8c-c77c-4985-9f73-2a5f5d1f1e6d'::uuid
), 'utf8');
```
Which recovers the original “plaintext” message:
```
-[ RECORD 1 ]+--------------------
convert_from | this is the message
```
In the above example, there is *no raw key* like shown in the pgcrypto example above, only a Key ID which is used to derive the key used to encrypt the message and authenticate it with the associated data. In fact, it is impossible for a SQL user to derive the key used above, and if the Key ID is stored, then no decrypted information will leak into backups, disk storage, or the database WAL stream.
## Transparent Column Encryption
As of **pgsodium** 3.0.0 and up, the extension offers a simple and declarative Transparent Column Encryption feature (TCE). This feature is now shipped with all Supabase projects. TCE allows you to specify encrypted columns within a table and generates a new view that “wraps” that table to decrypt the contents.
TCE works using two dynamically generated objects for tables that contain encrypted columns:
- an `INSERT UPDATE` trigger that encrypts data when it is inserted or modified
- a view that is created to wrap the table to decrypt the data when it is accessed
To “transparently” decrypt the table, access the dynamically generated view *instead of the table*. For every encrypted column in the table, the view will have an additional decrypted column that shows the decrypted result.
It's worth noting at this point that sometimes there is some confusion about handling encrypted data with TCE. The `T` stands for ***********Transparent*********** which means, you can always see decrypted data through the view, where the decrypted data can't be see is when stored on disk, or in pg_dumps, backups, WAL streams, etc. This is often called ******************Encryption At Rest****************** and is one layer in many that may be used to encrypt and protect your data.
Often Transparent encryption is understood to be “Transparent Disk Encryption” or “Full Disk Encryption”, this is where a drive is encrypted but reading and writing that drive is decrypted. TCE is similar to this, where data on disk is encrypted, but it is more fine grained, only particular columns are encrypted. The data is also encrypted in the sense that the table stored on disk contains encrypted data, without the view or the key, pg_dumps and backups still contain encrypted data, this is not possible with disk-only encryption.
For the moment TCE only works for columns of type `text` (or types castable to `text` like `json`). Soon we will also support `bytea` and possibly more as use cases and tests get better.
TCE uses one of PostgreSQL's lesser-known features: [`SECURITY LABEL`](https://www.postgresql.org/docs/current/sql-security-label.html). A security label can be thought of as a simple label which is attached to an object (a table, column, etc). Each label is scoped to an extension and that extension can provide security features depending on the label.
Let's see a simple example of using `SECURITY LABEL` to encrypt a column using **pgsodium**.
<div className="bg-gray-300 rounded-lg p-6 italic">
Note: We're using “credit cards” in our examples below, because they are very easy to understand. Please don't store credit cards in your Supabase database - we are not a certified PCI Service Provider.
</div>
### One Key ID for the Entire Column
For the simplest case, a column can be encrypted with one Key ID which must be of the type `aead-det` (as created above):
```sql
CREATE TABLE credit_cards (
id bigserial primary key,
credit_card_number text
);
SECURITY LABEL FOR pgsodium
ON COLUMN credit_cards.credit_card_number
IS 'ENCRYPT WITH KEY ID e348034b-3f07-4878-aad6-000511d12826';
```
The advantage of this approach is simplicity - the user creates one key and labels a column with it. The cryptographic algorithm for this approach uses a *nonceless* encryption algorithm called `crypto_aead_det_xchacha20()`. This algorithm is written by the author of libsodium and can be found [here](https://github.com/jedisct1/libsodium-xchacha20-siv).
Using one key for an entire column means that whoever can decrypt one row can decrypt them all from a database dump. Also changing (rotating) the key means rewriting the whole table.
### One Key ID per Row
A more fine grained approach would be storing one Key ID per row:
```sql
CREATE TABLE credit_cards (
id bigserial primary key,
credit_card_number text,
key_id uuid not null DEFAULT 'e348034b-3f07-4878-aad6-000511d12826'::uuid
);
SECURITY LABEL FOR pgsodium
ON COLUMN credit_cards.credit_card_number
IS 'ENCRYPT WITH KEY COLUMN key_id';
```
This approach ensures that a key for one user doesn't necessarily decrypt any others. While rows can share key IDs, they don't necessarily have to (unlike the first example above where one key id was used for the entire column). It also acts as a natural partition that can work in conjunction with Row Level Security to share distinct keys between owners.
Notice also how there is a `DEFAULT` value for the `key_id`. In a way, this gives you the best of both approaches - encrypting the column when a per-row key ID is not provided. The downside to this approach is that you need to store one key ID per row, which takes up more disk space (but that's cheap!).
### One Key ID per Row with Nonce Support
The default cryptographic algorithm for the above approach uses a *nonceless* encryption algorithm called `[crypto_aead_det_xchacha20()](https://github.com/jedisct1/libsodium-xchacha20-siv)`. This algorithm has the advantage that it does not require nonce values, the disadvantage is that duplicate plaintexts will produce duplicate ciphertexts.
Nonces are some extra cryptographic context that is used in many cryptographic algorithms to produce different ciphertexts, even if the plaintexts are the same. The nonce does not have to be secret, but it *does* have to be unique. **pgsodium** comes with a useful function `pgsodium.crypto_aead_det_noncegen()` that will generate a cryptographically secure nonce for you, and in almost all cases it's best to use that function unless you know specifically what you are doing. In password hashing approaches, this is often similar to how a “salt” value is used to deduplicate password hashes.
Duplicate ciphertexts cannot be used to “attack the key”, it can only reveal the duplication. However, duplication is still information. In our examples so far, an attacker might be able to use this information to determine that two accounts share the same credit card number. While not technically breaking the encryption, this still leaks information to an attacker.
```sql
CREATE TABLE credit_cards (
id bigserial primary key,
credit_card_number text,
key_id uuid not null DEFAULT 'e348034b-3f07-4878-aad6-000511d12826'::uuid,
nonce bytea default pgsodium.crypto_aead_det_noncegen()
);
SECURITY LABEL FOR pgsodium
ON COLUMN credit_cards.credit_card_number
IS 'ENCRYPT WITH KEY COLUMN key_id NONCE nonce';
```
This is the most secure form of TCE - there is a unique key ID and a unique nonce per row.
### One Key ID per Row with Associated Data
The encryption that is used for TCE is one of a family of functions provided by **libsodium** to do Authenticated Encryption with Associated Data or [AEAD Encryption](https://en.wikipedia.org/wiki/Authenticated_encryption). The “associated” data is plaintext (unencrypted) information that is mixed into the authentication signature of the encrypted data, such that when you authenticate the data, you also know that the associated data is authentic.
AEAD is helpful because often you have metadata associated with a secret, which isn't confidential but must not be forged.
In our credit card example, we might associate a "`credit_card_number`" with an "`account_id`". But what if a malicious actor wanted to use someone else's credit card on their own account? If someone could forge the `account_id` data column, swapping an `account_id` with their own `account_id`, then you could be tricked into using the wrong credit card. By “associating” the `account_id` with the `credit_card_number`, it cannot be forged without throwing an error.
Like above, this is done simply by extending the security label with the associated data column:
```sql
CREATE TABLE credit_cards (
id bigserial primary key,
credit_card_number text,
account_id integer,
key_id uuid not null DEFAULT 'e348034b-3f07-4878-aad6-000511d12826'::uuid,
nonce bytea default pgsodium.crypto_aead_det_noncegen()
);
SECURITY LABEL FOR pgsodium
ON COLUMN credit_cards.credit_card_number
IS 'ENCRYPT WITH KEY COLUMN key_id ASSOCIATED (account_id) NONCE nonce';
```
The new label indicates which column is to be associated with the secret, and that's it! Your `account_id` and credit card number are now protected under the same authentication signature as the secret itself.
## Using an Encrypted Table
Now that you have TCE setup for a table, it's easy to use by simply inserting data into the table, and querying that data by looking at its generated view. The view is named `decrypted_<table_name>` and by default is in the same schema as your table:
```sql
INSERT INTO credit_cards (
credit_card_number,
account_id
)
VALUES
('1234-5678-8765-4321', 123);
```
Now that you have inserted data, look at the table and notice how the credit card number is encrypted. This is the data that is stored on disk, the encrypted card number, the key id, and the account id, ********************************but the key itself is not stored********************************. This means if someone gets a backup or dump of your database, they cannot decrypt the credit card number, they do not have the key, only the key ID:
```sql
> select * from credit_cards where account_id = 123;
-[ RECORD 1 ]------+---------------------------------------------------------------------
id | 1
credit_card_number | jf8KfImkKTr+j4gzyDZQtLDEFL9eSlFuKjNlNEJvDg+OIKUr2wjF/8NnYcLisb5F9xiN
account_id | 123
key_id | 7f753c4f-8c68-457a-8801-1798b2e9f44d
nonce | \x300a14aa721184ff7cf0f6bf088da267
```
For you, the developer, you need the unencrypted credit card number for you application. No problem, you can access that data using the dynamically generated decryption view `decrypted_credit_cards`:
```sql
> select * from decrypted_credit_cards where account_id = 123;
-[ RECORD 1 ]----------------+---------------------------------------------------------------------
id | 1
credit_card_number | jf8KfImkKTr+j4gzyDZQtLDEFL9eSlFuKjNlNEJvDg+OIKUr2wjF/8NnYcLisb5F9xiN
decrypted_credit_card_number | 1234-5678-8765-4321
account_id | 123
key_id | 7f753c4f-8c68-457a-8801-1798b2e9f44d
nonce | \x300a14aa721184ff7cf0f6bf088da267
```
Notice how there is a new column called `decrypted_credit_card_number`. This column is not stored in database or on disk at all, it is generated “on-the-fly” as you select from the view. Database dumps do not contain this information, only the view itself, and most importantly, *************************************raw decryption keys are never stored*************************************.
## Future possibilities
We're always thinking about the future possibilities for features and tools that we can bring to the PostgreSQL community, and we'd love to hear from you about what kind of encryption features you'd like to see. Some things we've considered but not yet explored yet are:
- Built-in Key Management Server (KMS) with REST API ala AWS or GCP.
- Seamless Integration with external KMS services for key management.
- Signcryption-based Token formats for exchanging AEAD tokens.
- End-to-End encryption for user data using libsodium's `crypto_secretstream()` API.
- Group encryption using [signcryption](https://github.com/jedisct1/libsodium-signcryption)
- Your idea here?
There's a lot of potential in the world of cryptography with Postgres and pgsodium, and we'd love to hear any ideas you may have as well. Join us in our [Discord #encryption channel](https://discord.com/channels/839993398554656828/1009906326480101417) if you want to chat more about it with us!
## More Postgres Resources
- [Postgres Full Text Search vs the rest](https://supabase.com/blog/postgres-full-text-search-vs-the-rest)
- [Postgres WASM by Snaplet and Supabase](https://supabase.com/blog/postgres-wasm)
- [Choosing a Postgres Primary Key](https://supabase.com/blog/choosing-a-postgres-primary-key)
- [Implementing "seen by" functionality with Postgres](https://supabase.com/blog/seen-by-in-postgresql)
- [Partial data dumps using Postgres Row Level Security](https://supabase.com/blog/partial-postgresql-data-dumps-with-rls)
- [Realtime Postgres RLS on Supabase](https://supabase.com/blog/realtime-row-level-security-in-postgresql)
+72
View File
@@ -0,0 +1,72 @@
import createGlobe from 'cobe'
import { useEffect, useRef } from 'react'
import { useTheme } from './Providers/index'
const Globe = () => {
const { isDarkMode } = useTheme()
const canvasRef = useRef<any>()
useEffect(() => {
let rotation: number = 0
let width: number = 0
const onResize = () => canvasRef.current && (width = canvasRef.current.offsetWidth)
window.addEventListener('resize', onResize)
onResize()
const cobe = createGlobe(canvasRef.current, {
devicePixelRatio: 2,
width: width * 2,
height: width * 2,
phi: 0,
theta: 0.3,
dark: isDarkMode ? 1 : 0,
diffuse: 3,
scale: 1,
opacity: 0.8,
mapSamples: 20000,
mapBrightness: 4,
baseColor: [255 / 255, 255 / 255, 255 / 255],
markerColor: [62 / 255, 207 / 255, 142 / 255],
glowColor: [255 / 255, 255 / 255, 255 / 255],
markers: [
{ location: [53.4084, 2.9916], size: 0.06 },
{ location: [1.3521, 103.8198], size: 0.06 },
{ location: [-40.9006, 174.886], size: 0.06 },
{ location: [14.0583, 108.2772], size: 0.06 },
{ location: [37.7749, -122.4194], size: 0.06 },
{ location: [41.3874, 2.1686], size: 0.06 },
{ location: [49.2827, -123.1207], size: 0.06 },
{ location: [-36.9848, 143.3906], size: 0.06 },
{ location: [42.3601, -71.0589], size: 0.06 },
{ location: [52.52, 13.405], size: 0.06 },
{ location: [33.749, -84.388], size: 0.06 },
{ location: [35.6762, 139.6503], size: 0.06 },
{ location: [9.19, -75.0152], size: 0.06 },
{ location: [-25.2521, -52.0215], size: 0.06 },
],
onRender: (state) => {
state.phi = rotation
rotation += 0.0025
state.width = width * 2
state.height = width * 2
},
})
setTimeout(() => (canvasRef.current.style.opacity = '1'))
return () => cobe.destroy()
}, [isDarkMode])
return (
<canvas
ref={canvasRef}
style={{
width: '100%',
height: '100%',
contain: 'layout paint size',
opacity: 0,
transition: 'opacity 1s ease',
borderRadius: '100%',
}}
/>
)
}
export default Globe
@@ -33,23 +33,17 @@ export default function Conf({
setPageState,
}}
>
<ConfContainer>
{pageState === 'registration' && !sharePage ? (
<>
{/* <Hero /> */}
<Form />
{/* <LearnMore /> */}
</>
) : (
<Ticket
username={userData.username}
name={userData.name}
ticketNumber={userData.ticketNumber}
sharePage={sharePage}
golden={userData.golden}
/>
)}
</ConfContainer>
{pageState === 'registration' && !sharePage ? (
<Form align={defaultPageState === 'registration' ? 'Center' : 'Left'} />
) : (
<Ticket
username={userData.username}
name={userData.name}
ticketNumber={userData.ticketNumber}
sharePage={sharePage}
golden={userData.golden}
/>
)}
</ConfDataContext.Provider>
)
}
@@ -1,4 +1,4 @@
.container {
/* .container {
margin: auto 0;
padding: 0 var(--space-4x);
}
@@ -8,4 +8,4 @@
margin: auto;
padding: 0 var(--space-8x);
}
}
} */
@@ -1,5 +1,5 @@
import styles from './conf-container.module.css'
export default function ConfContainer({ children }: { children: React.ReactNode }) {
return <div className={styles.container}>{children}</div>
return children
}
@@ -1,31 +1,31 @@
.form {
/* .form {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
margin-bottom: 12px;
gap: 12px;
}
} */
.formLeft {
/* .formLeft {
align-items: center;
}
.formCenter {
align-items: center;
}
} */
@media (min-width: 1200px) {
/* @media (min-width: 1200px) {
.formLeft {
align-items: start;
}
.formCenter {
align-items: center;
}
}
} */
/*
.formInfo {
max-width: 520px;
}
} */
.formInfo h3 {
font-size: 1.25em;
@@ -45,19 +45,17 @@
} */
}
.formInfoLeft {
/* .formInfoLeft {
text-align: center;
margin: 0 auto;
}
.formInfoCenter {
text-align: center;
margin: 0 auto;
}
} */
@media (min-width: 1200px) {
.formInfoLeft {
text-align: left;
margin: inherit;
}
.formInfoCenter {
text-align: center;
+54 -44
View File
@@ -6,6 +6,7 @@ import LoadingDots from './loading-dots'
import styleUtils from './utils.module.css'
import styles from './form.module.css'
import useEmailQueryParam from '~/components/LaunchWeek/Ticket/hooks/use-email-query-param'
import { IconLoader } from '~/../../packages/ui'
// import Captcha, { useCaptcha } from './captcha'
type FormState = 'default' | 'loading' | 'error'
@@ -30,6 +31,7 @@ export default function Form({ sharePage, align = 'Center' }: Props) {
const [errorMsg, setErrorMsg] = useState('')
const [errorTryAgain, setErrorTryAgain] = useState(false)
const [focused, setFocused] = useState(false)
const [submitting, setSubmitting] = useState(false)
const [formState, setFormState] = useState<FormState>('default')
const { setPageState, setUserData, session, userData, supabase } = useConfData()
const router = useRouter()
@@ -62,6 +64,12 @@ export default function Form({ sharePage, align = 'Center' }: Props) {
// Prefetch the twitter share URL to eagerly generate the page
fetch(`/launch-week/tickets/${username}`).catch((_) => {})
// Prefetch ticket og image.
fetch(
`https://obuldanrptloktxcffvn.functions.supabase.co/launchweek-ticket-og?username=${encodeURIComponent(
username ?? ''
)}`
).catch((_) => {})
setPageState('ticket')
})
@@ -71,7 +79,7 @@ export default function Form({ sharePage, align = 'Center' }: Props) {
async function register(email: string, token?: string): Promise<ConfUser> {
const { error } = await supabase!.from('lw6_tickets').insert({ email })
if (error) {
console.log({ error })
// console.log({ error })
return {
id: 'new',
ticketNumber: 1234,
@@ -153,18 +161,17 @@ export default function Form({ sharePage, align = 'Center' }: Props) {
useEmailQueryParam('email', setEmail)
return (
<>
<div className="flex flex-col gap-8">
<div
className={cn(
styleUtils.appear,
styleUtils['appear-fifth'],
[styles.formInfo, styles[`formInfo${align}`]].join(' ')
'flex flex-col gap-2 items-center xl:items-start',
align === 'Left' ? 'text-center xl:text-left' : 'text-center'
)}
>
<h3>Get a ticket</h3>
<p>
A few of the lucky attendees will get a limited edition Supabase goodie bag. Make sure you
don’t skip your chance.
<p className="text-scale-1200 text-base w-full">Coming soon</p>
<p className="text-scale-1000 text-base max-w-[420px]">
Register to get your ticket and stay tuned all week for daily announcements
</p>
</div>
{formState === 'error' ? (
@@ -188,46 +195,49 @@ export default function Form({ sharePage, align = 'Center' }: Props) {
</div>
) : (
<form
className={cn(styles.form, styles[`form${align}`], {
[styles['share-page']]: sharePage,
[styleUtils.appear]: !errorTryAgain,
[styleUtils['appear-fifth']]: !errorTryAgain && !sharePage,
[styleUtils['appear-third']]: !errorTryAgain && sharePage,
})}
className="relative mx-auto xl:mx-0 w-full md:w-auto md:min-w-[320px] md:max-w-[420px]"
onSubmit={onSubmit}
>
<div className={styles['form-row']}>
<label
htmlFor="email-input-field"
className={cn(styles['input-label'], {
[styles.focused]: focused,
})}
>
<input
className={styles.input}
autoComplete="off"
type="email"
id="email-input-field"
value={email}
onChange={(e) => setEmail(e.target.value)}
onFocus={() => setFocused(true)}
onBlur={() => setFocused(false)}
placeholder="Enter email to register free"
aria-label="Your email address"
required
/>
</label>
<button
type="submit"
className={cn(styles.submit, styles.register, styles[formState])}
disabled={formState === 'loading'}
>
{formState === 'loading' ? <LoadingDots size={4} /> : <>Register</>}
</button>
</div>
<input
className={`
transition-all
border border-scale-300 bg-scaleA-200 h-10
focus:border-scale-500 focus:ring-scaleA-300
text-scale-1200 text-base rounded-full w-full px-5
`}
type="email"
autoComplete="email"
id="email-input-field"
value={email}
onChange={(e) => setEmail(e.target.value)}
onFocus={() => setFocused(true)}
onBlur={() => setFocused(false)}
placeholder="Enter email"
aria-label="Your email address"
required
/>
<button
type="submit"
className={[
'transition-all',
'absolute bg-scale-300 text-scale-1200 border border-scale-600 text-sm hover:bg-scale-400',
'rounded-full px-4',
'focus:invalid:border-scale-500 focus:invalid:ring-scaleA-300',
'absolute right-1 my-auto h-8 top-0 bottom-0',
].join(' ')}
disabled={formState === 'loading'}
>
{formState === 'loading' ? (
<div className="flex items-center gap-2">
<IconLoader size={14} className="animate-spin" /> Registering
</div>
) : (
'Register'
)}
</button>
{/* <Captcha ref={captchaRef} onVerify={handleRegister} /> */}
</form>
)}
</>
</div>
)
}
@@ -1,13 +1,8 @@
import { useState, useRef, useEffect } from 'react'
import cn from 'classnames'
import { useEffect, useRef, useState } from 'react'
import { SITE_URL, TWEET_TEXT, TWEET_TEXT_GOLDEN } from '~/lib/constants'
// import IconTwitter from '~/components/LaunchWeek/Ticket/icons/icon-twitter'
// import IconLinkedin from '~/components/LaunchWeek/Ticket/icons/icon-linkedin'
// import IconDownload from '~/components/LaunchWeek/Ticket/icons/icon-download'
import LoadingDots from './loading-dots'
import styleUtils from './utils.module.css'
import styles from './ticket-actions.module.css'
import { IconDownload, IconLinkedin, IconTwitter } from 'ui'
import useConfData from '~/components/LaunchWeek/Ticket//hooks/use-conf-data'
import LoadingDots from './loading-dots'
type Props = {
username: string
@@ -18,7 +13,7 @@ export default function TicketActions({ username, golden = false }: Props) {
const [imgReady, setImgReady] = useState(false)
const [loading, setLoading] = useState(false)
const downloadLink = useRef<HTMLAnchorElement>()
const permalink = encodeURIComponent(`${SITE_URL}/tickets/${username}`)
const permalink = encodeURIComponent(`${SITE_URL}/tickets/${username}?v=6`)
const text = encodeURIComponent(golden ? TWEET_TEXT_GOLDEN : TWEET_TEXT)
const { userData } = useConfData()
const tweetUrl = `https://twitter.com/intent/tweet?url=${permalink}&via=supabase&text=${text}`
@@ -43,48 +38,63 @@ export default function TicketActions({ username, golden = false }: Props) {
}
}, [downloadUrl])
const ActionStyle = ({ children }: any) => {
return (
<div className="rounded-full bg-white dark:bg-scale-400 dark:hover:bg-scale-500 py-1 px-3 border border-scale-500 dark:text-white text-sm mb-1 transition-all ease-out hover:bg-scale-500">
{children}
</div>
)
}
return (
<>
<a
className={cn(styles.button, styleUtils.appear, styles.first, 'icon-button')}
href={tweetUrl}
rel="noopener noreferrer"
target="_blank"
>
{/* <IconTwitter width={24} /> Tweet it! */}
</a>
<a
className={cn(
styles.button,
styleUtils.appear,
styles.second,
'icon-button',
// LinkedIn Share widget doesn’t work on mobile
styles['linkedin-button']
)}
href={linkedInUrl}
rel="noopener noreferrer"
target="_blank"
>
{/* <IconLinkedin width={20} /> Share on LinkedIn */}
</a>
<a
className={cn(styles.button, styleUtils.appear, styles.third, 'icon-button', {
[styles.loading]: loading,
})}
href={loading ? undefined : downloadUrl}
onClick={(e) => {
if (imgReady) return
<ActionStyle>
<a
href={tweetUrl}
rel="noopener noreferrer"
target="_blank"
className="flex items-center gap-2"
>
<div className="text-scale-900">
<IconTwitter size={12} strokeWidth={1.5} />
</div>
Tweet it
</a>
</ActionStyle>
<ActionStyle>
<a
href={linkedInUrl}
rel="noopener noreferrer"
target="_blank"
className="flex items-center gap-2"
>
<div className="text-scale-900">
<IconLinkedin size={12} strokeWidth={1.5} />
</div>
Share on Linkedin
</a>
</ActionStyle>
<ActionStyle>
<a
href={loading ? undefined : downloadUrl}
onClick={(e) => {
if (imgReady) return
e.preventDefault()
downloadLink.current = e.currentTarget
// Wait for the image download to finish
setLoading(true)
}}
download="ticket.png"
>
{loading ? <LoadingDots size={4} /> : <>{/* <IconDownload width={24} /> Download */}</>}
</a>
e.preventDefault()
downloadLink.current = e.currentTarget
// Wait for the image download to finish
setLoading(true)
}}
download="ticket.png"
className="flex items-center gap-2"
>
<div className="text-scale-900">
<IconDownload size={12} strokeWidth={1.5} />
</div>
Download
{loading ? <LoadingDots size={4} /> : <>{/* <IconDownload width={24} /> Download */}</>}
</a>
</ActionStyle>
</>
)
}
@@ -117,7 +117,7 @@
display: none;
}
.fade {
/* .fade {
position: absolute;
background: linear-gradient(90deg, rgba(37, 39, 41, 0%) 0%, #252729 100%);
top: 0;
@@ -128,7 +128,7 @@
display: block;
border-radius: var(--space-2x);
pointer-events: none;
}
} */
.desktop-copy.desktop-copy-disabled {
display: none;
@@ -4,6 +4,7 @@ import { SITE_URL } from '~/lib/constants'
import styleUtils from './utils.module.css'
// import IconCopy from '~/components/LaunchWeek/Ticket/icons/icon-copy'
import styles from './ticket-copy.module.css'
import { IconCopy, IconCheck } from 'ui'
type Props = {
username: string
@@ -14,7 +15,7 @@ export default function TicketCopy({ username }: Props) {
const [scrolling, setScrolling] = useState(false)
const [copyEnabled, setCopyEnabled] = useState(false)
const [copied, setCopied] = useState(false)
const scrollRef = useRef<HTMLSpanElement>(null)
const scrollRef = useRef<HTMLParagraphElement>(null)
const buttonRef = useRef<HTMLButtonElement>(null)
const url = `${SITE_URL}/tickets/${username}`
useEffect(() => {
@@ -23,20 +24,24 @@ export default function TicketCopy({ username }: Props) {
}
}, [])
const copiedText = (
<span
className={cn(styles.copied, {
[styles.visible]: copied,
})}
>
Copied!
</span>
)
const copiedText = <span className="text-xs text-scale-1200">Copied!</span>
// background: none;
// outline: none;
// border: none;
// z-index: 2;
// border-radius: var(--space-2x);
// width: 40px;
// display: flex;
// align-items: center;
// justify-content: center;
// margin-right: -10px;
const copyButton = (
<button
type="button"
className={styles['copy-button']}
name="Copy"
className="text-scale-900 hover:text-scale-1200 w-21 flex items-center cursor-pointer"
ref={buttonRef}
onClick={() => {
navigator.clipboard.writeText(url).then(() => {
@@ -47,60 +52,51 @@ export default function TicketCopy({ username }: Props) {
})
}}
>
{/* <IconCopy /> */}
{copied ? (
<div className="text-brand-900">
<IconCheck size={14} />
</div>
) : (
<IconCopy size={14} />
)}
</button>
)
return (
<div className={cn(styles.wrapper, styleUtils.appear)}>
<div className={styles['label-wrapper']}>
<div className={styles.label}>Your ticket URL:</div>
<div
className={cn(styles['mobile-copy'], {
[styles['mobile-copy-disabled']]: !copyEnabled,
})}
>
{copiedText}
{copyButton}
<div
className={cn(
styleUtils.appear,
styleUtils['appear-third'],
'bg-scaleA-200 h-8 rounded border border-scale-400 w-full'
)}
>
<div className="px-3 h-full flex items-center gap-3 w-full truncate relative pr-20">
<div className="text-scale-900 text-sm hidden lg:flex">Your ticket URL:</div>
<div className="flex items-center truncate">
<p
className={['text-xs font-mono text-scale-1200 truncate'].join(' ')}
ref={scrollRef}
onScroll={() => {
if (!scrolling) {
setScrolling(true)
const animationFrame = requestAnimationFrame(() => {
const scrollableWidth =
(scrollRef.current?.scrollWidth || 0) - (scrollRef.current?.clientWidth || 0)
setFadeOpacity(
(scrollableWidth - (scrollRef.current?.scrollLeft || 0)) /
(scrollableWidth || 1)
)
cancelAnimationFrame(animationFrame)
setScrolling(false)
})
}
}}
>
{url}
</p>
</div>
</div>
<div
className={cn(styles.field, {
[styles['desktop-copy-disabled']]: !copyEnabled,
})}
>
<span
className={styles.url}
ref={scrollRef}
onScroll={() => {
if (!scrolling) {
setScrolling(true)
const animationFrame = requestAnimationFrame(() => {
const scrollableWidth =
(scrollRef.current?.scrollWidth || 0) - (scrollRef.current?.clientWidth || 0)
setFadeOpacity(
(scrollableWidth - (scrollRef.current?.scrollLeft || 0)) / (scrollableWidth || 1)
)
cancelAnimationFrame(animationFrame)
setScrolling(false)
})
}
}}
>
{url}
</span>
<span
className={cn(styles.fade, {
[styles['desktop-copy-disabled']]: !copyEnabled,
})}
style={{ opacity: fadeOpacity }}
/>
<div
className={cn(styles['desktop-copy'], styleUtils['hide-on-mobile'], {
[styles['desktop-copy-disabled']]: !copyEnabled,
})}
>
{copiedText}
<div className="absolute right-3 with-auto height-auto flex items-center">
{copied && copiedText}
{copyButton}
</div>
</div>
@@ -62,41 +62,22 @@ export default function Form({ defaultUsername = '', setTicketGenerationState }:
options: { redirectTo: `${SITE_ORIGIN}/launch-week/tickets` },
})
}}
className="flex flex-col items-center xl:block"
>
<div className={cn(formStyles['form-row'], ticketFormStyles['form-row'])}>
<div className={cn(formStyles['github-wrapper'])}>
<div className="flex flex-col gap-3">
<div>
<button
type="submit"
className={cn(
formStyles.submit,
formStyles['generate-with-github'],
formStyles[formState]
)}
className="rounded-full bg-scale-400 py-1 px-3 border border-scale-500 dark:text-white text-sm mb-1 transition-all ease-out hover:bg-scale-500"
disabled={formState === 'loading' || Boolean(username)}
onClick={() => {
// if (formRef && formRef.current && isMobileOrTablet()) {
// scrollTo(formRef.current, formRef.current.offsetHeight);
// }
}}
>
<div className={ticketFormStyles.generateWithGithub}>
<span className={ticketFormStyles.githubIcon}>
{/* <GithubIcon color="#fff" size={24} /> */}
</span>
{formState === 'loading' ? (
<LoadingDots size={4} />
) : (
username || 'Generate with GitHub'
)}
</div>
{username ? (
<span className={ticketFormStyles.checkIcon}>
{/* <CheckIcon color="#fff" size={24} /> */}
</span>
) : null}
<span className={`${username && 'text-scale-900'}`}>
{username ? 'Done!' : 'Connect with GitHub'}
</span>
{username ? <span className={ticketFormStyles.checkIcon}></span> : null}
</button>
<p className={ticketFormStyles.description}>Only public info will be used.</p>
</div>
{!username && <p className={'text-xs text-scale-900'}>Only public info will be used.</p>}
</div>
</form>
)
@@ -1,6 +1,5 @@
.info {
display: flex;
flex-direction: column;
gap: 0px;
margin-bottom: 64px;
padding: 32px;
@@ -12,13 +11,19 @@
grid-template-columns: 1fr 2fr;
gap: calc(var(--space-4x) * var(--size));
font-size: calc(1em * var(--size)); */
gap: calc(8px * var(--size));
padding: 0;
margin-bottom: calc(32px * var(--size));
margin-left: calc(160px * var(--size));
}
}
@media (max-width: 768px) {
.info {
flex-direction: column;
align-items: center;
}
}
.logo {
/* margin-bottom: var(--space-6x); */
/* max-width: 96px; */
@@ -34,7 +39,7 @@
.date {
/* text-transform: uppercase; */
font-size: calc(18px * var(--size));
/* font-size: calc(18px * var(--size)); */
color: var(--lw-secondary-color);
}
@@ -47,7 +52,7 @@
.date {
margin-bottom: 0;
line-height: 1.15;
font-size: calc(20px * var(--size));
/* font-size: calc(20px * var(--size)); */
}
}
@@ -74,20 +79,9 @@
} */
.url {
margin-bottom: var(--space-12x);
font-size: calc(18px * var(--size));
color: var(--lw-secondary-color);
}
.url-golden {
color: var(--gold-primary);
}
@media (min-width: 768px) {
.url {
margin-bottom: 0;
font-size: calc(20px * var(--size));
line-height: 1.15;
}
}
@@ -11,10 +11,20 @@ export default function TicketInfo({
}) {
return (
<div className={styles.info}>
<div className={cn(styles.date, { [styles['date-golden']]: golden })}>
<div
className={`${cn(styles.date, { [styles['date-golden']]: golden })} text-sm mr-4 ${
golden && '!text-white'
}`}
>
<div>{DATE}</div>
</div>
<div className={cn(styles.url, { [styles['url-golden']]: golden })}>{siteUrlForTicket}</div>
<div
className={`${cn(styles.date, { [styles['date-golden']]: golden })} text-sm ${
golden && '!text-white'
}`}
>
<div>supabase.com/launch-week</div>
</div>
</div>
)
}
@@ -4,7 +4,7 @@ type Props = {
export default function TicketNumber({ number }: Props) {
const numDigits = `${number}`.length
const prefix = `000000`.slice(numDigits)
const prefix = `00000000`.slice(numDigits)
return (
<>
№ {prefix}
@@ -7,7 +7,7 @@
width: 60px;
height: 60px;
border-radius: 50%;
border: 1px solid #333;
border: 1px solid #999;
}
@media (min-width: 768px) {
@@ -34,6 +34,7 @@
line-height: 1.15;
letter-spacing: -0.02em;
margin: 0;
max-width: 300px;
}
.name-golden {
@@ -72,14 +73,25 @@
.empty-icon {
display: block;
background: linear-gradient(135deg, #b4b6b8, #7f8183);
background: linear-gradient(320deg, #121212, #191919);
width: 60px;
}
.empty-icon--golden {
display: block;
background: linear-gradient(90deg, #fff9eb, #e2ba52);
width: 60px;
border: 1px solid #ecc154ad;
}
@media (min-width: 768px) {
.empty-icon {
width: calc(80px * var(--size));
}
.empty-icon--golden {
width: calc(80px * var(--size));
}
}
@keyframes loading {
@@ -29,21 +29,30 @@ export default function TicketProfile({
{username ? (
<img src={`https://github.com/${username}.png`} alt={username} className={styles.image} />
) : (
<span className={cn(styles.image, styles['empty-icon'])}>{/* <IconAvatar /> */}</span>
<span
className={cn(
styles.image,
golden ? styles['empty-icon--golden'] : styles['empty-icon']
)}
>
{/* <IconAvatar /> */}
</span>
)}
</span>
<div className={styles.text}>
<p
className={cn(
className={`${cn(
styles.name,
{ [styles['name-blank']]: !username },
{ [styles['name-golden']]: golden }
)}
)} dark:text-white`}
>
<span
className={cn(styles.skeleton, styles.wrapper, {
className={`${cn(styles.skeleton, styles.wrapper, {
[styles.show]: ticketGenerationState === 'loading',
})}
})} text-4xl bg-gradient-to-r ${
golden ? 'to-[#ffe8af]' : 'to-slate-900'
} from-white via-white bg-clip-text text-transparent`}
>
{name || username || 'Your Name'}
</span>
@@ -54,13 +63,21 @@ export default function TicketProfile({
[styles.show]: ticketGenerationState === 'loading',
})}
>
<span className={cn(styles.githubIcon, { [styles['githubIcon-golden']]: golden })}>
<span className={`${golden ? 'text-white' : 'text-scale-1100'}`}>
{/* <GithubIcon
color={golden ? 'var(--gold-primary)' : 'var(--secondary-color)'}
size={20 * size}
/> */}
{username ? `@${username}` : <>username</>}
</span>
{username || <>username</>}
<img
alt="Supabase disconnected badge"
src={`/images/launchweek/supabadge-${
golden ? 'gold' : username ? 'connected' : 'disconnected'
}.svg`}
className="ml-2"
/>
</span>
</p>
</div>
@@ -9,49 +9,64 @@
border-radius: 16px;
background-color: rgba(0, 0, 0, 1);
-webkit-backdrop-filter: blur(5px);
backdrop-filter: blur(5px);
-webkit-backdrop-filter: blur(2px);
backdrop-filter: blur(2px);
/* background: linear-gradient(#fff000, var(--gold-secondary)); */
border: 1px solid rgba(255, 255, 0, 0.629);
border: 1px solid #1c5f52;
animation: blank-svg-shadow 2s ease-in-out infinite alternate;
background-image: url('/images/launchweek/regular-ticket-background.png');
background-repeat: no-repeat;
background-size: contain;
}
.visual--gold {
background-color: gold;
/* border: 10px solid;
border-image-slice: 1;
border-width: 2px;
border-radius: 16px; */
background: linear-gradient(#fff000, var(--gold-secondary));
background-color: linear-gradient(#fff000, #dea513);
background-color: #dea513;
border: 1px solid rgba(255, 255, 0, 0.629);
animation: svg-shadow 2s ease-in-out infinite alternate;
background-image: url('/images/launchweek/gold-ticket-background.png');
/* filter: drop-shadow(2.45223px 8.5828px 73.5669px #FFBD14); */
/* border-image-source: linear-gradient(to left, var(--gold-secondary), var(--gold-accent)); */
}
@media (max-width: 768px) {
.visual {
background-size: cover;
background-image: url('/images/launchweek/2x-regular-ticket-background.jpg');
}
.visual--gold {
background-image: url('/images/launchweek/2x-gold-ticket-background.jpg');
}
}
/* .svg-shadow {
} */
@keyframes svg-shadow {
from {
filter: drop-shadow(0 0 5px #fff) drop-shadow(0 0 15px var(--gold-primary))
drop-shadow(0 0 20px var(--gold-accent));
filter: drop-shadow(0 0 2px #fff) drop-shadow(0 0 4px #d99f0c) drop-shadow(0 0 6px #d99f0c);
}
to {
filter: drop-shadow(0 0 20px #fff) drop-shadow(0 0 10px var(--gold-accent))
drop-shadow(0 0 40px var(--gold-primary));
filter: drop-shadow(0 0 3px #fff) drop-shadow(0 0 6px #d99f0c) drop-shadow(0 0 9px #d99f0c);
}
}
@keyframes blank-svg-shadow {
from {
filter: drop-shadow(0 0 5px #fff) drop-shadow(0 0 15px #12f28d) drop-shadow(0 0 20px #12f28d);
filter: drop-shadow(0 0 2px #1c5f52) drop-shadow(0 0 4px #1c5f52) drop-shadow(0 0 6px #1c5f52);
}
to {
filter: drop-shadow(0 0 20px #fff) drop-shadow(0 0 10px #12f28d) drop-shadow(0 0 40px #12f28d);
filter: drop-shadow(0 0 3px #1c5f52) drop-shadow(0 0 6px #1c5f52) drop-shadow(0 0 9px #1c5f52);
}
}
@@ -105,22 +120,21 @@
position: absolute;
top: 90px;
width: 100%;
height: 100%;
padding: 46px 29px;
}
@media (min-width: 768px) {
.profile {
top: calc(48px * var(--size));
padding: calc(44px * var(--size)) calc(160px * var(--size)) calc(44px * var(--size))
calc(63px * var(--size));
top: 50%;
transform: translateY(-50%);
padding-left: 65px;
}
}
.info {
z-index: 1;
position: absolute;
bottom: 0;
bottom: 40px;
width: 100%;
height: 100%;
display: flex;
@@ -150,11 +164,11 @@
}
.ticket-number {
font-weight: bold;
font-size: calc(40px * var(--size));
line-height: 1;
width: calc(330px * var(--size));
text-align: center;
background-clip: text;
}
.ticket-number-golden {
@@ -25,15 +25,19 @@ export default function TicketVisual({
ticketGenerationState = 'default',
golden = false,
}: Props) {
// golden = true;
// golden = true
const router = useRouter()
const basePath = router.basePath
return (
<>
<div
className={[styles.visual, golden ? styles['visual--gold'] : ''].join(' ')}
style={{ ['--size' as string]: size }}
// className={`rounded-2xl shadow-xl shadow-brand-500`}
style={{
['--size' as string]: size,
}}
>
<div className={styles['horizontal-ticket']}>
{/* {username ? <TicketColored golden={golden} /> : <TicketMono golden={golden} />} */}
@@ -49,7 +53,7 @@ export default function TicketVisual({
<TicketMonoMobile golden={golden} />
</div>
<div className={styles.logo}>
<img src="/images/launchweek/launchweek-logo--dark.svg" />
<img src={`/images/launchweek/ticket-logo-${golden ? 'light' : 'dark'}.svg`} />
</div>
<div className={styles.profile}>
<TicketProfile
@@ -69,9 +73,13 @@ export default function TicketVisual({
/>
</div>
{ticketNumber && (
<div className={styles['ticket-number-wrapper']}>
<div className={`${styles['ticket-number-wrapper']} dark:text-white`}>
<div
className={cn(styles['ticket-number'], { [styles['ticket-number-golden']]: golden })}
className={`${cn(styles['ticket-number'], {
[styles['ticket-number-golden']]: golden,
})} bg-gradient-to-r from-white via-white ${
golden ? 'to-[#ffe8af]' : 'to-slate-900'
}`}
>
<TicketNumber number={ticketNumber} />
</div>
@@ -1,5 +1,4 @@
.hero {
font-size: 30px;
line-height: 1.15;
letter-spacing: -0.05em;
font-weight: 400;
@@ -9,7 +8,6 @@
@media (min-width: 768px) {
.hero {
font-size: 30px;
line-height: 1;
/* font-weight: 800; */
/* max-width: 800px; */
@@ -111,7 +109,7 @@
}
.ticket-actions {
margin-top: 30px;
margin-top: 70px;
display: flex;
align-items: center;
justify-content: center;
@@ -140,7 +138,6 @@
grid-template-columns: 1fr 1fr;
}
.hero {
font-size: 48px;
text-align: left;
}
.description {
@@ -153,9 +150,3 @@
border-radius: calc(16px * var(--size));
width: 100%;
}
@media screen and (min-width: 1280px) {
.ticket-layout {
padding: 0 40px;
}
}
@@ -45,53 +45,63 @@ export default function Ticket({ username, name, ticketNumber, sharePage, golden
// scrollTo(divRef.current, -30)
// }
// }, [divRef, sharePage])
// golden = true
return (
<div
className={cn(styles['ticket-layout'], {
[styles['ticket-share-layout']]: sharePage,
})}
>
<div ref={divRef}>
<div className={styles['ticket-text']}>
<h2 className={cn(styles.hero, styleUtils.appear, styleUtils['appear-first'])}>
<div className="grid grid-cols-1 xl:grid-cols-2 gap-8 xl:gap-8 2xl:gap-16">
<div ref={divRef} className="flex flex-col gap-6">
<div
className={`
text-scale-1200
flex flex-col
items-center
text-center xl:text-left
xl:items-start
gap-3
`}
>
<h1 className={cn(styleUtils.appear, styleUtils['appear-first'], 'text-xl xl:text-3xl')}>
{sharePage ? (
name ? (
<>{name}’s Ticket</>
<p className="text-3xl dark:text-scale-1200 tracking-[0.02rem]">{name}'s ticket</p>
) : (
<>{SITE_NAME}</>
<p className="text-3xl dark:text-scale-1200 tracking-[0.02rem]">{SITE_NAME}</p>
)
) : golden ? (
<p className="text-xl dark:text-scale-1200 tracking-[0.02rem]">Congratulations!</p>
) : (
<p className="text-xl dark:text-scale-1200 tracking-[0.02rem]">You have a ticket!</p>
)}
</h1>
<h2 className="text-base dark:text-scale-1000 max-w-[520px]">
{sharePage ? (
<p>
Join {name ?? 'us'} on {DATE}.
</p>
) : golden ? (
<>
You won a golden ticket! <br /> Claim it now!
You got a Golden ticket. This means you’re in, and you also won a Supabase goodie
bag. Tweet it to redeem your swag pack!
</>
) : username ? (
<>Here is your unique ticket image for bragging on socials!</>
) : (
<>
You're in. <br /> Make it unique.
This means you're in. Generate a unique ticket image with your GitHub profile, cause
a few of the lucky attendees will get a limited edition Supabase goodie bag. Make
sure you don't skip your chance.
</>
)}
</h2>
<p className={cn(styles.description, styleUtils.appear, styleUtils['appear-second'])}>
{sharePage ? (
<>
Join {name ?? 'us'} on {DATE}.
</>
) : golden ? (
<>Claim your ticket with GitHub and Tweet it to redeem your swag pack!</>
) : (
<>
Generate a unique ticket image with <br className={styleUtils['hide-on-mobile']} />
your GitHub profile.
</>
)}
</p>
</div>
<div className={cn(styleUtils.appear, styleUtils['appear-third'])}>
{!sharePage ? (
<TicketForm
defaultUsername={username ?? undefined}
setTicketGenerationState={setTicketGenerationState}
/>
<>
<TicketForm
defaultUsername={username ?? undefined}
setTicketGenerationState={setTicketGenerationState}
/>
</>
) : (
<Form sharePage align="Left" />
)}
@@ -113,11 +123,11 @@ export default function Ticket({ username, name, ticketNumber, sharePage, golden
{!sharePage && (
<>
{username ? (
<div>
<div className={styles['ticket-actions']}>
<div className="flex flex-col gap-6 py-16">
<div className={`flex flex-col xl:flex-row gap-3 items-center justify-center`}>
<TicketActions username={username} golden={golden} />
</div>
<div className={styles['ticket-copy']}>
<div>
<TicketCopy username={username} />
</div>
</div>
+5 -6
View File
@@ -58,27 +58,26 @@ const Announcement = () => {
hover:to-green-1100 relative flex
cursor-pointer flex-row
space-x-3
overflow-hidden bg-gradient-to-r from-green-900
overflow-hidden
text-white
"
>
<div
className="
mx-auto flex items-center justify-center divide-white p-3 text-sm font-medium lg:container lg:divide-x
mx-auto flex items-center gap-6 justify-center p-3 text-sm font-medium lg:container
lg:px-16 xl:px-20
"
>
<span className="item-center flex gap-2 px-3">
<span>{announcement.text}</span>
<span className="item-center flex gap-2 pr-8 sm:px-3">
{announcement.badge && (
<div className="bg-brand-400 text-brand-900 py-0.25 rounded px-1.5">
<div className="bg-[#2E2E2E] text-white py-0.25 rounded-2xl px-3 py-1 border border-gray-1100 whitespace-nowrap">
{announcement.badge}
</div>
)}
<span>{announcement.text}</span>
</span>
<span className="hidden items-center space-x-2 px-3 lg:flex">
<span>{announcement.cta}</span>
<IconChevronRight size={14} />
</span>
</div>
<div
+22 -1
View File
@@ -180,7 +180,7 @@ const Nav = () => {
return (
<>
<Announcement />
{/* <Announcement /> */}
<div className="sticky top-0 z-50">
<div className="bg-scale-200 absolute top-0 h-full w-full opacity-80"></div>
<nav className="border-scale-400 border-b backdrop-blur-sm">
@@ -234,6 +234,19 @@ const Nav = () => {
Blog
</a>
</Link>
<div className="xl:block hidden">
<Link href="/launch-week">
<a
className={`
text-brand-900 hover:text-brand-900 hover:border-brand-1000 dark:text-dark-100 dark:hover:border-dark-100 inline-flex items-center
border-b-2 border-transparent p-5 px-1
text-sm font-medium
`}
>
Launch Week 6
</a>
</Link>
</div>
</div>
</div>
<div className="flex items-center gap-2">
@@ -363,6 +376,14 @@ const Nav = () => {
Blog
</a>
</Link>
<Link href="/launch-week">
<a
target="_blank"
className="text-scale-900 dark:hover:bg-scale-600 block rounded-md py-2 pl-3 pr-4 text-base font-medium hover:border-gray-300 hover:bg-gray-50 dark:text-white"
>
Launch Week 6
</a>
</Link>
</div>
<div className="p-3">
<p className="text-scale-900 mb-6 text-sm">Products available:</p>
+5 -5
View File
@@ -1,7 +1,7 @@
{
"show": false,
"text": "Auth UI Launch 🚀",
"cta": "Vote now",
"link": "https://www.producthunt.com/posts/pre-built-auth-ui-for-react",
"badge": "Product Hunt"
"show": true,
"text": "Launch Week 6 begins December 12th",
"cta": "Don't miss a thing, get your free ticket",
"link": "https://supabase.com/launch-week",
"badge": "Get a reminder"
}
+1 -1
View File
@@ -111,7 +111,7 @@
},
{
"text": "Careers",
"url": "https://about.supabase.com/careers"
"url": "/careers"
},
{
"text": "Company",
+148
View File
@@ -0,0 +1,148 @@
{
"company": [
{
"number": "50+",
"text": "team members\n\nin 20+ countries"
},
{
"number": "15+",
"text": "languages spoken"
},
{
"number": "$116M",
"text": "in funding"
},
{
"number": "8500+",
"text": "community members"
},
{
"number": "1000+",
"text": "memes posted\n\n(and counting)"
}
],
"humanPowered": [
{
"icon": "egoless",
"title": "Egoless",
"text": "Willing to do the schlep, the team goals come first."
},
{
"icon": "curious",
"title": "Polyglot",
"text": "Broad skillets. Growth mindset."
},
{
"icon": "process",
"title": "Process Driven",
"text": "Engineer yourself out of the job."
},
{
"icon": "honesty",
"title": "Intellectual Honesty",
"text": "Put our own biases aside."
},
{
"icon": "flexibility",
"title": "Kaizen mindset",
"text": "Continuously improve all functions."
}
],
"contributors": [
"Isaiah-Hamilton",
"Hallidayo",
"mertovich",
"mikqi",
"florian-lefebvre",
"m4salah",
"carlosstenzel",
"M4Tdev",
"MisterJimson",
"aminamos",
"vasudeveloper001",
"erondpowell",
"alfredlua",
"saloni137",
"rubensdemelo",
"zernonia",
"timsonner",
"sureshdsk",
"owlnai"
],
"benefits": [
{
"icon": "remote",
"title": "Fully Remote",
"text": "We hire globally. We believe you can do your best work from anywhere. There are no Supabase offices, but we provide a co-working membership that you can use anywhere in the world."
},
{
"icon": "tech_allowance",
"title": "Hardware Budget",
"text": "Use this budget for anything you need to set up your work environment from tech to office setup."
},
{
"icon": "healthcare",
"title": "Health Benefits",
"text": "We provide 100% health coverage for employees and 80% for dependants. It's important to us that you & your family have health care covered."
},
{
"icon": "offsite",
"title": "Annual Off-Sites",
"text": "We believe in the value of in-person time, so we all come together annually for a week. Each year we pick somewhere different in the world. It's a highlight of the year!"
},
{
"icon": "flexibility",
"title": "Flexible Work",
"text": "Work remotely from anywhere in the world. We work in teams, but we work asynchronously. You know what you need to do and when it needs to be done."
},
{
"icon": "education",
"title": "Professional Development",
"text": "An annual allowance to spend on professional development and education. Use this for courses, books, conferences, or anything that supports your continuous learning."
}
],
"positions": [
{
"title": "Elixir Engineers",
"location": "US time zone",
"employment": "Full-time",
"description": "Elixir is powering more and more backend"
},
{
"title": "Elixir Engineers",
"location": "US time zone",
"employment": "Full-time",
"description": "Elixir is powering more and more backend"
},
{
"title": "Elixir Engineers",
"location": "US time zone",
"employment": "Full-time",
"description": "Elixir is powering more and more backend"
},
{
"title": "Elixir Engineers",
"location": "US time zone",
"employment": "Full-time",
"description": "Elixir is powering more and more backend"
}
],
"hiring": [
{
"title": "Recruiter Interview",
"text": "Chat with our recruiter to tell us about your career journey, professional goals and we'll share more about Supabase, the role & our recruitment process."
},
{
"title": "Technical / Skill-based Interview",
"text": "Meet with the team lead. In this interview we'll aim to learn about your hard skills and problem solving approach."
},
{
"title": "Another Technical / Skill-based Interview",
"text": "You'll either meet the department lead or peer. This interview will dive deeper into your hard skills and our expectations for this role."
},
{
"title": "Final Founders Interview",
"text": "Our co-founders meet every candidate as the final step. Learn more about Supabase's values, mission and vision directly from them."
}
]
}
+7 -4
View File
@@ -3,13 +3,16 @@ export const DESCRIPTION = 'The Open Source Alternative to Firebase.'
// Launch Weeek
export const SAMPLE_TICKET_NUMBER = 1234
export const SITE_URL = process.env.NEXT_PUBLIC_VERCEL_URL
? `https://${process.env.NEXT_PUBLIC_VERCEL_URL}/launch-week`
: 'http://localhost:3000/launch-week'
export const SITE_URL =
process.env.NEXT_PUBLIC_VERCEL_ENV === 'production'
? 'https://supabase.com/launch-week'
: process.env.NEXT_PUBLIC_VERCEL_URL
? `https://${process.env.NEXT_PUBLIC_VERCEL_URL}/launch-week`
: 'http://localhost:3000/launch-week'
export const SITE_ORIGIN = new URL(SITE_URL).origin
export const TWITTER_USER_NAME = 'supabase'
export const COOKIE = 'supa-launch-week-ticke-user-id'
export const DATE = 'December 12 8:00 PT | 11:00 ET'
export const DATE = 'December 12th, 2022'
export const TWEET_TEXT = 'Got my ticket for #SupaLaunchWeek 6!'
export const TWEET_TEXT_GOLDEN = `Woop! I got a Golden Ticket for #SupaLaunchWeek 6! SupaSwag on its way!`
export const SITE_NAME = 'Supabase'
+5
View File
@@ -1560,4 +1560,9 @@ module.exports = [
source: '/forgot-password',
destination: 'https://app.supabase.com/forgot-password',
},
{
permanent: true,
source: '/docs/guides/functions/examples',
destination: '/docs/guides/functions',
},
]
+1 -1
View File
@@ -3,7 +3,7 @@ import { Database } from './database.types'
const SUPABASE_URL = 'https://obuldanrptloktxcffvn.supabase.co'
const SUPABASE_ANON_KEY =
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlhdCI6MTYzNzY1NjAxNSwiZXhwIjoxOTUzMjMyMDE1fQ.0sfp_Njf7l4g-nOCF5a1TQE11rPqtz8Y10uctIetkBA'
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im9idWxkYW5ycHRsb2t0eGNmZnZuIiwicm9sZSI6ImFub24iLCJpYXQiOjE2Njk3MjcwMTIsImV4cCI6MTk4NTMwMzAxMn0.SZLqryz_-stF8dgzeVXmzZWPOqdOrBwqJROlFES8v3I'
const supabase = createClient<Database>(SUPABASE_URL, SUPABASE_ANON_KEY)
+2 -1
View File
@@ -43,12 +43,13 @@ const nextConfig = {
'https://s3-us-west-2.amazonaws.com',
's3-us-west-2.amazonaws.com',
'user-images.githubusercontent.com',
'pbs.twimg.com',
],
},
async headers() {
return [
{
source: '/(.*)',
source: '/:path*',
headers: [
{
key: 'Strict-Transport-Security',
+1
View File
@@ -21,6 +21,7 @@
"@supabase/auth-ui-react": "^0.1.6",
"@supabase/supabase-js": "^2.0.0",
"classnames": "^2.3.1",
"cobe": "^0.6.2",
"common": "*",
"config": "*",
"date-fns": "2.10.0",
+4
View File
@@ -7,6 +7,7 @@ import Meta from '~/components/Favicons'
import '../styles/index.css'
import { post } from './../lib/fetchWrapper'
import { ThemeProvider } from '~/components/Providers'
import Head from 'next/head'
export default function MyApp({ Component, pageProps }: AppProps) {
const router = useRouter()
@@ -36,6 +37,9 @@ export default function MyApp({ Component, pageProps }: AppProps) {
return (
<>
<Head>
<meta name="viewport" content="width=device-width, initial-scale=1" />
</Head>
<Meta />
<DefaultSeo
title={site_title}
+83
View File
@@ -0,0 +1,83 @@
.contributors-bg-circle {
@apply bg-gradient-to-r from-scale-100 via-scale-700 to-scale-100 rounded-full p-[1px] aspect-square absolute;
}
.contributors-0 {
@apply left-[11%] top-[95%] xl:left-[10%] xl:top-[94%];
}
.contributors-1 {
@apply hidden xl:block xl:left-[70%] xl:top-[61%];
}
.contributors-2 {
@apply left-[31%] top-[62%] xl:left-[16%] xl:top-[70%];
}
.contributors-3 {
@apply left-[11%] top-[52%] md:left-[9%] md:top-[55%] xl:left-[21%] xl:top-[85%];
}
.contributors-4 {
@apply left-[42%] top-[77%] xl:left-[29%] xl:top-[94%];
}
.contributors-5 {
@apply left-[61%] top-[93%] xl:left-[33%] xl:top-[75%];
}
.contributors-6 {
@apply hidden md:block md:z-10 md:left-[50%] md:top-[61%] xl:block xl:left-[83%] xl:top-[60%];
}
.contributors-7 {
@apply hidden xl:block xl:left-[78%] xl:top-[45%];
}
.contributors-8 {
@apply z-10 left-[81%] top-[65%] xl:left-[57%] xl:top-[93%];
}
.contributors-9 {
@apply hidden md:block md:z-10 md:left-[32%] md:top-[89%] xl:block xl:left-[94%] xl:top-[74%];
}
.contributors-10 {
@apply z-10 hidden xl:block xl:left-[58%] xl:top-[58%];
}
.contributors-11 {
@apply hidden xl:block xl:left-[74%] xl:top-[82%];
}
.contributors-12 {
@apply hidden xl:block xl:left-[43%] xl:top-[83%];
}
.contributors-13 {
@apply hidden xl:block xl:left-[61%] xl:top-[77%];
}
.contributors-14 {
@apply hidden xl:block xl:left-[52%] xl:top-[40%];
}
.contributors-15 {
@apply hidden xl:block xl:left-[85%] xl:top-[88%];
}
.contributors-16 {
@apply left-[17%] top-[76%] xl:left-[3%] xl:top-[69%];
}
.contributors-17 {
@apply z-10 left-[64%] top-[53%] xl:left-[47%] xl:top-[67%];
}
.contributors-18 {
@apply left-[67%] top-[74%] xl:left-[36%] xl:top-[57%];
}
.contributors-19 {
@apply z-10 left-[82%] top-[86%] xl:left-[27%] xl:top-[50%];
}
+516
View File
@@ -0,0 +1,516 @@
import { NextPage } from 'next'
import { NextSeo } from 'next-seo'
import Image from 'next/image'
import Link from 'next/link'
import { useRouter } from 'next/router'
import ReactMarkdown from 'react-markdown'
import { Badge, Button, IconCheck } from 'ui'
import Globe from '~/components/Globe'
import DefaultLayout from '~/components/Layouts/Default'
import SectionContainer from '~/components/Layouts/SectionContainer'
import { useTheme } from '~/components/Providers'
import career from '~/data/career.json'
import Styles from './career.module.css'
export async function getStaticProps() {
const job_res = await fetch('https://boards-api.greenhouse.io/v1/boards/supabase/jobs')
const job_data = await job_res.json()
const contributor_res = await fetch(
'https://api.github.com/repos/supabase/supabase/contributors?per_page=100'
)
const contributor_arr = await contributor_res.json()
const contributor_data = await contributor_arr.map(
(contributor: { login: string; avatar_url: string; html_url: string }) => {
return {
login: contributor.login,
avatar_url: contributor.avatar_url,
html_url: contributor.html_url,
}
}
)
const contributors = await contributor_data.filter((contributor: any) =>
career.contributors.includes(contributor.login)
)
contributors.push(
{
login: 'XquisiteDreamer',
avatar_url: 'https://pbs.twimg.com/profile_images/1475874191249399808/H6TPHpq7_400x400.png',
html_url: 'https://twitter.com/XquisiteDreamer',
},
{
login: 'marijanasimag',
avatar_url: 'https://avatars.githubusercontent.com/u/46031252?v=4',
html_url: 'https://github.com/marijanasimag',
},
{
login: 'estee_tey',
avatar_url: 'https://pbs.twimg.com/profile_images/1589662526941253632/s1cu3vuD_400x400.jpg',
html_url: 'https://twitter.com/estee_tey',
},
{
login: 'ghostdevv',
avatar_url: 'https://avatars.githubusercontent.com/u/47755378?v=4',
html_url: 'https://github.com/ghostdevv',
}
)
if (!job_data && !contributors) {
return {
props: {
notFound: true,
},
}
}
return {
props: {
jobs: job_data.jobs,
contributors: contributors,
},
}
}
const CareerPage: NextPage = ({ jobs, contributors }: any) => {
const { isDarkMode } = useTheme()
const { basePath } = useRouter()
const meta_title = 'Careers | Supabase'
const meta_description = 'Help build software developers love'
return (
<>
<NextSeo
title={meta_title}
description={meta_description}
openGraph={{
title: meta_title,
description: meta_description,
url: `https://supabase.com/careers`,
images: [
{
url: `https://supabase.com${basePath}/images/career/careers_og.jpg`,
},
],
}}
/>
<DefaultLayout>
<div className="text-scale-1200">
<div className="container relative mx-auto px-6 py-10 lg:pt-12 lg:px-16 xl:px-20 text-center space-y-4">
<span className="text-sm text-brand-900 md:text-base">Careers</span>
<h2 className="text-3xl md:text-4xl xl:text-5xl lg:max-w-2xl xl:max-w-3xl lg:mx-auto tracking-[-1.5px]">
We’re on a mission to build the best developer platform
</h2>
<p className="text-sm md:text-base text-scale-1000 max-w-sm sm:max-w-md md:max-w-lg mx-auto">
Explore remote possibilities and join our team to help us achieve it.
</p>
<Button
as="a"
// @ts-ignore
href="#positions"
className="text-white xl:text-sm"
>
Open positions
</Button>
</div>
<SectionContainer>
<div className="flex flex-wrap md:flex-nowrap -mt-6 md:mt-0 w-fit md:w-full mx-auto md:flex md:items-start justify-around lg:w-full lg:max-w-5xl">
{career.company.map((company: { number: string; text: string }, i: number) => {
return (
<div
key={i}
className="border-t-[1px] mt-6 mx-2 md:mx-2 md:mt-0 md:border-0 border-brand-900 w-[134px] md:max-w-none"
>
<div className="hidden md:block border-t-[1px] lg:border-t-2 border-brand-900 w-[60px] lg:w-[100px]"></div>
<h2 className="text-3xl lg:text-4xl pt-3 tracking-[-1.5px]">
{company.number}
</h2>
<ReactMarkdown className="text-scale-1100 text-sm lg:text-base">
{company.text}
</ReactMarkdown>
</div>
)
})}
</div>
</SectionContainer>
<div className="py-[1.25px] bg-gradient-to-r from-scale-100 via-scale-700 to-scale-100">
<div className="bg-scale-100 overflow-clip">
<SectionContainer className="!py-0 !pb-16 lg:!pt-16">
<div className="lg:flex lg:h-[500px]">
<div className="relative aspect-square -top-[110px] -left-[200px] w-[575px] sm:-top-[150px] sm:-left-[300px] sm:w-[850px] lg:-top-[225px] lg:-left-[330px] lg:w-[800px] lg:h-[800px] xl:-left-[200px] xl:-top-[210px] xl:w-[1000px]">
<Globe />
</div>
<div className="relative -top-[75px] lg:top-0 lg:-left-[325px] xl:-top-[45px] xl:-left-[150px] 2xl:-left-[50px] lg:min-w-[400px] lg:h-fit xl:mt-10">
<h2 className="text-2xl sm:text-3xl xl:text-4xl max-w-[300px] lg:max-w-xs tracking-[-1.5px]">
We work together, wherever we are
</h2>
<p className="text-scale-1100 mt-4 text-xs sm:text-sm lg:text-base md:w-5/6 lg:w-full">
Working in a globally distributed team is rewarding but has its challenges. We
are across many different timezones, so we use tools like Notion, Slack, and
Discord to stay connected to our team, and our community.
</p>
<div className="max-w-[300px] sm:max-w-md lg:max-w-md mt-20">
<div className="border-t-2 border-brand-900 w-4/12"></div>
<h2 className="text-2xl sm:text-3xl lg:text-4xl pt-2 tracking-[-1.5px]">
We deeply believe in the efficacy of collaborative open source
</h2>
</div>
</div>
</div>
</SectionContainer>
<SectionContainer className="-mt-16 md:mt-0">
<div className="md:flex md:gap-6">
<div className="md:w-1/2">
<div>
<h2 className="text-2xl sm:text-3xl xl:text-4xl tracking-[-1.5px]">
What is Supabase
</h2>
<p className="text-scale-1100 text-xs sm:text-sm lg:text-base pt-2 sm:max-w-md xl:max-w-lg">
Supabase is an open source Firebase alternative, built by developers for
developers. Supabase adds auth, realtime, storage, restful APIs, and edge
functions to Postgres without a single line of code. Supabase was
born-remote. Having a globally distributed, open source company is our
secret weapon to hiring top-tier talent.
</p>
</div>
<div className="md:w-full rounded-md mt-10 md:mt-36 lg:mt-40">
<div className="relative w-full aspect-[148/125]">
<Image
src="/images/career/1.jpg"
alt="team photo"
layout="fill"
objectFit="cover"
className="rounded-md"
/>
</div>
</div>
<div className="grid justify-items-end">
<div className="w-5/6 rounded-md mt-6">
<div className="relative w-full aspect-[29/22]">
<Image
src="/images/career/2.jpg"
alt="team photo"
layout="fill"
objectFit="cover"
className="rounded-md"
/>
</div>
</div>
</div>
</div>
<div className="mt-6 md:mt-0 md:w-1/2">
<div className="w-full rounded-md">
<div className="relative w-full aspect-[137/110]">
<Image
src="/images/career/3.jpg"
alt="team photo"
layout="fill"
objectFit="cover"
className="rounded-md"
/>
</div>
</div>
<div className="flex gap-6 mt-6">
<div className="w-full rounded-md">
<div className="relative w-full aspect-[142/189]">
<Image
src="/images/career/4.jpg"
alt="team photo"
layout="fill"
objectFit="cover"
className="rounded-md"
/>
</div>
</div>
<div className="w-full rounded-md">
<div className="relative w-full aspect-[142/189]">
<Image
src="/images/career/5.jpg"
alt="team photo"
layout="fill"
objectFit="cover"
className="rounded-md"
/>
</div>
</div>
</div>
<div className="w-5/6 rounded-md mt-6">
<div className="relative w-full aspect-[41/43]">
<Image
src="/images/career/6.jpg"
alt="team photo"
layout="fill"
objectFit="cover"
className="rounded-md"
/>
</div>
</div>
</div>
</div>
</SectionContainer>
<SectionContainer>
<h2 className="text-2xl sm:text-3xl xl:text-4xl tracking-[-1.5px]">
Human powered
</h2>
<p className="text-scale-1000 text-xs sm:text-sm lg:text-base pt-3 sm:w-3/5 lg:max-w-sm">
As a completely remote and asynchronous team, we focus on these five traits to
keep our team effective:
</p>
<div className="grid pt-10 gap-8 grid-cols-2 md:grid-cols-3 lg:gap-12 lg:grid-cols-5">
{career.humanPowered.map(
(human: { icon: string; title: string; text: string }, i: number) => {
return (
<div key={i} className="flex flex-col gap-3">
<div className="w-12 h-12 sm:w-10 sm:h-10 lg:w-12 lg:h-12 rounded-lg flex items-center">
<div className="relative w-full h-full mx-auto">
<Image
src={`/images/career/icons/${human.icon}${
isDarkMode ? '-dark' : '-light'
}.svg`}
className="w-12 h-12 sm:w-10 sm:h-10 lg:w-12 lg:h-12"
alt={`${human.icon} icon`}
layout="fill"
objectFit="fill"
/>
</div>
</div>
<div className="">
<h2 className="text-base">{human.title}</h2>
<p className="text-scale-1100 text-xs lg:text-sm">{human.text}</p>
</div>
</div>
)
}
)}
</div>
</SectionContainer>
<SectionContainer className="!pb-0">
<div className="w-14 h-14 rounded-lg flex items-center mx-auto mb-6">
<div className="relative w-full h-full mx-auto">
<Image
src={`/images/career/icons/open_source${isDarkMode ? '-dark' : '-light'}.svg`}
alt="open source icon"
layout="fill"
objectFit="cover"
/>
</div>
</div>
<div className="text-center">
<h2 className="text-2xl sm:text-3xl xl:text-4xl max-w-[300px] xl:max-w-none mx-auto tracking-[-1.5px]">
1,000 + Contributors building Supabase
</h2>
<p className="text-scale-1100 text-xs sm:text-sm lg:text-base sm:max-w-lg lg:max-w-2xl mx-auto pt-3">
We're building a community of communities, bringing together developers from
many different backgrounds, as well as new developers looking to get involved
with open source. We love celebrating everyone who contributes their time to the
Supabase mission.
</p>
</div>
<div className="w-[1080px] h-[370px] mx-auto sm:mt-10 md:mt-16 lg:mt-28 2xl:mt-60">
{contributors.map((contributor: any, i: number) => {
return (
<div
className={`${
Styles[`contributors-${i}`]
} absolute w-12 h-12 md:w-14 md:h-14 lg:w-16 lg:h-16 rounded-full border-[1.5px] border-scale-600 z-10
transition-all hover:scale-105 drop-shadow-sm hover:drop-shadow-md
`}
key={i}
>
<Link href={contributor.html_url}>
<a target="_blank">
<div className="relative w-full h-full">
<Image
src={contributor.avatar_url}
alt={`${contributor.login} github avatar`}
className="rounded-full"
layout="fill"
objectFit="cover"
/>
</div>
</a>
</Link>
</div>
)
})}
<div
className={`${Styles['contributors-bg-circle']} w-[100%] lg:w-[80%] left-[0%] lg:left-[10%] -bottom-[30%] xs:-bottom-[36%] sm:-bottom-[52%] md:-bottom-[64%] lg:-bottom-[80%] xl:-bottom-[100%]`}
>
<div className="flex flex-col justify-between h-full bg-scale-100 rounded-full p-4"></div>
</div>
<div
className={`${Styles['contributors-bg-circle']} w-[80%] lg:w-[60%] left-[10%] lg:left-[20%] -bottom-[25%] xs:-bottom-[30%] sm:-bottom-[44%] md:-bottom-[54%] lg:-bottom-[60%] xl:-bottom-[75%]`}
>
<div className="flex flex-col justify-between h-full bg-scale-100 rounded-full p-4"></div>
</div>
<div
className={`${Styles['contributors-bg-circle']} w-[60%] lg:w-[40%] left-[20%] lg:left-[30%] -bottom-[20%] xs:-bottom-[25%] sm:-bottom-[38%] md:-bottom-[44%] lg:-bottom-[40%] xl:-bottom-[50%]`}
>
<div className="flex flex-col justify-between h-full bg-scale-100 rounded-full p-4"></div>
</div>
<div
className={`${Styles['contributors-bg-circle']} w-[40%] lg:w-[20%] left-[30%] lg:left-[40%] -bottom-[15%] xs:-bottom-[19%] sm:-bottom-[30%] md:-bottom-[34%] lg:-bottom-[20%] xl:-bottom-[25%]`}
>
<div className="flex flex-col justify-between h-full bg-scale-100 rounded-full p-4"></div>
</div>
</div>
</SectionContainer>
</div>
</div>
<SectionContainer>
<div className="xl:flex lg:items-start xl:gap-10 justify-between">
<div className="xl:min-w-[300px] xl:max-w-[360px]">
<h2 className="text-2xl sm:text-3xl xl:text-4xl max-w-[280px] sm:max-w-xs xl:max-w-none tracking-[-1.5px]">
Great people deserve great benefits
</h2>
</div>
<div className="mt-12 xl:mt-0 space-y-6 lg:space-y-0 sm:w-fit sm:mx-auto lg:grid lg:grid-cols-2 lg:gap-6">
{career.benefits.map(
(benefits: { icon: string; title: string; text: string }, i: number) => {
return (
<div
className="h-full bg-white dark:bg-scale-300 drop-shadow-sm border-scale-400 border-[1px] p-6 rounded-lg flex items-start space-x-6 w-full"
key={i}
>
<div className="w-12 h-12 sm:w-10 sm:h-10 lg:w-12 lg:h-12 aspect-square rounded-lg flex items-center">
<div className="relative w-full h-full mx-auto">
<Image
src={`/images/career/icons/${benefits.icon}${
isDarkMode ? '-dark' : '-light'
}.svg`}
alt={`${benefits.icon} icon`}
layout="fill"
objectFit="cover"
/>
</div>
</div>
<div className="h-fit text-sm lg:text-base">
<h2 className="text-base">{benefits.title}</h2>
<ReactMarkdown className="prose pt-1 text-sm">
{benefits.text}
</ReactMarkdown>
</div>
</div>
)
}
)}
</div>
</div>
</SectionContainer>
<SectionContainer>
<div className="w-14 h-14 rounded-lg flex items-center mx-auto mb-6">
<div className="relative w-full h-full mx-auto">
<Image
src={`/images/career/icons/jobs${isDarkMode ? '-dark' : '-light'}.svg`}
alt="jobs icon"
layout="fill"
objectFit="cover"
/>
</div>
</div>
<div className="text-center sm:max-w-md md:w-3/4 lg:max-w-lg xl:max-w-2xl mx-auto">
<h2 className="text-2xl sm:text-3xl xl:text-4xl tracking-[-1.5px]">How we hire</h2>
<p className="text-xs sm:text-sm lg:text-base text-scale-1100 pt-3">
The entire process is fully remote and all communication happens over email or via
video chat in Google. Meet. The calls are all 1:1 and usually take between 20-45
minutes. We know you are interviewing us too, so please ask questions. We are happy
to answer.
</p>
</div>
<div className="mt-16 md:ml-36 lg:flex lg:items-start lg:w-fit lg:mx-auto">
{career.hiring.map((hiring: { title: string; text: string }, i: number) => {
return (
<div
key={i + 1}
className="flex lg:block items-start space-x-6 lg:space-x-0 lg:w-full"
>
<div className="lg:flex items-center">
<h3 className="bg-brand-600 border-[1px] border-brand-800 text-brand-900 text-md text-center w-[44px] px-2 py-1.5 rounded-md">
{i + 1}
</h3>
<div className="h-[100px] w-[1px] sm:h-[100px] mx-auto lg:h-[1px] lg:w-full bg-brand-800 lg:pr-6"></div>
</div>
<div className="lg:mt-6">
<h2 className="sm:text-lg max-w-[75%] xl:max-w-none xl:w-11/12 lg:max-w-none">
{hiring.title}
</h2>
<p className="text-scale-1100 text-xs sm:text-sm mt-1 md:w-3/4 lg:w-11/12">
{hiring.text}
</p>
</div>
</div>
)
})}
<h3 className="bg-brand-600 border-[1px] border-brand-800 text-brand-900 text-xl w-[44px] min-h-[40px] px-2 py-1 rounded-md grid justify-items-center items-center">
<IconCheck />
</h3>
</div>
</SectionContainer>
<div id="positions" className="positions">
<SectionContainer>
<h2 className="text-2xl sm:text-3xl xl:text-4xl tracking-[-1.5px]">Open positions</h2>
<div className="mt-10 space-y-4">
{jobs.map(
(
job: {
title: string
location: any
employment: string
description: string
absolute_url: string
},
i: number
) => {
return (
<div className="cursor-pointer md:cursor-default" key={i}>
<Link href={job.absolute_url}>
<div className="bg-white border-scale-300 drop-shadow-sm dark:bg-scale-300 border dark:border-scale-400 p-4 px-7 rounded-md sm:flex sm:items-center transition ease-out hover:bg-scale-100 dark:hover:bg-scale-400 hover:drop-shadow-md hover:cursor-pointer">
<h2 className="text-base min-w-[240px] lg:min-w-[316px] truncate mr-6">
{job.title}
</h2>
<div className="flex items-center justify-between justify-[normal] pt-2 sm:pt-0 sm:w-full">
<div className="flex items-center space-x-4">
<Badge className="rounded-full flex items-center lg:text-sm">
<div className="w-3 h-3">
<img
src="/images/career/icons/globe-dark.svg"
alt="globe icon"
width="100%"
height="100%"
/>
</div>
<span className="ml-1">{job.location.name}</span>
</Badge>
<span className="hidden md:block">{job.employment}</span>
</div>
<p className="hidden lg:block lg:text-sm">{job.description}</p>
<Button className="text-white">Apply for position</Button>
</div>
</div>
</Link>
</div>
)
}
)}
</div>
</SectionContainer>
</div>
</div>
</DefaultLayout>
</>
)
}
export default CareerPage
+93 -45
View File
@@ -1,20 +1,48 @@
import { NextSeo } from 'next-seo'
import _days from '~/components/LaunchWeek/days.json'
import { WeekDayProps } from '~/components/LaunchWeek/types'
import DefaultLayout from '~/components/Layouts/Default'
import SectionContainer from '~/components/Layouts/SectionContainer'
import _days from '~/components/LaunchWeek/days.json'
import HackathonSection from '~/components/LaunchWeek/HackathonSection'
import LaunchHero from '~/components/LaunchWeek/LaunchHero'
import { LaunchSection } from '~/components/LaunchWeek/LaunchSection'
import PreLaunchTeaser from '~/components/LaunchWeek/PreLaunchTeaser'
import ScheduleInfo from '~/components/LaunchWeek/ScheduleInfo'
import { WeekDayProps } from '~/components/LaunchWeek/types'
import { createClient, Session } from '@supabase/supabase-js'
import { useRouter } from 'next/router'
import { useEffect, useState } from 'react'
import TicketContainer from '~/components/LaunchWeek/Ticket/TicketContainer'
import { useTheme } from '~/components/Providers'
import classNames from 'classnames'
import styleUtils from '~/components/LaunchWeek/Ticket/utils.module.css'
import { SITE_ORIGIN } from '~/lib/constants'
const days = _days as WeekDayProps[]
export default function launchweek() {
const shippingHasStarted = days[0].shipped
const title = 'Launch Week 5'
const description = 'Supabase Launch Week 5 | 15-19 Aug 2022'
const { isDarkMode } = useTheme()
const title = 'Launch Week 6'
const description = 'Supabase Launch Week 6 | 12-18 Dec 2022'
const [supabase] = useState(() =>
createClient(process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!)
)
const [session, setSession] = useState<Session | null>(null)
const { query } = useRouter()
const ticketNumber = query.ticketNumber?.toString()
const defaultUserData = {
id: query.id?.toString(),
ticketNumber: ticketNumber ? parseInt(ticketNumber, 10) : undefined,
name: query.name?.toString(),
username: query.username?.toString(),
}
useEffect(() => {
supabase.auth.onAuthStateChange((event, session) => {
setSession(session)
})
}, [])
useEffect(() => {
document.body.className = isDarkMode ? 'dark bg-[#121212]' : 'light bg-[#fff]'
}, [isDarkMode])
return (
<>
@@ -26,48 +54,68 @@ export default function launchweek() {
url: `https://supabase.com/launch-week`,
images: [
{
url: `https://supabase.com/images/launchweek/og-image.jpg`,
url: `${SITE_ORIGIN}/images/launchweek/launch-week-6.jpg`,
},
],
}}
/>
<div className="launch-week-gradientBg"></div>
<DefaultLayout>
<SectionContainer className="flex flex-col gap-8 !pb-0 md:gap-16 lg:gap-16">
<img
src="/images/launchweek/launchweek-logo--light.svg"
className="md:40 w-28 dark:hidden lg:w-48"
/>
<img
src="/images/launchweek/launchweek-logo--dark.svg"
className="md:40 hidden w-28 dark:block lg:w-48"
/>
<LaunchHero />
{!shippingHasStarted && <PreLaunchTeaser />}
</SectionContainer>
<SectionContainer
className={[
'grid flex-col gap-24 lg:gap-16',
!shippingHasStarted && 'lg:grid-cols-2 ',
].join(' ')}
>
{!shippingHasStarted && <ScheduleInfo />}
<div>
{days.map((item: WeekDayProps, i) => {
return (
<LaunchSection
key={'launchweek-item ' + (item.title || i)}
{...item}
index={i}
shippingHasStarted={shippingHasStarted}
/>
)
})}
<SectionContainer className="flex flex-col !pb-24 items-center lg:pt-32 gap-32">
<div
className={classNames(
styleUtils.appear,
styleUtils['appear-first'],
'flex flex-col justify-center gap-3'
)}
>
<div className="flex justify-center">
<img
src="/images/launchweek/launchweek-logo--light.svg"
className="flex w-40 dark:hidden lg:w-80"
/>
<img
src="/images/launchweek/launchweek-logo--dark.svg"
className="hidden w-40 dark:flex lg:w-80"
/>
</div>
<p className="text-scale-1100 text-sm text-center">Dec 12 – 16 at 8 AM PT | 11 AM ET</p>
</div>
<div className={classNames(styleUtils.appear, styleUtils['appear-second'])}>
<TicketContainer
supabase={supabase}
session={session}
defaultUserData={defaultUserData}
defaultPageState={query.ticketNumber ? 'ticket' : 'registration'}
/>
</div>
</SectionContainer>
<SectionContainer>
<HackathonSection />
</SectionContainer>
<div
className={classNames(
styleUtils.appear,
styleUtils['appear-third'],
'gradient-container'
)}
>
<div
className={classNames(styleUtils.appear, styleUtils['appear-fourth'], 'gradient-mask')}
></div>
<div className="gradient-mask--masked bottom-of-the-circle"></div>
<div
className={classNames(
// styleUtils.appear,
// styleUtils['appear-second'],
'flair-mask-a the-stroke-of-the-circle'
)}
></div>
<div
className={classNames(
// styleUtils.appear,
// styleUtils['appear-second'],
'flair-mask-b inside-the-circle'
)}
></div>
</div>
</DefaultLayout>
</>
)
@@ -6,8 +6,9 @@ import SectionContainer from '~/components/Layouts/SectionContainer'
import TicketContainer from '~/components/LaunchWeek/Ticket/TicketContainer'
import { SITE_URL, SAMPLE_TICKET_NUMBER } from '~/lib/constants'
import { createClient } from '@supabase/supabase-js'
import { useState } from 'react'
import { useEffect, useState } from 'react'
import { FastGlobOptionsWithoutCwd } from 'globby'
import { useTheme } from '~/components/Providers'
type Props = {
username: string | null
@@ -22,6 +23,8 @@ const supabaseAdmin = createClient(
)
export default function TicketShare({ username, ticketNumber, name, golden }: Props) {
const { isDarkMode } = useTheme()
const [supabase] = useState(() =>
createClient(process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!)
)
@@ -35,6 +38,10 @@ export default function TicketShare({ username, ticketNumber, name, golden }: Pr
username ?? ''
)}`
useEffect(() => {
document.body.className = isDarkMode ? 'dark bg-[#121212]' : 'light bg-[#fff]'
}, [isDarkMode])
return (
<>
<NextSeo
@@ -50,16 +57,15 @@ export default function TicketShare({ username, ticketNumber, name, golden }: Pr
],
}}
/>
<div className="launch-week-gradientBg"></div>
<DefaultLayout>
<SectionContainer className="flex flex-col gap-8 !pb-0 md:gap-16 lg:gap-16">
<SectionContainer className="flex flex-col gap-8 pb-0 md:gap-16 lg:gap-16 items-center xl:items-start">
<img
src="/images/launchweek/launchweek-logo--light.svg"
className="md:40 w-28 dark:hidden lg:w-48"
className="md:40 w-40 dark:hidden lg:w-80"
/>
<img
src="/images/launchweek/launchweek-logo--dark.svg"
className="md:40 hidden w-28 dark:block lg:w-48"
className="md:40 hidden w-40 dark:block lg:w-80"
/>
<TicketContainer
supabase={supabase}
+20 -10
View File
@@ -6,8 +6,11 @@ import { SITE_URL } from '~/lib/constants'
import { useRouter } from 'next/router'
import { createClient, Session } from '@supabase/supabase-js'
import { useState, useEffect } from 'react'
import { useTheme } from '~/components/Providers'
export default function TicketHome() {
const { isDarkMode } = useTheme()
const [supabase] = useState(() =>
createClient(process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!)
)
@@ -28,6 +31,10 @@ export default function TicketHome() {
})
}, [])
useEffect(() => {
document.body.className = isDarkMode ? 'dark bg-[#121212]' : 'light bg-[#fff]'
}, [isDarkMode])
return (
<>
<NextSeo
@@ -43,17 +50,20 @@ export default function TicketHome() {
],
}}
/>
<div className="launch-week-gradientBg"></div>
<DefaultLayout>
<SectionContainer className="flex flex-col gap-8 !pb-0 md:gap-16 lg:gap-16">
<img
src="/images/launchweek/launchweek-logo--light.svg"
className="md:40 w-28 dark:hidden lg:w-48"
/>
<img
src="/images/launchweek/launchweek-logo--dark.svg"
className="md:40 hidden w-28 dark:block lg:w-48"
/>
<SectionContainer className="flex flex-col gap-16">
<div className="flex flex-col gap-3 items-center justify-center xl:justify-start">
<img
src="/images/launchweek/launchweek-logo--light.svg"
className="flex w-40 dark:hidden lg:w-80"
/>
<img
src="/images/launchweek/launchweek-logo--dark.svg"
className="hidden w-40 dark:flex lg:w-80"
/>
<p className="text-scale-1100 text-sm">Dec 12 – 16 at 8 AM PT | 11 AM ET</p>
</div>
<TicketContainer
supabase={supabase}
session={session}
Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 345 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 526 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 447 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 244 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 618 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 235 KiB

@@ -0,0 +1,12 @@
<svg width="49" height="49" viewBox="0 0 49 49" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M38.8733 7.13554L37.7064 7.1355L18.0235 7.1355C15.4562 7.1355 13.375 9.21668 13.375 11.784L13.375 12.0053V39.0138V41.8606H31.4791C34.0463 41.8606 36.1275 39.7794 36.1275 37.2122V13.6548L36.1081 11.2315" stroke="#299764" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M37.3191 7.13553L39.2058 7.1355M39.2058 7.1355C37.4971 7.1355 36.1118 8.52074 36.1118 10.2295L36.1192 11.1448L36.1265 12.06L36.1119 13.6126L42.3 13.6126V10.2295C42.3 8.5207 40.9146 7.1355 39.2058 7.1355Z" stroke="#299764" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M23.9648 14.5359L31.9847 14.5359" stroke="#92CEAC" stroke-width="2.01025" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M23.9648 17.5359L31.9847 17.5359" stroke="#92CEAC" stroke-width="2.01025" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M23.9648 20.5359L31.9847 20.5359" stroke="#92CEAC" stroke-width="2.01025" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M23.9648 23.5359L31.9847 23.5359" stroke="#92CEAC" stroke-width="2.01025" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M17.2461 26.5359H31.9846" stroke="#92CEAC" stroke-width="2.01025" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M21.7239 17.1679C21.7239 16.1613 20.9079 15.3453 19.9013 15.3453M19.9013 15.3453C18.8946 15.3453 18.0786 16.1613 18.0786 17.1679C18.0786 18.1746 18.8946 18.9906 19.9013 18.9906C20.9079 18.9906 21.7239 19.8066 21.7239 20.8132C21.7239 21.8198 20.9079 22.6359 19.9013 22.6359C18.8946 22.6359 18.0786 21.8198 18.0786 20.8132M19.9013 15.3453V13.5211M19.9013 22.6632V24.5415" stroke="#92CEAC" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M7.14746 37.2122C7.14746 39.7795 9.22865 41.8607 11.7959 41.8607H31.4789C28.9116 41.8607 26.8304 39.7795 26.8304 37.2122V32.1294H7.14746V37.2122Z" fill="#5BB98C"/>
<path d="M36.1273 37.2122C36.1273 39.7795 34.0461 41.8607 31.4789 41.8607M31.4789 41.8607C28.9116 41.8607 26.8304 39.7795 26.8304 37.2122V32.1294H7.14746V37.2122C7.14746 39.7795 9.22865 41.8607 11.7959 41.8607H31.4789Z" stroke="#299764" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

@@ -0,0 +1,8 @@
<svg width="49" height="49" viewBox="0 0 49 49" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19.0612 19.2371C20.5187 17.7712 22.432 17.0344 24.3478 17.0278C26.2662 17.0213 28.187 17.7471 29.6549 19.2066C32.5886 22.1235 32.6023 26.8664 29.6854 29.8002C26.7684 32.734 22.0255 32.7477 19.0917 29.8307C17.6971 28.4441 16.9623 26.6448 16.8886 24.8234" stroke="#92CEAC" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M15.4169 15.6135C17.8742 13.1419 21.1003 11.8996 24.3304 11.8886C27.5649 11.8776 30.8034 13.1013 33.2784 15.5621C38.2249 20.4802 38.248 28.4771 33.3299 33.4236C28.4117 38.3702 20.4148 38.3932 15.4683 33.4751C13.1168 31.1371 11.878 28.1034 11.7537 25.0323" stroke="#92CEAC" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M11.7843 12.0024C18.6972 5.04959 29.9376 5.01721 36.8904 11.9301C43.8432 18.843 43.8756 30.0834 36.9627 37.0363C30.0498 43.9891 18.8094 44.0215 11.8566 37.1086C8.46238 33.7339 6.71743 29.3279 6.6246 24.8928" stroke="#299764" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M3.49023 24.8623L25.645 24.8623" stroke="#299764" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<ellipse cx="31.1606" cy="17.4014" rx="3.09326" ry="3.09326" fill="#5BB98C" stroke="#299764" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<ellipse cx="12.2222" cy="37.24" rx="3.09326" ry="3.09326" fill="#5BB98C" stroke="#299764" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

@@ -0,0 +1,8 @@
<svg width="49" height="49" viewBox="0 0 49 49" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="0.615234" y="0.583008" width="48" height="48" rx="8" fill="#164430"/>
<circle cx="24.2974" cy="25.7347" r="4.87601" transform="rotate(30 24.2974 25.7347)" fill="#133929"/>
<circle cx="23.2931" cy="24.1459" r="2" transform="rotate(30 23.2931 24.1459)" fill="#4CC38A"/>
<path d="M15.8596 31.5868C16.2515 32.1909 16.7123 32.7633 17.2419 33.2929C21.3146 37.3656 27.9177 37.3656 31.9903 33.2929C36.063 29.2203 36.063 22.6171 31.9903 18.5445C27.9177 14.4718 21.3146 14.4718 17.2419 18.5445C15.7268 20.0596 14.7753 21.9249 14.3875 23.8799M14.3712 27.8737C14.281 27.3985 14.224 26.9183 14.2002 26.4367" stroke="#4CC38A" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M6.03418 25.9185L11.1938 20.7589C18.6062 13.3465 30.6241 13.3465 38.0365 20.7589L43.1961 25.9185L38.0365 31.078C30.6241 38.4904 18.6062 38.4905 11.1938 31.078L6.03418 25.9185Z" stroke="#4CC38A" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M6.03418 19.6809L9.05877 16.6563C17.6503 8.06478 31.5799 8.06478 40.1715 16.6563L43.1961 19.6809" stroke="#133929" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

@@ -0,0 +1,8 @@
<svg width="49" height="49" viewBox="0 0 49 49" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="0.615234" y="0.583008" width="48" height="48" rx="8" fill="#DDF3E4"/>
<circle cx="24.2974" cy="25.7347" r="4.87601" transform="rotate(30 24.2974 25.7347)" fill="#92CEAC"/>
<circle cx="23.2931" cy="24.1459" r="2" transform="rotate(30 23.2931 24.1459)" fill="#299764"/>
<path d="M15.8596 31.5868C16.2515 32.1909 16.7123 32.7633 17.2419 33.2929C21.3146 37.3656 27.9177 37.3656 31.9903 33.2929C36.063 29.2203 36.063 22.6171 31.9903 18.5445C27.9177 14.4718 21.3146 14.4718 17.2419 18.5445C15.7268 20.0596 14.7753 21.9249 14.3875 23.8799M14.3712 27.8737C14.281 27.3985 14.224 26.9183 14.2002 26.4367" stroke="#299764" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M6.03418 25.9185L11.1938 20.7589C18.6062 13.3465 30.6241 13.3465 38.0365 20.7589L43.1961 25.9185L38.0365 31.078C30.6241 38.4904 18.6062 38.4905 11.1938 31.078L6.03418 25.9185Z" stroke="#299764" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M6.03418 19.6809L9.05877 16.6563C17.6503 8.06478 31.5799 8.06478 40.1715 16.6563L43.1961 19.6809" stroke="#92CEAC" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

@@ -0,0 +1,8 @@
<svg width="49" height="49" viewBox="0 0 49 49" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="0.615234" y="0.583008" width="48" height="48" rx="8" fill="#164430"/>
<path d="M35.8613 30.3243V22.195C35.8613 19.563 30.8265 17.4294 24.6157 17.4294C18.4049 17.4294 13.3701 19.563 13.3701 22.195V30.3243" stroke="#4CC38A" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M35.8613 30.6747C35.8613 33.3067 30.8265 35.4403 24.6157 35.4403C18.4049 35.4403 13.3701 33.3067 13.3701 30.6747C13.3701 28.0428 21.1119 25.4777 24.6157 29.5282C28.0538 25.5538 35.8613 28.0428 35.8613 30.6747Z" fill="#1B543A" stroke="#4CC38A" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M45.1744 19.0121L24.6152 10.4058L4.05603 19.0121L13.1089 22.8017L13.3701 22.195C13.3701 19.563 18.4049 17.4295 24.6157 17.4295C30.8265 17.4295 35.8613 19.563 35.8613 22.195L36.1216 22.8017L45.1744 19.0121Z" stroke="#4CC38A" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M19.29 12.9666L19.29 35.0339" stroke="#4CC38A" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M20.7952 35.23H17.7847V40.5189H20.7952V35.23Z" stroke="#4CC38A" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

@@ -0,0 +1,8 @@
<svg width="49" height="49" viewBox="0 0 49 49" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="0.615234" y="0.583008" width="48" height="48" rx="8" fill="#DDF3E4"/>
<path d="M35.8613 30.3243V22.195C35.8613 19.563 30.8265 17.4294 24.6157 17.4294C18.4049 17.4294 13.3701 19.563 13.3701 22.195V30.3243" stroke="#299764" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M35.8613 30.6747C35.8613 33.3067 30.8265 35.4403 24.6157 35.4403C18.4049 35.4403 13.3701 33.3067 13.3701 30.6747C13.3701 28.0428 21.1119 25.4777 24.6157 29.5282C28.0538 25.5538 35.8613 28.0428 35.8613 30.6747Z" fill="#5BB98C" stroke="#299764" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M45.1744 19.0121L24.6152 10.4058L4.05603 19.0121L13.1089 22.8017L13.3701 22.195C13.3701 19.563 18.4049 17.4295 24.6157 17.4295C30.8265 17.4295 35.8613 19.563 35.8613 22.195L36.1216 22.8017L45.1744 19.0121Z" stroke="#299764" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M19.29 12.9666L19.29 35.0339" stroke="#299764" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M20.7952 35.23H17.7847V40.5189H20.7952V35.23Z" stroke="#299764" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

@@ -0,0 +1,23 @@
<svg width="49" height="49" viewBox="0 0 49 49" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="0.615234" y="0.583008" width="48" height="48" rx="8" fill="#164430"/>
<path d="M6.76514 23.174L10.3016 19.6375H17.079" stroke="#133929" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M19.3164 16.9529V11.1276L22.9104 7.53369" stroke="#133929" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M30.2568 10.5677L30.2568 12.6254L25.4849 17.3972" stroke="#133929" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M8.79021 28.7061H6.77979M8.79021 28.7061H6.77979" stroke="#133929" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M23.7925 31.3567L18.4341 36.7147L18.4341 42.3801" stroke="#133929" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M41.8525 29.0264L37.0755 29.0264L34.3086 26.2594" stroke="#133929" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M35.9183 12.9841L28.5356 20.3675" stroke="#133929" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M12.6284 12.696L17.9612 18.0288" stroke="#133929" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M20.1896 28.7061L12.2441 36.6511" stroke="#133929" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M36.3105 36.5022L31.165 31.3567" stroke="#133929" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M28.853 33.7903L28.853 38.7512" stroke="#133929" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M28.853 40.2567L28.853 42.2671M28.853 40.2567L28.853 42.2671" stroke="#133929" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M39.9998 21.5571L42.0103 21.5571M39.9998 21.5571L42.0103 21.5571" stroke="#133929" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M30.2568 7.11213L30.2568 9.12256M30.2568 7.11213L30.2568 9.12256" stroke="#133929" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<ellipse cx="10.3704" cy="38.7512" rx="2.95974" ry="2.95974" transform="rotate(135 10.3704 38.7512)" stroke="#133929" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<ellipse cx="38.4945" cy="10.6272" rx="2.95974" ry="2.95974" transform="rotate(135 38.4945 10.6272)" stroke="#133929" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<ellipse cx="38.4945" cy="39.0022" rx="2.95974" ry="2.95974" transform="rotate(135 38.4945 39.0022)" stroke="#133929" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<ellipse cx="10.3704" cy="10.6025" rx="2.95974" ry="2.95974" transform="rotate(135 10.3704 10.6025)" stroke="#133929" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M25.1287 20.3348V12.3914C25.1287 11.4561 23.9594 11.0327 23.3606 11.7512L11.3713 26.1364C10.577 27.0896 11.2547 28.5367 12.4955 28.5367H24.1023L24.1023 36.4984C24.1023 37.4316 25.2672 37.8562 25.8677 37.1419L37.9754 22.7399C38.7757 21.7879 38.099 20.3348 36.8553 20.3348L25.1287 20.3348Z" fill="#1B543A"/>
<path d="M25.1287 20.3348H24.1236L25.1287 21.3399V20.3348ZM23.3606 11.7512L24.1327 12.3947V12.3947L23.3606 11.7512ZM11.3713 26.1364L12.1435 26.78L12.1435 26.78L11.3713 26.1364ZM24.1023 28.5367H25.1074L24.1023 27.5316V28.5367ZM24.1023 36.4984H23.0971H24.1023ZM25.8677 37.1419L26.6371 37.7887L25.8677 37.1419ZM37.9754 22.7399L38.7448 23.3867L37.9754 22.7399ZM36.8553 20.3348V21.3399V20.3348ZM26.1339 20.3348V12.3914H24.1236V20.3348H26.1339ZM26.1339 12.3914C26.1339 10.5161 23.7891 9.66706 22.5884 11.1077L24.1327 12.3947C24.1365 12.3901 24.1389 12.3879 24.1395 12.3874C24.1402 12.3868 24.1397 12.3873 24.1381 12.3883C24.1344 12.3906 24.1296 12.3926 24.125 12.3937C24.1208 12.3947 24.119 12.3945 24.1199 12.3945C24.1209 12.3946 24.1236 12.395 24.127 12.3962C24.1304 12.3975 24.1327 12.3989 24.1336 12.3995C24.1343 12.4 24.1327 12.399 24.1301 12.3956C24.1273 12.3918 24.1249 12.3872 24.1235 12.383C24.1229 12.3812 24.1229 12.3805 24.123 12.3814C24.1232 12.3822 24.1236 12.3854 24.1236 12.3914H26.1339ZM22.5884 11.1077L10.5992 25.4929L12.1435 26.78L24.1327 12.3947L22.5884 11.1077ZM10.5992 25.4929C9.25923 27.1007 10.4025 29.5418 12.4955 29.5418V27.5316C12.1069 27.5316 11.8947 27.0784 12.1435 26.78L10.5992 25.4929ZM12.4955 29.5418H24.1023V27.5316H12.4955V29.5418ZM23.0971 28.5367L23.0971 36.4984H25.1074L25.1074 28.5367H23.0971ZM23.0971 36.4984C23.0971 38.3695 25.433 39.2209 26.6371 37.7887L25.0983 36.4951C25.0945 36.4996 25.0921 36.5018 25.0915 36.5023C25.0909 36.5029 25.0913 36.5024 25.0929 36.5014C25.0966 36.4991 25.1014 36.4972 25.106 36.4961C25.1101 36.4951 25.1119 36.4953 25.1111 36.4953C25.11 36.4952 25.1074 36.4948 25.104 36.4936C25.1006 36.4924 25.0984 36.4909 25.0975 36.4903C25.0968 36.4898 25.0984 36.4908 25.1009 36.4942C25.1037 36.498 25.1061 36.5026 25.1074 36.5067C25.108 36.5085 25.1081 36.5092 25.108 36.5083C25.1078 36.5075 25.1074 36.5043 25.1074 36.4984H23.0971ZM26.6371 37.7887L38.7448 23.3867L37.2061 22.0931L25.0983 36.4951L26.6371 37.7887ZM38.7448 23.3867C40.0948 21.7808 38.9532 19.3297 36.8553 19.3297V21.3399C37.2447 21.3399 37.4567 21.7949 37.2061 22.0931L38.7448 23.3867ZM36.8553 19.3297L25.1287 19.3297V21.3399L36.8553 21.3399V19.3297Z" fill="#4CC38A"/>
</svg>

After

Width:  |  Height:  |  Size: 5.3 KiB

@@ -0,0 +1,23 @@
<svg width="49" height="49" viewBox="0 0 49 49" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="0.615234" y="0.583008" width="48" height="48" rx="8" fill="#DDF3E4"/>
<path d="M6.76514 23.174L10.3016 19.6375H17.079" stroke="#92CEAC" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M19.3164 16.9529V11.1276L22.9104 7.53369" stroke="#92CEAC" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M30.2568 10.5677L30.2568 12.6254L25.4849 17.3972" stroke="#92CEAC" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M8.79021 28.7061H6.77979M8.79021 28.7061H6.77979" stroke="#92CEAC" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M23.7925 31.3567L18.4341 36.7147L18.4341 42.3801" stroke="#92CEAC" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M41.8525 29.0264L37.0755 29.0264L34.3086 26.2594" stroke="#92CEAC" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M35.9183 12.9841L28.5356 20.3675" stroke="#92CEAC" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M12.6284 12.696L17.9612 18.0288" stroke="#92CEAC" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M20.1896 28.7061L12.2441 36.6511" stroke="#92CEAC" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M36.3105 36.5022L31.165 31.3567" stroke="#92CEAC" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M28.853 33.7903L28.853 38.7512" stroke="#92CEAC" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M28.853 40.2567L28.853 42.2671M28.853 40.2567L28.853 42.2671" stroke="#92CEAC" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M39.9998 21.5571L42.0103 21.5571M39.9998 21.5571L42.0103 21.5571" stroke="#92CEAC" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M30.2568 7.11213L30.2568 9.12256M30.2568 7.11213L30.2568 9.12256" stroke="#92CEAC" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<ellipse cx="10.3704" cy="38.7512" rx="2.95974" ry="2.95974" transform="rotate(135 10.3704 38.7512)" stroke="#92CEAC" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<ellipse cx="38.4945" cy="10.6272" rx="2.95974" ry="2.95974" transform="rotate(135 38.4945 10.6272)" stroke="#92CEAC" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<ellipse cx="38.4945" cy="39.0022" rx="2.95974" ry="2.95974" transform="rotate(135 38.4945 39.0022)" stroke="#92CEAC" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<ellipse cx="10.3704" cy="10.6025" rx="2.95974" ry="2.95974" transform="rotate(135 10.3704 10.6025)" stroke="#92CEAC" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M25.1287 20.3348V12.3914C25.1287 11.4561 23.9594 11.0327 23.3606 11.7512L11.3713 26.1364C10.577 27.0896 11.2547 28.5367 12.4955 28.5367H24.1023L24.1023 36.4984C24.1023 37.4316 25.2672 37.8562 25.8677 37.1419L37.9754 22.7399C38.7757 21.7879 38.099 20.3348 36.8553 20.3348L25.1287 20.3348Z" fill="#5BB98C"/>
<path d="M25.1287 20.3348H24.1236L25.1287 21.3399V20.3348ZM23.3606 11.7512L24.1327 12.3947V12.3947L23.3606 11.7512ZM11.3713 26.1364L12.1435 26.78L12.1435 26.78L11.3713 26.1364ZM24.1023 28.5367H25.1074L24.1023 27.5316V28.5367ZM24.1023 36.4984H23.0971H24.1023ZM25.8677 37.1419L26.6371 37.7887L25.8677 37.1419ZM37.9754 22.7399L38.7448 23.3867L37.9754 22.7399ZM36.8553 20.3348V21.3399V20.3348ZM26.1339 20.3348V12.3914H24.1236V20.3348H26.1339ZM26.1339 12.3914C26.1339 10.5161 23.7891 9.66706 22.5884 11.1077L24.1327 12.3947C24.1365 12.3901 24.1389 12.3879 24.1395 12.3874C24.1402 12.3868 24.1397 12.3873 24.1381 12.3883C24.1344 12.3906 24.1296 12.3926 24.125 12.3937C24.1208 12.3947 24.119 12.3945 24.1199 12.3945C24.1209 12.3946 24.1236 12.395 24.127 12.3962C24.1304 12.3975 24.1327 12.3989 24.1336 12.3995C24.1343 12.4 24.1327 12.399 24.1301 12.3956C24.1273 12.3918 24.1249 12.3872 24.1235 12.383C24.1229 12.3812 24.1229 12.3805 24.123 12.3814C24.1232 12.3822 24.1236 12.3854 24.1236 12.3914H26.1339ZM22.5884 11.1077L10.5992 25.4929L12.1435 26.78L24.1327 12.3947L22.5884 11.1077ZM10.5992 25.4929C9.25923 27.1007 10.4025 29.5418 12.4955 29.5418V27.5316C12.1069 27.5316 11.8947 27.0784 12.1435 26.78L10.5992 25.4929ZM12.4955 29.5418H24.1023V27.5316H12.4955V29.5418ZM23.0971 28.5367L23.0971 36.4984H25.1074L25.1074 28.5367H23.0971ZM23.0971 36.4984C23.0971 38.3695 25.433 39.2209 26.6371 37.7887L25.0983 36.4951C25.0945 36.4996 25.0921 36.5018 25.0915 36.5023C25.0909 36.5029 25.0913 36.5024 25.0929 36.5014C25.0966 36.4991 25.1014 36.4972 25.106 36.4961C25.1101 36.4951 25.1119 36.4953 25.1111 36.4953C25.11 36.4952 25.1074 36.4948 25.104 36.4936C25.1006 36.4924 25.0984 36.4909 25.0975 36.4903C25.0968 36.4898 25.0984 36.4908 25.1009 36.4942C25.1037 36.498 25.1061 36.5026 25.1074 36.5067C25.108 36.5085 25.1081 36.5092 25.108 36.5083C25.1078 36.5075 25.1074 36.5043 25.1074 36.4984H23.0971ZM26.6371 37.7887L38.7448 23.3867L37.2061 22.0931L25.0983 36.4951L26.6371 37.7887ZM38.7448 23.3867C40.0948 21.7808 38.9532 19.3297 36.8553 19.3297V21.3399C37.2447 21.3399 37.4567 21.7949 37.2061 22.0931L38.7448 23.3867ZM36.8553 19.3297L25.1287 19.3297V21.3399L36.8553 21.3399V19.3297Z" fill="#299764"/>
</svg>

After

Width:  |  Height:  |  Size: 5.3 KiB

@@ -0,0 +1,18 @@
<svg width="49" height="49" viewBox="0 0 49 49" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="0.615234" y="0.583008" width="48" height="48" rx="8" fill="#164430"/>
<path d="M16.0391 9.83691L18.3369 13.8168" stroke="#133929" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M31.1187 35.9563L33.4165 39.9362" stroke="#133929" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M39.8047 16.2019L35.8248 18.4997" stroke="#133929" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M13.6577 31.2781L9.67781 33.5759" stroke="#133929" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M9.48242 16.1675L13.4623 18.4653" stroke="#133929" stroke-width="2.01025" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M35.6021 31.2473L39.582 33.5451" stroke="#133929" stroke-width="2.01025" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M33.2212 9.80664L30.9234 13.7865" stroke="#133929" stroke-width="2.01025" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M18.1411 35.9265L15.8433 39.9064" stroke="#133929" stroke-width="2.01025" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<ellipse cx="24.7217" cy="24.6892" rx="17.5771" ry="17.5772" stroke="#4CC38A" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M24.7246 7.51074V12.1063" stroke="#4CC38A" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M24.7246 37.6707V42.2663" stroke="#4CC38A" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M42.1025 24.8887L37.5069 24.8887" stroke="#4CC38A" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M11.9419 24.8887L7.34629 24.8887" stroke="#4CC38A" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M24.6636 16.2019V22.4829M34.0286 34.2248L26.1525 26.3487" stroke="#4CC38A" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<ellipse rx="3.02133" ry="3.02133" transform="matrix(-1 0 0 1 24.7218 24.6893)" fill="#1B543A" stroke="#4CC38A" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

@@ -0,0 +1,18 @@
<svg width="49" height="49" viewBox="0 0 49 49" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="0.615234" y="0.583008" width="48" height="48" rx="8" fill="#DDF3E4"/>
<path d="M16.0391 9.83691L18.3369 13.8168" stroke="#92CEAC" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M31.1187 35.9563L33.4165 39.9362" stroke="#92CEAC" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M39.8047 16.2019L35.8248 18.4997" stroke="#92CEAC" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M13.6577 31.2781L9.67781 33.5759" stroke="#92CEAC" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M9.48242 16.1675L13.4623 18.4653" stroke="#92CEAC" stroke-width="2.01025" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M35.6021 31.2473L39.582 33.5451" stroke="#92CEAC" stroke-width="2.01025" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M33.2212 9.80664L30.9234 13.7865" stroke="#92CEAC" stroke-width="2.01025" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M18.1411 35.9265L15.8433 39.9064" stroke="#92CEAC" stroke-width="2.01025" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<ellipse cx="24.7217" cy="24.6892" rx="17.5771" ry="17.5772" stroke="#299764" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M24.7246 7.51074V12.1063" stroke="#299764" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M24.7246 37.6707V42.2663" stroke="#299764" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M42.1025 24.8887L37.5069 24.8887" stroke="#299764" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M11.9419 24.8887L7.34629 24.8887" stroke="#299764" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M24.6636 16.2019V22.4829M34.0286 34.2248L26.1525 26.3487" stroke="#299764" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<ellipse rx="3.02133" ry="3.02133" transform="matrix(-1 0 0 1 24.7218 24.6893)" fill="#5BB98C" stroke="#299764" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

@@ -0,0 +1,3 @@
<svg width="11" height="10" viewBox="0 0 11 10" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1.52136 4.5H2.4939C3.04618 4.5 3.4939 4.94772 3.4939 5.5V6C3.4939 6.55228 3.94161 7 4.4939 7C5.04618 7 5.4939 7.44772 5.4939 8V9.47254M3.9939 0.96776V1.75C3.9939 2.44036 4.55354 3 5.2439 3H5.4939C6.04618 3 6.4939 3.44772 6.4939 4C6.4939 4.55228 6.94161 5 7.4939 5C8.04618 5 8.4939 4.55228 8.4939 4C8.4939 3.44772 8.94161 3 9.4939 3L10.0261 3M7.4939 9.24394V8C7.4939 7.44772 7.94161 7 8.4939 7H10.0261M10.4939 5C10.4939 7.48528 8.47918 9.5 5.9939 9.5C3.50862 9.5 1.4939 7.48528 1.4939 5C1.4939 2.51472 3.50862 0.5 5.9939 0.5C8.47918 0.5 10.4939 2.51472 10.4939 5Z" stroke="#3ECF8E" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 743 B

@@ -0,0 +1,10 @@
<svg width="49" height="49" viewBox="0 0 49 49" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="0.615234" y="0.583008" width="48" height="48" rx="8" fill="#164430"/>
<mask id="path-2-outside-1_1368_10900" maskUnits="userSpaceOnUse" x="11.7686" y="11.7014" width="26" height="26" fill="black">
<rect fill="white" x="11.7686" y="11.7014" width="26" height="26"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M26.8109 22.3534V13.7014H22.4209V22.3534H13.7686V26.7434H22.4209V35.3951H26.8109V26.7434H35.4623V22.3534H26.8109Z"/>
</mask>
<path fill-rule="evenodd" clip-rule="evenodd" d="M26.8109 22.3534V13.7014H22.4209V22.3534H13.7686V26.7434H22.4209V35.3951H26.8109V26.7434H35.4623V22.3534H26.8109Z" fill="#1B543A"/>
<path d="M26.8109 13.7014H28.8109L26.8109 11.7014V13.7014ZM26.8109 22.3534H24.8109L26.8109 24.3534V22.3534ZM22.4209 13.7014V11.7014L20.4209 13.7014H22.4209ZM22.4209 22.3534V24.3534L24.4209 22.3534H22.4209ZM13.7686 22.3534V20.3534L11.7686 22.3534H13.7686ZM13.7686 26.7434H11.7686L13.7686 28.7434V26.7434ZM22.4209 26.7434H24.4209L22.4209 24.7434V26.7434ZM22.4209 35.3951H20.4209L22.4209 37.3951V35.3951ZM26.8109 35.3951V37.3951L28.8109 35.3951H26.8109ZM26.8109 26.7434V24.7434L24.8109 26.7434H26.8109ZM35.4623 26.7434V28.7434L37.4623 26.7434H35.4623ZM35.4623 22.3534H37.4623L35.4623 20.3534V22.3534ZM24.8109 13.7014V22.3534H28.8109V13.7014H24.8109ZM22.4209 15.7014H26.8109V11.7014H22.4209V15.7014ZM24.4209 22.3534V13.7014H20.4209V22.3534H24.4209ZM13.7686 24.3534H22.4209V20.3534H13.7686V24.3534ZM15.7686 26.7434V22.3534H11.7686V26.7434H15.7686ZM22.4209 24.7434H13.7686V28.7434H22.4209V24.7434ZM24.4209 35.3951V26.7434H20.4209V35.3951H24.4209ZM26.8109 33.3951H22.4209V37.3951H26.8109V33.3951ZM24.8109 26.7434V35.3951H28.8109V26.7434H24.8109ZM35.4623 24.7434H26.8109V28.7434H35.4623V24.7434ZM33.4623 22.3534V26.7434H37.4623V22.3534H33.4623ZM26.8109 24.3534H35.4623V20.3534H26.8109V24.3534Z" fill="#4CC38A" mask="url(#path-2-outside-1_1368_10900)"/>
<ellipse cx="24.615" cy="24.5486" rx="17.6853" ry="17.6853" stroke="#4CC38A" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

@@ -0,0 +1,10 @@
<svg width="49" height="49" viewBox="0 0 49 49" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="0.615234" y="0.583008" width="48" height="48" rx="8" fill="#DDF3E4"/>
<mask id="path-2-outside-1_1368_10900" maskUnits="userSpaceOnUse" x="11.7686" y="11.7014" width="26" height="26" fill="black">
<rect fill="white" x="11.7686" y="11.7014" width="26" height="26"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M26.8109 22.3534V13.7014H22.4209V22.3534H13.7686V26.7434H22.4209V35.3951H26.8109V26.7434H35.4623V22.3534H26.8109Z"/>
</mask>
<path fill-rule="evenodd" clip-rule="evenodd" d="M26.8109 22.3534V13.7014H22.4209V22.3534H13.7686V26.7434H22.4209V35.3951H26.8109V26.7434H35.4623V22.3534H26.8109Z" fill="#5BB98C"/>
<path d="M26.8109 13.7014H28.8109L26.8109 11.7014V13.7014ZM26.8109 22.3534H24.8109L26.8109 24.3534V22.3534ZM22.4209 13.7014V11.7014L20.4209 13.7014H22.4209ZM22.4209 22.3534V24.3534L24.4209 22.3534H22.4209ZM13.7686 22.3534V20.3534L11.7686 22.3534H13.7686ZM13.7686 26.7434H11.7686L13.7686 28.7434V26.7434ZM22.4209 26.7434H24.4209L22.4209 24.7434V26.7434ZM22.4209 35.3951H20.4209L22.4209 37.3951V35.3951ZM26.8109 35.3951V37.3951L28.8109 35.3951H26.8109ZM26.8109 26.7434V24.7434L24.8109 26.7434H26.8109ZM35.4623 26.7434V28.7434L37.4623 26.7434H35.4623ZM35.4623 22.3534H37.4623L35.4623 20.3534V22.3534ZM24.8109 13.7014V22.3534H28.8109V13.7014H24.8109ZM22.4209 15.7014H26.8109V11.7014H22.4209V15.7014ZM24.4209 22.3534V13.7014H20.4209V22.3534H24.4209ZM13.7686 24.3534H22.4209V20.3534H13.7686V24.3534ZM15.7686 26.7434V22.3534H11.7686V26.7434H15.7686ZM22.4209 24.7434H13.7686V28.7434H22.4209V24.7434ZM24.4209 35.3951V26.7434H20.4209V35.3951H24.4209ZM26.8109 33.3951H22.4209V37.3951H26.8109V33.3951ZM24.8109 26.7434V35.3951H28.8109V26.7434H24.8109ZM35.4623 24.7434H26.8109V28.7434H35.4623V24.7434ZM33.4623 22.3534V26.7434H37.4623V22.3534H33.4623ZM26.8109 24.3534H35.4623V20.3534H26.8109V24.3534Z" fill="#299764" mask="url(#path-2-outside-1_1368_10900)"/>
<ellipse cx="24.615" cy="24.5486" rx="17.6853" ry="17.6853" stroke="#299764" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

@@ -0,0 +1,8 @@
<svg width="49" height="48" viewBox="0 0 49 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="0.828125" width="48" height="48" rx="8" fill="#164430"/>
<path d="M25.029 21.5028L31.4845 16.7603C33.0729 15.5934 34.0112 13.74 34.0113 11.769C34.0113 9.29518 32.0059 7.28978 29.5321 7.28978C27.0583 7.28978 25.0289 9.29519 25.0289 11.769C25.0289 9.29519 23.0235 7.28978 20.5497 7.28978C18.0759 7.28978 16.0705 9.29519 16.0705 11.769C16.0704 13.7403 17.0075 15.5944 18.5948 16.7635L25.029 21.5028Z" fill="#1B543A" stroke="#4CC38A" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M13.84 42.3721V35.4475L6.85449 30.6533L6.85449 16.3203C6.85449 15.2094 7.75504 14.3088 8.86591 14.3088V14.3088C9.97679 14.3088 10.8773 15.2094 10.8773 16.3203V26.2064L15.8137 29.4578M20.7043 42.3721V31.7677C20.7043 29.0313 19.3057 26.4848 16.9965 25.0166L10.8773 21.1262" stroke="#4CC38A" stroke-width="2" stroke-linejoin="bevel"/>
<path d="M36.2191 42.3721V35.4475L43.2046 30.6533L43.2046 16.3203C43.2046 15.2094 42.304 14.3088 41.1932 14.3088V14.3088C40.0823 14.3088 39.1817 15.2094 39.1817 16.3203V26.2064L34.2454 29.4578M29.3548 42.3721V31.7677C29.3548 29.0313 30.7534 26.4848 33.0626 25.0166L39.1817 21.1262" stroke="#4CC38A" stroke-width="2" stroke-linejoin="bevel"/>
<path d="M11.2598 42.186H22.9141" stroke="#4CC38A" stroke-width="2" stroke-linejoin="bevel"/>
<path d="M27.0825 42.186H38.7368" stroke="#4CC38A" stroke-width="2" stroke-linejoin="bevel"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1,8 @@
<svg width="49" height="48" viewBox="0 0 49 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="0.828125" width="48" height="48" rx="8" fill="#DDF3E4"/>
<path d="M25.029 21.5028L31.4845 16.7603C33.0729 15.5934 34.0112 13.74 34.0113 11.769C34.0113 9.29518 32.0059 7.28978 29.5321 7.28978C27.0583 7.28978 25.0289 9.29519 25.0289 11.769C25.0289 9.29519 23.0235 7.28978 20.5497 7.28978C18.0759 7.28978 16.0705 9.29519 16.0705 11.769C16.0704 13.7403 17.0075 15.5944 18.5948 16.7635L25.029 21.5028Z" fill="#5BB98C" stroke="#299764" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M13.84 42.3721V35.4475L6.85449 30.6533L6.85449 16.3203C6.85449 15.2094 7.75504 14.3088 8.86591 14.3088V14.3088C9.97679 14.3088 10.8773 15.2094 10.8773 16.3203V26.2064L15.8137 29.4578M20.7043 42.3721V31.7677C20.7043 29.0313 19.3057 26.4848 16.9965 25.0166L10.8773 21.1262" stroke="#299764" stroke-width="2" stroke-linejoin="bevel"/>
<path d="M36.2191 42.3721V35.4475L43.2046 30.6533L43.2046 16.3203C43.2046 15.2094 42.304 14.3088 41.1932 14.3088V14.3088C40.0823 14.3088 39.1817 15.2094 39.1817 16.3203V26.2064L34.2454 29.4578M29.3548 42.3721V31.7677C29.3548 29.0313 30.7534 26.4848 33.0626 25.0166L39.1817 21.1262" stroke="#299764" stroke-width="2" stroke-linejoin="bevel"/>
<path d="M11.2598 42.186H22.9141" stroke="#299764" stroke-width="2" stroke-linejoin="bevel"/>
<path d="M27.0825 42.186H38.7368" stroke="#299764" stroke-width="2" stroke-linejoin="bevel"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1,22 @@
<svg width="49" height="49" viewBox="0 0 49 49" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="0.615234" y="0.583008" width="48" height="48" rx="8" fill="#164430"/>
<path d="M38.0239 11.6172L33.7549 15.8862" stroke="#133929" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M15.7236 33.5352L11.4546 37.8042" stroke="#133929" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M37.833 37.9954L33.564 33.7263" stroke="#133929" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M15.915 15.6951L11.646 11.426" stroke="#133929" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<ellipse cx="9.5708" cy="39.6274" rx="2.5" ry="2.5" stroke="#133929" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<ellipse cx="9.5708" cy="9.55298" rx="2.5" ry="2.5" stroke="#133929" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<ellipse cx="6.12842" cy="24.7092" rx="2.5" ry="2.5" stroke="#133929" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<ellipse cx="43.3447" cy="24.7092" rx="2.5" ry="2.5" stroke="#133929" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<ellipse cx="24.7363" cy="6.10107" rx="2.5" ry="2.5" transform="rotate(90 24.7363 6.10107)" stroke="#133929" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<ellipse cx="24.7363" cy="43.3174" rx="2.5" ry="2.5" transform="rotate(90 24.7363 43.3174)" stroke="#133929" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<ellipse cx="39.6791" cy="39.7464" rx="2.5" ry="2.5" transform="rotate(-75 39.6791 39.7464)" stroke="#133929" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<ellipse cx="39.6791" cy="9.67217" rx="2.5" ry="2.5" transform="rotate(-75 39.6791 9.67217)" stroke="#133929" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<ellipse cx="24.7002" cy="23.1504" rx="5" ry="5" fill="#1B543A" stroke="#4CC38A" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M17.2734 33.9437V33.9437C17.2734 30.7441 19.8672 28.1504 23.0667 28.1504H26.1647C29.3642 28.1504 31.958 30.7441 31.958 33.9437V33.9437" stroke="#4CC38A" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<ellipse cx="24.7002" cy="24.6675" rx="12" ry="12" stroke="#4CC38A" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M8.62195 12.1765L6.72316 21.6558M9.50586 36.7533L6.86365 27.3889" stroke="#133929" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M40.0583 12.4807L42.5073 22.3351M40.0583 36.9554L42.5073 27.1" stroke="#133929" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M36.7715 9.1936L27.3297 6.89027M12.4145 8.77427L22.0908 6.73403" stroke="#133929" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M36.6646 40.457L27.0219 42.4319M12.3681 40.457L22.0765 42.4319" stroke="#133929" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
</svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

@@ -0,0 +1,22 @@
<svg width="49" height="49" viewBox="0 0 49 49" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="0.615234" y="0.583008" width="48" height="48" rx="8" fill="#DDF3E4"/>
<path d="M38.0239 11.6172L33.7549 15.8862" stroke="#92CEAC" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M15.7236 33.5352L11.4546 37.8042" stroke="#92CEAC" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M37.833 37.9954L33.564 33.7263" stroke="#92CEAC" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M15.915 15.6951L11.646 11.426" stroke="#92CEAC" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<ellipse cx="9.5708" cy="39.6274" rx="2.5" ry="2.5" stroke="#92CEAC" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<ellipse cx="9.5708" cy="9.55298" rx="2.5" ry="2.5" stroke="#92CEAC" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<ellipse cx="6.12842" cy="24.7092" rx="2.5" ry="2.5" stroke="#92CEAC" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<ellipse cx="43.3447" cy="24.7092" rx="2.5" ry="2.5" stroke="#92CEAC" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<ellipse cx="24.7363" cy="6.10107" rx="2.5" ry="2.5" transform="rotate(90 24.7363 6.10107)" stroke="#92CEAC" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<ellipse cx="24.7363" cy="43.3174" rx="2.5" ry="2.5" transform="rotate(90 24.7363 43.3174)" stroke="#92CEAC" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<ellipse cx="39.6791" cy="39.7464" rx="2.5" ry="2.5" transform="rotate(-75 39.6791 39.7464)" stroke="#92CEAC" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<ellipse cx="39.6791" cy="9.67217" rx="2.5" ry="2.5" transform="rotate(-75 39.6791 9.67217)" stroke="#92CEAC" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<ellipse cx="24.7002" cy="23.1504" rx="5" ry="5" fill="#5BB98C" stroke="#299764" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M17.2734 33.9437V33.9437C17.2734 30.7441 19.8672 28.1504 23.0667 28.1504H26.1647C29.3642 28.1504 31.958 30.7441 31.958 33.9437V33.9437" stroke="#299764" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<ellipse cx="24.7002" cy="24.6675" rx="12" ry="12" stroke="#299764" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M8.62195 12.1765L6.72316 21.6558M9.50586 36.7533L6.86365 27.3889" stroke="#92CEAC" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M40.0583 12.4807L42.5073 22.3351M40.0583 36.9554L42.5073 27.1" stroke="#92CEAC" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M36.7715 9.1936L27.3297 6.89027M12.4145 8.77427L22.0908 6.73403" stroke="#92CEAC" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M36.6646 40.457L27.0219 42.4319M12.3681 40.457L22.0765 42.4319" stroke="#92CEAC" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
</svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

@@ -0,0 +1,10 @@
<svg width="49" height="49" viewBox="0 0 49 49" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="0.615234" y="0.583008" width="48" height="48" rx="8" fill="#164430"/>
<ellipse cx="24.6155" cy="24.583" rx="10.4446" ry="10.4446" fill="#1B543A"/>
<path d="M38.5327 13.8105C44.4469 21.6305 42.902 32.7642 35.0821 38.6785C27.2621 44.5927 16.1283 43.0479 10.2141 35.2279C4.29982 27.4079 5.8447 16.2742 13.6647 10.3599C17.4822 7.47272 22.0894 6.36315 26.4936 6.89344" stroke="#133929" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M24.3734 42.272C14.5688 42.272 6.62061 34.3238 6.6206 24.5192C6.6206 19.7328 8.51477 15.3889 11.5944 12.1961" stroke="#4CC38A" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M12.125 17.2451L12.125 11.9753L6.85522 11.9753" stroke="#4CC38A" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M24.3732 6.76636C34.1778 6.76636 42.126 14.7146 42.126 24.5192C42.126 29.3055 40.2318 33.6494 37.1522 36.8423" stroke="#4CC38A" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M36.6216 31.7932L36.6216 37.063L41.8914 37.063" stroke="#4CC38A" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M19.0513 24.7319L22.6614 28.342L30.1792 20.8242" stroke="#4CC38A" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1,10 @@
<svg width="49" height="49" viewBox="0 0 49 49" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="0.615234" y="0.583008" width="48" height="48" rx="8" fill="#DDF3E4"/>
<ellipse cx="24.6155" cy="24.583" rx="10.4446" ry="10.4446" fill="#5BB98C"/>
<path d="M38.5327 13.8105C44.4469 21.6305 42.902 32.7642 35.0821 38.6785C27.2621 44.5927 16.1283 43.0479 10.2141 35.2279C4.29982 27.4079 5.8447 16.2742 13.6647 10.3599C17.4822 7.47272 22.0894 6.36315 26.4936 6.89344" stroke="#92CEAC" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M24.3734 42.272C14.5688 42.272 6.62061 34.3238 6.6206 24.5192C6.6206 19.7328 8.51477 15.3889 11.5944 12.1961" stroke="#299764" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M12.125 17.2451L12.125 11.9753L6.85522 11.9753" stroke="#299764" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M24.3732 6.76636C34.1778 6.76636 42.126 14.7146 42.126 24.5192C42.126 29.3055 40.2318 33.6494 37.1522 36.8423" stroke="#299764" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M36.6216 31.7932L36.6216 37.063L41.8914 37.063" stroke="#299764" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M19.0513 24.7319L22.6614 28.342L30.1792 20.8242" stroke="#299764" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1,7 @@
<svg width="49" height="49" viewBox="0 0 49 49" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="0.615234" y="0.583008" width="48" height="48" rx="8" fill="#164430"/>
<path d="M10.7681 21.8865L24.47 17.4888H25.007L38.709 21.8865C39.1034 19.5185 38.15 14.3765 31.1812 12.7527L37.4394 8.52389C34.9303 6.77623 28.9312 4.70169 25.007 10.3848H24.47C20.5458 4.70169 14.5467 6.77623 12.0377 8.52389L18.2959 12.7527C11.327 14.3765 10.3737 19.5185 10.7681 21.8865Z" fill="#1B543A" stroke="#4CC38A" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M20.9399 34.1702L22.6231 19.3773C22.7424 18.3291 23.6294 17.5374 24.6843 17.5374V17.5374C25.7132 17.5374 26.5867 18.2915 26.7366 19.3095L28.8885 33.915" stroke="#4CC38A" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M39.5092 40.4512C35.8861 36.3764 30.6041 33.8093 24.7226 33.8093C18.8411 33.8093 13.5591 36.3764 9.93604 40.4512" stroke="#4CC38A" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M6.31982 41.2867C8.04818 39.5584 9.77653 39.5584 11.5049 41.2867C13.2332 43.0151 14.9616 43.0151 16.6899 41.2867C18.4183 39.5584 20.1466 39.5584 21.875 41.2867C23.6034 43.0151 25.3317 43.0151 27.0601 41.2867C28.7884 39.5584 30.5168 39.5584 32.2451 41.2867C33.9735 43.0151 35.7018 43.0151 37.4302 41.2867C39.1585 39.5584 40.8869 39.5584 42.6152 41.2867" stroke="#4CC38A" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1,7 @@
<svg width="49" height="49" viewBox="0 0 49 49" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="0.615234" y="0.583008" width="48" height="48" rx="8" fill="#DDF3E4"/>
<path d="M10.7681 21.8865L24.47 17.4888H25.007L38.709 21.8865C39.1034 19.5185 38.15 14.3765 31.1812 12.7527L37.4394 8.52389C34.9303 6.77623 28.9312 4.70169 25.007 10.3848H24.47C20.5458 4.70169 14.5467 6.77623 12.0377 8.52389L18.2959 12.7527C11.327 14.3765 10.3737 19.5185 10.7681 21.8865Z" fill="#5BB98C" stroke="#299764" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M20.9399 34.1702L22.6231 19.3773C22.7424 18.3291 23.6294 17.5374 24.6843 17.5374V17.5374C25.7132 17.5374 26.5867 18.2915 26.7366 19.3095L28.8885 33.915" stroke="#299764" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M39.5092 40.4512C35.8861 36.3764 30.6041 33.8093 24.7226 33.8093C18.8411 33.8093 13.5591 36.3764 9.93604 40.4512" stroke="#299764" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M6.31982 41.2867C8.04818 39.5584 9.77653 39.5584 11.5049 41.2867C13.2332 43.0151 14.9616 43.0151 16.6899 41.2867C18.4183 39.5584 20.1466 39.5584 21.875 41.2867C23.6034 43.0151 25.3317 43.0151 27.0601 41.2867C28.7884 39.5584 30.5168 39.5584 32.2451 41.2867C33.9735 43.0151 35.7018 43.0151 37.4302 41.2867C39.1585 39.5584 40.8869 39.5584 42.6152 41.2867" stroke="#299764" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1,24 @@
<svg width="49" height="49" viewBox="0 0 49 49" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="0.615234" y="0.583008" width="48" height="48" rx="8" fill="#164430"/>
<path d="M18.8701 24.5488L21.6753 30.3739" stroke="#133929" stroke-width="2.01025" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M32.5746 30.683L30.3 32.5934" stroke="#133929" stroke-width="2.01025" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M24.1841 19.3013L30.3125 21.4368" stroke="#133929" stroke-width="2.01025" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M11.7754 39.0007L17.439 37.7939" stroke="#133929" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M37.2285 39.8081L30.6406 38.8569" stroke="#133929" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<ellipse cx="24.1048" cy="35.4532" rx="2.34112" ry="2.34112" fill="#1B543A" stroke="#4CC38A" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M20.1445 40.9811V40.9811C20.1445 39.2209 21.5715 37.7939 23.3317 37.7939H25.0361C26.7963 37.7939 28.2233 39.2209 28.2233 40.9811V40.9811" stroke="#4CC38A" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<ellipse cx="24.1837" cy="36.0558" rx="6.55531" ry="6.55532" stroke="#4CC38A" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M12.0375 25.0637L10.0928 36.694" stroke="#133929" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M37.7832 16.6614L38.7832 12.0378" stroke="#133929" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M36.8184 9.90649L24.105 12.6675" stroke="#133929" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<ellipse cx="15.0146" cy="14.2197" rx="4.00096" ry="4.00096" fill="#1B543A" stroke="#4CC38A" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M9.42188 22.7209V22.7209C9.42188 20.2354 11.4368 18.2205 13.9224 18.2205H16.329C18.8146 18.2205 20.8295 20.2354 20.8295 22.7209V22.7209" stroke="#4CC38A" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<ellipse cx="15.1256" cy="15.766" rx="9.25647" ry="9.25647" stroke="#4CC38A" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<ellipse cx="9.64062" cy="39.6973" rx="2.56982" ry="2.56982" stroke="#133929" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<ellipse cx="39.6504" cy="39.6973" rx="2.56982" ry="2.56982" stroke="#133929" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M39.554 36.884L39.1313 32.1631" stroke="#133929" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<ellipse cx="39.6504" cy="9.46802" rx="2.56982" ry="2.56982" stroke="#133929" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<ellipse cx="37.5046" cy="23.2946" rx="3.24434" ry="3.24434" fill="#1B543A" stroke="#4CC38A" stroke-width="2.01025" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M32.9697 30.1885V30.1885C32.9697 28.173 34.6036 26.5391 36.6191 26.5391H38.5707C40.5862 26.5391 42.2201 28.173 42.2201 30.1885V30.1885" stroke="#4CC38A" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<ellipse cx="37.5948" cy="24.5487" rx="7.50598" ry="7.50598" stroke="#4CC38A" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="bevel"/>
</svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

Loaded 100 of 137 files, more files were not shown because too many files have changed in this diff. Show more