diff --git a/apps/docs/components/DocSearch.tsx b/apps/docs/components/DocSearch.tsx index 52fc0956ecd..d8bde374580 100644 --- a/apps/docs/components/DocSearch.tsx +++ b/apps/docs/components/DocSearch.tsx @@ -74,11 +74,14 @@ export function SearchProvider({ children }: any) { appId={APP_ID} // @ts-ignore navigator={{ - navigate({ suggestionUrl }) { + navigate({ itemUrl }) { setIsOpen(false) - router.push(suggestionUrl) + router.push(itemUrl) }, }} + getMissingResultsUrl={({ query }) => { + return `https://github.com/supabase/supabase/issues/new?title=Unable+to+search+docs+with+query:+${query}` + }} hitComponent={Hit} transformItems={(items) => { return items.map((item, index) => { diff --git a/apps/docs/data/nav/supabase-dart/v1.ts b/apps/docs/data/nav/supabase-dart/v1.ts index fb1491e2555..8fe2fad4791 100644 --- a/apps/docs/data/nav/supabase-dart/v1.ts +++ b/apps/docs/data/nav/supabase-dart/v1.ts @@ -118,6 +118,11 @@ const Nav = [ url: '/reference/dart/storage-from-createsignedurl', items: [], }, + { + name: 'from.createSignedUrls()', + url: '/reference/dart/storage-from-createsignedurls', + items: [], + }, { name: 'from.getPublicUrl()', url: '/reference/dart/storage-from-getpublicurl', diff --git a/apps/docs/docs/reference/dart/initializing.mdx b/apps/docs/docs/reference/dart/initializing.mdx index 00a69bbf1f3..7d914d71efd 100644 --- a/apps/docs/docs/reference/dart/initializing.mdx +++ b/apps/docs/docs/reference/dart/initializing.mdx @@ -28,6 +28,33 @@ Once you initialize Supabase in your `main()` method, you can access the `Supaba final supabase = Supabase.instance.client; ``` +### Call `initialize()` with custom headers + +You can pass `headers` to initialize your Supabase client with customer headers. +Here is an example of passing a custom auth header to Supabase client. +```dart title=main.dart +Future main() async { + await Supabase.initialize( + url: 'https://xyzcompany.supabase.co', + anonKey: 'public-anon-key', + headers: { + 'Authorization': 'Bearer $accessToken', + }, + ); + runApp(const MyApp()); +} +``` + +### Call `initialize()`multiple times + +You can initialize Supabase multiple times in your app. When doing so, call `dispose()` each time before you initialize again. + +```dart +Supabase.instance.dispose(); + +await Supabase.initialize(url: 'https://xyzcompany.supabase.co', anonKey: 'public-anon-key'); +``` + ## Other Dart Projects You can initialize a new Supabase client using the `SupabaseClient()` method. diff --git a/apps/docs/docs/reference/javascript/v1/initializing.mdx b/apps/docs/docs/reference/javascript/v1/initializing.mdx index 46d057e1cca..3ad13aa9f13 100644 --- a/apps/docs/docs/reference/javascript/v1/initializing.mdx +++ b/apps/docs/docs/reference/javascript/v1/initializing.mdx @@ -117,7 +117,7 @@ const supabase = createClient('https://xyzcompany.supabase.co', 'public-anon-key ``` By default the API server points to the `public` schema. You can enable other database schemas within the Dashboard. -Go to `Settings > API > Schema` and add the schema which you want to expose to the API. +Go to `Settings > API > Schema` and add the schema which you want to expose to the API. You also need to grant `USAGE` on your new schema with the grants you desire, such as `SELECT, INSERT, UPDATE, DELETE`. Note: each client connection can only access a single schema, so the code above can access the `other_schema` schema but cannot access the `public` schema. diff --git a/apps/docs/package.json b/apps/docs/package.json index cdad7d64c77..7a6df1f501a 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -36,7 +36,7 @@ "@algolia/autocomplete-js": "^1.7.2", "@algolia/autocomplete-plugin-recent-searches": "^1.7.2", "@apollo/client": "^3.6.6", - "@docsearch/react": "1.0.0-alpha.27", + "@docsearch/react": "^3.3.0", "@mdx-js/loader": "^1.6.22", "@mdx-js/react": "^1.6.22", "@next/mdx": "^12.0.4", diff --git a/apps/docs/pages/guides/auth.mdx b/apps/docs/pages/guides/auth.mdx index 75e514e5b7e..71986837e96 100644 --- a/apps/docs/pages/guides/auth.mdx +++ b/apps/docs/pages/guides/auth.mdx @@ -55,31 +55,43 @@ You can enable third-party providers with the click of a button by navigating to When using third-party providers, the [Supabase client library](/docs/reference/javascript/auth-signinwithoauth#sign-in-using-a-third-party-provider-with-redirect) redirects the user to the provider. When the third-party provider successfully authenticates the user, the provider redirects the user to the Supabase Auth callback URL where they are further redirected to the URL specified in the `redirectTo` parameter. This parameter defaults to the [`SITE_URL`](/docs/reference/auth/config#site_url). You can modify the `SITE_URL` or [add additional redirect URLs](https://app.supabase.com/project/_/auth/settings). -You can use wildcard match patterns to support preview URLs from providers like Netlify and Vercel. See the [full list of supported patterns](https://pkg.go.dev/github.com/gobwas/glob#Compile). +You can use wildcard match patterns to support preview URLs from providers like Netlify and Vercel. See the [full list of supported patterns](https://pkg.go.dev/github.com/gobwas/glob#Compile). Use [this tool](https://www.digitalocean.com/community/tools/glob?comments=true&glob=http%3A%2F%2Flocalhost%3A3000%2F%2A%2A&matches=false&tests=http%3A%2F%2Flocalhost%3A3000&tests=http%3A%2F%2Flocalhost%3A3000%2F&tests=http%3A%2F%2Flocalhost%3A3000%2F%3Ftest%3Dtest&tests=http%3A%2F%2Flocalhost%3A3000%2Ftest-test%3Ftest%3Dtest&tests=http%3A%2F%2Flocalhost%3A3000%2Ftest%2Ftest%3Ftest%3Dtest) to test your patterns. + +> ⚠️ WARNING: While the "globstar" (`**`) is useful for local development and preview URLs, we recommend setting the exact redirect URL path for your site URL in production. #### Netlify preview URLs For deployments with Netlify, set the `SITE_URL` to your official site URL. Add the following additional redirect URLs for local development and deployment previews: -- `http://localhost:3000/*/*` -- `https://**--my_org.netlify.app/*` +- `http://localhost:3000/**` +- `https://**--my_org.netlify.app/**` #### Vercel preview URLs For deployments with Vercel, set the `SITE_URL` to your official site URL. Add the following additional redirect URLs for local development and deployment previews: -- `http://localhost:3000/*/*` -- `https://**vercel.app/*/*` +- `http://localhost:3000/**` +- `https://**.vercel.app/**` Vercel provides an environment variable for the URL of the deployment called `NEXT_PUBLIC_VERCEL_URL`. See the [Vercel docs](https://vercel.com/docs/concepts/projects/environment-variables#system-environment-variables) for more details. You can use this variable to dynamically redirect depending on the environment: ```js +const getURL = () => { + let url = + process?.env?.NEXT_PUBLIC_SITE_URL ?? // Set this to your site URL in production env. + process?.env?.NEXT_PUBLIC_VERCEL_URL ?? // Automatically set by Vercel. + 'http://localhost:3000/'; + // Make sure to include `https://` when not localhost. + url = url.includes('http') ? url : `https://${url}`; + // Make sure to including trailing `/`. + url = url.charAt(url.length - 1) === '/' ? url : `${url}/`; + return url; +}; + const { data, error } = await supabase.auth.signInWithOAuth({ provider: 'github' options: { - redirectTo: process.env.NEXT_PUBLIC_VERCEL_URL - ? `https://${process.env.NEXT_PUBLIC_VERCEL_URL}` - : "http://localhost:3000" + redirectTo: getURL() } } ``` diff --git a/apps/docs/pages/guides/database.mdx b/apps/docs/pages/guides/database.mdx index d557ac7a79e..7efa2ad7513 100644 --- a/apps/docs/pages/guides/database.mdx +++ b/apps/docs/pages/guides/database.mdx @@ -85,7 +85,7 @@ Read about resetting your database password [here](/docs/guides/database/managin ## Next steps -- Read more about [Postgres](/docs/postgres/server/about) +- Read more about [Postgres](https://www.postgresql.org/about/) - Sign in: [app.supabase.com](https://app.supabase.com) export const Page = ({ children }) => diff --git a/apps/docs/scripts/build-search.js b/apps/docs/scripts/build-search.js index 489354b56e5..91e69c3fae8 100644 --- a/apps/docs/scripts/build-search.js +++ b/apps/docs/scripts/build-search.js @@ -4,6 +4,7 @@ const path = require('path') const matter = require('gray-matter') const dotenv = require('dotenv') const algoliasearch = require('algoliasearch/lite') +const { isEmpty } = require('lodash') // [Joshen] We initially thought of building out our search using Algolia directly, // but eventually decided to just use DocSearch since it provides us a UI and some @@ -17,12 +18,20 @@ const algoliasearch = require('algoliasearch/lite') // A lot of them are not even linked to within the docs site, so just need to // double check if they can be removed or if we want them in the side bars. const ignoredFiles = [ - 'docs/404.mdx', - 'docs/support.mdx', - 'docs/faqs.mdx', - 'docs/guides.mdx', - 'docs/guides/database/arrays.mdx', - 'docs/guides/database/json.mdx', + 'pages/404.mdx', + 'pages/faq.mdx', + 'pages/support.mdx', + 'pages/oss.tsx', + 'pages/_app.tsx', + 'pages/_document.tsx', + 'pages/[...slug].tsx', + 'pages/company/aup.md', + 'pages/company/privacy.md', + 'pages/company/sla.md', + 'pages/company/terms.md', + 'pages/handbook/contributing.mdx', + 'pages/handbook/introduction.mdx', + 'pages/handbook/supasquad.mdx', ] const nameMap = { @@ -69,16 +78,39 @@ async function walk(dir) { ) const index = client.initIndex(indexName) - const slugs = (await walk('docs')).filter((slug) => !ignoredFiles.includes(slug)) + const referencePages = await walk('docs') + const guidePages = (await walk('pages')).filter((slug) => !ignoredFiles.includes(slug)) + const allPages = guidePages.concat(referencePages) - const searchObjects = slugs + const searchObjects = allPages .map((slug) => { + let id, title, description const fileContents = fs.readFileSync(slug, 'utf8') const { data, content } = matter(fileContents) - const { id, title, description } = data + if (isEmpty(data)) { + // Guide pages do not have front-matter meta, unlike reference pages, have to manually extract + const metaIndex = fileContents.indexOf('export const meta = {') + if (metaIndex !== -1) { + const metaString = + fileContents + .slice(metaIndex + 20, fileContents.indexOf('}') + 1) + .replace(/\n/g, '') + .slice(0, -2) + '}' + const meta = eval(`(${metaString})`) + id = meta.id + title = meta.title + description = meta.description + } + } else { + id = data.id + title = data.title + description = data.description + } + const url = (slug.includes('/generated') ? slug.replace('/generated', '') : slug) .replace('docs', '') + .replace('pages', '') .replace(/\.mdx$/, '') const source = slug.includes('/reference') ? 'reference' : 'guide' const object = { diff --git a/apps/docs/styles/docsearch.scss b/apps/docs/styles/docsearch.scss index 6ae9773850b..a9ca46569c5 100644 --- a/apps/docs/styles/docsearch.scss +++ b/apps/docs/styles/docsearch.scss @@ -384,8 +384,25 @@ @apply text-slate-1200 dark:text-scale-1200 font-normal; } -.DocSearch-StartScreen .DocSearch-Help { - @apply py-16 px-6 text-center text-slate-400; +.DocSearch-NoResults-Prefill-List { + .DocSearch-Help { + color: var(--colors-scale11) !important; + } +} + +.DocSearch-StartScreen { + .DocSearch-Help { + @apply text-center py-16; + } +} + +.DocSearch-Help { + @apply text-scale-1100; + font-size: 0.875rem !important; + a { + color: var(--colors-brand9) !important; + font-weight: 400 !important; + } } .DocSearch-NoResults { diff --git a/apps/www/_blog/2022-11-22-flutter-authentication-and-authorization-with-rls.mdx b/apps/www/_blog/2022-11-22-flutter-authentication-and-authorization-with-rls.mdx index c76f1e18ffd..7183c9fb539 100644 --- a/apps/www/_blog/2022-11-22-flutter-authentication-and-authorization-with-rls.mdx +++ b/apps/www/_blog/2022-11-22-flutter-authentication-and-authorization-with-rls.mdx @@ -52,7 +52,7 @@ flutter_bloc: ^8.0.0 Since the app has evolved, we also need to update our table schema. In order to store rooms data, we will add a rooms table. We will also modify the messages table to add a foreign key constraint to the rooms table so that we can tell which message belongs to which room. -We will also intorudce a `create_new_room` function, which is a [database function](https://supabase.com/docs/guides/database/functions) that handles chat room creation. It knows to create a new room if a chat room with the two users does not exist yet, or to just return the room ID if it already exists. +We will also introduce a `create_new_room` function, which is a [database function](https://supabase.com/docs/guides/database/functions) that handles chat room creation. It knows to create a new room if a chat room with the two users does not exist yet, or to just return the room ID if it already exists. ```sql -- *** Table definitions *** @@ -1154,7 +1154,7 @@ import 'package:my_chat_app/pages/rooms_page.dart'; import 'package:my_chat_app/utils/constants.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; -/// Page to redirect users to the appropreate page depending on the initial auth state +/// Page to redirect users to the appropriate page depending on the initial auth state class SplashPage extends StatefulWidget { const SplashPage({Key? key}) : super(key: key); diff --git a/apps/www/_blog/2022-11-24-sql-or-nosql-both-with-postgresql.mdx b/apps/www/_blog/2022-11-24-sql-or-nosql-both-with-postgresql.mdx new file mode 100644 index 00000000000..5198e23f3df --- /dev/null +++ b/apps/www/_blog/2022-11-24-sql-or-nosql-both-with-postgresql.mdx @@ -0,0 +1,336 @@ +--- +title: SQL or NoSQL? Why not use both (with PostgreSQL)? +description: How to turn Postgres into an easy-to-use NoSQL database that retains all the power of SQL +author: burggraf +image: 2022-11-24-sql-nosql-postgresql/sql-nosql-postgresql.jpg +thumb: 2022-11-24-sql-nosql-postgresql/sql-nosql-postgresql.jpg +tags: + - postgres +date: '2022-11-24' +toc_depth: 3 +--- + +It's a tough decision for any developer starting a new project. Should you store your data in a standard, time-tested SQL database, or go with one of the newer NoSQL document-based databases? This seemingly simple decision can literally make or break your project down the line. Choose correctly and structure your data well, and you may sail smoothly into production and watch your app take off. Make the wrong choice and you could be headed for nightmares (and maybe even some major re-writes) before your app ever makes it out the door. + +## Simplicity vs Power +There are tradeoffs with both SQL and NoSQL solutions. Typically it's easier to get started with NoSQL data structures, especially when the data is complex or hierarchical. You can just take a JSON data object from your front-end code and throw it in the database and be done with it. But later when you need to access that data to answer some basic business questions, it's much more difficult. A SQL solution makes it easier to gather data and draw conclusions down the line. Let's look at an example: + +Each day I track the food I eat, along with the number of calories in each item: + +| Day | Food Item | Calories | Meal | +|--------|-----------|----------|------| +| 01 Jan | Apple | 72 | Breakfast | +| 01 Jan | Oatmeal | 146 | Breakfast | +| 01 Jan | Sandwich | 445 | Lunch | +| 01 Jan | Chips | 280 | Lunch | +| 01 Jan | Cookie | 108 | Lunch | +| 01 Jan | Mixed Nuts | 175 | Snack | +| 01 Jan | Pasta/Sauce | 380 | Dinner | +| 01 Jan | Garlic Bread | 200 | Dinner | +| 01 Jan | Broccoli | 32 | Dinner | + +I also track the number of cups of water I drink and when I drink them: + +| Day | Time | Cups | +|--------|-------|------| +| Jan 01 | 08:15 | 1 | +| Jan 01 | 09:31 | 1 | +| Jan 01 | 10:42 | 2 | +| Jan 01 | 12:07 | 2 | +| Jan 01 | 14:58 | 1 | +| Jan 01 | 17:15 | 1 | +| Jan 01 | 18:40 | 1 | +| Jan 01 | 19:05 | 1 | + +And finally, I track my exercise: + +| Day | Time | Duration | Exercise | +|--------|-------|----------|----------| +| Jan 01 | 11:02 | 0.5 | Walking | +| Jan 02 | 09:44 | 0.75 | Bicycling | +| Jan 02 | 17:00 | 0.25 | Walking | + +For each day I also track my current weight along with any notes for the day: + +| Day | Weight | Notes | +|--------|--------|-------| +| Jan 01 | 172.6 | This new diet is awesome! | +| Jan 14 | 170.2 | Not sure all this is worth it. | +| Jan 22 | 169.8 | Jogged past a McDonald's today. It was hard. | +| Feb 01 | 168.0 | I feel better, but sure miss all that greasy food. | + +## Gathering All That Data + +That's a lot of different data that needs to be gathered, stored, retrieved, and later analyzed. It's organized simply and easily, but the number of records varies from day to day. On any given day I may have zero or more entries for food, water, and exercise, and I may have zero or one entry for weight & notes. + +In my app, I gather all the data for a single day on one page, to make it easier for my users. So, I get a JSON object for each day that looks like this: + +```json +{ "date": "2022-01-01", + "weight": 172.6, + "notes": "This new diet is awesome!", + "food": [ + { "title": "Apple", "calories": 72, "meal": "Breakfast"}, + { "title": "Oatmeal", "calories": 146, "meal": "Breakfast"}, + { "title": "Sandwich", "calories": 445, "meal": "Lunch"}, + { "title": "Chips", "calories": 280, "meal": "Lunch"}, + { "title": "Cookie", "calories": 108, "meal": "Lunch"}, + { "title": "Mixed Nuts", "calories": 175, "meal": "Snack"}, + { "title": "Pasta/Sauce", "calories": 380, "meal": "Dinner"}, + { "title": "Garlic Bread", "calories": 200, "meal": "Dinner"}, + { "title": "Broccoli", "calories": 32, "meal": "Dinner"} + ], + "water": [ + {"time": "08:15", "qty": 1}, + {"time": "09:31", "qty": 1}, + {"time": "10:42", "qty": 2}, + {"time": "10:42", "qty": 2}, + {"time": "12:07", "qty": 1}, + {"time": "14:58", "qty": 1}, + {"time": "17:15", "qty": 1}, + {"time": "18:40", "qty": 1}, + {"time": "19:05", "qty": 1} + ], + "exercise": [ + {"time": "11:02", "duration": 0.5, "type": "Walking"} + ] +} +``` + +## Saving the Data + +Once we've gathered all the data for a day, we need to store it in our database. In a NoSQL database, this can be a pretty easy process, as we can just create a record (document) for a specific user for a specific date and throw document into a collection and we're done. With SQL, we have some structure we have to work within, and in this case it looks like 4 separate tables: food, water, exercise, and notes. We'd want to do 4 separate inserts here, one for each table. If we don't have data for a specific table (say no exercise was recorded today) then we can skip that table. + +If you're using SQL to store this data, you might want to save each table's data as it's entered in your data entry form (and not wait until all the data is entered.) Or you might want to create a database function that takes all the JSON data, parses it, and writes it to all the related tables in a single transaction. There's a lot of ways to handle this, but suffice it to say this: it's a bit more complicated than saving the data in a NoSQL database. + +### Retrieving the Data +If we want to display all the data for a single day, it's pretty much the same. With NoSQL you can grab the data for the user's day and then use it in your application. Nice! With SQL we need to query 4 tables to get all the data (or we could use a function to get it all in a single call.) Of course, when displaying the data, we'd need to first break up our JSON data into pieces that are needed by each section of our dashboard screen, and you could argue that it's simpler to map each SQL table with the dashboard section on the screen, but that's a trivial point. + +### Analyzing the Data +Now that we've saved the data and we can retrieve it and display it, let's use it for some analysis. *Let's display a graph of how many total calories I've eaten over the past month.* With SQL, this is a simple task: + +```sql +select + date, sum(calories) as total_calories +from food_log +group by date +where user_id = 'xyz' +and day between '2022-01-01' and '2022-01-31' +order by date; +``` + +Bam! Done! Now we can send those results to our graphing library and make a nice pretty picture of my eating habits. + +But if we've stored this data in NoSQL, it gets a little more complicated. We'll need to: + +- grab all the data for the user for the month +- parse each day's data to get the food log information +- loop through each day and total the calories +- send the aggregate data to our graphing module + +If this is something we're going to do regularly, it makes sense to calculate the total calories for each day and store it in the day's document so we can get at that data faster. But that requires more work up front, and we still need to pull the data for each day and parse out that calorie total first. And if we update the data we still need to recalculate things and update that total. Eventually we'll want to do that with the water and exercise totals as well. The code will eventually start to get longer and more complex. + +### SQL and NoSQL Together - FTW +Let's see how we can use the power of SQL together with the ease-of-use of NoSQL in the same database to make this all a bit easier. We'll create a table for each day of data (for each user) and store the basic fields such as `weight` and `notes` first. Then we'll just throw the `food_log`, `water_log`, and `exercise_log` fields in a `JSONB` field. + +```sql +CREATE TABLE calendar ( + id uuid DEFAULT uuid_generate_v4() NOT NULL, + date date, + user_id uuid NOT NULL, + weight numeric, + notes text, + food_log jsonb, + water_log jsonb, + exercise_log jsonb +); +-- (Optional) - create a foreign key relationship for the user_id field +ALTER TABLE ONLY calendar + ADD CONSTRAINT calendar_user_id_fkey FOREIGN KEY (user_id) REFERENCES auth.users(id); +``` + +Now let's insert some data into the table. PostgreSQL offers both JSON and JSONB fields, and since the latter are more optimized by the database and much faster for query processing, we’ll almost always want to use JSONB. We’ll use JSONB fields for `food_log`, `water_log`, and `exercise_log` and just dump the data we got from our app right into those fields as a string: + +```sql +insert into calendar (date, user_id, weight, notes, food_log, water_log, exercise_log) +values ( + '2022-01-01', + 'xyz', + 172.6, + 'This new diet is awesome!', + '[ + { "title": "Apple", "calories": 72, "meal": "Breakfast"}, + { "title": "Oatmeal", "calories": 146, "meal": "Breakfast"}, + { "title": "Sandwich", "calories": 445, "meal": "Lunch"}, + { "title": "Chips", "calories": 280, "meal": "Lunch"}, + { "title": "Cookie", "calories": 108, "meal": "Lunch"}, + { "title": "Mixed Nuts", "calories": 175, "meal": "Snack"}, + { "title": "Pasta/Sauce", "calories": 380, "meal": "Dinner"}, + { "title": "Garlic Bread", "calories": 200, "meal": "Dinner"}, + { "title": "Broccoli", "calories": 32, "meal": "Dinner"} + ]', + '[ + {"time": "08:15", "qty": 1}, + {"time": "09:31", "qty": 1}, + {"time": "10:42", "qty": 2}, + {"time": "10:42", "qty": 2}, + {"time": "12:07", "qty": 1}, + {"time": "14:58", "qty": 1}, + {"time": "17:15", "qty": 1}, + {"time": "18:40", "qty": 1}, + {"time": "19:05", "qty": 1} + ]', + '[ + {"time": "11:02", "duration": 0.5, "type": "Walking"} + ]' +); +``` + +While that's a big insert statement, it sure beats doing inserts on 4 separate tables. With all those food entries and water log entries, we would have had to made 1 entry in the main table, then 9 food_log entries, 9 water_log entries, and one exercise_log entry for a total of 20 database records. We've wrapped that into a single record. + +## But How Do We Query This Data? + +Great, we're collecting the data now, and it's easy to insert the data into the database. Editing the data isn't too bad either because we're just downloading the data to the client, updating the JSON field(s) as needed, and throwing them back into the database. Not too hard. But how can I query this data? What about that task from before? *Let's display a graph of how many total calories I've eaten over the past month.* + +In this case, that data is stored inside the `food_log` field inside the `calendar` table. If only PostgreSQL had a way of converting JSONB arrays into individual database records (recordsets). Well, it does! The `jsonb_array_elements` function will do this for us, allowing to create a simple table we can use to calculate our caloric intake. + +Here's some SQL to turn that `food_log` array into individual output records: + +```sql +select + user_id, + date, + jsonb_array_elements(food_log)->>'title' as title, + jsonb_array_elements(food_log)->'calories' as calories, + jsonb_array_elements(food_log)->'meal' as meal +from calendar +where user_id = 'xyz' + and date between + '2022-01-01' and '2022-01-31'; +``` + +This returns a table that looks like this: + +| date | title | calories | meal | +| ---------- | ------------ | -------- | --------- | +| 2022-01-01 | Apple | 72 | Breakfast | +| 2022-01-01 | Oatmeal | 146 | Breakfast | +| 2022-01-01 | Sandwich | 445 | Lunch | +| 2022-01-01 | Chips | 280 | Lunch | +| 2022-01-01 | Cookie | 108 | Lunch | +| 2022-01-01 | Mixed Nuts | 175 | Snack | +| 2022-01-01 | Pasta/Sauce | 380 | Dinner | +| 2022-01-01 | Garlic Bread | 200 | Dinner | +| 2022-01-01 | Broccoli | 32 | Dinner | + +A couple things to note: + +- `jsonb_array_elements(food_log)->>'title' as title` this returns a text field, since the `->>` operator returns TEXT +- `jsonb_array_elements(food_log)->'calories' as calories` this returns a JSON object, since the `->` operator return JSON + +If we want to `sum` the calories to get some totals, we can't have a JSON object, so we need to cast that to something more useful, like an `INTEGER`: + +- `(jsonb_array_elements(food_log)->'calories')::INTEGER as calories` this returns an INTEGER + +Now we can't just throw the `sum` operator on this to get the total calories by day. If we try this: + +```sql +select + date, + sum((jsonb_array_elements(food_log)->'calories')::integer) as total_calories +from calendar where user_id = 'xyz' + and date between '2022-01-01' and '2022-01-31' +group by date; +``` + +we get an error back from PostgreSQL: **Failed to run sql query: aggregate function calls cannot contain set-returning function calls**. + +Instead, we need to think of this as a set of building blocks, where our first SQL statement returns a table: + +```sql +select + date, + (jsonb_array_elements(food_log)->'calories')::integer as calories +from calendar where user_id = 'xyz' + and date between + '2022-01-01' and '2022-01-31'; +``` + +Now we can take that "table" statement, throw some (parenthesis) around it, and query **it**: + +```sql +with data as + ( + select + date, + (jsonb_array_elements(food_log)->'calories')::integer as calories + from calendar + where user_id = 'xyz' + and date between + '2022-01-01' and '2022-01-31' + ) +select date, sum(calories) from + data group by date; +``` + +This gives us exactly what we want: + +| date | sum | +| ---------- | ---- | +| 2022-01-01 | 1838 | + +If we add more data for the rest of the days of the month, we'll have all the data we need for a beautiful graph. + +### Searching the Data +What if we want to answer the question: *How many calories were in the garlic bread I ate last month?* This data is stored inside the `food_log` field in the `calendar` table. We can use the same type of query we used before to "flatten" the `food_log` data so we can search it. + +To get every item I ate during the month of January, we can use: + +```sql +select + date, + jsonb_array_elements(food_log)->>'title' as title, + (jsonb_array_elements(food_log)->'calories')::integer as calories +from calendar +where user_id = 'xyz' + and date between + '2022-01-01' and '2022-01-31' +``` + +Now to search for the **garlic bread** we can just put (parenthesis) around this to make a "table" and then search for the item we want: + +```sql +with my_food as +( + select + date, + jsonb_array_elements(food_log)->>'title' as title, + (jsonb_array_elements(food_log)->'calories')::integer as calories + from calendar + where user_id = 'xyz' + and date between + '2022-01-01' and '2022-01-31' +) +select title, calories +from my_food +where title = 'Garlic Bread'; +``` +which gives us: + +| title | calories | +| ------------ | -------- | +| Garlic Bread | 200 | + +## Conclusion + +If we take a little time to study the [JSON Functions and Operators](https://www.postgresql.org/docs/9.5/functions-json.html) that PostgreSQL offers, we can turn Postgres into an easy-to-use NoSQL database that still retains all the power of SQL. This gives us a super easy way to store our complex JSON data coming from our application code in our database. Then we can use powerful SQL capabilities to analyze and present that data in our application. It's the best of both worlds! + +## More Postgres resources + +- [Postgres Full Text Search vs the rest](https://supabase.com/blog/postgres-full-text-search-vs-the-rest) +- [Postgres WASM by Snaplet and Supabase](https://supabase.com/blog/postgres-wasm) +- [Choosing a Postgres Primary Key](https://supabase.com/blog/choosing-a-postgres-primary-key) +- [Partial data dumps using Postgres Row Level Security](https://supabase.com/blog/partial-postgresql-data-dumps-with-rls) +- [Postgres Views](https://supabase.com/blog/postgresql-views) +- [Realtime Postgres RLS on Supabase](https://supabase.com/blog/realtime-row-level-security-in-postgresql) + diff --git a/apps/www/components/LaunchWeek/Ticket/TicketContainer.tsx b/apps/www/components/LaunchWeek/Ticket/TicketContainer.tsx new file mode 100644 index 00000000000..632c731bfc4 --- /dev/null +++ b/apps/www/components/LaunchWeek/Ticket/TicketContainer.tsx @@ -0,0 +1,55 @@ +import { useState } from 'react' +import { PageState, ConfDataContext, UserData } from './hooks/use-conf-data' +import Ticket from './ticket' +import ConfContainer from './conf-container' +import Form from './form' +import { SupabaseClient, Session } from '@supabase/supabase-js' + +type Props = { + supabase: SupabaseClient + session: Session | null + defaultUserData: UserData + sharePage?: boolean + defaultPageState?: PageState +} + +export default function Conf({ + supabase, + session, + defaultUserData, + sharePage, + defaultPageState = 'registration', +}: Props) { + const [userData, setUserData] = useState(defaultUserData) + const [pageState, setPageState] = useState(defaultPageState) + + return ( + + + {pageState === 'registration' && !sharePage ? ( + <> + {/* */} +
+ {/* */} + + ) : ( + + )} + + + ) +} diff --git a/apps/www/components/LaunchWeek/Ticket/conf-container.module.css b/apps/www/components/LaunchWeek/Ticket/conf-container.module.css new file mode 100644 index 00000000000..2a7eac88ac0 --- /dev/null +++ b/apps/www/components/LaunchWeek/Ticket/conf-container.module.css @@ -0,0 +1,11 @@ +.container { + margin: auto 0; + padding: 0 var(--space-4x); +} + +@media (min-width: 768px) { + .container { + margin: auto; + padding: 0 var(--space-8x); + } +} diff --git a/apps/www/components/LaunchWeek/Ticket/conf-container.tsx b/apps/www/components/LaunchWeek/Ticket/conf-container.tsx new file mode 100644 index 00000000000..e331c3e882c --- /dev/null +++ b/apps/www/components/LaunchWeek/Ticket/conf-container.tsx @@ -0,0 +1,5 @@ +import styles from './conf-container.module.css' + +export default function ConfContainer({ children }: { children: React.ReactNode }) { + return
{children}
+} diff --git a/apps/www/components/LaunchWeek/Ticket/form.module.css b/apps/www/components/LaunchWeek/Ticket/form.module.css new file mode 100644 index 00000000000..fa1a4736cc3 --- /dev/null +++ b/apps/www/components/LaunchWeek/Ticket/form.module.css @@ -0,0 +1,246 @@ +.form { + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + margin-bottom: 12px; + gap: 12px; +} + +.formLeft { + align-items: center; +} +.formCenter { + align-items: center; +} + +@media (min-width: 1200px) { + .formLeft { + align-items: start; + } + .formCenter { + align-items: center; + } +} + +.formInfo { + max-width: 520px; +} + +.formInfo h3 { + font-size: 1.25em; + font-weight: 400; +} + +.formInfo p { + font-size: 1.1em; + line-height: 1.4; + margin-bottom: 32px; + color: var(--lw-secondary-color); +} + +@media (min-width: 1200px) { + /* .form.share-page { + justify-content: flex-start; + } */ +} + +.formInfoLeft { + text-align: center; + margin: 0 auto; +} +.formInfoCenter { + text-align: center; + margin: 0 auto; +} + +@media (min-width: 1200px) { + .formInfoLeft { + text-align: left; + margin: inherit; + } + .formInfoCenter { + text-align: center; + margin: 0 auto; + } +} + +.input { + border: none; + width: 100%; + background: transparent; + outline: none; + height: 56px; + padding-left: 15px; + padding-right: 15px; + font-size: var(--text-md); + color: #fff; + font-family: inherit; +} + +@media (min-width: 768px) { + .input { + width: calc(100% - var(--space-32x)); + padding-right: 0; + } +} + +.input::placeholder { + color: var(--lw-secondary-color); +} + +.input-label { + background-color: var(--gray); + border-radius: var(--space-2x); + border: 1px solid transparent; + transition: border-color 0.2s ease, background-color 0.2s ease; + display: block; +} + +.input-label.focused { + background-color: #33373c; +} + +.input-label.error { + background: red; +} + +.input-label.success { + background: #0070f3; +} + +.input-text { + display: flex; + align-items: center; + width: 100%; +} + +.form-row { + position: relative; + max-width: 400px; + width: 100%; +} + +@media (min-width: 768px) { + .form-row { + max-width: 480px; + } +} + +.submit { + width: 100%; + height: 56px; + margin-top: var(--space-4x); + border-radius: var(--space-2x); + border: 1px solid var(--brand); + background: var(--brand); + cursor: pointer; + font-family: inherit; + font-size: var(--text-md); + letter-spacing: -0.02em; + outline: none; + font-weight: 500; + color: white; + transition: background-color 0.2s ease, color 0.2s ease; + display: inline-flex; + align-items: center; + justify-content: center; +} + +.submit.generate-with-github { + display: flex; + margin-bottom: 12px; + position: relative; +} + +@media (min-width: 768px) { + .submit.register { + width: 120px; + height: 40px; + margin-top: 0; + position: absolute; + border-radius: 5px; + right: var(--space-2x); + top: var(--space-2x); + } +} + +.submit.loading { + cursor: default; +} + +.submit.default:hover, +.submit.default:focus { + background: black; + color: var(--brand); +} + +.submit.default.generate-with-github:hover path, +.submit.default.generate-with-github:focus path { + fill: var(--brand); +} + +.submit.error:hover, +.submit.error:focus { + background: #000; + color: #fff; +} + +.submit.default:disabled, +.submit.default:disabled:hover, +.submit.default:disabled:focus { + cursor: default; + background: var(--gray); + border-color: var(--gray); + color: #fff; + justify-content: flex-start; + overflow: hidden; +} + +.submit.default.generate-with-github:disabled path, +.submit.default.generate-with-github:disabled:hover path, +.submit.default.generate-with-github:disabled:focus path { + fill: #fff; +} + +.submit.default.generate-with-github.not-allowed:disabled { + cursor: not-allowed; +} + +@media (min-width: 1200px) { + .form-row { + margin: 0; + } + + .submit.generate-with-github { + width: 240px; + } +} + +.stage-btn { + background: #323332; + border: 2px solid #34b27b; +} + +.github-wrapper { + display: flex; + align-items: center; + flex-direction: column; +} + +.or-divider { + width: 100%; + text-align: center; + color: var(--lw-secondary-color); + margin: var(--space-4x) 0; +} + +@media (min-width: 1200px) { + .github-wrapper { + flex-direction: row; + } + .or-divider { + width: 240px; + margin: 0; + } +} diff --git a/apps/www/components/LaunchWeek/Ticket/form.tsx b/apps/www/components/LaunchWeek/Ticket/form.tsx new file mode 100644 index 00000000000..6cbe2532aee --- /dev/null +++ b/apps/www/components/LaunchWeek/Ticket/form.tsx @@ -0,0 +1,233 @@ +import { useState, useCallback, useEffect } from 'react' +import cn from 'classnames' +import useConfData from '~/components/LaunchWeek/Ticket/hooks/use-conf-data' +import { useRouter } from 'next/router' +import LoadingDots from './loading-dots' +import styleUtils from './utils.module.css' +import styles from './form.module.css' +import useEmailQueryParam from '~/components/LaunchWeek/Ticket/hooks/use-email-query-param' +// import Captcha, { useCaptcha } from './captcha' + +type FormState = 'default' | 'loading' | 'error' + +export type ConfUser = { + id?: string + email?: string + ticketNumber?: number + name?: string + username?: string + createdAt?: number + golden?: boolean +} + +type Props = { + sharePage?: boolean + align?: 'Left' | 'Center' +} + +export default function Form({ sharePage, align = 'Center' }: Props) { + const [email, setEmail] = useState('') + const [errorMsg, setErrorMsg] = useState('') + const [errorTryAgain, setErrorTryAgain] = useState(false) + const [focused, setFocused] = useState(false) + const [formState, setFormState] = useState('default') + const { setPageState, setUserData, session, userData, supabase } = useConfData() + const router = useRouter() + const isCaptchaEnabled = false + + useEffect(() => { + if (session?.user) { + document.body.classList.add('ticket-generated') + const username = session.user.user_metadata.user_name + const name = session.user.user_metadata.full_name + const email = session.user.email + supabase + .from('lw6_tickets') + .upsert({ email, name, username }, { onConflict: 'email', ignoreDuplicates: false }) + .eq('email', email) + .select() + .single() + .then(async ({ error }) => { + if (error) return supabase.auth.signOut() + const { data } = await supabase + .from('lw6_tickets_golden') + .select('*') + .eq('username', username) + .single() + setUserData(data) + setFormState('default') + + // Prefetch GitHub avatar + new Image().src = `https://github.com/${username}.png` + + // Prefetch the twitter share URL to eagerly generate the page + fetch(`/launch-week/tickets/${username}`).catch((_) => {}) + + setPageState('ticket') + }) + } + }, [session]) + + async function register(email: string, token?: string): Promise { + const { error } = await supabase!.from('lw6_tickets').insert({ email }) + if (error) { + console.log({ error }) + return { + id: 'new', + ticketNumber: 1234, + name: '', + username: '', + golden: false, + } + } + const { data } = await supabase!.from('lw6_tickets_golden').select('*').limit(1).single() + return { + id: data?.id ?? 'new', + ticketNumber: data?.ticketNumber ?? 1234, + name: data?.name ?? '', + username: data?.username ?? '', + golden: data?.golden ?? false, + } + } + + const handleRegister = useCallback( + (token?: string) => { + register(email, token) + .then(async (params) => { + if (!params) { + throw new Error() + } + + if (sharePage) { + const queryString = Object.keys(params) + .map( + (key) => + `${encodeURIComponent(key)}=${encodeURIComponent( + params[key as keyof typeof params] || '' + )}` + ) + .join('&') + await router.replace(`/launch-week/tickets?${queryString}`, '/launch-week/tickets') + } else { + setUserData(params) + setPageState('ticket') + } + }) + .catch(async (err) => { + let message = 'Error! Please try again.' + + setErrorMsg(message) + setFormState('error') + }) + }, + [email, router, setPageState, setUserData, sharePage] + ) + + const onSubmit = useCallback( + (e: React.FormEvent) => { + e.preventDefault() + + if (formState === 'default') { + setFormState('loading') + + if (isCaptchaEnabled) { + // return executeCaptcha() + } + + return handleRegister() + } else { + setFormState('default') + } + }, + [formState, isCaptchaEnabled, handleRegister] + ) + + const onTryAgainClick = useCallback((e: React.MouseEvent) => { + e.preventDefault() + + setFormState('default') + setErrorTryAgain(true) + // resetCaptcha() + }, []) + + useEmailQueryParam('email', setEmail) + + return ( + <> +
+

Get a ticket

+

+ A few of the lucky attendees will get a limited edition Supabase goodie bag. Make sure you + don’t skip your chance. +

+
+ {formState === 'error' ? ( +
+
+
+
{errorMsg}
+ +
+
+
+ ) : ( + +
+ + +
+ {/* */} + + )} + + ) +} diff --git a/apps/www/components/LaunchWeek/Ticket/hooks/use-conf-data.ts b/apps/www/components/LaunchWeek/Ticket/hooks/use-conf-data.ts new file mode 100644 index 00000000000..4855f4f9a56 --- /dev/null +++ b/apps/www/components/LaunchWeek/Ticket/hooks/use-conf-data.ts @@ -0,0 +1,30 @@ +import { Session, SupabaseClient } from '@supabase/supabase-js' +import { createContext, useContext } from 'react' + +export type PageState = 'registration' | 'ticket' + +export type UserData = { + id?: string + ticketNumber?: number + username?: string + name?: string + golden?: boolean +} + +type ConfDataContextType = { + supabase: SupabaseClient + session: Session | null + userData: UserData + setUserData: React.Dispatch> + setPageState: React.Dispatch> +} + +export const ConfDataContext = createContext(null) + +export default function useConfData() { + const result = useContext(ConfDataContext) + if (!result) { + throw new Error() + } + return result +} diff --git a/apps/www/components/LaunchWeek/Ticket/hooks/use-email-query-param.ts b/apps/www/components/LaunchWeek/Ticket/hooks/use-email-query-param.ts new file mode 100644 index 00000000000..16304ef5f79 --- /dev/null +++ b/apps/www/components/LaunchWeek/Ticket/hooks/use-email-query-param.ts @@ -0,0 +1,32 @@ +import { useEffect } from 'react' +import { useRouter } from 'next/router' + +/** + * If `paramName` exists in query string, then call `setEmail()` with the value + * and delete it from the URL. + */ +export default function useEmailQueryParam( + paramName: string, + setEmail: (email: string) => unknown +) { + const router = useRouter() + useEffect(() => { + if ('URLSearchParams' in window) { + const { search, pathname } = window.location + const params = new URLSearchParams(search) + const email = params.get(paramName) + if (email) { + setEmail(email) + params.delete(paramName) + const newSearch = params.toString() + const newAsPath = pathname + (newSearch ? `?${newSearch}` : '') + const newPathname = router.pathname + (newSearch ? `?${newSearch}` : '') + history.replaceState( + { url: newPathname, as: newAsPath, options: { shallow: true } }, + '', + newAsPath + ) + } + } + }, [setEmail, router.pathname, paramName]) +} diff --git a/apps/www/components/LaunchWeek/Ticket/loading-dots.module.css b/apps/www/components/LaunchWeek/Ticket/loading-dots.module.css new file mode 100644 index 00000000000..181f48448e8 --- /dev/null +++ b/apps/www/components/LaunchWeek/Ticket/loading-dots.module.css @@ -0,0 +1,48 @@ +.loading { + display: inline-flex; + align-items: center; + --loading-dots-height: auto; + --loading-dots-size: 2px; + height: var(--loading-dots-height); +} + +.loading .spacer { + margin-right: var(--space-3x); +} + +.loading span { + animation-name: blink; + animation-duration: 1.4s; + animation-iteration-count: infinite; + animation-fill-mode: both; + width: var(--loading-dots-size); + height: var(--loading-dots-size); + border-radius: 50%; + background-color: var(--accents-6); + display: inline-block; + margin: 0 1px; +} + +.loading.reverse span { + background-color: var(--accents-2); +} + +.loading span:nth-of-type(2) { + animation-delay: 0.2s; +} + +.loading span:nth-of-type(3) { + animation-delay: 0.4s; +} + +@keyframes blink { + 0% { + opacity: 0.2; + } + 20% { + opacity: 1; + } + 100% { + opacity: 0.2; + } +} diff --git a/apps/www/components/LaunchWeek/Ticket/loading-dots.tsx b/apps/www/components/LaunchWeek/Ticket/loading-dots.tsx new file mode 100644 index 00000000000..2d4cf1a669e --- /dev/null +++ b/apps/www/components/LaunchWeek/Ticket/loading-dots.tsx @@ -0,0 +1,27 @@ +import { toPixels as px } from '~/lib/helpers' +import cn from 'classnames' +import styles from './loading-dots.module.css' + +interface Props { + size?: number + height?: number | string + reverse?: boolean + children?: React.ReactNode +} + +export default function LoadingDots({ size = 2, height, children, reverse }: Props) { + return ( + + {children &&
{children}
} + + + + + ) +} diff --git a/apps/www/components/LaunchWeek/Ticket/ticket-actions.module.css b/apps/www/components/LaunchWeek/Ticket/ticket-actions.module.css new file mode 100644 index 00000000000..d13c42418d7 --- /dev/null +++ b/apps/www/components/LaunchWeek/Ticket/ticket-actions.module.css @@ -0,0 +1,70 @@ +.button { + border-radius: var(--space-2x); + border: 1px solid #fff; + color: #000; + background: #fff; + cursor: pointer; + font-family: inherit; + font-size: var(--text-md); + letter-spacing: -0.02em; + font-weight: 500; + outline: none; + transition: background-color 0.2s ease; + display: grid; + grid-template-columns: 24px 1fr; + text-align: center; + align-items: center; + padding: 0 var(--space-4x); + height: 55px; + width: 100%; + max-width: 400px; + margin: var(--space-2x) auto; +} + +.loading { + grid-template-columns: 1fr; + justify-items: center; +} + +@media (min-width: 768px) { + .button { + width: 200px; + margin: 0 6px; + } +} + +.button:hover, +.button:focus { + background: black; + color: #fff; +} + +.first { + animation-delay: 2s; +} +.second { + animation-delay: 2.2s; +} +.third { + animation-delay: 2.4s; +} + +:global(.ticket-generated) .first { + animation-delay: 0.6s; +} +:global(.ticket-generated) .second { + animation-delay: 0.8s; +} +:global(.ticket-generated) .third { + animation-delay: 1s; +} + +.linkedin-button { + display: none !important; +} + +@media (min-width: 768px) { + .linkedin-button { + display: grid !important; + } +} diff --git a/apps/www/components/LaunchWeek/Ticket/ticket-actions.tsx b/apps/www/components/LaunchWeek/Ticket/ticket-actions.tsx new file mode 100644 index 00000000000..1a86e3aed8d --- /dev/null +++ b/apps/www/components/LaunchWeek/Ticket/ticket-actions.tsx @@ -0,0 +1,90 @@ +import { useState, useRef, useEffect } from 'react' +import cn from 'classnames' +import { SITE_URL, TWEET_TEXT, TWEET_TEXT_GOLDEN } from '~/lib/constants' +// import IconTwitter from '~/components/LaunchWeek/Ticket/icons/icon-twitter' +// import IconLinkedin from '~/components/LaunchWeek/Ticket/icons/icon-linkedin' +// import IconDownload from '~/components/LaunchWeek/Ticket/icons/icon-download' +import LoadingDots from './loading-dots' +import styleUtils from './utils.module.css' +import styles from './ticket-actions.module.css' +import useConfData from '~/components/LaunchWeek/Ticket//hooks/use-conf-data' + +type Props = { + username: string + golden?: boolean +} + +export default function TicketActions({ username, golden = false }: Props) { + const [imgReady, setImgReady] = useState(false) + const [loading, setLoading] = useState(false) + const downloadLink = useRef() + const permalink = encodeURIComponent(`${SITE_URL}/tickets/${username}`) + const text = encodeURIComponent(golden ? TWEET_TEXT_GOLDEN : TWEET_TEXT) + const { userData } = useConfData() + const tweetUrl = `https://twitter.com/intent/tweet?url=${permalink}&via=supabase&text=${text}` + const linkedInUrl = `https://www.linkedin.com/sharing/share-offsite/?url=${permalink}` + const downloadUrl = `https://obuldanrptloktxcffvn.functions.supabase.co/launchweek-ticket-og?username=${encodeURIComponent( + userData.username ?? '' + )}` + + useEffect(() => { + setImgReady(false) + + const img = new Image() + + img.src = downloadUrl + img.onload = () => { + setImgReady(true) + setLoading(false) + if (downloadLink.current) { + downloadLink.current.click() + downloadLink.current = undefined + } + } + }, [downloadUrl]) + + return ( + <> + + {/* Tweet it! */} + + + {/* Share on LinkedIn */} + + { + if (imgReady) return + + e.preventDefault() + downloadLink.current = e.currentTarget + // Wait for the image download to finish + setLoading(true) + }} + download="ticket.png" + > + {loading ? : <>{/* Download */}} + + + ) +} diff --git a/apps/www/components/LaunchWeek/Ticket/ticket-copy.module.css b/apps/www/components/LaunchWeek/Ticket/ticket-copy.module.css new file mode 100644 index 00000000000..f9a49f205fd --- /dev/null +++ b/apps/www/components/LaunchWeek/Ticket/ticket-copy.module.css @@ -0,0 +1,190 @@ +.wrapper { + color: var(--lw-secondary-color); + display: flex; + align-items: center; + justify-content: center; + animation-delay: 2.6s; + flex-direction: column; +} + +:global(.ticket-generated) .wrapper { + animation-delay: 1.2s; +} + +.label { + width: 100%; + margin-bottom: var(--space-2x); +} + +.label-wrapper { + display: flex; + align-items: center; + max-width: 400px; + width: 100%; + justify-content: space-between; +} + +@media (min-width: 768px) { + .wrapper { + flex-direction: row; + } + + .label { + width: auto; + margin-right: 10px; + margin-bottom: 0px; + } + + .label-wrapper { + width: auto; + } +} + +.field { + position: relative; + border-radius: var(--space-2x); + overflow: hidden; + background: var(--gray); +} + +@media (min-width: 768px) { + .field { + padding-right: 50px; + } + .field.desktop-copy-disabled { + padding-right: 0px; + } +} + +.mobile-copy { + margin-bottom: 10px; + display: flex; +} + +.mobile-copy-disabled.mobile-copy-disabled { + display: none; +} + +.mobile-copy * { + color: var(--lw-secondary-color); + transition: color 0.2s ease; +} + +.mobile-copy:hover * { + color: #fff; + transition: color 0.2s ease; +} + +.field * { + color: var(--lw-secondary-color); + transition: color 0.2s ease; +} + +.field:hover * { + color: #fff; + transition: color 0.2s ease; +} + +.url { + width: calc(100vw - 36px); + user-select: all; + overflow-x: scroll; + white-space: nowrap; + display: block; + + padding: var(--space-2x) 14px; + -ms-overflow-style: none; /* IE and Edge */ + scrollbar-width: none; /* Firefox */ +} + +@media (min-width: 436px) { + .url { + width: 400px; + } +} + +@media (min-width: 768px) { + .url { + width: 400px; + } + .mobile-copy { + margin-bottom: 0; + display: none; + } +} + +.url::-webkit-scrollbar { + display: none; +} + +.fade { + position: absolute; + background: linear-gradient(90deg, rgba(37, 39, 41, 0%) 0%, #252729 100%); + top: 0; + bottom: 0; + right: 0px; + width: 50px; + z-index: 1; + display: block; + border-radius: var(--space-2x); + pointer-events: none; +} + +.desktop-copy.desktop-copy-disabled { + display: none; +} + +@media (min-width: 768px) { + .fade { + right: 50px; + } + .fade.desktop-copy-disabled { + right: 0px; + } +} + +.copied { + display: flex; + z-index: 2; + align-items: center; + justify-content: flex-end; + opacity: 0; + transition: opacity 0.2s ease !important; +} + +.copied.visible { + opacity: 1; +} + +.copy-button { + background: none; + outline: none; + border: none; + cursor: pointer; + z-index: 2; + border-radius: var(--space-2x); + width: 40px; + display: flex; + align-items: center; + justify-content: center; + margin-right: -10px; +} + +@media (min-width: 768px) { + .copied { + position: absolute; + right: 50px; + top: 0px; + bottom: 0px; + background: linear-gradient(90deg, rgba(37, 39, 41, 0%) 0px, #252729 40px); + padding-left: 50px; + } + .copy-button { + position: absolute; + top: 0px; + right: 0px; + bottom: 0px; + margin-right: 0px; + width: 50px; + } +} diff --git a/apps/www/components/LaunchWeek/Ticket/ticket-copy.tsx b/apps/www/components/LaunchWeek/Ticket/ticket-copy.tsx new file mode 100644 index 00000000000..3ec81fbb729 --- /dev/null +++ b/apps/www/components/LaunchWeek/Ticket/ticket-copy.tsx @@ -0,0 +1,109 @@ +import { useEffect, useState, useRef } from 'react' +import cn from 'classnames' +import { SITE_URL } from '~/lib/constants' +import styleUtils from './utils.module.css' +// import IconCopy from '~/components/LaunchWeek/Ticket/icons/icon-copy' +import styles from './ticket-copy.module.css' + +type Props = { + username: string +} + +export default function TicketCopy({ username }: Props) { + const [fadeOpacity, setFadeOpacity] = useState(1) + const [scrolling, setScrolling] = useState(false) + const [copyEnabled, setCopyEnabled] = useState(false) + const [copied, setCopied] = useState(false) + const scrollRef = useRef(null) + const buttonRef = useRef(null) + const url = `${SITE_URL}/tickets/${username}` + useEffect(() => { + if (navigator.clipboard) { + setCopyEnabled(true) + } + }, []) + + const copiedText = ( + + Copied! + + ) + + const copyButton = ( + + ) + + return ( +
+
+
Your ticket URL:
+
+ {copiedText} + {copyButton} +
+
+
+ { + if (!scrolling) { + setScrolling(true) + const animationFrame = requestAnimationFrame(() => { + const scrollableWidth = + (scrollRef.current?.scrollWidth || 0) - (scrollRef.current?.clientWidth || 0) + setFadeOpacity( + (scrollableWidth - (scrollRef.current?.scrollLeft || 0)) / (scrollableWidth || 1) + ) + cancelAnimationFrame(animationFrame) + setScrolling(false) + }) + } + }} + > + {url} + + +
+ {copiedText} + {copyButton} +
+
+
+ ) +} diff --git a/apps/www/components/LaunchWeek/Ticket/ticket-form.module.css b/apps/www/components/LaunchWeek/Ticket/ticket-form.module.css new file mode 100644 index 00000000000..d10b43a0284 --- /dev/null +++ b/apps/www/components/LaunchWeek/Ticket/ticket-form.module.css @@ -0,0 +1,65 @@ +.githubIcon { + margin-left: 12px; + margin-right: var(--space-4x); + display: inline-flex; +} + +.checkIcon { + position: absolute; + right: var(--space-4x); + display: inline-flex; +} + +.generateWithGithub { + display: flex; + align-items: center; + font-weight: 500; +} + +.stageIcon { + position: absolute; + left: var(--space-4x); + display: inline-flex; +} + +.form-row { + margin-left: auto; + margin-right: auto; + max-width: 600px; +} + +.description { + color: var(--secondary-color); + margin: 0; + text-align: center; + margin-left: var(--space-4x); +} + +@media (min-width: 768px) { + .form-row { + margin-bottom: 20px; + } +} + +@media (min-width: 1200px) { + .form-row { + margin: 0; + } + + .description { + text-align: left; + } + + .generateWithGithub { + width: 100%; + } +} + +.learn-more { + color: #fff; +} + +.learn-more:hover { + text-decoration: underline; + color: #fff; +} diff --git a/apps/www/components/LaunchWeek/Ticket/ticket-form.tsx b/apps/www/components/LaunchWeek/Ticket/ticket-form.tsx new file mode 100644 index 00000000000..1674bd7c2ba --- /dev/null +++ b/apps/www/components/LaunchWeek/Ticket/ticket-form.tsx @@ -0,0 +1,103 @@ +import { useState, useRef } from 'react' +// import { scrollTo } from '@lib/smooth-scroll'; +import cn from 'classnames' +// import GithubIcon from '~/components/LaunchWeek/Ticket/icons/icon-github' +// import CheckIcon from '~/components/LaunchWeek/Ticket/icons/icon-check' +import { SITE_ORIGIN } from '~/lib/constants' +import useConfData from '~/components/LaunchWeek/Ticket//hooks/use-conf-data' +import LoadingDots from './loading-dots' +import formStyles from './form.module.css' +import ticketFormStyles from './ticket-form.module.css' + +type FormState = 'default' | 'loading' | 'error' +type TicketGenerationState = 'default' | 'loading' + +type Props = { + defaultUsername?: string + setTicketGenerationState: React.Dispatch> +} + +export default function Form({ defaultUsername = '', setTicketGenerationState }: Props) { + const [username] = useState(defaultUsername) + const [formState, setFormState] = useState('default') + const [errorMsg] = useState('') + const { supabase } = useConfData() + const formRef = useRef(null) + + return formState === 'error' ? ( +
+
+
+
{errorMsg}
+ +
+
+
+ ) : ( +
{ + e.preventDefault() + + if (formState !== 'default') { + setTicketGenerationState('default') + setFormState('default') + return + } + + setFormState('loading') + setTicketGenerationState('loading') + + await supabase.auth.signInWithOAuth({ + provider: 'github', + options: { redirectTo: `${SITE_ORIGIN}/launch-week/tickets` }, + }) + }} + > +
+
+ +

