From 7a490af15522207f220e06f95fd052d4e4f2058f Mon Sep 17 00:00:00 2001 From: Brion Mario Date: Fri, 6 Jun 2025 16:01:29 +0530 Subject: [PATCH 001/131] chore(recipes): update react sample feat: implement utility function for class name merging using clsx and tailwind-merge chore: configure Tailwind CSS with custom theme and extend default settings fix: update TypeScript configuration for path aliases and improve type checking chore: update Vite configuration to include Tailwind CSS plugin and path alias resolution --- recipes/vite-react-ts/components.json | 21 ++ recipes/vite-react-ts/package.json | 17 +- recipes/vite-react-ts/src/App.css | 42 ---- recipes/vite-react-ts/src/App.tsx | 142 ++++++++--- .../vite-react-ts/src/components/Header.tsx | 185 ++++++++++++++ .../vite-react-ts/src/components/ui/alert.tsx | 66 +++++ .../src/components/ui/button.tsx | 59 +++++ .../vite-react-ts/src/components/ui/card.tsx | 92 +++++++ .../vite-react-ts/src/components/ui/input.tsx | 21 ++ .../vite-react-ts/src/components/ui/label.tsx | 22 ++ recipes/vite-react-ts/src/index.css | 185 ++++++++++---- recipes/vite-react-ts/src/lib/utils.ts | 6 + .../src/pages/CreateOrganization.tsx | 197 +++++++++++++++ recipes/vite-react-ts/src/pages/Dashboard.tsx | 213 ++++++++++++++++ .../vite-react-ts/src/pages/Organizations.tsx | 151 +++++++++++ recipes/vite-react-ts/src/pages/Profile.tsx | 236 ++++++++++++++++++ recipes/vite-react-ts/src/pages/SignIn.tsx | 122 +++++++++ recipes/vite-react-ts/tailwind.config.js | 34 +++ recipes/vite-react-ts/tsconfig.app.json | 7 +- recipes/vite-react-ts/tsconfig.json | 11 +- recipes/vite-react-ts/vite.config.ts | 15 +- 21 files changed, 1703 insertions(+), 141 deletions(-) create mode 100644 recipes/vite-react-ts/components.json delete mode 100644 recipes/vite-react-ts/src/App.css create mode 100644 recipes/vite-react-ts/src/components/Header.tsx create mode 100644 recipes/vite-react-ts/src/components/ui/alert.tsx create mode 100644 recipes/vite-react-ts/src/components/ui/button.tsx create mode 100644 recipes/vite-react-ts/src/components/ui/card.tsx create mode 100644 recipes/vite-react-ts/src/components/ui/input.tsx create mode 100644 recipes/vite-react-ts/src/components/ui/label.tsx create mode 100644 recipes/vite-react-ts/src/lib/utils.ts create mode 100644 recipes/vite-react-ts/src/pages/CreateOrganization.tsx create mode 100644 recipes/vite-react-ts/src/pages/Dashboard.tsx create mode 100644 recipes/vite-react-ts/src/pages/Organizations.tsx create mode 100644 recipes/vite-react-ts/src/pages/Profile.tsx create mode 100644 recipes/vite-react-ts/src/pages/SignIn.tsx create mode 100644 recipes/vite-react-ts/tailwind.config.js diff --git a/recipes/vite-react-ts/components.json b/recipes/vite-react-ts/components.json new file mode 100644 index 000000000..1d282e640 --- /dev/null +++ b/recipes/vite-react-ts/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "tailwind.config.js", + "css": "src/index.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} \ No newline at end of file diff --git a/recipes/vite-react-ts/package.json b/recipes/vite-react-ts/package.json index 31d814cff..0cda07072 100644 --- a/recipes/vite-react-ts/package.json +++ b/recipes/vite-react-ts/package.json @@ -11,20 +11,31 @@ }, "dependencies": { "@asgardeo/react": "workspace:^", + "@radix-ui/react-label": "^2.1.7", + "@radix-ui/react-slot": "^1.2.3", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "lucide-react": "^0.294.0", "react": "^19.1.0", - "react-dom": "^19.1.0" + "react-dom": "^19.1.0", + "react-router-dom": "^6.20.0", + "tailwind-merge": "^3.3.0" }, "devDependencies": { - "@eslint/js": "^9.25.0", + "@tailwindcss/vite": "^4.1.8", "@types/react": "^19.1.2", "@types/react-dom": "^19.1.2", "@vitejs/plugin-react": "^4.4.1", + "autoprefixer": "^10.4.16", "eslint": "^9.25.0", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.19", "globals": "^16.0.0", + "postcss": "^8.4.31", + "tailwindcss": "^4.1.8", + "tw-animate-css": "^1.3.4", "typescript": "~5.8.3", "typescript-eslint": "^8.30.1", "vite": "^6.3.5" } -} +} \ No newline at end of file diff --git a/recipes/vite-react-ts/src/App.css b/recipes/vite-react-ts/src/App.css deleted file mode 100644 index b9d355df2..000000000 --- a/recipes/vite-react-ts/src/App.css +++ /dev/null @@ -1,42 +0,0 @@ -#root { - max-width: 1280px; - margin: 0 auto; - padding: 2rem; - text-align: center; -} - -.logo { - height: 6em; - padding: 1.5em; - will-change: filter; - transition: filter 300ms; -} -.logo:hover { - filter: drop-shadow(0 0 2em #646cffaa); -} -.logo.react:hover { - filter: drop-shadow(0 0 2em #61dafbaa); -} - -@keyframes logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} - -@media (prefers-reduced-motion: no-preference) { - a:nth-of-type(2) .logo { - animation: logo-spin infinite 20s linear; - } -} - -.card { - padding: 2em; -} - -.read-the-docs { - color: #888; -} diff --git a/recipes/vite-react-ts/src/App.tsx b/recipes/vite-react-ts/src/App.tsx index ea14f7e37..2c1a49521 100644 --- a/recipes/vite-react-ts/src/App.tsx +++ b/recipes/vite-react-ts/src/App.tsx @@ -1,42 +1,112 @@ -import {UserDropdown, SignInButton, SignedOut, SignOutButton, SignedIn, User, UserProfile} from '@asgardeo/react'; -import './App.css'; +'use client'; + +import {BrowserRouter as Router, Routes, Route, Navigate} from 'react-router-dom'; +import {useState, createContext, useContext} from 'react'; +import Header from './components/Header'; +import DashboardPage from './pages/Dashboard'; +import ProfilePage from './pages/Profile'; +import OrganizationsPage from './pages/Organizations'; +import CreateOrganizationPage from './pages/CreateOrganization'; +import SignInPage from './pages/SignIn'; + +// Types +export interface User { + id: string; + name: string; + email: string; + avatar: string; + username: string; +} + +export interface Organization { + id: string; + name: string; + slug: string; + avatar: string; + role: 'owner' | 'admin' | 'member'; + memberCount: number; +} + +// Context +interface AppContextType { + user: User | null; + currentOrg: Organization | null; + organizations: Organization[]; + setCurrentOrg: (org: Organization) => void; + addOrganization: (org: Organization) => void; +} + +const AppContext = createContext(null); + +export const useApp = () => { + const context = useContext(AppContext); + if (!context) throw new Error('useApp must be used within AppProvider'); + return context; +}; + +// Mock data +const mockUser: User = { + id: '1', + name: 'John Doe', + email: 'john@example.com', + avatar: '/placeholder.svg?height=32&width=32', + username: 'johndoe', +}; + +const mockOrganizations: Organization[] = [ + { + id: '1', + name: 'Acme Corp', + slug: 'acme-corp', + avatar: '/placeholder.svg?height=32&width=32', + role: 'owner', + memberCount: 12, + }, + { + id: '2', + name: 'Tech Startup', + slug: 'tech-startup', + avatar: '/placeholder.svg?height=32&width=32', + role: 'admin', + memberCount: 8, + }, +]; function App() { + const [user] = useState(mockUser); + const [currentOrg, setCurrentOrg] = useState(mockOrganizations[0]); + const [organizations, setOrganizations] = useState(mockOrganizations); + + const addOrganization = (org: Organization) => { + setOrganizations(prev => [...prev, org]); + }; + return ( - <> - - Sign In - - - - {user => ( -
-

