├── supabase ├── seed.sql ├── .gitignore └── config.toml ├── .eslintrc.json ├── public ├── favicon.ico └── vercel.svg ├── jest.config.js ├── docs └── ongoing.md ├── next.config.js ├── .storybook ├── preview.js └── main.js ├── utils ├── supabaseClient.ts └── database.types.ts ├── ui-components ├── AuthForm │ ├── AuthForm.spec.tsx │ ├── LoginWithPassword.tsx │ ├── LoginWithPasswordless.tsx │ ├── useLogin.ts │ └── index.tsx ├── AppList │ └── index.tsx └── Account.tsx ├── pages ├── api │ └── hello.ts ├── _app.tsx ├── goals │ └── index.tsx └── index.tsx ├── styles ├── globals.css └── Home.module.css ├── .gitignore ├── .github └── dependabot.yml ├── stories ├── header.css ├── Header.stories.tsx ├── button.css ├── Page.stories.tsx ├── Button.tsx ├── assets │ ├── direction.svg │ ├── flow.svg │ ├── code-brackets.svg │ ├── comments.svg │ ├── repo.svg │ ├── plugin.svg │ ├── stackalt.svg │ └── colors.svg ├── Button.stories.tsx ├── page.css ├── Header.tsx ├── AuthForm.stories.tsx ├── Page.tsx └── Introduction.stories.mdx ├── Dev.md ├── tsconfig.json ├── babel.config.js ├── README.md └── package.json /supabase/seed.sql: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /supabase/.gitignore: -------------------------------------------------------------------------------- 1 | # Supabase 2 | .branches 3 | .temp 4 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drFabio/garden-gate/main/public/favicon.ico -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | rootDir: "./", 3 | 4 | testEnvironment: "jsdom", 5 | }; 6 | -------------------------------------------------------------------------------- /docs/ongoing.md: -------------------------------------------------------------------------------- 1 | Following next.js + storybook from [tutorial](https://storybook.js.org/blog/get-started-with-storybook-and-next-js/) 2 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | swcMinify: true, 5 | } 6 | 7 | module.exports = nextConfig 8 | -------------------------------------------------------------------------------- /.storybook/preview.js: -------------------------------------------------------------------------------- 1 | export const parameters = { 2 | actions: { argTypesRegex: "^on[A-Z].*" }, 3 | controls: { 4 | matchers: { 5 | color: /(background|color)$/i, 6 | date: /Date$/, 7 | }, 8 | }, 9 | } -------------------------------------------------------------------------------- /utils/supabaseClient.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from '@supabase/supabase-js' 2 | 3 | const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL! 4 | const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! 5 | 6 | export const supabase = createClient(supabaseUrl, supabaseAnonKey) -------------------------------------------------------------------------------- /ui-components/AuthForm/AuthForm.spec.tsx: -------------------------------------------------------------------------------- 1 | import { Standard } from "@stories/AuthForm.stories"; 2 | import { render } from "@testing-library/react"; 3 | import React from "react"; 4 | 5 | describe(``, () => { 6 | it(`Renders`, () => { 7 | render(); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /pages/api/hello.ts: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | import type { NextApiRequest, NextApiResponse } from 'next' 3 | 4 | type Data = { 5 | name: string 6 | } 7 | 8 | export default function handler( 9 | req: NextApiRequest, 10 | res: NextApiResponse 11 | ) { 12 | res.status(200).json({ name: 'John Doe' }) 13 | } 14 | -------------------------------------------------------------------------------- /ui-components/AuthForm/LoginWithPassword.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | 3 | interface LoginWithPasswordProps {} 4 | /** 5 | * 6 | */ 7 | export const LoginWithPassword: FC = () => { 8 | return ( 9 | <> 10 | 11 | 12 | 13 | 14 | 15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /styles/globals.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | padding: 0; 4 | margin: 0; 5 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 6 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 7 | } 8 | 9 | a { 10 | color: inherit; 11 | text-decoration: none; 12 | } 13 | 14 | * { 15 | box-sizing: border-box; 16 | } 17 | 18 | @media (prefers-color-scheme: dark) { 19 | html { 20 | color-scheme: dark; 21 | } 22 | body { 23 | color: white; 24 | background: black; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "npm" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import '../styles/globals.css' 2 | import type { AppProps } from 'next/app' 3 | 4 | import { createBrowserSupabaseClient } from '@supabase/auth-helpers-nextjs' 5 | import { SessionContextProvider } from '@supabase/auth-helpers-react' 6 | import { useState } from 'react' 7 | 8 | function MyApp({ Component, pageProps }: AppProps) { 9 | const [supabaseClient] = useState(() => createBrowserSupabaseClient()) 10 | 11 | return ( 12 | 15 | 16 | 17 | ) 18 | } 19 | 20 | export default MyApp 21 | -------------------------------------------------------------------------------- /stories/header.css: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; 3 | border-bottom: 1px solid rgba(0, 0, 0, 0.1); 4 | padding: 15px 20px; 5 | display: flex; 6 | align-items: center; 7 | justify-content: space-between; 8 | } 9 | 10 | svg { 11 | display: inline-block; 12 | vertical-align: top; 13 | } 14 | 15 | h1 { 16 | font-weight: 900; 17 | font-size: 20px; 18 | line-height: 1; 19 | margin: 6px 0 6px 10px; 20 | display: inline-block; 21 | vertical-align: top; 22 | } 23 | 24 | button + button { 25 | margin-left: 10px; 26 | } 27 | 28 | .welcome { 29 | color: #333; 30 | font-size: 14px; 31 | margin-right: 10px; 32 | } 33 | -------------------------------------------------------------------------------- /ui-components/AuthForm/LoginWithPasswordless.tsx: -------------------------------------------------------------------------------- 1 | import { FC, useState } from "react"; 2 | 3 | interface LoginWithPasswordlessProps { 4 | onLogin?: (email: string) => void 5 | 6 | } 7 | /** 8 | * 9 | */ 10 | export const LoginWithPasswordless: FC = ({ onLogin }) => { 11 | const [email, setEmail] = useState('') 12 | 13 | const handleLogin = () => { 14 | onLogin?.(email) 15 | } 16 | return ( 17 | <> 18 | 19 | setEmail(e.target.value)} 20 | /> 21 | 22 | 23 | 24 | ); 25 | }; 26 | -------------------------------------------------------------------------------- /Dev.md: -------------------------------------------------------------------------------- 1 | ## setting up supabase cli 2 | 3 | ```sh 4 | 5 | yarn add @supabase/supabase-js 6 | yarn supabase init 7 | yarn supabase start 8 | yarn supabase gen types typescript --db-url $SUPA_DB_URL > utils/database.types.ts 9 | ``` 10 | 11 | ## doc improvement 12 | 13 | How to generate the database , fix on doc 14 | 15 | ## Supabase redirect 16 | 17 | In order to make the redirect work on local and on remote we needed to [follow](https://www.notion.so/drfabio/Technology-c3a04e2a06204d2e84684f35cdbc9ce7?p=15ac3fef1dbe47c8bc1df011a02e20ae&showMoveTo=true). Adding redirect url to the authentication on the [dashboard](https://app.supabase.com/project/hvdvnzdakxyzwjjsqxzw/auth/url-configuration) and possibly to the TOML (not entirely sure). 18 | -------------------------------------------------------------------------------- /stories/Header.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ComponentStory, ComponentMeta } from '@storybook/react'; 3 | 4 | import { Header } from './Header'; 5 | 6 | export default { 7 | title: 'Example/Header', 8 | component: Header, 9 | parameters: { 10 | // More on Story layout: https://storybook.js.org/docs/react/configure/story-layout 11 | layout: 'fullscreen', 12 | }, 13 | } as ComponentMeta; 14 | 15 | const Template: ComponentStory = (args) =>
; 16 | 17 | export const LoggedIn = Template.bind({}); 18 | LoggedIn.args = { 19 | user: { 20 | name: 'Jane Doe', 21 | }, 22 | }; 23 | 24 | export const LoggedOut = Template.bind({}); 25 | LoggedOut.args = {}; 26 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "baseUrl": "./", 18 | "paths": { 19 | "@components/*": ["ui-components/*"], 20 | "@stories/*": ["stories/*"] 21 | } 22 | }, 23 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 24 | "exclude": ["node_modules"] 25 | } 26 | -------------------------------------------------------------------------------- /stories/button.css: -------------------------------------------------------------------------------- 1 | .storybook-button { 2 | font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; 3 | font-weight: 700; 4 | border: 0; 5 | border-radius: 3em; 6 | cursor: pointer; 7 | display: inline-block; 8 | line-height: 1; 9 | } 10 | .storybook-button--primary { 11 | color: white; 12 | background-color: #1ea7fd; 13 | } 14 | .storybook-button--secondary { 15 | color: #333; 16 | background-color: transparent; 17 | box-shadow: rgba(0, 0, 0, 0.15) 0px 0px 0px 1px inset; 18 | } 19 | .storybook-button--small { 20 | font-size: 12px; 21 | padding: 10px 16px; 22 | } 23 | .storybook-button--medium { 24 | font-size: 14px; 25 | padding: 11px 20px; 26 | } 27 | .storybook-button--large { 28 | font-size: 16px; 29 | padding: 12px 24px; 30 | } 31 | -------------------------------------------------------------------------------- /ui-components/AuthForm/useLogin.ts: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | import { supabase } from '../../utils/supabaseClient' 3 | 4 | 5 | /** 6 | * Abstraxt login flow 7 | */ 8 | export const useLogin = () => { 9 | const [loading, setLoading] = useState(false) 10 | const handleLogin = async (email: string) => { 11 | console.log(`Trying to login with ${email}`) 12 | try { 13 | setLoading(true) 14 | const { error } = await supabase.auth.signInWithOtp({ email }) 15 | if (error) throw error 16 | alert('Check your email for the login link!') 17 | } catch (error: any) { 18 | alert(error.error_description || error.message) 19 | } finally { 20 | setLoading(false) 21 | } 22 | } 23 | return { loading, handleLogin, } 24 | } -------------------------------------------------------------------------------- /pages/goals/index.tsx: -------------------------------------------------------------------------------- 1 | import Head from "next/head"; 2 | import { createServerSupabaseClient } from "@supabase/auth-helpers-nextjs"; 3 | import { Session } from "@supabase/auth-helpers-nextjs"; 4 | import { FC } from "react"; 5 | 6 | export interface GoalsProps { 7 | session?: Session; 8 | } 9 | export const Goals: FC = ({ session }) => { 10 | if (!session) { 11 | return

Logged out! I Should redirect

; 12 | } 13 | return ( 14 | <> 15 | 16 | Goals 17 | 18 |

Goals

19 | 20 | ); 21 | }; 22 | 23 | export async function getServerSideProps(context: any) { 24 | const supabase = createServerSupabaseClient(context); 25 | 26 | const { data } = await supabase.auth.getSession(); 27 | return { props: { session: data?.session } }; 28 | } 29 | 30 | export default Goals; 31 | -------------------------------------------------------------------------------- /ui-components/AppList/index.tsx: -------------------------------------------------------------------------------- 1 | import { useSupabaseClient } from "@supabase/auth-helpers-react"; 2 | import { useEffect, useState } from "react"; 3 | import { Database } from "../../utils/database.types"; 4 | 5 | type App = Database["public"]["Tables"]["apps"]["Row"]; 6 | 7 | export const AppList = () => { 8 | const supabase = useSupabaseClient(); 9 | const [apps, setApps] = useState([]); 10 | useEffect(() => { 11 | const getData = async () => { 12 | const { data } = await supabase.from("apps").select(); 13 | if (data) { 14 | setApps(data); 15 | } 16 | }; 17 | getData(); 18 | }, [setApps]); 19 | 20 | return ( 21 |
    22 | <> 23 |
  • Profile
  • 24 | {apps.map(({ name, id }) => ( 25 |
  • {name}
  • 26 | ))} 27 | 28 |
29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /.storybook/main.js: -------------------------------------------------------------------------------- 1 | const TsconfigPathsPlugin = require("tsconfig-paths-webpack-plugin"); 2 | 3 | /** 4 | * this is trhowning an error 5 | * 6 | * @see {@link https://github.com/storybookjs/storybook/issues/18801} 7 | */ 8 | module.exports = { 9 | stories: [ 10 | "../stories/**/*.stories.mdx", 11 | "../stories/**/*.stories.@(js|jsx|ts|tsx)", 12 | ], 13 | addons: [ 14 | "@storybook/addon-links", 15 | "@storybook/addon-essentials", 16 | "@storybook/addon-interactions", 17 | "@storybook/addon-a11y", 18 | ], 19 | framework: "@storybook/react", 20 | core: { 21 | builder: "@storybook/builder-webpack5", 22 | }, 23 | webpackFinal: async (config) => { 24 | config.resolve.plugins = [ 25 | ...(config.resolve.plugins || []), 26 | new TsconfigPathsPlugin({ 27 | extensions: config.resolve.extensions, 28 | }), 29 | ]; 30 | return config; 31 | }, 32 | }; 33 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Babel is only being used by Jest 3 | * 4 | * @todo Figure out how to make it simpler, if we could only use the Next.JS , storybook tooling that would be better 5 | */ 6 | 7 | const { 8 | compilerOptions: { paths }, 9 | } = require("./tsconfig.json"); 10 | 11 | function turnTsConfigPathIntoAlias() { 12 | return Object.entries(paths).reduce( 13 | (acc, [key, [value]]) => ({ 14 | ...acc, 15 | [key.replace("/*", "")]: `./${value.replace("/*", "")}`, 16 | }), 17 | {} 18 | ); 19 | } 20 | const alias = turnTsConfigPathIntoAlias(); 21 | 22 | module.exports = { 23 | presets: [ 24 | ["@babel/preset-env", { targets: { node: "current" } }], 25 | "@babel/preset-typescript", 26 | ["@babel/preset-react", { runtime: "automatic" }], 27 | ], 28 | plugins: [ 29 | [ 30 | "module-resolver", 31 | { 32 | alias, 33 | }, 34 | ], 35 | ], 36 | }; 37 | -------------------------------------------------------------------------------- /pages/index.tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage } from "next"; 2 | import { useSession, useSupabaseClient } from "@supabase/auth-helpers-react"; 3 | import { Auth, ThemeSupa } from "@supabase/auth-ui-react"; 4 | import Account from "@components/Account"; 5 | import { AppList } from "@components/AppList"; 6 | 7 | const Home: NextPage = () => { 8 | const session = useSession(); 9 | const supabase = useSupabaseClient(); 10 | 11 | return ( 12 |
13 | {!session ? ( 14 | 22 | ) : ( 23 | <> 24 | 25 | 26 | 27 | )} 28 |
29 | ); 30 | }; 31 | 32 | export default Home; 33 | -------------------------------------------------------------------------------- /stories/Page.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ComponentStory, ComponentMeta } from '@storybook/react'; 3 | import { within, userEvent } from '@storybook/testing-library'; 4 | import { Page } from './Page'; 5 | 6 | export default { 7 | title: 'Example/Page', 8 | component: Page, 9 | parameters: { 10 | // More on Story layout: https://storybook.js.org/docs/react/configure/story-layout 11 | layout: 'fullscreen', 12 | }, 13 | } as ComponentMeta; 14 | 15 | const Template: ComponentStory = (args) => ; 16 | 17 | export const LoggedOut = Template.bind({}); 18 | 19 | export const LoggedIn = Template.bind({}); 20 | 21 | // More on interaction testing: https://storybook.js.org/docs/react/writing-tests/interaction-testing 22 | LoggedIn.play = async ({ canvasElement }) => { 23 | const canvas = within(canvasElement); 24 | const loginButton = await canvas.getByRole('button', { name: /Log in/i }); 25 | await userEvent.click(loginButton); 26 | }; 27 | -------------------------------------------------------------------------------- /ui-components/AuthForm/index.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@stories/Button"; 2 | import { FC, ReactElement, ReactNode } from "react"; 3 | import { LoginWithPassword } from "./LoginWithPassword"; 4 | import { LoginWithPasswordless } from "./LoginWithPasswordless"; 5 | 6 | export enum AUTH_STRATEGIES { 7 | PASSWORD = "PASSWORD", 8 | PASSWORDLESS = "PASSWORDLESS", 9 | } 10 | interface AuthFormProps { 11 | strategy?: AUTH_STRATEGIES; 12 | onLogin?: (email: string) => void 13 | } 14 | /** 15 | * 16 | */ 17 | export const AuthForm: FC = ({ 18 | strategy = AUTH_STRATEGIES.PASSWORD, 19 | onLogin 20 | }) => { 21 | let loginComponent: ReactElement 22 | switch (strategy) { 23 | case AUTH_STRATEGIES.PASSWORDLESS: 24 | loginComponent = ; 25 | case AUTH_STRATEGIES.PASSWORD: 26 | default: 27 | loginComponent = ; 28 | } 29 | 30 | return ( 31 | <> 32 | {loginComponent} 33 |
34 | 35 | 36 | ) 37 | }; 38 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /stories/Button.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './button.css'; 3 | 4 | interface ButtonProps { 5 | /** 6 | * Is this the principal call to action on the page? 7 | */ 8 | primary?: boolean; 9 | /** 10 | * What background color to use 11 | */ 12 | backgroundColor?: string; 13 | /** 14 | * How large should the button be? 15 | */ 16 | size?: 'small' | 'medium' | 'large'; 17 | /** 18 | * Button contents 19 | */ 20 | label: string; 21 | /** 22 | * Optional click handler 23 | */ 24 | onClick?: () => void; 25 | } 26 | 27 | /** 28 | * Primary UI component for user interaction 29 | */ 30 | export const Button = ({ 31 | primary = false, 32 | size = 'medium', 33 | backgroundColor, 34 | label, 35 | ...props 36 | }: ButtonProps) => { 37 | const mode = primary ? 'storybook-button--primary' : 'storybook-button--secondary'; 38 | return ( 39 | 47 | ); 48 | }; 49 | -------------------------------------------------------------------------------- /stories/assets/direction.svg: -------------------------------------------------------------------------------- 1 | illustration/direction -------------------------------------------------------------------------------- /stories/Button.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ComponentStory, ComponentMeta } from '@storybook/react'; 3 | 4 | import { Button } from './Button'; 5 | 6 | // More on default export: https://storybook.js.org/docs/react/writing-stories/introduction#default-export 7 | export default { 8 | title: 'Example/Button', 9 | component: Button, 10 | // More on argTypes: https://storybook.js.org/docs/react/api/argtypes 11 | argTypes: { 12 | backgroundColor: { control: 'color' }, 13 | }, 14 | } as ComponentMeta; 15 | 16 | // More on component templates: https://storybook.js.org/docs/react/writing-stories/introduction#using-args 17 | const Template: ComponentStory = (args) =>
56 | ); 57 | -------------------------------------------------------------------------------- /stories/AuthForm.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { ComponentStory, ComponentMeta } from "@storybook/react"; 3 | import { screen, userEvent } from "@storybook/testing-library"; 4 | 5 | import { AuthForm, AUTH_STRATEGIES } from "@components/AuthForm"; 6 | 7 | // More on default export: https://storybook.js.org/docs/react/writing-stories/introduction#default-export 8 | export default { 9 | title: "Components/AuthForm", 10 | component: AuthForm, 11 | // More on argTypes: https://storybook.js.org/docs/react/api/argtypes 12 | } as ComponentMeta; 13 | 14 | // More on component templates: https://storybook.js.org/docs/react/writing-stories/introduction#using-args 15 | const Template: ComponentStory = (args) => ( 16 | 17 | ); 18 | /** 19 | * The basic component 20 | */ 21 | export const Standard = Template.bind({}); 22 | Standard.args = { 23 | strategy: AUTH_STRATEGIES.PASSWORD, 24 | }; 25 | 26 | export const LoggingIn = Template.bind({}); 27 | LoggingIn.args = { 28 | strategy: AUTH_STRATEGIES.PASSWORD, 29 | }; 30 | 31 | LoggingIn.play = async ({ args, canvasElement }) => { 32 | const fillEmail = async () => { 33 | const emailInput = screen.getByLabelText("E-mail", { 34 | selector: "input", 35 | }); 36 | 37 | await userEvent.type(emailInput, "example-email@email.com", { 38 | delay: 100, 39 | }); 40 | }; 41 | switch (args.strategy) { 42 | case AUTH_STRATEGIES.PASSWORD: 43 | { 44 | await fillEmail(); 45 | 46 | const passwordInput = screen.getByLabelText("Password", { 47 | selector: "input", 48 | }); 49 | 50 | await userEvent.type(passwordInput, "secret", { 51 | delay: 100, 52 | }); 53 | } 54 | break; 55 | case AUTH_STRATEGIES.PASSWORDLESS: 56 | { 57 | await fillEmail(); 58 | } 59 | break; 60 | } 61 | }; 62 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "garden-gate", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "storybook": "start-storybook -p 6006", 11 | "build-storybook": "build-storybook", 12 | "test-storybook": "test-storybook", 13 | "test": "jest" 14 | }, 15 | "dependencies": { 16 | "@supabase/auth-helpers-nextjs": "^0.4.1", 17 | "@supabase/auth-helpers-react": "^0.3.0", 18 | "@supabase/auth-ui-react": "^0.2.2", 19 | "@supabase/supabase-js": "^2.0.2", 20 | "next": "12.2.5", 21 | "react": "18.2.0", 22 | "react-dom": "18.2.0", 23 | "supabase": "^1.10.2" 24 | }, 25 | "devDependencies": { 26 | "@babel/core": "^7.19.0", 27 | "@babel/preset-env": "^7.19.0", 28 | "@babel/preset-react": "^7.18.6", 29 | "@babel/preset-typescript": "^7.18.6", 30 | "@storybook/addon-actions": "^6.5.12", 31 | "@storybook/addon-essentials": "^6.5.10", 32 | "@storybook/addon-interactions": "^6.5.10", 33 | "@storybook/addon-links": "^6.5.10", 34 | "@storybook/builder-webpack5": "^6.5.10", 35 | "@storybook/jest": "^0.0.10", 36 | "@storybook/manager-webpack5": "^6.5.12", 37 | "@storybook/react": "^6.5.10", 38 | "@storybook/test-runner": "^0.7.2", 39 | "@storybook/testing-library": "^0.0.13", 40 | "@testing-library/jest-dom": "^5.16.5", 41 | "@testing-library/react": "^13.4.0", 42 | "@types/node": "18.11.0", 43 | "@types/react": "18.0.21", 44 | "@types/react-dom": "18.0.6", 45 | "babel-jest": "^29.1.2", 46 | "babel-loader": "^8.2.5", 47 | "babel-plugin-module-resolver": "^4.1.0", 48 | "eslint": "8.23.0", 49 | "eslint-config-next": "12.3.1", 50 | "eslint-plugin-storybook": "^0.6.4", 51 | "jest": "^29.0.2", 52 | "jest-environment-jsdom": "^29.2.0", 53 | "react-test-renderer": "^18.2.0", 54 | "tsconfig-paths-webpack-plugin": "^4.0.0", 55 | "typescript": "4.8.4" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /stories/assets/plugin.svg: -------------------------------------------------------------------------------- 1 | illustration/plugin -------------------------------------------------------------------------------- /stories/assets/stackalt.svg: -------------------------------------------------------------------------------- 1 | illustration/stackalt -------------------------------------------------------------------------------- /styles/Home.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | padding: 0 2rem; 3 | } 4 | 5 | .main { 6 | min-height: 100vh; 7 | padding: 4rem 0; 8 | flex: 1; 9 | display: flex; 10 | flex-direction: column; 11 | justify-content: center; 12 | align-items: center; 13 | } 14 | 15 | .footer { 16 | display: flex; 17 | flex: 1; 18 | padding: 2rem 0; 19 | border-top: 1px solid #eaeaea; 20 | justify-content: center; 21 | align-items: center; 22 | } 23 | 24 | .footer a { 25 | display: flex; 26 | justify-content: center; 27 | align-items: center; 28 | flex-grow: 1; 29 | } 30 | 31 | .title a { 32 | color: #0070f3; 33 | text-decoration: none; 34 | } 35 | 36 | .title a:hover, 37 | .title a:focus, 38 | .title a:active { 39 | text-decoration: underline; 40 | } 41 | 42 | .title { 43 | margin: 0; 44 | line-height: 1.15; 45 | font-size: 4rem; 46 | } 47 | 48 | .title, 49 | .description { 50 | text-align: center; 51 | } 52 | 53 | .description { 54 | margin: 4rem 0; 55 | line-height: 1.5; 56 | font-size: 1.5rem; 57 | } 58 | 59 | .code { 60 | background: #fafafa; 61 | border-radius: 5px; 62 | padding: 0.75rem; 63 | font-size: 1.1rem; 64 | font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, 65 | Bitstream Vera Sans Mono, Courier New, monospace; 66 | } 67 | 68 | .grid { 69 | display: flex; 70 | align-items: center; 71 | justify-content: center; 72 | flex-wrap: wrap; 73 | max-width: 800px; 74 | } 75 | 76 | .card { 77 | margin: 1rem; 78 | padding: 1.5rem; 79 | text-align: left; 80 | color: inherit; 81 | text-decoration: none; 82 | border: 1px solid #eaeaea; 83 | border-radius: 10px; 84 | transition: color 0.15s ease, border-color 0.15s ease; 85 | max-width: 300px; 86 | } 87 | 88 | .card:hover, 89 | .card:focus, 90 | .card:active { 91 | color: #0070f3; 92 | border-color: #0070f3; 93 | } 94 | 95 | .card h2 { 96 | margin: 0 0 1rem 0; 97 | font-size: 1.5rem; 98 | } 99 | 100 | .card p { 101 | margin: 0; 102 | font-size: 1.25rem; 103 | line-height: 1.5; 104 | } 105 | 106 | .logo { 107 | height: 1em; 108 | margin-left: 0.5rem; 109 | } 110 | 111 | @media (max-width: 600px) { 112 | .grid { 113 | width: 100%; 114 | flex-direction: column; 115 | } 116 | } 117 | 118 | @media (prefers-color-scheme: dark) { 119 | .card, 120 | .footer { 121 | border-color: #222; 122 | } 123 | .code { 124 | background: #111; 125 | } 126 | .logo img { 127 | filter: invert(1); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /stories/Page.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Header } from './Header'; 4 | import './page.css'; 5 | 6 | type User = { 7 | name: string; 8 | }; 9 | 10 | export const Page: React.VFC = () => { 11 | const [user, setUser] = React.useState(); 12 | 13 | return ( 14 |
15 |
setUser({ name: 'Jane Doe' })} 18 | onLogout={() => setUser(undefined)} 19 | onCreateAccount={() => setUser({ name: 'Jane Doe' })} 20 | /> 21 | 22 |
23 |