Only public info will be used.

+
+
+
+ ) +} diff --git a/apps/www/components/LaunchWeek/Ticket/ticket-image.module.css b/apps/www/components/LaunchWeek/Ticket/ticket-image.module.css new file mode 100644 index 00000000000..8af0bcf2aa6 --- /dev/null +++ b/apps/www/components/LaunchWeek/Ticket/ticket-image.module.css @@ -0,0 +1,17 @@ +.background { + width: 2000px; + height: 1000px; + background: #000; + color: #fff; + overflow: hidden; + display: flex; + align-items: center; + justify-content: center; +} + +.page { + width: 1700px; + font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', + 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 'Noto Sans', 'Noto Sans JP', + 'Noto Sans KR', 'Noto Sans SC', 'Noto Sans TC', 'Noto Sans HK', sans-serif; +} diff --git a/apps/www/components/LaunchWeek/Ticket/ticket-image.tsx b/apps/www/components/LaunchWeek/Ticket/ticket-image.tsx new file mode 100644 index 00000000000..43fe8a2f68f --- /dev/null +++ b/apps/www/components/LaunchWeek/Ticket/ticket-image.tsx @@ -0,0 +1,37 @@ +import { useRouter } from 'next/router' +import Head from 'next/head' +import TicketVisual from './ticket-visual' +import styles from './ticket-image.module.css' + +export default function TicketImage() { + const { query } = useRouter() + if (query.ticketNumber) { + return ( +
+
+ + + + + +
+
+ ) + } + return <> +} diff --git a/apps/www/components/LaunchWeek/Ticket/ticket-info.module.css b/apps/www/components/LaunchWeek/Ticket/ticket-info.module.css new file mode 100644 index 00000000000..117922b8074 --- /dev/null +++ b/apps/www/components/LaunchWeek/Ticket/ticket-info.module.css @@ -0,0 +1,93 @@ +.info { + display: flex; + flex-direction: column; + gap: 0px; + margin-bottom: 64px; + padding: 32px; +} + +@media (min-width: 768px) { + .info { + /* display: grid; + grid-template-columns: 1fr 2fr; + gap: calc(var(--space-4x) * var(--size)); + font-size: calc(1em * var(--size)); */ + gap: calc(8px * var(--size)); + padding: 0; + margin-bottom: calc(32px * var(--size)); + margin-left: calc(160px * var(--size)); + } +} + +.logo { + /* margin-bottom: var(--space-6x); */ + /* max-width: 96px; */ + font-size: 18px; +} + +/* @media (min-width: 768px) { + .logo { + margin-bottom: 0; + font-size: calc(16px * var(--size)); + } +} */ + +.date { + /* text-transform: uppercase; */ + font-size: calc(18px * var(--size)); + + color: var(--lw-secondary-color); +} + +.date-golden { + color: var(--gold-primary); +} + +@media (min-width: 768px) { + .date { + margin-bottom: 0; + line-height: 1.15; + font-size: calc(20px * var(--size)); + } +} + +.created-by { + display: flex; + align-items: center; + color: var(--accents-4); +} + +@media (min-width: 768px) { + .created-by { + margin-right: var(--space-4x); + } +} + +.created-by-text { + white-space: nowrap; + margin-right: var(--space); +} + +/* .created-by-logo { + height: calc(16px * var(--size)); + display: inline-flex; +} */ + +.url { + margin-bottom: var(--space-12x); + font-size: calc(18px * var(--size)); + + color: var(--lw-secondary-color); +} + +.url-golden { + color: var(--gold-primary); +} + +@media (min-width: 768px) { + .url { + margin-bottom: 0; + font-size: calc(20px * var(--size)); + line-height: 1.15; + } +} diff --git a/apps/www/components/LaunchWeek/Ticket/ticket-info.tsx b/apps/www/components/LaunchWeek/Ticket/ticket-info.tsx new file mode 100644 index 00000000000..0bf9bb1f9c8 --- /dev/null +++ b/apps/www/components/LaunchWeek/Ticket/ticket-info.tsx @@ -0,0 +1,20 @@ +import styles from './ticket-info.module.css' +import cn from 'classnames' +import { DATE, SITE_URL } from '~/lib/constants' + +const siteUrl = new URL(SITE_URL) +const siteUrlForTicket = `${siteUrl.host}${siteUrl.pathname}`.replace(/\/$/, '') + +export default function TicketInfo({ + logoTextSecondaryColor = 'var(--accents-5)', + golden = false, +}) { + return ( +
+
+
{DATE}
+
+
{siteUrlForTicket}
+
+ ) +} diff --git a/apps/www/components/LaunchWeek/Ticket/ticket-mono-mobile.tsx b/apps/www/components/LaunchWeek/Ticket/ticket-mono-mobile.tsx new file mode 100644 index 00000000000..f55de57df16 --- /dev/null +++ b/apps/www/components/LaunchWeek/Ticket/ticket-mono-mobile.tsx @@ -0,0 +1,34 @@ +// NOTE: When you export SVG from Figma, you must: +// - Update width and height as 100% +// - Change id to "mobile-..." +// - Change url(#) to url(#mobile-) +// This is because if a page has two same IDs it will fail. +export default function TicketMonoMobile({ golden = false }: { golden?: boolean }) { + const frameColor = golden ? '#F2C94C' : '#3fcf8e' + + const perforationColor = golden ? 'var(--gold-accent)' : '#252729' + + return ( + + {/* */} + {/* */} + + + ) +} diff --git a/apps/www/components/LaunchWeek/Ticket/ticket-mono.tsx b/apps/www/components/LaunchWeek/Ticket/ticket-mono.tsx new file mode 100644 index 00000000000..c4e5eb46b0f --- /dev/null +++ b/apps/www/components/LaunchWeek/Ticket/ticket-mono.tsx @@ -0,0 +1,23 @@ +export default function TicketMono({ golden = false }: { golden?: boolean }) { + const frameColor = golden ? '#F2C94C' : '#252729' + + const perforationColor = golden ? 'var(--gold-accent)' : '#252729' + + return ( + + + + + ) +} diff --git a/apps/www/components/LaunchWeek/Ticket/ticket-number.tsx b/apps/www/components/LaunchWeek/Ticket/ticket-number.tsx new file mode 100644 index 00000000000..f5f80bbacc6 --- /dev/null +++ b/apps/www/components/LaunchWeek/Ticket/ticket-number.tsx @@ -0,0 +1,14 @@ +type Props = { + number: number +} + +export default function TicketNumber({ number }: Props) { + const numDigits = `${number}`.length + const prefix = `000000`.slice(numDigits) + return ( + <> + № {prefix} + {number} + + ) +} diff --git a/apps/www/components/LaunchWeek/Ticket/ticket-profile.module.css b/apps/www/components/LaunchWeek/Ticket/ticket-profile.module.css new file mode 100644 index 00000000000..0eda9d1fd2f --- /dev/null +++ b/apps/www/components/LaunchWeek/Ticket/ticket-profile.module.css @@ -0,0 +1,155 @@ +.profile { + display: flex; + align-items: center; +} + +.image { + width: 60px; + height: 60px; + border-radius: 50%; + border: 1px solid #333; +} + +@media (min-width: 768px) { + .image { + width: calc(80px * var(--size)); + height: calc(80px * var(--size)); + } +} + +.text { + flex: 1; + margin-left: 10px; +} + +@media (min-width: 768px) { + .text { + margin-left: calc(15px * var(--size)); + } +} + +.name { + font-size: 24px; + display: inline-block; + line-height: 1.15; + letter-spacing: -0.02em; + margin: 0; +} + +.name-golden { + color: var(--gold-primary); +} + +@media (min-width: 768px) { + .name { + font-size: calc(var(--space-8x) * var(--size)); + margin-bottom: 5px; + } +} + +.username { + display: inline-block; + font-size: calc(1em * var(--size)); + color: var(--lw-secondary-color); + display: flex; + align-items: center; + margin: 0; +} + +.username-golden { + color: var(--gold-secondary); +} + +.githubIcon { + margin-right: calc(5px * var(--size)); + display: inline-flex; +} + +.githubIcon-golden svg { + color: var(--gold-secondary); + opacity: 0.5; +} + +.empty-icon { + display: block; + background: linear-gradient(135deg, #b4b6b8, #7f8183); + width: 60px; +} + +@media (min-width: 768px) { + .empty-icon { + width: calc(80px * var(--size)); + } +} + +@keyframes loading { + 0% { + background-position: 200% 0; + } + 100% { + background-position: -200% 0; + } +} + +.skeleton { + display: flex; + align-items: center; + border-radius: 5px; +} + +.skeleton.loaded { + width: unset !important; +} + +.skeleton:not(.wrapper):not(.show) { + display: none; +} + +.wrapper:not(.show)::before { + content: none; +} + +.skeleton:not(.wrapper):not(.loaded) { + border-radius: var(--space); + background-image: linear-gradient(270deg, #252729, #34383d, #34383d, #252729); + background-size: 200% 100%; + animation: loading 2s ease-in-out infinite; +} + +.wrapper { + position: relative; +} + +.wrapper::before { + content: ''; + position: absolute; + top: -1px; + left: -1px; + right: 0; + bottom: 0; + border-radius: var(--space); + z-index: 100; + background-image: linear-gradient(270deg, #111111, #333333, #333333, #111111); + background-size: 200% 100%; + animation: loading 2s ease-in-out infinite; +} + +.inline { + display: inline-block !important; +} + +.rounded, +.rounded.wrapper::before { + width: 62px; + height: 62px; + border-radius: 50% !important; +} + +@media (min-width: 768px) { + .rounded, + .rounded.wrapper::before { + width: calc(82px * var(--size)); + height: calc(82px * var(--size)); + border-radius: 50% !important; + } +} diff --git a/apps/www/components/LaunchWeek/Ticket/ticket-profile.tsx b/apps/www/components/LaunchWeek/Ticket/ticket-profile.tsx new file mode 100644 index 00000000000..6df6e14961d --- /dev/null +++ b/apps/www/components/LaunchWeek/Ticket/ticket-profile.tsx @@ -0,0 +1,69 @@ +// import GithubIcon from '~/components/LaunchWeek/Ticket/icons/icon-github' +import cn from 'classnames' +// import IconAvatar from '~/components/LaunchWeek/Ticket/icons/icon-avatar' +import styles from './ticket-profile.module.css' + +type TicketGenerationState = 'default' | 'loading' +type Props = { + name?: string + username?: string + size?: number + ticketGenerationState: TicketGenerationState + golden?: boolean +} + +export default function TicketProfile({ + name, + username, + size = 1, + ticketGenerationState, + golden = false, +}: Props) { + return ( +
+ + {username ? ( + {username} + ) : ( + {/* */} + )} + +
+