- Welcome, {user?.firstName} {user?.lastName}! -

-

Email: {user?.email}

-
- )} -
- null, - }, - { - label: 'Logout', - icon: null, - onClick: () => null, - }, - ]} - portalId="custom-dropdown" - /> - - Logout -
- + + +
+
+
+ + } /> + } /> + } /> + } /> + } /> + } /> + +
+
+
+
); } diff --git a/recipes/vite-react-ts/src/components/Header.tsx b/recipes/vite-react-ts/src/components/Header.tsx new file mode 100644 index 000000000..6edf6e34e --- /dev/null +++ b/recipes/vite-react-ts/src/components/Header.tsx @@ -0,0 +1,185 @@ +'use client'; + +import {useState, useRef, useEffect} from 'react'; +import {Link, useNavigate} from 'react-router-dom'; +import {useApp} from '../App'; +import {Users, ChevronDown, Settings, User, LogOut, Plus, Check, Building2} from 'lucide-react'; +import {SignOutButton} from '@asgardeo/react'; + +export default function Header() { + const {user, currentOrg, organizations, setCurrentOrg} = useApp(); + const [showUserDropdown, setShowUserDropdown] = useState(false); + const [showOrgDropdown, setShowOrgDropdown] = useState(false); + const userDropdownRef = useRef(null); + const orgDropdownRef = useRef(null); + const navigate = useNavigate(); + + // Close dropdowns when clicking outside + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (userDropdownRef.current && !userDropdownRef.current.contains(event.target as Node)) { + setShowUserDropdown(false); + } + if (orgDropdownRef.current && !orgDropdownRef.current.contains(event.target as Node)) { + setShowOrgDropdown(false); + } + }; + + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + }, []); + + const handleOrgSwitch = (org: typeof currentOrg) => { + if (org) { + setCurrentOrg(org); + setShowOrgDropdown(false); + } + }; + + return ( +
+
+
+ {/* Left side - Logo and Navigation */} +
+ +
+ +
+ Teamspace + + + +
+ + {/* Right side - Organization switcher and User dropdown */} +
+ {/* Organization Switcher */} +
+ + + {showOrgDropdown && ( +
+
+ Switch organization +
+ + {organizations.map(org => ( + + ))} + +
+ setShowOrgDropdown(false)} + > + + Manage organizations + + setShowOrgDropdown(false)} + > + + New organization + +
+
+ )} +
+ + {/* User Dropdown */} +
+ + + {showUserDropdown && ( +
+
+
{user?.name}
+
@{user?.username}
+
+ + setShowUserDropdown(false)} + > + + Your profile + + + setShowUserDropdown(false)} + > + + Settings + + +
+ + {({signOut}) => ( + + )} + +
+
+ )} +
+
+
+
+
+ ); +} diff --git a/recipes/vite-react-ts/src/components/ui/alert.tsx b/recipes/vite-react-ts/src/components/ui/alert.tsx new file mode 100644 index 000000000..14213546e --- /dev/null +++ b/recipes/vite-react-ts/src/components/ui/alert.tsx @@ -0,0 +1,66 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const alertVariants = cva( + "relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current", + { + variants: { + variant: { + default: "bg-card text-card-foreground", + destructive: + "text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +function Alert({ + className, + variant, + ...props +}: React.ComponentProps<"div"> & VariantProps) { + return ( +
+ ) +} + +function AlertTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function AlertDescription({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ ) +} + +export { Alert, AlertTitle, AlertDescription } diff --git a/recipes/vite-react-ts/src/components/ui/button.tsx b/recipes/vite-react-ts/src/components/ui/button.tsx new file mode 100644 index 000000000..a2df8dce6 --- /dev/null +++ b/recipes/vite-react-ts/src/components/ui/button.tsx @@ -0,0 +1,59 @@ +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 gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", + { + variants: { + variant: { + default: + "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90", + destructive: + "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", + outline: + "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", + secondary: + "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", + ghost: + "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-9 px-4 py-2 has-[>svg]:px-3", + sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", + lg: "h-10 rounded-md px-6 has-[>svg]:px-4", + icon: "size-9", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +function Button({ + className, + variant, + size, + asChild = false, + ...props +}: React.ComponentProps<"button"> & + VariantProps & { + asChild?: boolean + }) { + const Comp = asChild ? Slot : "button" + + return ( + + ) +} + +export { Button, buttonVariants } diff --git a/recipes/vite-react-ts/src/components/ui/card.tsx b/recipes/vite-react-ts/src/components/ui/card.tsx new file mode 100644 index 000000000..d05bbc6c7 --- /dev/null +++ b/recipes/vite-react-ts/src/components/ui/card.tsx @@ -0,0 +1,92 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +function Card({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardDescription({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardAction({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardContent({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +export { + Card, + CardHeader, + CardFooter, + CardTitle, + CardAction, + CardDescription, + CardContent, +} diff --git a/recipes/vite-react-ts/src/components/ui/input.tsx b/recipes/vite-react-ts/src/components/ui/input.tsx new file mode 100644 index 000000000..03295ca6a --- /dev/null +++ b/recipes/vite-react-ts/src/components/ui/input.tsx @@ -0,0 +1,21 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +function Input({ className, type, ...props }: React.ComponentProps<"input">) { + return ( + + ) +} + +export { Input } diff --git a/recipes/vite-react-ts/src/components/ui/label.tsx b/recipes/vite-react-ts/src/components/ui/label.tsx new file mode 100644 index 000000000..ef7133a75 --- /dev/null +++ b/recipes/vite-react-ts/src/components/ui/label.tsx @@ -0,0 +1,22 @@ +import * as React from "react" +import * as LabelPrimitive from "@radix-ui/react-label" + +import { cn } from "@/lib/utils" + +function Label({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { Label } diff --git a/recipes/vite-react-ts/src/index.css b/recipes/vite-react-ts/src/index.css index 08a3ac9e1..eb293356b 100644 --- a/recipes/vite-react-ts/src/index.css +++ b/recipes/vite-react-ts/src/index.css @@ -1,68 +1,151 @@ -:root { - font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; - line-height: 1.5; - font-weight: 400; +@import "tailwindcss"; +@import "tw-animate-css"; - color-scheme: light dark; - color: rgba(255, 255, 255, 0.87); - background-color: #242424; +@custom-variant dark (&:is(.dark *)); - font-synthesis: none; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; +/* Custom scrollbar */ +::-webkit-scrollbar { + width: 6px; } -a { - font-weight: 500; - color: #646cff; - text-decoration: inherit; +::-webkit-scrollbar-track { + background: #f1f5f9; } -a:hover { - color: #535bf2; + +::-webkit-scrollbar-thumb { + background: #cbd5e1; + border-radius: 3px; } -body { - margin: 0; - display: flex; - place-items: center; - min-width: 320px; - min-height: 100vh; +::-webkit-scrollbar-thumb:hover { + background: #94a3b8; } -h1 { - font-size: 3.2em; - line-height: 1.1; +/* Smooth transitions */ +* { + transition: colors 0.15s ease-in-out; } -button { - border-radius: 8px; - border: 1px solid transparent; - padding: 0.6em 1.2em; - font-size: 1em; - font-weight: 500; - font-family: inherit; - background-color: #1a1a1a; - cursor: pointer; - transition: border-color 0.25s; +/* Focus styles */ +button:focus, +input:focus, +textarea:focus { + outline: 2px solid #3b82f6; + outline-offset: 2px; } -button:hover { - border-color: #646cff; + +@theme inline { + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); } -button:focus, -button:focus-visible { - outline: 4px auto -webkit-focus-ring-color; + +:root { + --radius: 0.625rem; + --background: oklch(1 0 0); + --foreground: oklch(0.145 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0.145 0 0); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.145 0 0); + --primary: oklch(0.205 0 0); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.97 0 0); + --secondary-foreground: oklch(0.205 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --accent: oklch(0.97 0 0); + --accent-foreground: oklch(0.205 0 0); + --destructive: oklch(0.577 0.245 27.325); + --border: oklch(0.922 0 0); + --input: oklch(0.922 0 0); + --ring: oklch(0.708 0 0); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.145 0 0); + --sidebar-primary: oklch(0.205 0 0); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.97 0 0); + --sidebar-accent-foreground: oklch(0.205 0 0); + --sidebar-border: oklch(0.922 0 0); + --sidebar-ring: oklch(0.708 0 0); } -@media (prefers-color-scheme: light) { - :root { - color: #213547; - background-color: #ffffff; - } - a:hover { - color: #747bff; +.dark { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.205 0 0); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.922 0 0); + --primary-foreground: oklch(0.205 0 0); + --secondary: oklch(0.269 0 0); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --accent: oklch(0.269 0 0); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.556 0 0); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.205 0 0); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.269 0 0); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.556 0 0); +} + +@layer base { + * { + @apply border-border outline-ring/50; } - button { - background-color: #f9f9f9; + body { + @apply bg-background text-foreground; } -} +} \ No newline at end of file diff --git a/recipes/vite-react-ts/src/lib/utils.ts b/recipes/vite-react-ts/src/lib/utils.ts new file mode 100644 index 000000000..bd0c391dd --- /dev/null +++ b/recipes/vite-react-ts/src/lib/utils.ts @@ -0,0 +1,6 @@ +import { clsx, type ClassValue } from "clsx" +import { twMerge } from "tailwind-merge" + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} diff --git a/recipes/vite-react-ts/src/pages/CreateOrganization.tsx b/recipes/vite-react-ts/src/pages/CreateOrganization.tsx new file mode 100644 index 000000000..ba613075a --- /dev/null +++ b/recipes/vite-react-ts/src/pages/CreateOrganization.tsx @@ -0,0 +1,197 @@ +'use client'; + +import type React from 'react'; + +import {useState} from 'react'; +import {useNavigate} from 'react-router-dom'; +import {useApp} from '../App'; +import {Building2, Upload, ArrowLeft} from 'lucide-react'; + +export default function CreateOrganization() { + const navigate = useNavigate(); + const {addOrganization} = useApp(); + const [formData, setFormData] = useState({ + name: '', + slug: '', + description: '', + website: '', + avatar: '/placeholder.svg?height=64&width=64', + }); + const [isSubmitting, setIsSubmitting] = useState(false); + + const handleNameChange = (e: React.ChangeEvent) => { + const name = e.target.value; + const slug = name + .toLowerCase() + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-|-$/g, ''); + setFormData({...formData, name, slug}); + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setIsSubmitting(true); + + // Simulate API call + await new Promise(resolve => setTimeout(resolve, 1000)); + + const newOrg = { + id: Date.now().toString(), + name: formData.name, + slug: formData.slug, + avatar: formData.avatar, + role: 'owner' as const, + memberCount: 1, + }; + + addOrganization(newOrg); + navigate('/organizations'); + }; + + return ( +
+ {/* Header */} +
+ +

Create a new organization

+

+ Organizations are shared accounts where teams can collaborate across many projects at once. +

+
+ + {/* Form */} +
+
+ {/* Organization Avatar */} +
+ +
+ Organization avatar + +
+

We recommend an image that's at least 64×64 pixels.

+
+ + {/* Organization Name */} +
+ + +
+ + {/* Organization Slug */} +
+ +
+ + teamspace.com/ + + setFormData({...formData, slug: e.target.value})} + className="flex-1 px-3 py-2 border border-gray-300 rounded-r-md shadow-sm focus:ring-blue-500 focus:border-blue-500" + placeholder="organization-slug" + /> +
+

+ This will be your organization's URL. Only lowercase letters, numbers, and hyphens are allowed. +

+
+ + {/* Description */} +
+ +