Pages in Storybook

24 |

25 | We recommend building UIs with a{' '} 26 | 27 | component-driven 28 | {' '} 29 | process starting with atomic components and ending with pages. 30 |

31 |

32 | Render pages with mock data. This makes it easy to build and review page states without 33 | needing to navigate to them in your app. Here are some handy patterns for managing page 34 | data in Storybook: 35 |

36 |
    37 |
  • 38 | Use a higher-level connected component. Storybook helps you compose such data from the 39 | "args" of child component stories 40 |
  • 41 |
  • 42 | Assemble data in the page component from your services. You can mock these services out 43 | using Storybook. 44 |
  • 45 |
46 |

47 | Get a guided tutorial on component-driven development at{' '} 48 | 49 | Storybook tutorials 50 | 51 | . Read more in the{' '} 52 | 53 | docs 54 | 55 | . 56 |

57 |
58 | Tip Adjust the width of the canvas with the{' '} 59 | 60 | 61 | 66 | 67 | 68 | Viewports addon in the toolbar 69 |
70 |
71 |
72 | ); 73 | }; 74 | -------------------------------------------------------------------------------- /supabase/config.toml: -------------------------------------------------------------------------------- 1 | # A string used to distinguish different Supabase projects on the same host. Defaults to the working 2 | # directory name when running `supabase init`. 3 | project_id = "garden-gate" 4 | 5 | [api] 6 | # Port to use for the API URL. 7 | port = 54321 8 | # Schemas to expose in your API. Tables, views and stored procedures in this schema will get API 9 | # endpoints. public and storage are always included. 10 | schemas = [] 11 | # Extra schemas to add to the search_path of every request. 12 | extra_search_path = ["extensions"] 13 | # The maximum number of rows returns from a view, table, or stored procedure. Limits payload size 14 | # for accidental or malicious requests. 15 | max_rows = 1000 16 | 17 | [db] 18 | # Port to use for the local database URL. 19 | port = 54322 20 | # The database major version to use. This has to be the same as your remote database's. Run `SHOW 21 | # server_version;` on the remote database to check. 22 | major_version = 14 23 | 24 | [studio] 25 | # Port to use for Supabase Studio. 26 | port = 54323 27 | 28 | # Email testing server. Emails sent with the local dev setup are not actually sent - rather, they 29 | # are monitored, and you can view the emails that would have been sent from the web interface. 30 | [inbucket] 31 | # Port to use for the email testing server web interface. 32 | port = 54324 33 | smtp_port = 54325 34 | pop3_port = 54326 35 | 36 | [storage] 37 | # The maximum file size allowed (e.g. "5MB", "500KB"). 38 | file_size_limit = "50MiB" 39 | 40 | [auth] 41 | # The base URL of your website. Used as an allow-list for redirects and for constructing URLs used 42 | # in emails. 43 | site_url = "http://localhost:3000" 44 | # A list of *exact* URLs that auth providers are permitted to redirect to post authentication. 45 | additional_redirect_urls = ["https://localhost:3000","https://garden-gate.vercel.app/"] 46 | # How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 seconds (one 47 | # week). 48 | jwt_expiry = 3600 49 | # Allow/disallow new user signups to your project. 50 | enable_signup = true 51 | 52 | [auth.email] 53 | # Allow/disallow new user signups via email to your project. 54 | enable_signup = true 55 | # If enabled, a user will be required to confirm any email change on both the old, and new email 56 | # addresses. If disabled, only the new email is required to confirm. 57 | double_confirm_changes = true 58 | # If enabled, users need to confirm their email address before signing in. 59 | enable_confirmations = false 60 | 61 | # Use an external OAuth provider. The full list of providers are: `apple`, `azure`, `bitbucket`, 62 | # `discord`, `facebook`, `github`, `gitlab`, `google`, `twitch`, `twitter`, `slack`, `spotify`. 63 | [auth.external.apple] 64 | enabled = false 65 | client_id = "" 66 | secret = "" 67 | # Overrides the default auth provider URL. Used to support self-hosted gitlab, single-tenant Azure, 68 | # or any other third-party OIDC providers. 69 | url = "" 70 | -------------------------------------------------------------------------------- /ui-components/Account.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react' 2 | import { 3 | useUser, 4 | useSupabaseClient, 5 | Session, 6 | } from '@supabase/auth-helpers-react' 7 | import { Database } from '../utils/database.types' 8 | type Profiles = Database['public']['Tables']['profiles']['Row'] 9 | 10 | export default function Account({ session }: { session: Session }) { 11 | const supabase = useSupabaseClient() 12 | const user = useUser() 13 | const [loading, setLoading] = useState(true) 14 | const [username, setUsername] = useState(null) 15 | const [website, setWebsite] = useState(null) 16 | const [avatar_url, setAvatarUrl] = useState(null) 17 | 18 | useEffect(() => { 19 | getProfile() 20 | }, [session]) 21 | 22 | async function getProfile() { 23 | try { 24 | setLoading(true) 25 | if (!user) throw new Error('No user') 26 | 27 | let { data, error, status } = await supabase 28 | .from('profiles') 29 | .select(`username, website, avatar_url`) 30 | .eq('id', user.id) 31 | .single() 32 | 33 | if (error && status !== 406) { 34 | throw error 35 | } 36 | 37 | if (data) { 38 | setUsername(data.username) 39 | setWebsite(data.website) 40 | setAvatarUrl(data.avatar_url) 41 | } 42 | } catch (error) { 43 | alert('Error loading user data!') 44 | console.log(error) 45 | } finally { 46 | setLoading(false) 47 | } 48 | } 49 | 50 | async function updateProfile({ 51 | username, 52 | website, 53 | avatar_url, 54 | }: { 55 | username: Profiles['username'] 56 | website: Profiles['website'] 57 | avatar_url: Profiles['avatar_url'] 58 | }) { 59 | try { 60 | setLoading(true) 61 | if (!user) throw new Error('No user') 62 | 63 | const updates = { 64 | id: user.id, 65 | username, 66 | website, 67 | avatar_url, 68 | updated_at: new Date().toISOString(), 69 | } 70 | 71 | let { error } = await supabase.from('profiles').upsert(updates) 72 | if (error) throw error 73 | alert('Profile updated!') 74 | } catch (error) { 75 | alert('Error updating the data!') 76 | console.log(error) 77 | } finally { 78 | setLoading(false) 79 | } 80 | } 81 | 82 | return ( 83 |
84 |
85 | 86 | 87 |
88 |
89 | 90 | setUsername(e.target.value)} 95 | /> 96 |
97 |
98 | 99 | setWebsite(e.target.value)} 104 | /> 105 |
106 | 107 |
108 | 115 |
116 | 117 |
118 | 124 |
125 |
126 | ) 127 | } -------------------------------------------------------------------------------- /stories/Introduction.stories.mdx: -------------------------------------------------------------------------------- 1 | import { Meta } from '@storybook/addon-docs'; 2 | import Code from './assets/code-brackets.svg'; 3 | import Colors from './assets/colors.svg'; 4 | import Comments from './assets/comments.svg'; 5 | import Direction from './assets/direction.svg'; 6 | import Flow from './assets/flow.svg'; 7 | import Plugin from './assets/plugin.svg'; 8 | import Repo from './assets/repo.svg'; 9 | import StackAlt from './assets/stackalt.svg'; 10 | 11 | 12 | 13 | 116 | 117 | # Welcome to Storybook 118 | 119 | Storybook helps you build UI components in isolation from your app's business logic, data, and context. 120 | That makes it easy to develop hard-to-reach states. Save these UI states as **stories** to revisit during development, testing, or QA. 121 | 122 | Browse example stories now by navigating to them in the sidebar. 123 | View their code in the `stories` directory to learn how they work. 124 | We recommend building UIs with a [**component-driven**](https://componentdriven.org) process starting with atomic components and ending with pages. 125 | 126 |
Configure
127 | 128 | 174 | 175 |
Learn
176 | 177 | 207 | 208 |
209 | TipEdit the Markdown in{' '} 210 | stories/Introduction.stories.mdx 211 |
212 | -------------------------------------------------------------------------------- /stories/assets/colors.svg: -------------------------------------------------------------------------------- 1 | illustration/colors --------------------------------------------------------------------------------