Merge pull request #13797 from supabase/master

Prod deploy studio
This commit is contained in:
Alaister Young authored and GitHub committed 2023-04-18 19:20:17 +10:00
commit 01b9b4ae21
410 files changed
+3694 -1506

No files matched your search

+5 -5
View File
@@ -22,15 +22,15 @@ const Favicons = () => {
{/* prettier-ignore */}
<link rel="apple-touch-icon-precomposed" sizes="152x152" href={`${basePath}/favicon/apple-touch-icon-152x152.png`}/>
{/* prettier-ignore */}
<link rel="icon" type="image/png" href={`${basePath}/favicon/favicon-196x196.png" sizes="196x196`}/>
<link rel="icon" type="image/png" href={`${basePath}/favicon/favicon-196x196.png`} sizes="196x196"/>
{/* prettier-ignore */}
<link rel="icon" type="image/png" href={`${basePath}/favicon/favicon-96x96.png" sizes="96x96`}/>
<link rel="icon" type="image/png" href={`${basePath}/favicon/favicon-96x96.png`} sizes="96x96"/>
{/* prettier-ignore */}
<link rel="icon" type="image/png" href={`${basePath}/favicon/favicon-32x32.png" sizes="32x32`}/>
<link rel="icon" type="image/png" href={`${basePath}/favicon/favicon-32x32.png`} sizes="32x32"/>
{/* prettier-ignore */}
<link rel="icon" type="image/png" href={`${basePath}/favicon/favicon-16x16.png" sizes="16x16`}/>
<link rel="icon" type="image/png" href={`${basePath}/favicon/favicon-16x16.png`} sizes="16x16"/>
{/* prettier-ignore */}
<link rel="icon" type="image/png" href={`${basePath}/favicon/favicon-128.png" sizes="128x128`}/>
<link rel="icon" type="image/png" href={`${basePath}/favicon/favicon-128.png`} sizes="128x128"/>
<meta name="application-name" content="&nbsp;" />
<meta name="msapplication-TileColor" content="#1E1E1E" />
<meta name="msapplication-TileImage" content="mstile-144x144.png" />
@@ -41,7 +41,7 @@ const App = () => <Auth supabaseClient={supabase} />
This renders the Auth component without any styling.
We recommend using one of the predefined themes to style the UI.
Import the theme you want to use and pass it to the `appearence.theme` prop.
Import the theme you want to use and pass it to the `appearance.theme` prop.
```js lines=4,16 title=/src/index.js
import { Auth } from '@supabase/auth-ui-react'
@@ -109,7 +109,7 @@ There are several ways to customize Auth UI:
### Predefined themes
Auth UI comes with several themes to customize the appearance. Each predefined theme comes with at least two variations, a `default` variation, and a `dark` variation. You can switch between these themes using the `theme` prop. Import the theme you want to use and pass it to the `appearence.theme` prop.
Auth UI comes with several themes to customize the appearance. Each predefined theme comes with at least two variations, a `default` variation, and a `dark` variation. You can switch between these themes using the `theme` prop. Import the theme you want to use and pass it to the `appearance.theme` prop.
```js lines=2,13 title=/src/index.js
import { createClient } from '@supabase/supabase-js'
@@ -36,7 +36,7 @@ Primary keys are **guaranteed not to change**. Columns, indices, constraints or
## Deleting Users
You may delete users directly or via the management console at Authentication > Users. Note that deleting a user from the `auth.users` table does not automatically sign out a user. As Supabase makes use of JSON Web Tokens (JWT), a user's JWT will remain "valid" until it has expired. Should you wish to immediately revoke access for a user, do considering making use of a Row Level Security policy as described below.
You may delete users directly or via the management console at Authentication > Users. Note that deleting a user from the `auth.users` table does not automatically sign out a user. As Supabase makes use of JSON Web Tokens (JWT), a user's JWT will remain "valid" until it has expired. Should you wish to immediately revoke access for a user, do consider making use of a Row Level Security policy as described below.
<Admonition type="caution">
You cannot delete a user if they are the owner of any objects in Supabase Storage.
+6
View File
@@ -25,6 +25,12 @@ Install the CLI as dev dependency via [npm](https://www.npmjs.com/package/supaba
npm install supabase --save-dev
```
Run the CLI by prefixing each command with `npx` (only applicable when installing through `npm`):
```sh
npx supabase <command>
```
</TabPanel>
<TabPanel id="macos" label="macOS">
+13 -11
View File
@@ -10,17 +10,6 @@ export const meta = {
Every Supabase project comes with a full [Postgres](https://www.postgresql.org/) database, a free and open source
database which is considered one of the world's most stable and advanced databases.
## Postgres or PostgreSQL?
PostgreSQL the database was derived from the POSTGRES Project, a package written at the University of California at Berkeley in 1986.
This package included a query language called "PostQUEL".
In 1994, Postgres95 was built on top of POSTGRES code, adding an SQL language interpreter as a replacement for PostQUEL.
Eventually, Postgres95 was renamed to PostgreSQL to reflect the SQL query capability.
After this, many people referred to it as Postgres since it's less prone to confusion. Supabase is all about
simplicity, so we also refer to it as Postgres.
## Features
### Table View
@@ -97,6 +86,19 @@ You can enable Postgres extensions with the click of a button within the Supabas
[Learn more](/docs/guides/database/extensions) about all the extensions provided on Supabase.
## Terminology
### Postgres or PostgreSQL?
PostgreSQL the database was derived from the POSTGRES Project, a package written at the University of California at Berkeley in 1986.
This package included a query language called "PostQUEL".
In 1994, Postgres95 was built on top of POSTGRES code, adding an SQL language interpreter as a replacement for PostQUEL.
Eventually, Postgres95 was renamed to PostgreSQL to reflect the SQL query capability.
After this, many people referred to it as Postgres since it's less prone to confusion. Supabase is all about
simplicity, so we also refer to it as Postgres.
## Tips
Read about resetting your database password [here](/docs/guides/database/managing-passwords) and changing the timezone of your server [here](/docs/guides/database/managing-timezones).
+4 -56
View File
@@ -50,7 +50,7 @@ void main() {
}
```
`SupabaseFunctions` class has a `fetch` handler, which is expected to return a `Response` object, and has a `request` parameter, which contains information about the request such as body and headers.
`SupabaseFunctions` class has a `fetch` handler, which is expected to return a `Response` object, and has a `request` parameter, which contains information about the request such as body and headers.
## Using Supabase Client
@@ -70,43 +70,11 @@ We also need to use a special HTTP client to HTTP requests in the edge environme
dart pub add edge_http_client
```
We also need to add the `yet_another_json_isolate` package, which is the JSON encoder/decoder that utilizes isolate.
```bash
dart pub add yet_another_json_isolate
```
### Override the JSON isolate
Since isolates are not supported in the edge, we have to override its behavior to handle JSON serialization using the main thread.
```dart
import 'package:yet_another_json_isolate/yet_another_json_isolate.dart';
class EdgeIsolate implements YAJsonIsolate {
@override
Future decode(String json) {
return Future.value(jsonDecode(json));
}
@override
Future<void> dispose() async {}
@override
Future<String> encode(Object? json) {
return Future.value(jsonEncode(json));
}
@override
Future<void> initialize() async {}
}
```
### Initialize SupabaseClient
At this point, you can initialize a `SupabaseClient` by passing the [EdgeHttpClient](https://pub.dev/packages/edge_http_client) and the `EdgeIsolate`. We can access the Supabase credentials through the [environment variables](https://supabase.com/docs/guides/functions/secrets) using `Deno.env.get()` method. Note that [Supabase credentials are available by default](https://supabase.com/docs/guides/functions/secrets), so no configurations are necessary.
At this point, you can initialize a `SupabaseClient` by passing the [EdgeHttpClient](https://pub.dev/packages/edge_http_client). We can access the Supabase credentials through the [environment variables](https://supabase.com/docs/guides/functions/secrets) using `Deno.env.get()` method. Note that [Supabase credentials are available by default](https://supabase.com/docs/guides/functions/secrets), so no configurations are necessary.
Combining the isolates we overrode earlier, we have the following sample functions code to query Supabase database using the Supabase client.
Now we have the following sample functions code to query Supabase database using the Supabase client.
```dart
import 'dart:convert';
@@ -114,14 +82,12 @@ import 'dart:convert';
import 'package:supabase_functions/supabase_functions.dart';
import 'package:edge_http_client/edge_http_client.dart';
import 'package:supabase/supabase.dart';
import 'package:yet_another_json_isolate/yet_another_json_isolate.dart';
void main() {
final supabase = SupabaseClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!, // Use service role key to bypass RLS
httpClient: EdgeHttpClient(),
isolate: EdgeIsolate(),
);
SupabaseFunctions(fetch: (request) async {
@@ -130,24 +96,6 @@ void main() {
return Response.json(users);
});
}
class EdgeIsolate implements YAJsonIsolate {
@override
Future decode(String json) {
return Future.value(jsonDecode(json));
}
@override
Future<void> dispose() async {}
@override
Future<String> encode(Object? json) {
return Future.value(jsonEncode(json));
}
@override
Future<void> initialize() async {}
}
```
## Run the Functions
@@ -178,7 +126,7 @@ You should be able to access your local function here:
### Deploy to the Edge
Run the following commands to compile and deploy your function on Supabase Edge functions.
Run the following commands to compile and deploy your function on Supabase Edge functions.
```bash
edge build supabase_functions
@@ -130,7 +130,7 @@ import type { LayoutServerLoad } from './$types';
export const load: LayoutServerLoad = async ({ locals: { getSession } }) => {
return {
session: getSession()
session: await getSession()
};
};
```
@@ -118,7 +118,7 @@ limit 100;
This query will show you statistics about queries ordered by the cumulative total execution time. It shows the total time the query has spent running as well as the proportion of total execution time the query has taken up.
Queries which are the most time consuming are not necessarily bad, you may have a very effiecient and frequently ran queries that end up taking a large total % time, but it can be useful to help spot queries that are taking up more time than they should.
Queries which are the most time consuming are not necessarily bad, you may have a very efficient and frequently ran queries that end up taking a large total % time, but it can be useful to help spot queries that are taking up more time than they should.
### Hit rate
+1 -1
View File
@@ -21,7 +21,7 @@ If the tool doesn't exist, we build and open source it ourselves.
![Supabase Architecture](/docs/img/supabase-architecture.png)
- [Kong](https://github.com/Kong/kong) is a cloud-native API gateway.
- [GoTrue](https://github.com/netlify/gotrue) is an SWT based API for managing users and issuing SWT tokens.
- [GoTrue](https://github.com/supabase/gotrue) is an SWT based API for managing users and issuing SWT tokens.
- [PostgREST](http://postgrest.org/) is a web server that turns your PostgreSQL database directly into a RESTful API
- [Realtime](https://github.com/supabase/realtime) is an Elixir server that allows you to listen to PostgreSQL inserts, updates, and deletes using websockets. Realtime polls Postgres' built-in replication functionality for database changes, converts changes to JSON, then broadcasts the JSON over websockets to authorized clients.
- [Storage](https://github.com/supabase/storage-api) provides a RESTful interface for managing Files stored in S3, using Postgres to manage permissions.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 475 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 869 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 452 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 859 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

+1 -8
View File
@@ -1,9 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<browserconfig>
<msapplication>
<tile>
<square150x150logo src="/favicons/mstile-150x150.png"/>
<TileColor>#000000</TileColor>
</tile>
</msapplication>
</browserconfig>
<browserconfig><msapplication><tile><square70x70logo src="/ms-icon-70x70.png"/><square150x150logo src="/ms-icon-150x150.png"/><square310x310logo src="/ms-icon-310x310.png"/><TileColor>#ffffff</TileColor></tile></msapplication></browserconfig>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 452 B

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 859 B

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

File diff suppressed because it is too large. Load diff
@@ -69,7 +69,7 @@ Why is this useful? Once we have generated embeddings on multiple texts, it is t
## Embeddings in practice
At a small scale, you could store your embeddings in a CSV file, load them into Python, and use a library like `numPy` to calculated similarity between them using something like cosine distance or dot product. OpenAI has a cookbook [example](https://github.com/openai/openai-cookbook/blob/main/examples/Semantic_text_search_using_embeddings.ipynb) that does just that. Unfortunately this likely won't scale well:
At a small scale, you could store your embeddings in a CSV file, load them into Python, and use a library like `numPy` to calculate similarity between them using something like cosine distance or dot product. OpenAI has a cookbook [example](https://github.com/openai/openai-cookbook/blob/main/examples/Semantic_text_search_using_embeddings.ipynb) that does just that. Unfortunately this likely won't scale well:
- What if I need to store and search over a large number of documents and embeddings (more than can fit in memory)?
- What if I want to create/update/delete embeddings dynamically?
+248
View File
@@ -0,0 +1,248 @@
---
title: 'dbdev: PostgreSQL Package Manager'
description: We're publicly previewing dbdev, a PostgreSQL package manager.
launchweek: 7
tags:
- launch-week
- postgres
date: '2023-04-14'
published_at: '2023-04-14T08:00:00.000-07:00'
toc_depth: 3
author: oli_rice,daltjoh-aws
image: launch-week-7/day-5-one-more-thing/day-5-dbdev-og.jpg
thumb: launch-week-7/day-5-one-more-thing/day-5-dbdev-thumb.jpg
---
Today we’re publicly previewing [**`database.dev`**](https://database.dev), a PostgreSQL package manager. At this stage the package registry is read-only. We've preloaded it with a handful of packages, or pglets (PostGres appLETs), to showcase some of the more interesting possibilities.
<Img
src="/images/blog/launch-week-7/day-5-one-more-thing/dbdev-landing.png"
wide={true}
alt="db dev landing page"
/>
`dbdev` fills the same role for PostgreSQL as `npm` for JavaScript, `pip` for Python and `cargo` for Rust in that it enables publishing libraries and applications for repeatable deployment. We'll be releasing the tooling necessary for third-parties to publish `pglets` to the registry once we’ve collected some community feedback and incorporate any great new ideas. Our goal is to create an [open ecosystem](https://github.com/supabase/dbdev) for packaging and discovering SQL.
The initial preview is compatible with [new projects](https://database.new) on the Supabase platform. It can also be installed on any PostgreSQL instance that support [`pg_tle`](https://github.com/aws/pg_tle) and [`pgsql-http`](https://github.com/pramsey/pgsql-http).
## Get Started with dbdev
The in-database client is the easiest way to get started. You can setup the installer by executing the SQL snippet available at [database.dev/installer](https://database.dev/installer).
Once the `dbdev` client is present, `pglet`s can be installed from the registry as shown below:
```sql
-- Load the package from the package index
select
dbdev.install ('olirice-asciiplot');
-- Enable the extension
create extension "olirice-asciiplot" version '0.2.1';
```
You can explore all available `pglet`s on [database.dev](https://database.dev).
Notice that PostgreSQL sees the [`olirice-asciiplot`](https://database.dev/olirice/asciiplot) `pglet` as a native extension, rather than a raw snippet of SQL. That approach allows us to leverage PostgreSQL's builtin tooling for extension management.
With our extension installed, you can use it like any other PostgreSQL extension. Continuing with the [`olirice-asciiplot`](https://database.dev/olirice/asciiplot) example, we can call the `scatter` function it provides to create an ASCII scatterplot:
```sql
select
scatter(
val::numeric, -- x
val::numeric, -- y
'stonks!', -- title
15, -- height
50 -- width
)
from
generate_series(1,10) z(val);
/*
stonks!
----------------------------------------------
| *
|
| *
| *
|
| *
|
| *
| *
|
| *
|
| *
| *
*/
```
PostgreSQL's extension tooling is excellent, but it predates some practices learned from best-in-class package indexes like crates.io. To give developers a more modern development experience, we opted to layer additional strictness on top of what PostgreSQL imposes:
### Versioning
The extension system has full support for versioning and migrations. Officially, PostgreSQL has loose constraints for version names. We made the choice to enforce a lite version of [Semantic Versioning](https://semver.org) that restricts version numbers to `major.minor.patch` so authors can communicate bug-fixes, features, and breaking changes in a familiar way.
### Namespaces
Two common challenges faced by package indexes are name squatting and typo squatting.
- Name squatting: reserving names for future use
- Typo squatting: reserving misspelling of existing package
The ethics of name squatting get dicey at scale while typo squatting is widely viewed as malicious behavior. To mitigate both issues, all `pglet`s published to [database.dev](https://database.dev) are namespaced to their owning organization or user’s handle. For example a `pglet` named [`olirice-index_advisor`](https://database.dev/olirice/index_advisor) was created by the account `olirice` under the name `index_advisor`. If another user, `some_user`, forks and republishes the project, it would be available under `some_user-index_advisor`. Problem solved ✅
## Running on Supabase
[database.dev](https://database.dev) is not coupled to the Supabase platform. `dbdev` can load SQL libraries on any PostgreSQL instance with the required base extensions. However, using `dbdev` in tandem with Supabase yields some extra possibilities.
Supabase reflects APIs directly from your database’s structure, so a `pglet` can contain an entire stateful application, pre-configured with authentication, REST, GraphQL, and realtime change data capture all baked in!
For example, our friends at [LangChain](https://python.langchain.com/en/latest/index.html) published a Supabase backend for their docs search tool that uses a hybrid of document embeddings and full text search to find relevant documents for a user’s query
Its available at [`langchain-hybrid_search`](https://database.dev/langchain/hybrid_search) and here’s how you’d set it up:
```sql
select
dbdev.install ('langchain-hybrid_search');
create extension if not exists vector;
create extension "langchain-hybrid_search" schema public version '1.0.0';
```
That creates the relevant `documents` table and associated search functions. Then, you can immediately hit it from your front end for best-in-class document search.
```jsx
import { OpenAIEmbeddings } from 'langchain/embeddings/openai'
import { createClient } from '@supabase/supabase-js'
import { SupabaseHybridSearch } from 'langchain/retrievers/supabase'
const privateKey = process.env.SUPABASE_PRIVATE_KEY
if (!privateKey) throw new Error(`Expected env var SUPABASE_PRIVATE_KEY`)
const url = process.env.SUPABASE_URL
if (!url) throw new Error(`Expected env var SUPABASE_URL`)
export const run = async () => {
const client = createClient(url, privateKey)
const embeddings = new OpenAIEmbeddings()
const retriever = new SupabaseHybridSearch(embeddings, {
client,
// Below are the defaults, expecting that you set up your supabase table and functions according to the guide above. Please change if necessary.
similarityK: 2,
keywordK: 2,
tableName: 'documents',
similarityQueryName: 'match_documents',
keywordQueryName: 'kw_match_documents',
})
const results = await retriever.getRelevantDocuments('hello bye')
console.log(results)
}
```
## Package Highlights
That's it for the [`dbdev`](http://database.new) announcement, but a package index is less interesting than what you can do with it! In that vein, the following highlights a few of packages I thought were interesting enough to callout:
### burggraf-pg_headerkit
[`burggraf-pg_headerkit`](https://database.dev/burggraf/pg_headerkit) is a toolkit for adding advanced features to PostgREST APIs (including Supabase REST):
- rate limiting
- IP allowlisting/denylisting
- request logging
and more.
For example, you could apply a deny listing to your API using `hdr.in_deny_list()` in a row level security policy or view:
```sql
select
*
from
app.memos
where
not hdr.in_deny_list ();
```
### olirice-index_advisor
[`olirice-index_advisor`](https://database.dev/olirice/index_advisor) is one of the projects we cut from [Launch Week 7](/launch-week). It is simple tool that takes a query and recommends indexes to minimize the “total_cost” according to the query’s [explain plan](https://www.postgresql.org/docs/current/using-explain.html).
We ultimately ran out of time to squeeze the feature in, but the optimizer works just fine:
```sql
select dbdev.install('olirice-index_advisor');
create extension if not exists hypopg;
create extension "olirice-index_advisor";
-- Create a dummy table
create table account(
id int primary key,
name text
);
-- Search for indexes to optimize "select id from account where name = 'adsf'"
select
*
from
index_advisor($$select id from account where name = 'Foo'$$)
```
which shows
```markdown
| startup_cost_before | startup_cost_after | total_cost_before | total_cost_after | index_statements |
| ------------------- | ------------------ | ----------------- | ---------------- | ----------------------------------------------------- |
| 0.00 | 1.17 | 25.88 | 6.40 | {"CREATE INDEX ON public.account USING btree (name)"} |
```
In other words, it recommends the index `CREATE INDEX ON public.account USING btree (name)` which is expected to reduce the total cost from 25.88 to 6.40 for a 4x decrease.
`olirice-index_advisor` is compatible with tables, views, and materialized views. It can also see through views to find relevant indexes on underlying tables, and supports generic query arguments. For example, `$1` in `select id from account where name = $1`, which makes it compatible with queries from `pg_stat_statements` and queries generated by the REST API.
Keep an eye open for it in Launch Week 8.
### michelp-adminpack
[`michelp-adminpack`](https://database.dev/michelp/adminpack) is a collection of tools helpful for administrating your database that we often use internally at Supabase. It holds views for reviewing useful info for debugging and optimizing performance like duplicate indexes, index usage, and table size, to name a few.
For example, to identify potentially unused indexes that can be dropped, you could use the `index_usage` view, which has columns for:
| Column | Type |
| --------------- | ------ |
| schemaname | name |
| tablename | name |
| num_rows | bigint |
| table_size | text |
| index_name | name |
| index_size | text |
| unique | text |
| number_of_scans | bigint |
| tuples_read | bigint |
| tuples_fetched | bigint |
## Limitations
There are several procedural languages (PL) that can be embedded in PostgreSQL and used to define functions. The ones that ship with stock PostgreSQL are `SQL`, and `pl/pgSQL` but there others that can be installed separately, including `pl/v8` for JavaScript, or `pl/perl` for Perl. A **trusted** language has been restricted to remove potentially hazardous functionality like access to the network stack and file system. `pl/v8` and `pl/perl` are examples of trusted languages. In contrast, `pl/python3u` is **untrusted**.
A [Trusted Language Extension (TLE)](https://github.com/aws/pg_tle) is a [PostgreSQL extension](https://www.postgresql.org/docs/current/extend-extensions.html), written exclusively using trusted languages. In some ways that makes them less flexible than classic extensions, which can have C language components (more on that in a second). The advantage to TLEs is that they don't require direct access to the PostgreSQL server’s file system to install. That enables TLEs to be installed by end-users rather than by database administrators or hosting providers. TLEs are the enabling technology that allows a package manager like `dbdev` to function on hosted PostgreSQL platforms like Supabase.
For a more in-depth explanation of Trusted Language Extensions checkout [AWS's pg_tle on Supabase blog post](https://aws.amazon.com/blogs/opensource/supabase-makes-extensions-easier-for-developers-with-trusted-language-extensions-for-postgresql/) or dive into the code at [github.com/aws/pg_tle](https://github.com/aws/pg_tle).
A recent development in the PostgreSQL extension ecosystem is the 1.0 release of a new trusted language, [`pl/rust`](https://github.com/tcdi/plrust), allowing users to define SQL functions written in Rust. As a compiled language, `pl/rust` functions can execute an order of magnitude faster than `pl/pgSQL` for computationally heavy workloads. That closes the biggest capability gap between native extensions with C components and TLEs. `pl/rust` hasn’t released to Supabase yet, but we’re excited about rolling it out in the coming weeks.
## Please Give Feedback
As this is a preview, we anticipate that there may be a few rough edges. If you do take the time to explore `dbdev` at this stage, please contribute to its development at [github.com/supabase/dbdev](https://github.com/supabase/dbdev).
We are particularly interested in hearing about:
1. Any issues or bugs you encounter
2. Feature requests and suggestions for improvement
3. Contributions in the form of code, documentation, or testing
@@ -0,0 +1,199 @@
---
title: 'Launch Week 7 Community Highlights'
launchweek: 7
tags:
- launch-week
date: '2023-04-14'
published_at: '2023-04-14T07:00:00.000-07:00'
toc_depth: 3
author: paul_copplestone
image: launch-week-7/day-5-community-highlights/day-5-community-highlights-og.jpg
thumb: launch-week-7/day-5-community-highlights/day-5-community-highlights-thumb.jpg
---
Supabase is a collaborative company. We work with, sponsor, and support as many open source tools as possible. Here’s a few of this community highlights from the past 3 months.
## Ecosystem Partners
![Ecosystem Partners](/images/blog/launch-week-7/day-5-community-highlights/day-5-community-highlights-partners.png)
Our [Integrations Marketplace](/docs/guides/integrations) continues to grow. This past few months has brought a number of new partners and collaborations:
### AWS RDS
We’ve worked with the RDS team to build out the pg_tle extension and tooling and integrating it into Supabase. These changes are a stepping stone towards running the Supabase stack on AWS RDS. Special shoutout to [@jkatz](https://github.com/jkatz), who was an instrumental collaborator and the rest of the RDS team who have been helping to transition [PL/Rust](https://github.com/tcdi/plrust) into a Trusted Language.
[Read the press announcement on Amazon.com.](https://aws.amazon.com/blogs/opensource/supabase-makes-extensions-easier-for-developers-with-trusted-language-extensions-for-postgresql/)
[Read about pg_tle at Supabase and our collaboration with AWS.](https://www.notion.so/6f1bac83308846488ee7b01ae5c20200)
[Read about our experimental database registry, database.dev.](/blog/dbdev)
### Clickhouse
Along with our new Clickhouse [Foreign Data Wrapper](https://supabase.github.io/wrappers/clickhouse/), our friends at Clickhouse created guides showing the bidirectional capabilities between Supabase Postgres and Clickhouse Cloud. This is a perfect pairing when you’re consolidating your operational data with your analytical data, or if you have long-running queries that you need to offload to an analytical database like Clickhouse.
[Read the guide.](https://clickhouse.com/blog/migrating-data-between-clickhouse-postgres)
### LangChain
It’s always fun when two open source communities join forces. LangChain is an open source framework for developing applications powered by language models. This week, to support the Supabase AI Hackathon, they released a [Supabase x Langchain template](https://github.com/langchain-ai/langchain-template-supabase), one of the fastest ways to build AI applications. Their community recently made two contributions for Supabase: adding Supabase as a [Vector Store](https://js.langchain.com/docs/modules/indexes/vector_stores/integrations/supabase), and a neat [Hybrid Search](https://js.langchain.com/docs/modules/indexes/retrievers/supabase-hybrid) algorithm using Postgres’ Full Text Search. The also
[Read the integration docs.](https://js.langchain.com/docs/modules/indexes/vector_stores/integrations/supabase)
[Clone the template.](https://github.com/langchain-ai/langchain-template-supabase)
### Vercel + OpenAI
Do we even need to introduce Vercel and OpenAI? We have a new template in the [Vercel Marketplace](https://vercel.com/templates/next.js/openai-doc-search-starter) for creating GPT-search for your MDX files. The templates takes all the `.mdx` files in the `pages`directory and processes them to use as custom context within [OpenAI Text Completion](https://platform.openai.com/docs/guides/completion) prompts.
[View the template.](https://vercel.com/templates/next.js/openai-doc-search-starter)
### Doppler
Doppler is a fully managed SecretOps platform that helps centralize, orchestrate, and rotate secrets in any environment. Your team can mitigate and remediate risks while using tools that fit natively into existing developer workflows.They just released an integration to automatically sync all of your team's secrets to Supabase.
[Check out the docs.](https://docs.doppler.com/docs/supabase)
### Redwood
The RedwoodJS team has contributed a new framework [Quick Start](/docs/guides/getting-started/quickstarts/redwoodjs) to our docs that will get you from zero to fullstack app with Prisma in just 2 minutes. Big shoutout to [@dthyresson](https://github.com/dthyresson) who continues to be a valuable contributor across many open source communities.
[Read the QuickStart.](/docs/guides/getting-started/quickstarts/redwoodjs)
### Dart Edge for Supabase Edge Functions
Dart Edge, built by [Invertase](https://invertase.io/), allows developers to write edge functions in Dart. Previously, Flutter developers had to use TypeScript for their edge functions, as it was the only supported language. However, with Dart Edge, Flutter developers can write backend code using their favorite programming language, Dart!
[Check out the guide.](/docs/guides/functions/dart-edge)
### **T3 Turbo x Supabase**
T3 Turbo is a type-safe, full-stack template to create Next.js and Expo React Native applications. Using our recent [Mobile Auth updates](/blog/supabase-auth-sso-pkce), the T3 community has helped build the [Supabase Create T3 Turbo template](https://github.com/supabase-community/create-t3-turbo), the fastest and easiest way to get started with Supabase and the T3 Stack. Huge shout out to [Julius](https://twitter.com/jullerino) who single-handedly created the template!
[Use the template.](https://github.com/supabase-community/create-t3-turbo)
## Self-hosting with Digital Ocean and Stackgres
![Self-hosting with Digital Ocean and Stackgres](/images/blog/launch-week-7/day-5-community-highlights/day-5-community-highlights-digitalocean-and-stackgres.png)
This Launch Week had a big focus on self-hosting, adding support for self-hosted [Logs](/blog/supabase-logs-self-hosted) and [Edge Functions](/blog/edge-runtime-self-hosted-deno-functions). Along with our official [self-hosting guides](/docs/guides/self-hosting) (and existing community support for [AWS](https://github.com/supabase-community/supabase-on-aws), [k8s](https://github.com/supabase-community/supabase-kubernetes), [Traefik](https://github.com/supabase-community/supabase-traefik), and [Terraform](https://github.com/supabase-community/supabase-terraform)), we have a couple of new entrants into the community:
- [Digital Ocean](https://supabase-on-do.product-docs.pages.dev/developer-center/hosting-supabase-on-digitalocean/)
- [Stackgres](https://stackgres.io/blog/running-supabase-on-top-of-stackgres/)
We’re looking for more self-hosting maintainers, especially for the self-hosted guides. Everything we do is MIT, Apache2, or PostgreSQL licensed, but our team is relatively small and we don’t have expertise for every deployment strategy that our community might want. If you are willing to help out on a regular basis please reach out!
## Postgres Extensions and improvements
![Postgres Extensions and improvements](/images/blog/launch-week-7/day-5-community-highlights/day-5-community-highlights-extensions.png)
The community continues to add and improve the extensions and configuration for our [Postgres image](https://github.com/supabase/postgres). Here are a few of the crowd favorites:
### pgvector: Open-source vector similarity search for Postgres
Just as it says on the label: `pgvector` enables vector functionality within Postgres. It’s been, by far, our most popular addition to date as it can be used to store OpenAI embeddings and serves as an alternative to Pinecone and Milvus.
[Read the announcement.](/blog/openai-embeddings-postgres-vector)
### **HypoPG: Hypothetical indexes**
HypoPG allows you to create hypothetical indexes that are visible to the PostgreSQL query
planner, with no resource cost (CPU, disk, memory). This allows users to “try out” an index without waiting for the server to build them. We have big plans for this extension - we’ll integrate this functionality into the dashboard with an Index Advisor to help you optimize slow-running queries.
[Read the docs.](/docs/guides/database/extensions/hypopg)
### pg_tle: Install your own extensions
We’ve been working with the RDS team to make Postgres _even more_ extensible. While you typically need to wait for Cloud Providers to add extension support to their Postgres offering, pg*tle is a step towards making this \_user managed*. We’ve even built a proof-of-concept registry (similar to NPM or Cargo) where you can upload extensions and share them with the community.
Link to blog post.
### PGroonga v3.0
PGroonga is an extension implementing full-text search in multiple languages. In v3, PGroonga becomes fully compatible with the Supabase _extensions_ schema deprecating the need to explicitly write a schema for operators. It also increases support for _text_ and _varchar_ array types. They just released this in time for Supabase Launch Week, so we’ll roll it out to the platform in the next month.
[Read the announcement.](https://groonga.org/en/blog/2023/04/13/pgroonga-3.0.0.html)
### LZ4 compression
PG has supported LZ4 page and row compression since v14, with WAL compression added in v15. LZ4 is faster than the standard pglz method. Supabase is now fully-compatible with this option. This is especially useful for TOAST values and WAL compression.
### ICU compatibility
Supabase Postgres has been configured with ICU support. This makes it possible to use ICU locales as collation providers. ICU supports many languages and makes the ordering of text type attributes according to these language rules.
## Client Libraries and utilities
![Client Libraries and utilities](/images/blog/launch-week-7/day-5-community-highlights/day-5-community-highlights-libraries.png)
It’s becoming easier to build applications with any language.
### Support for Swift, Python, and C#
In the past few months, the community has rallied and shipped 3 new client libraries, complete with Docs:
- Swift:
- [Documentation](/docs/reference/swift/introduction) | [Source Code](https://github.com/supabase-community/supabase-swift)
- Shout-out to: [grsouza](https://github.com/grsouza) & [@maail](https://github.com/maail)
- Python:
- [Documentation](/docs/reference/python/initializing) | [Source Code](https://github.com/supabase-community/supabase-py)
- Shout-out to: @
- C#
- [Documentation](/docs/reference/csharp/introduction) | [Source Code](https://github.com/supabase-community/supabase-csharp)
- Maintainers: [@acupofjose](https://github.com/acupofjose), [@veleek](https://github.com/veleek), and [@rhuanbarros](https://github.com/rhuanbarros)
These libraries are in various stages of readiness, so if you find anything that can be improved don’t hesitate to jump into the source code and help out.
### Supabase Cache Helpers
[Philipp](https://github.com/psteinroe) just released 1.0 of “Supabase Cache Helpers”, a collection of cache utilities for Supabase. With just a line of code, you can simplify the logic of **f**etching, subscribing to updates, and mutating data as well as storage objects in your project, and have all the amazing features of SWR or React Query. The cache helpers parse queries into a unique and definite query keys, automatically populating the query cache with every mutation using implicit knowledge of the schema.
[Check it out on GitHub.](https://github.com/psteinroe/supabase-cache-helpers)
[Read the docs.](https://supabase-cache-helpers.vercel.app/)
### SupaShim by Roboflow
The team at [Roboflow](https://roboflow.com/) just released [`supashim`](https://github.com/roboflow/supashim), which hooks into the Firebase calls from your app and translates them into the Supabase equivalents. With supashim, you can take an existing Firebase app, swap out the `firebase` global for `supashim`, then point it to a self-hosted Supabase instance without changing any application code. Pretty neat!
[Check it out on GitHub.](https://github.com/roboflow/supashim)
## Giving back
We’ve been upstreaming as many changes as possible for Supabase stack.
### Postgres Patches
We’ve submitted a number of patches to the Postgres Core:
- Added the “USER SET” parameter values for `pg_db_role_setting`. This GUC variable lets ordinary roles set placeholder variables, when permission requirements are not known yet [[Patch](https://www.postgresql.org/message-id/flat/20230102154240.GL1153%40telsasoft.com#c04239ecde38752a3132236306784250)]
- Added support for custom tuple slots in the custom executor nodes. This is useful for custom table access methods. [[Patch](https://www.postgresql.org/message-id/flat/CAPpHfduJUU6ToecvTyRE_yjxTS80FyPpct4OHaLFk3OEheMTNA%40mail.gmail.com)]
- Optimizations for LWlock waiting queue scalability. Long LWlock wait queues were not effective at unlocking. We’ve submitted proposals to make the queue lockless (still under community consideration). [Discussion [one](https://www.postgresql.org/message-id/flat/CALT9ZEEz%2B%3DNepc5eti6x531q64Z6%2BDxtP3h-h_8O5HDdtkJcPw%40mail.gmail.com), [two](https://www.postgresql.org/message-id/flat/20221120204310.xywrhyxyytsajuuq%40awork3.anarazel.de), [three](https://www.postgresql.org/message-id/flat/20221027165914.2hofzp4cvutj6gin%40awork3.anarazel.de)]
### OrioleDB Patches
We continue to support the development of [OrioleDB](https://github.com/orioledb/orioledb), who are building scalable storage mechanisms for Postgres using table access methods. The OrioleDB extension is now version [Alpha12](https://github.com/orioledb/orioledb/releases/tag/alpha12) and compatible with Postgres 15.
This release includes various improvements and fixes, including parallel sequential scan of OrioleDB relations. This allows multi-CPU systems to speed up select queries that involve sequential scans. It’s an analogue of heap parallel scan in Postgres core but for index-organized tables. With this base, we’re now implementing methods to build Orioledb indexes using many parallel workers.
[Read the release notes.](https://github.com/orioledb/orioledb/releases/tag/alpha12)
## Contributors
We also want to highlight all the humans who have made meaningful contributions to the Supabase ecosystem since our last Launch Week:
- [Gary](https://github.com/GaryAustin1) and [Olyno](https://github.com/olyno) - official moderators of our [Discord](https://discord.supabase.com) and overall incredibly helpful people. The community would not be the same without them
- [Bruno D'Luka](https://github.com/bdlukaa) - Caring for the details of Flutter SDK so that we can provide the best DX for Flutter devs
- [CodewithGuillaume](https://twitter.com/blackevilgoblin) - Creating a series of YouTube videos explaining different components of Supabase.
- [Daniel Mossaband](https://github.com/DanMossa) - Worked on fixing bugs and adding features to bring the mobile auth to Supabase
- [David Plugge](https://github.com/david-plugge) - Working on improving the [auth-helpers](https://github.com/supabase/auth-helpers) and fixing issues users report.
- [Jonathan Wilke](https://twitter.com/jonathan_wilke) - built [SupaStarter.dev](https://supastarter.dev/). A SaaS for building SaaSes.
- [grsouza](https://github.com/grsouza) & [Maail](https://github.com/maail) - Built the Swift libraries and documentation.
- [Raphaël Moreau](https://twitter.com/rphlmr) - Built the [supa-stripe-stack](https://github.com/rphlmr/supa-stripe-stack) for Remix to get started building your SaaS in one command. Also the creator the [supa-fly-stack](https://github.com/rphlmr/supa-fly-stack) and [supa-remix-stack](https://github.com/rphlmr/supa-remix-stack)
- [Vinzent](https://github.com/Vinzent03) - Constantly smashing it on the Flutter SDK adding major features such as MFA.
- [Tiniscule](https://twitter.com/tiniscule) - for his open source [Supabase Starter kit](https://github.com/usebasejump/basejump).
## Community
And you, our community. Thank you for using Supabase, reading our blog posts, and showing your support. With [over 48,000 GitHub stars](https://github.com/supabase/supabase), we are now in the top 180 most-popular repositories in all of GitHub. We still have a long way to go, but it’s a lot more fun doing it collaboratively.
+60
View File
@@ -0,0 +1,60 @@
---
title: 'Trusted Language Extensions for Postgres'
launchweek: 7
tags:
- launch-week
- postgres
date: '2023-04-14'
published_at: '2023-04-14T07:00:00.000-07:00'
to_depth: 3
author: michel,daltjoh-aws
image: launch-week-7/day-5-supabase-pg-tle/day-5-postgres-tle-thumb.jpg
thumb: launch-week-7/day-5-supabase-pg-tle/day-5-postgres-tle-thumb.jpg
---
Most PostgreSQL cloud providers, such as Supabase and AWS RDS, do not allow users to install Postgres extensions into their databases until they have been rigorously vetted. Because of this, many useful Postgres extensions cannot be used on cloud platforms. To solve this, the RDS team has developed a new kind of extension called a **Trusted Language Extension** (TLE).
A TLE is a Postgres extension that is written in a Trusted Language, and it can be installed in any Postgres provider that has the [pg_tle](https://github.com/aws/pg_tle) extension installed. The goal is to make more extensions available to developers, even if your database is hosted on the cloud.
## How Postgres extensions work
Before exploring what a Trusted Language Extension is, let’s cover the basics of Postgres Extensions and the notion of “Trusted Languages” in general.
A Postgres Extension is a bundle of SQL and other code, primarily [Data Definition Language](https://www.postgresql.org/docs/current/ddl.html) (DDL) code installed into the server’s filesystem, and instantiated in a database with the [`CREATE EXTENSION`](https://www.postgresql.org/docs/current/sql-createextension.html) command. This code can contain any SQL, but almost always includes additional function objects created with the [`CREATE FUNCTION`](https://www.postgresql.org/docs/current/sql-createfunction.html) DDL statement that expose the core features of the extension to the database in some programming language, including an external library function written in C.
Extensions provide a variety of additional functionality and allow third-party feature distribution without requiring changes to the Postgres database itself - including new data types, storage engines, helper functions, and even entire language runtimes like Javascript or Python.
Some examples of popular Postgres extensions include:
- [PostGIS](https://postgis.net/): for working with geospatial data
- [Timescale DB](https://www.timescale.com/): for working with timeseries data
- [Citus](https://www.citusdata.com/): for working with distributed tables
- [plv8](https://plv8.github.io/): for running Node.js inside Postgres
- [pgsodium](https://github.com/michelp/pgsodium): for accessing the libsodium encryption API
Before extensions can be created, they must first be installed on the filesystem of the database server, which is an important security measure because in some cases extensions may require external libraries and can interact with Postgres internals. Their installation is something that requires careful consideration.
## Trusted vs Untrusted
Many extensions provide functions whose implementation is written in C, and creating them in a database means that the compiled C code is “dynamically linked” into your running Postgres process. These dynamically-loaded libraries can now access every aspect of your running database process, right down to raw memory. They are essentially database superusers on steroids. Because of this, C is an “untrusted language” and installing extensions written in C requires filesystem access.
However there are several _trusted_ languages in Postgres that don’t provide raw access to process memory, the filesytem, or other system functions. These languages obey the security rules of Postgres. The most obvious example of these trusted languages is SQL itself. Another built-in trusted language in Postgres is called [pl/pgsql](https://www.postgresql.org/docs/current/plpgsql.html). It looks a lot like SQL but with some imperative programming features like `IF` blocks and `FOR` loops. The aforementioned plv8 is also a trusted language, as are the languages [pl/perl](https://www.postgresql.org/docs/current/plperl.html) and [pl/tcl](https://www.postgresql.org/docs/current/pltcl.html). Other languages like [pl/python](https://www.postgresql.org/docs/current/plpython.html) are **_not_** trusted because of the way they are integrated into Postgres, they do not obey the “rules” of Postgres security, and so are not included in most cloud provider platforms since they would allow database users to effectively act as a superuser.
## Trusted Language Extensions
Near the end of 2022, we got a surprise gift from the team at Amazon Web Services, a new Postgres extension called `pg_tle`, that allows you to **Trusted Language Extensions (TLEs).** A TLE is just like a regular Postgres extension, except that **does not need to be installed on the server filesystem** it is written in a **trusted language** you can install extensions from any database client using the function `pgtle.install_extension()`, just like you can create other objects like tables and views.
Of course, TLEs _must_ be written in trusted languages as described above, hence the name, so no extensions that are written in C can be TLEs. There are still many extensions that are already written in trusted languages, and soon many more to come with some exciting progress in more performant trusted languages like [pl/rust](https://github.com/tcdi/plrust) which recently reached an official 1.0.0 version release. By removing the constraint that extensions must be installed on the server filesystem, TLEs open up a whole new world of possibilities for packaging and distributing new and modular functionality for Postgres.
## Collaborating with AWS
Because untrusted extensions are, by nature, able to break the security rules of Postgres, cloud providers are quite conservative about what extensions to provide to their customers. New extensions are rarely adopted quickly. This slows down the speed that new software can be created and shared. Open source requires opportunity and momentum, and by closing off the opportunity for Postgres users to quickly create, install, evaluate and iterate extensions, progress is slowed. TLEs offer an opportunity for open source developers to create and share code without having to wait for cloud providers or other gatekeepers to install it.
When AWS released `pg_tle`, we realized immediately how powerful the tool is, and how it will change the way people release and install trusted extensions forever. We started collaborating with the TLE team at AWS, exchanging ideas and resources to further promote TLEs in both our database platform and theirs. By providing the TLE extension in both Supabase and AWS, we are unifying a standard package platform across two of the largest Postgres providers. We hope that other Postgres cloud providers will join the collaboration so that extension developers can leverage a large community base that greatly multiplies the reach of their efforts.
## Installing TLEs with database.dev
pg_tle is available on new Supabase projects and on Amazon RDS for Postgres 14.5 and higher. Supabase also includes an extension called [pgsql-http](https://github.com/pramsey/pgsql-http) for making HTTP calls from SQL to REST APIs. So, naturally putting two and two together, we thought it would be cool to see if it’s possible to install TLEs from a REST API using only SQL!
We've created a new package registry, dbdev, that makes it easy to install extensions in your database migrations - as easy as: `select dbdev.install('langchain-embedding_search')`.
You can read more about dbdev in our other blog post: [dbdev: a PostgreSQL Package Manager](/blog/dbdev).
@@ -0,0 +1,163 @@
---
title: 'Supabase Studio 2.0: help when you need it most'
launchweek: 7
tags:
- launch-week
- studio
date: '2023-04-14'
published_at: '2023-04-14T09:00:00.000-07:00'
to_depth: 3
author: alaister,joshenlim,jonny,saltcod
image: launch-week-7/day-5-supabase-studio-2.0/day-5-supabase-studio-2.0-og.jpg
thumb: launch-week-7/day-5-supabase-studio-2.0/day-5-supabase-studio-2.0-thumb.jpg
---
Today we're announcing Supabase Studio 2.0, packed with a ton of new stuff. We have features people have been asking for forever, and new capabilities that will change the way you work.
<div className="video-container">
<iframe
className="w-full"
src="https://www.youtube-nocookie.com/embed/0rcNqHt5KWU"
title="YouTube video player"
frameBorder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowfullscreen
></iframe>
</div>
Here's the birds-eye-view:
<ul>
<li>
[**Supabase AI**](#supabase-ai--assisted-database-development): ChatGPT, right in the Studio
</li>
<li>[**GraphiQL**](#graphiql): Query your database with GraphQL</li>
<li>[**Cascade deletes**](#cascade-deletes): Our #1 most requested feature from the community</li>
<li>[**Query Performance**](#query-performance): Investigate slow running queries</li>
<li>[**Foreign key Selector**](#foreign-key-selector): Look up a row in reference table</li>
<li>[**Postgres Roles**](#postgres-roles): Manage your Postgres roles and privileges</li>
<li>[**Database Webhooks**](#database-webhooks): Database triggers for Edge Functions</li>
<li>
[**Table/View definitions**](#tableview-definitions): View SQL definitions for tables and views
</li>
<li>[**API Autodocs**](#api-autodocs): View auto-generated docs right from the Table Editor</li>
<li>
[**Support for many tables**](#support-for-many-tables): Table Editor now supports 1000s of
tables
</li>
<li>[**JSON editing**](#json-editing): Improved JSON editing</li>
<li>[**Nullable columns**](#nullable-columns): Allow text/bool cells to be null or empty</li>
</ul>
## Supabase AI — assisted database development
<video width="99%" autoPlay="" loop="" muted="" playsInline="" controls={true}>
<source
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/cmdk.mp4"
type="video/mp4"
/>
</video>
One of our guiding principles at Supabase is to make SQL more accessible to developers. We don't want to just abstract it away with a custom implementation. By embracing SQL, developers learn important, transferable skills while they build on top of Supabase.
In the past few months, AI advancements have made this easy! Writing a complex SQL query is now as simple as asking ChatGPT, and we're leaning into this approach with our Studio. AI gives developers superpowers, and by providing relevant context, Supabase gives AI superpowers.
AI has already changed a lot about the way we work as developers. We can use ChatGPT to write code for us as fast as we can prompt it. We wanted to bring this power into the Studio with integrations that will help you work even faster than before.
Today you'll be able to do many common SQL operations with the help of AI. You can create tables, views and indexes, write functions and triggers, and more, right from the ⌘K menu.
Soon, we'll also let you opt-in to sending your table schemas to OpenAI to help fine tune queries to your project. All this should result in a dramatic boost in development speed. Maybe you'll only need half a weekend to scale to millions now!
As with every ⌘K menu, quick navigation is at the heart of things. You'll now be able to jump to any page in the Studio in a couple of keystrokes. You can also search the Docs directly from the menu.
We'll be actively working on our ⌘K menu in the coming months to make it faster and even more useful for you. Next, we're going to focus on better support for writing RLS policies. Stay tuned, and make sure to explore how we're integrating AI [behind the scenes](https://supabase.com/blog/tags/AI)!
Huge props for the amazing work done on the [⌘K package](https://cmdk.paco.me/) we've built on top of here.
---
Along with the new AI features, we also doubled-down on some of the critical missing pieces for a Postgres UI. Before we started to work on any of these, we combed through user feedback and tallied up the most submitted Feature Requests on GitHub.
<div className="bg-gray-300 rounded-lg px-6 py-2 italic">
📢 Many of the features and enhancements below came from user requests — [please keep them coming](https://github.com/orgs/supabase/discussions/categories/feature-requests)!
</div>
## GraphiQL
![Logs UI](/images/blog/lw7-studio/graphiql.png)
While GraphQL has been available in Supabase for just over a year, we haven't provided a visual tool for using it. You can now use the very popular GraphiQL directly from the Studio. GraphiQL is a browser tool for writing, validating, and testing GraphQL testing. Being able to use a fully integrated GraphQL IDE is a huge DX win.
## Cascade deletes
<video width="99%" autoPlay="" loop="" muted="" playsInline="" controls={true}>
<source
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/cascade-delete.mp4"
type="video/mp4"
/>
</video>
Cascade deletes are a core feature of Postgres, but have not been available in the Studio UI until now. This has been the #1 feature request from the community for a long time. You can now choose what you want to happen when deleting a referenced row in another table, right from Table Editor side panel.
## Query Performance
![Logs UI](/images/blog/lw7-studio/query-performance.png)
We've just released a new Query Performance tool to help you identify slow queries. Using this tool, along with our new [guide](https://supabase.com/docs/guides/platform/performance#examining-query-performance) should help you speed things up. These tools can help you uncover where you might have inefficient queries or schemas, or where you might need indexes or even additional compute resources.
## Foreign key selector
<video width="99%" autoPlay="" loop="" muted="" playsInline="" controls={true}>
<source
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/fk-lookup.mp4"
type="video/mp4"
/>
</video>
You can now select a referencing row from another table, rather than having to pass a value manually.
You can also quickly jump to the row being referenced in another table with the _View referencing_ record
button from the grid itself.
## Postgres Roles
![Logs UI](/images/blog/lw7-studio/roles.png)
The Roles page got a huge revision, making it easy for you to manage access control through users, groups, and permissions. It's now easy to see how many connections you're using and where they're coming from.
## Database Webhooks
![Logs UI](/images/blog/lw7-studio/webhooks.png)
We've wanted to improve the webhooks interface for a long time, and we've finally gotten to it. We now have full support for editing your webhooks, and you can now select an edge function to call instead of having to pass in a url. We're happy to roll this very common request out to the community.
## Table/View definitions
![Logs UI](/images/blog/lw7-studio/definitions.png)
You can now see the SQL definitions for your tables and views, directly from the Table Editor. This is a great way to see how Supabase is translating your UI changes into SQL. This can be useful for creating migrations, getting help, or just for learning more about how Supabase and SQL works.
## API Autodocs
![Logs UI](/images/blog/lw7-studio/api-autodocs.png)
You can now view the auto-generated API docs right from the Table Editor. Grab the SQL and `supabase-js` code for all of the CRUD operations, straight from the table. You can also quickly generate and download a Typescript types file for your table, right from the editor.
### Quality of life improvements
#### Support for many tables
Previously, the Table Editor would get slow and unresponsive when you had many tables. We've made a number of improvements to make it much faster and more responsive. Feel free to make all the tables you need!
#### JSON editing
You've always been able to use the JSON data type in your tables, but editing the data wasn't easy. We've improved the inline Table Editor, and also now allow you open json cells from the side panel for a more spacious editing experience. Next Launch Week we're hoping to decide if it's pronounced “Jason” or “Jay-sawn”. Stay tuned.
#### Nullable columns
Speaking of extremely common feature requests, we've gotten this one a lot in the past few months. You used to have to handle this manually, but now you can now allow text/boolean cells to be null or empty. Supabase, where productivity is more than just an empty (or null) promise.
## Wrapping Up
We hope you get a lot of value out of these new features and enhancements. As we mentioned earlier, many of the features listed here came directly from [Feature Requests](https://github.com/orgs/supabase/discussions/categories/feature-requests) on GitHub. Thanks to everyone who have taken the time to submit these, and encourage submissions for anything else you'd like to see.
+5 -5
View File
@@ -22,15 +22,15 @@ const Favicons = () => {
{/* prettier-ignore */}
<link rel="apple-touch-icon-precomposed" sizes="152x152" href={`${basePath}/favicon/apple-touch-icon-152x152.png`}/>
{/* prettier-ignore */}
<link rel="icon" type="image/png" href={`${basePath}/favicon/favicon-196x196.png" sizes="196x196`}/>
<link rel="icon" type="image/png" href={`${basePath}/favicon/favicon-196x196.png`} sizes="196x196"/>
{/* prettier-ignore */}
<link rel="icon" type="image/png" href={`${basePath}/favicon/favicon-96x96.png" sizes="96x96`}/>
<link rel="icon" type="image/png" href={`${basePath}/favicon/favicon-96x96.png`} sizes="96x96"/>
{/* prettier-ignore */}
<link rel="icon" type="image/png" href={`${basePath}/favicon/favicon-32x32.png" sizes="32x32`}/>
<link rel="icon" type="image/png" href={`${basePath}/favicon/favicon-32x32.png`} sizes="32x32"/>
{/* prettier-ignore */}
<link rel="icon" type="image/png" href={`${basePath}/favicon/favicon-16x16.png" sizes="16x16`}/>
<link rel="icon" type="image/png" href={`${basePath}/favicon/favicon-16x16.png`} sizes="16x16"/>
{/* prettier-ignore */}
<link rel="icon" type="image/png" href={`${basePath}/favicon/favicon-128.png" sizes="128x128`}/>
<link rel="icon" type="image/png" href={`${basePath}/favicon/favicon-128.png`} sizes="128x128"/>
<meta name="application-name" content="&nbsp;" />
<meta name="msapplication-TileColor" content="#1E1E1E" />
<meta name="msapplication-TileImage" content="mstile-144x144.png" />
+81
View File
@@ -0,0 +1,81 @@
import Image from 'next/image'
import React, { useState, useEffect } from 'react'
import { IconPauseCircle, IconPlayCircle } from 'ui'
import { cn } from '~/../../packages/ui/src/utils/cn'
interface ImageFadeStackProps {
autoplay?: boolean
images: string[]
height?: 'default'
delay?: number
altText?: string
showNavigation?: boolean
}
const ImageFadeStack = ({
images,
height = 'default',
delay = 3000,
autoplay = false,
altText = 'Image',
showNavigation = false,
}: ImageFadeStackProps) => {
const [currentImageIndex, setCurrentImageIndex] = useState(0)
const [isPlaying, setIsPlaying] = useState(autoplay)
useEffect(() => {
if (isPlaying) {
const intervalId = setInterval(() => {
setCurrentImageIndex((prevIndex) => (prevIndex + 1) % images.length)
}, delay)
return () => clearInterval(intervalId)
}
}, [images, isPlaying])
function handleNavClick(index: number) {
setIsPlaying(false)
setCurrentImageIndex(index)
}
return (
<div className={`relative ${height === 'default' ? 'h-[300px]' : 'h-[400px]'} my-6`}>
{images.map((image, index) => (
<>
<Image
className="absolute top-0 left-0"
key={index}
src={image}
layout="fill"
alt={altText}
style={{
opacity: currentImageIndex === index ? 1 : 0,
transition: 'opacity 1s ease-in-out',
}}
/>
</>
))}
<div className="absolute bottom-4 right-4 flex gap-3">
{showNavigation &&
images.map((_, index) => (
<button className="text-white" onClick={() => handleNavClick(index)}>
<div
className={cn(
'w-2 h-2 rounded-full bg-scale-900 hover:bg-gray-300 transition-colors',
currentImageIndex === index && 'bg-scale-700'
)}
/>
</button>
))}
<button
onClick={() => setIsPlaying(!isPlaying)}
className="text-white bg-scale-1100 hover:bg-gray-1100 dark:hover:bg-gray-500 rounded-full p-0.5 transition-colors"
>
{isPlaying ? <IconPauseCircle w={12} /> : <IconPlayCircle w={12} />}
</button>
</div>
</div>
)
}
export default ImageFadeStack
+65 -57
View File
@@ -13,68 +13,76 @@ const BlogLinks = () => {
<div className="flex flex-col gap-3 lg:gap-4 border-t border-scale-400 py-4 lg:py-8 mt-4 lg:mt-8">
<h3 className="text-black dark:text-white text-xl mb-4">More Launch Week 7</h3>
{activeDays.map((day) =>
day.steps?.map((step) => (
<motion.div
className={`h-auto w-full flex flex-col lg:flex-row relative overflow-hidden`}
initial="default"
animate="default"
whileHover="hover"
>
<Link
href={
(step.blog || step.github || step.hackernews || step.blog || step.docs) as string
}
>
<a
className={`
flex flex-col flex-1 gap-3 items-start justify-center border rounded-xl h-full relative overflow-hidden
p-6 lg:p-10 text-2xl bg-[#1C1C1C]
before:absolute before:w-full before:h-full before:top-52 before:right-0 before:bottom-0 before:left-0
before:border-[#1f3536] before:-z-10
`}
>
<div className="relative z-10 flex items-center text-lg flex-col-reverse lg:flex-row lg:justify-start gap-2 text-black dark:text-white">
<div
className={[
'text-transparent bg-clip-text bg-gradient-to-r text-base from-[#F4FFFA] to-[#B7B2C9] drop-shadow-lg',
step.break_thumb_title && 'max-w-[240px]',
].join(' ')}
>
{step.title}
</div>
</div>
{step.thumb && (
<motion.div
className="absolute inset-0 w-full h-full z-0"
variants={hoverVariant}
>
<Image
src={step.thumb}
className={`
absolute
w-full h-full -z-10 transition-all duration-300
`}
layout="fill"
objectPosition="100% 50%"
objectFit="cover"
/>
</motion.div>
)}
<div
className={`absolute opacity-10 w-full h-full -z-10 transition-all duration-300`}
style={{
background: `radial-gradient(650px 150px at 50% 100%, #4635A730, transparent)`,
}}
/>
</a>
</Link>
</motion.div>
))
day.steps?.map((step, i) => {
return (
<>
{!step.hideInBlog && <StepCard step={step} key={i} />}
{step.steps?.map(
(nestedStep, i) => !nestedStep.hideInBlog && <StepCard step={nestedStep} key={i} />
)}
</>
)
})
)}
</div>
)
}
const StepCard = ({ step }: { step: any }) => (
<motion.div
className={`h-auto w-full flex flex-col lg:flex-row relative overflow-hidden`}
initial="default"
animate="default"
whileHover="hover"
>
<Link
href={
(step.blog || step.github || step.hackernews || step.blog || step.docs || '#') as string
}
>
<a
className={`
flex flex-col flex-1 gap-3 items-start justify-center border rounded-xl h-full relative overflow-hidden
p-6 lg:p-10 text-2xl bg-[#1C1C1C]
before:absolute before:w-full before:h-full before:top-52 before:right-0 before:bottom-0 before:left-0
before:border-[#1f3536] before:-z-10
`}
>
<div className="relative z-10 flex items-center text-lg flex-col-reverse lg:flex-row lg:justify-start gap-2 text-black dark:text-white">
<div
className={[
'text-transparent bg-clip-text bg-gradient-to-r text-base from-[#F4FFFA] to-[#B7B2C9] drop-shadow-lg',
step.break_thumb_title && 'max-w-[240px]',
].join(' ')}
>
{step.title}
</div>
</div>
{step.thumb && (
<motion.div className="absolute inset-0 w-full h-full z-0" variants={hoverVariant}>
<Image
src={step.thumb}
className={`
absolute
w-full h-full -z-10 transition-all duration-300
`}
layout="fill"
objectPosition="100% 50%"
objectFit="cover"
/>
</motion.div>
)}
<div
className={`absolute opacity-10 w-full h-full -z-10 transition-all duration-300`}
style={{
background: `radial-gradient(650px 150px at 50% 100%, #4635A730, transparent)`,
}}
/>
</a>
</Link>
</motion.div>
)
const defaultEase = [0.5, 0, 0.35, 1]
const hoverVariant = {
default: { scale: 1, ease: defaultEase, duration: 0.2 },
@@ -37,7 +37,7 @@ function CountdownBanner() {
<div>
<Link href="/launch-week">
<a className="bg-white text-xs px-1.5 md:px-2.5 py-1 rounded-full text-[#9E44EF] shadow-none hover:shadow-mg cursor-pointer">
Now live
Explore
</a>
</Link>
</div>
@@ -1,11 +1,11 @@
import { motion, useScroll, useTransform } from 'framer-motion'
import Image from 'next/image'
import { useMobileViewport } from '../../hooks/useMobileViewport'
import { useBreakpoint } from 'common/hooks/useBreakpoint'
import styles from './lw7-bg-graphic.module.css'
export default function LW7BgGraphic() {
const { scrollYProgress } = useScroll()
const isMobile = useMobileViewport(768)
const isMobile = useBreakpoint(768)
const graphicsY = useTransform(
scrollYProgress,
@@ -1,10 +1,10 @@
import classNames from 'classnames'
import Image from 'next/image'
import { motion } from 'framer-motion'
import { useMobileViewport } from '../../../hooks/useMobileViewport'
import { useBreakpoint } from 'common/hooks/useBreakpoint'
export function LaunchWeekLogoHeader() {
const isMobile = useMobileViewport(640)
const isMobile = useBreakpoint(640)
return (
<div className="flex flex-col gap-1 md:gap-2 items-center my-4 lg:pt-8 justify-start">
@@ -1,7 +1,7 @@
import Link from 'next/link'
import React from 'react'
import { Button } from 'ui'
import { useMobileViewport } from '../../../hooks/useMobileViewport'
import { useBreakpoint } from 'common/hooks/useBreakpoint'
import { UserData } from '../Ticket/hooks/use-conf-data'
import { TicketBrickWallSlider } from './TicketBrickWallSlider'
@@ -12,7 +12,7 @@ interface Props {
}
export default function TicketBrickWall({ users }: Props) {
const isMobile = useMobileViewport(768)
const isMobile = useBreakpoint(768)
return (
<div className="relative pb-20 pt-28 md:pt-40">
<div className="mx-auto bg-[#1C1C1C]">
@@ -0,0 +1,742 @@
import React from 'react'
import Image from 'next/image'
import { motion } from 'framer-motion'
import { useBreakpoint } from 'common/hooks/useBreakpoint'
import {
ArrowTopRightSvg,
CartTitle,
ChipLink,
MultistepSectionHeader,
SectionButtons,
StyledArticleBadge,
} from './components'
import { WeekDayProps } from '~/components/LaunchWeek/lw7_days'
import { opacityVariant4, scaleOpacityVariant2 } from './LW7Releases'
import styles from './day5.module.css'
import Link from 'next/link'
const getDay5Motion = (index: number) => {
switch (index) {
case 0:
return scaleOpacityVariant2
default:
return undefined
}
}
const getDay5omt01Motion = (index: number) => {
switch (index) {
case 2:
return opacityVariant4
default:
return undefined
}
}
const getDay5omt02Motion = (index: number) => {
switch (index) {
case 2:
return opacityVariant4
default:
return undefined
}
}
const Day5 = ({ day }: { day: WeekDayProps }) => {
const isMobile = useBreakpoint(767)
const isTablet = useBreakpoint(1023)
const isDesktop = useBreakpoint(1279)
return (
<>
{/* Community */}
<div className="h-auto flex flex-col gap-5 lg:flex-row">
<motion.div
className={`
relative overflow-hidden flex-1 flex flex-col items-center gap-5 lg:items-start justify-between
w-full border rounded-xl px-4 sm:px-8 lg:px-14 py-10 xs:text-2xl text-xl sm:text-2xl md:text-xl text-center shadow-lg
min-h-[400px]
`}
initial="default"
animate="default"
whileHover="hover"
>
<div className="flex items-center text-center lg:text-left justify-between flex-col lg:flex-row lg:justify-start gap-3 text-white">
<CartTitle>{day.steps[0].steps[0].title}</CartTitle>
<StyledArticleBadge className="lg:ml-2">New</StyledArticleBadge>
</div>
<SectionButtons blog={day.steps[0].steps[0].blog} />
{day.steps[0]?.steps[0]?.bg_layers &&
day.steps[0].steps[0].bg_layers?.map(
(layer, i) =>
!!layer.img && (
<motion.div
className={['absolute inset-0 w-full h-full -z-10'].join(' ')}
variants={getDay5Motion(i)}
>
<Image
src={!!layer.mobileImg && isTablet ? (layer.mobileImg as any) : layer.img}
className={[`absolute w-full h-full -z-10 transition-all duration-300`].join(
' '
)}
layout="fill"
objectPosition={!!layer.mobileImg && isTablet ? '50% 50%' : '50% 50%'}
objectFit="cover"
quality={100}
/>
</motion.div>
)
)}
</motion.div>
</div>
{/* Studio */}
<MultistepSectionHeader title={day.steps[1].title} blog={day.steps[1].blog} />
<div
className={[
'h-auto flex flex-col sm:grid grid-cols-2 md:grid-cols-4 xl:grid-cols-6 gap-5 lg:flex-row',
styles['day5-grid'],
].join(' ')}
>
<motion.div
className={`
relative overflow-hidden flex-1 flex flex-col items-center gap-5 md:items-start justify-between
col-span-full grid-ro w-full border rounded-xl px-4 sm:px-8 lg:px-14 py-10 text-xl sm:text-2xl md:text-xl text-center shadow-lg
min-h-[350px] md:min-h-[220px]
`}
initial="default"
animate="default"
whileHover="hover"
style={{ gridArea: 'commandK' }}
>
<Link href={`${day.steps[1].blog}${day.steps[1].steps[0].url}`}>
<a className="absolute inset-0 z-10" />
</Link>
<div className="flex items-center text-center md:text-left justify-between flex-col md:flex-row lg:justify-start gap-3 text-white">
<CartTitle>{day.steps[1].steps[0].title}</CartTitle>
<div className="flex gap-2">
<StyledArticleBadge className="lg:ml-2">
{day.steps[1].steps[4].badge}
</StyledArticleBadge>
<ChipLink
uiOnly
className="!min-h-[28px] !min-w-[28px] !p-0 !items-center !justify-center"
>
<ArrowTopRightSvg />
</ChipLink>
</div>
</div>
{day.steps[1].steps[0].bg_layers &&
day.steps[1].steps[0].bg_layers?.map(
(layer, i) =>
!!layer.img && (
<motion.div
className={['absolute inset-0 w-full h-full -z-10'].join(' ')}
variants={getDay5Motion(i)}
>
<Image
src={!!layer.mobileImg && isMobile ? (layer.mobileImg as any) : layer.img}
className={[`absolute w-full h-full -z-10 transition-all duration-300`].join(
' '
)}
layout="fill"
objectPosition={!!layer.mobileImg && isMobile ? '50% 50%' : '80% 50%'}
objectFit="cover"
quality={100}
/>
</motion.div>
)
)}
</motion.div>
<motion.div
className={`
relative overflow-hidden flex-1 flex flex-col items-center gap-5 md:items-start justify-between text-xl sm:text-2xl md:text-xl shadow-lg
w-full border rounded-xl px-4 sm:px-8 lg:px-14 py-10 text-center
min-h-[350px] md:min-h-[220px]
`}
initial="default"
animate="default"
whileHover="hover"
style={{ gridArea: 'wrappers' }}
>
<Link href={`${day.steps[1].blog}${day.steps[1].steps[1].url}`}>
<a className="absolute inset-0 z-10" />
</Link>
<div className="flex items-center text-center md:text-left justify-between flex-col md:items-start gap-3 text-white">
<CartTitle>{day.steps[1].steps[1].title}</CartTitle>
<div className="flex gap-2">
<StyledArticleBadge>{day.steps[1].steps[1].badge}</StyledArticleBadge>
<ChipLink
uiOnly
className="!min-h-[28px] !min-w-[28px] !p-0 !items-center !justify-center"
>
<ArrowTopRightSvg />
</ChipLink>
</div>
</div>
{day.steps[1].steps[1].bg_layers &&
day.steps[1].steps[1].bg_layers?.map(
(layer, i) =>
!!layer.img && (
<motion.div
className={['absolute inset-0 w-full h-full -z-10'].join(' ')}
variants={getDay5Motion(i)}
>
<Image
src={!!layer.mobileImg && isMobile ? (layer.mobileImg as any) : layer.img}
className={[`absolute w-full h-full -z-10 transition-all duration-300`].join(
' '
)}
layout="fill"
objectPosition={!!layer.mobileImg && isMobile ? '50% 50%' : '80% 50%'}
objectFit="cover"
quality={100}
/>
</motion.div>
)
)}
</motion.div>
<motion.div
className={`
relative overflow-hidden flex-1 flex flex-col items-center gap-5 justify-between text-xl sm:text-2xl md:text-xl shadow-lg
w-full border rounded-xl px-4 py-10 text-center
min-h-[350px] md:min-h-[220px]
`}
initial="default"
animate="default"
whileHover="hover"
style={{ gridArea: 'nullable' }}
>
<Link href={`${day.steps[1].blog}${day.steps[1].steps[2].url}`}>
<a className="absolute inset-0 z-10" />
</Link>
<div className="flex flex-col items-center text-center gap-3 text-white">
<CartTitle>{day.steps[1].steps[2].title}</CartTitle>
<div className="flex gap-2">
<StyledArticleBadge className="lg:ml-2">
{day.steps[1].steps[2].badge}
</StyledArticleBadge>
<ChipLink
uiOnly
className="!min-h-[28px] !min-w-[28px] !p-0 !items-center !justify-center"
>
<ArrowTopRightSvg />
</ChipLink>
</div>
</div>
{day.steps[1].steps[2].bg_layers &&
day.steps[1].steps[2].bg_layers?.map(
(layer, i) =>
!!layer.img && (
<motion.div
className={['absolute inset-0 w-full h-full -z-10'].join(' ')}
variants={getDay5Motion(i)}
>
<Image
src={!!layer.mobileImg && isMobile ? (layer.mobileImg as any) : layer.img}
className={[`absolute w-full h-full -z-10 transition-all duration-300`].join(
' '
)}
layout="fill"
objectPosition={isMobile ? '50% 70%' : '50% 50%'}
objectFit="cover"
quality={100}
/>
</motion.div>
)
)}
</motion.div>
<motion.div
className={`
relative overflow-hidden flex-1 flex flex-col items-center gap-5 justify-between text-xl sm:text-2xl md:text-xl shadow-lg
w-full border rounded-xl px-4 sm:px-8 py-10 text-center
min-h-[350px] md:min-h-[220px]
`}
initial="default"
animate="default"
whileHover="hover"
style={{ gridArea: 'apiAutodocs' }}
>
<Link href={`${day.steps[1].blog}${day.steps[1].steps[3].url}`}>
<a className="absolute inset-0 z-10" />
</Link>
<div className="flex items-center text-center flex-col gap-3 text-white">
<CartTitle>{day.steps[1].steps[3].title}</CartTitle>
<div className="flex gap-2">
<StyledArticleBadge className="lg:ml-2">
{day.steps[1].steps[3].badge}
</StyledArticleBadge>
<ChipLink
uiOnly
className="!min-h-[28px] !min-w-[28px] !p-0 !items-center !justify-center"
>
<ArrowTopRightSvg />
</ChipLink>
</div>
</div>
{day.steps[1].steps[3].bg_layers &&
day.steps[1].steps[3].bg_layers?.map(
(layer, i) =>
!!layer.img && (
<motion.div
className={['absolute inset-0 w-full h-full -z-10'].join(' ')}
variants={getDay5Motion(i)}
>
<Image
src={!!layer.mobileImg && isTablet ? (layer.mobileImg as any) : layer.img}
className={[`absolute w-full h-full -z-10 transition-all duration-300`].join(
' '
)}
layout="fill"
objectPosition={isMobile ? '50% 90%' : '50% 50%'}
objectFit="cover"
quality={100}
/>
</motion.div>
)
)}
</motion.div>
<motion.div
className={`
relative overflow-hidden flex-1 flex flex-col items-center gap-5 lg:items-start justify-between text-xl sm:text-2xl md:text-xl shadow-lg
w-full border rounded-xl px-4 sm:px-8 lg:px-14 py-10 text-center
h-auto min-h-[400px] md:min-h-[600px] xl:min-h-full
`}
initial="default"
animate="default"
whileHover="hover"
style={{ gridArea: 'pgRoles' }}
>
<Link href={`${day.steps[1].blog}${day.steps[1].steps[4].url}`}>
<a className="absolute inset-0 z-10" />
</Link>
<div className="flex items-center text-center lg:text-left justify-between flex-col lg:flex-row lg:justify-start gap-3 lg:gap-2 text-white">
<CartTitle>{day.steps[1].steps[4].title}</CartTitle>
<div className="flex gap-2">
<StyledArticleBadge className="lg:ml-2">
{day.steps[1].steps[4].badge}
</StyledArticleBadge>
<ChipLink
uiOnly
className="!min-h-[28px] !min-w-[28px] !p-0 !items-center !justify-center"
>
<ArrowTopRightSvg />
</ChipLink>
</div>
</div>
{day.steps[1].steps[4].bg_layers &&
day.steps[1].steps[4].bg_layers?.map(
(layer, i) =>
!!layer.img && (
<motion.div
className={['absolute inset-0 w-full h-full -z-10'].join(' ')}
variants={getDay5Motion(i)}
>
<Image
src={!!layer.mobileImg && isTablet ? (layer.mobileImg as any) : layer.img}
className={[`absolute w-full h-full -z-10 transition-all duration-300`].join(
' '
)}
layout="fill"
objectPosition={!!layer.mobileImg && isTablet ? '50% 50%' : '50% 50%'}
objectFit="cover"
quality={100}
/>
</motion.div>
)
)}
</motion.div>
<motion.div
className={`
relative overflow-hidden flex-1 flex flex-col items-center gap-5 lg:items-start justify-between text-xl sm:text-2xl md:text-xl shadow-lg
w-full border rounded-xl px-4 sm:px-8 lg:px-14 py-10 text-center
h-auto min-h-[400px] md:min-h-[600px] xl:min-h-full
`}
initial="default"
animate="default"
whileHover="hover"
style={{ gridArea: 'casDeletes' }}
>
<Link href={`${day.steps[1].blog}${day.steps[1].steps[5].url}`}>
<a className="absolute inset-0 z-10" />
</Link>
<div className="flex items-center text-center lg:text-left justify-between flex-col lg:flex-row lg:justify-start gap-3 text-white">
<CartTitle>{day.steps[1].steps[5].title}</CartTitle>
<div className="flex gap-2">
<StyledArticleBadge className="lg:ml-2">
{day.steps[1].steps[5].badge}
</StyledArticleBadge>
<ChipLink
uiOnly
className="!min-h-[28px] !min-w-[28px] !p-0 !items-center !justify-center"
>
<ArrowTopRightSvg />
</ChipLink>
</div>
</div>
{day.steps[1].steps[5].bg_layers &&
day.steps[1].steps[5].bg_layers?.map(
(layer, i) =>
!!layer.img && (
<motion.div
className={['absolute inset-0 w-full h-full -z-10'].join(' ')}
variants={getDay5Motion(i)}
>
<Image
src={!!layer.mobileImg && isTablet ? (layer.mobileImg as any) : layer.img}
className={[`absolute w-full h-full -z-10 transition-all duration-300`].join(
' '
)}
layout="fill"
objectPosition={!!layer.mobileImg && isTablet ? '50% 50%' : '50% 50%'}
objectFit="cover"
quality={100}
/>
</motion.div>
)
)}
</motion.div>
<motion.div
className={`
relative overflow-hidden flex-1 flex flex-col items-center gap-5 md:items-start justify-between text-xl sm:text-2xl md:text-xl shadow-lg
w-full border rounded-xl px-4 sm:px-8 lg:px-14 py-10 text-center
min-h-[350px] md:min-h-[220px]
`}
initial="default"
animate="default"
whileHover="hover"
style={{ gridArea: 'graphiQL' }}
>
<Link href={`${day.steps[1].blog}${day.steps[1].steps[6].url}`}>
<a className="absolute inset-0 z-10" />
</Link>
<div className="flex items-center text-center md:text-left justify-between flex-col md:flex-row md:justify-start gap-3 text-white">
<CartTitle>{day.steps[1].steps[6].title}</CartTitle>
<div className="flex gap-2">
<StyledArticleBadge className="md:ml-2">
{day.steps[1].steps[6].badge}
</StyledArticleBadge>
<ChipLink
uiOnly
className="!min-h-[28px] !min-w-[28px] !p-0 !items-center !justify-center"
>
<ArrowTopRightSvg />
</ChipLink>
</div>
</div>
{day.steps[1].steps[6].bg_layers &&
day.steps[1].steps[6].bg_layers?.map(
(layer, i) =>
!!layer.img && (
<motion.div
className={['absolute inset-0 w-full h-full -z-10'].join(' ')}
variants={getDay5Motion(i)}
>
<Image
src={!!layer.mobileImg && isTablet ? (layer.mobileImg as any) : layer.img}
className={[`absolute w-full h-full -z-10 transition-all duration-300`].join(
' '
)}
layout="fill"
objectPosition={isMobile ? '0% 50%' : isDesktop ? '100% 50%' : '0% 50%'}
objectFit={isMobile ? 'cover' : isDesktop ? 'contain' : 'cover'}
quality={100}
/>
</motion.div>
)
)}
</motion.div>
<motion.div
className={`
relative overflow-hidden flex-1 flex flex-col items-center gap-5 lg:items-start justify-between text-xl sm:text-2xl md:text-xl shadow-lg
w-full border rounded-xl px-4 sm:px-8 lg:px-14 py-10 text-center
min-h-[350px] md:min-h-[220px]
`}
initial="default"
animate="default"
whileHover="hover"
style={{ gridArea: 'dbWebhooks' }}
>
<Link href={`${day.steps[1].blog}${day.steps[1].steps[7].url}`}>
<a className="absolute inset-0 z-10" />
</Link>
<div className="flex items-center text-center lg:text-left justify-between flex-col lg:flex-row lg:justify-start gap-3 text-white">
<CartTitle>{day.steps[1].steps[7].title}</CartTitle>
<div className="flex gap-2">
<StyledArticleBadge className="lg:ml-2">
{day.steps[1].steps[7].badge}
</StyledArticleBadge>
<ChipLink
uiOnly
className="!min-h-[28px] !min-w-[28px] !p-0 !items-center !justify-center"
>
<ArrowTopRightSvg />
</ChipLink>
</div>
</div>
{day.steps[1].steps[7].bg_layers &&
day.steps[1].steps[7].bg_layers?.map(
(layer, i) =>
!!layer.img && (
<motion.div
className={['absolute inset-0 w-full h-full -z-10'].join(' ')}
variants={getDay5Motion(i)}
>
<Image
src={!!layer.mobileImg && isTablet ? (layer.mobileImg as any) : layer.img}
className={[`absolute w-full h-full -z-10 transition-all duration-300`].join(
' '
)}
layout="fill"
objectPosition={!!layer.mobileImg && isTablet ? '50% 50%' : '50% 50%'}
objectFit="cover"
quality={100}
/>
</motion.div>
)
)}
</motion.div>
<motion.div
className={`
relative overflow-hidden flex-1 flex flex-col items-center gap-5 justify-between text-xl sm:text-2xl md:text-xl shadow-lg
w-full border rounded-xl px-4 sm:px-8 lg:px-14 py-10 text-center
min-h-[350px] md:min-h-[220px]
`}
initial="default"
animate="default"
whileHover="hover"
style={{ gridArea: 'viewsTables' }}
>
<Link href={`${day.steps[1].blog}${day.steps[1].steps[9].url}`}>
<a className="absolute inset-0 z-10" />
</Link>
<div className="flex items-center text-center flex-col gap-3 text-white">
<CartTitle>{day.steps[1].steps[8].title}</CartTitle>
<div className="flex gap-2">
<StyledArticleBadge className="lg:ml-2">
{day.steps[1].steps[8].badge}
</StyledArticleBadge>
<ChipLink
uiOnly
className="!min-h-[28px] !min-w-[28px] !p-0 !items-center !justify-center"
>
<ArrowTopRightSvg />
</ChipLink>
</div>
</div>
{day.steps[1].steps[8].bg_layers &&
day.steps[1].steps[8].bg_layers?.map(
(layer, i) =>
!!layer.img && (
<motion.div
className={['absolute inset-0 w-full h-full -z-10'].join(' ')}
variants={getDay5Motion(i)}
>
<Image
src={!!layer.mobileImg && isTablet ? (layer.mobileImg as any) : layer.img}
className={[`absolute w-full h-full -z-10 transition-all duration-300`].join(
' '
)}
layout="fill"
objectPosition={!!layer.mobileImg && isTablet ? '50% 50%' : '50% 50%'}
objectFit="cover"
quality={100}
/>
</motion.div>
)
)}
</motion.div>
<motion.div
className={`
relative overflow-hidden flex-1 flex flex-col items-center gap-5 justify-between text-xl sm:text-2xl md:text-xl shadow-lg
w-full border rounded-xl px-4 sm:px-8 py-10 text-center
min-h-[350px] md:min-h-[220px]
`}
initial="default"
animate="default"
whileHover="hover"
style={{ gridArea: 'JSONsupport' }}
>
<Link href={`${day.steps[1].blog}${day.steps[1].steps[9].url}`}>
<a className="absolute inset-0 z-10" />
</Link>
<div className="flex items-center text-center flex-col gap-3 text-white">
<CartTitle>{day.steps[1].steps[9].title}</CartTitle>
<div className="flex gap-2">
<StyledArticleBadge className="lg:ml-2">
{day.steps[1].steps[9].badge}
</StyledArticleBadge>
<ChipLink
uiOnly
className="!min-h-[28px] !min-w-[28px] !p-0 !items-center !justify-center"
>
<ArrowTopRightSvg />
</ChipLink>
</div>
</div>
{day.steps[1].steps[9].bg_layers &&
day.steps[1].steps[9].bg_layers?.map(
(layer, i) =>
!!layer.img && (
<motion.div
className={['absolute inset-0 w-full h-full -z-10'].join(' ')}
variants={getDay5Motion(i)}
>
<Image
src={!!layer.mobileImg && isTablet ? (layer.mobileImg as any) : layer.img}
className={[`absolute w-full h-full -z-10 transition-all duration-300`].join(
' '
)}
layout="fill"
objectPosition={!!layer.mobileImg && isTablet ? '50% 50%' : '50% 50%'}
objectFit="cover"
quality={100}
/>
</motion.div>
)
)}
</motion.div>
<motion.div
className={`
relative overflow-hidden flex-1 flex flex-col items-center gap-5 lg:items-start justify-between text-xl sm:text-2xl md:text-xl shadow-lg
w-full border rounded-xl px-4 sm:px-8 lg:px-14 py-10 text-center
min-h-[350px] md:min-h-[220px]
`}
initial="default"
animate="default"
whileHover="hover"
style={{ gridArea: 'insights' }}
>
<Link href={`${day.steps[1].blog}${day.steps[1].steps[10].url}`}>
<a className="absolute inset-0 z-10" />
</Link>
<div className="flex items-center text-center lg:text-left justify-between flex-col lg:flex-row lg:justify-start gap-3 text-white">
<CartTitle>{day.steps[1].steps[10].title}</CartTitle>
<div className="flex gap-2">
<StyledArticleBadge className="lg:ml-2">
{day.steps[1].steps[10].badge}
</StyledArticleBadge>
<ChipLink
uiOnly
className="!min-h-[28px] !min-w-[28px] !p-0 !items-center !justify-center"
>
<ArrowTopRightSvg />
</ChipLink>
</div>
</div>
{day.steps[1].steps[10].bg_layers &&
day.steps[1].steps[10].bg_layers?.map(
(layer, i) =>
!!layer.img && (
<motion.div
className={['absolute inset-0 w-full h-full -z-10'].join(' ')}
variants={getDay5Motion(i)}
>
<Image
src={!!layer.mobileImg && isTablet ? (layer.mobileImg as any) : layer.img}
className={[`absolute w-full h-full -z-10 transition-all duration-300`].join(
' '
)}
layout="fill"
objectPosition={!!layer.mobileImg && isTablet ? '50% 50%' : '50% 50%'}
objectFit="cover"
quality={100}
/>
</motion.div>
)
)}
</motion.div>
</div>
{/* One More Thing */}
<MultistepSectionHeader title={day.steps[2].title} />
<div className="h-auto flex flex-col md:grid md:grid-cols-3 gap-5 lg:flex-row">
<motion.div
className={`
relative overflow-hidden flex-1 flex flex-col items-center gap-5 md:items-start justify-between
grid-ro w-full border rounded-xl px-4 sm:px-8 lg:px-14 py-10 text-xl sm:text-2xl md:text-xl text-center shadow-lg
min-h-[400px] md:min-h-[250px] col-span-2
`}
initial="default"
animate="default"
whileHover="hover"
>
<div className="flex items-center text-center md:text-left justify-between flex-col md:flex-row lg:justify-start gap-3 text-white">
<CartTitle>{day.steps[2].steps[0].title}</CartTitle>
<StyledArticleBadge className="lg:ml-2">
{day.steps[2].steps[0].badge}
</StyledArticleBadge>
</div>
<SectionButtons blog={day.steps[2].steps[0].blog} />
{day.steps[2].steps[0].bg_layers &&
day.steps[2].steps[0].bg_layers?.map(
(layer, i) =>
!!layer.img && (
<motion.div
className={[
'absolute inset-0 w-full h-full -z-10',
i === 2 && '!mix-blend-overlay blur-2xl',
].join(' ')}
variants={getDay5omt01Motion(i)}
>
<Image
src={!!layer.mobileImg && isMobile ? (layer.mobileImg as any) : layer.img}
className={[`absolute w-full h-full -z-10 transition-all duration-300`].join(
' '
)}
layout="fill"
objectPosition={!!layer.mobileImg && isMobile ? '50% 50%' : '80% 50%'}
objectFit="cover"
quality={100}
/>
</motion.div>
)
)}
</motion.div>
<motion.div
className={`
relative overflow-hidden flex-1 flex flex-col items-center gap-5 md:items-start justify-between text-xl sm:text-2xl md:text-xl shadow-lg
w-full border rounded-xl px-4 sm:px-8 lg:px-14 py-10 text-center
min-h-[400px] md:min-h-[250px]
`}
initial="default"
animate="default"
whileHover="hover"
>
<div className="flex items-center text-center md:text-left justify-between flex-col md:flex-row lg:justify-start gap-3 text-white">
<CartTitle>{day.steps[2].steps[1].title}</CartTitle>
<StyledArticleBadge>{day.steps[2].steps[1].badge}</StyledArticleBadge>
</div>
<SectionButtons blog={day.steps[2].steps[1].blog} />
{day.steps[2].steps[1].bg_layers &&
day.steps[2].steps[1].bg_layers?.map(
(layer, i) =>
!!layer.img && (
<motion.div
className={[
'absolute inset-0 w-full h-full -z-10',
i === 2 && '!mix-blend-overlay opacity-50 blur-2xl',
].join(' ')}
variants={getDay5omt02Motion(i)}
>
<Image
src={!!layer.mobileImg && isMobile ? (layer.mobileImg as any) : layer.img}
className={[`absolute w-full h-full -z-10 transition-all duration-300`].join(
' '
)}
layout="fill"
objectPosition={
i == 1 && !!layer.mobileImg && isMobile ? '50% 60%' : '80% 50%'
}
objectFit={i == 1 && !!layer.mobileImg && isMobile ? 'contain' : 'cover'}
quality={100}
/>
</motion.div>
)
)}
</motion.div>
</div>
</>
)
}
export default Day5
@@ -15,14 +15,15 @@ import {
StyledArticleBadge,
} from './components'
import { useMobileViewport } from '../../../../hooks/useMobileViewport'
import { useBreakpoint } from 'common/hooks/useBreakpoint'
import { motion } from 'framer-motion'
import Day5 from './Day5'
const defaultEase = [0.25, 0.25, 0, 1]
const defaultDuratonIn = 0.25
const defaultDuratonOut = 0.1
export const defaultEase = [0.25, 0.25, 0, 1]
export const defaultDuratonIn = 0.25
export const defaultDuratonOut = 0.1
const opacityVariant = {
export const opacityVariant = {
default: { opacity: 0.9, ease: defaultEase, duration: defaultDuratonOut },
hover: {
opacity: 1,
@@ -32,7 +33,7 @@ const opacityVariant = {
},
},
}
const opacityVariant2 = {
export const opacityVariant2 = {
default: { opacity: 0, ease: defaultEase, duration: defaultDuratonOut },
hover: {
opacity: 0.2,
@@ -42,7 +43,7 @@ const opacityVariant2 = {
},
},
}
const opacityVariant3 = {
export const opacityVariant3 = {
default: { opacity: 0, ease: defaultEase, duration: defaultDuratonOut },
hover: {
opacity: 1,
@@ -52,7 +53,7 @@ const opacityVariant3 = {
},
},
}
const opacityVariant4 = {
export const opacityVariant4 = {
default: { opacity: 0.3, ease: defaultEase, duration: defaultDuratonOut },
hover: {
opacity: 1,
@@ -62,7 +63,7 @@ const opacityVariant4 = {
},
},
}
const scaleOpacityVariant = {
export const scaleOpacityVariant = {
default: { scale: 1, opacity: 0.9, ease: defaultEase, duration: defaultDuratonOut },
hover: {
scale: 1.05,
@@ -73,13 +74,24 @@ const scaleOpacityVariant = {
},
},
}
const aiImageMobileVariant = {
export const scaleOpacityVariant2 = {
default: { scale: 1, opacity: 0.8, ease: defaultEase, duration: defaultDuratonOut },
hover: {
scale: 1.025,
opacity: 1,
transition: {
duration: defaultDuratonIn,
ease: defaultEase,
},
},
}
export const aiImageMobileVariant = {
default: { scale: 0.9 },
hover: {
scale: 0.9,
},
}
const moveX10 = {
export const moveX10 = {
default: { x: 0, ease: defaultEase, duration: defaultDuratonOut },
hover: {
x: 10,
@@ -132,8 +144,7 @@ const getDay4Motion = (index: number) => {
export default function LW7Releases() {
const [preRelease, day1, day2, day3, day4, day5] = days
const isMobile = useMobileViewport(767)
const isTablet = useMobileViewport(1023)
const isTablet = useBreakpoint(1023)
const showAll = false
const publishedSections =
days
@@ -183,13 +194,13 @@ export default function LW7Releases() {
</svg>
</div>
<div className="flex flex-col lg:flex-row ml-2 sm:ml-4">
<span className="text-white mr-2">Supabase AI Hackathon has begun. Join now!</span>
<span className="text-white mr-2">Supabase AI Hackathon</span>
</div>
</div>
<div className="flex w-full sm:w-auto justify-center gap-2 z-10">
<ChipLink href={'/blog/launch-week-7-hackathon'}>
Blog post
<div className="bg-[#313131] rounded-full inline-block p-1 ml-2">
<div className="bg-[#313131] rounded-full hidden sm:inline-block p-1 ml-2">
<PencilSvg />
</div>
</ChipLink>
@@ -227,7 +238,7 @@ export default function LW7Releases() {
</div>
</div>
<div className="flex w-full sm:w-auto justify-center gap-2 z-10">
<ChipLink href="#lw-7-prizes" className="pr-3 !justify-center">
<ChipLink href="#lw-7-prizes" className="!pr-3 !justify-center">
More info
</ChipLink>
</div>
@@ -412,7 +423,6 @@ export default function LW7Releases() {
<StyledArticleBadge className="lg:ml-2">New</StyledArticleBadge>
</div>
<SectionButtons
docs={day1.steps[0].docs}
blog={day1.steps[0].blog}
video={day1.steps[0].video}
hackernews={day1.steps[0].hackernews}
@@ -500,7 +510,6 @@ export default function LW7Releases() {
<StyledArticleBadge className="lg:ml-2">New</StyledArticleBadge>
</div>
<SectionButtons
docs={day2.steps[0].docs}
blog={day2.steps[0].blog}
video={day2.steps[0].video}
hackernews={day2.steps[0].hackernews}
@@ -744,7 +753,7 @@ export default function LW7Releases() {
className="h-[79px]"
id={day5.d.toString()}
>
<div></div>
<Day5 day={day5} />
</Accordion.Item>
</div>
</Accordion>
@@ -47,7 +47,6 @@ export const LinkSvg = () => (
<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M6.875 4.0013H4.20833C3.47195 4.0013 2.875 4.59826 2.875 5.33464V12.0013C2.875 12.7377 3.47195 13.3346 4.20833 13.3346H10.875C11.6114 13.3346 12.2083 12.7377 12.2083 12.0013V9.33464M9.54167 2.66797H13.5417M13.5417 2.66797V6.66797M13.5417 2.66797L6.875 9.33464"
// stroke="#A69DC9"
stroke="#8F8F8F"
strokeWidth="1"
strokeLinecap="round"
@@ -55,6 +54,22 @@ export const LinkSvg = () => (
/>
</svg>
)
export const ArrowTopRightSvg = () => (
<svg width="17" height="17" viewBox="0 0 17 17" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M3.44531 13.846L13.1152 4.17578"
stroke="#8F8F8F"
strokeMiterlimit="10"
strokeLinejoin="bevel"
/>
<path
d="M3.44531 4.09375H13.1152V13.7637"
stroke="#8F8F8F"
strokeMiterlimit="10"
strokeLinejoin="bevel"
/>
</svg>
)
export const HackernewsSvg = () => (
<svg width="16" height="17" viewBox="0 0 16 17" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
@@ -142,31 +157,60 @@ export const AccordionHeader = ({ date, day, title, shipped }: any) => {
</div>
)
}
export const MultistepSectionHeader = ({ title, blog }: any) => {
return (
<div className="flex flex-1 flex-col sm:flex-row py-8">
<div className="flex gap-4 w-full items-center justify-between md:justify-start">
{title && <span className="text-scale-1200 text-lg mt-3 sm:mt-0">{title}</span>}
{!!blog && (
<ChipLink href={blog} className="!w-auto !text-left !justify-between !flex-none">
Blog post
<div className="bg-[#313131] rounded-full hidden sm:inline-block p-1 ml-2">
<PencilSvg />
</div>
</ChipLink>
)}
</div>
</div>
)
}
export const ChipLink = ({
href,
uiOnly,
className,
target,
children,
}: {
href: string
href?: string
className?: string
uiOnly?: boolean
target?: '_blank' | '_self' | '_parent' | '_top' | 'framename'
children: any
}) => (
<Link href={href}>
<a
target={target ?? '_self'}
rel="noopener"
}) =>
uiOnly ? (
<span
className={[
'flex justify-between w-full min-h-[43px] sm:w-auto items-center border border-[#232323] bg-gradient-to-r text-white from-[#46444490] to-[#19191980] hover:from-[#4e4e4e90] hover:to-[#19191980] backdrop-blur-xl rounded-full text-sm py-2 pl-3 pr-2',
'flex flex-auto justify-center sm:justify-between w-full text-center sm:text-left min-h-[43px] sm:w-auto items-center border border-[#232323] bg-gradient-to-r text-white from-[#46444490] to-[#19191980] hover:from-[#4e4e4e90] hover:to-[#19191980] backdrop-blur-xl rounded-full text-sm py-2 px-3 sm:pr-2',
className,
].join(' ')}
>
{children}
</a>
</Link>
)
</span>
) : !!href ? (
<Link href={href}>
<a
target={target ?? '_self'}
rel="noopener"
className={[
'flex flex-auto justify-center sm:justify-between w-full text-center sm:text-left min-h-[43px] sm:w-auto items-center border border-[#232323] bg-gradient-to-r text-white from-[#46444490] to-[#19191980] hover:from-[#4e4e4e90] hover:to-[#19191980] backdrop-blur-xl rounded-full text-sm py-2 px-3 sm:pr-2',
className,
].join(' ')}
>
{children}
</a>
</Link>
) : null
export const SectionButtons = ({
blog,
@@ -188,14 +232,14 @@ export const SectionButtons = ({
return (
<div
className={[
'flex w-full md:w-auto justify-center gap-2 z-10',
'flex w-full max-w-full md:w-auto justify-center gap-2 z-10',
mobileGrid && 'grid grid-cols-2 gap-2 sm:flex',
].join(' ')}
>
{!!blog && (
<ChipLink href={blog}>
Blog post
<div className="bg-[#313131] rounded-full inline-block p-1 ml-2">
<div className="bg-[#313131] rounded-full hidden sm:inline-block p-1 ml-2">
<PencilSvg />
</div>
</ChipLink>
@@ -203,7 +247,7 @@ export const SectionButtons = ({
{!!docs && (
<ChipLink href={docs}>
Docs
<div className="bg-[#313131] rounded-full inline-block p-1 ml-2">
<div className="bg-[#313131] rounded-full hidden sm:inline-block p-1 ml-2">
<DocsSvg />
</div>
</ChipLink>
@@ -211,15 +255,15 @@ export const SectionButtons = ({
{!!video && (
<ChipLink href={video} target="_blank">
Video
<div className="bg-[#313131] rounded-full inline-block p-1 ml-2">
<div className="bg-[#313131] rounded-full hidden sm:inline-block p-1 ml-2">
<PlaySvg />
</div>
</ChipLink>
)}
{!!github && (
<ChipLink href={github} target="_blank">
View on Github
<div className="bg-[#313131] rounded-full inline-block p-1 ml-2">
Github
<div className="bg-[#313131] rounded-full hidden sm:inline-block p-1 ml-2">
<GithubSvg />
</div>
</ChipLink>
@@ -227,15 +271,15 @@ export const SectionButtons = ({
{!!url && (
<ChipLink href={url}>
Read
<div className="bg-[#313131] rounded-full inline-block p-1 ml-2">
<div className="bg-[#313131] rounded-full hidden sm:inline-block p-1 ml-2">
<LinkSvg />
</div>
</ChipLink>
)}
{hackernews && (
<ChipLink href={hackernews} target="_blank">
Read more
<div className="bg-[#313131] rounded-full inline-block p-1 ml-2">
Hackernews
<div className="bg-[#313131] rounded-full hidden sm:inline-block p-1 ml-2">
<HackernewsSvg />
</div>
</ChipLink>
Loaded 100 of 410 files, more files were not shown because too many files have changed in this diff. Show more