+ + {name || username || 'Your Name'} + +

+

+ + + {/* */} + + {username || <>username} + +

+
+
+ ) +} diff --git a/apps/www/components/LaunchWeek/Ticket/ticket-visual.module.css b/apps/www/components/LaunchWeek/Ticket/ticket-visual.module.css new file mode 100644 index 00000000000..2ee2f212a38 --- /dev/null +++ b/apps/www/components/LaunchWeek/Ticket/ticket-visual.module.css @@ -0,0 +1,173 @@ +.visual { + position: relative; + transform: translateZ(0); + display: flex; + flex-direction: column; + justify-content: space-between; + /* background-color: black; */ + border: 2px solid rgba(0, 0, 0, 0.1); + border-radius: 16px; + + background-color: rgba(0, 0, 0, 1); + -webkit-backdrop-filter: blur(5px); + backdrop-filter: blur(5px); + + /* background: linear-gradient(#fff000, var(--gold-secondary)); */ + border: 1px solid rgba(255, 255, 0, 0.629); + animation: blank-svg-shadow 2s ease-in-out infinite alternate; +} + +.visual--gold { + background-color: gold; + /* border: 10px solid; + border-image-slice: 1; + border-width: 2px; + border-radius: 16px; */ + background: linear-gradient(#fff000, var(--gold-secondary)); + border: 1px solid rgba(255, 255, 0, 0.629); + animation: svg-shadow 2s ease-in-out infinite alternate; + /* filter: drop-shadow(2.45223px 8.5828px 73.5669px #FFBD14); */ + /* border-image-source: linear-gradient(to left, var(--gold-secondary), var(--gold-accent)); */ +} + +/* .svg-shadow { +} */ + +@keyframes svg-shadow { + from { + filter: drop-shadow(0 0 5px #fff) drop-shadow(0 0 15px var(--gold-primary)) + drop-shadow(0 0 20px var(--gold-accent)); + } + + to { + filter: drop-shadow(0 0 20px #fff) drop-shadow(0 0 10px var(--gold-accent)) + drop-shadow(0 0 40px var(--gold-primary)); + } +} + +@keyframes blank-svg-shadow { + from { + filter: drop-shadow(0 0 5px #fff) drop-shadow(0 0 15px #12f28d) drop-shadow(0 0 20px #12f28d); + } + + to { + filter: drop-shadow(0 0 20px #fff) drop-shadow(0 0 10px #12f28d) drop-shadow(0 0 40px #12f28d); + } +} + +.horizontal-ticket { + display: none; +} + +.vertical-ticket { + display: flex; +} + +@media (min-width: 768px) { + .horizontal-ticket { + display: flex; + } + + .vertical-ticket { + display: none; + } +} + +.logo { + z-index: 1; + position: absolute; + top: 0; + width: 100%; + height: 100%; + /* padding: 46px 29px;/ */ +} + +.logo img { + /* margin-top: 64px; */ + margin-top: calc(64px * var(--size)); + margin-left: calc(32px * var(--size)); + margin-left: calc(32px * var(--size)); + /* width: 272px; */ + width: calc(272px * var(--size)); +} + +@media (min-width: 768px) { + .logo img { + margin-top: calc(32px * var(--size)); + /* width: 280px; */ + width: calc(280px * var(--size)); + margin-left: calc(160px * var(--size)); + } +} + +.profile { + z-index: 1; + position: absolute; + top: 90px; + width: 100%; + height: 100%; + padding: 46px 29px; +} + +@media (min-width: 768px) { + .profile { + top: calc(48px * var(--size)); + padding: calc(44px * var(--size)) calc(160px * var(--size)) calc(44px * var(--size)) + calc(63px * var(--size)); + } +} + +.info { + z-index: 1; + position: absolute; + bottom: 0; + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + justify-content: flex-end; + /* padding: calc(46px * var(--size)) calc(29px * var(--size)); */ +} + +@media (min-width: 768px) { + .info { + bottom: 0; + /* padding: 0 calc(160px * var(--size)) calc(44px * var(--size)) + calc(63px * var(--size)); */ + } +} + +.ticket-number-wrapper { + position: absolute; + bottom: 40px; + right: 0; +} + +@media (min-width: 768px) { + .ticket-number-wrapper { + bottom: 0; + } +} + +.ticket-number { + font-weight: bold; + font-size: calc(40px * var(--size)); + line-height: 1; + width: calc(330px * var(--size)); + text-align: center; +} + +.ticket-number-golden { + color: var(--gold-primary); +} + +@media (min-width: 768px) { + .ticket-number { + transform: rotate(90deg) translateY(calc(100px * var(--size))); + transform-origin: bottom right; + } + + .ticket-number-golden { + color: var(--gold-accent); + } +} diff --git a/apps/www/components/LaunchWeek/Ticket/ticket-visual.tsx b/apps/www/components/LaunchWeek/Ticket/ticket-visual.tsx new file mode 100644 index 00000000000..bec9aceced7 --- /dev/null +++ b/apps/www/components/LaunchWeek/Ticket/ticket-visual.tsx @@ -0,0 +1,83 @@ +import styles from './ticket-visual.module.css' +import TicketProfile from './ticket-profile' +import TicketNumber from './ticket-number' +import TicketMono from './ticket-mono' +import TicketInfo from './ticket-info' +import TicketMonoMobile from './ticket-mono-mobile' +import cn from 'classnames' +import { useRouter } from 'next/router' + +type TicketGenerationState = 'default' | 'loading' +type Props = { + size?: number + name?: string + ticketNumber?: number + username?: string + ticketGenerationState?: TicketGenerationState + golden?: boolean +} + +export default function TicketVisual({ + size = 1, + name, + username, + ticketNumber, + ticketGenerationState = 'default', + golden = false, +}: Props) { + // golden = true; + + const router = useRouter() + const basePath = router.basePath + return ( + <> +
+
+ {/* {username ? : } */} + + +
+
+ {/* {username ? ( + + ) : ( + + )} */} + +
+
+ +
+
+ +
+
+ +
+ {ticketNumber && ( +
+
+ +
+
+ )} +
+ + ) +} diff --git a/apps/www/components/LaunchWeek/Ticket/ticket.module.css b/apps/www/components/LaunchWeek/Ticket/ticket.module.css new file mode 100644 index 00000000000..4101fabfef0 --- /dev/null +++ b/apps/www/components/LaunchWeek/Ticket/ticket.module.css @@ -0,0 +1,161 @@ +.hero { + font-size: 30px; + line-height: 1.15; + letter-spacing: -0.05em; + font-weight: 400; + margin: 0; + text-align: center; +} + +@media (min-width: 768px) { + .hero { + font-size: 30px; + line-height: 1; + /* font-weight: 800; */ + /* max-width: 800px; */ + /* margin-left: auto; + margin-right: auto; */ + } +} + +.description { + font-size: 20px; + line-height: 1.4; + color: var(--lw-secondary-color); + text-align: center; + margin: 30px auto; + max-width: 350px; +} + +@media (min-width: 768px) { + .description { + font-size: 24px; + max-width: 415px; + } +} + +.ticket-text { + margin-bottom: 30px; +} + +.ticket-layout { + display: grid; + grid-template-columns: 1fr; + gap: 45px; + margin: auto; + padding: var(--space-4x); + padding-top: 64px; + padding-bottom: 64px; +} + +@media (min-width: 1200px) { + .ticket-layout { + padding-top: 0; + padding-bottom: 0; + } +} + +.ticket-share-layout { + align-items: center; + margin-bottom: auto; +} + +.ticket-visual { + position: relative; + border-radius: calc(16px * var(--size)); + /* overflow: hidden; */ + width: 330px; + margin: 0 auto; +} + +/* .ticket-visual:before, +.ticket-visual:after { */ +/* content: ''; + display: block; + width: 72px; + height: 58px; */ +/* background-color: #1C1C1C; */ +/* position: absolute; + z-index: 100; + border-radius: 50%; */ +/* } */ + +.ticket-visual:before { + left: 50%; + top: -7.5%; + transform: translateZ(0) translateX(-50%) rotate(90deg); +} + +.ticket-visual:after { + left: 50%; + bottom: -7.5%; + transform: translateZ(0) translateX(-50%) rotate(90deg); +} + +@media (min-width: 768px) { + .ticket-visual { + width: 650px; + } + + .ticket-visual:before { + top: 50%; + left: -7%; + transform: translateZ(0) translateY(-50%); + } + + .ticket-visual:after { + top: 50%; + left: 96%; + transform: translateZ(0) translateY(-50%); + } +} + +.ticket-actions { + margin-top: 30px; + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; +} + +.ticket-copy { + margin-top: 30px; +} + +.ticket-actions-placeholder { + height: 280px; +} + +@media (min-width: 768px) { + .ticket-actions { + flex-direction: row; + } + .ticket-actions-placeholder { + height: 157px; + } +} + +@media (min-width: 1200px) { + .ticket-layout { + grid-template-columns: 1fr 1fr; + } + .hero { + font-size: 48px; + text-align: left; + } + .description { + text-align: left; + margin: 30px 0; + } +} + +.ticket-visual-wrapper { + border-radius: calc(16px * var(--size)); + width: 100%; +} + +@media screen and (min-width: 1280px) { + .ticket-layout { + padding: 0 40px; + } +} diff --git a/apps/www/components/LaunchWeek/Ticket/ticket.tsx b/apps/www/components/LaunchWeek/Ticket/ticket.tsx new file mode 100644 index 00000000000..3af3b7eb048 --- /dev/null +++ b/apps/www/components/LaunchWeek/Ticket/ticket.tsx @@ -0,0 +1,132 @@ +import cn from 'classnames' +import Tilt from 'vanilla-tilt' +import { useRef, useEffect, useState } from 'react' +// import { TicketGenerationState } from '@lib/constants' +// import {isMobileOrTablet} from '~/lib/helpers' +// import { scrollTo } from '@lib/smooth-scroll' +import styles from './ticket.module.css' +import styleUtils from './utils.module.css' +import TicketForm from './ticket-form' +import TicketVisual from './ticket-visual' +import TicketActions from './ticket-actions' +import TicketCopy from './ticket-copy' +import { DATE, SITE_NAME } from '~/lib/constants' +import Form from './form' +import { UserData } from './hooks/use-conf-data' + +type TicketGenerationState = 'default' | 'loading' +type Props = { + username: UserData['username'] + ticketNumber: UserData['ticketNumber'] + name: UserData['name'] + golden: UserData['golden'] + sharePage?: boolean +} + +export default function Ticket({ username, name, ticketNumber, sharePage, golden }: Props) { + const ticketRef = useRef(null) + const [ticketGenerationState, setTicketGenerationState] = + useState('default') + const divRef = useRef(null) + + useEffect(() => { + if (ticketRef.current && !window.matchMedia('(pointer: coarse)').matches) { + Tilt.init(ticketRef.current, { + glare: true, + max: 5, + 'max-glare': 0.16, + 'full-page-listening': true, + }) + } + }, [ticketRef]) + + // useEffect(() => { + // if (!sharePage && divRef && divRef.current && isMobileOrTablet()) { + // scrollTo(divRef.current, -30) + // } + // }, [divRef, sharePage]) + + return ( +
+
+
+

+ {sharePage ? ( + name ? ( + <>{name}’s Ticket + ) : ( + <>{SITE_NAME} + ) + ) : golden ? ( + <> + You won a golden ticket!
Claim it now! + + ) : ( + <> + You're in.
Make it unique. + + )} +

+

+ {sharePage ? ( + <> + Join {name ?? 'us'} on {DATE}. + + ) : golden ? ( + <>Claim your ticket with GitHub and Tweet it to redeem your swag pack! + ) : ( + <> + Generate a unique ticket image with
+ your GitHub profile. + + )} +

