feat: initialize serenity-webapp with UI components and dashboard layout

This commit is contained in:
mark 2025-06-17 18:39:18 +08:00
parent 9aef5ec6f6
commit ea117f24a0
89 changed files with 11051 additions and 0 deletions

24
.gitignore vendored Normal file
View File

@ -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?

21
components.json Normal file
View File

@ -0,0 +1,21 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/index.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}

28
eslint.config.js Normal file
View File

@ -0,0 +1,28 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
export default tseslint.config(
{ ignores: ['dist'] },
{
extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ['**/*.{ts,tsx}'],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
...reactHooks.configs.recommended.rules,
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
},
)

13
index.html Normal file
View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

5237
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

53
package.json Normal file
View File

@ -0,0 +1,53 @@
{
"name": "serenity-webapp",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@fortawesome/fontawesome-free": "^6.7.2",
"@radix-ui/react-alert-dialog": "^1.1.14",
"@radix-ui/react-avatar": "^1.1.10",
"@radix-ui/react-collapsible": "^1.1.11",
"@radix-ui/react-dialog": "^1.1.14",
"@radix-ui/react-dropdown-menu": "^2.1.15",
"@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-separator": "^1.1.7",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-switch": "^1.2.5",
"@radix-ui/react-tabs": "^1.1.12",
"@radix-ui/react-tooltip": "^1.2.7",
"@tailwindcss/vite": "^4.1.10",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"framer-motion": "^12.18.1",
"lucide-react": "^0.515.0",
"react": "^19.1.0",
"react-day-picker": "^9.7.0",
"react-dom": "^19.1.0",
"react-router-dom": "^7.6.2",
"tailwind-merge": "^3.3.1",
"tailwindcss": "^4.1.10"
},
"devDependencies": {
"@eslint/js": "^9.25.0",
"@types/node": "^24.0.1",
"@types/react": "^19.1.2",
"@types/react-dom": "^19.1.2",
"@vitejs/plugin-react": "^4.4.1",
"eslint": "^9.25.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.19",
"globals": "^16.0.0",
"tw-animate-css": "^1.3.4",
"typescript": "~5.8.3",
"typescript-eslint": "^8.30.1",
"vite": "^6.3.5"
}
}

1
public/vite.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

0
src/App.css Normal file
View File

25
src/App.tsx Normal file
View File

@ -0,0 +1,25 @@
import "./App.css";
import { ThemeProvider } from "./features/dashboard/components/theme-provider";
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import { Admin } from "./features/dashboard/pages";
import { Login } from "./features/auth/pages/Login";
import { Home } from "./features/user/pages";
import { FullGalleryPage } from "./features/user/components/sections/FullGalleryPage";
function App() {
return (
<ThemeProvider>
<Router>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/admin" element={<Admin />} />
<Route path="/login" element={<Login />} />
<Route path="/gallery" element={<FullGalleryPage />} />
</Routes>
</Router>
</ThemeProvider>
);
}
export default App;

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

BIN
src/assets/bg3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

BIN
src/assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

