diff --git a/supabase/migrations/20240830100103221_user_profiles.ts b/supabase/migrations/20240830100103221_user_profiles.ts new file mode 100644 index 0000000..91de2c0 --- /dev/null +++ b/supabase/migrations/20240830100103221_user_profiles.ts @@ -0,0 +1,29 @@ +import type { ColumnDefinitions, MigrationBuilder } from 'node-pg-migrate' + +export const shorthands: ColumnDefinitions | undefined = undefined + +export async function up(pgm: MigrationBuilder): Promise { + pgm.createTable('profile', { + user_id: { + type: 'uuid', + notNull: true, + references: '"auth"."users"', + onUpdate: 'CASCADE', + onDelete: 'CASCADE', + }, + firstname: { + type: 'text', + notNull: true, + }, + lastname: { + type: 'text', + notNull: true, + }, + }) + pgm.createIndex('profile', 'user_id') +} + +export async function down(pgm: MigrationBuilder): Promise { + pgm.dropIndex('profile', 'user_id') + pgm.dropTable('profile') +} diff --git a/web/.gitignore b/web/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/web/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/web/README.md b/web/README.md new file mode 100644 index 0000000..74872fd --- /dev/null +++ b/web/README.md @@ -0,0 +1,50 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: + +- Configure the top-level `parserOptions` property like this: + +```js +export default tseslint.config({ + languageOptions: { + // other options... + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + }, +}) +``` + +- Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked` +- Optionally add `...tseslint.configs.stylisticTypeChecked` +- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config: + +```js +// eslint.config.js +import react from 'eslint-plugin-react' + +export default tseslint.config({ + // Set the react version + settings: { react: { version: '18.3' } }, + plugins: { + // Add the react plugin + react, + }, + rules: { + // other rules... + // Enable its recommended rules + ...react.configs.recommended.rules, + ...react.configs['jsx-runtime'].rules, + }, +}) +``` diff --git a/web/bun.lockb b/web/bun.lockb new file mode 100755 index 0000000..7439206 Binary files /dev/null and b/web/bun.lockb differ diff --git a/web/components.json b/web/components.json new file mode 100644 index 0000000..80ca630 --- /dev/null +++ b/web/components.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "tailwind.config.js", + "css": "src/index.css", + "baseColor": "stone", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils" + } +} \ No newline at end of file diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..d7a6c5a --- /dev/null +++ b/web/index.html @@ -0,0 +1,19 @@ + + + + + + + + + + + @actcoding/supa-cli + + + +
+ + + + diff --git a/web/package.json b/web/package.json new file mode 100644 index 0000000..d34f158 --- /dev/null +++ b/web/package.json @@ -0,0 +1,48 @@ +{ + "name": "web", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "@hookform/resolvers": "^3.9.0", + "@radix-ui/react-dropdown-menu": "^2.1.1", + "@radix-ui/react-icons": "^1.3.0", + "@radix-ui/react-label": "^2.1.0", + "@radix-ui/react-slot": "^1.1.0", + "@radix-ui/react-tabs": "^1.1.0", + "@radix-ui/react-toast": "^1.2.1", + "@supabase/supabase-js": "^2.45.2", + "class-variance-authority": "^0.7.0", + "clsx": "^2.1.1", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-hook-form": "^7.53.0", + "tailwind-merge": "^2.5.2", + "tailwindcss-animate": "^1.0.7", + "usehooks-ts": "^3.1.0", + "zod": "^3.23.8" + }, + "devDependencies": { + "@eslint/js": "^9.9.0", + "@types/node": "^22.5.1", + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react": "^4.3.1", + "autoprefixer": "^10.4.20", + "eslint": "^9.9.0", + "eslint-plugin-react-hooks": "^5.1.0-rc.0", + "eslint-plugin-react-refresh": "^0.4.9", + "globals": "^15.9.0", + "postcss": "^8.4.41", + "tailwindcss": "^3.4.10", + "typescript": "^5.5.3", + "typescript-eslint": "^8.0.1", + "vite": "^5.4.1" + } +} diff --git a/web/postcss.config.js b/web/postcss.config.js new file mode 100644 index 0000000..2e7af2b --- /dev/null +++ b/web/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/web/public/apple-touch-icon.png b/web/public/apple-touch-icon.png new file mode 100644 index 0000000..8baca2f Binary files /dev/null and b/web/public/apple-touch-icon.png differ diff --git a/web/public/favicon.ico b/web/public/favicon.ico new file mode 100644 index 0000000..3497fde Binary files /dev/null and b/web/public/favicon.ico differ diff --git a/web/public/icon-192-maskable.png b/web/public/icon-192-maskable.png new file mode 100644 index 0000000..bbe65fd Binary files /dev/null and b/web/public/icon-192-maskable.png differ diff --git a/web/public/icon-192.png b/web/public/icon-192.png new file mode 100644 index 0000000..0d64cee Binary files /dev/null and b/web/public/icon-192.png differ diff --git a/web/public/icon-512-maskable.png b/web/public/icon-512-maskable.png new file mode 100644 index 0000000..d0d4c15 Binary files /dev/null and b/web/public/icon-512-maskable.png differ diff --git a/web/public/icon-512.png b/web/public/icon-512.png new file mode 100644 index 0000000..b58840f Binary files /dev/null and b/web/public/icon-512.png differ diff --git a/web/src/App.tsx b/web/src/App.tsx new file mode 100644 index 0000000..cf5fdc0 --- /dev/null +++ b/web/src/App.tsx @@ -0,0 +1,23 @@ +import SupabaseProvider, { useSupabase } from '@/components/supabase/Provider.tsx' +import { Toaster } from '@/components/ui/toaster.tsx' +import PageAuth from '@/pages/auth/PageAuth.tsx' +import PageHome from '@/pages/home/PageHome.tsx' + +export function Switch() { + const { session } = useSupabase() + + if (session) { + return + } + + return +} + +export default function App() { + return (<> + + + + + ) +} diff --git a/web/src/assets/react.svg b/web/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/web/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/src/components/Container.tsx b/web/src/components/Container.tsx new file mode 100644 index 0000000..09d9aac --- /dev/null +++ b/web/src/components/Container.tsx @@ -0,0 +1,31 @@ +import { cn } from '@/lib/utils.ts' +import { cva, VariantProps } from 'class-variance-authority' +import { HTMLAttributes, PropsWithChildren } from 'react' + +const containerVariants = cva( + 'container flex flex-col gap-y-6', + { + variants: { + variant: { + default: 'items-start', + centered: 'items-center', + }, + }, + defaultVariants: { + variant: 'default', + }, + }, +) + +type Props = PropsWithChildren> & VariantProps + +export default function Container({ className, variant, children, ...props}: Props) { + return ( +
+ {children} +
+ ) +} diff --git a/web/src/components/DarkModeToggle.tsx b/web/src/components/DarkModeToggle.tsx new file mode 100644 index 0000000..5b28dfc --- /dev/null +++ b/web/src/components/DarkModeToggle.tsx @@ -0,0 +1,48 @@ +import { useTernaryDarkMode } from 'usehooks-ts' +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu.tsx' +import { useEffect } from 'react' +import { MoonIcon, SunIcon } from '@radix-ui/react-icons' +import { Button } from '@/components/ui/button.tsx' + +export default function DarkModeToggle() { + const { isDarkMode, setTernaryDarkMode } = useTernaryDarkMode() + + useEffect(() => { + const html = document.querySelector('html') as HTMLHtmlElement + if (isDarkMode) { + html.classList.add('dark') + } else { + html.classList.remove('dark') + } + }, [isDarkMode]) + + return ( + + + + + + setTernaryDarkMode('light')}> + Hell + + setTernaryDarkMode('dark')}> + Dunkel + + setTernaryDarkMode('system')}> + System + + + + ) +} diff --git a/web/src/components/supabase/Provider.tsx b/web/src/components/supabase/Provider.tsx new file mode 100644 index 0000000..6a02230 --- /dev/null +++ b/web/src/components/supabase/Provider.tsx @@ -0,0 +1,51 @@ +import { supabase } from '@/components/supabase/client.ts' +import { Session } from '@supabase/supabase-js' +import { createContext, PropsWithChildren, useContext, useEffect, useMemo, useState } from 'react' + +type Props = PropsWithChildren +type Context = { + session: Session|null +} + +const SupabaseContext = createContext({} as Context) + +export default function SupabaseProvider(props: Props) { + const [session, setSession] = useState(null) + + const context = useMemo(() => ({ + session, + }), [session]) + + useEffect(() => { + supabase.auth.getSession().then(({ data: { session } }) => { + setSession(session) + }) + + const {data: { subscription }} = supabase.auth.onAuthStateChange( + (event, session) => { + if (import.meta.env.DEV) { + console.log(event) + } + + if (event === 'SIGNED_OUT') { + setSession(null) + } else if (session) { + setSession(session) + } + }) + + return () => { + subscription.unsubscribe() + } + }, [setSession]) + + return ( + + {props.children} + + ) +} + +export function useSupabase() { + return useContext(SupabaseContext) +} diff --git a/web/src/components/supabase/client.ts b/web/src/components/supabase/client.ts new file mode 100644 index 0000000..db69463 --- /dev/null +++ b/web/src/components/supabase/client.ts @@ -0,0 +1,12 @@ +import { createClient } from '@supabase/supabase-js' + +const url = import.meta.env.VITE_SUPABASE_URL! + +const isRemote = /^(?:https:\/\/)(?[\w]+)(?:\.supabase\.co)$/.test(url) + +const supabase = createClient( + url, + import.meta.env.VITE_SUPABASE_ANON_KEY!, +) + +export { supabase, isRemote } diff --git a/web/src/components/ui/button.tsx b/web/src/components/ui/button.tsx new file mode 100644 index 0000000..0270f64 --- /dev/null +++ b/web/src/components/ui/button.tsx @@ -0,0 +1,57 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const buttonVariants = cva( + "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50", + { + variants: { + variant: { + default: + "bg-primary text-primary-foreground shadow hover:bg-primary/90", + destructive: + "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", + outline: + "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", + secondary: + "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", + ghost: "hover:bg-accent hover:text-accent-foreground", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-9 px-4 py-2", + sm: "h-8 rounded-md px-3 text-xs", + lg: "h-10 rounded-md px-8", + icon: "h-9 w-9", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean +} + +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button" + return ( + + ) + } +) +Button.displayName = "Button" + +export { Button, buttonVariants } diff --git a/web/src/components/ui/card.tsx b/web/src/components/ui/card.tsx new file mode 100644 index 0000000..77e9fb7 --- /dev/null +++ b/web/src/components/ui/card.tsx @@ -0,0 +1,76 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +const Card = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +Card.displayName = "Card" + +const CardHeader = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardHeader.displayName = "CardHeader" + +const CardTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)) +CardTitle.displayName = "CardTitle" + +const CardDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)) +CardDescription.displayName = "CardDescription" + +const CardContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)) +CardContent.displayName = "CardContent" + +const CardFooter = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardFooter.displayName = "CardFooter" + +export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } diff --git a/web/src/components/ui/dropdown-menu.tsx b/web/src/components/ui/dropdown-menu.tsx new file mode 100644 index 0000000..0e4dccf --- /dev/null +++ b/web/src/components/ui/dropdown-menu.tsx @@ -0,0 +1,203 @@ +import * as React from "react" +import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" +import { + CheckIcon, + ChevronRightIcon, + DotFilledIcon, +} from "@radix-ui/react-icons" + +import { cn } from "@/lib/utils" + +const DropdownMenu = DropdownMenuPrimitive.Root + +const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger + +const DropdownMenuGroup = DropdownMenuPrimitive.Group + +const DropdownMenuPortal = DropdownMenuPrimitive.Portal + +const DropdownMenuSub = DropdownMenuPrimitive.Sub + +const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup + +const DropdownMenuSubTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, children, ...props }, ref) => ( + + {children} + + +)) +DropdownMenuSubTrigger.displayName = + DropdownMenuPrimitive.SubTrigger.displayName + +const DropdownMenuSubContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DropdownMenuSubContent.displayName = + DropdownMenuPrimitive.SubContent.displayName + +const DropdownMenuContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, sideOffset = 4, ...props }, ref) => ( + + + +)) +DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName + +const DropdownMenuItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + +)) +DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName + +const DropdownMenuCheckboxItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, checked, ...props }, ref) => ( + + + + + + + {children} + +)) +DropdownMenuCheckboxItem.displayName = + DropdownMenuPrimitive.CheckboxItem.displayName + +const DropdownMenuRadioItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + {children} + +)) +DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName + +const DropdownMenuLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + +)) +DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName + +const DropdownMenuSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName + +const DropdownMenuShortcut = ({ + className, + ...props +}: React.HTMLAttributes) => { + return ( + + ) +} +DropdownMenuShortcut.displayName = "DropdownMenuShortcut" + +export { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuGroup, + DropdownMenuPortal, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuRadioGroup, +} diff --git a/web/src/components/ui/form.tsx b/web/src/components/ui/form.tsx new file mode 100644 index 0000000..f6afdaf --- /dev/null +++ b/web/src/components/ui/form.tsx @@ -0,0 +1,176 @@ +import * as React from "react" +import * as LabelPrimitive from "@radix-ui/react-label" +import { Slot } from "@radix-ui/react-slot" +import { + Controller, + ControllerProps, + FieldPath, + FieldValues, + FormProvider, + useFormContext, +} from "react-hook-form" + +import { cn } from "@/lib/utils" +import { Label } from "@/components/ui/label" + +const Form = FormProvider + +type FormFieldContextValue< + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath +> = { + name: TName +} + +const FormFieldContext = React.createContext( + {} as FormFieldContextValue +) + +const FormField = < + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath +>({ + ...props +}: ControllerProps) => { + return ( + + + + ) +} + +const useFormField = () => { + const fieldContext = React.useContext(FormFieldContext) + const itemContext = React.useContext(FormItemContext) + const { getFieldState, formState } = useFormContext() + + const fieldState = getFieldState(fieldContext.name, formState) + + if (!fieldContext) { + throw new Error("useFormField should be used within ") + } + + const { id } = itemContext + + return { + id, + name: fieldContext.name, + formItemId: `${id}-form-item`, + formDescriptionId: `${id}-form-item-description`, + formMessageId: `${id}-form-item-message`, + ...fieldState, + } +} + +type FormItemContextValue = { + id: string +} + +const FormItemContext = React.createContext( + {} as FormItemContextValue +) + +const FormItem = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => { + const id = React.useId() + + return ( + +
+ + ) +}) +FormItem.displayName = "FormItem" + +const FormLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + const { error, formItemId } = useFormField() + + return ( +