+
+
+ {!sharePage ? ( + + ) : ( +
+ )} +
+
+
+
+ +
+ {!sharePage && ( + <> + {username ? ( +
+
+ +
+
+ +
+
+ ) : ( +
+ )} + + )} +
+
+ ) +} diff --git a/apps/www/components/LaunchWeek/Ticket/utils.module.css b/apps/www/components/LaunchWeek/Ticket/utils.module.css new file mode 100644 index 00000000000..c4aba2ab686 --- /dev/null +++ b/apps/www/components/LaunchWeek/Ticket/utils.module.css @@ -0,0 +1,76 @@ +.appear { + opacity: 0; + animation: appear 0.8s cubic-bezier(0.1, 0, 0.175, 1) forwards; +} + +.appear-first { + animation-delay: 0.4s; +} + +.appear-second { + animation-delay: 0.8s; +} + +.appear-third { + animation-delay: 1.2s; +} + +.appear-fourth { + animation-delay: 1.6s; +} + +.appear-fifth { + animation-delay: 2s; +} + +.appear-sixth { + animation-delay: 2.4s; +} + +@keyframes appear { + to { + opacity: 1; + } +} + +@media (min-width: 1200px) { + .hide-on-desktop { + display: none; + } +} + +.show-on-mobile { + display: block; +} + +.hide-on-mobile { + display: none; +} + +@media (min-width: 768px) { + .show-on-mobile { + display: none; + } + + .hide-on-mobile { + display: block; + } +} + +.hide-on-tablet { + display: block; +} + +.show-on-tablet { + display: none; +} + +@media (max-width: 1199px) and (min-width: 768px) { + .show-on-tablet { + display: block; + } + + .hide-on-tablet { + display: none; + } +} diff --git a/apps/www/lib/constants.ts b/apps/www/lib/constants.ts index e72ab554946..f6c16abf7f4 100644 --- a/apps/www/lib/constants.ts +++ b/apps/www/lib/constants.ts @@ -1,2 +1,15 @@ export const APP_NAME = 'Supabase' export const DESCRIPTION = 'The Open Source Alternative to Firebase.' + +// Launch Weeek +export const SAMPLE_TICKET_NUMBER = 1234 +export const SITE_URL = process.env.NEXT_PUBLIC_VERCEL_URL + ? `https://${process.env.NEXT_PUBLIC_VERCEL_URL}/launch-week` + : 'http://localhost:3000/launch-week' +export const SITE_ORIGIN = new URL(SITE_URL).origin +export const TWITTER_USER_NAME = 'supabase' +export const COOKIE = 'supa-launch-week-ticke-user-id' +export const DATE = 'December 12 8:00 PT | 11:00 ET' +export const TWEET_TEXT = 'Got my ticket for #SupaLaunchWeek 6!' +export const TWEET_TEXT_GOLDEN = `Woop! I got a Golden Ticket for #SupaLaunchWeek 6! SupaSwag on its way!` +export const SITE_NAME = 'Supabase' diff --git a/apps/www/lib/helpers.tsx b/apps/www/lib/helpers.tsx index 33b8fb8ed3f..2d9722c10f1 100644 --- a/apps/www/lib/helpers.tsx +++ b/apps/www/lib/helpers.tsx @@ -11,3 +11,21 @@ export const generateReadingTime = (text: string) => { export function capitalize(word: string) { return word[0].toUpperCase() + word.substring(1).toLowerCase() } + +export function isMobileOrTablet() { + // https://stackoverflow.com/a/8876069/114157 + const viewportWidth = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0) + return viewportWidth < 1200 +} + +// Convert numbers or strings to pixel value +// Helpful for styled-jsx when using a prop +// height: ${toPixels(height)}; (supports height={20} and height="20px") + +export const toPixels = (value: string | number) => { + if (typeof value === 'number') { + return `${value}px` + } + + return value +} diff --git a/apps/www/lib/redirects.js b/apps/www/lib/redirects.js index 40838ca9228..c370fc9ce29 100644 --- a/apps/www/lib/redirects.js +++ b/apps/www/lib/redirects.js @@ -1545,4 +1545,19 @@ module.exports = [ source: '/docs/guides/platform/disk-usage', destination: '/docs/guides/platform/database-usage', }, + { + permanent: true, + source: '/sign-in', + destination: 'https://app.supabase.com/sign-in', + }, + { + permanent: true, + source: '/sign-up', + destination: 'https://app.supabase.com/sign-up', + }, + { + permanent: true, + source: '/forgot-password', + destination: 'https://app.supabase.com/forgot-password', + }, ] diff --git a/apps/www/lib/rewrites.js b/apps/www/lib/rewrites.js index b5f9efbea19..1fd564e3039 100644 --- a/apps/www/lib/rewrites.js +++ b/apps/www/lib/rewrites.js @@ -17,20 +17,6 @@ module.exports = [ source: '/docs/:path*', destination: `${process.env.NEXT_PUBLIC_DOCS_URL}/:path*`, }, - // rewrite to keep existing ticket urls working - { - source: '/launch-week/tickets/:path', - destination: `${process.env.NEXT_PUBLIC_LAUNCHWEEKSITE_URL}/tickets/:path`, - }, - // rewrite to move ticket website to another path - { - source: '/launch-week-register', - destination: `${process.env.NEXT_PUBLIC_LAUNCHWEEKSITE_URL}`, - }, - { - source: '/launch-week-register/:path*', - destination: `${process.env.NEXT_PUBLIC_LAUNCHWEEKSITE_URL}/:path*`, - }, { source: '/new-docs', destination: `${process.env.NEXT_PUBLIC_REFERENCE_DOCS_URL}`, diff --git a/apps/www/package.json b/apps/www/package.json index ad8d7654ba9..b8808779fdf 100644 --- a/apps/www/package.json +++ b/apps/www/package.json @@ -29,7 +29,7 @@ "gray-matter": "^4.0.2", "markdown-toc": "^1.2.0", "marked": "^4.0.12", - "next": "latest", + "next": "12.1.0", "next-compose-plugins": "^2.2.1", "next-mdx-remote": "^4.1.0", "next-seo": "^4.20.0", @@ -55,6 +55,7 @@ "ui": "*", "use-debounce": "^7.0.1", "uuid": "^8.3.2", + "vanilla-tilt": "1.7.0", "yup": "^0.32.11" }, "devDependencies": { diff --git a/apps/www/pages/launch-week/ticket-image.tsx b/apps/www/pages/launch-week/ticket-image.tsx new file mode 100644 index 00000000000..9fa19e85f7f --- /dev/null +++ b/apps/www/pages/launch-week/ticket-image.tsx @@ -0,0 +1,5 @@ +import TicketImage from '~/components/LaunchWeek/Ticket/ticket-image' + +export default function TicketOnlyPage() { + return +} diff --git a/apps/www/pages/launch-week/tickets/[username].tsx b/apps/www/pages/launch-week/tickets/[username].tsx new file mode 100644 index 00000000000..97ac38e47ee --- /dev/null +++ b/apps/www/pages/launch-week/tickets/[username].tsx @@ -0,0 +1,114 @@ +import { NextSeo } from 'next-seo' +import { GetStaticProps, GetStaticPaths } from 'next' +import Error from 'next/error' +import DefaultLayout from '~/components/Layouts/Default' +import SectionContainer from '~/components/Layouts/SectionContainer' +import TicketContainer from '~/components/LaunchWeek/Ticket/TicketContainer' +import { SITE_URL, SAMPLE_TICKET_NUMBER } from '~/lib/constants' +import { createClient } from '@supabase/supabase-js' +import { useState } from 'react' +import { FastGlobOptionsWithoutCwd } from 'globby' + +type Props = { + username: string | null + name: string | null + ticketNumber: number | null + golden: boolean +} + +const supabaseAdmin = createClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.SUPABASE_SERVICE_ROLE_SECRET! +) + +export default function TicketShare({ username, ticketNumber, name, golden }: Props) { + const [supabase] = useState(() => + createClient(process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!) + ) + const description = 'Supabase Launch Week 6 | 12-16 Dec 2022' + + if (!ticketNumber) { + return + } + + const ogImageUrl = `https://obuldanrptloktxcffvn.functions.supabase.co/launchweek-ticket-og?username=${encodeURIComponent( + username ?? '' + )}` + + return ( + <> + +
+ + + + + + + + + ) +} + +export const getStaticProps: GetStaticProps = async ({ params }) => { + const username = params?.username?.toString() || null + let name: string | null | undefined + let ticketNumber: number | null | undefined + let golden = false + + if (username) { + const { data: user } = await supabaseAdmin! + .from('lw6_tickets_golden') + .select('name, ticketNumber, golden') + .eq('username', username) + .single() + name = user?.name + ticketNumber = user?.ticketNumber + golden = user?.golden ?? false + } + return { + props: { + username: ticketNumber ? username : null, + usernameFromParams: username || null, + name: ticketNumber ? name || username || null : null, + ticketNumber: ticketNumber || SAMPLE_TICKET_NUMBER, + golden, + }, + revalidate: 5, + } +} + +export const getStaticPaths: GetStaticPaths = async () => { + return Promise.resolve({ + paths: [], + fallback: 'blocking', + }) +} diff --git a/apps/www/pages/launch-week/tickets/index.tsx b/apps/www/pages/launch-week/tickets/index.tsx new file mode 100644 index 00000000000..56c1a368a47 --- /dev/null +++ b/apps/www/pages/launch-week/tickets/index.tsx @@ -0,0 +1,67 @@ +import { NextSeo } from 'next-seo' +import DefaultLayout from '~/components/Layouts/Default' +import SectionContainer from '~/components/Layouts/SectionContainer' +import TicketContainer from '~/components/LaunchWeek/Ticket/TicketContainer' +import { SITE_URL } from '~/lib/constants' +import { useRouter } from 'next/router' +import { createClient, Session } from '@supabase/supabase-js' +import { useState, useEffect } from 'react' + +export default function TicketHome() { + const [supabase] = useState(() => + createClient(process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!) + ) + const [session, setSession] = useState(null) + const description = 'Supabase Launch Week 6 | 12-16 Dec 2022' + const { query } = useRouter() + const ticketNumber = query.ticketNumber?.toString() + const defaultUserData = { + id: query.id?.toString(), + ticketNumber: ticketNumber ? parseInt(ticketNumber, 10) : undefined, + name: query.name?.toString(), + username: query.username?.toString(), + } + + useEffect(() => { + supabase.auth.onAuthStateChange((event, session) => { + setSession(session) + }) + }, []) + + return ( + <> + +
+ + + + + + + + + ) +} diff --git a/apps/www/pages/partners/[slug].tsx b/apps/www/pages/partners/[slug].tsx index 9eb63b5c310..dc498cae9fc 100644 --- a/apps/www/pages/partners/[slug].tsx +++ b/apps/www/pages/partners/[slug].tsx @@ -224,6 +224,7 @@ export const getStaticProps: GetStaticProps = async ({ params }) => { let { data: partner } = await supabase .from('partners') .select('*') + .eq('approved', true) .eq('slug', params!.slug as string) .single() diff --git a/apps/www/public/images/blog/2022-11-24-sql-nosql-postgresql/sql-nosql-postgresql.jpg b/apps/www/public/images/blog/2022-11-24-sql-nosql-postgresql/sql-nosql-postgresql.jpg new file mode 100644 index 00000000000..4bd166328fd Binary files /dev/null and b/apps/www/public/images/blog/2022-11-24-sql-nosql-postgresql/sql-nosql-postgresql.jpg differ diff --git a/apps/www/public/rss.xml b/apps/www/public/rss.xml index 34781c63723..4a306486453 100644 --- a/apps/www/public/rss.xml +++ b/apps/www/public/rss.xml @@ -5,924 +5,931 @@ https://supabase.com Latest news from Supabase en - Wed, 16 Nov 2022 03:03:30 +0000 + Mon, 21 Nov 2022 16:04:00 +0000 + https://supabase.com/blog/flutter-authentication-and-authorization-with-rls + Flutter Authentication and Authorization with RLS + https://supabase.com/blog/flutter-authentication-and-authorization-with-rls + Learn how you can secure your Flutter app using Supabase Row Level Security. + Mon, 21 Nov 2022 16:04:00 +0000 + + https://supabase.com/blog/fetching-and-caching-supabase-data-in-next-js-server-components Fetching and caching Supabase data in Next.js 13 Server Components https://supabase.com/blog/fetching-and-caching-supabase-data-in-next-js-server-components Next.js 13 introduces new data fetching and caching methods to enable React Server Components and Suspense. - Wed, 16 Nov 2022 03:03:30 +0000 + Wed, 16 Nov 2022 16:04:00 +0000 https://supabase.com/blog/authentication-in-ionic-angular Authentication in Ionic Angular with Supabase https://supabase.com/blog/authentication-in-ionic-angular Learn how to build an Ionic Angular app with authentication, Row Level Security, and Magic Link auth. - Mon, 07 Nov 2022 03:03:30 +0000 + Mon, 07 Nov 2022 16:04:00 +0000 https://supabase.com/blog/supabase-beta-update-october-2022 Supabase Beta October 2022 https://supabase.com/blog/supabase-beta-update-october-2022 New SDKs, quickstarts, Functions tricks, and more. But, more importantly, Launch Week 6️ has a date! - Tue, 01 Nov 2022 02:02:30 +0000 + Tue, 01 Nov 2022 16:04:00 +0000 https://supabase.com/blog/postgresql-commitfest What is PostgreSQL commitfest and how to contribute https://supabase.com/blog/postgresql-commitfest A time-tested method for contributing to the core Postgres code - Wed, 26 Oct 2022 02:02:30 +0000 + Wed, 26 Oct 2022 16:04:00 +0000 https://supabase.com/blog/supabase-flutter-sdk-v1-released supabase-flutter v1 Released https://supabase.com/blog/supabase-flutter-sdk-v1-released We've released supabase-flutter v1. More intuitive way of accessing Supabase from your Flutter application. - Thu, 20 Oct 2022 02:02:30 +0000 + Thu, 20 Oct 2022 16:04:00 +0000 https://supabase.com/blog/supabase-js-v2-released supabase-js v2 Released https://supabase.com/blog/supabase-js-v2-released We've released supabase-js v2. Updated examples, quickstarts, and an improved experience. - Wed, 19 Oct 2022 02:02:30 +0000 + Wed, 19 Oct 2022 16:04:00 +0000 https://supabase.com/blog/postgres-full-text-search-vs-the-rest Postgres Full Text Search vs the rest https://supabase.com/blog/postgres-full-text-search-vs-the-rest Comparing one of the most popular Postgres features against alternatives - Thu, 13 Oct 2022 02:02:30 +0000 + Thu, 13 Oct 2022 16:04:00 +0000 https://supabase.com/blog/supabase-beta-update-september-2022 Supabase Beta September 2022 https://supabase.com/blog/supabase-beta-update-september-2022 We were too focused on clearing out the backlog so we didn't ship anything new last month... or did we?! - Tue, 04 Oct 2022 02:02:30 +0000 + Tue, 04 Oct 2022 16:04:00 +0000 https://supabase.com/blog/postgres-wasm Postgres WASM by Snaplet and Supabase https://supabase.com/blog/postgres-wasm We're open sourcing postgres-wasm, a PostgresQL server that runs inside a browser, with our friends at Snaplet. - Sun, 02 Oct 2022 02:02:30 +0000 + Sun, 02 Oct 2022 16:04:00 +0000 https://supabase.com/blog/choosing-a-postgres-primary-key Choosing a Postgres Primary Key https://supabase.com/blog/choosing-a-postgres-primary-key Turns out the question of which identifier to use as a Primary Key is complicated -- we're going to dive into some of the complexity and inherent trade-offs, and figure things out - Wed, 07 Sep 2022 02:02:30 +0000 + Wed, 07 Sep 2022 16:04:00 +0000 https://supabase.com/blog/supabase-beta-update-august-2022 Supabase Beta August 2022 https://supabase.com/blog/supabase-beta-update-august-2022 Launch Week Special. See everything we shipped, plus winners of the Hackathon and the extended Community Highlights - Tue, 06 Sep 2022 02:02:30 +0000 + Tue, 06 Sep 2022 16:04:00 +0000 https://supabase.com/blog/launch-week-5-hackathon-winners Launch Week 5 Hackathon Winners https://supabase.com/blog/launch-week-5-hackathon-winners Announcing the winners of the Launch Week 5 Hackathon! - Wed, 24 Aug 2022 02:02:30 +0000 + Wed, 24 Aug 2022 16:04:00 +0000 https://supabase.com/blog/building-a-realtime-trello-board-with-supabase-and-angular Building a Realtime Trello Board with Supabase and Angular https://supabase.com/blog/building-a-realtime-trello-board-with-supabase-and-angular Go beyond the hello world example with this real world project. - Tue, 23 Aug 2022 02:02:30 +0000 + Tue, 23 Aug 2022 16:04:00 +0000 https://supabase.com/blog/supabase-vault Supabase Vault https://supabase.com/blog/supabase-vault Today we're announcing Vault, a Postgres extension for managing secrets and encryption inside your database. - Thu, 18 Aug 2022 02:02:30 +0000 + Thu, 18 Aug 2022 16:04:00 +0000 https://supabase.com/blog/postgrest-v10 PostgREST v10: EXPLAIN and Improved Relationship Detection https://supabase.com/blog/postgrest-v10 Today, PostgREST 10 was released. Let's take a look at some of the new features that go hand in hand with supabase-js v2. - Thu, 18 Aug 2022 02:02:30 +0000 + Thu, 18 Aug 2022 16:04:00 +0000 https://supabase.com/blog/pg-jsonschema-a-postgres-extension-for-json-validation pg_jsonschema: JSON Schema support for Postgres https://supabase.com/blog/pg-jsonschema-a-postgres-extension-for-json-validation Today we're releasing pg_jsonschema, a Postgres extension for JSON validation. - Thu, 18 Aug 2022 02:02:30 +0000 + Thu, 18 Aug 2022 16:04:00 +0000 https://supabase.com/blog/launch-week-5-one-more-thing One more thing https://supabase.com/blog/launch-week-5-one-more-thing Let's be honest, it's never just one more thing. - Thu, 18 Aug 2022 02:02:30 +0000 + Thu, 18 Aug 2022 16:04:00 +0000 https://supabase.com/blog/launch-week-5-community-day Community Day https://supabase.com/blog/launch-week-5-community-day Wrapping up Launch Week 5 with contributors, partners, and friends. - Thu, 18 Aug 2022 02:02:30 +0000 + Thu, 18 Aug 2022 16:04:00 +0000 https://supabase.com/blog/supabase-realtime-multiplayer-general-availability Realtime: Multiplayer Edition https://supabase.com/blog/supabase-realtime-multiplayer-general-availability Announcing the general availability of Realtime's Broadcast and Presence. - Wed, 17 Aug 2022 02:02:30 +0000 + Wed, 17 Aug 2022 16:04:00 +0000 https://supabase.com/blog/supabase-soc2 Supabase is SOC2 compliant https://supabase.com/blog/supabase-soc2 Supabase is now SOC2 compliant. Learn how we got here and what it means for our customers. - Tue, 16 Aug 2022 02:02:30 +0000 + Tue, 16 Aug 2022 16:04:00 +0000 https://supabase.com/blog/supabase-js-v2 supabase-js v2 https://supabase.com/blog/supabase-js-v2 A look at supabase-js v2, which brings type support and focuses on quality-of-life improvements for developers. - Mon, 15 Aug 2022 02:02:30 +0000 + Mon, 15 Aug 2022 16:04:00 +0000 https://supabase.com/blog/supabase-cli-v1-and-admin-api-beta Supabase CLI v1 and Management API Beta https://supabase.com/blog/supabase-cli-v1-and-admin-api-beta We are moving Supabase CLI v1 out of beta, and releasing Management API beta. - Sun, 14 Aug 2022 02:02:30 +0000 + Sun, 14 Aug 2022 16:04:00 +0000 https://supabase.com/blog/supabase-series-b Supabase Series B https://supabase.com/blog/supabase-series-b Supabase raised $80M in May, bringing our total funding to $116M. - Thu, 11 Aug 2022 02:02:30 +0000 + Thu, 11 Aug 2022 16:04:00 +0000 https://supabase.com/blog/launch-week-5-hackathon Launch Week 5 Hackathon https://supabase.com/blog/launch-week-5-hackathon Build to win $1500 - Friday 12th to Monday 21st August 2022 - Tue, 09 Aug 2022 02:02:30 +0000 + Tue, 09 Aug 2022 16:04:00 +0000 https://supabase.com/blog/slack-consolidate-slackbot-to-consolidate-messages Slack Consolidate: a slackbot built with Python and Supabase https://supabase.com/blog/slack-consolidate-slackbot-to-consolidate-messages A slackbot to consolidate messages from different channels using Supabase, Slack SDK and Python - Mon, 08 Aug 2022 02:02:30 +0000 + Mon, 08 Aug 2022 16:04:00 +0000 https://supabase.com/blog/supabase-beta-update-july-2022 Supabase Beta July 2022 https://supabase.com/blog/supabase-beta-update-july-2022 Launch Week Golden Tickets, Flutter SDK 1.0 Developer Preview and more... - Tue, 02 Aug 2022 02:02:30 +0000 + Tue, 02 Aug 2022 16:04:00 +0000 https://supabase.com/blog/supabase-flutter-sdk-1-developer-preview Supabase Flutter SDK 1.0 Developer Preview https://supabase.com/blog/supabase-flutter-sdk-1-developer-preview Supabase Flutter SDK is getting a major update and we need your help making it better. - Mon, 01 Aug 2022 02:02:30 +0000 + Mon, 01 Aug 2022 16:04:00 +0000 https://supabase.com/blog/seen-by-in-postgresql Implementing "seen by" functionality with Postgres https://supabase.com/blog/seen-by-in-postgresql Different approaches for tracking visitor counts with PostgreSQL. - Sun, 17 Jul 2022 02:02:30 +0000 + Sun, 17 Jul 2022 16:04:00 +0000 https://supabase.com/blog/supabase-auth-helpers-with-sveltekit-support Revamped Auth Helpers for Supabase (with SvelteKit support) https://supabase.com/blog/supabase-auth-helpers-with-sveltekit-support Supabase Auth Helpers now have improved developer experience, Sveltekit support, and more. - Tue, 12 Jul 2022 02:02:30 +0000 + Tue, 12 Jul 2022 16:04:00 +0000 https://supabase.com/blog/beta-update-june-2022 Supabase Beta June 2022 https://supabase.com/blog/beta-update-june-2022 Auth Helpers, Unlimited Free Projects, CLI and more... - Tue, 05 Jul 2022 02:02:30 +0000 + Tue, 05 Jul 2022 16:04:00 +0000 https://supabase.com/blog/flutter-tutorial-building-a-chat-app Flutter Tutorial: building a Flutter chat app https://supabase.com/blog/flutter-tutorial-building-a-chat-app Learn how to build a Flutter chat app with open source and scalable backend (inc. auth, realtime, database, and more). - Wed, 29 Jun 2022 02:02:30 +0000 + Wed, 29 Jun 2022 16:04:00 +0000 https://supabase.com/blog/visualizing-supabase-data-using-metabase Visualizing Supabase Data using Metabase https://supabase.com/blog/visualizing-supabase-data-using-metabase How to create different kinds of charts out of data stored in Supabase using Metabase. - Tue, 28 Jun 2022 02:02:30 +0000 + Tue, 28 Jun 2022 16:04:00 +0000 https://supabase.com/blog/partial-postgresql-data-dumps-with-rls Partial data dumps using Postgres Row Level Security https://supabase.com/blog/partial-postgresql-data-dumps-with-rls Using RLS to create seed files for local PostgreSQL testing. - Mon, 27 Jun 2022 02:02:30 +0000 + Mon, 27 Jun 2022 16:04:00 +0000 https://supabase.com/blog/loading-data-supabase-python Python data loading with Supabase https://supabase.com/blog/loading-data-supabase-python An example of how to load data into Supabase using supabase-py - Thu, 16 Jun 2022 02:02:30 +0000 + Thu, 16 Jun 2022 16:04:00 +0000 https://supabase.com/blog/beta-update-may-2022 Supabase Beta May 2022 https://supabase.com/blog/beta-update-may-2022 Product and community updates including wildcard auth redirects, edge functions with webhooks, and Prometheus endpoints for everybody. - Tue, 31 May 2022 02:02:30 +0000 + Tue, 31 May 2022 16:04:00 +0000 https://supabase.com/blog/how-supabase-accelerates-development-of-all-pull-together How Mike Lyndon is using Supabase to accelerate development of AllPullTogether https://supabase.com/blog/how-supabase-accelerates-development-of-all-pull-together Mike Lyndon is learning web development as he builds AllPullTogether, and Supabase is helping him accelerate what he can accomplish. - Wed, 25 May 2022 02:02:30 +0000 + Wed, 25 May 2022 16:04:00 +0000 https://supabase.com/blog/partner-gallery-works-with-supabase Works With Supabase - announcing our Partner Gallery https://supabase.com/blog/partner-gallery-works-with-supabase Introducing our Partner Gallery - open source and made with Supabase. - Tue, 19 Apr 2022 02:02:30 +0000 + Tue, 19 Apr 2022 16:04:00 +0000 https://supabase.com/blog/bring-the-func-hackathon-winners Bring the Func Hackathon Winners 2022 https://supabase.com/blog/bring-the-func-hackathon-winners Celebrating many amazing OSS Hackathon projects using GraphQL and Edge Functions. - Sun, 17 Apr 2022 02:02:30 +0000 + Sun, 17 Apr 2022 16:04:00 +0000 https://supabase.com/blog/beta-update-march-2022 Supabase Beta March 2022 https://supabase.com/blog/beta-update-march-2022 Functions, GraphQL, and much more. - Thu, 14 Apr 2022 02:02:30 +0000 + Thu, 14 Apr 2022 16:04:00 +0000 https://supabase.com/blog/supabrew Supabrew - Never Code Thirsty https://supabase.com/blog/supabrew A light and refreshing non-alcoholic beer for devs. - Fri, 01 Apr 2022 02:02:30 +0000 + Thu, 31 Mar 2022 16:04:00 +0000 https://supabase.com/blog/supabase-realtime-with-multiplayer-features Supabase Realtime, with Multiplayer Features https://supabase.com/blog/supabase-realtime-with-multiplayer-features Today we're announced Realtime, with multiplayer features. Realtime enables broadcast, presence, and listening to database changes delivered over WebSockets. - Fri, 01 Apr 2022 02:02:30 +0000 + Thu, 31 Mar 2022 16:04:00 +0000 https://supabase.com/blog/hackathon-bring-the-func Hackathon: Bring the Func(🕺) https://supabase.com/blog/hackathon-bring-the-func Build open-source projects with our latest features, win limited edition swag and plant a tree! - Fri, 01 Apr 2022 02:02:30 +0000 + Thu, 31 Mar 2022 16:04:00 +0000 https://supabase.com/blog/supabase-edge-functions Edge Functions are now available in Supabase https://supabase.com/blog/supabase-edge-functions Today we're launching Edge Functions. Edge Functions let you execute Typescript code close to your users, no matter where they're located. - Wed, 30 Mar 2022 02:02:30 +0000 + Wed, 30 Mar 2022 16:04:00 +0000 https://supabase.com/blog/supabase-enterprise Introducing Supabase Enterprise https://supabase.com/blog/supabase-enterprise Today we are releasing Supabase Enterprise, a suite of features to scale your project. - Tue, 29 Mar 2022 02:02:30 +0000 + Tue, 29 Mar 2022 16:04:00 +0000 https://supabase.com/blog/graphql-now-available GraphQL is now available in Supabase https://supabase.com/blog/graphql-now-available GraphQL support is now in general availability on the Supabase platform via our open source PostgreSQL extension, pg_graphql (beta). - Mon, 28 Mar 2022 02:02:30 +0000 + Mon, 28 Mar 2022 16:04:00 +0000 https://supabase.com/blog/community-day-lw4 Community Day https://supabase.com/blog/community-day-lw4 Kicking off Launch Week 4 with contributors, partners, and friends. - Sun, 27 Mar 2022 02:02:30 +0000 + Sun, 27 Mar 2022 16:04:00 +0000 https://supabase.com/blog/supabase-launch-week-four Supabase Launch Week 4 https://supabase.com/blog/supabase-launch-week-four Launch Week 4: One new feature every day for an entire week. Starting Monday 28th March. - Thu, 24 Mar 2022 02:02:30 +0000 + Thu, 24 Mar 2022 16:04:00 +0000 https://supabase.com/blog/should-i-open-source-my-company Should I Open Source my Company? https://supabase.com/blog/should-i-open-source-my-company The unexpected upsides of building in public - Thu, 24 Mar 2022 02:02:30 +0000 + Thu, 24 Mar 2022 16:04:00 +0000 https://supabase.com/blog/audit Postgres Auditing in 150 lines of SQL https://supabase.com/blog/audit PostgreSQL has a robust set of features which we can leverage to create a generic auditing solution in 150 lines of SQL. - Mon, 07 Mar 2022 03:03:30 +0000 + Mon, 07 Mar 2022 16:04:00 +0000 https://supabase.com/blog/supabase-beta-january-2022 Supabase Beta January 2022 https://supabase.com/blog/supabase-beta-january-2022 New auth providers, SMS providers, and new videos. - Mon, 21 Feb 2022 03:03:30 +0000 + Mon, 21 Feb 2022 16:04:00 +0000 https://supabase.com/blog/supabase-beta-december-2021 Supabase Beta December 2021 https://supabase.com/blog/supabase-beta-december-2021 New crypto extension, Postgres videos, and a bunch of cool integrations. - Wed, 19 Jan 2022 03:03:30 +0000 + Wed, 19 Jan 2022 16:04:00 +0000 https://supabase.com/blog/product-hunt-golden-kitty-awards-2021 Golden Kitty Awards Ceremony Watch Party with Supabase https://supabase.com/blog/product-hunt-golden-kitty-awards-2021 Hang out with us while watching the Product Hunt Golden Kitty Awards Ceremony - Wed, 19 Jan 2022 03:03:30 +0000 + Wed, 19 Jan 2022 16:04:00 +0000 https://supabase.com/blog/holiday-hackdays-winners-2021 Holiday Hackdays Winners 2021 https://supabase.com/blog/holiday-hackdays-winners-2021 Celebrating many amazing projects submitted to our Holiday Hackdays Hackathon. - Thu, 16 Dec 2021 03:03:30 +0000 + Thu, 16 Dec 2021 16:04:00 +0000 https://supabase.com/blog/beta-november-2021-launch-week-recap Supabase Beta November 2021: Launch Week Recap https://supabase.com/blog/beta-november-2021-launch-week-recap We wrapped up November with Supabase's third Launch Week. Here's all the awesome stuff that got shipped ... - Tue, 14 Dec 2021 03:03:30 +0000 + Tue, 14 Dec 2021 16:04:00 +0000 https://supabase.com/blog/supabase-holiday-hackdays-hackathon Kicking off the Holiday Hackdays https://supabase.com/blog/supabase-holiday-hackdays-hackathon Build cool stuff and celebrate open-source software with us during the Holiday Hackdays! - Thu, 02 Dec 2021 03:03:30 +0000 + Thu, 02 Dec 2021 16:04:00 +0000 https://supabase.com/blog/pg-graphql pg_graphql: A GraphQL extension for PostgreSQL https://supabase.com/blog/pg-graphql GraphQL support is in development for PostgreSQL + Supabase. - Thu, 02 Dec 2021 03:03:30 +0000 + Thu, 02 Dec 2021 16:04:00 +0000 https://supabase.com/blog/launch-week-three-friday-five-more-things Five more things https://supabase.com/blog/launch-week-three-friday-five-more-things It's never just one more thing! - Thu, 02 Dec 2021 03:03:30 +0000 + Thu, 02 Dec 2021 16:04:00 +0000 https://supabase.com/blog/supabase-acquires-logflare Supabase acquires Logflare https://supabase.com/blog/supabase-acquires-logflare Today, we're ecstatic to announce that Logflare is joining Supabase. - Wed, 01 Dec 2021 03:03:30 +0000 + Wed, 01 Dec 2021 16:04:00 +0000 https://supabase.com/blog/realtime-row-level-security-in-postgresql Realtime Postgres RLS now available on Supabase https://supabase.com/blog/realtime-row-level-security-in-postgresql Realtime database changes are now broadcast to authenticated users, respecting the same PostgreSQL policies that you use for Row Level Security. - Tue, 30 Nov 2021 03:03:30 +0000 + Tue, 30 Nov 2021 16:04:00 +0000 https://supabase.com/blog/supabase-studio Supabase Studio https://supabase.com/blog/supabase-studio The same Dashboard that you're using on our Platform is now available for local development and Self-Hosting. - Mon, 29 Nov 2021 03:03:30 +0000 + Mon, 29 Nov 2021 16:04:00 +0000 https://supabase.com/blog/community-day-lw3 Community Day https://supabase.com/blog/community-day-lw3 Kicking off launch week by highlighting the communities around Supabase. - Sun, 28 Nov 2021 03:03:30 +0000 + Sun, 28 Nov 2021 16:04:00 +0000 https://supabase.com/blog/whats-new-in-postgres-14 New in PostgreSQL 14: What every developer should know https://supabase.com/blog/whats-new-in-postgres-14 A quick look at some new features and functionality in PostgreSQL 14. - Sat, 27 Nov 2021 03:03:30 +0000 + Sat, 27 Nov 2021 16:04:00 +0000 https://supabase.com/blog/postgrest-9 PostgREST 9 https://supabase.com/blog/postgrest-9 New features and updates in PostgREST version 9. - Fri, 26 Nov 2021 03:03:30 +0000 + Fri, 26 Nov 2021 16:04:00 +0000 https://supabase.com/blog/supabase-launch-week-the-trilogy Supabase Launch Week III: Holiday Special https://supabase.com/blog/supabase-launch-week-the-trilogy Tis the season to be shipping. - Thu, 25 Nov 2021 03:03:30 +0000 + Thu, 25 Nov 2021 16:04:00 +0000 https://supabase.com/blog/supabase-how-we-launch How we launch at Supabase https://supabase.com/blog/supabase-how-we-launch The history and methodology of Supabase Launch Week. - Thu, 25 Nov 2021 03:03:30 +0000 + Thu, 25 Nov 2021 16:04:00 +0000 https://supabase.com/blog/supabase-beta-october-2021 Supabase Beta October 2021 https://supabase.com/blog/supabase-beta-october-2021 Three new Auth providers, multi-schema support, and we're gearing up for another Launch Week. - Sat, 06 Nov 2021 02:02:30 +0000 + Sat, 06 Nov 2021 16:04:00 +0000 https://supabase.com/blog/supabase-series-a Supabase $30m Series A https://supabase.com/blog/supabase-series-a Supabase just raised $30M, bringing our total funding to $36M. - Wed, 27 Oct 2021 02:02:30 +0000 + Wed, 27 Oct 2021 16:04:00 +0000 https://supabase.com/blog/replenysh-time-to-value-in-less-than-24-hours Replenysh uses Supabase to implement OTP in less than 24-hours https://supabase.com/blog/replenysh-time-to-value-in-less-than-24-hours Learn how Replenysh uses Supabase to power the circular economy, redefining how brands interact with their customers and products. - Mon, 18 Oct 2021 02:02:30 +0000 + Mon, 18 Oct 2021 16:04:00 +0000 https://supabase.com/blog/hacktoberfest-hackathon-winners-2021 Hacktoberfest Hackathon Winners 2021 https://supabase.com/blog/hacktoberfest-hackathon-winners-2021 Celebrating many amazing projects submitted to our Hacktoberfest Hackathon. - Wed, 13 Oct 2021 02:02:30 +0000 + Wed, 13 Oct 2021 16:04:00 +0000 https://supabase.com/blog/supabase-beta-sept-2021 Supabase Beta Sept 2021 https://supabase.com/blog/supabase-beta-sept-2021 Hackathon, Aborting request, UI updates, and now Hiring. - Sun, 03 Oct 2021 02:02:30 +0000 + Sun, 03 Oct 2021 16:04:00 +0000 https://supabase.com/blog/supabase-hacktoberfest-hackathon-2021 Supabase Hacktoberfest Hackathon 2021 https://supabase.com/blog/supabase-hacktoberfest-hackathon-2021 We're running another Supabase Hackathon during Hacktoberfest! - Mon, 27 Sep 2021 02:02:30 +0000 + Mon, 27 Sep 2021 16:04:00 +0000 https://supabase.com/blog/supabase-beta-august-2021 Supabase Beta August 2021 https://supabase.com/blog/supabase-beta-august-2021 Fundraising, Realtime Security, custom SMS templates, and deployments in South Korea. - Thu, 09 Sep 2021 02:02:30 +0000 + Thu, 09 Sep 2021 16:04:00 +0000 https://supabase.com/blog/supabase-beta-july-2021 Supabase Beta July 2021 https://supabase.com/blog/supabase-beta-july-2021 Discord Logins, Vercel Integration, Full text search, and OAuth guides. - Wed, 11 Aug 2021 02:02:30 +0000 + Wed, 11 Aug 2021 16:04:00 +0000 https://supabase.com/blog/hackathon-winners Open Source Hackathon Winners https://supabase.com/blog/hackathon-winners Let the medal ceremony begin for the best projects submitted during the Supabase Hackathon. - Sun, 08 Aug 2021 02:02:30 +0000 + Sun, 08 Aug 2021 16:04:00 +0000 https://supabase.com/blog/supabase-swag-store Supabase Swag Store https://supabase.com/blog/supabase-swag-store Today we are officially launching the Supabase Swag Store. - Thu, 29 Jul 2021 02:02:30 +0000 + Thu, 29 Jul 2021 16:04:00 +0000 https://supabase.com/blog/supabase-functions-updates Updates for Supabase Functions https://supabase.com/blog/supabase-functions-updates The question on everyone's mind - are we launching Supabase Functions? Well, it's complicated. - Thu, 29 Jul 2021 02:02:30 +0000 + Thu, 29 Jul 2021 16:04:00 +0000 https://supabase.com/blog/1-the-supabase-hackathon The Supabase Hackathon https://supabase.com/blog/1-the-supabase-hackathon A whole week of Hacking for Fun and Prizes. - Thu, 29 Jul 2021 02:02:30 +0000 + Thu, 29 Jul 2021 16:04:00 +0000 https://supabase.com/blog/supabase-reports-and-metrics Supabase Reports and Metrics https://supabase.com/blog/supabase-reports-and-metrics We're exposing a full set of metrics in your projects, so that you can build better (and faster) products for your users. - Wed, 28 Jul 2021 02:02:30 +0000 + Wed, 28 Jul 2021 16:04:00 +0000 https://supabase.com/blog/supabase-auth-passwordless-sms-login Supabase Auth v2: Phone Auth now available https://supabase.com/blog/supabase-auth-passwordless-sms-login Phone Auth is available today on all new and existing Supabase projects. - Tue, 27 Jul 2021 02:02:30 +0000 + Tue, 27 Jul 2021 16:04:00 +0000 https://supabase.com/blog/mobbin-supabase-200000-users Mobbin uses Supabase to authenticate 200,000 users https://supabase.com/blog/mobbin-supabase-200000-users Learn how Mobbin migrated 200,000 users from Firebase for a better authentication experience. - Tue, 27 Jul 2021 02:02:30 +0000 + Tue, 27 Jul 2021 16:04:00 +0000 https://supabase.com/blog/storage-beta Supabase Storage now in Beta https://supabase.com/blog/storage-beta Supabase Storage moves into Beta. - Mon, 26 Jul 2021 02:02:30 +0000 + Mon, 26 Jul 2021 16:04:00 +0000 https://supabase.com/blog/spot-flutter-with-postgres Spot: a video sharing app built with Flutter https://supabase.com/blog/spot-flutter-with-postgres Spot is a geolocation-based video-sharing app with some social networking features. - Mon, 26 Jul 2021 02:02:30 +0000 + Mon, 26 Jul 2021 16:04:00 +0000 https://supabase.com/blog/supabase-postgres-13 Supabase is now on Postgres 13.3 https://supabase.com/blog/supabase-postgres-13 From today, new Supabase projects will be on a version of Supabase Postgres that runs on Postgres 13.3. - Sun, 25 Jul 2021 02:02:30 +0000 + Sun, 25 Jul 2021 16:04:00 +0000 https://supabase.com/blog/supabase-community-day Supabase Community Day https://supabase.com/blog/supabase-community-day Community Day - Sun, 25 Jul 2021 02:02:30 +0000 + Sun, 25 Jul 2021 16:04:00 +0000 https://supabase.com/blog/epsilon3-self-hosting Epsilon3 Self-Host Supabase To Revolutionize Space Operations https://supabase.com/blog/epsilon3-self-hosting Learn how the team at Epsilon3 use Supabase to help teams execute secure and reliable operations in an industry that project spend runs into the billions. - Sun, 25 Jul 2021 02:02:30 +0000 + Sun, 25 Jul 2021 16:04:00 +0000 https://supabase.com/blog/supabase-launch-week-sql Supabase Launch Week II: The SQL https://supabase.com/blog/supabase-launch-week-sql Five days of Supabase. Again. - Wed, 21 Jul 2021 02:02:30 +0000 + Wed, 21 Jul 2021 16:04:00 +0000 https://supabase.com/blog/roles-postgres-hooks Protecting reserved roles with PostgreSQL Hooks https://supabase.com/blog/roles-postgres-hooks Using Postgres Hooks to protect functionality in your Postgres database. - Thu, 01 Jul 2021 02:02:30 +0000 + Thu, 01 Jul 2021 16:04:00 +0000 https://supabase.com/blog/supabase-beta-june-2021 Supabase Beta June 2021 https://supabase.com/blog/supabase-beta-june-2021 Discord Logins, Vercel Integration, Full text search, and OAuth guides. - Tue, 01 Jun 2021 02:02:30 +0000 + Tue, 01 Jun 2021 16:04:00 +0000 https://supabase.com/blog/supabase-beta-may-2021 Supabase Beta May 2021 https://supabase.com/blog/supabase-beta-may-2021 Apple & Twitter Logins, Supabase Grid, Go & Swift Libraries. - Tue, 01 Jun 2021 02:02:30 +0000 + Tue, 01 Jun 2021 16:04:00 +0000 https://supabase.com/blog/supabase-beta-april-2021 Supabase Beta April 2021 https://supabase.com/blog/supabase-beta-april-2021 Supabase "gardening" - stability, security, and community support. - Tue, 04 May 2021 02:02:30 +0000 + Tue, 04 May 2021 16:04:00 +0000 https://supabase.com/blog/supabase-beta-march-2021 Supabase Beta March 2021 https://supabase.com/blog/supabase-beta-march-2021 Launch week, Storage, Supabase CLI, Connection Pooling, Supabase UI, and Pricing. - Mon, 05 Apr 2021 02:02:30 +0000 + Mon, 05 Apr 2021 16:04:00 +0000 https://supabase.com/blog/supabase-workflows Workflows are coming to Supabase https://supabase.com/blog/supabase-workflows Functions are great, but you know what's better? - Thu, 01 Apr 2021 02:02:30 +0000 + Thu, 01 Apr 2021 16:04:00 +0000 https://supabase.com/blog/supabase-pgbouncer PgBouncer is now available in Supabase https://supabase.com/blog/supabase-pgbouncer Better support for Serverless and Postgres. - Thu, 01 Apr 2021 02:02:30 +0000 + Thu, 01 Apr 2021 16:04:00 +0000 https://supabase.com/blog/supabase-dot-com Supabase Dot Com https://supabase.com/blog/supabase-dot-com The Supabase Domain name is changing. - Thu, 01 Apr 2021 02:02:30 +0000 + Thu, 01 Apr 2021 16:04:00 +0000 https://supabase.com/blog/supabase-nft-marketplace Supabase Launches NFT Marketplace https://supabase.com/blog/supabase-nft-marketplace A fully encrypted NFT platform to protect and transact your digital assets - Thu, 01 Apr 2021 02:02:30 +0000 + Wed, 31 Mar 2021 16:04:00 +0000 https://supabase.com/blog/supabase-cli Supabase CLI https://supabase.com/blog/supabase-cli Local development, database migrations, and self-hosting. - Wed, 31 Mar 2021 02:02:30 +0000 + Tue, 30 Mar 2021 16:04:00 +0000 https://supabase.com/blog/supabase-storage Storage is now available in Supabase https://supabase.com/blog/supabase-storage Launching Supabase Storage and how you can use it in your apps - Tue, 30 Mar 2021 02:02:30 +0000 + Mon, 29 Mar 2021 16:04:00 +0000 https://supabase.com/blog/pricing Supabase Beta Pricing https://supabase.com/blog/pricing Supabase launches Beta pricing structure - Mon, 29 Mar 2021 02:02:30 +0000 + Sun, 28 Mar 2021 16:04:00 +0000 https://supabase.com/blog/launch-week Launch week https://supabase.com/blog/launch-week Five days of Supabase. - Wed, 24 Mar 2021 02:02:30 +0000 + Wed, 24 Mar 2021 16:04:00 +0000 https://supabase.com/blog/angels-of-supabase Angels of Supabase https://supabase.com/blog/angels-of-supabase Meet the investors of Supabase. - Wed, 24 Mar 2021 02:02:30 +0000 + Wed, 24 Mar 2021 16:04:00 +0000 https://supabase.com/blog/In-The-Loop Developers stay up to date with intheloop.dev https://supabase.com/blog/In-The-Loop Learn why Kevin is building intheloop.dev with Supabase - Mon, 22 Mar 2021 02:02:30 +0000 + Sun, 21 Mar 2021 16:04:00 +0000 https://supabase.com/blog/using-supabase-replit Using Supabase in Replit https://supabase.com/blog/using-supabase-replit Free hosted relational database from within your node.js repl - Thu, 11 Mar 2021 03:03:30 +0000 + Wed, 10 Mar 2021 16:04:00 +0000 https://supabase.com/blog/toad-a-link-shortener-with-simple-apis-for-low-coders Toad, a link shortener with simple APIs for low-coders https://supabase.com/blog/toad-a-link-shortener-with-simple-apis-for-low-coders An easy-to-use link shortening tool with simple APIs - Mon, 08 Mar 2021 03:03:30 +0000 + Sun, 07 Mar 2021 16:04:00 +0000 https://supabase.com/blog/postgres-as-a-cron-server Postgres as a CRON Server https://supabase.com/blog/postgres-as-a-cron-server Running repetitive tasks with your Postgres database. - Fri, 05 Mar 2021 03:03:30 +0000 + Thu, 04 Mar 2021 16:04:00 +0000 https://supabase.com/blog/supabase-beta-february-2021 Supabase Beta February 2021 https://supabase.com/blog/supabase-beta-february-2021 One year of building. - Tue, 02 Mar 2021 03:03:30 +0000 + Mon, 01 Mar 2021 16:04:00 +0000 https://supabase.com/blog/cracking-postgres-interview Cracking PostgreSQL Interview Questions https://supabase.com/blog/cracking-postgres-interview Understand the top PostgreSQL Interview Questions - Sat, 27 Feb 2021 03:03:30 +0000 + Fri, 26 Feb 2021 16:04:00 +0000 https://supabase.com/blog/case-study-roboflow Roboflow.com choose Supabase to power Paint.wtf leaderboard https://supabase.com/blog/case-study-roboflow Learn how Roboflow.com used Supabase to build their Paint.wtf leaderboard - Tue, 09 Feb 2021 03:03:30 +0000 + Mon, 08 Feb 2021 16:04:00 +0000 https://supabase.com/blog/supabase-beta-january-2021 Supabase Beta January 2021 https://supabase.com/blog/supabase-beta-january-2021 Eleven months of building. - Tue, 02 Feb 2021 03:03:30 +0000 + Mon, 01 Feb 2021 16:04:00 +0000 https://supabase.com/blog/supabase-beta-december-2020 Supabase Beta December 2020 https://supabase.com/blog/supabase-beta-december-2020 Ten months of building. - Sat, 02 Jan 2021 03:03:30 +0000 + Fri, 01 Jan 2021 16:04:00 +0000 https://supabase.com/blog/supabase-dashboard-performance Making the Supabase Dashboard Supa-fast https://supabase.com/blog/supabase-dashboard-performance Improving the performance of the Supabase dashboard - Sun, 13 Dec 2020 03:03:30 +0000 + Sat, 12 Dec 2020 16:04:00 +0000 https://supabase.com/blog/supabase-striveschool Supabase Partners With Strive School To Help Teach Open Source https://supabase.com/blog/supabase-striveschool Supabase Partners With Strive School To Help Teach Open Source To The Next Generation Of Developers - Wed, 02 Dec 2020 03:03:30 +0000 + Tue, 01 Dec 2020 16:04:00 +0000 https://supabase.com/blog/case-study-xendit Xendit Built a Counter-Fraud Watchlist for the Fintech Industry https://supabase.com/blog/case-study-xendit See how Xendit use Supabase to build a full-text search engine. - Wed, 02 Dec 2020 03:03:30 +0000 + Tue, 01 Dec 2020 16:04:00 +0000 https://supabase.com/blog/case-study-tayfa TAYFA Built a No-Code Website Builder in Seven Days https://supabase.com/blog/case-study-tayfa See how Tayfa went from idea to paying customer in less than 30 days. - Wed, 02 Dec 2020 03:03:30 +0000 + Tue, 01 Dec 2020 16:04:00 +0000 https://supabase.com/blog/case-study-monitoro Monitoro Built a Web Crawler Handling Millions of API Requests https://supabase.com/blog/case-study-monitoro See how Monitoro built an automated scraping platform using Supabase. - Wed, 02 Dec 2020 03:03:30 +0000 + Tue, 01 Dec 2020 16:04:00 +0000 https://supabase.com/blog/supabase-alpha-november-2020 Supabase Alpha November 2020 https://supabase.com/blog/supabase-alpha-november-2020 Nine months of building. - Tue, 01 Dec 2020 03:03:30 +0000 + Mon, 30 Nov 2020 16:04:00 +0000 https://supabase.com/blog/postgresql-views Postgres Views https://supabase.com/blog/postgresql-views Creating and using a view in PostgreSQL. - Wed, 18 Nov 2020 03:03:30 +0000 + Tue, 17 Nov 2020 16:04:00 +0000 https://supabase.com/blog/supabase-alpha-october-2020 Supabase Alpha October 2020 https://supabase.com/blog/supabase-alpha-october-2020 Eight months of building. - Mon, 02 Nov 2020 03:03:30 +0000 + Sun, 01 Nov 2020 16:04:00 +0000 https://supabase.com/blog/improved-dx Supabase.js 1.0 https://supabase.com/blog/improved-dx We're releasing a new version of our Supabase client with some awesome new improvements. - Fri, 30 Oct 2020 02:02:30 +0000 + Thu, 29 Oct 2020 16:04:00 +0000 https://supabase.com/blog/supabase-alpha-september-2020 Supabase Alpha September 2020 https://supabase.com/blog/supabase-alpha-september-2020 Seven months of building. - Sat, 03 Oct 2020 02:02:30 +0000 + Fri, 02 Oct 2020 16:04:00 +0000 https://supabase.com/blog/supabase-hacktoberfest-2020 Supabase Hacktoberfest 2020 https://supabase.com/blog/supabase-hacktoberfest-2020 Join us for a celebration of open source software and learn how to contribute to Supabase. - Fri, 11 Sep 2020 02:02:30 +0000 + Thu, 10 Sep 2020 16:04:00 +0000 https://supabase.com/blog/supabase-alpha-august-2020 Supabase Alpha August 2020 https://supabase.com/blog/supabase-alpha-august-2020 Six months of building - Thu, 03 Sep 2020 02:02:30 +0000 + Wed, 02 Sep 2020 16:04:00 +0000 https://supabase.com/blog/supabase-auth Supabase Auth https://supabase.com/blog/supabase-auth Authenticate and authorize your users with Supabase Auth - Wed, 05 Aug 2020 02:02:30 +0000 + Tue, 04 Aug 2020 16:04:00 +0000 https://supabase.com/blog/supabase-alpha-july-2020 Supabase Alpha July 2020 https://supabase.com/blog/supabase-alpha-july-2020 Five months of building - Sun, 02 Aug 2020 02:02:30 +0000 + Sat, 01 Aug 2020 16:04:00 +0000 https://supabase.com/blog/continuous-postgresql-backup-walg Continuous PostgreSQL Backups using WAL-G https://supabase.com/blog/continuous-postgresql-backup-walg Have you ever wanted to restore your database's state to a particular moment in time? This post explains how, using WAL-G. - Sun, 02 Aug 2020 02:02:30 +0000 + Sat, 01 Aug 2020 16:04:00 +0000 https://supabase.com/blog/alpha-launch-postmortem Alpha Launch Postmortem https://supabase.com/blog/alpha-launch-postmortem Everything that went wrong with Supabase's launch - Fri, 10 Jul 2020 02:02:30 +0000 + Thu, 09 Jul 2020 16:04:00 +0000 https://supabase.com/blog/postgresql-templates What are PostgreSQL Templates? https://supabase.com/blog/postgresql-templates What are PostgreSQL templates and what are they used for? - Thu, 09 Jul 2020 02:02:30 +0000 + Wed, 08 Jul 2020 16:04:00 +0000 https://supabase.com/blog/postgresql-physical-logical-backups Physical vs Logical Backups in PostgreSQL https://supabase.com/blog/postgresql-physical-logical-backups What are physical and logical backups in Postgres? - Tue, 07 Jul 2020 02:02:30 +0000 + Mon, 06 Jul 2020 16:04:00 +0000 https://supabase.com/blog/supabase-alpha-june-2020 Supabase Alpha June 2020 https://supabase.com/blog/supabase-alpha-june-2020 Four months of building - Wed, 01 Jul 2020 02:02:30 +0000 + Tue, 30 Jun 2020 16:04:00 +0000 https://supabase.com/blog/supabase-steve-chavez Steve Chavez has joined Supabase https://supabase.com/blog/supabase-steve-chavez Steve joins Supabase to help build Auth. - Mon, 15 Jun 2020 02:02:30 +0000 + Sun, 14 Jun 2020 16:04:00 +0000 https://supabase.com/blog/supabase-alpha-may-2020 Supabase Alpha May 2020 https://supabase.com/blog/supabase-alpha-may-2020 Three months of building - Mon, 01 Jun 2020 02:02:30 +0000 + Sun, 31 May 2020 16:04:00 +0000 https://supabase.com/blog/supabase-alpha-april-2020 Supabase Alpha April 2020 https://supabase.com/blog/supabase-alpha-april-2020 Two months of building - Mon, 01 Jun 2020 02:02:30 +0000 + Sun, 31 May 2020 16:04:00 +0000 diff --git a/apps/www/public/sitemap_www.xml b/apps/www/public/sitemap_www.xml index 3d294d2668b..3573f809d82 100644 --- a/apps/www/public/sitemap_www.xml +++ b/apps/www/public/sitemap_www.xml @@ -84,6 +84,12 @@ 0.5 + + https://supabase.com/launch-week/ticket-image + weekly + 0.5 + + https://supabase.com/legal/dpa weekly @@ -902,6 +908,12 @@ 0.5 + + https://supabase.com/blog/flutter-authentication-and-authorization-with-rls + weekly + 0.5 + + https://supabase.com/alternatives/supabase-vs-firebase weekly diff --git a/apps/www/styles/index.css b/apps/www/styles/index.css index b86beb234ef..aa7a528d138 100644 --- a/apps/www/styles/index.css +++ b/apps/www/styles/index.css @@ -373,6 +373,7 @@ launch week :root { --auth-ui-card-height: 50vh; + --lw-secondary-color: #8a8f98; } .glow-area { diff --git a/examples/edge-functions/supabase/functions/oak-server/README.md b/examples/edge-functions/supabase/functions/oak-server/README.md new file mode 100644 index 00000000000..b1db6166231 --- /dev/null +++ b/examples/edge-functions/supabase/functions/oak-server/README.md @@ -0,0 +1,24 @@ +# Writing functions using Oak Server Middleware + +This example shows how you can write functions using Oak server middleware (https://oakserver.github.io/oak/) + +## Run locally + +```bash +supabase functions serve oak-server +``` + +Use cURL or Postman to make a POST request to http://localhost:54321/functions/v1/oak-server. + +``` + curl --location --request POST 'http://localhost:54321/functions/v1/oak-server' \ +--header 'Authorization: Bearer YOUR_TOKEN' \ +--header 'Content-Type: application/json' \ +--data-raw '{ "name": "John Doe" }' +``` + +## Deploy + +```bash +supabase functions deploy oak-server +``` diff --git a/examples/edge-functions/supabase/functions/oak-server/index.ts b/examples/edge-functions/supabase/functions/oak-server/index.ts new file mode 100644 index 00000000000..8e96d1f0033 --- /dev/null +++ b/examples/edge-functions/supabase/functions/oak-server/index.ts @@ -0,0 +1,25 @@ +import { Application, Router } from 'https://deno.land/x/oak/mod.ts' + +const router = new Router() +router + // Note: path will be prefixed with function name + .get('/oak-server', (context) => { + context.response.body = 'This is an example Oak server running on Edge Functions!' + }) + .post('/oak-server/greet', (context) => { + // Note: request body will be streamed to the function as chunks, set limit to 0 to fully read it. + const result = context.request.body({ type: 'json', limit: 0 }) + const body = await result.value + const name = body.name || 'you' + + context.response.body = { msg: `Hey ${name}!` } + }) + .get('/oak-server/redirect', (context) => { + context.response.redirect('https://www.example.com') + }) + +const app = new Application() +app.use(router.routes()) +app.use(router.allowedMethods()) + +await app.listen({ port: 8000 }) diff --git a/examples/user-management/flutter-user-management/.gitignore b/examples/user-management/flutter-user-management/.gitignore index 0fa6b675c0a..6d47c6f3495 100644 --- a/examples/user-management/flutter-user-management/.gitignore +++ b/examples/user-management/flutter-user-management/.gitignore @@ -30,6 +30,7 @@ .pub-cache/ .pub/ /build/ +pubspec.lock # Web related lib/generated_plugin_registrant.dart diff --git a/examples/user-management/flutter-user-management/pubspec.lock b/examples/user-management/flutter-user-management/pubspec.lock deleted file mode 100644 index 306226cee79..00000000000 --- a/examples/user-management/flutter-user-management/pubspec.lock +++ /dev/null @@ -1,516 +0,0 @@ -# Generated by pub -# See https://dart.dev/tools/pub/glossary#lockfile -packages: - app_links: - dependency: transitive - description: - name: app_links - url: "https://pub.dartlang.org" - source: hosted - version: "3.2.0" - app_links_macos: - dependency: transitive - description: - name: app_links_macos - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.0" - app_links_platform_interface: - dependency: transitive - description: - name: app_links_platform_interface - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.2" - app_links_web: - dependency: transitive - description: - name: app_links_web - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.0" - app_links_windows: - dependency: transitive - description: - name: app_links_windows - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.0" - async: - dependency: transitive - description: - name: async - url: "https://pub.dartlang.org" - source: hosted - version: "2.9.0" - boolean_selector: - dependency: transitive - description: - name: boolean_selector - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.0" - characters: - dependency: transitive - description: - name: characters - url: "https://pub.dartlang.org" - source: hosted - version: "1.2.1" - clock: - dependency: transitive - description: - name: clock - url: "https://pub.dartlang.org" - source: hosted - version: "1.1.1" - collection: - dependency: transitive - description: - name: collection - url: "https://pub.dartlang.org" - source: hosted - version: "1.16.0" - cross_file: - dependency: transitive - description: - name: cross_file - url: "https://pub.dartlang.org" - source: hosted - version: "0.3.3+1" - crypto: - dependency: transitive - description: - name: crypto - url: "https://pub.dartlang.org" - source: hosted - version: "3.0.2" - fake_async: - dependency: transitive - description: - name: fake_async - url: "https://pub.dartlang.org" - source: hosted - version: "1.3.1" - ffi: - dependency: transitive - description: - name: ffi - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.1" - file: - dependency: transitive - description: - name: file - url: "https://pub.dartlang.org" - source: hosted - version: "6.1.2" - flutter: - dependency: "direct main" - description: flutter - source: sdk - version: "0.0.0" - flutter_plugin_android_lifecycle: - dependency: transitive - description: - name: flutter_plugin_android_lifecycle - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.7" - flutter_test: - dependency: "direct dev" - description: flutter - source: sdk - version: "0.0.0" - flutter_web_plugins: - dependency: transitive - description: flutter - source: sdk - version: "0.0.0" - functions_client: - dependency: transitive - description: - name: functions_client - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.0" - gotrue: - dependency: transitive - description: - name: gotrue - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.0" - hive: - dependency: transitive - description: - name: hive - url: "https://pub.dartlang.org" - source: hosted - version: "2.2.3" - hive_flutter: - dependency: transitive - description: - name: hive_flutter - url: "https://pub.dartlang.org" - source: hosted - version: "1.1.0" - http: - dependency: transitive - description: - name: http - url: "https://pub.dartlang.org" - source: hosted - version: "0.13.5" - http_parser: - dependency: transitive - description: - name: http_parser - url: "https://pub.dartlang.org" - source: hosted - version: "4.0.1" - image_picker: - dependency: "direct main" - description: - name: image_picker - url: "https://pub.dartlang.org" - source: hosted - version: "0.8.5+3" - image_picker_android: - dependency: transitive - description: - name: image_picker_android - url: "https://pub.dartlang.org" - source: hosted - version: "0.8.5+1" - image_picker_for_web: - dependency: transitive - description: - name: image_picker_for_web - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.8" - image_picker_ios: - dependency: transitive - description: - name: image_picker_ios - url: "https://pub.dartlang.org" - source: hosted - version: "0.8.5+6" - image_picker_platform_interface: - dependency: transitive - description: - name: image_picker_platform_interface - url: "https://pub.dartlang.org" - source: hosted - version: "2.6.1" - js: - dependency: transitive - description: - name: js - url: "https://pub.dartlang.org" - source: hosted - version: "0.6.4" - jwt_decode: - dependency: transitive - description: - name: jwt_decode - url: "https://pub.dartlang.org" - source: hosted - version: "0.3.1" - lint: - dependency: "direct dev" - description: - name: lint - url: "https://pub.dartlang.org" - source: hosted - version: "1.10.0" - matcher: - dependency: transitive - description: - name: matcher - url: "https://pub.dartlang.org" - source: hosted - version: "0.12.12" - material_color_utilities: - dependency: transitive - description: - name: material_color_utilities - url: "https://pub.dartlang.org" - source: hosted - version: "0.1.5" - meta: - dependency: transitive - description: - name: meta - url: "https://pub.dartlang.org" - source: hosted - version: "1.8.0" - mime: - dependency: transitive - description: - name: mime - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.2" - path: - dependency: transitive - description: - name: path - url: "https://pub.dartlang.org" - source: hosted - version: "1.8.2" - path_provider: - dependency: transitive - description: - name: path_provider - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.11" - path_provider_android: - dependency: transitive - description: - name: path_provider_android - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.17" - path_provider_ios: - dependency: transitive - description: - name: path_provider_ios - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.11" - path_provider_linux: - dependency: transitive - description: - name: path_provider_linux - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.7" - path_provider_macos: - dependency: transitive - description: - name: path_provider_macos - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.6" - path_provider_platform_interface: - dependency: transitive - description: - name: path_provider_platform_interface - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.4" - path_provider_windows: - dependency: transitive - description: - name: path_provider_windows - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.1" - platform: - dependency: transitive - description: - name: platform - url: "https://pub.dartlang.org" - source: hosted - version: "3.1.0" - plugin_platform_interface: - dependency: transitive - description: - name: plugin_platform_interface - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.2" - postgrest: - dependency: transitive - description: - name: postgrest - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.0" - process: - dependency: transitive - description: - name: process - url: "https://pub.dartlang.org" - source: hosted - version: "4.2.4" - realtime_client: - dependency: transitive - description: - name: realtime_client - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.0" - sky_engine: - dependency: transitive - description: flutter - source: sdk - version: "0.0.99" - source_span: - dependency: transitive - description: - name: source_span - url: "https://pub.dartlang.org" - source: hosted - version: "1.9.0" - stack_trace: - dependency: transitive - description: - name: stack_trace - url: "https://pub.dartlang.org" - source: hosted - version: "1.10.0" - storage_client: - dependency: transitive - description: - name: storage_client - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.0" - stream_channel: - dependency: transitive - description: - name: stream_channel - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.0" - string_scanner: - dependency: transitive - description: - name: string_scanner - url: "https://pub.dartlang.org" - source: hosted - version: "1.1.1" - supabase: - dependency: transitive - description: - name: supabase - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.1" - supabase_flutter: - dependency: "direct main" - description: - name: supabase_flutter - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.1" - term_glyph: - dependency: transitive - description: - name: term_glyph - url: "https://pub.dartlang.org" - source: hosted - version: "1.2.1" - test_api: - dependency: transitive - description: - name: test_api - url: "https://pub.dartlang.org" - source: hosted - version: "0.4.12" - typed_data: - dependency: transitive - description: - name: typed_data - url: "https://pub.dartlang.org" - source: hosted - version: "1.3.1" - universal_io: - dependency: transitive - description: - name: universal_io - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.4" - url_launcher: - dependency: transitive - description: - name: url_launcher - url: "https://pub.dartlang.org" - source: hosted - version: "6.1.5" - url_launcher_android: - dependency: transitive - description: - name: url_launcher_android - url: "https://pub.dartlang.org" - source: hosted - version: "6.0.17" - url_launcher_ios: - dependency: transitive - description: - name: url_launcher_ios - url: "https://pub.dartlang.org" - source: hosted - version: "6.0.17" - url_launcher_linux: - dependency: transitive - description: - name: url_launcher_linux - url: "https://pub.dartlang.org" - source: hosted - version: "3.0.1" - url_launcher_macos: - dependency: transitive - description: - name: url_launcher_macos - url: "https://pub.dartlang.org" - source: hosted - version: "3.0.1" - url_launcher_platform_interface: - dependency: transitive - description: - name: url_launcher_platform_interface - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.0" - url_launcher_web: - dependency: transitive - description: - name: url_launcher_web - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.12" - url_launcher_windows: - dependency: transitive - description: - name: url_launcher_windows - url: "https://pub.dartlang.org" - source: hosted - version: "3.0.1" - vector_math: - dependency: transitive - description: - name: vector_math - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.2" - web_socket_channel: - dependency: transitive - description: - name: web_socket_channel - url: "https://pub.dartlang.org" - source: hosted - version: "2.2.0" - win32: - dependency: transitive - description: - name: win32 - url: "https://pub.dartlang.org" - source: hosted - version: "2.7.0" - xdg_directories: - dependency: transitive - description: - name: xdg_directories - url: "https://pub.dartlang.org" - source: hosted - version: "0.2.0+1" -sdks: - dart: ">=2.17.0 <3.0.0" - flutter: ">=3.0.0" diff --git a/examples/user-management/flutter-user-management/pubspec.yaml b/examples/user-management/flutter-user-management/pubspec.yaml index c38e49f8498..82b27ca5aea 100644 --- a/examples/user-management/flutter-user-management/pubspec.yaml +++ b/examples/user-management/flutter-user-management/pubspec.yaml @@ -13,7 +13,7 @@ dependencies: sdk: flutter image_picker: ^0.8.4 - supabase_flutter: ^1.0.0 + supabase_flutter: ^1.2.0 dev_dependencies: flutter_test: diff --git a/package-lock.json b/package-lock.json index f0c3b85c5ce..b1b1707e092 100644 --- a/package-lock.json +++ b/package-lock.json @@ -41,7 +41,7 @@ "@algolia/autocomplete-js": "^1.7.2", "@algolia/autocomplete-plugin-recent-searches": "^1.7.2", "@apollo/client": "^3.6.6", - "@docsearch/react": "1.0.0-alpha.27", + "@docsearch/react": "^3.3.0", "@mdx-js/loader": "^1.6.22", "@mdx-js/react": "^1.6.22", "@next/mdx": "^12.0.4", @@ -228,7 +228,7 @@ "gray-matter": "^4.0.2", "markdown-toc": "^1.2.0", "marked": "^4.0.12", - "next": "latest", + "next": "12.1.0", "next-compose-plugins": "^2.2.1", "next-mdx-remote": "^4.1.0", "next-seo": "^4.20.0", @@ -254,6 +254,7 @@ "ui": "*", "use-debounce": "^7.0.1", "uuid": "^8.3.2", + "vanilla-tilt": "1.7.0", "yup": "^0.32.11" }, "devDependencies": { @@ -2820,25 +2821,62 @@ } }, "node_modules/@docsearch/css": { - "version": "1.0.0-alpha.28", - "resolved": "https://registry.npmjs.org/@docsearch/css/-/css-1.0.0-alpha.28.tgz", - "integrity": "sha512-1AhRzVdAkrWwhaxTX6/R7SnFHz8yLz1W8I/AldlTrfbNvZs9INk1FZiEFTJdgHaP68nhgQNWSGlQiDiI3y2RYg==" + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@docsearch/css/-/css-3.3.0.tgz", + "integrity": "sha512-rODCdDtGyudLj+Va8b6w6Y85KE85bXRsps/R4Yjwt5vueXKXZQKYw0aA9knxLBT6a/bI/GMrAcmCR75KYOM6hg==" }, "node_modules/@docsearch/react": { - "version": "1.0.0-alpha.27", - "resolved": "https://registry.npmjs.org/@docsearch/react/-/react-1.0.0-alpha.27.tgz", - "integrity": "sha512-jcgUHZsrNNRsaVsplqKhXWheh4VzRTCdhsPuVhJMRvfsFUqXEPo/7kVt5xIybtOj9u+/FVdeSO+APJEE2rakYA==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@docsearch/react/-/react-3.3.0.tgz", + "integrity": "sha512-fhS5adZkae2SSdMYEMVg6pxI5a/cE+tW16ki1V0/ur4Fdok3hBRkmN/H8VvlXnxzggkQIIRIVvYPn00JPjen3A==", "dependencies": { - "@docsearch/css": "^1.0.0-alpha.27", - "@francoischalifour/autocomplete-core": "^1.0.0-alpha.27", - "@francoischalifour/autocomplete-preset-algolia": "^1.0.0-alpha.27", + "@algolia/autocomplete-core": "1.7.2", + "@algolia/autocomplete-preset-algolia": "1.7.2", + "@docsearch/css": "3.3.0", "algoliasearch": "^4.0.0" }, "peerDependencies": { - "react": "^16.8.0", - "react-dom": "^16.8.0" + "@types/react": ">= 16.8.0 < 19.0.0", + "react": ">= 16.8.0 < 19.0.0", + "react-dom": ">= 16.8.0 < 19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } } }, + "node_modules/@docsearch/react/node_modules/@algolia/autocomplete-core": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-core/-/autocomplete-core-1.7.2.tgz", + "integrity": "sha512-eclwUDC6qfApNnEfu1uWcL/rudQsn59tjEoUYZYE2JSXZrHLRjBUGMxiCoknobU2Pva8ejb0eRxpIYDtVVqdsw==", + "dependencies": { + "@algolia/autocomplete-shared": "1.7.2" + } + }, + "node_modules/@docsearch/react/node_modules/@algolia/autocomplete-preset-algolia": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-preset-algolia/-/autocomplete-preset-algolia-1.7.2.tgz", + "integrity": "sha512-+RYEG6B0QiGGfRb2G3MtPfyrl0dALF3cQNTWBzBX6p5o01vCCGTTinAm2UKG3tfc2CnOMAtnPLkzNZyJUpnVJw==", + "dependencies": { + "@algolia/autocomplete-shared": "1.7.2" + }, + "peerDependencies": { + "@algolia/client-search": ">= 4.9.1 < 6", + "algoliasearch": ">= 4.9.1 < 6" + } + }, + "node_modules/@docsearch/react/node_modules/@algolia/autocomplete-shared": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-shared/-/autocomplete-shared-1.7.2.tgz", + "integrity": "sha512-QCckjiC7xXHIUaIL3ektBtjJ0w7tTA3iqKcAE/Hjn1lZ5omp7i3Y4e09rAr9ZybqirL7AbxCLLq0Ra5DDPKeug==" + }, "node_modules/@eslint/eslintrc": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.3.tgz", @@ -2888,16 +2926,6 @@ "npm": ">=6.0.0" } }, - "node_modules/@francoischalifour/autocomplete-core": { - "version": "1.0.0-alpha.28", - "resolved": "https://registry.npmjs.org/@francoischalifour/autocomplete-core/-/autocomplete-core-1.0.0-alpha.28.tgz", - "integrity": "sha512-rL9x+72btViw+9icfBKUJjZj87FgjFrD2esuTUqtj4RAX3s4AuVZiN8XEsfjQBSc6qJk31cxlvqZHC/BIyYXgg==" - }, - "node_modules/@francoischalifour/autocomplete-preset-algolia": { - "version": "1.0.0-alpha.28", - "resolved": "https://registry.npmjs.org/@francoischalifour/autocomplete-preset-algolia/-/autocomplete-preset-algolia-1.0.0-alpha.28.tgz", - "integrity": "sha512-bprfNmYt1opFUFEtD2XfY/kEsm13bzHQgU80uMjhuK0DJ914IjolT1GytpkdM6tJ4MBvyiJPP+bTtWO+BZ7c7w==" - }, "node_modules/@gar/promisify": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", @@ -9496,6 +9524,75 @@ "tailwindcss": ">=2.0.0" } }, + "node_modules/@tanstack/match-sorter-utils": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@tanstack/match-sorter-utils/-/match-sorter-utils-8.1.1.tgz", + "integrity": "sha512-IdmEekEYxQsoLOR0XQyw3jD1GujBpRRYaGJYQUw1eOT1eUugWxdc7jomh1VQ1EKHcdwDLpLaCz/8y4KraU4T9A==", + "dependencies": { + "remove-accents": "0.4.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kentcdodds" + } + }, + "node_modules/@tanstack/query-core": { + "version": "4.15.1", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-4.15.1.tgz", + "integrity": "sha512-+UfqJsNbPIVo0a9ANW0ZxtjiMfGLaaoIaL9vZeVycvmBuWywJGtSi7fgPVMCPdZQFOzMsaXaOsDtSKQD5xLRVQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "4.16.1", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-4.16.1.tgz", + "integrity": "sha512-PDE9u49wSDykPazlCoLFevUpceLjQ0Mm8i6038HgtTEKb/aoVnUZdlUP7C392ds3Cd75+EGlHU7qpEX06R7d9Q==", + "dependencies": { + "@tanstack/query-core": "4.15.1", + "use-sync-external-store": "^1.2.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-native": "*" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, + "node_modules/@tanstack/react-query-devtools": { + "version": "4.16.1", + "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-4.16.1.tgz", + "integrity": "sha512-VrDYLmG+OOcvGSZL5avG4R8jhqeMFP7pzW2sh2BWEV9UfI+aocG+CW8y8ygacxuKy48m8Tyo/xfe8H1z9BGb+g==", + "dependencies": { + "@tanstack/match-sorter-utils": "8.1.1", + "superjson": "^1.10.0", + "use-sync-external-store": "^1.2.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "@tanstack/react-query": "4.16.1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/@testdeck/core": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/@testdeck/core/-/core-0.2.2.tgz", @@ -13563,6 +13660,20 @@ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", "dev": true }, + "node_modules/copy-anything": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-3.0.2.tgz", + "integrity": "sha512-CzATjGXzUQ0EvuvgOCI6A4BGOo2bcVx8B+eC2nF862iv9fopnPQwlrbACakNCHRIJbCSBj+J/9JeDf60k64MkA==", + "dependencies": { + "is-what": "^4.1.6" + }, + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, "node_modules/copy-concurrently": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/copy-concurrently/-/copy-concurrently-1.0.5.tgz", @@ -20395,6 +20506,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-what": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.7.tgz", + "integrity": "sha512-DBVOQNiPKnGMxRMLIYSwERAS5MVY1B7xYiGnpgctsOFvVDz9f9PFXXxMcTOHuoqYp4NK9qFYQaIC1NRRxLMpBQ==", + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, "node_modules/is-whitespace-character": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-whitespace-character/-/is-whitespace-character-1.0.4.tgz", @@ -31101,6 +31223,11 @@ "sprintf-js": "~1.0.2" } }, + "node_modules/remove-accents": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.4.2.tgz", + "integrity": "sha512-7pXIJqJOq5tFgG1A2Zxti3Ht8jJF337m4sowbuHsW30ZnkQFnDzy9qBNhgzX8ZLW4+UBcXiiR7SwR6pokHsxiA==" + }, "node_modules/remove-trailing-separator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", @@ -32917,6 +33044,17 @@ "resolved": "tests", "link": true }, + "node_modules/superjson": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/superjson/-/superjson-1.11.0.tgz", + "integrity": "sha512-6PfAg1FKhqkwWvPb2uXhH4MkMttdc17eJ91+Aoz4s1XUEDZFmLfFx/xVA3wgkPxAGy5dpozgGdK6V/n20Wj9yg==", + "dependencies": { + "copy-anything": "^3.0.2" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -34999,6 +35137,14 @@ "react": "^16.8.0 || ^17.0.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", + "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/utf-8-validate": { "version": "5.0.10", "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz", @@ -35157,6 +35303,11 @@ "spdx-expression-parse": "^3.0.0" } }, + "node_modules/vanilla-tilt": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/vanilla-tilt/-/vanilla-tilt-1.7.0.tgz", + "integrity": "sha512-u9yUhpSasFeqQCuiTon+RSb0aHzcj9stvWVXQIswzKL5oG491lkYk7U1GmhOAEZt7yPT6EiYZRJhIh2MFBncOA==" + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -36857,7 +37008,9 @@ "@supabase/postgres-meta": "^0.26.1", "@supabase/react-data-grid": "^7.1.0-beta.7", "@supabase/shared-types": "^0.1.29", - "@supabase/supabase-js": "^2.1.0", + "@supabase/supabase-js": "^2.1.1", + "@tanstack/react-query": "^4.16.1", + "@tanstack/react-query-devtools": "^4.16.1", "ajv": "^8.6.3", "analytics-node": "^3.5.0", "awesome-debounce-promise": "^2.1.0", @@ -36961,7 +37114,7 @@ "storybook-dark-mode": "^1.0.4", "tailwindcss": "^3.0.22", "ts-jest": "^27.0.7", - "typescript": "4.3.4" + "typescript": "4.8.4" } }, "studio/node_modules/@next/bundle-analyzer": { @@ -37418,9 +37571,9 @@ } }, "studio/node_modules/typescript": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.4.tgz", - "integrity": "sha512-uauPG7XZn9F/mo+7MrsRjyvbxFpzemRjKEZXS4AK83oP2KKOJPvb+9cO/gmnv8arWZvhnjVOXz7B49m1l0e9Ew==", + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.4.tgz", + "integrity": "sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==", "dev": true, "bin": { "tsc": "bin/tsc", @@ -39308,19 +39461,42 @@ "dev": true }, "@docsearch/css": { - "version": "1.0.0-alpha.28", - "resolved": "https://registry.npmjs.org/@docsearch/css/-/css-1.0.0-alpha.28.tgz", - "integrity": "sha512-1AhRzVdAkrWwhaxTX6/R7SnFHz8yLz1W8I/AldlTrfbNvZs9INk1FZiEFTJdgHaP68nhgQNWSGlQiDiI3y2RYg==" + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@docsearch/css/-/css-3.3.0.tgz", + "integrity": "sha512-rODCdDtGyudLj+Va8b6w6Y85KE85bXRsps/R4Yjwt5vueXKXZQKYw0aA9knxLBT6a/bI/GMrAcmCR75KYOM6hg==" }, "@docsearch/react": { - "version": "1.0.0-alpha.27", - "resolved": "https://registry.npmjs.org/@docsearch/react/-/react-1.0.0-alpha.27.tgz", - "integrity": "sha512-jcgUHZsrNNRsaVsplqKhXWheh4VzRTCdhsPuVhJMRvfsFUqXEPo/7kVt5xIybtOj9u+/FVdeSO+APJEE2rakYA==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@docsearch/react/-/react-3.3.0.tgz", + "integrity": "sha512-fhS5adZkae2SSdMYEMVg6pxI5a/cE+tW16ki1V0/ur4Fdok3hBRkmN/H8VvlXnxzggkQIIRIVvYPn00JPjen3A==", "requires": { - "@docsearch/css": "^1.0.0-alpha.27", - "@francoischalifour/autocomplete-core": "^1.0.0-alpha.27", - "@francoischalifour/autocomplete-preset-algolia": "^1.0.0-alpha.27", + "@algolia/autocomplete-core": "1.7.2", + "@algolia/autocomplete-preset-algolia": "1.7.2", + "@docsearch/css": "3.3.0", "algoliasearch": "^4.0.0" + }, + "dependencies": { + "@algolia/autocomplete-core": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-core/-/autocomplete-core-1.7.2.tgz", + "integrity": "sha512-eclwUDC6qfApNnEfu1uWcL/rudQsn59tjEoUYZYE2JSXZrHLRjBUGMxiCoknobU2Pva8ejb0eRxpIYDtVVqdsw==", + "requires": { + "@algolia/autocomplete-shared": "1.7.2" + } + }, + "@algolia/autocomplete-preset-algolia": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-preset-algolia/-/autocomplete-preset-algolia-1.7.2.tgz", + "integrity": "sha512-+RYEG6B0QiGGfRb2G3MtPfyrl0dALF3cQNTWBzBX6p5o01vCCGTTinAm2UKG3tfc2CnOMAtnPLkzNZyJUpnVJw==", + "requires": { + "@algolia/autocomplete-shared": "1.7.2" + } + }, + "@algolia/autocomplete-shared": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-shared/-/autocomplete-shared-1.7.2.tgz", + "integrity": "sha512-QCckjiC7xXHIUaIL3ektBtjJ0w7tTA3iqKcAE/Hjn1lZ5omp7i3Y4e09rAr9ZybqirL7AbxCLLq0Ra5DDPKeug==" + } } }, "@eslint/eslintrc": { @@ -39360,16 +39536,6 @@ "integrity": "sha512-8YXBE2ZcU/pImVOHX7MWrSR/X5up7t6rPWZlk34RwZEcdr3ua6X+32pSd6XuOQRN+vbuvYNfA6iey8NbrjuMFQ==", "dev": true }, - "@francoischalifour/autocomplete-core": { - "version": "1.0.0-alpha.28", - "resolved": "https://registry.npmjs.org/@francoischalifour/autocomplete-core/-/autocomplete-core-1.0.0-alpha.28.tgz", - "integrity": "sha512-rL9x+72btViw+9icfBKUJjZj87FgjFrD2esuTUqtj4RAX3s4AuVZiN8XEsfjQBSc6qJk31cxlvqZHC/BIyYXgg==" - }, - "@francoischalifour/autocomplete-preset-algolia": { - "version": "1.0.0-alpha.28", - "resolved": "https://registry.npmjs.org/@francoischalifour/autocomplete-preset-algolia/-/autocomplete-preset-algolia-1.0.0-alpha.28.tgz", - "integrity": "sha512-bprfNmYt1opFUFEtD2XfY/kEsm13bzHQgU80uMjhuK0DJ914IjolT1GytpkdM6tJ4MBvyiJPP+bTtWO+BZ7c7w==" - }, "@gar/promisify": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", @@ -44312,6 +44478,38 @@ "lodash.uniq": "^4.5.0" } }, + "@tanstack/match-sorter-utils": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@tanstack/match-sorter-utils/-/match-sorter-utils-8.1.1.tgz", + "integrity": "sha512-IdmEekEYxQsoLOR0XQyw3jD1GujBpRRYaGJYQUw1eOT1eUugWxdc7jomh1VQ1EKHcdwDLpLaCz/8y4KraU4T9A==", + "requires": { + "remove-accents": "0.4.2" + } + }, + "@tanstack/query-core": { + "version": "4.15.1", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-4.15.1.tgz", + "integrity": "sha512-+UfqJsNbPIVo0a9ANW0ZxtjiMfGLaaoIaL9vZeVycvmBuWywJGtSi7fgPVMCPdZQFOzMsaXaOsDtSKQD5xLRVQ==" + }, + "@tanstack/react-query": { + "version": "4.16.1", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-4.16.1.tgz", + "integrity": "sha512-PDE9u49wSDykPazlCoLFevUpceLjQ0Mm8i6038HgtTEKb/aoVnUZdlUP7C392ds3Cd75+EGlHU7qpEX06R7d9Q==", + "requires": { + "@tanstack/query-core": "4.15.1", + "use-sync-external-store": "^1.2.0" + } + }, + "@tanstack/react-query-devtools": { + "version": "4.16.1", + "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-4.16.1.tgz", + "integrity": "sha512-VrDYLmG+OOcvGSZL5avG4R8jhqeMFP7pzW2sh2BWEV9UfI+aocG+CW8y8ygacxuKy48m8Tyo/xfe8H1z9BGb+g==", + "requires": { + "@tanstack/match-sorter-utils": "8.1.1", + "superjson": "^1.10.0", + "use-sync-external-store": "^1.2.0" + } + }, "@testdeck/core": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/@testdeck/core/-/core-0.2.2.tgz", @@ -47609,6 +47807,14 @@ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", "dev": true }, + "copy-anything": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-3.0.2.tgz", + "integrity": "sha512-CzATjGXzUQ0EvuvgOCI6A4BGOo2bcVx8B+eC2nF862iv9fopnPQwlrbACakNCHRIJbCSBj+J/9JeDf60k64MkA==", + "requires": { + "is-what": "^4.1.6" + } + }, "copy-concurrently": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/copy-concurrently/-/copy-concurrently-1.0.5.tgz", @@ -49232,7 +49438,7 @@ "@algolia/autocomplete-js": "^1.7.2", "@algolia/autocomplete-plugin-recent-searches": "^1.7.2", "@apollo/client": "^3.6.6", - "@docsearch/react": "1.0.0-alpha.27", + "@docsearch/react": "^3.3.0", "@mdx-js/loader": "^1.6.22", "@mdx-js/react": "^1.6.22", "@next/mdx": "^12.0.4", @@ -52995,6 +53201,11 @@ "get-intrinsic": "^1.1.1" } }, + "is-what": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.7.tgz", + "integrity": "sha512-DBVOQNiPKnGMxRMLIYSwERAS5MVY1B7xYiGnpgctsOFvVDz9f9PFXXxMcTOHuoqYp4NK9qFYQaIC1NRRxLMpBQ==" + }, "is-whitespace-character": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-whitespace-character/-/is-whitespace-character-1.0.4.tgz", @@ -61035,6 +61246,11 @@ } } }, + "remove-accents": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.4.2.tgz", + "integrity": "sha512-7pXIJqJOq5tFgG1A2Zxti3Ht8jJF337m4sowbuHsW30ZnkQFnDzy9qBNhgzX8ZLW4+UBcXiiR7SwR6pokHsxiA==" + }, "remove-trailing-separator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", @@ -62428,8 +62644,10 @@ "@supabase/postgres-meta": "^0.26.1", "@supabase/react-data-grid": "^7.1.0-beta.7", "@supabase/shared-types": "^0.1.29", - "@supabase/supabase-js": "^2.1.0", + "@supabase/supabase-js": "^2.1.1", "@tailwindcss/typography": "^0.5.2", + "@tanstack/react-query": "^4.16.1", + "@tanstack/react-query-devtools": "^4.16.1", "@testing-library/dom": "^8.11.1", "@testing-library/react": "^12.1.2", "@testing-library/user-event": "^13.5.0", @@ -62514,7 +62732,7 @@ "swr": "^1.2.1", "tailwindcss": "^3.0.22", "ts-jest": "^27.0.7", - "typescript": "4.3.4", + "typescript": "4.8.4", "ui": "*", "uniforms-bootstrap4": "^3.6.2", "uniforms-bridge-json-schema": "^3.6.2", @@ -62813,9 +63031,9 @@ "requires": {} }, "typescript": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.4.tgz", - "integrity": "sha512-uauPG7XZn9F/mo+7MrsRjyvbxFpzemRjKEZXS4AK83oP2KKOJPvb+9cO/gmnv8arWZvhnjVOXz7B49m1l0e9Ew==", + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.4.tgz", + "integrity": "sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==", "dev": true }, "webpack-bundle-analyzer": { @@ -63000,6 +63218,14 @@ } } }, + "superjson": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/superjson/-/superjson-1.11.0.tgz", + "integrity": "sha512-6PfAg1FKhqkwWvPb2uXhH4MkMttdc17eJ91+Aoz4s1XUEDZFmLfFx/xVA3wgkPxAGy5dpozgGdK6V/n20Wj9yg==", + "requires": { + "copy-anything": "^3.0.2" + } + }, "supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -64610,6 +64836,12 @@ "object-assign": "^4.1.1" } }, + "use-sync-external-store": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", + "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", + "requires": {} + }, "utf-8-validate": { "version": "5.0.10", "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz", @@ -64746,6 +64978,11 @@ "spdx-expression-parse": "^3.0.0" } }, + "vanilla-tilt": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/vanilla-tilt/-/vanilla-tilt-1.7.0.tgz", + "integrity": "sha512-u9yUhpSasFeqQCuiTon+RSb0aHzcj9stvWVXQIswzKL5oG491lkYk7U1GmhOAEZt7yPT6EiYZRJhIh2MFBncOA==" + }, "vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -65771,7 +66008,7 @@ "gray-matter": "^4.0.2", "markdown-toc": "^1.2.0", "marked": "^4.0.12", - "next": "latest", + "next": "12.1.0", "next-compose-plugins": "^2.2.1", "next-mdx-remote": "^4.1.0", "next-seo": "^4.20.0", @@ -65802,6 +66039,7 @@ "ui": "*", "use-debounce": "^7.0.1", "uuid": "^8.3.2", + "vanilla-tilt": "1.7.0", "yup": "^0.32.11" }, "dependencies": { diff --git a/spec/supabase_dart_v1.yml b/spec/supabase_dart_v1.yml index 382c76bae08..6f3ef71a653 100644 --- a/spec/supabase_dart_v1.yml +++ b/spec/supabase_dart_v1.yml @@ -836,7 +836,6 @@ pages: description: | Retrieves the details of all Storage buckets within an existing product. title: 'listBuckets()' - $ref: '@supabase/storage-js.packages/StorageBucketApi.default.listBuckets' notes: | - Policy permissions required: - `buckets` permissions: `select` @@ -855,7 +854,6 @@ pages: description: | Retrieves the details of an existing Storage bucket. title: 'getBucket()' - $ref: '@supabase/storage-js.packages/StorageBucketApi.default.getBucket' notes: | - Policy permissions required: - `buckets` permissions: `select` @@ -945,7 +943,6 @@ pages: description: | Uploads a file to an existing bucket. title: 'from.upload()' - $ref: '@supabase/storage-js.packages/StorageFileApi.default.upload' notes: | - Policy permissions required: - `buckets` permissions: none @@ -1022,6 +1019,24 @@ pages: .createSignedUrl('avatar1.png', 60); ``` + storage.from.createSignedUrls(): + title: 'from.createSignedUrls()' + notes: | + - RLS policy permissions required: + - `buckets` table permissions: none + - `objects` table permissions: `select` + - Refer to the [Storage guide](/docs/guides/storage#access-control) on how access control works + examples: + - name: Create Signed URLs + isSpotlight: true + dart: | + ```dart + final List signedUrls = await supabase + .storage + .from('avatars') + .createSignedUrls(['folder/avatar1.png', 'folder/avatar2.png'], 60); + ``` + storage.from.getPublicUrl(): description: | Retrieve URLs for assets in public buckets @@ -1671,7 +1686,6 @@ pages: .lte(): description: | Finds all rows whose value on the stated `column` is less than or equal to the specified `value`. - $ref: '@supabase/postgrest-js.PostgrestFilterBuilder.lte' examples: - name: With `select()` isSpotlight: true @@ -1710,7 +1724,6 @@ pages: .like(): description: | Finds all rows whose value in the stated `column` matches the supplied `pattern` (case sensitive). - $ref: '@supabase/postgrest-js.PostgrestFilterBuilder.lte' examples: - name: With `select()` isSpotlight: true @@ -2045,7 +2058,6 @@ pages: ``` .rangeLte(): - $ref: '@supabase/postgrest-js.PostgrestFilterBuilder.rangeLte' examples: - name: With `select()` isSpotlight: true diff --git a/studio/components/interfaces/Billing/AddOns/AddOns.utils.ts b/studio/components/interfaces/Billing/AddOns/AddOns.utils.ts index d3d559d3abf..4440c0a0f78 100644 --- a/studio/components/interfaces/Billing/AddOns/AddOns.utils.ts +++ b/studio/components/interfaces/Billing/AddOns/AddOns.utils.ts @@ -91,3 +91,39 @@ export const formatPITROptions = (addons: DatabaseAddon[]) => { if (pitrOptions.length === 0) return [] else return [noPITROption].concat(pitrOptions) } + +export const formatCustomDomainOptions = (addons: DatabaseAddon[]) => { + const customDomainOrder = ['addon_custom_domains'] + const noCustomDomainOption: DatabaseAddon = { + id: undefined, + name: 'Disable Custom Domains', + metadata: { + default_price_id: undefined, + supabase_prod_id: 'addon_custom_domains_disabled', + features: '', + }, + prices: [ + { + id: undefined, + currency: 'usd', + recurring: { + interval: 'month', + usage_type: 'licensed', + interval_count: 1, + aggregate_usage: null, + trial_period_days: null, + }, + unit_amount: 0, + }, + ], + } + + const customDomainOptions = customDomainOrder + .map((id: string) => { + return addons.find((option) => option.metadata.supabase_prod_id === id) + }) + .filter((option) => option !== undefined) as DatabaseAddon[] + + if (customDomainOptions.length === 0) return [] + else return [noCustomDomainOption].concat(customDomainOptions) +} diff --git a/studio/components/interfaces/Billing/AddOns/CustomDomainSelection.tsx b/studio/components/interfaces/Billing/AddOns/CustomDomainSelection.tsx new file mode 100644 index 00000000000..a43d9f17f34 --- /dev/null +++ b/studio/components/interfaces/Billing/AddOns/CustomDomainSelection.tsx @@ -0,0 +1,70 @@ +import { FC } from 'react' +import { Badge, Radio } from 'ui' + +import { useStore, useFlag } from 'hooks' +import { getProductPrice } from '../Billing.utils' +import DisabledWarningDueToIncident from 'components/ui/DisabledWarningDueToIncident' + +interface Props { + options: any[] + currentOption?: any + selectedOption: any + onSelectOption: (option: any) => void +} + +const CustomDomainSelection: FC = ({ + options, + currentOption, + selectedOption, + onSelectOption, +}) => { + const addonUpdateDisabled = useFlag('disableProjectCreationAndUpdate') + return ( +
+
+
+

Custom Domains

+ Optional +
+

Present a branded experience to your users

+
+ {addonUpdateDisabled ? ( + + ) : ( + + {options.map((option: any) => { + const defaultPrice = getProductPrice(option) + return ( +
+ } + // @ts-ignore + description={ +
+

{option.metadata.features}

+
+ } + value={option.id} + optionalLabel={
${defaultPrice.unit_amount / 100} / month
} + checked={selectedOption?.id === option.id} + onChange={() => onSelectOption(option)} + /> + ) + })} + + )} +
+ ) +} + +export default CustomDomainSelection diff --git a/studio/components/interfaces/Billing/AddOns/PITRDurationSelection.tsx b/studio/components/interfaces/Billing/AddOns/PITRDurationSelection.tsx index cf97a169668..ec5ed6bf2a6 100644 --- a/studio/components/interfaces/Billing/AddOns/PITRDurationSelection.tsx +++ b/studio/components/interfaces/Billing/AddOns/PITRDurationSelection.tsx @@ -29,7 +29,7 @@ const PITRDurationSelection: FC = ({ const addonUpdateDisabled = useFlag('disableProjectCreationAndUpdate') // Only projects of version greater than supabase-postgrest-14.1.0.44 can use PITR - const canUsePITR = getSemanticVersion(ui.selectedProject?.dbVersion ?? '') > 141044 + const canUsePITR = getSemanticVersion(ui.selectedProject?.dbVersion ?? '') >= 141044 return (
diff --git a/studio/components/interfaces/Billing/Billing.constants.tsx b/studio/components/interfaces/Billing/Billing.constants.tsx index e246a093ddd..00225246d08 100644 --- a/studio/components/interfaces/Billing/Billing.constants.tsx +++ b/studio/components/interfaces/Billing/Billing.constants.tsx @@ -1,4 +1,4 @@ -import { IconArchive, IconDatabase, IconKey } from 'ui' +import { IconArchive, IconCode, IconDatabase, IconKey } from 'ui' export const CANCELLATION_REASONS = [ 'Pricing', @@ -71,4 +71,25 @@ export const USAGE_BASED_PRODUCTS = [ }, ], }, + { + title: 'Functions', + icon: , + features: [ + { + key: 'func_count', + attribute: 'func_count', + title: 'Function Count', + units: 'absolute', + costPerUnit: 0.1, + }, + + { + key: 'func_invocations', + attribute: 'func_invocations', + title: 'Function Invocations', + units: 'absolute', + costPerUnit: 0.000002, + } + ], + }, ] diff --git a/studio/components/interfaces/Billing/Billing.utils.ts b/studio/components/interfaces/Billing/Billing.utils.ts index 2c839e9459a..d6d21a9652f 100644 --- a/studio/components/interfaces/Billing/Billing.utils.ts +++ b/studio/components/interfaces/Billing/Billing.utils.ts @@ -1,5 +1,9 @@ import { DatabaseAddon } from './AddOns/AddOns.types' -import { formatComputeSizes, formatPITROptions } from './AddOns/AddOns.utils' +import { + formatComputeSizes, + formatCustomDomainOptions, + formatPITROptions, +} from './AddOns/AddOns.utils' import { StripeSubscription } from './Subscription/Subscription.types' export const getProductPrice = (product: any) => { @@ -11,11 +15,15 @@ export const getProductPrice = (product: any) => { return price } -export const validateSubscriptionUpdatePayload = ( - selectedComputeSize: DatabaseAddon, - selectedPITRDuration?: DatabaseAddon -) => { - if (selectedPITRDuration?.id !== undefined && selectedComputeSize.id === undefined) { +export const validateSubscriptionUpdatePayload = (selectedAddons: { + computeSize: DatabaseAddon + pitrDuration: DatabaseAddon + customDomains: DatabaseAddon +}) => { + if ( + selectedAddons.pitrDuration?.id !== undefined && + selectedAddons.computeSize.id === undefined + ) { return 'To enable PITR for your project, your project must minimally be on a Small Add-on.' } return undefined @@ -23,18 +31,24 @@ export const validateSubscriptionUpdatePayload = ( export const formSubscriptionUpdatePayload = ( selectedTier: any, - selectedComputeSize: DatabaseAddon, - selectedPITRDuration: DatabaseAddon | undefined, + selectedAddons: { + computeSize: DatabaseAddon + pitrDuration: DatabaseAddon + customDomains: DatabaseAddon + }, selectedPaymentMethod: string, region: string ) => { + const { computeSize, pitrDuration, customDomains } = selectedAddons const defaultPrice = selectedTier ? getProductPrice(selectedTier) : undefined const addons = region === 'af-south-1' ? [] - : [selectedComputeSize.prices[0].id, selectedPITRDuration?.prices?.[0].id].filter( - (x) => x !== undefined - ) + : [ + computeSize.prices[0].id, + pitrDuration?.prices?.[0].id, + customDomains?.prices?.[0].id, + ].filter((x) => x !== undefined) const proration_date = Math.floor(Date.now() / 1000) return { ...(defaultPrice && { tier: defaultPrice.id }), @@ -50,8 +64,9 @@ export const getCurrentAddons = ( ) => { const computeSizes = formatComputeSizes(addons) const pitrDurationOptions = formatPITROptions(addons) + const customDomainOptions = formatCustomDomainOptions(addons) - const currentComputeSize = + const computeSize = computeSizes.find((option: any) => { const subscriptionComputeSize = currentSubscription?.addons.find((addon) => addon.supabase_prod_id.includes('_instance_') @@ -62,7 +77,7 @@ export const getCurrentAddons = ( (option: any) => option.metadata.supabase_prod_id === 'addon_instance_micro' ) as DatabaseAddon) - const currentPITRDuration = + const pitrDuration = pitrDurationOptions.find((option: any) => { const subscriptionComputeSize = currentSubscription?.addons.find((addon) => addon.supabase_prod_id.includes('_pitr_') @@ -73,5 +88,16 @@ export const getCurrentAddons = ( (option: any) => option.metadata.supabase_prod_id === 'addon_pitr_0days' ) as DatabaseAddon) - return { currentComputeSize, currentPITRDuration } + const customDomains = + customDomainOptions.find((option: any) => { + const subscriptionComputeSize = currentSubscription?.addons.find((addon) => + addon.supabase_prod_id.includes('_custom_domains') + ) + return option.id === subscriptionComputeSize?.prod_id + }) || + (customDomainOptions.find( + (option: any) => option.metadata.supabase_prod_id === 'addon__custom_domains_disabled' + ) as DatabaseAddon) + + return { computeSize, pitrDuration, customDomains } } diff --git a/studio/components/interfaces/Billing/EnterpriseUpdate.tsx b/studio/components/interfaces/Billing/EnterpriseUpdate.tsx index 360c2f96987..504d5931f8a 100644 --- a/studio/components/interfaces/Billing/EnterpriseUpdate.tsx +++ b/studio/components/interfaces/Billing/EnterpriseUpdate.tsx @@ -2,14 +2,18 @@ import { FC, useState, useEffect } from 'react' import { useRouter } from 'next/router' import { Transition } from '@headlessui/react' -import { useStore } from 'hooks' +import { useStore, useFlag } from 'hooks' import { getURL } from 'lib/helpers' import { post, patch } from 'lib/common/fetch' import { API_URL, PROJECT_STATUS } from 'lib/constants' import Divider from 'components/ui/Divider' import { DatabaseAddon } from './AddOns/AddOns.types' -import { formatComputeSizes, formatPITROptions } from './AddOns/AddOns.utils' +import { + formatComputeSizes, + formatCustomDomainOptions, + formatPITROptions, +} from './AddOns/AddOns.utils' import { AddNewPaymentMethodModal, ComputeSizeSelection, @@ -17,6 +21,7 @@ import { StripeSubscription, PaymentSummaryPanel, UpdateSuccess, + CustomDomainSelection, } from './' import { PaymentMethod, SubscriptionPreview } from './Billing.types' import { formSubscriptionUpdatePayload, getCurrentAddons } from './Billing.utils' @@ -36,6 +41,8 @@ const EnterpriseUpdate: FC = ({ }) => { const { app, ui } = useStore() const router = useRouter() + const isCustomDomainsEnabled = useFlag('customDomains') + const isPITRSelfServeEnabled = useFlag('pitrSelfServe') const projectId = ui.selectedProject?.id ?? -1 const projectRef = ui.selectedProject?.ref ?? 'default' @@ -44,12 +51,30 @@ const EnterpriseUpdate: FC = ({ const { addons } = products const computeSizes = formatComputeSizes(addons) const pitrDurationOptions = formatPITROptions(addons) - const { currentComputeSize, currentPITRDuration } = getCurrentAddons(currentSubscription, addons) + const customDomainOptions = formatCustomDomainOptions(addons) + const currentAddons = getCurrentAddons(currentSubscription, addons) + + // [Joshen TODO] Ideally we just have a state to hold all the add ons selection, rather than individual + // Even better if we can just use the component to handle all of these. Mainly to reduce the amount + // of unnecessary state management on this complex page. + const [selectedComputeSize, setSelectedComputeSize] = useState( + currentAddons.computeSize + ) + const [selectedPITRDuration, setSelectedPITRDuration] = useState( + currentAddons.pitrDuration + ) + const [selectedCustomDomainOption, setSelectedCustomDomainOption] = useState( + currentAddons.customDomains + ) + + // [Joshen] Scaffolded here + const selectedAddons = { + computeSize: selectedComputeSize, + pitrDuration: selectedPITRDuration, + customDomains: selectedCustomDomainOption, + } const [selectedPaymentMethodId, setSelectedPaymentMethodId] = useState() - const [selectedComputeSize, setSelectedComputeSize] = useState(currentComputeSize) - const [selectedPITRDuration, setSelectedPITRDuration] = - useState(currentPITRDuration) const [subscriptionPreview, setSubscriptionPreview] = useState() const [isRefreshingPreview, setIsRefreshingPreview] = useState(false) @@ -57,7 +82,7 @@ const EnterpriseUpdate: FC = ({ const [isSuccessful, setIsSuccessful] = useState(false) const [showAddPaymentMethodModal, setShowAddPaymentMethodModal] = useState(false) - const isChangingComputeSize = currentComputeSize?.id !== selectedComputeSize.id + const isChangingComputeSize = currentAddons.computeSize?.id !== selectedAddons.computeSize.id useEffect(() => { getSubscriptionPreview() @@ -75,8 +100,7 @@ const EnterpriseUpdate: FC = ({ const payload = { ...formSubscriptionUpdatePayload( null, - selectedComputeSize, - selectedPITRDuration, + selectedAddons, selectedPaymentMethodId, projectRegion ), @@ -100,8 +124,7 @@ const EnterpriseUpdate: FC = ({ const payload = { ...formSubscriptionUpdatePayload( null, - selectedComputeSize, - selectedPITRDuration, + selectedAddons, selectedPaymentMethodId, projectRegion ), @@ -176,13 +199,24 @@ const EnterpriseUpdate: FC = ({
{projectRegion !== 'af-south-1' && ( <> - {pitrDurationOptions.length > 0 && ( + {isCustomDomainsEnabled && customDomainOptions.length > 0 && ( + <> + + + + )} + {isPITRSelfServeEnabled && pitrDurationOptions.length > 0 && ( <> @@ -190,8 +224,8 @@ const EnterpriseUpdate: FC = ({ @@ -208,11 +242,9 @@ const EnterpriseUpdate: FC = ({ subscriptionPreview={subscriptionPreview} // Current subscription configuration based on DB currentPlan={currentSubscription.tier} - currentComputeSize={currentComputeSize} - currentPITRDuration={currentPITRDuration} + currentAddons={currentAddons} // Selected subscription configuration based on UI - selectedComputeSize={selectedComputeSize} - selectedPITRDuration={selectedPITRDuration} + selectedAddons={selectedAddons} paymentMethods={paymentMethods} isLoadingPaymentMethods={isLoadingPaymentMethods} selectedPaymentMethod={selectedPaymentMethodId} diff --git a/studio/components/interfaces/Billing/PaymentSummaryPanel/PaymentSummaryPanel.tsx b/studio/components/interfaces/Billing/PaymentSummaryPanel/PaymentSummaryPanel.tsx index 37f77b19b67..a3134c2629e 100644 --- a/studio/components/interfaces/Billing/PaymentSummaryPanel/PaymentSummaryPanel.tsx +++ b/studio/components/interfaces/Billing/PaymentSummaryPanel/PaymentSummaryPanel.tsx @@ -18,12 +18,21 @@ import ConfirmPaymentModal from './ConfirmPaymentModal' interface Props { isRefreshingPreview: boolean subscriptionPreview?: SubscriptionPreview + currentPlan: any - currentComputeSize: DatabaseAddon - currentPITRDuration: DatabaseAddon | undefined + currentAddons: { + computeSize: DatabaseAddon + pitrDuration: DatabaseAddon + customDomains: DatabaseAddon + } + selectedPlan?: any - selectedComputeSize: DatabaseAddon - selectedPITRDuration: DatabaseAddon | undefined + selectedAddons: { + computeSize: DatabaseAddon + pitrDuration: DatabaseAddon + customDomains: DatabaseAddon + } + isSpendCapEnabled: boolean paymentMethods?: any selectedPaymentMethod: any @@ -45,12 +54,13 @@ const PaymentSummaryPanel: FC = ({ isRefreshingPreview, isSpendCapEnabled, subscriptionPreview, + currentPlan, - currentComputeSize, - currentPITRDuration, + currentAddons, + selectedPlan, - selectedComputeSize, - selectedPITRDuration, + selectedAddons, + paymentMethods, selectedPaymentMethod, isLoadingPaymentMethods, @@ -70,9 +80,10 @@ const PaymentSummaryPanel: FC = ({ const [showConfirmModal, setShowConfirmModal] = useState(false) - const currentPITRDays = currentPITRDuration !== undefined ? getPITRDays(currentPITRDuration) : 0 + const currentPITRDays = + currentAddons.pitrDuration !== undefined ? getPITRDays(currentAddons.pitrDuration) : 0 const selectedPITRDays = - selectedPITRDuration !== undefined ? getPITRDays(selectedPITRDuration) : 0 + selectedAddons.pitrDuration !== undefined ? getPITRDays(selectedAddons.pitrDuration) : 0 const isEnterprise = currentPlan.supabase_prod_id === PRICING_TIER_PRODUCT_IDS.ENTERPRISE const isChangingPlan = @@ -81,8 +92,10 @@ const PaymentSummaryPanel: FC = ({ currentPlan.prod_id !== selectedPlan.id) || (currentPlan.prod_id !== STRIPE_PRODUCT_IDS.PAYG && !isSpendCapEnabled) || (currentPlan.prod_id === STRIPE_PRODUCT_IDS.PAYG && isSpendCapEnabled) - const isChangingComputeSize = currentComputeSize.id !== selectedComputeSize.id - const isChangingPITRDuration = currentPITRDuration?.id !== selectedPITRDuration?.id + const isChangingComputeSize = currentAddons.computeSize.id !== selectedAddons.computeSize.id + const isChangingPITRDuration = currentAddons.pitrDuration?.id !== selectedAddons.pitrDuration?.id + const isChangingCustomDomains = + currentAddons.customDomains?.id !== selectedAddons.customDomains?.id // If it's enterprise we only only changing of add-ons const hasChangesToPlan = isEnterprise @@ -96,7 +109,7 @@ const PaymentSummaryPanel: FC = ({ } const validateOrder = () => { - const error = validateSubscriptionUpdatePayload(selectedComputeSize, selectedPITRDuration) + const error = validateSubscriptionUpdatePayload(selectedAddons) if (error) { return ui.setNotification({ duration: 4000, @@ -147,45 +160,71 @@ const PaymentSummaryPanel: FC = ({

- {currentComputeSize.name} + {currentAddons.computeSize.name}

- ${(getProductPrice(currentComputeSize).unit_amount / 100).toFixed(2)} + ${(getProductPrice(currentAddons.computeSize).unit_amount / 100).toFixed(2)}

{isChangingComputeSize && (
-

{selectedComputeSize.name}

+

{selectedAddons.computeSize.name}

- ${(getProductPrice(selectedComputeSize).unit_amount / 100).toFixed(2)} + ${(getProductPrice(selectedAddons.computeSize).unit_amount / 100).toFixed(2)}

)} - {currentPITRDuration?.id !== undefined && ( + {currentAddons.pitrDuration?.id !== undefined && (

- {currentPITRDuration?.name} + {currentAddons.pitrDuration?.name}

- ${(getProductPrice(currentPITRDuration).unit_amount / 100).toFixed(2)} + ${(getProductPrice(currentAddons.pitrDuration).unit_amount / 100).toFixed(2)}

)} {isChangingPITRDuration && (
-

{selectedPITRDuration?.name}

+

{selectedAddons.pitrDuration?.name}

- ${(getProductPrice(selectedPITRDuration).unit_amount / 100).toFixed(2)} + ${(getProductPrice(selectedAddons.pitrDuration).unit_amount / 100).toFixed(2)} +

+
+ )} + {currentAddons.customDomains?.id !== undefined && ( +
+

+ {currentAddons.customDomains?.name} +

+

+ ${(getProductPrice(currentAddons.customDomains).unit_amount / 100).toFixed(2)} +

+
+ )} + {isChangingCustomDomains && ( +
+

{selectedAddons.customDomains?.name}

+

+ ${(getProductPrice(selectedAddons.customDomains).unit_amount / 100).toFixed(2)}

)} @@ -198,9 +237,9 @@ const PaymentSummaryPanel: FC = ({ defaultVisibility icon={} title={ - currentPITRDuration?.id === undefined + currentAddons.pitrDuration?.id === undefined ? 'Enabling PITR' - : selectedPITRDuration?.id === undefined + : selectedAddons.pitrDuration?.id === undefined ? 'Disabling PITR' : 'Updating PITR duration' } diff --git a/studio/components/interfaces/Billing/ProUpgrade.tsx b/studio/components/interfaces/Billing/ProUpgrade.tsx index 12fea2c0c3b..5ff559adc1a 100644 --- a/studio/components/interfaces/Billing/ProUpgrade.tsx +++ b/studio/components/interfaces/Billing/ProUpgrade.tsx @@ -13,6 +13,7 @@ import { PaymentSummaryPanel, ComputeSizeSelection, PITRDurationSelection, + CustomDomainSelection, StripeSubscription, AddNewPaymentMethodModal, } from './' @@ -21,7 +22,11 @@ import UpdateSuccess from './UpdateSuccess' import { PaymentMethod, SubscriptionPreview } from './Billing.types' import { formSubscriptionUpdatePayload, getCurrentAddons } from './Billing.utils' import { DatabaseAddon } from './AddOns/AddOns.types' -import { formatComputeSizes, formatPITROptions } from './AddOns/AddOns.utils' +import { + formatComputeSizes, + formatCustomDomainOptions, + formatPITROptions, +} from './AddOns/AddOns.utils' import BackButton from 'components/ui/BackButton' // Do not allow compute size changes for af-south-1 @@ -43,12 +48,14 @@ const ProUpgrade: FC = ({ }) => { const { app, ui } = useStore() const router = useRouter() + const isCustomDomainsEnabled = useFlag('customDomains') const isPITRSelfServeEnabled = useFlag('pitrSelfServe') const { addons } = products const computeSizes = formatComputeSizes(addons) const pitrDurationOptions = formatPITROptions(addons) - const { currentComputeSize, currentPITRDuration } = getCurrentAddons(currentSubscription, addons) + const customDomainOptions = formatCustomDomainOptions(addons) + const currentAddons = getCurrentAddons(currentSubscription, addons) const projectId = ui.selectedProject?.id ?? -1 const projectRef = ui.selectedProject?.ref @@ -67,10 +74,27 @@ const ProUpgrade: FC = ({ const [showAddPaymentMethodModal, setShowAddPaymentMethodModal] = useState(false) const [showSpendCapHelperModal, setShowSpendCapHelperModal] = useState(false) + // [Joshen TODO] Ideally we just have a state to hold all the add ons selection, rather than individual + // Even better if we can just use the component to handle all of these. Mainly to reduce the amount + // of unnecessary state management on this complex page. + const [selectedComputeSize, setSelectedComputeSize] = useState( + currentAddons.computeSize + ) + const [selectedPITRDuration, setSelectedPITRDuration] = useState( + currentAddons.pitrDuration + ) + const [selectedCustomDomainOption, setSelectedCustomDomainOption] = useState( + currentAddons.customDomains + ) + + // [Joshen] Scaffolded here + const selectedAddons = { + computeSize: selectedComputeSize, + pitrDuration: selectedPITRDuration, + customDomains: selectedCustomDomainOption, + } + const [selectedPaymentMethodId, setSelectedPaymentMethodId] = useState('') - const [selectedComputeSize, setSelectedComputeSize] = useState(currentComputeSize) - const [selectedPITRDuration, setSelectedPITRDuration] = - useState(currentPITRDuration) const [subscriptionPreview, setSubscriptionPreview] = useState() const selectedTier = isSpendCapEnabled @@ -81,7 +105,7 @@ const ProUpgrade: FC = ({ currentSubscription.tier.prod_id === STRIPE_PRODUCT_IDS.PRO || currentSubscription.tier.prod_id === STRIPE_PRODUCT_IDS.PAYG - const isChangingComputeSize = currentComputeSize?.id !== selectedComputeSize.id + const isChangingComputeSize = currentAddons.computeSize?.id !== selectedAddons.computeSize.id useEffect(() => { if (!isLoadingPaymentMethods && paymentMethods && paymentMethods.length > 0) { @@ -91,15 +115,14 @@ const ProUpgrade: FC = ({ useEffect(() => { getSubscriptionPreview() - }, [selectedComputeSize, selectedPITRDuration, isSpendCapEnabled]) + }, [selectedComputeSize, selectedPITRDuration, selectedCustomDomainOption, isSpendCapEnabled]) const getSubscriptionPreview = async () => { if (!selectedTier) return const payload = formSubscriptionUpdatePayload( selectedTier, - selectedComputeSize, - selectedPITRDuration, + selectedAddons, selectedPaymentMethodId, projectRegion ) @@ -119,8 +142,7 @@ const ProUpgrade: FC = ({ const onConfirmPayment = async () => { const payload = formSubscriptionUpdatePayload( selectedTier, - selectedComputeSize, - selectedPITRDuration, + selectedAddons, selectedPaymentMethodId, projectRegion ) @@ -220,15 +242,28 @@ const ProUpgrade: FC = ({ {projectRegion !== 'af-south-1' && ( <> + {isCustomDomainsEnabled && customDomainOptions.length > 0 && ( + <> + + + + )} {isPITRSelfServeEnabled && pitrDurationOptions.length > 0 && ( <> @@ -236,8 +271,8 @@ const ProUpgrade: FC = ({ @@ -253,12 +288,10 @@ const ProUpgrade: FC = ({ isSpendCapEnabled={isSpendCapEnabled} // Current subscription configuration based on DB currentPlan={currentSubscription.tier} - currentComputeSize={currentComputeSize} - currentPITRDuration={currentPITRDuration} + currentAddons={currentAddons} // Selected subscription configuration based on UI selectedPlan={selectedTier} - selectedComputeSize={selectedComputeSize} - selectedPITRDuration={selectedPITRDuration} + selectedAddons={selectedAddons} paymentMethods={paymentMethods} isLoadingPaymentMethods={isLoadingPaymentMethods} selectedPaymentMethod={selectedPaymentMethodId} diff --git a/studio/components/interfaces/Billing/index.ts b/studio/components/interfaces/Billing/index.ts index afcd29a86aa..ece846df89b 100644 --- a/studio/components/interfaces/Billing/index.ts +++ b/studio/components/interfaces/Billing/index.ts @@ -7,6 +7,7 @@ import ExitSurvey from './ExitSurvey/ExitSurvey' import PaymentSummaryPanel from './PaymentSummaryPanel/PaymentSummaryPanel' import ComputeSizeSelection from './AddOns/ComputeSizeSelection' import PITRDurationSelection from './AddOns/PITRDurationSelection' +import CustomDomainSelection from './AddOns/CustomDomainSelection' import AddNewPaymentMethodModal from './AddNewPaymentMethodModal/AddNewPaymentMethodModal' import UpdateSuccess from './UpdateSuccess' @@ -22,6 +23,7 @@ export { PaymentSummaryPanel, ComputeSizeSelection, PITRDurationSelection, + CustomDomainSelection, AddNewPaymentMethodModal, UpdateSuccess, Invoices, diff --git a/studio/components/interfaces/Database/Backups/BackupsList.tsx b/studio/components/interfaces/Database/Backups/BackupsList.tsx index 7609c648609..7f4f41434c5 100644 --- a/studio/components/interfaces/Database/Backups/BackupsList.tsx +++ b/studio/components/interfaces/Database/Backups/BackupsList.tsx @@ -11,7 +11,7 @@ import BackupsError from './BackupsError' import BackupsEmpty from './BackupsEmpty' import { PermissionAction } from '@supabase/shared-types/out/constants' import InformationBox from 'components/ui/InformationBox' -import { IconAlertCircle } from 'ui' +import { IconAlertCircle, IconClock } from 'ui' interface Props {} @@ -32,6 +32,7 @@ const BackupsList: FC = ({}) => { if (tierKey === 'FREE') { return ( } primaryText="Free Plan does not include project backups." projectRef={projectRef} secondaryText="Please upgrade to the Pro plan for up to 7 days of scheduled backups." diff --git a/studio/components/interfaces/Settings/API/JWTSettings.tsx b/studio/components/interfaces/Settings/API/JWTSettings.tsx index 7b53b539d3b..d01d9f1fedc 100644 --- a/studio/components/interfaces/Settings/API/JWTSettings.tsx +++ b/studio/components/interfaces/Settings/API/JWTSettings.tsx @@ -151,7 +151,7 @@ const JWTSettings: FC = ({}) => {
-

Generate a new JWT secret

+

Generate a new JWT secret

A random secret will be created, or you can create your own.

diff --git a/studio/components/interfaces/Settings/General/CustomDomainConfig/CustomDomainActivate.tsx b/studio/components/interfaces/Settings/General/CustomDomainConfig/CustomDomainActivate.tsx new file mode 100644 index 00000000000..ddac4535323 --- /dev/null +++ b/studio/components/interfaces/Settings/General/CustomDomainConfig/CustomDomainActivate.tsx @@ -0,0 +1,143 @@ +import Link from 'next/link' +import { useState } from 'react' +import { observer } from 'mobx-react-lite' +import { Button, IconExternalLink } from 'ui' + +import { useStore } from 'hooks' +import { useCustomDomainDeleteMutation } from 'data/custom-domains/custom-domains-delete-mutation' +import { useCustomDomainActivateMutation } from 'data/custom-domains/custom-domains-activate-mutation' +import { CustomDomainResponse } from 'data/custom-domains/custom-domains-query' +import Panel from 'components/ui/Panel' +import ConfirmModal from 'components/ui/Dialogs/ConfirmDialog' + +export type CustomDomainActivateProps = { + projectRef?: string + customDomain: CustomDomainResponse +} + +const CustomDomainActivate = ({ projectRef, customDomain }: CustomDomainActivateProps) => { + const { ui } = useStore() + + const [isActivateConfirmModalVisible, setIsActivateConfirmModalVisible] = useState(false) + + const { mutateAsync: activateCustomDomain } = useCustomDomainActivateMutation() + const { mutateAsync: deleteCustomDomain, isLoading: isDeleting } = useCustomDomainDeleteMutation() + + const onActivateCustomDomain = async () => { + if (!projectRef) { + throw new Error('Project ref is required') + } + + try { + await activateCustomDomain({ projectRef }) + + ui.setNotification({ + category: 'success', + message: `Successfully activated custom domain`, + }) + + setIsActivateConfirmModalVisible(false) + } catch (error: any) { + ui.setNotification({ + category: 'error', + message: error.message, + }) + } + } + + const onCancelCustomDomain = async () => { + if (!projectRef) { + throw new Error('Project ref is required') + } + try { + await deleteCustomDomain({ projectRef }) + } catch (error: any) { + ui.setNotification({ category: 'error', message: error.message }) + } + } + + return ( + <> +
+ +
+

+ Setup complete! Press activate to enable the custom domain{' '} + {customDomain.hostname} for this project. +

+ + We recommend that you schedule a downtime window of 20 - 30 minutes for your + application, as you will need to update any services that need to know about your + custom domain (e.g client side code or OAuth providers) + +
+
+ +
+ + +
+ + + + + +
+ + +
+
+
+
+ + + Are you sure you want to activate the custom domain{' '} + {customDomain.hostname} for the project? +
+ } + description="The existing Supabase subdomain will be deactivated." + buttonLabel="Activate" + buttonLoadingLabel="Activating" + onSelectCancel={() => setIsActivateConfirmModalVisible(false)} + onSelectConfirm={onActivateCustomDomain} + /> + + ) +} + +export default observer(CustomDomainActivate) diff --git a/studio/components/interfaces/Settings/General/CustomDomainConfig/CustomDomainConfig.tsx b/studio/components/interfaces/Settings/General/CustomDomainConfig/CustomDomainConfig.tsx new file mode 100644 index 00000000000..dde95501290 --- /dev/null +++ b/studio/components/interfaces/Settings/General/CustomDomainConfig/CustomDomainConfig.tsx @@ -0,0 +1,104 @@ +import Link from 'next/link' +import { observer } from 'mobx-react-lite' +import { IconAlertCircle } from 'ui' + +import { useParams, useStore } from 'hooks' +import { useProjectSettingsQuery } from 'data/config/project-settings-query' +import { useCustomDomainsQuery } from 'data/custom-domains/custom-domains-query' +import Panel from 'components/ui/Panel' +import { FormHeader } from 'components/ui/Forms' +import UpgradeToPro from 'components/ui/UpgradeToPro' +import CustomDomainDelete from './CustomDomainDelete' +import CustomDomainVerify from './CustomDomainVerify' +import CustomDomainActivate from './CustomDomainActivate' +import CustomDomainsConfigureHostname from './CustomDomainsConfigureHostname' +import CustomDomainsShimmerLoader from './CustomDomainsShimmerLoader' +import { PRICING_TIER_PRODUCT_IDS } from 'lib/constants' + +const CustomDomainConfig = () => { + const { ui } = useStore() + const { ref } = useParams() + + const tier = ui.selectedProject?.subscription_tier + const { isLoading: isSettingsLoading, data: settings } = useProjectSettingsQuery({ + projectRef: ref, + }) + + const { + isLoading: isCustomDomainsLoading, + isError, + isSuccess, + data, + } = useCustomDomainsQuery({ projectRef: ref }) + + const isLoading = isSettingsLoading || isCustomDomainsLoading + + return ( +
+ + {data?.status === '0_no_hostname_configured' ? ( + + ) : ( + + {isLoading && ( + + + + )} + + {data?.status === '0_not_allowed' && ( + + } + primaryText="Custom domains are a Pro plan add-on" + projectRef={ref as string} + secondaryText={ + tier === PRICING_TIER_PRODUCT_IDS.FREE + ? 'To configure a custom domain for your project, please upgrade to the Pro plan with the custom domains add-on selected' + : 'To configure a custom domain for your project, please enable the add-on' + } + /> + + )} + + {isError && ( + +
+ +

+ Failed to retrieve custom domain configuration. Please try again later or{' '} + + contact support + + . +

+
+
+ )} + + {isSuccess && ( +
+ {(data.status === '2_initiated' || data.status === '3_challenge_verified') && ( + + )} + + {data.status === '4_origin_setup_completed' && ( + + )} + + {data.status === '5_services_reconfigured' && ( + + )} +
+ )} +
+ )} +
+ ) +} + +export default observer(CustomDomainConfig) diff --git a/studio/components/interfaces/Settings/General/CustomDomainConfig/CustomDomainDelete.tsx b/studio/components/interfaces/Settings/General/CustomDomainConfig/CustomDomainDelete.tsx new file mode 100644 index 00000000000..bacebf659b4 --- /dev/null +++ b/studio/components/interfaces/Settings/General/CustomDomainConfig/CustomDomainDelete.tsx @@ -0,0 +1,104 @@ +import Link from 'next/link' +import { useState } from 'react' +import { observer } from 'mobx-react-lite' +import { Button, IconExternalLink, IconTrash } from 'ui' + +import { useStore } from 'hooks' +import { useCustomDomainDeleteMutation } from 'data/custom-domains/custom-domains-delete-mutation' +import { CustomDomainResponse } from 'data/custom-domains/custom-domains-query' +import Panel from 'components/ui/Panel' +import ConfirmModal from 'components/ui/Dialogs/ConfirmDialog' + +export type CustomDomainDeleteProps = { + projectRef?: string + customDomain: CustomDomainResponse +} + +const CustomDomainDelete = ({ projectRef, customDomain }: CustomDomainDeleteProps) => { + const { ui } = useStore() + + const [isDeleteConfirmModalVisible, setIsDeleteConfirmModalVisible] = useState(false) + + const { mutateAsync: deleteCustomDomain } = useCustomDomainDeleteMutation() + + const onDeleteCustomDomain = async () => { + if (!projectRef) { + throw new Error('Project ref is required') + } + + try { + await deleteCustomDomain({ projectRef }) + + ui.setNotification({ + category: 'success', + message: `Successfully deleted custom domain`, + }) + + setIsDeleteConfirmModalVisible(false) + } catch (error: any) { + ui.setNotification({ + category: 'error', + message: error.message, + }) + } + } + + return ( + <> + +
+

Active custom domain:

+
+ +
+ {customDomain.hostname} + +
+

+ Your custom domain is currently active and is serving traffic +

+
+ + +
+ + +
+ + + + + + +
+
+ + + Are you sure you want to delete the custom domain{' '} + {customDomain.hostname} for the project? +
+ } + description="Your custom domain will be deactivated. You will need to re-verify your domain if you want to use it again." + buttonLabel="Delete" + buttonLoadingLabel="Deleting" + onSelectCancel={() => setIsDeleteConfirmModalVisible(false)} + onSelectConfirm={onDeleteCustomDomain} + /> + + ) +} + +export default observer(CustomDomainDelete) diff --git a/studio/components/interfaces/Settings/General/CustomDomainConfig/CustomDomainVerify.tsx b/studio/components/interfaces/Settings/General/CustomDomainConfig/CustomDomainVerify.tsx new file mode 100644 index 00000000000..c8dee15035a --- /dev/null +++ b/studio/components/interfaces/Settings/General/CustomDomainConfig/CustomDomainVerify.tsx @@ -0,0 +1,183 @@ +import Link from 'next/link' +import { useState } from 'react' +import { observer } from 'mobx-react-lite' +import { Button, IconAlertCircle, IconExternalLink, IconHelpCircle, IconRefreshCw } from 'ui' + +import { useStore } from 'hooks' +import { ProjectSettingsResponse } from 'data/config/project-settings-query' +import { CustomDomainResponse } from 'data/custom-domains/custom-domains-query' +import { useCustomDomainDeleteMutation } from 'data/custom-domains/custom-domains-delete-mutation' +import { useCustomDomainReverifyMutation } from 'data/custom-domains/custom-domains-reverify-mutation' +import DNSRecord from './DNSRecord' +import Panel from 'components/ui/Panel' +import InformationBox from 'components/ui/InformationBox' + +export type CustomDomainVerifyProps = { + projectRef?: string + customDomain: CustomDomainResponse + settings?: ProjectSettingsResponse +} + +const CustomDomainVerify = ({ projectRef, customDomain, settings }: CustomDomainVerifyProps) => { + const { ui } = useStore() + + const [isNotVerifiedYet, setIsNotVerifiedYet] = useState(false) + + const { mutate: reverifyCustomDomain, isLoading: isReverifyLoading } = + useCustomDomainReverifyMutation({ + onSuccess: (res) => { + if (res.status === '2_initiated') setIsNotVerifiedYet(true) + }, + }) + + const { mutateAsync: deleteCustomDomain, isLoading: isDeleting } = useCustomDomainDeleteMutation() + + const onReverifyCustomDomain = () => { + if (!projectRef) { + throw new Error('Project ref is required') + } + + reverifyCustomDomain({ projectRef }) + } + + const onCancelCustomDomain = async () => { + if (!projectRef) { + throw new Error('Project ref is required') + } + try { + await deleteCustomDomain({ projectRef }) + } catch (error: any) { + ui.setNotification({ category: 'error', message: error.message }) + } + } + + return ( + <> + +
+

+ Successfully added your custom domain{' '} + {customDomain.hostname} +

+

+ Set the following record(s) in your DNS provider, then click verify to confirm your + control over the domain +

+

+ Records which have been successfully verified will be removed from this list below. +

+
+ + ) : ( + + ) + } + title={ + isNotVerifiedYet + ? 'Unable to verify records from DNS provider yet.' + : 'Please note that it may take up to 24 hours for the DNS records to propagate.' + } + description={ + isNotVerifiedYet + ? 'Do check again in a bit as it may take up to 24 hours for changes in DNS records to propagate.' + : undefined + } + /> +
+
+ +
+
+
+

Type

+
+
+
+ +
+
+
+
+ +
+
+
+ + {customDomain.verification_errors?.includes( + 'custom hostname does not CNAME to this zone.' + ) && ( + + )} + + {customDomain.ownership_verification && ( + + )} + + {customDomain.ssl.status === 'pending_validation' && ( + + )} + + {customDomain.ssl.status === 'pending_deployment' && ( +
+ +

+ SSL certificate is being deployed. Please wait a few minutes and try again. +

+
+ )} +
+
+ +
+ + +
+ + + + + +
+ + +
+
+
+ + ) +} + +export default observer(CustomDomainVerify) diff --git a/studio/components/interfaces/Settings/General/CustomDomainConfig/CustomDomainsConfigureHostname.tsx b/studio/components/interfaces/Settings/General/CustomDomainConfig/CustomDomainsConfigureHostname.tsx new file mode 100644 index 00000000000..a79ced1679c --- /dev/null +++ b/studio/components/interfaces/Settings/General/CustomDomainConfig/CustomDomainsConfigureHostname.tsx @@ -0,0 +1,111 @@ +import Link from 'next/link' +import * as yup from 'yup' +import { Button, Form, IconExternalLink, Input } from 'ui' +import { observer } from 'mobx-react-lite' +import { PermissionAction } from '@supabase/shared-types/out/constants' + +import { checkPermissions, useStore } from 'hooks' +import { + FormActions, + FormPanel, + FormSection, + FormSectionContent, + FormSectionLabel, +} from 'components/ui/Forms' +import { useCustomDomainCreateMutation } from 'data/custom-domains/custom-domains-create-mutation' + +const FORM_ID = 'custom-domains-form' + +const schema = yup.object({ + domain: yup.string().required('A value for your custom domain is required'), +}) + +export type CustomDomainsConfigureHostnameProps = { + projectRef?: string +} + +const CustomDomainsConfigureHostname = ({ projectRef }: CustomDomainsConfigureHostnameProps) => { + const { ui } = useStore() + + const canConfigureCustomDomain = checkPermissions(PermissionAction.UPDATE, 'projects') + const { mutateAsync: createCustomDomain } = useCustomDomainCreateMutation() + + const onCreateCustomDomain = async (values: yup.InferType) => { + if (!projectRef) { + throw new Error('Project ref is required') + } + + try { + await createCustomDomain({ + projectRef, + customDomain: values.domain, + }) + } catch (error: any) { + ui.setNotification({ + category: 'error', + message: error.message, + }) + } + } + + return ( + + {({ isSubmitting, handleReset, values, initialValues }: any) => { + const hasChanges = JSON.stringify(values) !== JSON.stringify(initialValues) + + return ( + <> + + + + + + + ) + } + /> +
+ } + > + Add a Custom Domain}> + + + + + + + ) + }} + + ) +} + +export default observer(CustomDomainsConfigureHostname) diff --git a/studio/components/interfaces/Settings/General/CustomDomainConfig/CustomDomainsShimmerLoader.tsx b/studio/components/interfaces/Settings/General/CustomDomainConfig/CustomDomainsShimmerLoader.tsx new file mode 100644 index 00000000000..3caaa3b1b2c --- /dev/null +++ b/studio/components/interfaces/Settings/General/CustomDomainConfig/CustomDomainsShimmerLoader.tsx @@ -0,0 +1,15 @@ +const CustomDomainsShimmerLoader = () => { + return ( +
+
+
+
+ +
+
+
+
+ ) +} + +export default CustomDomainsShimmerLoader diff --git a/studio/components/interfaces/Settings/General/CustomDomainConfig/DNSRecord.tsx b/studio/components/interfaces/Settings/General/CustomDomainConfig/DNSRecord.tsx new file mode 100644 index 00000000000..9541c0abf9d --- /dev/null +++ b/studio/components/interfaces/Settings/General/CustomDomainConfig/DNSRecord.tsx @@ -0,0 +1,21 @@ +import { Input } from 'ui' + +export type DNSRecordProps = { + type: string + name: string + value: string +} + +const DNSRecord = ({ type, name, value }: DNSRecordProps) => { + return ( +
+
+

{type.toUpperCase()}

+
+ + +
+ ) +} + +export default DNSRecord diff --git a/studio/components/interfaces/Settings/General/DeleteProjectPanel/DeleteProjectPanel.tsx b/studio/components/interfaces/Settings/General/DeleteProjectPanel/DeleteProjectPanel.tsx index b5bdafb317d..4c8c7267fdb 100644 --- a/studio/components/interfaces/Settings/General/DeleteProjectPanel/DeleteProjectPanel.tsx +++ b/studio/components/interfaces/Settings/General/DeleteProjectPanel/DeleteProjectPanel.tsx @@ -5,6 +5,7 @@ import { useStore } from 'hooks' import Panel from 'components/ui/Panel' import DeleteProjectButton from './DeleteProjectButton' +import { FormHeader } from 'components/ui/Forms' interface Props {} @@ -16,7 +17,8 @@ const DeleteProjectPanel: FC = ({}) => { return (
- Danger Zone

}> + + = ({ projectRef }) => { /> ) : ( - {feature.units === 'bytes' ? formatBytes(usageValue) : ''} + {feature.units === 'bytes' + ? formatBytes(usageValue) + : usageValue} )} diff --git a/studio/components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/ColumnType.tsx b/studio/components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/ColumnType.tsx index 33ba1198cc3..a31f97e92ac 100644 --- a/studio/components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/ColumnType.tsx +++ b/studio/components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/ColumnType.tsx @@ -37,7 +37,9 @@ const ColumnType: FC = ({ readOnly disabled label={showLabel ? 'Type' : ''} - layout="horizontal" + layout={showLabel ? 'horizontal' : undefined} + className="md:gap-x-0" + size="small" value={value} descriptionText={ showLabel diff --git a/studio/components/layouts/ProjectLayout/LayoutHeader/LayoutHeader.tsx b/studio/components/layouts/ProjectLayout/LayoutHeader/LayoutHeader.tsx index a218a446c35..ebca922f3e1 100644 --- a/studio/components/layouts/ProjectLayout/LayoutHeader/LayoutHeader.tsx +++ b/studio/components/layouts/ProjectLayout/LayoutHeader/LayoutHeader.tsx @@ -63,7 +63,9 @@ const LayoutHeader = ({ customHeaderComponents, breadcrumbs = [], headerBorder = {/* Project Dropdown */} - {showOverUsageBadge && ( + + {/* [Joshen TODO] Temporarily hidden until usage endpoint is sorted out */} + {/* {showOverUsageBadge && ( - )} + )} */} )} diff --git a/studio/components/to-be-cleaned/Docs/Snippets.js b/studio/components/to-be-cleaned/Docs/Snippets.js index 6e4995dc0a8..955409e15bb 100644 --- a/studio/components/to-be-cleaned/Docs/Snippets.js +++ b/studio/components/to-be-cleaned/Docs/Snippets.js @@ -366,7 +366,7 @@ curl -X POST '${endpoint}/rest/v1/${resourceId}' \\ -H "apikey: ${apiKey}" \\ -H "Authorization: Bearer ${apiKey}" \\ -H "Content-Type: application/json" \\ --H "Prefer: return=representation" \\ +-H "Prefer: return=minimal" \\ -d '{ "some_column": "someValue", "other_column": "otherValue" }' `, }, @@ -436,7 +436,7 @@ curl -X PATCH '${endpoint}/rest/v1/${resourceId}?some_column=eq.someValue' \\ -H "apikey: ${apiKey}" \\ -H "Authorization: Bearer ${apiKey}" \\ -H "Content-Type: application/json" \\ --H "Prefer: return=representation" \\ +-H "Prefer: return=minimal" \\ -d '{ "other_column": "otherValue" }' `, }, diff --git a/studio/components/to-be-cleaned/Storage/StorageSettings/StorageSettings.tsx b/studio/components/to-be-cleaned/Storage/StorageSettings/StorageSettings.tsx index 6f78c2dd369..be80781933a 100644 --- a/studio/components/to-be-cleaned/Storage/StorageSettings/StorageSettings.tsx +++ b/studio/components/to-be-cleaned/Storage/StorageSettings/StorageSettings.tsx @@ -1,6 +1,6 @@ import useSWR from 'swr' import { FC, useState } from 'react' -import { Button, Form, Input, Listbox } from 'ui' +import { Button, Form, IconClock, Input, Listbox } from 'ui' import { useStore } from 'hooks' import { patch, get } from 'lib/common/fetch' @@ -173,6 +173,7 @@ const StorageConfig = ({ config, projectRef }: any) => { {isFreeTier && (
} primaryText="Free Plan has a fixed upload file size limit of 50 MB." projectRef={projectRef} secondaryText="Please upgrade to Pro plan for a configurable upload file size limit of up to 5 GB." diff --git a/studio/components/ui/Dialogs/ConfirmDialog.js b/studio/components/ui/Dialogs/ConfirmDialog.js index c9f8d79ee45..33e4df18d70 100644 --- a/studio/components/ui/Dialogs/ConfirmDialog.js +++ b/studio/components/ui/Dialogs/ConfirmDialog.js @@ -56,13 +56,20 @@ const ConfirmModal = ({
-
diff --git a/studio/components/ui/UpgradeToPro.tsx b/studio/components/ui/UpgradeToPro.tsx index 0266397cfdb..0dd4d6e3b46 100644 --- a/studio/components/ui/UpgradeToPro.tsx +++ b/studio/components/ui/UpgradeToPro.tsx @@ -1,38 +1,49 @@ -import { Button, IconClock } from 'ui' +import { Button } from 'ui' +import { observer } from 'mobx-react-lite' import Link from 'next/link' -import { FC } from 'react' +import { FC, ReactNode } from 'react' +import { useStore } from 'hooks' +import { PRICING_TIER_PRODUCT_IDS } from 'lib/constants' interface Props { + icon?: ReactNode primaryText: string projectRef: string secondaryText: string } -const UpgradeToPro: FC = ({ primaryText, projectRef, secondaryText }) => ( -
-
-
- -
-
-
-

{primaryText}

-
-

{secondaryText}

+const UpgradeToPro: FC = ({ icon, primaryText, projectRef, secondaryText }) => { + const { ui } = useStore() + const tier = ui.selectedProject?.subscription_tier + + return ( +
+
+ {icon &&
{icon}
} + - - -
-
-) + ) +} -export default UpgradeToPro +export default observer(UpgradeToPro) diff --git a/studio/data/config/keys.ts b/studio/data/config/keys.ts new file mode 100644 index 00000000000..875848a4247 --- /dev/null +++ b/studio/data/config/keys.ts @@ -0,0 +1,3 @@ +export const configKeys = { + settings: (projectRef: string | undefined) => ['projects', projectRef, 'settings'] as const, +} diff --git a/studio/data/config/project-settings-query.ts b/studio/data/config/project-settings-query.ts new file mode 100644 index 00000000000..ad473e32e38 --- /dev/null +++ b/studio/data/config/project-settings-query.ts @@ -0,0 +1,122 @@ +import { useQuery, useQueryClient, UseQueryOptions } from '@tanstack/react-query' +import { get } from 'lib/common/fetch' +import { API_URL } from 'lib/constants' +import { useCallback } from 'react' +import { configKeys } from './keys' + +export type ProjectSettingsVariables = { + projectRef?: string +} + +export type Project = { + id: number + name: string + ref: string + status: string + db_host: string + db_name: string + db_user: string + db_schema: string + db_port: number + db_ssl: boolean + services: Service[] +} + +export type App = { + id: number + name: string +} + +export type AppConfig = { + endpoint: string + db_schema: string + realtime_multitenant_enabled: boolean +} + +export type Service = { + id: number + name: string + app_config: AppConfig + app: App + service_api_keys: ServiceApiKey[] +} + +export type ServiceApiKey = { + api_key_encrypted: string + tags: string + name: string +} + +export type AutoApiService = Service & { + endpoint: string + restUrl: string + project: Project + defaultApiKey: string + serviceApiKey: string +} + +export type ProjectSettingsResponse = { + project: Project + autoApiService: AutoApiService +} + +export async function getProjectSettings( + { projectRef }: ProjectSettingsVariables, + signal?: AbortSignal +) { + if (!projectRef) { + throw new Error('projectRef is required') + } + + const response = await get(`${API_URL}/props/project/${projectRef}/api`, { + signal, + }) + if (response.error) { + throw response.error + } + + return response as ProjectSettingsResponse +} + +export type ProjectSettingsData = Awaited> +export type ProjectSettingsError = unknown + +export const useProjectSettingsQuery = ( + { projectRef }: ProjectSettingsVariables, + { + enabled = true, + ...options + }: UseQueryOptions = {} +) => + useQuery( + configKeys.settings(projectRef), + ({ signal }) => getProjectSettings({ projectRef }, signal), + { + enabled: enabled && typeof projectRef !== 'undefined', + refetchInterval(data, query) { + if (!data) { + return false + } + + const { autoApiService } = data as unknown as ProjectSettingsData + + const apiKeys = autoApiService?.service_api_keys ?? [] + const interval = apiKeys.length === 0 ? 2000 : 0 + + return interval + }, + ...options, + } + ) + +export const useProjectSettingsPrefetch = ({ projectRef }: ProjectSettingsVariables) => { + const client = useQueryClient() + + return useCallback(() => { + if (projectRef) { + client.prefetchQuery(configKeys.settings(projectRef), ({ signal }) => + getProjectSettings({ projectRef }, signal) + ) + } + }, [projectRef]) +} diff --git a/studio/data/custom-domains/custom-domains-activate-mutation.ts b/studio/data/custom-domains/custom-domains-activate-mutation.ts new file mode 100644 index 00000000000..2fc29e5d231 --- /dev/null +++ b/studio/data/custom-domains/custom-domains-activate-mutation.ts @@ -0,0 +1,49 @@ +import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query' +import { post } from 'lib/common/fetch' +import { customDomainKeys } from './keys' + +export type CustomDomainActivateVariables = { + projectRef: string +} + +export async function activateCustomDomain({ projectRef }: CustomDomainActivateVariables) { + if (!projectRef) { + throw new Error('projectRef is required') + } + + const response = await post( + `${process.env.NEXT_PUBLIC_API_ADMIN_URL}/projects/${projectRef}/custom-hostname/activate`, + {} + ) + if (response.error) { + throw response.error + } + + return response +} + +type CustomDomainActivateData = Awaited> + +export const useCustomDomainActivateMutation = ({ + onSuccess, + ...options +}: Omit< + UseMutationOptions, + 'mutationFn' +> = {}) => { + const queryClient = useQueryClient() + + return useMutation( + (vars) => activateCustomDomain(vars), + { + async onSuccess(data, variables, context) { + const { projectRef } = variables + + await queryClient.invalidateQueries(customDomainKeys.list(projectRef)) + + await onSuccess?.(data, variables, context) + }, + ...options, + } + ) +} diff --git a/studio/data/custom-domains/custom-domains-create-mutation.ts b/studio/data/custom-domains/custom-domains-create-mutation.ts new file mode 100644 index 00000000000..689ed60af47 --- /dev/null +++ b/studio/data/custom-domains/custom-domains-create-mutation.ts @@ -0,0 +1,55 @@ +import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query' +import { post } from 'lib/common/fetch' +import { customDomainKeys } from './keys' + +export type CustomDomainCreateVariables = { + projectRef: string + customDomain: string +} + +export async function createCustomDomain({ + projectRef, + customDomain, +}: CustomDomainCreateVariables) { + if (!projectRef) { + throw new Error('projectRef is required') + } + + const response = await post( + `${process.env.NEXT_PUBLIC_API_ADMIN_URL}/projects/${projectRef}/custom-hostname/initialize`, + { + custom_hostname: customDomain, + } + ) + if (response.error) { + throw response.error + } + + return response +} + +type CustomDomainCreateData = Awaited> + +export const useCustomDomainCreateMutation = ({ + onSuccess, + ...options +}: Omit< + UseMutationOptions, + 'mutationFn' +> = {}) => { + const queryClient = useQueryClient() + + return useMutation( + (vars) => createCustomDomain(vars), + { + async onSuccess(data, variables, context) { + const { projectRef } = variables + + await queryClient.invalidateQueries(customDomainKeys.list(projectRef)) + + await onSuccess?.(data, variables, context) + }, + ...options, + } + ) +} diff --git a/studio/data/custom-domains/custom-domains-delete-mutation.ts b/studio/data/custom-domains/custom-domains-delete-mutation.ts new file mode 100644 index 00000000000..014ce571e4d --- /dev/null +++ b/studio/data/custom-domains/custom-domains-delete-mutation.ts @@ -0,0 +1,58 @@ +import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query' +import { delete_ } from 'lib/common/fetch' +import { getCustomDomains } from './custom-domains-query' +import { customDomainKeys } from './keys' + +export type CustomDomainDeleteVariables = { + projectRef: string +} + +export async function deleteCustomDomain({ projectRef }: CustomDomainDeleteVariables) { + if (!projectRef) { + throw new Error('projectRef is required') + } + + const response = await delete_( + `${process.env.NEXT_PUBLIC_API_ADMIN_URL}/projects/${projectRef}/custom-hostname`, + {} + ) + if (response.error) { + throw response.error + } + + return response +} + +type CustomDomainDeleteData = Awaited> + +export const useCustomDomainDeleteMutation = ({ + onSuccess, + ...options +}: Omit< + UseMutationOptions, + 'mutationFn' +> = {}) => { + const queryClient = useQueryClient() + + return useMutation( + (vars) => deleteCustomDomain(vars), + { + async onSuccess(data, variables, context) { + const { projectRef } = variables + + // we manually setQueriesData here instead of using + // the standard invalidateQueries is the custom domains + // endpoint doesn't immediately return the new state + queryClient.setQueriesData(customDomainKeys.list(projectRef), () => { + return { + customDomain: null, + status: '0_no_hostname_configured', + } + }) + + await onSuccess?.(data, variables, context) + }, + ...options, + } + ) +} diff --git a/studio/data/custom-domains/custom-domains-query.ts b/studio/data/custom-domains/custom-domains-query.ts new file mode 100644 index 00000000000..645f756b8be --- /dev/null +++ b/studio/data/custom-domains/custom-domains-query.ts @@ -0,0 +1,147 @@ +import { useQuery, useQueryClient, UseQueryOptions } from '@tanstack/react-query' +import { get } from 'lib/common/fetch' +import { useCallback } from 'react' +import { customDomainKeys } from './keys' + +export type CustomDomainsVariables = { + projectRef?: string +} + +type CustomDomainStatus = + | '0_not_allowed' + | '0_no_hostname_configured' + | '1_not_started' + | '2_initiated' + | '3_challenge_verified' + | '4_origin_setup_completed' + | '5_services_reconfigured' + +type Settings = { + http2: string + tls_1_3: string + min_tls_version: string +} + +type Ssl = { + id: string + type: string + method: string + status: string + txt_name?: string + txt_value?: string + settings: Settings + wildcard: boolean + bundle_method: string + certificate_authority: string + validation_records?: { + status: string + txt_name: string + txt_value: string + }[] +} + +type OwnershipVerification = { + name: string + type: string + value: string +} + +type OwnershipVerificationHttp = { + http_url: string + http_body: string +} + +export type CustomDomainResponse = { + id: string + ssl: Ssl + status: string + hostname: string + created_at: string + custom_metadata: any + verification_errors?: string[] + custom_origin_server: string + ownership_verification?: OwnershipVerification + ownership_verification_http?: OwnershipVerificationHttp +} + +export async function getCustomDomains( + { projectRef }: CustomDomainsVariables, + signal?: AbortSignal +) { + if (!projectRef) { + throw new Error('projectRef is required') + } + + const response = (await get( + `${process.env.NEXT_PUBLIC_API_ADMIN_URL}/projects/${projectRef}/custom-hostname`, + { + signal, + } + )) as { + data: { + errors: any[] + result: CustomDomainResponse + success: boolean + messages: any[] + } + status: CustomDomainStatus + error: unknown + } + + if (response.error) { + // not allowed error and no hostname configured error are + // valid steps in the process of setting up a custom domain + // so we convert them to data instead of errors + + const isNotAllowedError = + (response.error as any)?.code === 400 && + (response.error as any)?.message?.includes('not allowed to set up custom domain') + + if (isNotAllowedError) { + return { + customDomain: null, + status: '0_not_allowed', + } as const + } + + const isNoHostnameConfiguredError = + (response.error as any)?.code === 400 && + (response.error as any)?.message?.includes('custom hostname configuration') + + if (isNoHostnameConfiguredError) { + return { + customDomain: null, + status: '0_no_hostname_configured', + } as const + } + + throw response.error + } + + return { customDomain: response?.data?.result, status: response.status } +} + +export type CustomDomainsData = Awaited> +export type CustomDomainsError = unknown + +export const useCustomDomainsQuery = ( + { projectRef }: CustomDomainsVariables, + { enabled = true, ...options }: UseQueryOptions = {} +) => + useQuery( + customDomainKeys.list(projectRef), + ({ signal }) => getCustomDomains({ projectRef }, signal), + { enabled: enabled && typeof projectRef !== 'undefined', ...options } + ) + +export const useCustomDomainsPrefetch = ({ projectRef }: CustomDomainsVariables) => { + const client = useQueryClient() + + return useCallback(() => { + if (projectRef) { + client.prefetchQuery(customDomainKeys.list(projectRef), ({ signal }) => + getCustomDomains({ projectRef }, signal) + ) + } + }, [projectRef]) +} diff --git a/studio/data/custom-domains/custom-domains-reverify-mutation.ts b/studio/data/custom-domains/custom-domains-reverify-mutation.ts new file mode 100644 index 00000000000..8b7e2c8c671 --- /dev/null +++ b/studio/data/custom-domains/custom-domains-reverify-mutation.ts @@ -0,0 +1,49 @@ +import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query' +import { post } from 'lib/common/fetch' +import { customDomainKeys } from './keys' + +export type CustomDomainReverifyVariables = { + projectRef: string +} + +export async function reverifyCustomDomain({ projectRef }: CustomDomainReverifyVariables) { + if (!projectRef) { + throw new Error('projectRef is required') + } + + const response = await post( + `${process.env.NEXT_PUBLIC_API_ADMIN_URL}/projects/${projectRef}/custom-hostname/reverify`, + {} + ) + if (response.error) { + throw response.error + } + + return response +} + +type CustomDomainReverifyData = Awaited> + +export const useCustomDomainReverifyMutation = ({ + onSuccess, + ...options +}: Omit< + UseMutationOptions, + 'mutationFn' +> = {}) => { + const queryClient = useQueryClient() + + return useMutation( + (vars) => reverifyCustomDomain(vars), + { + async onSuccess(data, variables, context) { + const { projectRef } = variables + + await queryClient.invalidateQueries(customDomainKeys.list(projectRef)) + + await onSuccess?.(data, variables, context) + }, + ...options, + } + ) +} diff --git a/studio/data/custom-domains/keys.ts b/studio/data/custom-domains/keys.ts new file mode 100644 index 00000000000..e63bfeb67ca --- /dev/null +++ b/studio/data/custom-domains/keys.ts @@ -0,0 +1,3 @@ +export const customDomainKeys = { + list: (projectRef: string | undefined) => ['projects', projectRef, 'custom-domains'] as const, +} diff --git a/studio/data/query-client.ts b/studio/data/query-client.ts new file mode 100644 index 00000000000..ee9ce6f2522 --- /dev/null +++ b/studio/data/query-client.ts @@ -0,0 +1,21 @@ +import { QueryClient } from '@tanstack/react-query' +import { useState } from 'react' + +let queryClient: QueryClient | undefined + +export function getQueryClient() { + const _queryClient = queryClient ?? new QueryClient() + + // For SSG and SSR always create a new queryClient + if (typeof window === 'undefined') return _queryClient + // Create the queryClient once in the client + if (!queryClient) queryClient = _queryClient + + return queryClient +} + +export function useRootQueryClient() { + const [_queryClient] = useState(() => getQueryClient()) + + return _queryClient +} diff --git a/studio/lib/constants/metrics.tsx b/studio/lib/constants/metrics.tsx index 14b79718a50..796925abace 100644 --- a/studio/lib/constants/metrics.tsx +++ b/studio/lib/constants/metrics.tsx @@ -144,6 +144,13 @@ export const METRICS = [ // provider: 'daily-stats', // category: METRIC_CATEGORIES.API_AUTH, // }, + + { + key: 'total_auth_billing_period_mau', + label: 'Auth Monthly Active User', + provider: 'daily-stats', + category: METRIC_CATEGORIES.API_AUTH, + }, { key: 'total_auth_ingress', label: 'Auth Ingress', diff --git a/studio/package.json b/studio/package.json index 80597c5488e..f127a053018 100644 --- a/studio/package.json +++ b/studio/package.json @@ -28,7 +28,9 @@ "@supabase/postgres-meta": "^0.26.1", "@supabase/react-data-grid": "^7.1.0-beta.7", "@supabase/shared-types": "^0.1.29", - "@supabase/supabase-js": "^2.1.0", + "@supabase/supabase-js": "^2.1.1", + "@tanstack/react-query": "^4.16.1", + "@tanstack/react-query-devtools": "^4.16.1", "ajv": "^8.6.3", "analytics-node": "^3.5.0", "awesome-debounce-promise": "^2.1.0", @@ -132,6 +134,6 @@ "storybook-dark-mode": "^1.0.4", "tailwindcss": "^3.0.22", "ts-jest": "^27.0.7", - "typescript": "4.3.4" + "typescript": "4.8.4" } } diff --git a/studio/pages/_app.tsx b/studio/pages/_app.tsx index 358300caca2..7272e752b9f 100644 --- a/studio/pages/_app.tsx +++ b/studio/pages/_app.tsx @@ -24,11 +24,14 @@ import Head from 'next/head' import { AppPropsWithLayout } from 'types' import { useEffect, useState } from 'react' +import { Hydrate, QueryClientProvider } from '@tanstack/react-query' +import { ReactQueryDevtools } from '@tanstack/react-query-devtools' import { RootStore } from 'stores' import { StoreProvider } from 'hooks' import { GOTRUE_ERRORS } from 'lib/constants' import { auth } from 'lib/gotrue' import { dart } from 'lib/constants/prism' +import { useRootQueryClient } from 'data/query-client' import { PortalToast, RouteValidationWrapper, AppBannerWrapper } from 'components/interfaces/App' import PageTelemetry from 'components/ui/PageTelemetry' @@ -42,6 +45,7 @@ dayjs.extend(timezone) dart(Prism) function MyApp({ Component, pageProps }: AppPropsWithLayout) { + const queryClient = useRootQueryClient() const [rootStore] = useState(() => new RootStore()) useEffect(() => { @@ -96,21 +100,28 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) { const getLayout = Component.getLayout ?? ((page) => page) return ( - - - - Supabase - - - - - - {getLayout()} - - - - - + + + + + + Supabase + + + + + + + {getLayout()} + + + + + + + + + ) } export default MyApp diff --git a/studio/pages/project/[ref]/database/backups/pitr.tsx b/studio/pages/project/[ref]/database/backups/pitr.tsx index c0e74fcb7c9..2d0468532e2 100644 --- a/studio/pages/project/[ref]/database/backups/pitr.tsx +++ b/studio/pages/project/[ref]/database/backups/pitr.tsx @@ -13,6 +13,7 @@ import InformationBox from 'components/ui/InformationBox' import UpgradeToPro from 'components/ui/UpgradeToPro' import { PITRNotice, PITRSelection } from 'components/interfaces/Database/Backups/PITR' import BackupsError from 'components/interfaces/Database/Backups/BackupsError' +import { PRICING_TIER_PRODUCT_IDS } from 'lib/constants' const DatabasePhysicalBackups: NextPageWithLayout = () => { const { ui } = useStore() @@ -51,6 +52,7 @@ const PITR = () => { const { configuration, error, isLoading } = backups const ref = ui.selectedProject?.ref ?? 'default' + const tier = ui.selectedProject?.subscription_tier const isEnabled = configuration.walg_enabled const isPITRSelfServeEnabled = useFlag('pitrSelfServe') @@ -63,8 +65,12 @@ const PITR = () => { return isPITRSelfServeEnabled ? ( ) : ( {
- {projectTier !== undefined && } + {/* [Joshen TODO] Temporarily hidden until usage endpoint is sorted out */} + {/* {projectTier !== undefined && } */}
{project?.status === PROJECT_STATUS.INACTIVE && } diff --git a/studio/pages/project/[ref]/settings/api.tsx b/studio/pages/project/[ref]/settings/api.tsx index 15224b86f1e..63af221d05f 100644 --- a/studio/pages/project/[ref]/settings/api.tsx +++ b/studio/pages/project/[ref]/settings/api.tsx @@ -6,7 +6,6 @@ import { useStore } from 'hooks' import { NextPageWithLayout } from 'types' import { SettingsLayout } from 'components/layouts' import ServiceList from 'components/interfaces/Settings/API/ServiceList' -import { FormsContainer } from 'components/ui/Forms' export const PageContext: any = createContext(null) diff --git a/studio/pages/project/[ref]/settings/billing/invoices.tsx b/studio/pages/project/[ref]/settings/billing/invoices.tsx index 29c76ba8d21..45481e4ff1b 100644 --- a/studio/pages/project/[ref]/settings/billing/invoices.tsx +++ b/studio/pages/project/[ref]/settings/billing/invoices.tsx @@ -57,7 +57,8 @@ const Settings: FC = ({ project }) => { return (
- {projectTier !== undefined && } + {/* [Joshen TODO] Temporarily hidden until usage endpoint is sorted out */} + {/* {projectTier !== undefined && } */}

Invoices

diff --git a/studio/pages/project/[ref]/settings/billing/subscription.tsx b/studio/pages/project/[ref]/settings/billing/subscription.tsx index 34a925b6079..9744f34e911 100644 --- a/studio/pages/project/[ref]/settings/billing/subscription.tsx +++ b/studio/pages/project/[ref]/settings/billing/subscription.tsx @@ -58,7 +58,8 @@ const Settings: FC = ({ project }) => { return (
- {projectTier !== undefined && } + {/* [Joshen TODO] Temporarily hidden until usage endpoint is sorted out */} + {/* {projectTier !== undefined && } */} { + const isCustomDomainsEnabled = useFlag('customDomains') + return (
+ {isCustomDomainsEnabled && }
)