diff --git a/.github/workflows/IOSDeploy.yaml b/.github/workflows/IOSDeploy.yaml index c85106021..64ee22754 100644 --- a/.github/workflows/IOSDeploy.yaml +++ b/.github/workflows/IOSDeploy.yaml @@ -20,7 +20,7 @@ jobs: uses: oven-sh/setup-bun@v2 - name: Install dependencies - working-directory: clients + working-directory: clients run: make install-shared && make install-mobile - name: Setup Expo and EAS diff --git a/CLAUDE.md b/CLAUDE.md index fa8c164d3..bc0ac2800 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -49,12 +49,13 @@ const variantStyles: Record = { secondary: "bg-bg-container text-text-default hover:bg-bg-selected", }; -export function Button({ variant = "secondary", className = "", ...props }: ButtonProps) { +export function Button({ + variant = "secondary", + className = "", + ...props +}: ButtonProps) { return ( - + + {invited && ( +

+ Invite sent! +

+ )} +

+ You can also do this later from your settings. +

+ + + {/* Actions */} +
+ + +
+ + + + + ); +} diff --git a/clients/web/src/components/onboarding/LeftPanel.tsx b/clients/web/src/components/onboarding/LeftPanel.tsx new file mode 100644 index 000000000..97b3ef2d5 --- /dev/null +++ b/clients/web/src/components/onboarding/LeftPanel.tsx @@ -0,0 +1,15 @@ +export function LeftPanel() { + return ( +
+
+ + SelfServe + +

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec risus + nunc, ullamcorper vitae risus vel, tristique vehicula lectus. +

+
+
+ ); +} diff --git a/clients/web/src/components/onboarding/OnboardingPage.tsx b/clients/web/src/components/onboarding/OnboardingPage.tsx new file mode 100644 index 000000000..3fe2be050 --- /dev/null +++ b/clients/web/src/components/onboarding/OnboardingPage.tsx @@ -0,0 +1,83 @@ +import { useState } from "react"; +import { WelcomeStep } from "./WelcomeStep"; +import { RoleSelectionStep } from "./RoleSelectionStep"; +import { EmployeeRoleStep } from "./EmployeeRoleStep"; +import { PropertyDetailsStep } from "./PropertyDetailsStep"; +import { InviteTeamStep } from "./InviteTeamStep"; +import type { OnboardingFormData } from "./types"; + +const INITIAL_FORM_DATA: OnboardingFormData = { + role: null, + employeeRole: null, + hotelName: "", + numberOfRooms: "", + propertyType: "", + inviteEmail: "", +}; + +type Step = + | "welcome" + | "role" + | "employeeRole" + | "propertyDetails" + | "inviteTeam"; + +export function OnboardingPage() { + const [currentStep, setCurrentStep] = useState("welcome"); + const [formData, setFormData] = + useState(INITIAL_FORM_DATA); + + const updateForm = (updates: Partial) => { + setFormData((prev) => ({ ...prev, ...updates })); + }; + + const handleRoleSelected = (role: string) => { + updateForm({ role }); + if (role === "employee") { + setCurrentStep("employeeRole"); + } else { + setCurrentStep("propertyDetails"); + } + }; + + const renderStep = () => { + switch (currentStep) { + case "welcome": + return setCurrentStep("role")} />; + case "role": + return ( + + ); + case "employeeRole": + return ( + setCurrentStep("propertyDetails")} + onBack={() => setCurrentStep("role")} + /> + ); + case "propertyDetails": + return ( + setCurrentStep("inviteTeam")} + onBack={() => + setCurrentStep( + formData.role === "employee" ? "employeeRole" : "role", + ) + } + /> + ); + case "inviteTeam": + return ; + } + }; + + return
{renderStep()}
; +} diff --git a/clients/web/src/components/onboarding/PropertyDetailsStep.tsx b/clients/web/src/components/onboarding/PropertyDetailsStep.tsx new file mode 100644 index 000000000..ac8dfc31f --- /dev/null +++ b/clients/web/src/components/onboarding/PropertyDetailsStep.tsx @@ -0,0 +1,116 @@ +import { LeftPanel } from "./LeftPanel"; +import type { OnboardingFormData } from "./types"; + +type PropertyDetailsStepProps = { + formData: OnboardingFormData; + updateForm: (updates: Partial) => void; + onNext: () => void; + onBack: () => void; +}; + +const PROPERTY_TYPES = [ + "Hotel", + "Motel", + "Resort", + "Bed & Breakfast", + "Hostel", +]; + +export function PropertyDetailsStep({ + formData, + updateForm, + onNext, + onBack, +}: PropertyDetailsStepProps) { + const isValid = + formData.hotelName && formData.numberOfRooms && formData.propertyType; + + return ( +
+ +
+
+
+ {/* Header */} +
+

+ Property Details +

+

+ Lorem ipsum dolor sit amet. +

+
+ + {/* Form fields */} +
+
+ + updateForm({ hotelName: e.target.value })} + placeholder="Lorem ipsum dolor sit amet" + className="border border-[var(--color-stroke-subtle)] rounded-lg px-3 py-2 text-sm outline-none w-full box-border" + /> +
+
+ + + updateForm({ numberOfRooms: e.target.value }) + } + placeholder="e.g. 150" + className="border border-[var(--color-stroke-subtle)] rounded-lg px-3 py-2 text-sm outline-none w-full box-border" + /> +
+
+ + +
+
+ + {/* Actions */} +
+ + +
+
+
+
+
+ ); +} diff --git a/clients/web/src/components/onboarding/RoleCard.tsx b/clients/web/src/components/onboarding/RoleCard.tsx new file mode 100644 index 000000000..e76bba2cf --- /dev/null +++ b/clients/web/src/components/onboarding/RoleCard.tsx @@ -0,0 +1,28 @@ +type RoleCardProps = { + label: string; + description: string; + selected: boolean; + onSelect: () => void; +}; + +export function RoleCard({ + label, + description, + selected, + onSelect, +}: RoleCardProps) { + return ( + + ); +} diff --git a/clients/web/src/components/onboarding/RoleSelectionStep.tsx b/clients/web/src/components/onboarding/RoleSelectionStep.tsx new file mode 100644 index 000000000..59ff511de --- /dev/null +++ b/clients/web/src/components/onboarding/RoleSelectionStep.tsx @@ -0,0 +1,62 @@ +import { LeftPanel } from "./LeftPanel"; +import { RoleCard } from "./RoleCard"; +import type { OnboardingFormData } from "./types"; + +type RoleSelectionStepProps = { + formData: OnboardingFormData; + updateForm: (updates: Partial) => void; + onNext: (role: string) => void; +}; + +const ROLES = [ + { + id: "manager", + label: "Manager", + description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", + }, + { + id: "employee", + label: "Employee", + description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", + }, +]; + +export function RoleSelectionStep({ + formData, + onNext, +}: RoleSelectionStepProps) { + return ( +
+ +
+
+ {/* Logo */} +
+ + {/* Title */} + + Role + + + {/* Subtitle */} + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. + + + {/* Role cards grid */} +
+ {ROLES.map((role) => ( + onNext(role.id)} + /> + ))} +
+
+
+
+ ); +} diff --git a/clients/web/src/components/onboarding/WelcomeStep.tsx b/clients/web/src/components/onboarding/WelcomeStep.tsx new file mode 100644 index 000000000..402092780 --- /dev/null +++ b/clients/web/src/components/onboarding/WelcomeStep.tsx @@ -0,0 +1,35 @@ +import { LeftPanel } from "./LeftPanel"; + +type WelcomeStepProps = { + onNext: () => void; +}; + +export function WelcomeStep({ onNext }: WelcomeStepProps) { + return ( +
+ +
+
+ {/* Logo */} +
+ + {/* Welcome + Button */} +
+

+ Welcome +

+ +
+
+
+
+ ); +} diff --git a/clients/web/src/components/onboarding/onboardingMocks.ts b/clients/web/src/components/onboarding/onboardingMocks.ts new file mode 100644 index 000000000..f25b35696 --- /dev/null +++ b/clients/web/src/components/onboarding/onboardingMocks.ts @@ -0,0 +1,30 @@ +export const ROLES = [ + { + id: "manager", + label: "Manager", + description: "Oversees operations and staff.", + }, + { + id: "front_desk", + label: "Front Desk", + description: "Handles guest check-ins and requests.", + }, + { + id: "housekeeping", + label: "Housekeeping", + description: "Manages room cleaning and maintenance.", + }, + { + id: "maintenance", + label: "Maintenance", + description: "Handles repairs and facilities.", + }, +]; + +export const PROPERTY_TYPES = [ + "Hotel", + "Motel", + "Resort", + "Bed & Breakfast", + "Hostel", +]; diff --git a/clients/web/src/components/onboarding/types.ts b/clients/web/src/components/onboarding/types.ts new file mode 100644 index 000000000..57b09b223 --- /dev/null +++ b/clients/web/src/components/onboarding/types.ts @@ -0,0 +1,8 @@ +export interface OnboardingFormData { + role: string | null; + employeeRole: string | null; + hotelName: string; + numberOfRooms: string; + propertyType: string; + inviteEmail: string; +} diff --git a/clients/web/src/routeTree.gen.ts b/clients/web/src/routeTree.gen.ts index d03f3cd37..a07724011 100644 --- a/clients/web/src/routeTree.gen.ts +++ b/clients/web/src/routeTree.gen.ts @@ -24,13 +24,13 @@ import { Route as ProtectedGuestsIndexRouteImport } from './routes/_protected/gu import { Route as ProtectedGuestsGuestIdRouteImport } from './routes/_protected/guests.$guestId' const SignUpRoute = SignUpRouteImport.update({ - id: '/sign-up', - path: '/sign-up', + id: "/sign-up", + path: "/sign-up", getParentRoute: () => rootRouteImport, -} as any) +} as any); const SignInRoute = SignInRouteImport.update({ - id: '/sign-in', - path: '/sign-in', + id: "/sign-in", + path: "/sign-in", getParentRoute: () => rootRouteImport, } as any) const NoOrgRoute = NoOrgRouteImport.update({ @@ -39,54 +39,54 @@ const NoOrgRoute = NoOrgRouteImport.update({ getParentRoute: () => rootRouteImport, } as any) const ProtectedRoute = ProtectedRouteImport.update({ - id: '/_protected', + id: "/_protected", getParentRoute: () => rootRouteImport, -} as any) +} as any); const IndexRoute = IndexRouteImport.update({ - id: '/', - path: '/', + id: "/", + path: "/", getParentRoute: () => rootRouteImport, -} as any) +} as any); const ProtectedTestApiRoute = ProtectedTestApiRouteImport.update({ - id: '/test-api', - path: '/test-api', + id: "/test-api", + path: "/test-api", getParentRoute: () => ProtectedRoute, -} as any) +} as any); const ProtectedSettingsRoute = ProtectedSettingsRouteImport.update({ - id: '/settings', - path: '/settings', + id: "/settings", + path: "/settings", getParentRoute: () => ProtectedRoute, -} as any) +} as any); const ProtectedRoomsRoute = ProtectedRoomsRouteImport.update({ - id: '/rooms', - path: '/rooms', + id: "/rooms", + path: "/rooms", getParentRoute: () => ProtectedRoute, -} as any) +} as any); const ProtectedProfileRoute = ProtectedProfileRouteImport.update({ - id: '/profile', - path: '/profile', + id: "/profile", + path: "/profile", getParentRoute: () => ProtectedRoute, -} as any) +} as any); const ProtectedHomeRoute = ProtectedHomeRouteImport.update({ - id: '/home', - path: '/home', + id: "/home", + path: "/home", getParentRoute: () => ProtectedRoute, -} as any) +} as any); const ProtectedRoomsIndexRoute = ProtectedRoomsIndexRouteImport.update({ - id: '/', - path: '/', + id: "/", + path: "/", getParentRoute: () => ProtectedRoomsRoute, -} as any) +} as any); const ProtectedGuestsIndexRoute = ProtectedGuestsIndexRouteImport.update({ - id: '/guests/', - path: '/guests/', + id: "/guests/", + path: "/guests/", getParentRoute: () => ProtectedRoute, -} as any) +} as any); const ProtectedGuestsGuestIdRoute = ProtectedGuestsGuestIdRouteImport.update({ - id: '/guests/$guestId', - path: '/guests/$guestId', + id: "/guests/$guestId", + path: "/guests/$guestId", getParentRoute: () => ProtectedRoute, -} as any) +} as any); export interface FileRoutesByFullPath { '/': typeof IndexRoute @@ -132,7 +132,7 @@ export interface FileRoutesById { '/_protected/rooms/': typeof ProtectedRoomsIndexRoute } export interface FileRouteTypes { - fileRoutesByFullPath: FileRoutesByFullPath + fileRoutesByFullPath: FileRoutesByFullPath; fullPaths: | '/' | '/no-org' @@ -184,7 +184,7 @@ export interface RootRouteChildren { SignUpRoute: typeof SignUpRoute } -declare module '@tanstack/react-router' { +declare module "@tanstack/react-router" { interface FileRoutesByPath { '/sign-up': { id: '/sign-up' @@ -281,25 +281,25 @@ declare module '@tanstack/react-router' { } interface ProtectedRoomsRouteChildren { - ProtectedRoomsIndexRoute: typeof ProtectedRoomsIndexRoute + ProtectedRoomsIndexRoute: typeof ProtectedRoomsIndexRoute; } const ProtectedRoomsRouteChildren: ProtectedRoomsRouteChildren = { ProtectedRoomsIndexRoute: ProtectedRoomsIndexRoute, -} +}; const ProtectedRoomsRouteWithChildren = ProtectedRoomsRoute._addFileChildren( ProtectedRoomsRouteChildren, -) +); interface ProtectedRouteChildren { - ProtectedHomeRoute: typeof ProtectedHomeRoute - ProtectedProfileRoute: typeof ProtectedProfileRoute - ProtectedRoomsRoute: typeof ProtectedRoomsRouteWithChildren - ProtectedSettingsRoute: typeof ProtectedSettingsRoute - ProtectedTestApiRoute: typeof ProtectedTestApiRoute - ProtectedGuestsGuestIdRoute: typeof ProtectedGuestsGuestIdRoute - ProtectedGuestsIndexRoute: typeof ProtectedGuestsIndexRoute + ProtectedHomeRoute: typeof ProtectedHomeRoute; + ProtectedProfileRoute: typeof ProtectedProfileRoute; + ProtectedRoomsRoute: typeof ProtectedRoomsRouteWithChildren; + ProtectedSettingsRoute: typeof ProtectedSettingsRoute; + ProtectedTestApiRoute: typeof ProtectedTestApiRoute; + ProtectedGuestsGuestIdRoute: typeof ProtectedGuestsGuestIdRoute; + ProtectedGuestsIndexRoute: typeof ProtectedGuestsIndexRoute; } const ProtectedRouteChildren: ProtectedRouteChildren = { @@ -310,11 +310,11 @@ const ProtectedRouteChildren: ProtectedRouteChildren = { ProtectedTestApiRoute: ProtectedTestApiRoute, ProtectedGuestsGuestIdRoute: ProtectedGuestsGuestIdRoute, ProtectedGuestsIndexRoute: ProtectedGuestsIndexRoute, -} +}; const ProtectedRouteWithChildren = ProtectedRoute._addFileChildren( ProtectedRouteChildren, -) +); const rootRouteChildren: RootRouteChildren = { IndexRoute: IndexRoute, @@ -322,16 +322,16 @@ const rootRouteChildren: RootRouteChildren = { NoOrgRoute: NoOrgRoute, SignInRoute: SignInRoute, SignUpRoute: SignUpRoute, -} +}; export const routeTree = rootRouteImport ._addFileChildren(rootRouteChildren) - ._addFileTypes() + ._addFileTypes(); -import type { getRouter } from './router.tsx' -import type { createStart } from '@tanstack/react-start' -declare module '@tanstack/react-start' { +import type { getRouter } from "./router.tsx"; +import type { createStart } from "@tanstack/react-start"; +declare module "@tanstack/react-start" { interface Register { - ssr: true - router: Awaited> + ssr: true; + router: Awaited>; } } diff --git a/clients/web/src/routes/onboarding.tsx b/clients/web/src/routes/onboarding.tsx new file mode 100644 index 000000000..7b7acee11 --- /dev/null +++ b/clients/web/src/routes/onboarding.tsx @@ -0,0 +1,6 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { OnboardingPage } from "../components/onboarding/OnboardingPage"; + +export const Route = createFileRoute("/onboarding")({ + component: OnboardingPage, +}); diff --git a/clients/web/src/styles.css b/clients/web/src/styles.css index 170a805a2..c93c538d2 100644 --- a/clients/web/src/styles.css +++ b/clients/web/src/styles.css @@ -68,6 +68,10 @@ --color-request-assigned: #1a56db; --color-request-completed-secondary: #cde4ce; --color-request-completed: #047a0d; + --color-text-heading: #0f172b; + --color-text-muted: #62748e; + --color-bg-input: #f1f5f9; + --color-bg-avatar: #cbd5e1; } button {