1
src/assets/react.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@ -0,0 +1,155 @@
import * as React from "react"
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"
function AlertDialog({
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {
return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />
}
function AlertDialogTrigger({
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {
return (
<AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
)
}
function AlertDialogPortal({
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {
return (
<AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />
)
}
function AlertDialogOverlay({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) {
return (
<AlertDialogPrimitive.Overlay
data-slot="alert-dialog-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className
)}
{...props}
/>
)
}
function AlertDialogContent({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Content>) {
return (
<AlertDialogPortal>
<AlertDialogOverlay />
<AlertDialogPrimitive.Content
data-slot="alert-dialog-content"
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
className
)}
{...props}
/>
</AlertDialogPortal>
)
}
function AlertDialogHeader({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-dialog-header"
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
{...props}
/>
)
}
function AlertDialogFooter({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-dialog-footer"
className={cn(
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
className
)}
{...props}
/>
)
}
function AlertDialogTitle({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
return (
<AlertDialogPrimitive.Title
data-slot="alert-dialog-title"
className={cn("text-lg font-semibold", className)}
{...props}
/>
)
}
function AlertDialogDescription({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
return (
<AlertDialogPrimitive.Description
data-slot="alert-dialog-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
function AlertDialogAction({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Action>) {
return (
<AlertDialogPrimitive.Action
className={cn(buttonVariants(), className)}
{...props}
/>
)
}
function AlertDialogCancel({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Cancel>) {
return (
<AlertDialogPrimitive.Cancel
className={cn(buttonVariants({ variant: "outline" }), className)}
{...props}
/>
)
}
export {
AlertDialog,
AlertDialogPortal,
AlertDialogOverlay,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogHeader,
AlertDialogFooter,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogAction,
AlertDialogCancel,
}

View File

@ -0,0 +1,51 @@
import * as React from "react"
import * as AvatarPrimitive from "@radix-ui/react-avatar"
import { cn } from "@/lib/utils"
function Avatar({
className,
...props
}: React.ComponentProps<typeof AvatarPrimitive.Root>) {
return (
<AvatarPrimitive.Root
data-slot="avatar"
className={cn(
"relative flex size-8 shrink-0 overflow-hidden rounded-full",
className
)}
{...props}
/>
)
}
function AvatarImage({
className,
...props
}: React.ComponentProps<typeof AvatarPrimitive.Image>) {
return (
<AvatarPrimitive.Image
data-slot="avatar-image"
className={cn("aspect-square size-full", className)}
{...props}
/>
)
}
function AvatarFallback({
className,
...props
}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
return (
<AvatarPrimitive.Fallback
data-slot="avatar-fallback"
className={cn(
"bg-muted flex size-full items-center justify-center rounded-full",
className
)}
{...props}
/>
)
}
export { Avatar, AvatarImage, AvatarFallback }

View File

@ -0,0 +1,109 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { ChevronRight, MoreHorizontal } from "lucide-react"
import { cn } from "@/lib/utils"
function Breadcrumb({ ...props }: React.ComponentProps<"nav">) {
return <nav aria-label="breadcrumb" data-slot="breadcrumb" {...props} />
}
function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
return (
<ol
data-slot="breadcrumb-list"
className={cn(
"text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5",
className
)}
{...props}
/>
)
}
function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
return (
<li
data-slot="breadcrumb-item"
className={cn("inline-flex items-center gap-1.5", className)}
{...props}
/>
)
}
function BreadcrumbLink({
asChild,
className,
...props
}: React.ComponentProps<"a"> & {
asChild?: boolean
}) {
const Comp = asChild ? Slot : "a"
return (
<Comp
data-slot="breadcrumb-link"
className={cn("hover:text-foreground transition-colors", className)}
{...props}
/>
)
}
function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
return (
<span
data-slot="breadcrumb-page"
role="link"
aria-disabled="true"
aria-current="page"
className={cn("text-foreground font-normal", className)}
{...props}
/>
)
}
function BreadcrumbSeparator({
children,
className,
...props
}: React.ComponentProps<"li">) {
return (
<li
data-slot="breadcrumb-separator"
role="presentation"
aria-hidden="true"
className={cn("[&>svg]:size-3.5", className)}
{...props}
>
{children ?? <ChevronRight />}
</li>
)
}
function BreadcrumbEllipsis({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="breadcrumb-ellipsis"
role="presentation"
aria-hidden="true"
className={cn("flex size-9 items-center justify-center", className)}
{...props}
>
<MoreHorizontal className="size-4" />
<span className="sr-only">More</span>
</span>
)
}
export {
Breadcrumb,
BreadcrumbList,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbPage,
BreadcrumbSeparator,
BreadcrumbEllipsis,
}

View File

@ -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<typeof buttonVariants> & {
asChild?: boolean
}) {
const Comp = asChild ? Slot : "button"
return (
<Comp
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
}
export { Button, buttonVariants }

View File

@ -0,0 +1,208 @@
import * as React from "react"
import {
ChevronDownIcon,
ChevronLeftIcon,
ChevronRightIcon,
} from "lucide-react"
import { DayButton, DayPicker, getDefaultClassNames } from "react-day-picker"
import { cn } from "@/lib/utils"
import { Button, buttonVariants } from "@/components/ui/button"
function Calendar({
className,
classNames,
showOutsideDays = true,
captionLayout = "label",
buttonVariant = "ghost",
formatters,
components,
...props
}: React.ComponentProps<typeof DayPicker> & {
buttonVariant?: React.ComponentProps<typeof Button>["variant"]
}) {
const defaultClassNames = getDefaultClassNames()
return (
<DayPicker
showOutsideDays={showOutsideDays}
className={cn(
"bg-background group/calendar p-3 [--cell-size:--spacing(8)] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent",
String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`,
String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
className
)}
captionLayout={captionLayout}
formatters={{
formatMonthDropdown: (date) =>
date.toLocaleString("default", { month: "short" }),
...formatters,
}}
classNames={{
root: cn("w-fit", defaultClassNames.root),
months: cn(
"flex gap-4 flex-col md:flex-row relative",
defaultClassNames.months
),
month: cn("flex flex-col w-full gap-4", defaultClassNames.month),
nav: cn(
"flex items-center gap-1 w-full absolute top-0 inset-x-0 justify-between",
defaultClassNames.nav
),
button_previous: cn(
buttonVariants({ variant: buttonVariant }),
"size-(--cell-size) aria-disabled:opacity-50 p-0 select-none",
defaultClassNames.button_previous
),
button_next: cn(
buttonVariants({ variant: buttonVariant }),
"size-(--cell-size) aria-disabled:opacity-50 p-0 select-none",
defaultClassNames.button_next
),
month_caption: cn(
"flex items-center justify-center h-(--cell-size) w-full px-(--cell-size)",
defaultClassNames.month_caption
),
dropdowns: cn(
"w-full flex items-center text-sm font-medium justify-center h-(--cell-size) gap-1.5",
defaultClassNames.dropdowns
),
dropdown_root: cn(
"relative has-focus:border-ring border border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] rounded-md",
defaultClassNames.dropdown_root
),
dropdown: cn("absolute inset-0 opacity-0", defaultClassNames.dropdown),
caption_label: cn(
"select-none font-medium",
captionLayout === "label"
? "text-sm"
: "rounded-md pl-2 pr-1 flex items-center gap-1 text-sm h-8 [&>svg]:text-muted-foreground [&>svg]:size-3.5",
defaultClassNames.caption_label
),
table: "w-full border-collapse",
weekdays: cn("flex", defaultClassNames.weekdays),
weekday: cn(
"text-muted-foreground rounded-md flex-1 font-normal text-[0.8rem] select-none",
defaultClassNames.weekday
),
week: cn("flex w-full mt-2", defaultClassNames.week),
week_number_header: cn(
"select-none w-(--cell-size)",
defaultClassNames.week_number_header
),
week_number: cn(
"text-[0.8rem] select-none text-muted-foreground",
defaultClassNames.week_number
),
day: cn(
"relative w-full h-full p-0 text-center [&:first-child[data-selected=true]_button]:rounded-l-md [&:last-child[data-selected=true]_button]:rounded-r-md group/day aspect-square select-none",
defaultClassNames.day
),
range_start: cn(
"rounded-l-md bg-accent",
defaultClassNames.range_start
),
range_middle: cn("rounded-none", defaultClassNames.range_middle),
range_end: cn("rounded-r-md bg-accent", defaultClassNames.range_end),
today: cn(
"bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none",
defaultClassNames.today
),
outside: cn(
"text-muted-foreground aria-selected:text-muted-foreground",
defaultClassNames.outside
),
disabled: cn(
"text-muted-foreground opacity-50",
defaultClassNames.disabled
),
hidden: cn("invisible", defaultClassNames.hidden),
...classNames,
}}
components={{
Root: ({ className, rootRef, ...props }) => {
return (
<div
data-slot="calendar"
ref={rootRef}
className={cn(className)}
{...props}
/>
)
},
Chevron: ({ className, orientation, ...props }) => {
if (orientation === "left") {
return (
<ChevronLeftIcon className={cn("size-4", className)} {...props} />
)
}
if (orientation === "right") {
return (
<ChevronRightIcon
className={cn("size-4", className)}
{...props}
/>
)
}
return (
<ChevronDownIcon className={cn("size-4", className)} {...props} />
)
},
DayButton: CalendarDayButton,
WeekNumber: ({ children, ...props }) => {
return (
<td {...props}>
<div className="flex size-(--cell-size) items-center justify-center text-center">
{children}
</div>
</td>
)
},
...components,
}}
{...props}
/>
)
}
function CalendarDayButton({
className,
day,
modifiers,
...props
}: React.ComponentProps<typeof DayButton>) {
const defaultClassNames = getDefaultClassNames()
const ref = React.useRef<HTMLButtonElement>(null)
React.useEffect(() => {
if (modifiers.focused) ref.current?.focus()
}, [modifiers.focused])
return (
<Button
ref={ref}
variant="ghost"
size="icon"
data-day={day.date.toLocaleDateString()}
data-selected-single={
modifiers.selected &&
!modifiers.range_start &&
!modifiers.range_end &&
!modifiers.range_middle
}
data-range-start={modifiers.range_start}
data-range-end={modifiers.range_end}
data-range-middle={modifiers.range_middle}
className={cn(
"data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-ring/50 dark:hover:text-accent-foreground flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 leading-none font-normal group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:ring-[3px] data-[range-end=true]:rounded-md data-[range-end=true]:rounded-r-md data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-md data-[range-start=true]:rounded-l-md [&>span]:text-xs [&>span]:opacity-70",
defaultClassNames.day,
className
)}
{...props}
/>
)
}
export { Calendar, CalendarDayButton }

View File

@ -0,0 +1,92 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Card({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card"
className={cn(
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
className
)}
{...props}
/>
)
}
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-header"
className={cn(
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
className
)}
{...props}
/>
)
}
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-title"
className={cn("leading-none font-semibold", className)}
{...props}
/>
)
}
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-action"
className={cn(
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
className
)}
{...props}
/>
)
}
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-content"
className={cn("px-6", className)}
{...props}
/>
)
}
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-footer"
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
{...props}
/>
)
}
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardAction,
CardDescription,
CardContent,
}

View File

@ -0,0 +1,31 @@
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
function Collapsible({
...props
}: React.ComponentProps<typeof CollapsiblePrimitive.Root>) {
return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />
}
function CollapsibleTrigger({
...props
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) {
return (
<CollapsiblePrimitive.CollapsibleTrigger
data-slot="collapsible-trigger"
{...props}
/>
)
}
function CollapsibleContent({
...props
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>) {
return (
<CollapsiblePrimitive.CollapsibleContent
data-slot="collapsible-content"
{...props}
/>
)
}
export { Collapsible, CollapsibleTrigger, CollapsibleContent }

View File

@ -0,0 +1,141 @@
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { XIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function Dialog({
...props
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
return <DialogPrimitive.Root data-slot="dialog" {...props} />
}
function DialogTrigger({
...props
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
}
function DialogPortal({
...props
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
}
function DialogClose({
...props
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
}
function DialogOverlay({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
return (
<DialogPrimitive.Overlay
data-slot="dialog-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className
)}
{...props}
/>
)
}
function DialogContent({
className,
children,
showCloseButton = true,
...props
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
showCloseButton?: boolean
}) {
return (
<DialogPortal data-slot="dialog-portal">
<DialogOverlay />
<DialogPrimitive.Content
data-slot="dialog-content"
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
className
)}
{...props}
>
{children}
{showCloseButton && (
<DialogPrimitive.Close
data-slot="dialog-close"
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
>
<XIcon />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
)}
</DialogPrimitive.Content>
</DialogPortal>
)
}
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-header"
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
{...props}
/>
)
}
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-footer"
className={cn(
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
className
)}
{...props}
/>
)
}
function DialogTitle({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
return (
<DialogPrimitive.Title
data-slot="dialog-title"
className={cn("text-lg leading-none font-semibold", className)}
{...props}
/>
)
}
function DialogDescription({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
return (
<DialogPrimitive.Description
data-slot="dialog-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
export {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogOverlay,
DialogPortal,
DialogTitle,
DialogTrigger,
}

View File

@ -0,0 +1,255 @@
import * as React from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function DropdownMenu({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />
}
function DropdownMenuPortal({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
return (
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
)
}
function DropdownMenuTrigger({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
return (
<DropdownMenuPrimitive.Trigger
data-slot="dropdown-menu-trigger"
{...props}
/>
)
}
function DropdownMenuContent({
className,
sideOffset = 4,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
return (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
data-slot="dropdown-menu-content"
sideOffset={sideOffset}
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
className
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
)
}
function DropdownMenuGroup({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
return (
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
)
}
function DropdownMenuItem({
className,
inset,
variant = "default",
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
variant?: "default" | "destructive"
}) {
return (
<DropdownMenuPrimitive.Item
data-slot="dropdown-menu-item"
data-inset={inset}
data-variant={variant}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
)
}
function DropdownMenuCheckboxItem({
className,
children,
checked,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
return (
<DropdownMenuPrimitive.CheckboxItem
data-slot="dropdown-menu-checkbox-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
checked={checked}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
)
}
function DropdownMenuRadioGroup({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
return (
<DropdownMenuPrimitive.RadioGroup
data-slot="dropdown-menu-radio-group"
{...props}
/>
)
}
function DropdownMenuRadioItem({
className,
children,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
return (
<DropdownMenuPrimitive.RadioItem
data-slot="dropdown-menu-radio-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CircleIcon className="size-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
)
}
function DropdownMenuLabel({
className,
inset,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
}) {
return (
<DropdownMenuPrimitive.Label
data-slot="dropdown-menu-label"
data-inset={inset}
className={cn(
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
className
)}
{...props}
/>
)
}
function DropdownMenuSeparator({
className,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
return (
<DropdownMenuPrimitive.Separator
data-slot="dropdown-menu-separator"
className={cn("bg-border -mx-1 my-1 h-px", className)}
{...props}
/>
)
}
function DropdownMenuShortcut({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="dropdown-menu-shortcut"
className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest",
className
)}
{...props}
/>
)
}
function DropdownMenuSub({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />
}
function DropdownMenuSubTrigger({
className,
inset,
children,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean
}) {
return (
<DropdownMenuPrimitive.SubTrigger
data-slot="dropdown-menu-sub-trigger"
data-inset={inset}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8",
className
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto size-4" />
</DropdownMenuPrimitive.SubTrigger>
)
}
function DropdownMenuSubContent({
className,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
return (
<DropdownMenuPrimitive.SubContent
data-slot="dropdown-menu-sub-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
className
)}
{...props}
/>
)
}
export {
DropdownMenu,
DropdownMenuPortal,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuLabel,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuSub,
DropdownMenuSubTrigger,
DropdownMenuSubContent,
}

View File

@ -0,0 +1,21 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
return (
<input
type={type}
data-slot="input"
className={cn(
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"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",
className
)}
{...props}
/>
)
}
export { Input }

View File

@ -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<typeof LabelPrimitive.Root>) {
return (
<LabelPrimitive.Root
data-slot="label"
className={cn(
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
className
)}
{...props}
/>
)
}
export { Label }

View File

@ -0,0 +1,28 @@
"use client"
import * as React from "react"
import * as SeparatorPrimitive from "@radix-ui/react-separator"
import { cn } from "@/lib/utils"
function Separator({
className,
orientation = "horizontal",
decorative = true,
...props
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
return (
<SeparatorPrimitive.Root
data-slot="separator"
decorative={decorative}
orientation={orientation}
className={cn(
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
className
)}
{...props}
/>
)
}
export { Separator }

137
src/components/ui/sheet.tsx Normal file
View File

@ -0,0 +1,137 @@
import * as React from "react"
import * as SheetPrimitive from "@radix-ui/react-dialog"
import { XIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {
return <SheetPrimitive.Root data-slot="sheet" {...props} />
}
function SheetTrigger({
...props
}: React.ComponentProps<typeof SheetPrimitive.Trigger>) {
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />
}
function SheetClose({
...props
}: React.ComponentProps<typeof SheetPrimitive.Close>) {
return <SheetPrimitive.Close data-slot="sheet-close" {...props} />
}
function SheetPortal({
...props
}: React.ComponentProps<typeof SheetPrimitive.Portal>) {
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />
}
function SheetOverlay({
className,
...props
}: React.ComponentProps<typeof SheetPrimitive.Overlay>) {
return (
<SheetPrimitive.Overlay
data-slot="sheet-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className
)}
{...props}
/>
)
}
function SheetContent({
className,
children,
side = "right",
...props
}: React.ComponentProps<typeof SheetPrimitive.Content> & {
side?: "top" | "right" | "bottom" | "left"
}) {
return (
<SheetPortal>
<SheetOverlay />
<SheetPrimitive.Content
data-slot="sheet-content"
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
side === "right" &&
"data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm",
side === "left" &&
"data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm",
side === "top" &&
"data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b",
side === "bottom" &&
"data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t",
className
)}
{...props}
>
{children}
<SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none">
<XIcon className="size-4" />
<span className="sr-only">Close</span>
</SheetPrimitive.Close>
</SheetPrimitive.Content>
</SheetPortal>
)
}
function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sheet-header"
className={cn("flex flex-col gap-1.5 p-4", className)}
{...props}
/>
)
}
function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sheet-footer"
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
{...props}
/>
)
}
function SheetTitle({
className,
...props
}: React.ComponentProps<typeof SheetPrimitive.Title>) {
return (
<SheetPrimitive.Title
data-slot="sheet-title"
className={cn("text-foreground font-semibold", className)}
{...props}
/>
)
}
function SheetDescription({
className,
...props
}: React.ComponentProps<typeof SheetPrimitive.Description>) {
return (
<SheetPrimitive.Description
data-slot="sheet-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
export {
Sheet,
SheetTrigger,
SheetClose,
SheetContent,
SheetHeader,
SheetFooter,
SheetTitle,
SheetDescription,
}

View File

@ -0,0 +1,724 @@
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { PanelLeftIcon } from "lucide-react";
import { useIsMobile } from "@/hooks/use-mobile";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Separator } from "@/components/ui/separator";
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
} from "@/components/ui/sheet";
import { Skeleton } from "@/components/ui/skeleton";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
const SIDEBAR_COOKIE_NAME = "sidebar_state";
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
const SIDEBAR_WIDTH = "16rem";
const SIDEBAR_WIDTH_MOBILE = "18rem";
const SIDEBAR_WIDTH_ICON = "3rem";
const SIDEBAR_KEYBOARD_SHORTCUT = "b";
type SidebarContextProps = {
state: "expanded" | "collapsed";
open: boolean;
setOpen: (open: boolean) => void;
openMobile: boolean;
setOpenMobile: (open: boolean) => void;
isMobile: boolean;
toggleSidebar: () => void;
};
const SidebarContext = React.createContext<SidebarContextProps | null>(null);
function useSidebar() {
const context = React.useContext(SidebarContext);
if (!context) {
throw new Error("useSidebar must be used within a SidebarProvider.");
}
return context;
}
function SidebarProvider({
defaultOpen = true,
open: openProp,
onOpenChange: setOpenProp,
className,
style,
children,
...props
}: React.ComponentProps<"div"> & {
defaultOpen?: boolean;
open?: boolean;
onOpenChange?: (open: boolean) => void;
}) {
const isMobile = useIsMobile();
const [openMobile, setOpenMobile] = React.useState(false);
// This is the internal state of the sidebar.
// We use openProp and setOpenProp for control from outside the component.
const [_open, _setOpen] = React.useState(defaultOpen);
const open = openProp ?? _open;
const setOpen = React.useCallback(
(value: boolean | ((value: boolean) => boolean)) => {
const openState = typeof value === "function" ? value(open) : value;
if (setOpenProp) {
setOpenProp(openState);
} else {
_setOpen(openState);
}
// This sets the cookie to keep the sidebar state.
document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
},
[setOpenProp, open]
);
// Helper to toggle the sidebar.
const toggleSidebar = React.useCallback(() => {
return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open);
}, [isMobile, setOpen, setOpenMobile]);
// Adds a keyboard shortcut to toggle the sidebar.
React.useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (
event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
(event.metaKey || event.ctrlKey)
) {
event.preventDefault();
toggleSidebar();
}
};
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, [toggleSidebar]);
// We add a state so that we can do data-state="expanded" or "collapsed".
// This makes it easier to style the sidebar with Tailwind classes.
const state = open ? "expanded" : "collapsed";
const contextValue = React.useMemo<SidebarContextProps>(
() => ({
state,
open,
setOpen,
isMobile,
openMobile,
setOpenMobile,
toggleSidebar,
}),
[state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar]
);
return (
<SidebarContext.Provider value={contextValue}>
<TooltipProvider delayDuration={0}>
<div
data-slot="sidebar-wrapper"
style={
{
"--sidebar-width": SIDEBAR_WIDTH,
"--sidebar-width-icon": SIDEBAR_WIDTH_ICON,
...style,
} as React.CSSProperties
}
className={cn(
"group/sidebar-wrapper has-data-[variant=inset]:bg-sidebar flex min-h-svh w-full",
className
)}
{...props}
>
{children}
</div>
</TooltipProvider>
</SidebarContext.Provider>
);
}
function Sidebar({
side = "left",
variant = "sidebar",
collapsible = "offcanvas",
className,
children,
...props
}: React.ComponentProps<"div"> & {
side?: "left" | "right";
variant?: "sidebar" | "floating" | "inset";
collapsible?: "offcanvas" | "icon" | "none";
}) {
const { isMobile, state, openMobile, setOpenMobile } = useSidebar();
if (collapsible === "none") {
return (
<div
data-slot="sidebar"
className={cn(
"bg-sidebar text-sidebar-foreground flex h-full w-(--sidebar-width) flex-col",
className
)}
{...props}
>
{children}
</div>
);
}
if (isMobile) {
return (
<Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>
<SheetContent
data-sidebar="sidebar"
data-slot="sidebar"
data-mobile="true"
className="bg-sidebar text-sidebar-foreground w-(--sidebar-width) p-0 [&>button]:hidden"
style={
{
"--sidebar-width": SIDEBAR_WIDTH_MOBILE,
} as React.CSSProperties
}
side={side}
>
<SheetHeader className="sr-only">
<SheetTitle>Sidebar</SheetTitle>
<SheetDescription>Displays the mobile sidebar.</SheetDescription>
</SheetHeader>
<div className="flex h-full w-full flex-col">{children}</div>
</SheetContent>
</Sheet>
);
}
return (
<div
className="group peer text-sidebar-foreground hidden md:block"
data-state={state}
data-collapsible={state === "collapsed" ? collapsible : ""}
data-variant={variant}
data-side={side}
data-slot="sidebar"
>
{/* This is what handles the sidebar gap on desktop */}
<div
data-slot="sidebar-gap"
className={cn(
"relative w-(--sidebar-width) bg-transparent transition-[width] duration-200 ease-linear",
"group-data-[collapsible=offcanvas]:w-0",
"group-data-[side=right]:rotate-180",
variant === "floating" || variant === "inset"
? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]"
: "group-data-[collapsible=icon]:w-(--sidebar-width-icon)"
)}
/>
<div
data-slot="sidebar-container"
className={cn(
"fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear md:flex",
side === "left"
? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]"
: "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]",
// Adjust the padding for floating and inset variants.
variant === "floating" || variant === "inset"
? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]"
: "group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l",
className
)}
{...props}
>
<div
data-sidebar="sidebar"
data-slot="sidebar-inner"
className="bg-sidebar group-data-[variant=floating]:border-sidebar-border flex h-full w-full flex-col group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:shadow-sm"
>
{children}
</div>
</div>
</div>
);
}
function SidebarTrigger({
className,
onClick,
...props
}: React.ComponentProps<typeof Button>) {
const { toggleSidebar } = useSidebar();
return (
<Button
data-sidebar="trigger"
data-slot="sidebar-trigger"
variant="ghost"
size="icon"
className={cn("size-7", className)}
onClick={(event) => {
onClick?.(event);
toggleSidebar();
}}
{...props}
>
<PanelLeftIcon />
<span className="sr-only">Toggle Sidebar</span>
</Button>
);
}
function SidebarRail({ className, ...props }: React.ComponentProps<"button">) {
const { toggleSidebar } = useSidebar();
return (
<button
data-sidebar="rail"
data-slot="sidebar-rail"
aria-label="Toggle Sidebar"
tabIndex={-1}
onClick={toggleSidebar}
title="Toggle Sidebar"
className={cn(
"hover:after:bg-sidebar-border absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] sm:flex",
"in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize",
"[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize",
"hover:group-data-[collapsible=offcanvas]:bg-sidebar group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full",
"[[data-side=left][data-collapsible=offcanvas]_&]:-right-2",
"[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",
className
)}
{...props}
/>
);
}
function SidebarInset({ className, ...props }: React.ComponentProps<"main">) {
return (
<main
data-slot="sidebar-inset"
className={cn(
"bg-background relative flex w-full flex-1 flex-col",
"md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2",
className
)}
{...props}
/>
);
}
function SidebarInput({
className,
...props
}: React.ComponentProps<typeof Input>) {
return (
<Input
data-slot="sidebar-input"
data-sidebar="input"
className={cn("bg-background h-8 w-full shadow-none", className)}
{...props}
/>
);
}
function SidebarHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sidebar-header"
data-sidebar="header"
className={cn("flex flex-col gap-2 p-2", className)}
{...props}
/>
);
}
function SidebarFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sidebar-footer"
data-sidebar="footer"
className={cn("flex flex-col gap-2 p-2", className)}
{...props}
/>
);
}
function SidebarSeparator({
className,
...props
}: React.ComponentProps<typeof Separator>) {
return (
<Separator
data-slot="sidebar-separator"
data-sidebar="separator"
className={cn("bg-sidebar-border mx-2 w-auto", className)}
{...props}
/>
);
}
function SidebarContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sidebar-content"
data-sidebar="content"
className={cn(
"flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden",
className
)}
{...props}
/>
);
}
function SidebarGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sidebar-group"
data-sidebar="group"
className={cn("relative flex w-full min-w-0 flex-col p-2", className)}
{...props}
/>
);
}
function SidebarGroupLabel({
className,
asChild = false,
...props
}: React.ComponentProps<"div"> & { asChild?: boolean }) {
const Comp = asChild ? Slot : "div";
return (
<Comp
data-slot="sidebar-group-label"
data-sidebar="group-label"
className={cn(
"text-sidebar-foreground/70 ring-sidebar-ring flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium outline-hidden transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
"group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
className
)}
{...props}
/>
);
}
function SidebarGroupAction({
className,
asChild = false,
...props
}: React.ComponentProps<"button"> & { asChild?: boolean }) {
const Comp = asChild ? Slot : "button";
return (
<Comp
data-slot="sidebar-group-action"
data-sidebar="group-action"
className={cn(
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground absolute top-3.5 right-3 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
// Increases the hit area of the button on mobile.
"after:absolute after:-inset-2 md:after:hidden",
"group-data-[collapsible=icon]:hidden",
className
)}
{...props}
/>
);
}
function SidebarGroupContent({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="sidebar-group-content"
data-sidebar="group-content"
className={cn("w-full text-sm", className)}
{...props}
/>
);
}
function SidebarMenu({ className, ...props }: React.ComponentProps<"ul">) {
return (
<ul
data-slot="sidebar-menu"
data-sidebar="menu"
className={cn("flex w-full min-w-0 flex-col gap-1", className)}
{...props}
/>
);
}
function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) {
return (
<li
data-slot="sidebar-menu-item"
data-sidebar="menu-item"
className={cn("group/menu-item relative", className)}
{...props}
/>
);
}
const sidebarMenuButtonVariants = cva(
"peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
{
variants: {
variant: {
default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
outline:
"bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]",
},
size: {
default: "h-8 text-sm",
sm: "h-7 text-xs",
lg: "h-12 text-sm group-data-[collapsible=icon]:p-0!",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
);
function SidebarMenuButton({
asChild = false,
isActive = false,
variant = "default",
size = "default",
tooltip,
className,
...props
}: React.ComponentProps<"button"> & {
asChild?: boolean;
isActive?: boolean;
tooltip?: string | React.ComponentProps<typeof TooltipContent>;
} & VariantProps<typeof sidebarMenuButtonVariants>) {
const Comp = asChild ? Slot : "button";
const { isMobile, state } = useSidebar();
const button = (
<Comp
data-slot="sidebar-menu-button"
data-sidebar="menu-button"
data-size={size}
data-active={isActive}
className={cn(sidebarMenuButtonVariants({ variant, size }), className)}
{...props}
/>
);
if (!tooltip) {
return button;
}
if (typeof tooltip === "string") {
tooltip = {
children: tooltip,
};
}
return (
<Tooltip>
<TooltipTrigger asChild>{button}</TooltipTrigger>
<TooltipContent
side="right"
align="center"
hidden={state !== "collapsed" || isMobile}
{...tooltip}
/>
</Tooltip>
);
}
function SidebarMenuAction({
className,
asChild = false,
showOnHover = false,
...props
}: React.ComponentProps<"button"> & {
asChild?: boolean;
showOnHover?: boolean;
}) {
const Comp = asChild ? Slot : "button";
return (
<Comp
data-slot="sidebar-menu-action"
data-sidebar="menu-action"
className={cn(
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground peer-hover/menu-button:text-sidebar-accent-foreground absolute top-1.5 right-1 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
// Increases the hit area of the button on mobile.
"after:absolute after:-inset-2 md:after:hidden",
"peer-data-[size=sm]/menu-button:top-1",
"peer-data-[size=default]/menu-button:top-1.5",
"peer-data-[size=lg]/menu-button:top-2.5",
"group-data-[collapsible=icon]:hidden",
showOnHover &&
"peer-data-[active=true]/menu-button:text-sidebar-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 md:opacity-0",
className
)}
{...props}
/>
);
}
function SidebarMenuBadge({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="sidebar-menu-badge"
data-sidebar="menu-badge"
className={cn(
"text-sidebar-foreground pointer-events-none absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums select-none",
"peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground",
"peer-data-[size=sm]/menu-button:top-1",
"peer-data-[size=default]/menu-button:top-1.5",
"peer-data-[size=lg]/menu-button:top-2.5",
"group-data-[collapsible=icon]:hidden",
className
)}
{...props}
/>
);
}
function SidebarMenuSkeleton({
className,
showIcon = false,
...props
}: React.ComponentProps<"div"> & {
showIcon?: boolean;
}) {
// Random width between 50 to 90%.
const width = React.useMemo(() => {
return `${Math.floor(Math.random() * 40) + 50}%`;
}, []);
return (
<div
data-slot="sidebar-menu-skeleton"
data-sidebar="menu-skeleton"
className={cn("flex h-8 items-center gap-2 rounded-md px-2", className)}
{...props}
>
{showIcon && (
<Skeleton
className="size-4 rounded-md"
data-sidebar="menu-skeleton-icon"
/>
)}
<Skeleton
className="h-4 max-w-(--skeleton-width) flex-1"
data-sidebar="menu-skeleton-text"
style={
{
"--skeleton-width": width,
} as React.CSSProperties
}
/>
</div>
);
}
function SidebarMenuSub({ className, ...props }: React.ComponentProps<"ul">) {
return (
<ul
data-slot="sidebar-menu-sub"
data-sidebar="menu-sub"
className={cn(
"border-sidebar-border mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l px-2.5 py-0.5",
"group-data-[collapsible=icon]:hidden",
className
)}
{...props}
/>
);
}
function SidebarMenuSubItem({
className,
...props
}: React.ComponentProps<"li">) {
return (
<li
data-slot="sidebar-menu-sub-item"
data-sidebar="menu-sub-item"
className={cn("group/menu-sub-item relative", className)}
{...props}
/>
);
}
function SidebarMenuSubButton({
asChild = false,
size = "md",
isActive = false,
className,
...props
}: React.ComponentProps<"a"> & {
asChild?: boolean;
size?: "sm" | "md";
isActive?: boolean;
}) {
const Comp = asChild ? Slot : "a";
return (
<Comp
data-slot="sidebar-menu-sub-button"
data-sidebar="menu-sub-button"
data-size={size}
data-active={isActive}
className={cn(
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground [&>svg]:text-sidebar-accent-foreground flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 outline-hidden focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
"data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground",
size === "sm" && "text-xs",
size === "md" && "text-sm",
"group-data-[collapsible=icon]:hidden",
className
)}
{...props}
/>
);
}
export {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupAction,
SidebarGroupContent,
SidebarGroupLabel,
SidebarHeader,
SidebarInput,
SidebarInset,
SidebarMenu,
SidebarMenuAction,
SidebarMenuBadge,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSkeleton,
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuSubItem,
SidebarProvider,
SidebarRail,
SidebarSeparator,
SidebarTrigger,
useSidebar,
};

View File

@ -0,0 +1,13 @@
import { cn } from "@/lib/utils"
function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="skeleton"
className={cn("bg-accent animate-pulse rounded-md", className)}
{...props}
/>
)
}
export { Skeleton }

View File

@ -0,0 +1,29 @@
import * as React from "react"
import * as SwitchPrimitive from "@radix-ui/react-switch"
import { cn } from "@/lib/utils"
function Switch({
className,
...props
}: React.ComponentProps<typeof SwitchPrimitive.Root>) {
return (
<SwitchPrimitive.Root
data-slot="switch"
className={cn(
"peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
>
<SwitchPrimitive.Thumb
data-slot="switch-thumb"
className={cn(
"bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0"
)}
/>
</SwitchPrimitive.Root>
)
}
export { Switch }

View File

@ -0,0 +1,64 @@
import * as React from "react"
import * as TabsPrimitive from "@radix-ui/react-tabs"
import { cn } from "@/lib/utils"
function Tabs({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.Root>) {
return (
<TabsPrimitive.Root
data-slot="tabs"
className={cn("flex flex-col gap-2", className)}
{...props}
/>
)
}
function TabsList({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.List>) {
return (
<TabsPrimitive.List
data-slot="tabs-list"
className={cn(
"bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]",
className
)}
{...props}
/>
)
}
function TabsTrigger({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
return (
<TabsPrimitive.Trigger
data-slot="tabs-trigger"
className={cn(
"data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
)
}
function TabsContent({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.Content>) {
return (
<TabsPrimitive.Content
data-slot="tabs-content"
className={cn("flex-1 outline-none", className)}
{...props}
/>
)
}
export { Tabs, TabsList, TabsTrigger, TabsContent }

View File

@ -0,0 +1,61 @@
"use client"
import * as React from "react"
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
import { cn } from "@/lib/utils"
function TooltipProvider({
delayDuration = 0,
...props
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
return (
<TooltipPrimitive.Provider
data-slot="tooltip-provider"
delayDuration={delayDuration}
{...props}
/>
)
}
function Tooltip({
...props
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
return (
<TooltipProvider>
<TooltipPrimitive.Root data-slot="tooltip" {...props} />
</TooltipProvider>
)
}
function TooltipTrigger({
...props
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
}
function TooltipContent({
className,
sideOffset = 0,
children,
...props
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
return (
<TooltipPrimitive.Portal>
<TooltipPrimitive.Content
data-slot="tooltip-content"
sideOffset={sideOffset}
className={cn(
"bg-primary text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
className
)}
{...props}
>
{children}
<TooltipPrimitive.Arrow className="bg-primary fill-primary z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
</TooltipPrimitive.Content>
</TooltipPrimitive.Portal>
)
}
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }

View File

@ -0,0 +1,101 @@
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Separator } from "@/components/ui/separator";
import logo from "@/assets/logo.png";
export function LoginForm({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
className={cn("flex flex-col gap-6 font-['Overpass']", className)}
{...props}
>
<Card className="shadow-lg overflow-hidden rounded-md">
<CardHeader>
<CardTitle className="flex justify-center mb-3">
<img src={logo} alt="Serenity Space Logo" />
</CardTitle>
<CardDescription className=" text-gray-600">
Enter your credentials to access your account
</CardDescription>
</CardHeader>
<CardContent>
<form>
<div className="flex flex-col gap-6">
<div className="grid gap-3">
<Label htmlFor="email" className=" font-medium text-gray-700">
Email
</Label>
<Input
id="email"
type="email"
placeholder="m@example.com"
className="rounded-md border-gray-300 focus:border-purple-400 focus:ring focus:ring-purple-200 focus:ring-opacity-50 transition-all duration-300"
required
/>
</div>
<div className="grid gap-3">
<div className="flex items-center">
<Label
htmlFor="password"
className=" font-medium text-gray-700"
>
Password
</Label>
<a
href="#"
className="ml-auto inline-block text-sm text-purple-600 hover:text-purple-700 underline-offset-4 hover:underline transition-all duration-300"
>
Forgot your password?
</a>
</div>
<Input
id="password"
type="password"
className="rounded-md border-gray-300 focus:border-purple-400 focus:ring focus:ring-purple-200 focus:ring-opacity-50 transition-all duration-300"
required
/>
</div>
<div className="flex flex-col gap-3 pt-2">
<Button
type="submit"
className="w-full bg-gradient-to-r from-purple-500 to-pink-400 hover:opacity-90 transition-all duration-300 rounded-md font-medium py-2.5"
>
Login
</Button>
<Separator className="my-4 bg-gray-200" />
<Button
variant="outline"
className="w-full border-gray-300 hover:bg-gray-50 hover:border-purple-300 transition-all duration-300 rounded-md font-medium py-2"
>
<i className="fab fa-google mr-2"></i> Login with Google
</Button>
</div>
</div>
<div className="mt-6 text-center text-sm text-gray-600">
Don&apos;t have an account?{" "}
<a
href="#"
className="text-purple-600 hover:text-purple-700 font-medium underline-offset-4 hover:underline transition-all duration-300"
>
Sign up
</a>
</div>
</form>
</CardContent>
</Card>
</div>
);
}

View File

@ -0,0 +1,11 @@
import { LoginForm } from "@/features/auth/components/login-form";
export function Login() {
return (
<div className="flex min-h-svh w-full items-center justify-center p-6 md:p-10">
<div className="w-full max-w-sm">
<LoginForm />
</div>
</div>
);
}

View File

View File

@ -0,0 +1,35 @@
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button";
export function Modal() {
return (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="outline">Show Dialog</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
<AlertDialogDescription>
This action cannot be undone. This will permanently delete your
account and remove your data from our servers.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction>Continue</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
}

View File

@ -0,0 +1,9 @@
const Typography1 = ({ children }: { children: React.ReactNode }) => {
return (
<h1 className="scroll-m-20 text-center text-4xl font-extrabold tracking-tight text-balance">
{children}
</h1>
);
};
export default Typography1;

View File

@ -0,0 +1,9 @@
const TypographyH2 = ({ children }: { children: React.ReactNode }) => {
return (
<h2 className="scroll-m-20 border-b pb-2 text-3xl font-semibold tracking-tight first:mt-0">
{children}
</h2>
);
};
export default TypographyH2;

View File

@ -0,0 +1,9 @@
const TypographyH3 = ({ children }: { children: React.ReactNode }) => {
return (
<h3 className="scroll-m-20 text-2xl font-semibold tracking-tight">
{children}
</h3>
);
};
export default TypographyH3;

View File

@ -0,0 +1,314 @@
"use client";
import * as React from "react";
import { ArchiveX, Command, File, Inbox, Send, Trash2 } from "lucide-react";
import { NavUser } from "@/features/dashboard/components/nav-user";
import { Label } from "@/components/ui/label";
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupContent,
SidebarHeader,
SidebarInput,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
} from "@/components/ui/sidebar";
import { Switch } from "@/components/ui/switch";
import { Separator } from "@/components/ui/separator";
// This is sample data
const data = {
user: {
name: "shadcn",
email: "m@example.com",
avatar: "/avatars/shadcn.jpg",
},
navMain: [
{
title: "Inbox",
url: "#",
icon: Inbox,
isActive: true,
},
{
title: "Drafts",
url: "#",
icon: File,
isActive: false,
},
{
title: "Sent",
url: "#",
icon: Send,
isActive: false,
},
{
title: "Junk",
url: "#",
icon: ArchiveX,
isActive: false,
},
{
title: "Trash",
url: "#",
icon: Trash2,
isActive: false,
},
],
mails: [
{
name: "William Smith",
email: "williamsmith@example.com",
subject: "Meeting Tomorrow",
date: "09:34 AM",
teaser:
"Hi team, just a reminder about our meeting tomorrow at 10 AM.\nPlease come prepared with your project updates.",
},
{
name: "Alice Smith",
email: "alicesmith@example.com",
subject: "Re: Project Update",
date: "Yesterday",
teaser:
"Thanks for the update. The progress looks great so far.\nLet's schedule a call to discuss the next steps.",
},
{
name: "Bob Johnson",
email: "bobjohnson@example.com",
avatar: "https://ui.shadcn.com/avatars/03.png",
subject: "Weekend Plans",
date: "2 days ago",
teaser:
"Hey everyone! I'm thinking of organizing a team outing this weekend.\nWould you be interested in a hiking trip or a beach day?",
},
{
name: "Emily Davis",
email: "emilydavis@example.com",
avatar: "https://ui.shadcn.com/avatars/04.png",
subject: "Re: Question about Budget",
date: "2 days ago",
teaser:
"I've reviewed the budget numbers you sent over.\nCan we set up a quick call to discuss some potential adjustments?",
},
{
name: "Michael Wilson",
email: "michaelwilson@example.com",
avatar: "https://ui.shadcn.com/avatars/05.png",
subject: "Important Announcement",
date: "1 week ago",
teaser:
"Please join us for an all-hands meeting this Friday at 3 PM.\nWe have some exciting news to share about the company's future.",
},
{
name: "Sarah Brown",
email: "sarahbrown@example.com",
avatar: "https://ui.shadcn.com/avatars/06.png",
subject: "Re: Feedback on Proposal",
date: "1 week ago",
teaser:
"Thank you for sending over the proposal. I've reviewed it and have some thoughts.\nCould we schedule a meeting to discuss my feedback in detail?",
},
{
name: "David Lee",
email: "davidlee@example.com",
avatar: "https://ui.shadcn.com/avatars/07.png",
subject: "New Project Idea",
date: "1 week ago",
teaser:
"I've been brainstorming and came up with an interesting project concept.\nDo you have time this week to discuss its potential impact and feasibility?",
},
{
name: "Olivia Wilson",
email: "oliviawilson@example.com",
avatar: "https://ui.shadcn.com/avatars/08.png",
subject: "Vacation Plans",
date: "1 week ago",
teaser:
"Just a heads up that I'll be taking a two-week vacation next month.\nI'll make sure all my projects are up to date before I leave.",
},
{
name: "James Martin",
email: "jamesmartin@example.com",
avatar: "https://ui.shadcn.com/avatars/09.png",
subject: "Re: Conference Registration",
date: "1 week ago",
teaser:
"I've completed the registration for the upcoming tech conference.\nLet me know if you need any additional information from my end.",
},
{
name: "Sophia White",
email: "sophiawhite@example.com",
avatar: "https://ui.shadcn.com/avatars/10.png",
subject: "Team Dinner",
date: "1 week ago",
teaser:
"To celebrate our recent project success, I'd like to organize a team dinner.\nAre you available next Friday evening? Please let me know your preferences.",
},
],
};
export interface Mail {
name: string;
email: string;
avatar?: string;
subject: string;
date: string;
teaser: string;
}
interface AppSidebarProps {
onSelectMail?: (mail: Mail) => void;
selectedMailId?: string;
}
export function AppSidebar({ onSelectMail, selectedMailId }: AppSidebarProps) {
const [mails, setMails] = React.useState(data.mails.slice(0, 5));
const [activeItem, setActiveItem] = React.useState(data.navMain[0]);
// Apply font to the entire component
React.useEffect(() => {
document.documentElement.classList.add('font-["Overpass"]');
}, []);
const { setOpen: setSidebarOpen } = useSidebar();
return (
<Sidebar
collapsible="icon"
className="overflow-hidden *:data-[sidebar=sidebar]:flex-row"
>
{/* This is the first sidebar */}
{/* We disable collapsible and adjust width to icon. */}
{/* This will make the sidebar appear as icons. */}
<Sidebar
collapsible="none"
className="w-[calc(var(--sidebar-width-icon)+1px)]! border-r bg-white font-['Overpass']"
>
<SidebarHeader>
<div className="h-2 bg-gradient-to-r from-purple-500 to-pink-400 w-full"></div>
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton size="lg" asChild className="md:h-10 md:p-2">
<a href="#">
<div className="bg-gradient-to-r from-purple-500 to-pink-400 text-white flex aspect-square size-8 items-center justify-center rounded-lg shadow-md">
<Command className="size-4" />
</div>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-medium text-purple-700">
Serenity Space
</span>
<span className="truncate text-xs text-gray-600">
Dashboard
</span>
</div>
</a>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarHeader>
<SidebarContent>
<SidebarGroup>
<SidebarGroupContent className="px-1.5 md:px-0">
<SidebarMenu>
{data.navMain.map((item) => (
<SidebarMenuItem key={item.title}>
<SidebarMenuButton
tooltip={{
children: item.title,
hidden: false,
}}
onClick={() => {
setActiveItem(item);
const mail = data.mails.sort(() => Math.random() - 0.5);
setMails(
mail.slice(
0,
Math.max(5, Math.floor(Math.random() * 10) + 1)
)
);
setSidebarOpen(true);
}}
isActive={activeItem?.title === item.title}
className={`px-2.5 md:px-2 rounded-md transition-all duration-300 ${activeItem?.title === item.title ? "bg-purple-100 text-purple-700" : "hover:bg-gray-100"}`}
>
<item.icon
className={`${activeItem?.title === item.title ? "text-purple-700" : "text-gray-600"}`}
/>
<span className=" font-medium">{item.title}</span>
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
</SidebarContent>
<SidebarFooter>
<NavUser user={data.user} />
</SidebarFooter>
</Sidebar>
{/* This is the second sidebar */}
{/* We disable collapsible and let it fill remaining space */}
<Sidebar
collapsible="none"
className="hidden flex-1 md:flex font-['Overpass'] bg-white"
>
<SidebarHeader className="gap-3.5 border-b p-4">
<div className="flex w-full items-center justify-between">
<div className="text-purple-700 text-base font-medium">
{activeItem?.title}
</div>
<Label className="flex items-center gap-2 text-sm ">
<span>Unreads</span>
<Switch className="shadow-none data-[state=checked]:bg-purple-600" />
</Label>
</div>
<SidebarInput
placeholder="Type to search..."
className="rounded-md border-gray-300 focus:border-purple-400 focus:ring focus:ring-purple-200 focus:ring-opacity-50 transition-all duration-300"
/>
<Separator className="my-2 bg-gray-200" />
</SidebarHeader>
<SidebarContent>
<SidebarGroup className="px-0">
<SidebarGroupContent>
{mails.map((mail, index) => (
<button
key={index}
onClick={() => onSelectMail && onSelectMail(mail)}
className={`hover:bg-purple-50 transition-all duration-300 flex flex-col items-start gap-2 border-b p-4 text-sm leading-tight whitespace-nowrap last:border-b-0 w-full text-left ${mail.email + mail.subject === selectedMailId ? "bg-purple-50" : ""}`}
>
<div className="flex w-full items-center gap-2">
<div className="h-8 w-8 rounded-md bg-gradient-to-r from-purple-500 to-pink-400 text-white mr-2 flex-shrink-0 flex items-center justify-center">
<span className=" font-medium text-sm">
{mail.name
.split(" ")
.map((n) => n[0])
.join("")}
</span>
</div>
<span className=" text-gray-700">{mail.name}</span>{" "}
<span className="ml-auto text-xs text-gray-500">
{mail.date}
</span>
</div>
<span className=" font-medium text-purple-700">
{mail.subject}
</span>
<span className="line-clamp-2 w-[260px] text-xs text-gray-600 whitespace-break-spaces">
{mail.teaser}
</span>
</button>
))}
</SidebarGroupContent>
</SidebarGroup>
</SidebarContent>
</Sidebar>
</Sidebar>
);
}

View File

@ -0,0 +1,179 @@
import type { Mail } from "./app-sidebar";
import { Button } from "@/components/ui/button";
import { ArrowLeft, Reply, Trash2, Forward, Star } from "lucide-react";
import { Separator } from "@/components/ui/separator";
import { format } from "date-fns";
interface MessageViewerProps {
mail: Mail | null;
onBack?: () => void;
}
export function MessageViewer({ mail, onBack }: MessageViewerProps) {
if (!mail) {
return (
<div className="flex flex-col items-center justify-center h-full p-6 text-center bg-gray-50 font-['Overpass']">
<div className="bg-white p-10 rounded-xl shadow-sm border border-gray-100 max-w-md">
<h2 className="text-xl font-bold text-purple-700 mb-4">
Select a message
</h2>
<p className="text-gray-600 mb-6">
Choose a message from your inbox to view its contents here.
</p>
<div className="bg-gradient-to-r from-purple-500/10 to-pink-400/10 p-4 rounded-lg">
<p className="text-sm text-gray-700">
Your messages are displayed in the sidebar. Click on any message
to open it.
</p>
</div>
</div>
</div>
);
}
const initials = mail.name
.split(" ")
.map((n) => n[0])
.join("");
const currentDate = new Date();
const formattedDate = format(currentDate, "EEEE, MMMM d, yyyy 'at' h:mm a");
return (
<div className="flex flex-col h-full overflow-hidden bg-gray-50 font-['Overpass']">
{/* Message Header */}
<div className="bg-white border-b p-4 flex items-center justify-between shadow-sm">
<div className="flex items-center gap-2">
<Button
variant="ghost"
size="icon"
onClick={onBack}
className="h-8 w-8 rounded-md hover:bg-purple-50 text-purple-700"
>
<ArrowLeft className="h-4 w-4" />
</Button>
<h2 className="text-lg font-medium text-purple-700">
{mail.subject}
</h2>
</div>
<div className="flex items-center gap-1">
<Button
variant="ghost"
size="icon"
className="h-8 w-8 rounded-md hover:bg-purple-50 text-purple-700"
>
<Reply className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
className="h-8 w-8 rounded-md hover:bg-purple-50 text-purple-700"
>
<Forward className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
className="h-8 w-8 rounded-md hover:bg-purple-50 text-purple-700"
>
<Star className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
className="h-8 w-8 rounded-md hover:bg-purple-50 text-purple-700"
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</div>
{/* Message Content */}
<div className="flex-1 overflow-auto p-6">
<div className="bg-white rounded-xl shadow-sm border border-gray-100 p-6 max-w-4xl mx-auto">
{/* Message Header */}
<div className="flex items-start gap-4 mb-6">
<div className="h-12 w-12 rounded-md bg-gradient-to-r from-purple-500 to-pink-400 text-white flex-shrink-0 flex items-center justify-center">
<span className=" font-medium">{initials}</span>
</div>
<div className="flex-1">
<div className="flex items-center justify-between">
<h3 className="text-lg font-bold">{mail.name}</h3>
<span className="text-sm text-gray-500">{mail.date}</span>
</div>
<p className="text-sm text-gray-600">{mail.email}</p>
<p className="text-xs text-gray-500 mt-1">{formattedDate}</p>
</div>
</div>
<Separator className="my-4 bg-gray-200" />
{/* Subject */}
<h1 className="text-xl font-bold text-gray-900 mb-4">
{mail.subject}
</h1>
{/* Message Body */}
<div className="prose prose-sm max-w-none text-gray-700 whitespace-pre-line">
<p className="mb-4">Hello,</p>
<p className="mb-4">{mail.teaser}</p>
<p className="mb-4">
Please let me know if you have any questions or need additional
information.
</p>
<p className="mb-4">
Best regards,
<br />
{mail.name}
</p>
</div>
{/* Attachments */}
<div className="mt-6 pt-6 border-t border-gray-100">
<h4 className=" font-medium text-gray-700 mb-3">Attachments (1)</h4>
<div className="bg-gray-50 rounded-md p-3 flex items-center gap-3 max-w-xs border border-gray-200">
<div className="h-10 w-10 bg-purple-100 rounded flex items-center justify-center">
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="text-purple-700"
>
<path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"></path>
<polyline points="14 2 14 8 20 8"></polyline>
</svg>
</div>
<div>
<p className=" text-sm font-medium text-gray-800">
Document.pdf
</p>
<p className="text-xs text-gray-500">245 KB</p>
</div>
</div>
</div>
{/* Reply Section */}
<div className="mt-6 pt-6 border-t border-gray-100">
<h4 className=" font-medium text-gray-700 mb-3">Reply</h4>
<div className="bg-gray-50 rounded-md p-4 border border-gray-200">
<textarea
className="w-full h-24 p-3 rounded-md border border-gray-300 focus:outline-none focus:ring focus:ring-purple-200 focus:border-purple-400 transition-all duration-300 font-['Overpass'] text-sm"
placeholder="Write your reply here..."
></textarea>
<div className="flex justify-end mt-3">
<Button className="bg-gradient-to-r from-purple-500 to-pink-400 hover:opacity-90 text-white transition-all duration-300 rounded-md">
<span className="">Send Reply</span>
</Button>
</div>
</div>
</div>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,37 @@
import { Moon, Sun } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { useTheme } from "./theme-provider";
export function ModeToggle() {
const { setTheme } = useTheme();
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="icon">
<Sun className="h-[1.2rem] w-[1.2rem] scale-100 rotate-0 transition-all dark:scale-0 dark:-rotate-90" />
<Moon className="absolute h-[1.2rem] w-[1.2rem] scale-0 rotate-90 transition-all dark:scale-100 dark:rotate-0" />
<span className="sr-only">Toggle theme</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => setTheme("light")}>
Light
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("dark")}>
Dark
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("system")}>
System
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}

View File

@ -0,0 +1,73 @@
"use client";
import { ChevronRight, type LucideIcon } from "lucide-react";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import {
SidebarGroup,
SidebarGroupLabel,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuSubItem,
} from "@/components/ui/sidebar";
export function NavMain({
items,
}: {
items: {
title: string;
url: string;
icon?: LucideIcon;
isActive?: boolean;
items?: {
title: string;
url: string;
}[];
}[];
}) {
return (
<SidebarGroup>
<SidebarGroupLabel>Platform</SidebarGroupLabel>
<SidebarMenu>
{items.map((item) => (
<Collapsible
key={item.title}
asChild
defaultOpen={item.isActive}
className="group/collapsible"
>
<SidebarMenuItem>
<CollapsibleTrigger asChild>
<SidebarMenuButton tooltip={item.title}>
{item.icon && <item.icon />}
<span>{item.title}</span>
<ChevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
</SidebarMenuButton>
</CollapsibleTrigger>
<CollapsibleContent>
<SidebarMenuSub>
{item.items?.map((subItem) => (
<SidebarMenuSubItem key={subItem.title}>
<SidebarMenuSubButton asChild>
<a href={subItem.url}>
<span>{subItem.title}</span>
</a>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
))}
</SidebarMenuSub>
</CollapsibleContent>
</SidebarMenuItem>
</Collapsible>
))}
</SidebarMenu>
</SidebarGroup>
);
}

View File

@ -0,0 +1,89 @@
"use client";
import {
Folder,
Forward,
MoreHorizontal,
Trash2,
type LucideIcon,
} from "lucide-react";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
SidebarGroup,
SidebarGroupLabel,
SidebarMenu,
SidebarMenuAction,
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
} from "@/components/ui/sidebar";
export function NavProjects({
projects,
}: {
projects: {
name: string;
url: string;
icon: LucideIcon;
}[];
}) {
const { isMobile } = useSidebar();
return (
<SidebarGroup className="group-data-[collapsible=icon]:hidden">
<SidebarGroupLabel>Projects</SidebarGroupLabel>
<SidebarMenu>
{projects.map((item) => (
<SidebarMenuItem key={item.name}>
<SidebarMenuButton asChild>
<a href={item.url}>
<item.icon />
<span>{item.name}</span>
</a>
</SidebarMenuButton>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<SidebarMenuAction showOnHover>
<MoreHorizontal />
<span className="sr-only">More</span>
</SidebarMenuAction>
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-48 rounded-lg"
side={isMobile ? "bottom" : "right"}
align={isMobile ? "end" : "start"}
>
<DropdownMenuItem>
<Folder className="text-muted-foreground" />
<span>View Project</span>
</DropdownMenuItem>
<DropdownMenuItem>
<Forward className="text-muted-foreground" />
<span>Share Project</span>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem>
<Trash2 className="text-muted-foreground" />
<span>Delete Project</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
))}
<SidebarMenuItem>
<SidebarMenuButton className="text-sidebar-foreground/70">
<MoreHorizontal className="text-sidebar-foreground/70" />
<span>More</span>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarGroup>
);
}

View File

@ -0,0 +1,40 @@
import * as React from "react";
import { type LucideIcon } from "lucide-react";
import {
SidebarGroup,
SidebarGroupContent,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
} from "@/components/ui/sidebar";
export function NavSecondary({
items,
...props
}: {
items: {
title: string;
url: string;
icon: LucideIcon;
}[];
} & React.ComponentPropsWithoutRef<typeof SidebarGroup>) {
return (
<SidebarGroup {...props}>
<SidebarGroupContent>
<SidebarMenu>
{items.map((item) => (
<SidebarMenuItem key={item.title}>
<SidebarMenuButton asChild size="sm">
<a href={item.url}>
<item.icon />
<span>{item.title}</span>
</a>
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
);
}

View File

@ -0,0 +1,123 @@
"use client";
import {
BadgeCheck,
Bell,
ChevronsUpDown,
CreditCard,
LogOut,
Sparkles,
} from "lucide-react";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
} from "@/components/ui/sidebar";
export function NavUser({
user,
}: {
user: {
name: string;
email: string;
avatar: string;
};
}) {
const { isMobile } = useSidebar();
return (
<SidebarMenu>
<SidebarMenuItem>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<SidebarMenuButton
size="lg"
className="data-[state=open]:bg-purple-50 data-[state=open]:text-purple-700 md:h-8 md:p-0 hover:bg-purple-50 transition-all duration-300"
>
<Avatar className="h-8 w-8 rounded-lg bg-gradient-to-r from-purple-500 to-pink-400 text-white">
<AvatarImage src={user.avatar} alt={user.name} />
<AvatarFallback className="rounded-lg font-medium">
{user.name.substring(0, 2).toUpperCase()}
</AvatarFallback>
</Avatar>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-medium text-purple-700">
{user.name}
</span>
<span className="truncate text-xs text-gray-600">
{user.email}
</span>
</div>
<ChevronsUpDown className="ml-auto size-4 text-purple-600" />
</SidebarMenuButton>
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-xl border-0 shadow-lg overflow-hidden font-['Overpass']"
side={isMobile ? "bottom" : "right"}
align="end"
sideOffset={4}
>
<div className="h-1 bg-gradient-to-r from-purple-500 to-pink-400 w-full"></div>
<DropdownMenuLabel className="p-0 font-normal">
<div className="flex items-center gap-2 px-3 py-3 text-left text-sm">
<Avatar className="h-10 w-10 rounded-lg bg-gradient-to-r from-purple-500 to-pink-400 text-white">
<AvatarImage src={user.avatar} alt={user.name} />
<AvatarFallback className="rounded-lg font-medium">
{user.name.substring(0, 2).toUpperCase()}
</AvatarFallback>
</Avatar>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-medium text-purple-700">
{user.name}
</span>
<span className="truncate text-xs text-gray-600">
{user.email}
</span>
</div>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem className="focus:bg-purple-50 focus:text-purple-700">
<Sparkles className="text-purple-600" />
<span className=" ml-2">Upgrade to Pro</span>
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem className="focus:bg-purple-50 focus:text-purple-700">
<BadgeCheck className="text-purple-600" />
<span className=" ml-2">Account</span>
</DropdownMenuItem>
<DropdownMenuItem className="focus:bg-purple-50 focus:text-purple-700">
<CreditCard className="text-purple-600" />
<span className=" ml-2">Billing</span>
</DropdownMenuItem>
<DropdownMenuItem className="focus:bg-purple-50 focus:text-purple-700">
<Bell className="text-purple-600" />
<span className=" ml-2">Notifications</span>
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuItem className="focus:bg-purple-50 focus:text-purple-700">
<LogOut className="text-purple-600" />
<span className=" ml-2">Log out</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
</SidebarMenu>
);
}

View File

@ -0,0 +1,91 @@
"use client";
import * as React from "react";
import { ChevronsUpDown, Plus } from "lucide-react";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
} from "@/components/ui/sidebar";
export function TeamSwitcher({
teams,
}: {
teams: {
name: string;
logo: React.ElementType;
plan: string;
}[];
}) {
const { isMobile } = useSidebar();
const [activeTeam, setActiveTeam] = React.useState(teams[0]);
if (!activeTeam) {
return null;
}
return (
<SidebarMenu>
<SidebarMenuItem>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<SidebarMenuButton
size="lg"
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
>
<div className="bg-sidebar-primary text-sidebar-primary-foreground flex aspect-square size-8 items-center justify-center rounded-lg">
<activeTeam.logo className="size-4" />
</div>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-medium">{activeTeam.name}</span>
<span className="truncate text-xs">{activeTeam.plan}</span>
</div>
<ChevronsUpDown className="ml-auto" />
</SidebarMenuButton>
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg"
align="start"
side={isMobile ? "bottom" : "right"}
sideOffset={4}
>
<DropdownMenuLabel className="text-muted-foreground text-xs">
Teams
</DropdownMenuLabel>
{teams.map((team, index) => (
<DropdownMenuItem
key={team.name}
onClick={() => setActiveTeam(team)}
className="gap-2 p-2"
>
<div className="flex size-6 items-center justify-center rounded-md border">
<team.logo className="size-3.5 shrink-0" />
</div>
{team.name}
<DropdownMenuShortcut>{index + 1}</DropdownMenuShortcut>
</DropdownMenuItem>
))}
<DropdownMenuSeparator />
<DropdownMenuItem className="gap-2 p-2">
<div className="flex size-6 items-center justify-center rounded-md border bg-transparent">
<Plus className="size-4" />
</div>
<div className="text-muted-foreground font-medium">Add team</div>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
</SidebarMenu>
);
}

View File

@ -0,0 +1,73 @@
import { createContext, useContext, useEffect, useState } from "react";
type Theme = "dark" | "light" | "system";
type ThemeProviderProps = {
children: React.ReactNode;
defaultTheme?: Theme;
storageKey?: string;
};
type ThemeProviderState = {
theme: Theme;
setTheme: (theme: Theme) => void;
};
const initialState: ThemeProviderState = {
theme: "system",
setTheme: () => null,
};
const ThemeProviderContext = createContext<ThemeProviderState>(initialState);
export function ThemeProvider({
children,
defaultTheme = "system",
storageKey = "vite-ui-theme",
...props
}: ThemeProviderProps) {
const [theme, setTheme] = useState<Theme>(
() => (localStorage.getItem(storageKey) as Theme) || defaultTheme
);
useEffect(() => {
const root = window.document.documentElement;
root.classList.remove("light", "dark");
if (theme === "system") {
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
.matches
? "dark"
: "light";
root.classList.add(systemTheme);
return;
}
root.classList.add(theme);
}, [theme]);
const value = {
theme,
setTheme: (theme: Theme) => {
localStorage.setItem(storageKey, theme);
setTheme(theme);
},
};
return (
<ThemeProviderContext.Provider {...props} value={value}>
{children}
</ThemeProviderContext.Provider>
);
}
export const useTheme = () => {
const context = useContext(ThemeProviderContext);
if (context === undefined)
throw new Error("useTheme must be used within a ThemeProvider");
return context;
};

View File

@ -0,0 +1,176 @@
import React from "react";
import { AppSidebar } from "@/features/dashboard/components/app-sidebar";
import type { Mail } from "@/features/dashboard/components/app-sidebar";
import { MessageViewer } from "@/features/dashboard/components/message-viewer";
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from "@/components/ui/breadcrumb";
import { Separator } from "@/components/ui/separator";
import {
SidebarInset,
SidebarProvider,
SidebarTrigger,
} from "@/components/ui/sidebar";
import { Button } from "@/components/ui/button";
export function Admin() {
// State for content tabs and selected mail
const [activeTab, setActiveTab] = React.useState("overview");
const [selectedMail, setSelectedMail] = React.useState<Mail | null>(null);
const [showMailView, setShowMailView] = React.useState(false);
return (
<SidebarProvider
style={
{
"--sidebar-width": "350px",
} as React.CSSProperties
}
>
<AppSidebar
onSelectMail={(mail) => {
setSelectedMail(mail);
setShowMailView(true);
}}
selectedMailId={
selectedMail ? selectedMail.email + selectedMail.subject : undefined
}
/>
<SidebarInset>
<header className="bg-white sticky top-0 flex shrink-0 items-center gap-2 border-b p-4 shadow-sm">
<SidebarTrigger className="-ml-1 text-purple-700 hover:bg-purple-50 transition-all duration-300 rounded-md" />
<Separator
orientation="vertical"
className="mr-2 data-[orientation=vertical]:h-4 bg-gray-200"
/>
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem className="hidden md:block">
<BreadcrumbLink
href="#"
className="text-gray-600 hover:text-purple-700 transition-all duration-300 "
>
Dashboard
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator className="hidden md:block text-gray-400" />
<BreadcrumbItem>
<BreadcrumbPage className="text-purple-700 font-medium">
Messages
</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
<div className="ml-auto flex items-center gap-2">
<Button className="bg-gradient-to-r from-purple-500 to-pink-400 hover:opacity-90 text-white text-sm rounded-md transition-all duration-300">
<span className="">New Message</span>
</Button>
</div>
</header>
{showMailView ? (
<MessageViewer
mail={selectedMail}
onBack={() => {
setShowMailView(false);
setSelectedMail(null);
}}
/>
) : (
<div className="flex flex-1 flex-col gap-4 p-6 bg-gray-50 font-['Overpass']">
<div className="bg-white p-4 rounded-xl shadow-sm border border-gray-100">
<h1 className="text-xl font-bold text-purple-700 mb-4">
Welcome to Serenity Space Dashboard
</h1>
<div className="flex mb-6 border-b">
{["Overview", "Messages", "Calendar", "Tasks", "Analytics"].map(
(tab) => {
const tabKey = tab.toLowerCase();
const isActive = activeTab === tabKey;
return (
<button
key={tab}
onClick={() => {
setActiveTab(tabKey);
if (tabKey === "messages" && !showMailView) {
// Show empty message state if Messages tab is selected
setShowMailView(true);
}
}}
className={`px-4 py-2 transition-all duration-300 ${
isActive
? "text-purple-700 border-b-2 border-purple-700 font-medium"
: "text-gray-600 hover:text-purple-600"
}`}
>
{tab}
</button>
);
}
)}
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
{[
"Upcoming Sessions",
"Recent Messages",
"Task Completion",
].map((card, i) => (
<div
key={card}
className="bg-white border border-gray-100 p-4 rounded-lg shadow-sm hover:shadow-md transition-all duration-300"
>
<h3 className="text-base font-medium text-purple-700 mb-2">
{card}
</h3>
<p className="text-gray-600 text-sm mb-3">
You have {i + 2} new items to review.
</p>
<div className="flex justify-between items-center">
<span className="text-xs text-gray-500">
Last updated: Today
</span>
<Button
variant="outline"
size="sm"
className="text-xs border-gray-200 hover:bg-purple-50 hover:text-purple-700 transition-all duration-300"
onClick={() => {
if (card === "Recent Messages") {
// Open messages view when clicking on Recent Messages card
setActiveTab("messages");
setShowMailView(true);
}
}}
>
View All
</Button>
</div>
</div>
))}
</div>
<div className="bg-gradient-to-r from-purple-500/10 to-pink-400/10 rounded-xl p-6 border border-purple-100">
<h2 className=" font-bold text-lg text-purple-700 mb-2">
Enhance Your Serenity Journey
</h2>
<p className="text-gray-700 mb-4">
Discover premium features to better support your clients and
grow your practice.
</p>
<Button className="bg-gradient-to-r from-purple-500 to-pink-400 hover:opacity-90 text-white transition-all duration-300 rounded-md">
Explore Premium Features
</Button>
</div>
</div>
</div>
)}
</SidebarInset>
</SidebarProvider>
);
}

View File

@ -0,0 +1,10 @@
export * from "./sections/HeroSection";
export * from "./sections/AboutSection";
export * from "./sections/ServiceSection";
export * from "./sections/GallerySection";
export * from "./sections/ParticipateSection";
export * from "./sections/FeesSection";
export * from "./sections/TeamSection";
export * from "./sections/ContactSection";
export * from "./sections/FaqSection";
export * from "./sections/NewsletterSection";

View File

@ -0,0 +1,190 @@
import womenWithCat from "@/assets/professional-pet-sitter-with-client-cat.png";
export const AboutSection = () => {
return (
<section id="about" className="py-20 bg-gray-50 relative overflow-hidden">
{/* Background decoration circles */}
<div className="absolute top-40 left-0 w-64 h-64 bg-gradient-to-br from-purple-200 to-pink-100 rounded-full opacity-20 -translate-x-1/2"></div>
<div className="absolute bottom-20 right-0 w-80 h-80 bg-gradient-to-br from-purple-200 to-pink-100 rounded-full opacity-30 translate-x-1/3"></div>
<div className="container mx-auto px-6 relative">
<div className="text-center mb-16">
<h2 className="text-3xl md:text-4xl font-bold mb-4 text-gray-800 ">
About Serenity Space
</h2>
<div className="h-1 w-32 bg-gradient-to-r from-purple-500 to-pink-400 rounded-full mx-auto"></div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-16 items-start">
<div className="relative inline-block">
<div className="bg-gradient-to-r from-purple-500 to-pink-400 absolute -top-4 -left-4 right-4 bottom-4 rounded-xl opacity-30 transform -rotate-2"></div>
<div className="bg-white p-6 rounded-xl shadow-xl relative z-10">
<div className="w-full rounded-lg mb-6 overflow-hidden max-h-[80vh]">
<img
src={womenWithCat}
alt="Peaceful garden with seating area"
className="w-full h-full object-scale-down"
/>
</div>
<div className="bg-gradient-to-r from-purple-500 to-pink-400 h-1 w-24 rounded-full mb-4"></div>
<div className="flex flex-wrap gap-4">
<div className="flex items-center gap-2 bg-purple-50 px-4 py-2 rounded-full">
<i className="fas fa-heart text-purple-500"></i>
<span className="text-purple-700 font-medium">
Compassionate Care
</span>
</div>
<div className="flex items-center gap-2 bg-pink-50 px-4 py-2 rounded-full">
<i className="fas fa-paw text-pink-500"></i>
<span className="text-pink-700 font-medium">
Animal Therapy
</span>
</div>
<div className="flex items-center gap-2 bg-purple-50 px-4 py-2 rounded-full">
<i className="fas fa-brain text-purple-500"></i>
<span className="text-purple-700 font-medium">
Neurodiversity Affirming
</span>
</div>
</div>
</div>
</div>
<div>
<div className="mb-8">
<h3 className="text-2xl font-bold text-gray-800 mb-3 ">
Our Mission
</h3>
<p className="text-gray-600 mb-4">
To provide care and support based on people's needs to help find
their serenity in a safe space.
</p>
</div>
<div>
<h3 className="text-2xl font-bold text-gray-800 mb-3 ">
Our Values
</h3>
<ul className="text-gray-600 space-y-2 mb-4">
<li className="flex items-start gap-2">
<div className="h-6 w-6 rounded-full bg-gradient-to-r from-purple-500 to-pink-400 flex-shrink-0 flex items-center justify-center text-white text-sm mt-0.5">
</div>
<span>
<strong className="text-gray-700 ">
Honesty and Integrity
</strong>
</span>
</li>
<li className="flex items-start gap-2">
<div className="h-6 w-6 rounded-full bg-gradient-to-r from-purple-500 to-pink-400 flex-shrink-0 flex items-center justify-center text-white text-sm mt-0.5">
</div>
<span>
<strong className="text-gray-700 ">Accountability</strong>
</span>
</li>
<li className="flex items-start gap-2">
<div className="h-6 w-6 rounded-full bg-gradient-to-r from-purple-500 to-pink-400 flex-shrink-0 flex items-center justify-center text-white text-sm mt-0.5">
</div>
<span>
<strong className="text-gray-700 ">
Respect and professionalism
</strong>
</span>
</li>
<li className="flex items-start gap-2">
<div className="h-6 w-6 rounded-full bg-gradient-to-r from-purple-500 to-pink-400 flex-shrink-0 flex items-center justify-center text-white text-sm mt-0.5">
</div>
<span>
<strong className="text-gray-700 ">Empowerment</strong>
</span>
</li>
<li className="flex items-start gap-2">
<div className="h-6 w-6 rounded-full bg-gradient-to-r from-purple-500 to-pink-400 flex-shrink-0 flex items-center justify-center text-white text-sm mt-0.5">
</div>
<span>
<strong className="text-gray-700 ">
Dependability and responsibility
</strong>
</span>
</li>
<li className="flex items-start gap-2">
<div className="h-6 w-6 rounded-full bg-gradient-to-r from-purple-500 to-pink-400 flex-shrink-0 flex items-center justify-center text-white text-sm mt-0.5">
</div>
<span>
<strong className="text-gray-700 ">Adaptability</strong>
</span>
</li>
</ul>
<div className="mt-8">
<h3 className="text-2xl font-bold text-gray-800 mb-3 ">
How we work
</h3>
<div className="space-y-4">
<p className="text-gray-600">
We support and encourage self-directed goals and requests
for experiences. We are committed to a person-centred
strengths-based approach ensuring children, young people and
vulnerable adults are at the centre of every decision made.
We work with you to improve your quality of life and
encourage your personal growth and self-empowerment. We
provide hands-on support to persons to achieve their
serenity in a safe and comfortable space throughout the
community and onsite.
</p>
<p className="text-gray-600">
We believe every individual has the right to be safe and
respected; we recognise and upholds the dignity and rights
of all children, young people and vulnerable adults. We
uphold and maintain the highest standards of professional
conduct and will not behave in a manner that is harmful when
working with children, young people, and vulnerable adults.
</p>
<p className="text-gray-600">
We are a dedicated team of professionals with a skills base
spanning health and safety, community services, support and
care work, early education, management and much more. We
have personal experience or caring and supporting experience
with mental health, physical health, disabilities and
vulnerability. We're about making your life better and
helping you achieve your goals and building your skills to
achieve serenity in any space available.
</p>
<div className="bg-purple-50 rounded-lg p-4 mt-4">
<p className="text-purple-700">
While the meaning can vary from person to person, the
practice of serenity can make a big difference. Serenity
can help you find inner peace, enjoy a less chaotic
lifestyle and throw away the things that hindered you in
the past. Your life is beautiful, and through serenity and
skill based development, you can reclaim control over all
situations.
</p>
</div>
<div className="bg-gradient-to-r from-purple-500 to-pink-400 text-white p-4 rounded-lg mt-2">
<p>
The definition of serenity is a state of being calm,
peaceful and untroubled. Achieving this positive state of
mind means you won't feel as troubled by life's ups and
downs; serenity will help you stay calm and true to
yourself.
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
);
};

View File

@ -0,0 +1,214 @@
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
export const ContactSection = () => {
return (
<section
id="contact"
className="py-24 bg-gradient-to-b from-white to-purple-50"
>
<div className="container mx-auto px-6">
<div className="flex flex-col lg:flex-row gap-16">
<div className="lg:w-1/2">
<h2 className="text-3xl md:text-4xl font-bold text-gray-800 mb-6 ">
Contact Us
</h2>
<p className="text-gray-600 mb-8 leading-relaxed">
We're here to support you with compassion and understanding. Reach
out to learn more about our services or to schedule your initial
consultation.
</p>
<div className="space-y-6 mb-8">
<div className="flex items-start gap-4">
<div className="w-10 h-10 rounded-full bg-purple-100 flex items-center justify-center flex-shrink-0">
<i className="fas fa-map-marker-alt text-purple-600"></i>
</div>
<div>
<h3 className="text-lg font-semibold text-gray-800 ">
Location
</h3>
<p className="text-gray-600">
42 Brisbane Street, Ipswich, QLD 4305, Australia
</p>
</div>
</div>
<div className="flex items-start gap-4">
<div className="w-10 h-10 rounded-full bg-purple-100 flex items-center justify-center flex-shrink-0">
<i className="fas fa-phone-alt text-purple-600"></i>
</div>
<div>
<h3 className="text-lg font-semibold text-gray-800 ">
Phone
</h3>
<p className="text-gray-600">(555) 123-4567</p>
</div>
</div>
<div className="flex items-start gap-4">
<div className="w-10 h-10 rounded-full bg-purple-100 flex items-center justify-center flex-shrink-0">
<i className="fas fa-envelope text-purple-600"></i>
</div>
<div>
<h3 className="text-lg font-semibold text-gray-800 ">
Email
</h3>
<p className="text-gray-600">connect@serenityspace.com</p>
</div>
</div>
<div className="flex items-start gap-4">
<div className="w-10 h-10 rounded-full bg-purple-100 flex items-center justify-center flex-shrink-0">
<i className="fas fa-clock text-purple-600"></i>
</div>
<div>
<h3 className="text-lg font-semibold text-gray-800 ">
Hours
</h3>
<p className="text-gray-600">
Monday - Friday: 9am - 6pm
<br />
Saturday: 10am - 2pm
</p>
</div>
</div>
</div>
<div className="flex gap-4 mb-8">
<a
href="#"
className="w-10 h-10 rounded-full bg-purple-600 flex items-center justify-center text-white hover:bg-purple-700 transition-colors duration-300 cursor-pointer"
>
<i className="fab fa-facebook-f"></i>
</a>
<a
href="#"
className="w-10 h-10 rounded-full bg-purple-600 flex items-center justify-center text-white hover:bg-purple-700 transition-colors duration-300 cursor-pointer"
>
<i className="fab fa-instagram"></i>
</a>
<a
href="#"
className="w-10 h-10 rounded-full bg-purple-600 flex items-center justify-center text-white hover:bg-purple-700 transition-colors duration-300 cursor-pointer"
>
<i className="fab fa-twitter"></i>
</a>
</div>
<div className="mt-6 w-full rounded-lg overflow-hidden shadow-md h-[300px]">
<iframe
src="https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d56496.26842460963!2d152.75956799999998!3d-27.627821!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x6b96b5cb23cec917%3A0x502a35af3de8c10!2sIpswich%20QLD%204305%2C%20Australia!5e0!3m2!1sen!2sus!4v1655071788026!5m2!1sen!2sus"
width="100%"
height="100%"
style={{ border: 0 }}
allowFullScreen
loading="lazy"
referrerPolicy="no-referrer-when-downgrade"
title="Ipswich QLD Australia Map"
aria-label="Location map showing Ipswich QLD, Australia"
></iframe>
</div>
</div>
<div className="lg:w-1/2">
<Card className="bg-white shadow-md">
<CardHeader>
<CardTitle className="text-2xl text-gray-800">
Get in Touch
</CardTitle>
<CardDescription>
Fill out the form below and we'll respond within 24 hours.
</CardDescription>
</CardHeader>
<CardContent>
<form className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<label
htmlFor="name"
className="text-sm font-medium text-gray-700"
>
Name
</label>
<Input
id="name"
placeholder="Your name"
className="border-gray-200 focus:border-purple-500"
/>
</div>
<div className="space-y-2">
<label
htmlFor="email"
className="text-sm font-medium text-gray-700"
>
Email
</label>
<Input
id="email"
type="email"
placeholder="Your email"
className="border-gray-200 focus:border-purple-500"
/>
</div>
</div>
<div className="space-y-2">
<label
htmlFor="phone"
className="text-sm font-medium text-gray-700"
>
Phone
</label>
<Input
id="phone"
placeholder="Your phone number"
className="border-gray-200 focus:border-purple-500"
/>
</div>
<div className="space-y-2">
<label
htmlFor="service"
className="text-sm font-medium text-gray-700"
>
Service of Interest
</label>
<select
id="service"
className="w-full rounded-md border border-gray-200 py-2 px-3 text-gray-700 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent"
>
<option value="">Select a service</option>
<option value="animal">Animal-Assisted Therapy</option>
<option value="talk">Talk Therapy</option>
<option value="relaxation">Relaxation Techniques</option>
<option value="support">General Support Work</option>
</select>
</div>
<div className="space-y-2">
<label
htmlFor="message"
className="text-sm font-medium text-gray-700"
>
Message
</label>
<textarea
id="message"
rows={4}
placeholder="How can we help you?"
className="w-full rounded-md border border-gray-200 py-2 px-3 text-gray-700 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent resize-none"
></textarea>
</div>
</form>
</CardContent>
<CardFooter>
<Button className="w-full bg-gradient-to-r from-purple-500 to-pink-400 hover:opacity-90 transition-all duration-300 !rounded-button whitespace-nowrap cursor-pointer">
Send Message
</Button>
</CardFooter>
</Card>
</div>
</div>
</div>
</section>
);
};

View File

@ -0,0 +1,150 @@
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
export const FaqSection = () => {
return (
<section className="py-24 bg-white">
<div className="container mx-auto px-6">
<div className="text-center mb-16">
<h2 className="text-3xl md:text-4xl font-bold text-gray-800 mb-4 ">
Frequently Asked Questions
</h2>
<p className="text-gray-600 max-w-2xl mx-auto">
Find answers to common questions about our therapeutic approach and
services.
</p>
</div>
<div className="max-w-3xl mx-auto">
<Tabs defaultValue="general" className="w-full">
<TabsList className="grid w-full grid-cols-3 mb-8">
<TabsTrigger
value="general"
className="!rounded-button whitespace-nowrap cursor-pointer"
>
General
</TabsTrigger>
<TabsTrigger
value="services"
className="!rounded-button whitespace-nowrap cursor-pointer"
>
Services
</TabsTrigger>
<TabsTrigger
value="animals"
className="!rounded-button whitespace-nowrap cursor-pointer"
>
Our Animals
</TabsTrigger>
</TabsList>
<TabsContent value="general" className="space-y-4">
<div className="bg-purple-50 rounded-lg p-6">
<h3 className="text-lg font-semibold text-gray-800 mb-2">
Who can benefit from your services?
</h3>
<p className="text-gray-600">
Our services are designed for teenagers and adults with
autism, ASD, ADHD, and other disabilities who benefit from a
gentle, supportive approach. We also work with individuals
experiencing anxiety, depression, and those who simply need
emotional support.
</p>
</div>
<div className="bg-purple-50 rounded-lg p-6">
<h3 className="text-lg font-semibold text-gray-800 mb-2">
How long are the sessions?
</h3>
<p className="text-gray-600">
Standard sessions are 50 minutes, but we offer flexibility
based on individual needs. Some clients, particularly those
new to therapy or with specific sensory needs, may benefit
from shorter 30-minute sessions.
</p>
</div>
<div className="bg-purple-50 rounded-lg p-6">
<h3 className="text-lg font-semibold text-gray-800 mb-2">
Do you accept insurance?
</h3>
<p className="text-gray-600">
We currently operate on a private-pay basis, but can provide
documentation for you to submit to your insurance for
potential reimbursement. We also offer sliding scale fees for
those with financial need.
</p>
</div>
</TabsContent>
<TabsContent value="services" className="space-y-4">
<div className="bg-pink-50 rounded-lg p-6">
<h3 className="text-lg font-semibold text-gray-800 mb-2">
What happens in an animal-assisted therapy session?
</h3>
<p className="text-gray-600">
During animal-assisted therapy, our trained therapy animals
participate in structured therapeutic activities. This might
include gentle interaction, grooming, or simply being present
while you talk. The animals provide emotional support and can
help facilitate communication and emotional expression.
</p>
</div>
<div className="bg-pink-50 rounded-lg p-6">
<h3 className="text-lg font-semibold text-gray-800 mb-2">
Can I request a specific therapy animal?
</h3>
<p className="text-gray-600">
Yes, you can request to work with a specific therapy animal.
We'll do our best to accommodate your preference, while also
considering which animal might be the best therapeutic match
for your needs.
</p>
</div>
<div className="bg-pink-50 rounded-lg p-6">
<h3 className="text-lg font-semibold text-gray-800 mb-2">
What relaxation techniques do you teach?
</h3>
<p className="text-gray-600">
We offer a variety of relaxation techniques including deep
breathing, progressive muscle relaxation, guided imagery,
mindfulness practices, and sensory-based calming strategies.
These are customized to your preferences and needs.
</p>
</div>
</TabsContent>
<TabsContent value="animals" className="space-y-4">
<div className="bg-purple-50 rounded-lg p-6">
<h3 className="text-lg font-semibold text-gray-800 mb-2">
Are the therapy animals specially trained?
</h3>
<p className="text-gray-600">
Yes, all our therapy animals undergo extensive training and
certification. They are selected for their calm temperaments
and are trained to interact safely and comfortably with
clients in a therapeutic setting.
</p>
</div>
<div className="bg-purple-50 rounded-lg p-6">
<h3 className="text-lg font-semibold text-gray-800 mb-2">
What if I'm allergic to certain animals?
</h3>
<p className="text-gray-600">
We have a variety of therapy animals and can work with you to
find an animal that doesn't trigger allergies. We also offer
therapy without animal assistance if needed.
</p>
</div>
<div className="bg-purple-50 rounded-lg p-6">
<h3 className="text-lg font-semibold text-gray-800 mb-2">
How do the animals help with therapy?
</h3>
<p className="text-gray-600">
Therapy animals provide unconditional acceptance and can
reduce anxiety, lower blood pressure, and release endorphins.
They also serve as a bridge for communication, helping clients
express emotions and develop social skills in a
non-threatening environment.
</p>
</div>
</TabsContent>
</Tabs>
</div>
</div>
</section>
);
};

View File

@ -0,0 +1,150 @@
import { Button } from "@/components/ui/button";
export const FeesSection = () => {
return (
<section id="fees" className="py-20 bg-gray-50 relative overflow-hidden">
<div className="container mx-auto px-6 relative">
<div className="text-center mb-16">
<h2 className="text-3xl md:text-4xl font-bold mb-4 text-gray-800 ">
Schedule of Fees
</h2>
<div className="h-1 w-24 bg-gradient-to-r from-purple-500 to-pink-400 rounded-full mx-auto mb-6"></div>
<p className="text-gray-600 max-w-3xl mx-auto">
We believe in transparent pricing and strive to make our services
accessible to all who need them.
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{/* Standard Fee Card */}
<div className="bg-white rounded-xl shadow-lg overflow-hidden border-0 transition-transform duration-300 hover:-translate-y-1">
<div className="h-2 bg-gradient-to-r from-purple-500 to-pink-400"></div>
<div className="p-6">
<h3 className="text-xl font-bold text-gray-800 mb-4 ">
Standard Session Fees
</h3>
<ul className="space-y-4">
<li className="flex justify-between">
<span className="text-gray-600">Initial Consultation</span>
<span className="font-semibold text-gray-800">Free</span>
</li>
<li className="flex justify-between">
<span className="text-gray-600">
Individual Therapy (50 min)
</span>
<span className="font-semibold text-gray-800">$120</span>
</li>
<li className="flex justify-between">
<span className="text-gray-600">
Couples Therapy (80 min)
</span>
<span className="font-semibold text-gray-800">$150</span>
</li>
<li className="flex justify-between">
<span className="text-gray-600">Group Session (90 min)</span>
<span className="font-semibold text-gray-800">$45</span>
</li>
<li className="flex justify-between">
<span className="text-gray-600">Art Therapy (75 min)</span>
<span className="font-semibold text-gray-800">$95</span>
</li>
<li className="flex justify-between">
<span className="text-gray-600">
Mindfulness Training (60 min)
</span>
<span className="font-semibold text-gray-800">$85</span>
</li>
</ul>
</div>
</div>
{/* Insurance Card */}
<div className="bg-white rounded-xl shadow-lg overflow-hidden border-0 transition-transform duration-300 hover:-translate-y-1">
<div className="h-2 bg-gradient-to-r from-purple-500 to-pink-400"></div>
<div className="p-6">
<h3 className="text-xl font-bold text-gray-800 mb-4 ">
Insurance Coverage
</h3>
<p className="text-gray-600 mb-6">
We accept most major insurance plans. Your out-of-pocket cost
will depend on your specific coverage.
</p>
<p className="text-gray-700 font-medium mb-4">
Accepted Insurance Plans:
</p>
<ul className="space-y-2">
<li className="flex items-center">
<i className="fas fa-check text-purple-500 mr-2"></i>
<span className="text-gray-600">Blue Cross Blue Shield</span>
</li>
<li className="flex items-center">
<i className="fas fa-check text-purple-500 mr-2"></i>
<span className="text-gray-600">Aetna</span>
</li>
<li className="flex items-center">
<i className="fas fa-check text-purple-500 mr-2"></i>
<span className="text-gray-600">United Healthcare</span>
</li>
<li className="flex items-center">
<i className="fas fa-check text-purple-500 mr-2"></i>
<span className="text-gray-600">Cigna</span>
</li>
<li className="flex items-center">
<i className="fas fa-check text-purple-500 mr-2"></i>
<span className="text-gray-600">Medicare</span>
</li>
</ul>
</div>
</div>
{/* Sliding Scale Card */}
<div className="bg-white rounded-xl shadow-lg overflow-hidden border-0 transition-transform duration-300 hover:-translate-y-1">
<div className="h-2 bg-gradient-to-r from-purple-500 to-pink-400"></div>
<div className="p-6">
<h3 className="text-xl font-bold text-gray-800 mb-4 ">
Financial Assistance
</h3>
<p className="text-gray-600 mb-6">
We offer sliding scale fees based on financial need to ensure
everyone can access the care they deserve.
</p>
<div className="bg-purple-50 rounded-lg p-4 mb-4">
<h4 className="text-purple-700 font-medium mb-2">
Sliding Scale Range:
</h4>
<p className="text-gray-700">$50-$100 per session</p>
</div>
<p className="text-gray-600 mb-4">
To apply for our sliding scale program, please provide:
</p>
<ul className="space-y-2">
<li className="flex items-start">
<i className="fas fa-circle text-xs text-purple-400 mt-1.5 mr-2"></i>
<span className="text-gray-600">Proof of income</span>
</li>
<li className="flex items-start">
<i className="fas fa-circle text-xs text-purple-400 mt-1.5 mr-2"></i>
<span className="text-gray-600">
Brief financial statement
</span>
</li>
<li className="flex items-start">
<i className="fas fa-circle text-xs text-purple-400 mt-1.5 mr-2"></i>
<span className="text-gray-600">
Completed application form
</span>
</li>
</ul>
</div>
</div>
</div>
<div className="mt-12 text-center">
<Button className="bg-gradient-to-r from-purple-500 to-pink-400 hover:opacity-90 transition-all duration-300 !rounded-button">
Download Fee Schedule
</Button>
</div>
</div>
</section>
);
};

View File

@ -0,0 +1,136 @@
// FullGalleryPage.tsx
import { Link } from "react-router-dom";
import { ArrowLeft } from "lucide-react";
const galleryItems = [
{
image: "src/assets/gallery/counselling-session.png",
featured: true,
title:
"One-on-one counselling session between a therapist and client in a calm, private setting",
},
{
image: "src/assets/gallery/group-support-therapy.png",
featured: true,
title:
"A supportive group therapy session with people sharing and listening in a circle",
},
{
image: "src/assets/gallery/skill-development-session.png",
title:
"A client and coach discussing personal goals and skills using interactive tools",
},
{
image: "src/assets/gallery/life-recovery-coaching.png",
title:
"A recovery coaching session focused on emotional healing and future goals",
},
{
image: "src/assets/gallery/additional-therapies-room.png",
featured: true,
title:
"A therapy space featuring alternative healing options like art and music.",
},
{
image: "src/assets/gallery/individual-therapy.png",
title:
"A person in a solo therapy session practicing reflection or journaling",
},
{
image: "src/assets/gallery/family-therapy-session.png",
title:
"A family therapy session focusing on communication and emotional connection",
},
{
image: "src/assets/gallery/couples-counselling.png",
title:
"A couples counselling session encouraging healthy conversation and trust.",
},
{
image: "src/assets/gallery/small-group-therapy.png",
title:
"A small group therapy session with participants sharing experiences and support.",
},
{
image: "src/assets/gallery/therapy-travel-experience.png",
title:
"A group therapy experience taking place outdoors during a healing journey.",
},
{
image: "src/assets/gallery/peer-to-peer-mentoring.png",
title:
"Two peers sharing advice and encouragement in a relaxed mentoring session.",
},
{
image: "src/assets/gallery/mentoring-session.png",
title:
"A mentorship session focused on personal growth and long-term development.",
},
{
image: "src/assets/gallery/employment-development-coaching.png",
title: "A career support session helping a person prepare for employment.",
},
{
image: "src/assets/gallery/animal-assisted-therapy.png",
title:
"A therapy session with various calming animals providing emotional support.",
},
];
export const FullGalleryPage = () => {
return (
<div className="min-h-screen bg-white">
{/* Gallery Header */}
<div className="sticky top-0 z-10 bg-white/80 backdrop-blur-sm border-b">
<div className="container mx-auto px-6 py-4 flex justify-between items-center">
<Link
to="/"
className="flex items-center text-gray-800 hover:text-purple-500 transition-colors"
>
<ArrowLeft className="mr-2 h-5 w-5" />
<span className="font-medium">Back</span>
</Link>
<div className="text-center">
<h1 className="text-xl font-bold text-gray-800">
Therapeutic Spaces
</h1>
<p className="text-xs text-gray-500">
Alternative Healing Environments
</p>
</div>
<div className="w-10"></div>
</div>
</div>
{/* Tight Bento Grid */}
<div className="container mx-auto px-4 py-8">
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3">
{" "}
{/* Reduced gap */}
{galleryItems.map((item, index) => (
<div
key={index}
className={`
group relative overflow-hidden rounded-lg
${index === 0 ? "md:col-span-2 md:row-span-2" : ""}
${index === 3 ? "lg:col-span-2" : ""}
`}
>
<img
src={item.image}
alt={item.title}
className="w-full h-full object-cover transition-transform duration-500 group-hover:scale-105"
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/50 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300 flex flex-col justify-end p-4">
<h3 className="text-white font-medium">{item.title}</h3>
</div>
</div>
))}
</div>
</div>
{/* Footer */}
<div className="bg-gray-50 border-t mt-8 py-6"></div>
</div>
);
};

View File

@ -0,0 +1,72 @@
import { Button } from "@/components/ui/button";
import { Link } from "react-router-dom";
export const GallerySection = () => {
return (
<section id="gallery" className="py-20 bg-gray-50">
<div className="container mx-auto px-6">
<div className="text-center mb-16">
<h2 className="text-3xl md:text-4xl font-bold mb-4 text-gray-800 ">
Our Gallery
</h2>
<div className="h-1 w-24 bg-gradient-to-r from-purple-500 to-pink-400 rounded-full mx-auto mb-6"></div>
<p className="text-gray-600 max-w-3xl mx-auto">
Take a virtual tour of our healing spaces and get a glimpse of the
Serenity Space experience.
</p>
</div>
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
{[
{
image: "src/assets/gallery/counselling-session.png",
title:
"One-on-one counselling session between a therapist and client in a calm, private setting",
},
{
image: "src/assets/gallery/group-support-therapy.png",
title:
"A supportive group therapy session with people sharing and listening in a circle",
},
{
image: "src/assets/gallery/additional-therapies-room.png",
title:
"A therapy space featuring alternative healing options like art and music.",
},
{
image: "src/assets/gallery/animal-assisted-therapy.png",
title:
"A therapy session with various calming animals providing emotional support.",
},
].map((item, index) => (
<div
key={index}
className="group relative overflow-hidden rounded-xl cursor-pointer"
>
<div className="aspect-square w-full overflow-hidden rounded-xl">
<img
src={item.image}
alt={item.title}
className="object-cover w-full h-full transition-transform duration-500 group-hover:scale-110"
/>
</div>
<div className="absolute inset-0 bg-gradient-to-t from-black/70 via-black/50 to-transparent opacity-0 group-hover:opacity-100 transition-all duration-300 flex items-end p-4">
<h3 className="text-white font-bold group-hover:translate-y-0 translate-y-4 transition-transform duration-300">
{item.title}
</h3>
</div>
</div>
))}
</div>
<div className="mt-12 text-center">
<Link to="/gallery">
<Button className="bg-gradient-to-r from-purple-500 to-pink-400 hover:opacity-90 transition-all duration-300 !rounded-button">
View Full Gallery
</Button>
</Link>
</div>
</div>
</section>
);
};

View File

@ -0,0 +1,94 @@
import { useState, useEffect } from "react";
import background_image from "@/assets/bg3.png";
import { Button } from "@/components/ui/button";
import { motion, AnimatePresence } from "framer-motion";
const rotatingTexts = [
"Gentle therapy and animal-assisted healing for a calmer mind and connected heart.",
"Experience healing through connection, compassion, and care.",
"Empowering wellness with therapeutic support and furry companionship.",
"Where calm meets care — creating safe spaces for healing.",
];
export const HeroSection = () => {
const [currentTextIndex, setCurrentTextIndex] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setCurrentTextIndex((prev) => (prev + 1) % rotatingTexts.length);
}, 4000);
return () => clearInterval(interval);
}, []);
return (
<section
id="home"
className="relative h-screen flex items-center justify-center overflow-hidden"
>
{/* Background */}
<div className="absolute inset-0 z-0">
<img
src={background_image}
alt="Professional pet consultation at home with family, golden retriever dog and multiple cats in modern living room during in-home pet care visit"
className="w-full h-full object-cover"
/>
<div className="absolute inset-0 bg-gradient-to-b from-black/60 to-black/20"></div>
</div>
{/* Content */}
<div className="container mx-auto px-6 z-10 text-center">
<motion.div
className="max-w-3xl mx-auto"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
>
<h1 className="text-4xl md:text-6xl font-extrabold text-white mb-4 leading-tight">
Serenity Space
</h1>
{/* Animated Paragraph */}
<div className="min-h-[72px] mb-8">
<AnimatePresence mode="wait">
<motion.p
key={rotatingTexts[currentTextIndex]}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
transition={{ duration: 0.6 }}
className="text-lg md:text-2xl text-white/80 font-light"
>
{rotatingTexts[currentTextIndex]}
</motion.p>
</AnimatePresence>
</div>
{/* Buttons */}
<div className="flex flex-col sm:flex-row items-center justify-center gap-4">
<Button className="bg-white text-gray-900 hover:bg-white/90 px-6 py-4 text-base font-medium rounded-full shadow-sm transition-all">
Begin Your Journey
</Button>
<a href="#">
<Button
variant="ghost"
className="text-white border border-white px-6 py-4 text-base font-medium rounded-full hover:bg-white/10 transition-all"
>
Learn More
</Button>
</a>
</div>
</motion.div>
</div>
{/* Bouncing Chevron */}
<motion.div
className="absolute bottom-8 left-1/2 transform -translate-x-1/2 text-white"
animate={{ y: [0, 6, 0] }}
transition={{ repeat: Infinity, duration: 2 }}
>
<i className="fas fa-chevron-down text-sm opacity-80"></i>
</motion.div>
</section>
);
};

View File

@ -0,0 +1,27 @@
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
export const NewsletterSection = () => {
return (
<section className="py-16 bg-gradient-to-r from-purple-500 to-pink-400 text-white">
<div className="container mx-auto px-6">
<div className="max-w-3xl mx-auto text-center">
<h2 className="text-3xl font-bold mb-4 ">Stay Connected</h2>
<p className="mb-8 opacity-90">
Subscribe to our newsletter for helpful resources, event
announcements, and therapeutic insights.
</p>
<div className="flex flex-col sm:flex-row gap-4 max-w-lg mx-auto">
<Input
placeholder="Your email address"
className="bg-white/20 border-white/30 text-white placeholder:text-white/70 focus:border-white"
/>
<Button className="bg-white text-purple-700 hover:bg-white/90 !rounded-button whitespace-nowrap cursor-pointer">
Subscribe
</Button>
</div>
</div>
</div>
</section>
);
};

View File

@ -0,0 +1,156 @@
import { Button } from "@/components/ui/button";
export const ParticipateSection = () => {
return (
<section
id="participate"
className="py-20 bg-white relative overflow-hidden"
>
{/* Background decoration */}
<div className="absolute top-20 right-0 w-72 h-72 bg-purple-100 rounded-full opacity-20"></div>
<div className="absolute bottom-20 left-0 w-96 h-96 bg-pink-100 rounded-full opacity-20"></div>
<div className="container mx-auto px-6 relative">
<div className="text-center mb-16">
<h2 className="text-3xl md:text-4xl font-bold mb-4 text-gray-800 ">
Become a Participant
</h2>
<div className="h-1 w-24 bg-gradient-to-r from-purple-500 to-pink-400 rounded-full mx-auto mb-6"></div>
<p className="text-gray-600 max-w-3xl mx-auto">
Join our supportive community and take the first step on your
wellness journey with Serenity Space.
</p>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 items-center">
<div>
<h3 className="text-2xl font-bold text-gray-800 mb-6 ">
Your Path to Wellness Starts Here
</h3>
<div className="space-y-6">
{[
{
number: "01",
title: "Initial Consultation",
description:
"Schedule a free 20-minute consultation to discuss your needs and learn how we can support you.",
},
{
number: "02",
title: "Personalized Assessment",
description:
"We'll work together to understand your unique circumstances and develop a customized support plan.",
},
{
number: "03",
title: "Match with a Therapist",
description:
"Based on your needs and preferences, we'll connect you with the right therapist from our diverse team.",
},
{
number: "04",
title: "Begin Your Journey",
description:
"Start your sessions with ongoing support and adjustments to ensure you're getting exactly what you need.",
},
].map((step, index) => (
<div key={index} className="flex gap-6 items-start">
<div className="w-12 h-12 rounded-full bg-gradient-to-r from-purple-500 to-pink-400 flex-shrink-0 flex items-center justify-center text-white font-bold">
{step.number}
</div>
<div>
<h4 className="text-xl font-bold text-gray-800 mb-2 ">
{step.title}
</h4>
<p className="text-gray-600">{step.description}</p>
</div>
</div>
))}
</div>
<div className="mt-8">
<Button className="bg-gradient-to-r from-purple-500 to-pink-400 hover:opacity-90 transition-all duration-300 !rounded-button">
Start Your Journey
</Button>
</div>
</div>
<div className="relative">
<div className="bg-gradient-to-r from-purple-500 to-pink-400 absolute -bottom-4 -right-4 left-4 top-4 rounded-xl opacity-30 transform rotate-2"></div>
<div className="bg-white p-8 rounded-xl shadow-xl relative z-10">
<h3 className="text-xl font-bold mb-6 text-center text-gray-800 ">
Participant Eligibility
</h3>
<div className="space-y-5">
<div className="flex items-start gap-3">
<div className="text-purple-600 mt-1">
<i className="fas fa-check-circle"></i>
</div>
<div>
<h4 className="font-bold text-gray-700 ">Teens & Adults</h4>
<p className="text-gray-600">
Our services are designed for individuals age 13 and older
</p>
</div>
</div>
<div className="flex items-start gap-3">
<div className="text-purple-600 mt-1">
<i className="fas fa-check-circle"></i>
</div>
<div>
<h4 className="font-bold text-gray-700 ">
All Backgrounds Welcome
</h4>
<p className="text-gray-600">
We embrace diversity and provide inclusive care for all
identities
</p>
</div>
</div>
<div className="flex items-start gap-3">
<div className="text-purple-600 mt-1">
<i className="fas fa-check-circle"></i>
</div>
<div>
<h4 className="font-bold text-gray-700 ">
Neurodiversity Affirming
</h4>
<p className="text-gray-600">
Specialized support for individuals with autism, ADHD, and
other neurodivergent experiences
</p>
</div>
</div>
<div className="flex items-start gap-3">
<div className="text-purple-600 mt-1">
<i className="fas fa-check-circle"></i>
</div>
<div>
<h4 className="font-bold text-gray-700 ">
Insurance Accepted
</h4>
<p className="text-gray-600">
We work with many major insurance providers and offer
sliding scale fees
</p>
</div>
</div>
</div>
<div className="mt-8 p-4 bg-purple-50 rounded-lg">
<p className="text-purple-700 text-center font-medium">
Not sure if our services are right for you? Contact us for a
free consultation.
</p>
</div>
</div>
</div>
</div>
</div>
</section>
);
};

View File

@ -0,0 +1,152 @@
import {
Card,
CardContent,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import animalAssistedTherapy from "@/assets/Animal-Assisted-Therapy.png";
import { Button } from "@/components/ui/button";
export const ServiceSection = () => {
return (
<section
id="services"
className="py-24 bg-gradient-to-b from-white to-purple-50"
>
<div className="container mx-auto px-6">
<div className="text-center mb-16">
<h2 className="text-3xl md:text-4xl font-bold text-gray-800 mb-4 ">
Our Healing Services
</h2>
<p className="text-gray-600 max-w-2xl mx-auto">
We offer a range of supportive services designed to meet you exactly
where you are, with the gentle guidance you need to thrive.
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
{/* Service 1 */}
<Card className="bg-white border-none shadow-md hover:shadow-lg transition-all duration-300 overflow-hidden">
<div className="h-48 overflow-hidden">
<img
src={animalAssistedTherapy}
alt="Animal-Assisted Therapy"
className="w-full h-full object-cover hover:scale-105 transition-transform duration-500"
/>
</div>
<CardHeader>
<CardTitle className="text-xl text-purple-700">
Animal-Assisted Therapy
</CardTitle>
</CardHeader>
<CardContent>
<p className="text-gray-600">
Experience the healing power of animal connection. Our therapy
animals provide comfort, reduce anxiety, and create a bridge for
emotional expression.
</p>
</CardContent>
<CardFooter>
<Button
variant="ghost"
className="text-purple-700 hover:bg-purple-50 !rounded-button whitespace-nowrap cursor-pointer"
>
Learn More <i className="fas fa-arrow-right ml-2"></i>
</Button>
</CardFooter>
</Card>
{/* Service 2 */}
<Card className="bg-white border-none shadow-md hover:shadow-lg transition-all duration-300 overflow-hidden">
<div className="h-48 overflow-hidden">
<img
src="#"
alt="Talk Therapy"
className="w-full h-full object-cover object-top hover:scale-105 transition-transform duration-500"
/>
</div>
<CardHeader>
<CardTitle className="text-xl text-pink-600">
Talk Therapy
</CardTitle>
</CardHeader>
<CardContent>
<p className="text-gray-600">
A safe space to express yourself without judgment. Our
conversational approach helps you process emotions and develop
coping strategies at your own pace.
</p>
</CardContent>
<CardFooter>
<Button
variant="ghost"
className="text-pink-600 hover:bg-pink-50 !rounded-button whitespace-nowrap cursor-pointer"
>
Learn More <i className="fas fa-arrow-right ml-2"></i>
</Button>
</CardFooter>
</Card>
{/* Service 3 */}
<Card className="bg-white border-none shadow-md hover:shadow-lg transition-all duration-300 overflow-hidden">
<div className="h-48 overflow-hidden">
<img
src="#"
alt="Relaxation Techniques"
className="w-full h-full object-cover object-top hover:scale-105 transition-transform duration-500"
/>
</div>
<CardHeader>
<CardTitle className="text-xl text-purple-700">
Relaxation Techniques
</CardTitle>
</CardHeader>
<CardContent>
<p className="text-gray-600">
Learn practical methods to calm your mind and body. Our guided
relaxation practices help reduce stress and create a foundation
for emotional regulation.
</p>
</CardContent>
<CardFooter>
<Button
variant="ghost"
className="text-purple-700 hover:bg-purple-50 !rounded-button whitespace-nowrap cursor-pointer"
>
Learn More <i className="fas fa-arrow-right ml-2"></i>
</Button>
</CardFooter>
</Card>
{/* Service 4 */}
<Card className="bg-white border-none shadow-md hover:shadow-lg transition-all duration-300 overflow-hidden">
<div className="h-48 overflow-hidden">
<img
src="#"
alt="General Support Work"
className="w-full h-full object-cover object-top hover:scale-105 transition-transform duration-500"
/>
</div>
<CardHeader>
<CardTitle className="text-xl text-pink-600">
General Support Work
</CardTitle>
</CardHeader>
<CardContent>
<p className="text-gray-600">
Holistic assistance for daily challenges. We provide practical
guidance for social skills, emotional regulation, and navigating
life's complexities.
</p>
</CardContent>
<CardFooter>
<Button
variant="ghost"
className="text-pink-600 hover:bg-pink-50 !rounded-button whitespace-nowrap cursor-pointer"
>
Learn More <i className="fas fa-arrow-right ml-2"></i>
</Button>
</CardFooter>
</Card>
</div>
</div>
</section>
);
};

View File

@ -0,0 +1,97 @@
import { Button } from "@/components/ui/button";
export const TeamSection = () => {
return (
<section id="team" className="py-20 bg-white relative overflow-hidden">
<div className="container mx-auto px-6 relative">
<div className="text-center mb-16">
<h2 className="text-3xl md:text-4xl font-bold mb-4 text-gray-800 ">
Meet Our Team
</h2>
<div className="h-1 w-24 bg-gradient-to-r from-purple-500 to-pink-400 rounded-full mx-auto mb-6"></div>
<p className="text-gray-600 max-w-3xl mx-auto">
Our diverse team of licensed therapists and wellness professionals
bring decades of combined experience and specialized training to
support your unique needs.
</p>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-8">
{[
{
name: "Dr. Emily Chen",
role: "Founder & Clinical Director",
image:
"https://images.unsplash.com/photo-1573496359142-b8d87734a5a2",
description:
"With over 15 years of experience in clinical psychology, Dr. Chen specializes in trauma-informed therapy and mindfulness-based approaches.",
},
{
name: "Dr. Marcus Johnson",
role: "Lead Therapist",
image:
"https://images.unsplash.com/photo-1560250097-0b93528c311a",
description:
"Specializing in cognitive behavioral therapy and anxiety disorders, Dr. Johnson has helped hundreds of clients develop effective coping strategies.",
},
{
name: "Sarah Williams",
role: "Art Therapist",
image:
"https://images.unsplash.com/photo-1580489944761-15a19d654956",
description:
"Sarah combines traditional therapeutic approaches with creative expression to help clients process emotions and experiences.",
},
{
name: "James Rivera",
role: "Mindfulness Coach",
image:
"https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d",
description:
"A certified meditation instructor and former stress management consultant, James helps clients build sustainable mindfulness practices.",
},
].map((member, index) => (
<div
key={index}
className="bg-white rounded-xl shadow-lg border-0 overflow-hidden transition-all duration-300 hover:shadow-xl group"
>
<div className="h-2 bg-gradient-to-r from-purple-500 to-pink-400 w-full"></div>
<div className="p-6">
<div className="w-32 h-32 rounded-full mx-auto mb-6 overflow-hidden">
<img
src={member.image}
alt={member.name}
className="w-full h-full object-cover rounded-lg mb-6"
/>
</div>
<h3 className="text-xl font-bold text-gray-800 mb-1 text-center ">
{member.name}
</h3>
<p className="text-purple-600 mb-4 text-center font-medium ">
{member.role}
</p>
<p className="text-gray-600 text-center">
{member.description}
</p>
<div className="mt-4 flex justify-center space-x-3">
<button className="w-8 h-8 rounded-full bg-gray-100 hover:bg-purple-100 flex items-center justify-center transition-colors duration-300">
<i className="fab fa-linkedin-in text-gray-600 hover:text-purple-700"></i>
</button>
<button className="w-8 h-8 rounded-full bg-gray-100 hover:bg-purple-100 flex items-center justify-center transition-colors duration-300">
<i className="far fa-envelope text-gray-600 hover:text-purple-700"></i>
</button>
</div>
</div>
</div>
))}
</div>
<div className="flex justify-center mt-12">
<Button className="mt-4 bg-gradient-to-r from-purple-500 to-pink-400 hover:opacity-90 transition-all duration-300 !rounded-button">
View All Team Members
</Button>
</div>
</div>
</section>
);
};

View File

@ -0,0 +1,164 @@
import { Separator } from "@/components/ui/separator";
export const Footer = () => {
return (
<footer className="bg-gray-50 py-16">
<div className="container mx-auto px-6">
<div className="grid grid-cols-1 md:grid-cols-4 gap-8">
<div className="md:col-span-1">
<h3 className="text-xl font-bold text-gray-800 mb-4 ">
SerenitySpace
</h3>
<p className="text-gray-600 mb-4">
Providing gentle support and animal-assisted therapy for those who
need a compassionate approach to healing.
</p>
<div className="flex gap-4">
<a
href="#"
className="text-gray-500 hover:text-purple-600 transition-colors duration-300 cursor-pointer"
>
<i className="fab fa-facebook-f"></i>
</a>
<a
href="#"
className="text-gray-500 hover:text-purple-600 transition-colors duration-300 cursor-pointer"
>
<i className="fab fa-instagram"></i>
</a>
<a
href="#"
className="text-gray-500 hover:text-purple-600 transition-colors duration-300 cursor-pointer"
>
<i className="fab fa-twitter"></i>
</a>
</div>
</div>
<div>
<h3 className="text-lg font-semibold text-gray-800 mb-4 ">
Services
</h3>
<ul className="space-y-2 text-gray-600">
<li>
<a
href="#"
className="hover:text-purple-600 transition-colors duration-300 cursor-pointer"
>
Animal-Assisted Therapy
</a>
</li>
<li>
<a
href="#"
className="hover:text-purple-600 transition-colors duration-300 cursor-pointer"
>
Talk Therapy
</a>
</li>
<li>
<a
href="#"
className="hover:text-purple-600 transition-colors duration-300 cursor-pointer"
>
Relaxation Techniques
</a>
</li>
<li>
<a
href="#"
className="hover:text-purple-600 transition-colors duration-300 cursor-pointer"
>
General Support Work
</a>
</li>
</ul>
</div>
<div>
<h3 className="text-lg font-semibold text-gray-800 mb-4 ">
Resources
</h3>
<ul className="space-y-2 text-gray-600">
<li>
<a
href="#"
className="hover:text-purple-600 transition-colors duration-300 cursor-pointer"
>
Blog
</a>
</li>
<li>
<a
href="#"
className="hover:text-purple-600 transition-colors duration-300 cursor-pointer"
>
FAQ
</a>
</li>
<li>
<a
href="#"
className="hover:text-purple-600 transition-colors duration-300 cursor-pointer"
>
Testimonials
</a>
</li>
<li>
<a
href="#"
className="hover:text-purple-600 transition-colors duration-300 cursor-pointer"
>
Events
</a>
</li>
</ul>
</div>
<div>
<h3 className="text-lg font-semibold text-gray-800 mb-4 ">
Contact
</h3>
<ul className="space-y-2 text-gray-600">
<li className="flex items-start gap-2">
<i className="fas fa-map-marker-alt mt-1 text-purple-600"></i>
<span>123 Healing Way, Serenity Valley, CA 94123</span>
</li>
<li className="flex items-start gap-2">
<i className="fas fa-phone-alt mt-1 text-purple-600"></i>
<span>(555) 123-4567</span>
</li>
<li className="flex items-start gap-2">
<i className="fas fa-envelope mt-1 text-purple-600"></i>
<span>connect@serenityspace.com</span>
</li>
</ul>
</div>
</div>
<Separator className="my-8 bg-gray-200" />
<div className="flex flex-col md:flex-row justify-between items-center">
<p className="text-gray-500 text-sm">
© 2025 SerenitySpace. All rights reserved.
</p>
<div className="flex gap-4 mt-4 md:mt-0">
<a
href="#"
className="text-gray-500 text-sm hover:text-purple-600 transition-colors duration-300 cursor-pointer"
>
Privacy Policy
</a>
<a
href="#"
className="text-gray-500 text-sm hover:text-purple-600 transition-colors duration-300 cursor-pointer"
>
Terms of Service
</a>
<a
href="#"
className="text-gray-500 text-sm hover:text-purple-600 transition-colors duration-300 cursor-pointer"
>
Accessibility
</a>
</div>
</div>
</div>
</footer>
);
};

View File

@ -0,0 +1,154 @@
import { useState, useEffect } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { Button } from "@/components/ui/button";
import logo from "@/assets/logo.png";
const Header = () => {
const [isScrolled, setIsScrolled] = useState(false);
const [activeSection, setActiveSection] = useState("home");
const [menuOpen, setMenuOpen] = useState(false);
useEffect(() => {
const handleScroll = () => {
setIsScrolled(window.scrollY > 50);
const sections = [
"home",
"about",
"services",
"gallery",
"participate",
"fees",
"team",
"contact",
];
let current = "home";
for (let section of sections) {
const el = document.getElementById(section);
if (el) {
const top = el.getBoundingClientRect().top;
if (top <= 120) {
current = section;
}
}
}
setActiveSection(current);
};
window.addEventListener("scroll", handleScroll);
handleScroll();
return () => window.removeEventListener("scroll", handleScroll);
}, []);
const menuItems = [
"Home",
"About",
"Services",
"Gallery",
"Participate",
"Fees",
"Team",
"Contact",
];
return (
<nav
className={`fixed top-0 left-0 w-full z-50 transition-all duration-300 ${
isScrolled
? "bg-white/90 backdrop-blur-md shadow-sm py-3"
: "bg-transparent py-5"
}`}
>
<div className="container mx-auto px-6 flex items-center justify-between">
<div className="flex items-center">
<img
src={logo}
alt="Serenity Space Logo"
className={`h-10 object-contain ${isScrolled ? "filter brightness-75" : ""}`}
/>
</div>
{/* Desktop Menu */}
<div className="hidden md:flex items-center space-x-6">
{menuItems.map((item) => {
const id = item.toLowerCase();
const isActive = activeSection === id;
return (
<a
key={item}
href={`#${id}`}
className={`text-sm font-medium transition-all duration-300 cursor-pointer whitespace-nowrap ${
isScrolled ? "text-gray-700" : "text-white"
} ${
isActive
? "text-purple-700 underline underline-offset-4 font-semibold"
: "hover:opacity-80"
}`}
>
{item}
</a>
);
})}
<Button className="bg-gradient-to-r from-purple-500 to-pink-400 hover:opacity-90 transition-all duration-300 !rounded-button whitespace-nowrap cursor-pointer">
Book Appointment
</Button>
</div>
{/* Mobile Hamburger */}
<div
className="md:hidden cursor-pointer"
onClick={() => setMenuOpen(!menuOpen)}
>
<i
className={`fas fa-${menuOpen ? "times" : "bars"} text-xl ${
isScrolled ? "text-purple-700" : "text-white"
}`}
></i>
</div>
</div>
{/* Animated Mobile Menu */}
<AnimatePresence>
{menuOpen && (
<motion.div
key="mobile-menu"
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ type: "spring", stiffness: 300, damping: 24 }}
className="md:hidden bg-white shadow-md rounded-md mt-2 mx-4 p-4 space-y-4 z-50"
>
{menuItems.map((item) => {
const id = item.toLowerCase();
const isActive = activeSection === id;
return (
<a
key={item}
href={`#${id}`}
onClick={() => setMenuOpen(false)}
className={`block text-base font-medium transition-all duration-300 ${
isActive
? "text-purple-700 underline underline-offset-4 font-semibold"
: "text-gray-800 hover:text-purple-500"
}`}
>
{item}
</a>
);
})}
<Button
className="w-full bg-gradient-to-r from-purple-500 to-pink-400 hover:opacity-90 transition-all duration-300 !rounded-button"
onClick={() => setMenuOpen(false)}
>
Book Appointment
</Button>
</motion.div>
)}
</AnimatePresence>
</nav>
);
};
export default Header;

View File

@ -0,0 +1,52 @@
import Header from "../layout/Header";
import { Footer } from "../layout/Footer";
import {
HeroSection,
AboutSection,
ServiceSection,
GallerySection,
ParticipateSection,
FeesSection,
TeamSection,
ContactSection,
FaqSection,
NewsletterSection,
} from "@/features/user/components";
export const Home: React.FC = () => {
return (
<div className="min-h-screen bg-white">
{/* Navigation */}
<Header />
{/* Hero Section */}
<HeroSection />
{/* About Section */}
<AboutSection />
{/* Services Section */}
<ServiceSection />
{/* Gallery Section */}
<GallerySection />
{/* Participate Section */}
<ParticipateSection />
{/* Fees Section */}
<FeesSection />
{/* Team Section */}
<TeamSection />
{/* Contact Section */}
<ContactSection />
{/* FAQ Section */}
<FaqSection />
{/* Newsletter Section */}
<NewsletterSection />
{/* Footer */}
<Footer />
</div>
);
};

19
src/hooks/use-mobile.ts Normal file
View File

@ -0,0 +1,19 @@
import * as React from "react"
const MOBILE_BREAKPOINT = 768
export function useIsMobile() {
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
React.useEffect(() => {
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
const onChange = () => {
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
}
mql.addEventListener("change", onChange)
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
return () => mql.removeEventListener("change", onChange)
}, [])
return !!isMobile
}

136
src/index.css Normal file
View File

@ -0,0 +1,136 @@
@import "tailwindcss";
@import "tw-animate-css";
@import "@fortawesome/fontawesome-free/css/all.min.css";
html {
scroll-behavior: smooth;
}
@custom-variant dark (&:is(.dark *));
:root {
font-family: Inter, ui-sans-serif, system-ui, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
--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);
}
@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);
}
.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;
}
body {
@apply bg-background text-foreground;
}
}

6
src/lib/utils.ts Normal file
View File

@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

10
src/main.tsx Normal file
View File

@ -0,0 +1,10 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.tsx'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
)

1
src/vite-env.d.ts vendored Normal file
View File

@ -0,0 +1 @@
/// <reference types="vite/client" />

33
tsconfig.app.json Normal file
View File

@ -0,0 +1,33 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"baseUrl": ".",
"paths": {
"@/*": [
"./src/*"
]
},
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src"]
}

17
tsconfig.json Normal file
View File

@ -0,0 +1,17 @@
{
"files": [],
"references": [
{
"path": "./tsconfig.app.json"
},
{
"path": "./tsconfig.node.json"
}
],
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}

25
tsconfig.node.json Normal file
View File

@ -0,0 +1,25 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2022",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["vite.config.ts"]
}

14
vite.config.ts Normal file
View File

@ -0,0 +1,14 @@
import path from "path";
import tailwindcss from "@tailwindcss/vite";
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";
export default defineConfig({
base: "/",
plugins: [react(), tailwindcss()],
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
});