Merge branch 'master' into feat/nav-layout-change

This commit is contained in:
Joshen Lim committed 2023-07-27 16:13:53 +08:00
commit fa524b1bf7
206 files changed
+7294 -1254

No files matched your search

@@ -20,15 +20,15 @@ Now we are going to set up the database schema. You can just copy/paste the SQL
scrollable
size="small"
type="underlined"
defaultActiveId="dashboard"
defaultActiveId="sql"
>
<TabPanel id="dashboard" label="Dashboard">
{/* <TabPanel id="dashboard" label="Dashboard">
1. Go to the [SQL Editor](https://app.supabase.com/project/_/sql) page in the Dashboard.
2. Click **Product Management**.
3. Click **Run**.
</TabPanel>
</TabPanel> */}
<TabPanel id="sql" label="SQL">
<ProductManagementSQLTemplate />
@@ -46,25 +46,8 @@ We just need to get the Project URL and `anon` key from the API settings.
### Set up Google Authentication
From [Google Console](https://console.developers.google.com/apis/library), create a Project
In the project, create new credential
From the [Google Console](https://console.developers.google.com/apis/library), create a new project and add OAuth2 credentials.
<img
width="750"
alt="Screenshot 2023-07-15 at 20 47 31"
src="https://github.com/hieuwu/product-sample-supabase-kt/assets/43868345/f57fc92c-9458-44b2-ba94-688ec61a6a24"
/>
![Create Google OAuth credentials](/docs/img/guides/kotlin/google-cloud-oauth-credentials-create.png)
Then go to Supabase dashboard, in Authentication tab, open Providers to enable Google. Turn on toggle then copy and paste Client ID and Secret from Google Cloud
<img
width="750"
alt="Screenshot 2023-07-15 at 20 48 26"
src="https://github.com/hieuwu/product-sample-supabase-kt/assets/43868345/9b8adf4f-05a9-4ed3-a48c-e64cc7701225"
/>
<img
width="750"
alt="Screenshot 2023-07-15 at 20 49 34"
src="https://github.com/hieuwu/product-sample-supabase-kt/assets/43868345/9dc29384-f7ba-4ace-97f1-2ca0a2ff299a"
/>
In your [Supabase Auth settings](https://app.supabase.com/project/_/auth/providers) enable Google as a provider and set the required credentials as outlined in the [auth docs](/docs/guides/auth/social-login/auth-google).
@@ -477,6 +477,7 @@ export const auth = {
url: undefined,
items: [
{ name: 'Enable Captcha Protection', url: '/guides/auth/auth-captcha' },
{ name: 'Configuring Custom SMTP', url: '/guides/auth/auth-smtp' },
{ name: 'Managing User Data', url: '/guides/auth/managing-user-data' },
{ name: 'Multi-Factor Authentication', url: '/guides/auth/auth-mfa' },
{ name: 'Row Level Security', url: '/guides/auth/row-level-security' },
+11 -9
View File
@@ -18,15 +18,17 @@ This reference documents every object and method available in Supabase's Kotlin
Supported Kotlin targets:
| **Module** | **GoTrue** | **Realtime** | **Postgrest** | **Storage** | **Functions** | **Apollo-GraphQL** |
| -------------------------------------- | ---------- | ------------ | ------------- | ----------- | ------------- | ------------------ |
| **JVM** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| **Android** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| **JS** _(Browser)_ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| **IOS** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| **MacOS** _(macosX64 & macosArm64)_ 🚧 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| **Windows** _(mingwX64)_ 🚧 | ☑️ | ✅ | ✅ | ✅ | ✅ | ❌ |
| **Linux** _(linuxX64)_ 🚧 | ☑️ | ✅ | ✅ | ✅ | ✅ | ❌ |
| | **GoTrue** | **Realtime** | **Postgrest** | **Storage** | **Functions** | **Apollo-GraphQL** |
| ------------------------------------------------------------------ | ---------- | ------------ | ------------- | ----------- | ------------- | ------------------ |
| **JVM** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| **Android** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| **JS** _(Browser, NodeJS)_ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| **IOS** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| **tvOS** _(tvosArm64, tvosX64, tvosSimulatorArm64)_ 🚧 | ☑️ | ✅ | ✅ | ✅ | ✅ | ✅ |
| **watchOS** _(watchosArm64, watchosX64, watchosSimulatorArm64)_ 🚧 | ☑️ | ✅ | ✅ | ✅ | ✅ | ✅ |
| **MacOS** _(macosX64 & macosArm64)_ 🚧 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| **Windows** _(mingwX64)_ 🚧 | ☑️ | ✅ | ✅ | ✅ | ✅ | ❌ |
| **Linux** _(linuxX64)_ 🚧 | ☑️ | ✅ | ✅ | ✅ | ✅ | ❌ |
✅ = full support
+4
View File
@@ -8,6 +8,7 @@ import Head from 'next/head'
import { PropsWithChildren, memo } from 'react'
import Footer from '~/components/Navigation/Footer'
import { menuState, useMenuLevelId, useMenuMobileOpen } from '~/hooks/useMenuState'
import { Announcement, LW8CountdownBanner } from 'ui'
const levelsData = {
home: {
@@ -326,6 +327,9 @@ const SiteLayout = ({ children }: PropsWithChildren<{}>) => {
<title>Supabase Docs</title>
</Head>
<main>
<Announcement>
<LW8CountdownBanner />
</Announcement>
<div className="flex flex-row h-screen">
<NavContainer />
<Container>
@@ -153,7 +153,7 @@ And after ChatGPT receives a response from the plugin it will answer your questi
## Resources
- ChatGPT Retrieval Plugin: [github.com/openai/chatgpt-retrieval-plugin](https://github.com/openai/chatgpt-retrieval-plugin)
- ChatGTP Plugins: [official documentation](https://platform.openai.com/docs/plugins/introduction)
- ChatGPT Plugins: [official documentation](https://platform.openai.com/docs/plugins/introduction)
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
+1 -1
View File
@@ -51,7 +51,7 @@ In the Settings page, look for the **Sitekey** section and copy the key.
## Enable Captcha protection for your Supabase project
Navigate to the **[Authentication](https://supabase.com/dashboard/project/_/settings/auth)** page in the Supabase Dashboard and find the **Enable Captcha protection** toggle under the **Security and Protection** section.
Navigate to the **[Auth](https://supabase.com/dashboard/project/_/settings/auth)** section of your Project Settings in the Supabase Dashboard and find the **Enable Captcha protection** toggle under the **Security and Protection** section.
![supabase_auth_general_settings.png](/docs/img/guides/auth-captcha/supabase_auth_general_settings.png)
@@ -92,7 +92,7 @@ Options are available via `queryParams`:
prompt: 'consent',
hd: 'domain.com',
}}
onlyThirdPartyProviders={true}
onlyThirdPartyProviders
/>
```
+39
View File
@@ -0,0 +1,39 @@
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'auth-smtp',
title: 'Configure a Custom SMTP',
description: 'Moving towards production: Configuring a custom SMTP provider',
}
# Auth SMTP
At present, you can trial the Supabase platform by sending up to **4** emails per hour via the built-in service. The default email service as a whole is offered on a best effort basis: we will do our best to maintain it and will review usage of the service on a regular basis to see if the email service should be continued.
As you progress toward production, you may find yourself wanting for a custom SMTP service in order to increase your limits. A custom SMTP server will allow you to set your own cap on the number of emails sent per hour.
Beyond rate limits, an SMTP server might also help with:
- Deliverability and Reputation Management
- Scalability
- Analytics and Tracking
- Compliance and Anti Spam measures
## How to Set up SMTP
Head over to the [Auth Settings Page](https://supabase.com/dashboard/project/_/settings/auth) and hit "Enable Custom SMTP" under the SMTP Provider section.
Fill in fields below with the relevant details obtained from your custom SMTP provider:
![SMTP settings](/docs/img/guides/auth-smtp/smtp.png)
### SMTP Providers
You can use Supabase Auth with any major SMTP provider of your choosing. Some SMTP providers you could consider using are:
- [Twilio SendGrid](https://docs.sendgrid.com/for-developers/sending-email/integrating-with-the-smtp-api)
- [AWS SES](https://docs.aws.amazon.com/ses/latest/dg/send-email-smtp.html)
- [Resend](https://resend.com/docs/dashboard/emails/introduction)
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
@@ -7,16 +7,42 @@ export const meta = {
video: 'https://www.youtube.com/v/akScoPO01bc',
}
## Overview
In this guide we'll show you how to authenticate your users with SMS based One-Time Password (OTP) tokens.
There are two reasons to use Supabase SMS OTP tokens:
- You want users to log in with mobile number + password, and the mobile number should be verified via SMS
- You want users to log in with mobile number ONLY (i.e. passwordless login)
- You want users to log in with a phone number and password, and verify the phone number on signup with SMS
- You want users to log in with a phone number ONLY (i.e. passwordless login)
We'll cover:
What you'll need:
- Twilio account ([sign up](https://www.twilio.com/try-twilio))
- Supabase project (create one [here](https://supabase.com/dashboard))
- Mobile phone capable of receiving SMS
SMS Authentication can be done with either Twilio Verify or Twilio Programmable Messaging. [Twilio Verify](https://www.twilio.com/en-us/trusted-activation/verify) is a specialized OTP solution and is recommended for most developers that need over-the-phone authentication. Alternatively you can use [Twilio Programmable Messaging](https://www.twilio.com/docs/messaging) which offers generic SMS sending support.
## Twilio Verify
To set up Twilio Verify, you will need to:
1. Create a new [verification service](https://support.twilio.com/hc/en-us/articles/360033309133-Getting-Started-with-Twilio-Verify-V2) in the Twilio dashboard.
2. [Switch Phone Provider to Twilio Verify](https://supabase.com/dashboard/project/_/auth/providers)
3. Configure the Twilio Verify Service ID field using the Verification Service ID obtained in 1.
When using Twilio Verify, OTPs are generated by Twilio. This means that:
- Unlike other providers, the OTP expiry duration and message content fields are not configurable via the Supabase dashboard. Please head to Twilio Verify to configure these settings.
- The token remains the same during its validity period until the verification is successful. This means if your user make another request within that period, they will receive the same token.
- Twilio Verify has a separate set of rate limits that apply. Visit Twilio's [Rate Limit and Timeouts page](https://www.twilio.com/docs/verify/api/rate-limits-and-timeouts) to find out more.
<Admonition type="caution">
At this time, Twilio Verify is only supported on the `whatsapp` and `sms` channels.
</Admonition>
## Twilio (Programmable Messaging)
In this section we'll cover:
- [Finding your Twilio credentials](#finding-your-twilio-credentials)
- [Using OTP with password based logins](#using-otp-with-password-based-logins)
@@ -349,6 +375,8 @@ let { data, error } = await supabase.auth.verifyOtp({
## Resources
- [Reasons to use Twilio Verify](https://www.twilio.com/blog/9-reasons-to-use-the-verify-api)
- [Twilio Verify Documentation](https://www.twilio.com/docs/verify)
- [Twilio Signup](https://www.twilio.com/try-twilio)
- [Supabase Dashboard](https://supabase.com/dashboard)
- [Supabase Row Level Security](/docs/guides/auth#row-level-security)
@@ -33,7 +33,7 @@ Setting up Facebook logins for your application consists of 3 parts:
<SocialProviderSetup provider="Facebook" />
## Set up FaceBook Login for your Facebook App
## Set up Facebook Login for your Facebook App
From the `Add Products to your App` screen:
@@ -403,8 +403,8 @@ To use Auth locally, update your project's `supabase/config.toml` file that gets
```bash supabase/config.toml
[auth.external.github]
enabled = true
client_id = "env($SUPABASE_AUTH_GITHUB_CLIENT_ID)"
secret = "env($SUPABASE_AUTH_GITHUB_SECRET)"
client_id = "env(SUPABASE_AUTH_GITHUB_CLIENT_ID)"
secret = "env(SUPABASE_AUTH_GITHUB_SECRET)"
redirect_uri = "http://localhost:54321/auth/v1/callback"
```
@@ -4,7 +4,7 @@ import StepHikeCompact from '~/components/StepHikeCompact'
export const meta = {
title: 'Use Supabase with Android Kotlin',
subtitle:
'Learn how to create a Supabase project, add some sample data to your database, and query the data from a Android Kotlin app.',
'Learn how to create a Supabase project, add some sample data to your database, and query the data from an Android Kotlin app.',
breadcrumb: 'Framework Quickstarts',
}
@@ -45,8 +45,7 @@ export const meta = {
<StepHikeCompact.Step step={2}>
<StepHikeCompact.Details title="Create an Android app with Android Studio">
Setup environment for Android Development following official document.
Open Android Studio > New > New Android Project
Open Android Studio > New > New Android Project.
</StepHikeCompact.Details>
@@ -55,16 +54,7 @@ export const meta = {
<StepHikeCompact.Step step={3}>
<StepHikeCompact.Details title="Install the Supabase client library">
Setup Supabase libraries requires some others libraries from ktor, so make sure all dependencies are added.
Replace the dependency version placeholders `$supabase_version` and `$ktor_version` with the latest versions as shown below.
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
<a href="https://github.com/supabase-community/supabase-kt/releases" style={{ marginRight: '8px' }}>
<img src="https://img.shields.io/github/release/supabase-community/supabase-kt?label=supabase-kt stable" alt="latest stable supabase-kt version"/>
</a>
<a href="https://github.com/ktorio/ktor/releases">
<img src="https://img.shields.io/github/v/release/ktorio/ktor.svg?label=ktor" alt="latest ktor version"/>
</a>
</div>
Import Supabase and all required dependencies. Replace the version placeholders `$supabase_version` and `$ktor_version` with the respective latest versions.
</StepHikeCompact.Details>
@@ -83,10 +73,7 @@ export const meta = {
<StepHikeCompact.Step step={4}>
<StepHikeCompact.Details title="Install the serializable plugin">
Open `build.gradle` (app), add serialization plugin to use annotation for data parsing. Please note that the plugin version should be the same with Kotlin version in your app.
<a href="https://github.com/JetBrains/kotlin" style={{ marginRight: '8px' }}>
<img src="https://img.shields.io/maven-central/v/org.jetbrains.kotlin/kotlin-maven-plugin.svg?label=kotlin_version" alt="latest stable supabase-kt version"/>
</a>
Open the `build.gradle` (app), add the serialization plugin to use annotation for data parsing. Please note that the plugin version should be the same as the Kotlin version in your app.
</StepHikeCompact.Details>
<StepHikeCompact.Code>
@@ -105,14 +92,12 @@ export const meta = {
<StepHikeCompact.Step step={5}>
<StepHikeCompact.Details title="Initialize the Supabase client">
You can create a whenever you need to perform some API call.
However, it is recommended that we should provide this kind of instance by using dependency injection library like Hilt. So see the code below to provide them properly. This is better for testability and maintainability
You can create a Supabase client whenever you need to perform an API call. That being said, it is recommended to use a dependency injection library like [Hilt](https://developer.android.com/training/dependency-injection/hilt-android#kts).
</StepHikeCompact.Details>
<StepHikeCompact.Code>
```kotlin
// Handy create instance when need
val client = createSupabaseClient(
supabaseUrl = "https://xyzcompany.supabase.co",
supabaseKey = "public-anon-key"
@@ -129,7 +114,7 @@ export const meta = {
<StepHikeCompact.Step step={6}>
<StepHikeCompact.Details title="Create data transfer object">
<StepHikeCompact.Details title="Create a data transfer object">
</StepHikeCompact.Details>
<StepHikeCompact.Code>
@@ -161,8 +146,8 @@ export const meta = {
<StepHikeCompact.Step step={7}>
<StepHikeCompact.Details title="Create domain object">
This kind of object will be consumed by the view
<StepHikeCompact.Details title="Create a domain object">
This kind of object will be consumed by the view.
</StepHikeCompact.Details>
<StepHikeCompact.Code>
@@ -186,7 +171,7 @@ export const meta = {
<StepHikeCompact.Step step={8}>
<StepHikeCompact.Details title="Query data from the app">
Usually, we will put the code of interacting with data source in repository. Create a repository:
Create a repository to interact with the data source.
</StepHikeCompact.Details>
<StepHikeCompact.Code>
@@ -212,7 +197,7 @@ export const meta = {
<StepHikeCompact.Step step={9}>
<StepHikeCompact.Details title="Create a module to provide repository">
For dependency injection, we will use Hilt. Learn more about [Dependency injection with Hilt](https://developer.android.com/training/dependency-injection/hilt-android).
Use [Hilt](https://developer.android.com/training/dependency-injection/hilt-android) for dependency injection.
</StepHikeCompact.Details>
<StepHikeCompact.Code>
```kotlin
@@ -229,7 +214,7 @@ export const meta = {
<StepHikeCompact.Step step={10}>
<StepHikeCompact.Details title="Get data from ViewModel inside a coroutine scope">
To use the repository in a ViewModel, use the `@Inject` annotation.
Add the `@Inject` annotation to use the repository in a ViewModel.
</StepHikeCompact.Details>
<StepHikeCompact.Code>
```kotlin
@@ -269,7 +254,7 @@ export const meta = {
</StepHikeCompact.Step>
<StepHikeCompact.Step step={11}>
<StepHikeCompact.Details title="Observe data in Composable">
<StepHikeCompact.Details title="Observe data in a Composable">
</StepHikeCompact.Details>
<StepHikeCompact.Code>
```kotlin
@@ -1,17 +1,17 @@
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
title: 'Build a Product Management App with Jetpack Compose in Android',
title: 'Build a Product Management Android App with Jetpack Compose',
description: 'Learn how to use Supabase in your Android Kotlin App.',
}
This tutorial demonstrates how to build a basic product management app. The app demonstrates management operations, photo upload, account creation and authentication
This tutorial demonstrates how to build a basic product management app. The app demonstrates management operations, photo upload, account creation and authentication using:
- [Supabase Database](/docs/guides/database) - a Postgres database for storing your user data and [Row Level Security](/docs/guides/auth#row-level-security) so data is protected and users can only access their own information.
- [Supabase Auth](/docs/guides/auth) - users log in through magic links sent to their email (without having to set up passwords).
- [Supabase Auth](/docs/guides/auth) - users log in through magic links sent to their email (without having to set up a password).
- [Supabase Storage](/docs/guides/storage) - users can upload a profile photo.
![manage-product-cover](https://github.com/hieuwu/product-sample-supabase-kt/assets/43868345/1bed0c84-208a-4266-a2ec-2bac50ddf80c)
![manage-product-cover](/docs/img/guides/kotlin/manage-product-cover.png)
<Admonition type="note">
If you get stuck while working through this guide, refer to the [full example on
@@ -24,34 +24,26 @@ This tutorial demonstrates how to build a basic product management app. The app
### Create new Android project
Open Android Studio > New Project > Base Activity (Jetpack Compose)
Open Android Studio > New Project > Base Activity (Jetpack Compose).
<img
width="720"
alt="Screen Shot 2023-06-02 at 21 33 16"
src="https://github.com/hieuwu/product-sample-supabase-kt/assets/43868345/288eef97-d8fe-422f-8bb3-1c8424bab08e"
></img>
![Android Studio new project](/docs/img/guides/kotlin/android-studio-new-project.png)
### Set up API key and secret securely
API key and secret from any services are credential information, we should not want to expose these to others because some strangers will use it for bad purpose
#### Create local environment secret
Create or edit `local.properties` file at top level (same level at build.gradle of the project).
Create or edit the `local.properties` file at the root (same level as `build.gradle`) of your project.
> **Note**: This file should not be pushed directly to the repository, meaning that you should include this in `.gitignore` file
> **Note**: Do not commit this file to your source control, e.g. by adding it to your `.gitignore` file!
```kotlin
API_KEY=YOUR_SUPABASE_API_KEY
SECRET=YOUR_SUPABASE_SECRET
SUPABASE_ANON_KEY=YOUR_SUPABASE_ANON_KEY
SUPABASE_URL=YOUR_SUPABASE_URL
```
#### Read and set value to BuildConfig
In `build.gradle (app)`, create `Properties` object and read value from `local.properties` set value for `BuildConfig` by calling `buildConfigField` method
In your `build.gradle` (app) file, create a `Properties` object and read the values from your `local.properties` file by calling the `buildConfigField` method:
```kotlin
defaultConfig {
@@ -65,7 +57,7 @@ defaultConfig {
// Set value part
Properties properties = new Properties()
properties.load(project.rootProject.file("local.properties").newDataInputStream())
buildConfigField("String", "API_KEY", "\"${properties.getProperty("API_KEY")}\"")
buildConfigField("String", "SUPABASE_ANON_KEY", "\"${properties.getProperty("SUPABASE_ANON_KEY")}\"")
buildConfigField("String", "SECRET", "\"${properties.getProperty("SECRET")}\"")
buildConfigField("String", "SUPABASE_URL", "\"${properties.getProperty("SUPABASE_URL")}\"")
}
@@ -73,43 +65,20 @@ defaultConfig {
#### Use value from `BuildConfig`
Read value from `BuildConfig` (Remember to import the right class, it is one from your project). For example:
Read the value from `BuildConfig`:
```kotlin
val url = BuildConfig.SUPABASE_URL
val apiKey = BuildConfig.API_KEY
val apiKey = BuildConfig.SUPABASE_ANON_KEY
```
### Set up Supabase dependencies
<img
width="720"
alt="Screen Shot 2023-06-05 at 21 39 24"
src="https://github.com/hieuwu/product-sample-supabase-kt/assets/43868345/3f154fda-aa9e-4baa-81f2-5b744c7ad2fe"
></img>
![Gradle dependencies](/docs/img/guides/kotlin/gradle-dependencies.png)
In build.gradle (app) file, add these dependencies then press Sync now. Replace the dependency version placeholders `$supabase_version` and `$ktor_version` with the latest versions as shown below.
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
<a
href="https://github.com/supabase-community/supabase-kt/releases"
style={{ marginRight: '8px' }}
>
<img
src="https://img.shields.io/github/release/supabase-community/supabase-kt?label=supabase-kt stable"
alt="latest stable supabase-kt version"
/>
</a>
<a href="https://github.com/supabase-community/supabase-kt/releases">
<img
src="https://img.shields.io/github/v/release/ktorio/ktor.svg?label=ktor"
alt="latest ktor version"
/>
</a>
</div>
In the `build.gradle` (app) file, add these dependencies then press "Sync now". Replace the dependency version placeholders `$supabase_version` and `$ktor_version` with their respective latest versions.
```kotlin
// Supabase setup
implementation "io.github.jan-tennert.supabase:postgrest-kt:$supabase_version"
implementation "io.github.jan-tennert.supabase:storage-kt:$supabase_version"
implementation "io.github.jan-tennert.supabase:gotrue-kt:$supabase_version"
@@ -118,11 +87,8 @@ implementation "io.ktor:ktor-client-core:$ktor_version"
implementation "io.ktor:ktor-utils:$ktor_version"
```
Also in build.gradle (app) file, add plugin for serialization. The version of this plugin should be the same with Kotlin version
Also in the `build.gradle` (app) file, add the plugin for serialization. The version of this plugin should be the same as your Kotlin version.
<a href="https://github.com/JetBrains/kotlin" style={{ marginRight: '8px' }}>
<img src="https://img.shields.io/maven-central/v/org.jetbrains.kotlin/kotlin-maven-plugin.svg?label=kotlin_version" alt="latest kotlin version"/>
</a>
```kotlin
plugins {
...
@@ -133,7 +99,7 @@ plugins {
### Set up Hilt for dependency injection
In build.gradle (app) file, add these
In the `build.gradle` (app) file, add the following:
```kotlin
implementation "com.google.dagger:hilt-android:$hilt_version"
@@ -141,7 +107,7 @@ annotationProcessor "com.google.dagger:hilt-compiler:$hilt_version"
implementation("androidx.hilt:hilt-navigation-compose:1.0.0")
```
Create new `ManageProductApplication.kt` class extending Application with `@HiltAndroidApp` annotation
Create a new `ManageProductApplication.kt` class extending Application with `@HiltAndroidApp` annotation:
```kotlin
// ManageProductApplication.kt
@@ -149,7 +115,7 @@ Create new `ManageProductApplication.kt` class extending Application with `@Hilt
class ManageProductApplication: Application()
```
Open AndroidManifest.xml file, update name property of Application tag
Open the `AndroidManifest.xml` file, update name property of Application tag:
```xml
<application
@@ -160,7 +126,7 @@ Open AndroidManifest.xml file, update name property of Application tag
```
Add annotation for MainActivity
Add annotation for the `MainActivity`:
```kotlin
@AndroidEntryPoint
@@ -170,8 +136,7 @@ class MainActivity : ComponentActivity() {
### Provide Supabase instances with Hilt
You can create it by just calling the constructor. However to make the app more testable, these should be provided
Create SupabaseMdule.kt
To make the app easier to test, create a `SupabaseMdule.kt` file as follows:
```kotlin
@InstallIn(SingletonComponent::class)
@@ -183,7 +148,7 @@ object SupabaseModule {
fun provideSupabaseClient(): SupabaseClient {
return createSupabaseClient(
supabaseUrl = BuildConfig.SUPABASE_URL,
supabaseKey = BuildConfig.API_KEY
supabaseKey = BuildConfig.SUPABASE_ANON_KEY
) {
install(Postgrest)
install(GoTrue) {
@@ -217,9 +182,9 @@ object SupabaseModule {
}
```
### Create data transfer object to get data
### Create a data transfer object
Create `ProductDto.kt` class and use annotations to parse data from Supabase
Create a `ProductDto.kt` class and use annotations to parse data from Supabase:
```kotlin
@Serializable
@@ -239,7 +204,7 @@ data class ProductDto(
)
```
Domain object `Product.kt`to provide to view
Create a Domain object in `Product.kt` expose the data in your view:
```kotlin
data class Product(
@@ -252,10 +217,9 @@ data class Product(
### Implement repositories
Create ProductRepository interface and its implementation named ProductRepositoryImpl.
This kind of class will hold every logic to interact with data sources from Supabase. Do the same with AuthenticationRepository
Create a `ProductRepository` interface and its implementation named `ProductRepositoryImpl`. This holds the logic to interact with data sources from Supabase. Do the same with the `AuthenticationRepository`.
Create Product Repository
Create the Product Repository:
```kotlin
interface ProductRepository {
@@ -355,7 +319,7 @@ class ProductRepositoryImpl @Inject constructor(
}
```
Create Authentication Repository
Create the Authentication Repository:
```kotlin
interface AuthenticationRepository {
@@ -406,15 +370,9 @@ class AuthenticationRepositoryImpl @Inject constructor(
### Implement screens
<Admonition type="note">
By the time this page is written, there might be some changes in the code to make it more simple.
If you face any problem when implement, feel free to refer the example repository above
</Admonition>
Create ProductListViewModel
Create a `ProductListViewModel`:
```kotlin
@HiltViewModel
class ProductListViewModel @Inject constructor(
private val productRepository: ProductRepository,
@@ -463,7 +421,7 @@ private val productRepository: ProductRepository,
```
Create ProductListScreen.kt
Create the `ProductListScreen.kt`:
```kotlin
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class)
@@ -581,7 +539,7 @@ private fun AddProductButton(
}
```
Create ProductDetailsViewModel.kt
Create the `ProductDetailsViewModel.kt`:
```kotlin
@@ -654,7 +612,7 @@ class ProductDetailsViewModel @Inject constructor(
}
```
Create ProductDetailsScreen.kt
Create the `ProductDetailsScreen.kt`:
```kotlin
@OptIn(ExperimentalCoilApi::class)
@@ -826,7 +784,7 @@ private fun uriToByteArray(contentResolver: ContentResolver, uri: Uri): ByteArra
}
```
Create AddProductScreen
Create a AddProductScreen:
```kotlin
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
@@ -885,7 +843,7 @@ fun AddProductScreen(
}
```
Create AddProductViewModel.kt
Create the `AddProductViewModel.kt`:
```kotlin
@HiltViewModel
@@ -917,7 +875,7 @@ class AddProductViewModel @Inject constructor(
}
```
Create SignUpViewModel
Create a `SignUpViewModel`:
```kotlin
@HiltViewModel
@@ -950,7 +908,7 @@ class SignUpViewModel @Inject constructor(
}
```
Create SignUpScreen.kt
Create the `SignUpScreen.kt`:
```kotlin
@Composable
@@ -1048,7 +1006,7 @@ fun SignUpScreen(
}
```
Create SignInViewModel
Create a `SignInViewModel`:
```kotlin
@HiltViewModel
@@ -1088,7 +1046,7 @@ class SignInViewModel @Inject constructor(
}
```
Create SignInScreen.kt
Create the `SignInScreen.kt`:
```kotlin
@OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class)
@@ -1203,10 +1161,9 @@ fun SignInScreen(
}
```
Create successful screen
Implement Sign In Success screen
### Create the success screen
Create new Empty Activity and in AndroidManifest.xml, add deep link, make sure `scheme` and `host` are the same with ones when set up GoTrue instance
Implement a "Sign In" success screen. Create new "Empty Activity" and in `AndroidManifest.xml`, add a deep link, make sure `scheme` and `host` are the same as the ones set in the GoTrue instance.
```xml
<?xml version="1.0" encoding="utf-8"?>
@@ -21,7 +21,7 @@ After developing your project and deciding it's Production Ready, you should run
- Go to the Authentication > Policies page in the Supabase Dashboard to enable RLS and create security policies.
- Go to the Database > Replication page in the Supabase Dashboard to manage replication tables.
- Enable 2FA on GitHub. Since your GitHub account gives you administrative rights to your Supabase project, you should protect it with a strong password and 2FA using a U2F key or a TOTP app.
- Ensure email confirmations are enabled in the `Auth > Settings` page.
- Ensure email confirmations are enabled in the `Settings > Auth` page.
- Use a custom SMTP server for auth emails so that your users can see that the mails are coming from a trusted domain (preferably the same domain that your app is hosted on). Grab SMTP credentials from any major email provider such as SendGrid, AWS SES, etc.
- Think hard about how _you_ would abuse your service as an attacker, and mitigate.
- Review these [common cybersecurity threats](https://auth0.com/docs/security/prevent-threats).
@@ -39,9 +39,9 @@ After developing your project and deciding it's Production Ready, you should run
## Availability
- Use your own SMTP credentials so that you have full control over the deliverability of your transactional auth emails (see Auth > Settings)
- you can grab SMTP credentials from any major email provider such as SendGrid, AWS SES, etc.
- The default rate limit for auth emails provided by Supabase is 30 new users per hour, if doing a major public announcement you will likely require more than this.
- Use your own SMTP credentials so that you have full control over the deliverability of your transactional auth emails (see Settings > Auth)
- you can grab SMTP credentials from any major email provider such as SendGrid, AWS SES, etc. You can refer to our [SMTP guide](/docs/guides/auth/auth-smtp) for more details.
- The default rate limit for auth emails when using a custom SMTP provider is 30 new users per hour, if doing a major public announcement you will likely require more than this.
- If your application is on the free plan and is **not** expected to be queried at least once every 7 days, then it may be paused by Supabase to save on server resources.
- You can restore paused projects from the Supabase dashboard.
- Upgrade to Pro to guarantee that your project will not be paused for inactivity.
@@ -48,14 +48,10 @@ We can use your JWT Secret to generate new `anon` and `service` API keys using t
### Update API Keys
Replace the values in these files:
Replace the values the `.env` file:
- `.env`:
- `ANON_KEY` - replace with an `anon` key
- `SERVICE_ROLE_KEY` - replace with a `service` key
- `volumes/api/kong.yml`
- `anon` - replace with an `anon` key
- `service_role` - replace with a `service` key
### Update Secrets
@@ -40,7 +40,7 @@ curl -X POST 'https://<project-ref>.supabase.co/auth/v1/magiclink' \
}'
```
Gotrue is responsible for issuing access tokens for your users, sends confirmation, magic-link, and password recovery emails (by default we send these from a Supabase SMTP server, but you can easily plug in your own inside the dashboard at Auth > Settings) and also transacting with third party OAuth providers to get basic user data.
Gotrue is responsible for issuing access tokens for your users, sends confirmation, magic-link, and password recovery emails (by default we send these from a Supabase SMTP server, but you can easily plug in your own inside the dashboard at Project Settings > Auth) and also transacting with third party OAuth providers to get basic user data.
The community even recently built in the functionality to request custom OAuth scopes, if your users need to interact more closely with the provider. See the scopes parameter here: [https://github.com/supabase/gotrue#get-authorize](https://github.com/supabase/gotrue#get-authorize).
Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 183 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 273 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 662 KiB

@@ -57,7 +57,7 @@ And voila, we had a baseline for generating variations:
The final visual that you see everywhere won us over because of its vibrant swirls. While each swirl looks slightly different, a consistent style is still maintained throughout.
Fortunately, we didn't require full-width images for each ticket. Upscaled MidJourney images sufficed. Nevertheless, we had to recreate the main visual on the landing page in vectors to use it effectively. You can view it in full glory at [https://supabase.com/launch-week](https://supabase.com/launch-week).
Fortunately, we didn't require full-width images for each ticket. Upscaled MidJourney images sufficed. Nevertheless, we had to recreate the main visual on the landing page in vectors to use it effectively. You can view it in full glory at [https://supabase.com/launch-week/7](https://supabase.com/launch-week/7).
![Screenshot 2023-04-06 at 09.47.47.png](/images/blog/launch-week-7/designing-with-ai-midjourney/launch-week-7-ticket-home.png)
@@ -258,13 +258,13 @@ const channel = supabase
.subscribe()
```
Interested to know how this fits within your Next.js application? Find the code [here](https://github.com/supabase/supabase/blob/master/apps/www/components/LaunchWeek/Ticket/TicketForm.tsx#L70-L102).
Interested to know how this fits within your Next.js application? Find the code [here](https://github.com/supabase/supabase/blob/master/apps/www/components/LaunchWeek/7/Ticket/TicketForm.tsx#L70-L102).
## Displaying the images
We have some images, so how can we now display them somewhere? [@Francesco Sansalvadore](https://twitter.com/frnk_snslvdr) saw one of our team members trying to “swipe” on the tickets slider component on the [launch week page](https://supabase.com/launch-week/tickets) and he thought, why not feature all the fantastic people who generated tickets on a single page?
We have some images, so how can we now display them somewhere? [@Francesco Sansalvadore](https://twitter.com/frnk_snslvdr) saw one of our team members trying to “swipe” on the tickets slider component on the [launch week page](https://supabase.com/launch-week/7/tickets) and he thought, why not feature all the fantastic people who generated tickets on a single page?
We built a [ticket wall](https://supabase.com/launch-week/tickets) that you can scroll endlessly. We approached it with an infinite scroll technique, lazy-loading a few tickets at a time.
We built a [ticket wall](https://supabase.com/launch-week/7/tickets) that you can scroll endlessly. We approached it with an infinite scroll technique, lazy-loading a few tickets at a time.
![launch week 7 ticket wall](/images/blog/launch-week-7/designing-with-ai-midjourney/launch-week-7-ticket-wall.png)
+15 -7
View File
@@ -1,18 +1,26 @@
import { Button } from 'ui'
const CTABanner = (props: any) => {
const { darkerBg } = props
interface Props {
className?: string
darkerBg?: boolean
}
const CTABanner = ({ darkerBg, className }: Props) => {
return (
<div
className={`
bg-scale-200 grid grid-cols-12 items-center gap-4 border-t py-32 text-center
${darkerBg ? 'dark:bg-dark-900' : ''} px-16
`}
className={[
`bg-scale-200 grid grid-cols-12 items-center gap-4 border-t py-32 text-center px-16`,
darkerBg && 'dark:bg-dark-900',
className,
].join(' ')}
>
<div className="col-span-12">
<h2 className="h2">
<span className="text-scale-900">Build in a weekend,</span>
<span className="text-scale-1200 dark:text-white"> scale to millions</span>
<span className="text-scale-1200 dark:text-white block sm:inline">
{' '}
scale to millions
</span>
</h2>
</div>
<div className="col-span-12 mt-4">
+4 -3
View File
@@ -1,20 +1,21 @@
import { useTheme } from 'common/Providers'
import { IconSun, IconMoon } from 'ui'
function DarkModeToggle() {
function DarkModeToggle({ disabled = false }: { disabled?: boolean }) {
const { isDarkMode, toggleTheme } = useTheme()
return (
<div className="flex items-center">
<div className={`flex items-center ${disabled && 'opacity-30'}`}>
<IconSun className="text-scale-900" strokeWidth={2} />
<button
type="button"
aria-pressed="false"
disabled={disabled}
className={`
relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent
transition-colors duration-200 ease-in-out focus:outline-none ${
isDarkMode ? 'bg-scale-500' : 'bg-scale-900'
} mx-5
} ${disabled && '!cursor-not-allowed'} mx-5
`}
onClick={() => toggleTheme()}
>
+2 -2
View File
@@ -17,7 +17,7 @@ interface Props {
const Footer = (props: Props) => {
const { isDarkMode } = useTheme()
const { pathname } = useRouter()
const isLaunchWeekPage = pathname.includes('launch-week')
const isLaunchWeekPage = pathname.includes('launch-week') || pathname === '/'
return (
<footer
@@ -149,7 +149,7 @@ const Footer = (props: Props) => {
</div>
<div className="border-scale-500 dark:border-scale-600 mt-32 flex justify-between border-t pt-8">
<small className="small">&copy; Supabase Inc</small>
<DarkModeToggle />
<DarkModeToggle disabled={isLaunchWeekPage} />
</div>
</SectionContainer>
</footer>
+56 -5
View File
@@ -3,31 +3,59 @@ import { useRouter } from 'next/router'
import Telemetry, { TelemetryEvent } from '~/lib/telemetry'
import { useTelemetryProps } from 'common/hooks/useTelemetryProps'
import gaEvents from '~/lib/gaEvents'
import { Button, IconBookOpen } from 'ui'
import { Badge, Button, IconArrowRight, IconBookOpen } from 'ui'
import SectionContainer from '~/components/Layouts/SectionContainer'
import HeroFrameworks from './HeroFrameworks'
import styles from './hero.module.css'
import Image from 'next/image'
import { range } from 'lodash'
import ParticlesCanvas from '../LaunchWeek/8/AnimatedParticles/ParticlesCanvas'
import { useBreakpoint } from 'common'
import { useEffect, useState } from 'react'
const Hero = () => {
const router = useRouter()
const [showAnnouncement, setShowAnnouncement] = useState(false)
const telemetryProps = useTelemetryProps()
const isMobile = useBreakpoint()
const sendTelemetryEvent = async (event: TelemetryEvent) => {
await Telemetry.sendEvent(event, telemetryProps, router)
}
useEffect(() => {
if (typeof window !== undefined) {
const lw8AnnouncementKey = window.localStorage.getItem('announcement_SupabaseLaunchWeek8')
setShowAnnouncement(!!lw8AnnouncementKey)
}
}, [showAnnouncement])
return (
<div className="relative">
<SectionContainer className="py-12 md:py-16 lg:py-20">
<div className="relative -mt-[65px]">
<SectionContainer className="py-12 md:py-16 lg:py-20 overflow-hidden">
<div className="relative">
<div className="mx-auto">
<div className="mx-auto max-w-2xl lg:col-span-6 lg:flex lg:items-center justify-center text-center">
<div
className={[
'appear-first lg:h-[50vh] lg:min-h-[300px] lg:max-h-[450px] flex flex-col items-center justify-center sm:mx-auto md:w-3/4 lg:mx-0 lg:w-full gap-4 lg:gap-8',
'relative z-10 appear-first lg:h-auto pt-[250px] lg:pt-[250px] lg:min-h-[300px] flex flex-col items-center justify-center sm:mx-auto md:w-3/4 lg:mx-0 lg:w-full gap-4 lg:gap-8',
styles['hero-text'],
].join(' ')}
>
<div>
<div className="flex flex-col items-center">
{showAnnouncement && (
<Link href="/launch-week">
<a className="group w-auto rounded-full p-1 mb-6 mt-8 md:mb-10 bg-gradient-to-b from-[#1b1f2124] to-[#02040550] hover:from-[#ffffff15] hover:to-[#ffffff05] transition-all backdrop-blur-lg border flex items-center justify-between gap-2 md:gap-4 text-scale-1100 hover:text-scale-1200 text-sm">
<div className="text-scale-200 rounded-full bg-brand-900 px-3 py-1 flex items-center justify-center">
Get you ticket
</div>
<span>Launch Week 8 - August 7th</span>
<IconArrowRight
size={16}
className="transform mr-2 transition-transform translate-x-0 sm:-translate-x-1 group-hover:translate-x-0"
/>
</a>
</Link>
)}
<h1 className="text-scale-1200 text-4xl sm:text-5xl sm:leading-none lg:text-7xl">
<span className="block text-[#F4FFFA00] bg-clip-text bg-gradient-to-b from-scale-1200 to-scale-1200 dark:to-scale-1100">
Build in a weekend
@@ -63,12 +91,35 @@ const Hero = () => {
</a>
</Link>
</div>
<HeroFrameworks className="mt-4 lg:mt-6" />
</div>
</div>
</div>
</div>
<div className="absolute -top-2 md:top-0 -left-10 -right-10 md:left-0 md:right-0 h-[500px] lg:h-[550px] z-0">
<ParticlesCanvas users={range(0, 60)} />
<Image
src="/images/launchweek/8/stars.svg"
alt="starts background"
layout="fill"
objectFit="cover"
className="opacity-70"
draggable={false}
/>
</div>
</SectionContainer>
<div className="absolute w-full aspect-[1/1] md:aspect-[1.5/1] lg:aspect-[2.5/1] inset-0 z-0">
<Image
src="/images/launchweek/8/LW8-gradient.png"
layout="fill"
objectFit={isMobile ? 'cover' : 'contain'}
objectPosition="top"
priority
draggable={false}
/>
</div>
<div className="absolute top-0 left-0 right-0 h-screen bg-gradient-to-b from-[#020405] to-transparent -z-10" />
<div className="w-1/2 container mx-auto h-px bg-gradient-to-r from-transparent via-scale-600 to-transparent" />
</div>
)
+1 -1
View File
@@ -33,7 +33,7 @@ const frameworks = [
},
{
name: 'Android Kotlin',
icon: '/images/logos/frameworks/kotlin.svg',
icon: 'M29.6,11.4L11.4,28.2V13.4c0-1.1,1-2,2.1-2H29.6z M45.1,14.7L25.4,32.9L11.6,45.5c-0.2-0.3-0.3-0.6-0.3-0.9V33.7l24.2-22.3h8C45.5,11.4,46.4,13.5,45.1,14.7z M44.5,46.6H16.5l16.5-15.2L46,43.3C47.3,44.5,46.4,46.6,44.5,46.6z',
docs: '/docs/guides/getting-started/quickstarts/kotlin',
gaEvent: 'www_hp_hero_frameworks_kotlin',
},
@@ -2,7 +2,7 @@ import { motion } from 'framer-motion'
import Image from 'next/image'
import Link from 'next/link'
import React from 'react'
import days from '~/components/LaunchWeek/lw7_days'
import days from '~/components/LaunchWeek/7/lw7_days'
const BlogLinks = () => {
const activeDays = days.filter((day) => Date.parse(day.publishedAt) <= Date.now())
@@ -1,4 +1,4 @@
import { Announcement as AnnouncementType } from '../types'
import { Announcement as AnnouncementType } from '../../types'
export const Announcement = (props: AnnouncementType) => {
const containerClasses = []
@@ -1,5 +1,5 @@
import { NewspaperIcon } from '@heroicons/react/outline'
import { Article } from '../types'
import { Article } from '../../types'
const ArticleButton = (props: Article) => {
return (
@@ -1,6 +1,6 @@
import { NewspaperIcon } from '@heroicons/react/outline'
import Link from 'next/link'
import { Article } from '../types'
import { Article } from '../../types'
const ArticleButtonListItem = (props: Article) => {
return (
@@ -1,5 +1,5 @@
import { SparklesIcon } from '@heroicons/react/outline'
import { Article } from '../types'
import { Article } from '../../types'
const ProductButton = (props: Article) => {
return (
@@ -1,6 +1,6 @@
import { SparklesIcon } from '@heroicons/react/outline'
import Link from 'next/link'
import { Article } from '../types'
import { Article } from '../../types'
export const ProductButtonListItem = (props: Article) => {
return (
@@ -1,7 +1,7 @@
import React, { useState } from 'react'
import TicketHeader from '../Ticket/TicketHeader'
import TicketNumber from '../Ticket/TicketNumber'
import { UserData } from '../Ticket/hooks/use-conf-data'
import { UserData } from '~/components/LaunchWeek/hooks/use-conf-data'
import Image from 'next/image'
interface Props {
@@ -2,7 +2,7 @@ import Link from 'next/link'
import React from 'react'
import { Button } from 'ui'
import { useBreakpoint } from 'common/hooks/useBreakpoint'
import { UserData } from '../Ticket/hooks/use-conf-data'
import { UserData } from '~/components/LaunchWeek/hooks/use-conf-data'
import { TicketBrickWallSlider } from './TicketBrickWallSlider'
type user = UserData
@@ -1,7 +1,7 @@
import { useEffect, useRef, useState } from 'react'
import Image from 'next/image'
import Link from 'next/link'
import { UserData } from '../Ticket/hooks/use-conf-data'
import { UserData } from '~/components/LaunchWeek/hooks/use-conf-data'
import { useWindowSize } from 'react-use'
import { Swiper, SwiperSlide } from 'swiper/react'
@@ -34,7 +34,7 @@ export function TicketBrickWallSlider({ users, reverse, speed = 10000 }: Props)
return (
<div ref={ref} className="relative h-auto w-full m-0 overflow-hidden mb-2.5">
<div className="flex swiper-transition-linear">
<div className="flex swiper-transition-linear ticket-brick-swiper">
<Swiper
onSwiper={(swiper) => setSwiperInstance(swiper)}
centeredSlides={true}
@@ -65,7 +65,7 @@ export function TicketBrickWallSlider({ users, reverse, speed = 10000 }: Props)
>
{users.map((user, i) => (
<SwiperSlide key={user.username}>
<Link href={`/launch-week/tickets/${user.username}`} key={user.username}>
<Link href={`/launch-week/7/tickets/${user.username}`} key={user.username}>
<a className="relative !w-[230px] md:w-[450px] !h-[200px] rounded-md md:rounded-lg transition-transform">
<div className="relative w-full pt-[50%] transform rounded-md md:rounded-lg overflow-hidden bg-gradient-to-b from-[#ffffff60] to-[#ffffff10]">
<div className="absolute inset-[1px] w-full h-full rounded-md md:rounded-lg overflow-hidden p-[1px]">
@@ -3,7 +3,7 @@ import { useState } from 'react'
import { PlayIcon, TruckIcon, XIcon } from '@heroicons/react/outline'
import { Badge, Modal } from 'ui'
import { Article, Product, WeekDayProps } from '../types'
import { Article, Product, WeekDayProps } from '../../types'
import _days from './../days.json'
import ArticleButtonListItem from './ArticleButtonListItem'
import ProductButtonListItem from './ProductButtonListItem'
@@ -1,5 +1,5 @@
import React from 'react'
import LabelBadge from '../LabelBadge'
import LabelBadge from './LabelBadge'
import LaunchWeekPrizeCard from './LaunchWeekPrizeCard'
import { motion } from 'framer-motion'
@@ -1,6 +1,6 @@
import React from 'react'
import useConfData from '~/components/LaunchWeek/Ticket//hooks/use-conf-data'
import useWinningChances from './Ticket/hooks/useWinningChances'
import useConfData from '~/components/LaunchWeek/hooks/use-conf-data'
import useWinningChances from '../hooks/useWinningChances'
export default function ReferralIndicator() {
const winningChances = useWinningChances()
@@ -11,8 +11,8 @@ import {
StyledArticleBadge,
} from './components'
import { WeekDayProps } from '~/components/LaunchWeek/lw7_days'
import { opacityVariant4, scaleOpacityVariant2 } from './LW7Releases'
import { WeekDayProps } from '~/components/LaunchWeek/7/lw7_days'
import { opacityVariant4, scaleOpacityVariant2 } from '.'
import styles from './day5.module.css'
import Link from 'next/link'
@@ -3,7 +3,7 @@ import { useEffect } from 'react'
import { Accordion } from 'ui'
import Lottie from 'lottie-react'
import days, { WeekDayProps, endOfLW7 } from '~/components/LaunchWeek/lw7_days'
import days, { WeekDayProps, endOfLW7 } from '~/components/LaunchWeek/7/lw7_days'
import SectionContainer from '~/components/Layouts/SectionContainer'
import {
AccordionHeader,
@@ -1,14 +1,14 @@
import cn from 'classnames'
import { useRef, useState } from 'react'
import styles from './ticket.module.css'
import styleUtils from './utils.module.css'
import styleUtils from '../../utils.module.css'
import TicketVisual from './TicketVisual'
import TicketActions from './TicketActions'
import TicketCopy from './ticket-copy'
import { UserData } from './hooks/use-conf-data'
import { UserData } from '~/components/LaunchWeek/hooks/use-conf-data'
import ReferralIndicator from '../ReferralIndicator'
import useWinningChances from './hooks/useWinningChances'
import useWinningChances from '../../hooks/useWinningChances'
import { SITE_URL } from '~/lib/constants'
import { useBreakpoint } from 'common/hooks/useBreakpoint'
@@ -1,7 +1,7 @@
import { useEffect, useRef, useState } from 'react'
import { SITE_URL, TWEET_TEXT, TWEET_TEXT_GOLDEN } from '~/lib/constants'
import { IconCheckCircle } from 'ui'
import useConfData from '~/components/LaunchWeek/Ticket//hooks/use-conf-data'
import useConfData from '~/components/LaunchWeek/hooks/use-conf-data'
import { useParams } from '~/hooks/useParams'
import TicketForm from './TicketForm'
@@ -62,7 +62,7 @@ export default function TicketActions({
<div className="text-scale-900">
<IconCheckCircle size={10} strokeWidth={1} />
</div>
Connect with Github
Connect with GitHub
</div>
</div>
<div
@@ -1,5 +1,5 @@
import { useState } from 'react'
import { PageState, ConfDataContext, UserData } from './hooks/use-conf-data'
import { PageState, ConfDataContext, UserData } from '~/components/LaunchWeek/hooks/use-conf-data'
import Ticket from './ActualTicket'
import Form from './form'
import { SupabaseClient, Session } from '@supabase/supabase-js'
@@ -2,7 +2,7 @@ import { useState, useRef, useEffect } from 'react'
import { useRouter } from 'next/router'
import cn from 'classnames'
import { SITE_ORIGIN } from '~/lib/constants'
import useConfData from '~/components/LaunchWeek/Ticket//hooks/use-conf-data'
import useConfData from '~/components/LaunchWeek/hooks/use-conf-data'
import formStyles from './form.module.css'
import ticketFormStyles from './ticket-form.module.css'
import { Button, IconCheckCircle, IconLoader } from 'ui'
@@ -40,7 +40,7 @@ export default function TicketForm({ defaultUsername = '', setTicketGenerationSt
.eq('email', email)
.select()
.single()
.then(async ({ error }) => {
.then(async ({ error }: any) => {
// If error because of duplicate email, ignore and proceed, otherwise sign out.
if (error && error?.code !== '23505') return supabase.auth.signOut()
const { data } = await supabase
@@ -79,7 +79,7 @@ export default function TicketForm({ defaultUsername = '', setTicketGenerationSt
table: 'lw7_tickets',
filter: `username=eq.${username}`,
},
(payload) => {
(payload: any) => {
const golden = !!payload.new.sharedOnTwitter && !!payload.new.sharedOnLinkedIn
setUserData({
...payload.new,
@@ -157,12 +157,12 @@ export default function TicketForm({ defaultUsername = '', setTicketGenerationSt
{session ? (
<>
<IconCheckCircle />
Connect with Github
Connect with GitHub
</>
) : (
<span className="flex items-center gap-2">
{formState === 'loading' && <IconLoader size={14} className="animate-spin" />}
Connect with Github
Connect with GitHub
</span>
)}
</span>
@@ -1,5 +1,5 @@
import React from 'react'
import { DATE } from '~/lib/constants'
import { LW7_DATE } from '~/lib/constants'
interface Props {
size?: 'default' | 'small'
@@ -12,7 +12,7 @@ export default function TicketHeader({ size }: Props) {
className={size === 'small' ? 'w-[165px]' : 'w-[220px] md:w-[230px]'}
src={`/images/launchweek/ticket-header-logo.png`}
/>
<span className="text-white text-xs md:text-sm">{DATE}</span>
<span className="text-white text-xs md:text-sm">{LW7_DATE}</span>
</div>
</div>
)
@@ -2,7 +2,7 @@ import styles from './ticket-visual.module.css'
import TicketProfile from './TicketProfile'
import TicketNumber from './TicketNumber'
import Tilt from 'vanilla-tilt'
import useConfData from '~/components/LaunchWeek/Ticket/hooks/use-conf-data'
import useConfData from '~/components/LaunchWeek/hooks/use-conf-data'
import TicketHeader from './TicketHeader'
import { useEffect, useRef, useState } from 'react'
import Image from 'next/image'
@@ -1,10 +1,10 @@
import { useState, useCallback } from 'react'
import cn from 'classnames'
import useConfData from '~/components/LaunchWeek/Ticket/hooks/use-conf-data'
import useConfData from '~/components/LaunchWeek/hooks/use-conf-data'
import { useRouter } from 'next/router'
import styleUtils from './utils.module.css'
import styleUtils from '../../utils.module.css'
import styles from './form.module.css'
import useEmailQueryParam from '~/components/LaunchWeek/Ticket/hooks/use-email-query-param'
import useEmailQueryParam from '~/components/LaunchWeek/hooks/use-email-query-param'
import { IconLoader } from '~/../../packages/ui'
type FormState = 'default' | 'loading' | 'error'
@@ -1,7 +1,7 @@
import { useEffect, useState, useRef } from 'react'
import cn from 'classnames'
import { SITE_URL } from '~/lib/constants'
import styleUtils from './utils.module.css'
import styleUtils from '../../utils.module.css'
import { IconCopy, IconCheck } from 'ui'
type Props = {
@@ -1,6 +1,6 @@
import styles from './ticket-info.module.css'
import cn from 'classnames'
import { DATE, SITE_URL } from '~/lib/constants'
import { LW7_DATE, SITE_URL } from '~/lib/constants'
const siteUrl = new URL(SITE_URL)
const siteUrlForTicket = `${siteUrl.host}${siteUrl.pathname}`.replace(/\/$/, '')
@@ -16,7 +16,7 @@ export default function TicketInfoFooter({
golden && '!text-white'
}`}
>
<div>{DATE}</div>
<div>{LW7_DATE}</div>
</div>
<div
className={`${cn(styles.date, { [styles['date-golden']]: golden })} text-sm ${
@@ -2,7 +2,7 @@ import { motion } from 'framer-motion'
import Image from 'next/image'
import Link from 'next/link'
import { useBreakpoint } from 'common/hooks/useBreakpoint'
import { UserData } from './hooks/use-conf-data'
import { UserData } from '../hooks/use-conf-data'
interface Props {
loadedUsers: UserData[]
@@ -36,7 +36,7 @@ export default function TicketsGrid({ loadedUsers, isLoading, pageCount, offset
return (
<Link
href={`/launch-week/tickets/${user.username}`}
href={`/launch-week/7/tickets/${user.username}`}
key={`${user.username}-000${i}`}
onClick={() => window.scrollTo(0, 0)}
>
@@ -0,0 +1,49 @@
import React, { useRef } from 'react'
import { useFrame } from '@react-three/fiber'
interface Props {
animate?: boolean
children: any
config: any
user?: any
}
const Particle = ({ animate = true, children, config }: Props) => {
const particle = useRef<any>(null)
const pathOffsetShape =
Math.pow(
Math.random() * config.xRandomnessShape,
config.xRandomness - 1 + 1 - config.xRandomness / 2
) * config.xThickness
const pathOffset = pathOffsetShape * (pathOffsetShape < 1 || Math.random() < 0.5 ? 1 : -1)
const verticalRandomness = Math.random() * (config.yThickness - 1) + 1 - config.yThickness / 2
const speed = Math.random() * (config.min_speed - config.max_speed) + config.max_speed
const circumference = (config.widthRadius * Math.PI * 2) / 100
const delayOffsetFactor = 100
const delayOffset = Math.random() * delayOffsetFactor
useFrame(({ clock }) => {
if (animate) {
const timer = clock.getElapsedTime() * speed + delayOffset
// When the loop count is even, draw bottom 8 shape
// if odd, draw top 8 shape
const isEven = Math.floor(timer / circumference) % 2 == 0
particle.current.position.x = isEven
? Math.sin(timer) * config.widthRadius * config.widthRatio + pathOffset
: Math.sin(timer) * config.widthRadius + pathOffset
particle.current.position.y = isEven
? Math.cos(timer) * config.bottomHeightRadius -
config.bottomHeightRadius +
verticalRandomness
: -Math.cos(timer) * config.topHeightRadius + config.topHeightRadius + verticalRandomness
}
})
return <mesh ref={particle}>{children}</mesh>
}
export default Particle
@@ -0,0 +1,140 @@
import React, { useMemo, useEffect, useState } from 'react'
import { Canvas } from '@react-three/fiber'
import { AdditiveBlending } from 'three'
import Particle from './Particle'
import useParticlesConfig from './hooks/useParticlesConfig'
import { SupabaseClient } from '@supabase/supabase-js'
const ParticlesCanvas = ({ supabase, users }: { supabase?: SupabaseClient; users: any }) => {
const isWindowUndefined = typeof window === 'undefined'
if (isWindowUndefined) return null
const canvasRef = React.useRef(null)
const [animate, setAnimate] = useState<boolean>(true)
const { config, handleSetConfig, particles, setParticles, isDebugMode } =
useParticlesConfig(users)
const [realtimeChannel, setRealtimeChannel] = useState<ReturnType<
(typeof supabase | any)['channel']
> | null>(null)
const loadUsers = async () => {
return await supabase!.from('lw8_tickets_golden').select('id, golden', { count: 'exact' })
}
// Update particles live when new tickets are generated
useEffect(() => {
if (!!supabase) {
const channel = supabase
.channel('lw8_tickets_changes')
.on(
'postgres_changes',
{
event: 'INSERT',
schema: 'public',
table: 'lw8_tickets_golden',
},
async () => {
const { data: users } = await loadUsers()
handleSetConfig('particles', users?.length)
setParticles((prev: any) => users! ?? prev)
}
)
.subscribe()
setRealtimeChannel(channel)
}
return () => {
// Cleanup realtime subscription on unmount
realtimeChannel?.unsubscribe()
}
}, [])
// stop animation if canvas if is not in viewport
// to avoid unnecessary computations
const handleScroll = () => {
if (canvasRef.current && typeof window !== 'undefined') {
const rect = (canvasRef.current as HTMLDivElement)?.getBoundingClientRect()
const isInView = rect.bottom > 0
setAnimate(isInView)
}
}
useEffect(() => {
window.addEventListener('scroll', handleScroll)
return () => {
window.removeEventListener('scroll', handleScroll)
}
}, [])
/* ThreeJs setup */
const Geometry = useMemo(
() => () => <circleGeometry args={[config.particlesSize, config.particlesSides]} />,
[]
)
const GoldGeometry = useMemo(
() => () => <circleGeometry args={[config.goldParticlesSize, config.particlesSides]} />,
[]
)
const Material = () =>
useMemo(
() => (
<meshStandardMaterial
color={config.color}
blending={config.particlesBlending ? AdditiveBlending : undefined}
/>
),
[]
)
const GoldMaterial = () =>
useMemo(
() => (
<meshPhysicalMaterial
color={config.colorGold}
metalness={35}
blending={config.particlesBlending ? AdditiveBlending : undefined}
/>
),
[]
)
return (
<Canvas
ref={canvasRef}
dpr={[1, 2]}
camera={{ fov: 75, position: [0, 0, 500] }}
className="relative z-30"
>
<ambientLight intensity={config.lightIntensity} />
<group position={[0, 70, 0]} scale={[0.9, 0.9, 0.9]}>
{/* Animated 8 shape particles */}
{particles?.map((user: any, index: number) => (
<Particle
key={`particle-${user.username ?? index}`}
user={user}
config={config}
animate={animate}
>
{(!!user.id && user.golden) || (config.showGold && Math.random() <= 0.5) ? (
<>
<GoldGeometry />
<GoldMaterial />
</>
) : (
<>
<Geometry />
<Material />
</>
)}
</Particle>
))}
</group>
</Canvas>
)
}
export default ParticlesCanvas
@@ -0,0 +1,182 @@
import { useEffect, useState } from 'react'
import { range } from 'lodash'
let defaultConfig = {
particles: 20,
particlesSize: 1.2,
goldParticlesSize: 1.5,
particlesSides: 5,
particlesBlending: true,
lightIntensity: 0.3,
widthRadius: 100,
widthRatio: 1.2,
topHeightRadius: 80,
bottomHeightRadius: 100,
trails: 0,
trailWidth: 40,
trailLength: 100,
trailDecay: 450,
color: 'white',
colorGold: '#b89d18',
xThickness: 7,
xRandomnessFactor: 2.2,
xRandomnessShape: 2.2,
xRandomness: 5,
yThickness: 20,
max_speed: 0.1,
min_speed: -0.1,
showGold: true,
}
const useParticlesConfig = (users: any): any => {
const isWindowUndefined = typeof window === 'undefined'
if (isWindowUndefined) return null
const hash = window.location.hash
const isDebugMode = hash.includes('#debug')
const [particles, setParticles] = useState<any[]>(users)
const [config, setConfig] = useState(defaultConfig)
const handleSetConfig = (name: string, value: any) => {
setConfig((prevConfig) => ({ ...prevConfig, [name]: value }))
}
const init = async () => {
if (!isDebugMode) return
const dat = await import('dat.gui')
const gui = new dat.GUI()
const particlesFolder = gui.addFolder('Particles')
const shapeFolder = gui.addFolder('Shape')
const speedFolder = gui.addFolder('Speed')
// const trailFolder = gui.addFolder('Trail')
gui.width = 500
particlesFolder
.add(config, 'particles')
.min(1)
.max(5000)
.step(1)
.name('Count')
.onChange((value) => {
handleSetConfig('particles', value)
setParticles(range(0, value))
})
particlesFolder
.add(config, 'particlesSize')
.min(1)
.max(10)
.step(0.05)
.name('Size')
.onChange((value) => handleSetConfig('particlesSize', value))
particlesFolder
.add(config, 'goldParticlesSize')
.min(1)
.max(10)
.step(0.05)
.name('Gold Particles Size')
.onChange((value) => handleSetConfig('goldParticlesSize', value))
particlesFolder
.add(config, 'particlesSides')
.min(3)
.max(20)
.step(1)
.name('Sides')
.onChange((value) => handleSetConfig('particlesSides', value))
particlesFolder
.add(config, 'lightIntensity')
.min(0)
.max(10)
.step(0.05)
.name('Light intensity')
.onChange((value) => handleSetConfig('lightIntensity', value))
particlesFolder
.add(config, 'showGold')
.name('Show Gold Particles')
.onChange((value) => handleSetConfig('showGold', value))
particlesFolder
.add(config, 'particlesBlending')
.name('Blending')
.onChange((value) => handleSetConfig('particlesBlending', value))
shapeFolder
.add(config, 'widthRadius')
.min(1)
.max(200)
.step(1)
.name('Width Radius')
.onChange((value) => handleSetConfig('widthRadius', value))
shapeFolder
.add(config, 'widthRatio')
.min(0.5)
.max(3)
.step(0.01)
.name('Top/Bottom Ratio')
.onChange((value) => handleSetConfig('widthRatio', value))
shapeFolder
.add(config, 'topHeightRadius')
.min(1)
.max(200)
.step(1)
.name('Height Radius - Top')
.onChange((value) => handleSetConfig('topHeightRadius', value))
shapeFolder
.add(config, 'bottomHeightRadius')
.min(1)
.max(200)
.step(1)
.name('Height Radius - Bottom')
.onChange((value) => handleSetConfig('bottomHeightRadius', value))
shapeFolder
.add(config, 'xThickness')
.min(1)
.max(100)
.step(0.1)
.name('Stroke Width')
.onChange((value) => handleSetConfig('xThickness', value))
shapeFolder
.add(config, 'xRandomnessShape')
.min(0)
.max(5)
.step(0.001)
.name('Randomness shape')
.onChange((value) => handleSetConfig('xRandomnessShape', value))
shapeFolder
.add(config, 'xRandomness')
.min(0)
.max(50)
.step(0.01)
.name('Randomness')
.onChange((value) => handleSetConfig('xRandomness', value))
shapeFolder
.add(config, 'yThickness')
.min(1)
.max(50)
.step(0.1)
.name('y thickness')
.onChange((value) => handleSetConfig('yThickness', value))
speedFolder
.add(config, 'min_speed')
.min(-6)
.max(6)
.step(0.01)
.name('Min speed')
.onChange((value) => handleSetConfig('min_speed', value))
speedFolder
.add(config, 'max_speed')
.min(-6)
.max(6)
.step(0.01)
.name('Max speed')
.onChange((value) => handleSetConfig('max_speed', value))
particlesFolder.open()
shapeFolder.open()
speedFolder.open()
}
useEffect(() => {
init()
}, [])
return { config, handleSetConfig, particles, setParticles, isDebugMode }
}
export default useParticlesConfig
@@ -0,0 +1,20 @@
import React from 'react'
import Countdown from 'react-countdown'
import { CountdownWidget } from 'ui'
import { LW8_LAUNCH_DATE } from '~/lib/constants'
const CountdownComponent = () => {
const renderer = ({ days, hours, minutes, seconds, completed }: any) => {
if (completed) {
// Render a completed state
return null
} else {
// Render countdown
return <CountdownWidget days={days} hours={hours} minutes={minutes} seconds={seconds} />
}
}
return <Countdown date={new Date(LW8_LAUNCH_DATE)} renderer={renderer} />
}
export default CountdownComponent
@@ -0,0 +1,14 @@
import React from 'react'
interface Props {
text: string
className?: string
}
export default function LabelBadge({ text, className }: Props) {
return (
<span className={['bg-[#222428] rounded px-1 py-[1px] text-xs', className].join(' ')}>
<span className="text-scale-1000">{text}</span>
</span>
)
}
@@ -0,0 +1,31 @@
import Link from 'next/link'
import CountdownComponent from './Countdown'
export function LaunchWeekLogoHeader() {
return (
<div className="flex flex-col gap-1 md:gap-2 items-center justify-end">
<div className="opacity-0 !animate-[fadeIn_0.5s_cubic-bezier(0.25,0.25,0,1)_0.5s_both] px-2 flex flex-col items-center text-center gap-3">
<div className="">
<CountdownComponent />
</div>
<h1 className="sr-only font-normal uppercase text-[28px] sm:text-[32px]">Launch week 8</h1>
<p className="text-white radial-gradient-text-scale-600 text-lg sm:text-2xl">
<span className="block">August 7th–11th, 2023</span>
<span>9:00 AM PT</span>
</p>
<div className="text-[#9296AA]">
Join us in a week of announcing new features. <br className="hidden md:block" />
Connect with GitHub to generate your unique ticket and contribute to the constellation.
<span className="inline sm:block">
{' '}
You might also win{' '}
<Link href="#lw8-prizes">
<a className="underline transition-opacity hover:text-scale-1200">swag</a>
</Link>
.
</span>
</div>
</div>
</div>
)
}
@@ -0,0 +1,69 @@
import Image from 'next/image'
import React from 'react'
import { motion } from 'framer-motion'
type animateFromType = 'left' | 'right' | 'up' | 'down' | 'opacityOnly'
export default function LaunchWeekPrizeCard({
imageUrl = '',
imageWrapperClassName,
content,
className,
animateFrom,
imgObjectPosition = 'center',
}: {
imageUrl: string
imageWrapperClassName?: string
content: any
className?: string
animateFrom?: animateFromType
imgObjectPosition?: string
}) {
const finalState = { x: 0, y: 0, opacity: 1 }
function getAnimationFrom(animationDirection: animateFromType) {
switch (animationDirection) {
case 'left':
return { x: -20, opacity: 0 }
case 'up':
return { y: -20, opacity: 0 }
case 'right':
return { x: 20, opacity: 0 }
case 'down':
return { y: 20, opacity: 0 }
case 'opacityOnly':
return { opacity: 0 }
}
}
return (
<motion.div
className={[
'relative p-[1px] bg-gradient-to-b from-[#22282a] to-[#030A0C] rounded-lg overflow-hidden shadow-lg',
className && className,
].join(' ')}
initial={animateFrom ? getAnimationFrom(animateFrom) : finalState}
whileInView={finalState}
viewport={{ once: true, margin: '-50px' }}
transition={{ type: 'spring', bounce: 0, delay: 0.5 }}
>
<div className="relative h-full flex flex-col bg-[#030A0C] rounded-lg overflow-hidden">
<div
className={[
"relative w-full flex-grow before:content[' '] before:absolute before:inset-0 before:z-10 before:bg-gradient-to-t before:from-[#030A0C] before:via-transparent",
imageWrapperClassName && imageWrapperClassName,
].join(' ')}
>
<Image
src={imageUrl}
layout="fill"
objectFit="cover"
quality={100}
objectPosition={imgObjectPosition}
/>
</div>
<div className="p-4 flex flex-col gap-2 items-start">{content && content}</div>
</div>
</motion.div>
)
}
@@ -0,0 +1,99 @@
import React from 'react'
import LabelBadge from './LabelBadge'
import LaunchWeekPrizeCard from './LaunchWeekPrizeCard'
import Image from 'next/image'
export default function LaunchWeekPrizeSection({
className,
ticket,
}: {
className?: string
ticket?: any
}) {
const Ticket = ticket
return (
<div id="lw8-prizes" className={['relative scroll-mt-[66px]', className].join(' ')}>
<div className="absolute z-0 w-full aspect-[1.75/1] top-0 items-center justify-center">
<Image
src="/images/launchweek/8/swag-bg.png"
alt="prizes background"
layout="fill"
objectFit="cover"
objectPosition="top"
quality={100}
/>
</div>
<div className="!max-w-[100vw]">
<div className="w-full container mx-auto h-px bg-gradient-to-r from-transparent via-scale-600 to-transparent" />
<div className="text-center relative z-10 text-white">
<div className="max-w-[38rem] mx-auto flex flex-col items-center gap-4 px-4">
<div className="relative z-10 w-full h-[100px] md:h-[130px] rounded-sm flex mt-16 items-center justify-center">
<Image
src="/images/launchweek/8/swag-illustration.png"
alt="prizes"
layout="fill"
objectFit="contain"
quality={100}
/>
</div>
<h2 className="text-4xl">Get your winning ticket</h2>
<p className="text-[#9296AA]">
Join us on August 11th for Launch Week 8's final day
<br className="hidden md:inline-block" /> and find out if you are one of the lucky
winners. Get sharing!
</p>
</div>
</div>
{ticket && <Ticket />}
<div className="pt-8 lg:pt-16 px-4">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 max-w-7xl mx-auto h-auto text-white">
<LaunchWeekPrizeCard
className="col-span-full"
imageUrl="/images/launchweek/8/swag/deskmat-lg.jpg"
imageWrapperClassName="w-full aspect-[1.9/1] md:h-[300px]"
imgObjectPosition="center"
content={
<h3 className="w-full text-sm md:text-center flex md:justify-center items-center gap-4">
Supabase Desk Mat <LabelBadge text="10 mats" />
</h3>
}
animateFrom="down"
/>
<LaunchWeekPrizeCard
imageUrl="/images/launchweek/8/swag/shirt.jpg"
imageWrapperClassName="w-full aspect-[1.9/1]"
imgObjectPosition="right"
content={
<h3 className="text-sm flex items-center gap-4">
Supabase T-Shirt <LabelBadge text="20 t-shirts" />
</h3>
}
animateFrom="down"
/>
<LaunchWeekPrizeCard
imageUrl="/images/launchweek/8/swag/cap.jpg"
imageWrapperClassName="w-full aspect-[1.9/1]"
animateFrom="down"
content={
<h3 className="text-sm flex items-center gap-4">
Supabase Cap <LabelBadge text="25 caps" />
</h3>
}
/>
<LaunchWeekPrizeCard
imageUrl="/images/launchweek/8/swag/stickers.jpg"
imageWrapperClassName="w-full aspect-[1.9/1]"
animateFrom="down"
content={
<h3 className="text-sm flex items-center gap-4">
Supabase Sticker Pack <LabelBadge text="50 packs" />
</h3>
}
/>
</div>
</div>
</div>
</div>
)
}
@@ -0,0 +1,79 @@
import styles from './ticket-visual.module.css'
import TicketProfile from './TicketProfile'
import TicketNumber from './TicketNumber'
import { UserData } from '~/components/LaunchWeek/hooks/use-conf-data'
import TicketHeader from './TicketHeader'
import Image from 'next/image'
import TicketForm from './TicketForm'
import TicketFooter from './TicketFooter'
type TicketGenerationState = 'default' | 'loading'
type Props = {
user: UserData
ticketGenerationState?: TicketGenerationState
setTicketGenerationState?: any
}
export default function TicketVisual({
user,
ticketGenerationState = 'default',
setTicketGenerationState,
}: Props) {
const { username, golden = false, bg_image_id: bgImageId, ticketNumber } = user
const ticketBg = {
regular: {
background: `/images/launchweek/8/ticket-bg/regular.png`,
},
golden: {
background: `/images/launchweek/8/ticket-bg/golden.png`,
},
}
const CURRENT_TICKET = golden ? 'golden' : 'regular'
const CURRENT_TICKET_BG = ticketBg[CURRENT_TICKET].background
return (
<div className="flex relative flex-col w-[300px] h-auto md:w-full md:max-w-none backdrop-blur-md">
<div
className={[
styles.visual,
golden ? styles['visual--gold'] : '',
!golden && 'overflow-hidden',
'flex relative flex-col justify-between w-full aspect-[1/1.6] md:aspect-[1.935/1] bg-gradient-to-b from-[#ffffff80] to-[#ffffff20] before:rounded-[13px] box-border backdrop-blur-md rounded-xl',
].join(' ')}
>
{username ? (
<div className="absolute inset-0 h-full px-4 pb-6 z-10 flex flex-col items-center justify-between w-full md:h-full flex-1 md:pb-0 md:pl-8 md:pr-[15%] overflow-hidden">
<TicketHeader golden={golden} />
<div className="flex-1 w-full h-full md:h-auto flex py-6 md:py-4 flex-col justify-center">
<TicketProfile
user={user}
ticketGenerationState={ticketGenerationState}
setTicketGenerationState={setTicketGenerationState}
golden={golden}
/>
</div>
<TicketFooter />
<TicketNumber number={ticketNumber} golden={golden} />
<div className="absolute z-500 inset-[1px] overflow-hidden rounded-xl">
<Image
src={CURRENT_TICKET_BG}
alt="ticket background"
layout="fill"
objectFit="cover"
objectPosition="center"
quality={100}
/>
</div>
</div>
) : (
<TicketForm
defaultUsername={username ?? undefined}
setTicketGenerationState={setTicketGenerationState}
/>
)}
</div>
</div>
)
}
@@ -0,0 +1,119 @@
import { useEffect, useRef, useState } from 'react'
import { SITE_URL, TWEET_TEXT, TWEET_TEXT_GOLDEN } from '~/lib/constants'
import { IconCheckCircle } from 'ui'
import useConfData from '~/components/LaunchWeek/hooks/use-conf-data'
import { useParams } from '~/hooks/useParams'
import TicketForm from './TicketForm'
type TicketGenerationState = 'default' | 'loading'
type Props = {
username: string
golden?: boolean
ticketGenerationState?: TicketGenerationState
setTicketGenerationState: (ticketGenerationState: TicketGenerationState) => void
}
export default function TicketActions({
username,
golden = false,
ticketGenerationState,
setTicketGenerationState,
}: Props) {
const [imgReady, setImgReady] = useState(false)
const [loading, setLoading] = useState(false)
const downloadLink = useRef<HTMLAnchorElement>()
const link = `${SITE_URL}/tickets/${username}?lw=8${golden ? `&golden=true` : ''}`
const permalink = encodeURIComponent(link)
const text = golden ? TWEET_TEXT_GOLDEN : TWEET_TEXT
const encodedText = encodeURIComponent(text)
const { userData, supabase } = useConfData()
const tweetUrl = `https://twitter.com/intent/tweet?url=${permalink}&via=supabase&text=${encodedText}`
const linkedInUrl = `https://www.linkedin.com/sharing/share-offsite/?url=${permalink}`
const downloadUrl = `https://obuldanrptloktxcffvn.functions.supabase.co/lw8-ticket?username=${encodeURIComponent(
username
)}`
const params = useParams()
const sharePage = params.username
const LW_TABLE = 'lw8_tickets'
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])
const handleShare = async (social: 'twitter' | 'linkedin') => {
if (!supabase) return
if (social === 'twitter') {
await supabase.from(LW_TABLE).update({ sharedOnTwitter: 'now' }).eq('username', username)
window.open(tweetUrl, '_blank')
} else if (social === 'linkedin') {
await supabase.from(LW_TABLE).update({ sharedOnLinkedIn: 'now' }).eq('username', username)
window.open(linkedInUrl, '_blank')
}
}
return (
<div className="grid gap-1 grid-cols-1 sm:grid-cols-3">
{!sharePage ? (
<>
<div className="rounded bg-[#E6E8EB] text-scale-500 py-1 px-3 border border-[#3e3e3e] text-xs mb-1">
<div className="flex items-center justify-center gap-2">
<div className="text-scale-300">
<IconCheckCircle size={10} strokeWidth={1.5} />
</div>
Connect with GitHub
</div>
</div>
<button
onClick={() => handleShare('twitter')}
className={[
`flex items-center justify-center gap-2 rounded text-scale-500 py-1 px-3 border border-[#3e3e3e] text-xs mb-1 transition-all ease-out hover:text-scale-100 hover:bg-[#dfe1e3]`,
userData.sharedOnTwitter ? 'bg-[#E6E8EB] text-scale-500' : 'text-white',
].join(' ')}
>
{userData.sharedOnTwitter && (
<div className="text-scale-900">
<IconCheckCircle size={10} strokeWidth={1.5} />
</div>
)}
Share on Twitter
</button>
<button
onClick={() => handleShare('linkedin')}
className={[
`flex items-center justify-center gap-2 rounded text-scale-500 py-1 px-3 border border-[#3e3e3e] text-xs mb-1 transition-all ease-out hover:text-scale-100 hover:bg-[#dfe1e3]`,
userData.sharedOnLinkedIn ? 'bg-[#E6E8EB] text-scale-500' : 'text-white',
].join(' ')}
>
{userData.sharedOnLinkedIn && (
<div className="text-scale-900">
<IconCheckCircle size={10} strokeWidth={1.5} />
</div>
)}
Share on Linkedin
</button>
</>
) : (
!username && (
<TicketForm
defaultUsername={username ?? undefined}
ticketGenerationState={ticketGenerationState}
setTicketGenerationState={setTicketGenerationState}
/>
)
)}
</div>
)
}
@@ -0,0 +1,179 @@
import { useState } from 'react'
import { SupabaseClient } from '@supabase/supabase-js'
import Link from 'next/link'
import cn from 'classnames'
import { Button } from 'ui'
import styles from './ticket.module.css'
import styleUtils from '../../utils.module.css'
import Ticket from './Ticket'
import TicketActions from '~/components/LaunchWeek/8/Ticket/TicketActions'
import TicketCopy from '~/components/LaunchWeek/8/Ticket/ticket-copy'
import { UserData } from '~/components/LaunchWeek/hooks/use-conf-data'
import useWinningChances from '~/components/LaunchWeek/hooks/useWinningChances'
import { SITE_URL } from '~/lib/constants'
import { useBreakpoint } from 'common/hooks/useBreakpoint'
import TicketCustomizationForm from './TicketCustomizationForm'
import TicketDisclaimer from './TicketDisclaimer'
type TicketGenerationState = 'default' | 'loading'
type Props = {
user: UserData
supabase: SupabaseClient
referrals: number
sharePage?: boolean
}
export default function TicketContainer({ user, sharePage, referrals, supabase }: Props) {
const { username, name, golden } = user
const isMobile = useBreakpoint(1023)
const [ticketGenerationState, setTicketGenerationState] =
useState<TicketGenerationState>('default')
const winningChances = useWinningChances()
if (!user.username)
return (
<div className="w-full flex items-center justify-center min-h-[400px]">
<div
className={cn(
styles['ticket-visual'],
styleUtils.appear,
styleUtils['appear-first'],
'relative flex flex-col items-center gap-2 w-full max-w-2xl'
)}
>
<Ticket
user={user}
ticketGenerationState={ticketGenerationState}
setTicketGenerationState={setTicketGenerationState}
/>
<TicketDisclaimer className="mt-4" />
</div>
</div>
)
return (
<div
className={[
`relative w-full max-w-sm md:max-w-[700px] lg:max-w-[1100px] min-h-[400px] flex flex-col items-center lg:grid lg:grid-cols-12 gap-4 lg:p-4 rounded-3xl backdrop-blur lg:items-stretch h-auto`,
!isMobile && styles['ticket-hero'],
].join(' ')}
>
<div
className={[
'flex flex-col !w-full h-full justify-center max-w-lg lg:max-w-none col-span-full p-6 lg:col-span-4 rounded-3xl backdrop-blur lg:backdrop-blur-none',
isMobile && styles['ticket-hero'],
].join(' ')}
>
<div className="text-scale-1200 flex flex-col w-full items-center text-white text-center lg:text-left lg:items-start gap-3">
<h1 className={cn('text-2xl tracking-[-0.02rem] leading-7 block text-white')}>
{!sharePage ? (
name ? (
<>
{winningChances === 1 && (
<>
<span className="text-white">You're in! </span>
Now make it unique and share.
</>
)}
{winningChances === 2 && (
<>
<span className="text-white">That's x2!</span>
<br className="inline lg:hidden" /> Share again to get a golden ticket.
</>
)}
{winningChances === 3 && (
<>
You have a <span className={styles['gold-text']}>golden </span>
chance of winning!
</>
)}
</>
) : (
<>
Generate your ticket. <br />
Win the <span className="gradient-text-purple-800">SupaKeyboard</span>.
</>
)
) : (
<>
{name ? name : username}'s <br className="hidden lg:inline" />
unique ticket
</>
)}
</h1>
<div className="text-sm text-scale-1100 leading-5">
{!sharePage ? (
golden ? (
<p>
Join us on August 11th for Launch Week 8's final day and find out if you are one
of the lucky winners.
</p>
) : (
<p>
Customize your ticket and boost your chances of winning{' '}
<Link href="#lw8-prizes">
<a className="underline hover:text-scale-1200">limited edition awards</a>
</Link>{' '}
by sharing it with the community.
</p>
)
) : (
<>
<p>
Generate and share your own custom ticket for a chance to win{' '}
<Link href="#lw8-prizes">
<a className="underline hover:text-scale-1200">awesome swag</a>
</Link>
.
</p>
<Button type="secondary" asChild>
<a
href={`${SITE_URL}/${username ? '?referral=' + username : ''}`}
className="w-full mt-4 lg:mt-8"
>
Join Launch Week 8
</a>
</Button>
</>
)}
</div>
{!sharePage && user.username && (
<TicketCustomizationForm user={user} supabase={supabase} />
)}
</div>
</div>
<div className="w-full flex-1 col-span-8 lg:-mt-12">
<div
className={cn(
styles['ticket-visual'],
styleUtils.appear,
styleUtils['appear-first'],
'relative flex flex-col items-center gap-4 w-full'
)}
>
<Ticket
user={user}
ticketGenerationState={ticketGenerationState}
setTicketGenerationState={setTicketGenerationState}
/>
{username && (
<div className="w-full">
<TicketActions
username={username}
golden={golden}
ticketGenerationState={ticketGenerationState}
setTicketGenerationState={setTicketGenerationState}
/>
<TicketCopy username={username} isGolden={golden} />
</div>
)}
</div>
</div>
</div>
)
}
Loaded 100 of 206 files, more files were not shown because too many files have changed in this diff. Show more