snapshot of https://github.com/shadcn-ui/ui.git (main, 2026-09-15; blobs > 131072B omitted)
This commit is contained in:
921
apps/v4/registry/bases/base/blocks/_registry.ts
Normal file
921
apps/v4/registry/bases/base/blocks/_registry.ts
Normal file
@@ -0,0 +1,921 @@
|
||||
import { type Registry } from "shadcn/schema"
|
||||
|
||||
export const blocks: Registry["items"] = [
|
||||
{
|
||||
name: "preview",
|
||||
title: "Preview",
|
||||
type: "registry:block",
|
||||
registryDependencies: [
|
||||
"alert-dialog",
|
||||
"avatar",
|
||||
"badge",
|
||||
"button",
|
||||
"button-group",
|
||||
"card",
|
||||
"chart",
|
||||
"checkbox",
|
||||
"combobox",
|
||||
"dropdown-menu",
|
||||
"empty",
|
||||
"field",
|
||||
"input",
|
||||
"input-group",
|
||||
"item",
|
||||
"label",
|
||||
"popover",
|
||||
"radio-group",
|
||||
"select",
|
||||
"separator",
|
||||
"sheet",
|
||||
"slider",
|
||||
"spinner",
|
||||
"switch",
|
||||
"textarea",
|
||||
"tooltip",
|
||||
"example",
|
||||
],
|
||||
files: [
|
||||
{
|
||||
path: "blocks/preview/index.tsx",
|
||||
type: "registry:block",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "preview-02",
|
||||
title: "Preview 02",
|
||||
type: "registry:block",
|
||||
dependencies: ["react-qr-code"],
|
||||
registryDependencies: [
|
||||
"accordion",
|
||||
"badge",
|
||||
"breadcrumb",
|
||||
"button",
|
||||
"calendar",
|
||||
"card",
|
||||
"chart",
|
||||
"checkbox",
|
||||
"combobox",
|
||||
"dropdown-menu",
|
||||
"empty",
|
||||
"field",
|
||||
"input",
|
||||
"input-group",
|
||||
"item",
|
||||
"label",
|
||||
"native-select",
|
||||
"progress",
|
||||
"radio-group",
|
||||
"select",
|
||||
"separator",
|
||||
"sidebar",
|
||||
"skeleton",
|
||||
"slider",
|
||||
"spinner",
|
||||
"switch",
|
||||
"table",
|
||||
"tabs",
|
||||
"textarea",
|
||||
"toggle-group",
|
||||
],
|
||||
files: [
|
||||
{
|
||||
path: "blocks/preview-02/index.tsx",
|
||||
type: "registry:block",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "preview-03",
|
||||
title: "Preview 03",
|
||||
type: "registry:block",
|
||||
registryDependencies: [],
|
||||
files: [
|
||||
{
|
||||
path: "blocks/preview-03/index.tsx",
|
||||
type: "registry:block",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "login-01",
|
||||
title: "Login 01",
|
||||
description: "A simple login form.",
|
||||
type: "registry:block",
|
||||
registryDependencies: ["button", "card", "input", "label", "field"],
|
||||
files: [
|
||||
{
|
||||
path: "blocks/login-01/page.tsx",
|
||||
target: "app/login/page.tsx",
|
||||
type: "registry:page",
|
||||
},
|
||||
{
|
||||
path: "blocks/login-01/components/login-form.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
],
|
||||
categories: ["authentication", "login"],
|
||||
},
|
||||
{
|
||||
name: "login-02",
|
||||
title: "Login 02",
|
||||
description: "A two column login page with a cover image.",
|
||||
type: "registry:block",
|
||||
registryDependencies: ["button", "input", "label", "field"],
|
||||
files: [
|
||||
{
|
||||
path: "blocks/login-02/page.tsx",
|
||||
target: "app/login/page.tsx",
|
||||
type: "registry:page",
|
||||
},
|
||||
{
|
||||
path: "blocks/login-02/components/login-form.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
],
|
||||
categories: ["authentication", "login"],
|
||||
},
|
||||
{
|
||||
name: "login-03",
|
||||
title: "Login 03",
|
||||
description: "A login page with a muted background color.",
|
||||
type: "registry:block",
|
||||
registryDependencies: ["button", "card", "input", "label", "field"],
|
||||
files: [
|
||||
{
|
||||
path: "blocks/login-03/page.tsx",
|
||||
target: "app/login/page.tsx",
|
||||
type: "registry:page",
|
||||
},
|
||||
{
|
||||
path: "blocks/login-03/components/login-form.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
],
|
||||
categories: ["authentication", "login"],
|
||||
},
|
||||
{
|
||||
name: "login-04",
|
||||
title: "Login 04",
|
||||
description: "A login page with form and image.",
|
||||
type: "registry:block",
|
||||
registryDependencies: ["button", "card", "input", "label", "field"],
|
||||
files: [
|
||||
{
|
||||
path: "blocks/login-04/page.tsx",
|
||||
target: "app/login/page.tsx",
|
||||
type: "registry:page",
|
||||
},
|
||||
{
|
||||
path: "blocks/login-04/components/login-form.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
],
|
||||
categories: ["authentication", "login"],
|
||||
},
|
||||
{
|
||||
name: "login-05",
|
||||
title: "Login 05",
|
||||
description: "A simple email-only login page.",
|
||||
type: "registry:block",
|
||||
registryDependencies: ["button", "input", "label", "field"],
|
||||
files: [
|
||||
{
|
||||
path: "blocks/login-05/page.tsx",
|
||||
target: "app/login/page.tsx",
|
||||
type: "registry:page",
|
||||
},
|
||||
{
|
||||
path: "blocks/login-05/components/login-form.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
],
|
||||
categories: ["authentication", "login"],
|
||||
},
|
||||
{
|
||||
name: "signup-01",
|
||||
title: "Signup 01",
|
||||
description: "A simple signup form.",
|
||||
type: "registry:block",
|
||||
registryDependencies: ["button", "card", "input", "label"],
|
||||
files: [
|
||||
{
|
||||
path: "blocks/signup-01/page.tsx",
|
||||
target: "app/signup/page.tsx",
|
||||
type: "registry:page",
|
||||
},
|
||||
{
|
||||
path: "blocks/signup-01/components/signup-form.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
],
|
||||
categories: ["authentication", "signup"],
|
||||
},
|
||||
{
|
||||
name: "signup-02",
|
||||
title: "Signup 02",
|
||||
description: "A two column signup page with a cover image.",
|
||||
type: "registry:block",
|
||||
registryDependencies: ["button", "input", "label", "field"],
|
||||
files: [
|
||||
{
|
||||
path: "blocks/signup-02/page.tsx",
|
||||
target: "app/signup/page.tsx",
|
||||
type: "registry:page",
|
||||
},
|
||||
{
|
||||
path: "blocks/signup-02/components/signup-form.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
],
|
||||
categories: ["authentication", "signup"],
|
||||
},
|
||||
{
|
||||
name: "signup-03",
|
||||
title: "Signup 03",
|
||||
description: "A signup page with a muted background color.",
|
||||
type: "registry:block",
|
||||
registryDependencies: ["button", "card", "input", "label", "field"],
|
||||
files: [
|
||||
{
|
||||
path: "blocks/signup-03/page.tsx",
|
||||
target: "app/signup/page.tsx",
|
||||
type: "registry:page",
|
||||
},
|
||||
{
|
||||
path: "blocks/signup-03/components/signup-form.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
],
|
||||
categories: ["authentication", "signup"],
|
||||
},
|
||||
{
|
||||
name: "signup-04",
|
||||
title: "Signup 04",
|
||||
description: "A signup page with form and image.",
|
||||
type: "registry:block",
|
||||
registryDependencies: ["button", "card", "input", "label", "field"],
|
||||
files: [
|
||||
{
|
||||
path: "blocks/signup-04/page.tsx",
|
||||
target: "app/signup/page.tsx",
|
||||
type: "registry:page",
|
||||
},
|
||||
{
|
||||
path: "blocks/signup-04/components/signup-form.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
],
|
||||
categories: ["authentication", "signup"],
|
||||
},
|
||||
{
|
||||
name: "signup-05",
|
||||
title: "Signup 05",
|
||||
description: "A simple signup form with social providers.",
|
||||
type: "registry:block",
|
||||
registryDependencies: ["button", "input", "label"],
|
||||
files: [
|
||||
{
|
||||
path: "blocks/signup-05/page.tsx",
|
||||
target: "app/signup/page.tsx",
|
||||
type: "registry:page",
|
||||
},
|
||||
{
|
||||
path: "blocks/signup-05/components/signup-form.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
],
|
||||
categories: ["authentication", "signup"],
|
||||
},
|
||||
{
|
||||
name: "dashboard-01",
|
||||
title: "Dashboard 01",
|
||||
type: "registry:block",
|
||||
description: "A dashboard with sidebar, charts and data table.",
|
||||
dependencies: [
|
||||
"@dnd-kit/core",
|
||||
"@dnd-kit/modifiers",
|
||||
"@dnd-kit/sortable",
|
||||
"@dnd-kit/utilities",
|
||||
"@tanstack/react-table",
|
||||
"zod",
|
||||
],
|
||||
registryDependencies: [
|
||||
"sidebar",
|
||||
"breadcrumb",
|
||||
"separator",
|
||||
"label",
|
||||
"chart",
|
||||
"card",
|
||||
"select",
|
||||
"tabs",
|
||||
"table",
|
||||
"toggle-group",
|
||||
"badge",
|
||||
"button",
|
||||
"checkbox",
|
||||
"dropdown-menu",
|
||||
"drawer",
|
||||
"input",
|
||||
"avatar",
|
||||
"sheet",
|
||||
"sonner",
|
||||
],
|
||||
files: [
|
||||
{
|
||||
path: "blocks/dashboard-01/page.tsx",
|
||||
type: "registry:page",
|
||||
target: "app/dashboard/page.tsx",
|
||||
},
|
||||
{
|
||||
path: "blocks/dashboard-01/data.json",
|
||||
type: "registry:file",
|
||||
target: "app/dashboard/data.json",
|
||||
},
|
||||
{
|
||||
path: "blocks/dashboard-01/components/app-sidebar.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
{
|
||||
path: "blocks/dashboard-01/components/chart-area-interactive.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
{
|
||||
path: "blocks/dashboard-01/components/data-table.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
{
|
||||
path: "blocks/dashboard-01/components/nav-documents.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
{
|
||||
path: "blocks/dashboard-01/components/nav-main.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
{
|
||||
path: "blocks/dashboard-01/components/nav-secondary.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
{
|
||||
path: "blocks/dashboard-01/components/nav-user.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
{
|
||||
path: "blocks/dashboard-01/components/section-cards.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
{
|
||||
path: "blocks/dashboard-01/components/site-header.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
],
|
||||
categories: ["dashboard"],
|
||||
meta: {
|
||||
iframeHeight: "1000px",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "sidebar-01",
|
||||
title: "Sidebar 01",
|
||||
type: "registry:block",
|
||||
description: "A simple sidebar with navigation grouped by section.",
|
||||
registryDependencies: [
|
||||
"sidebar",
|
||||
"breadcrumb",
|
||||
"separator",
|
||||
"label",
|
||||
"dropdown-menu",
|
||||
],
|
||||
files: [
|
||||
{
|
||||
path: "blocks/sidebar-01/page.tsx",
|
||||
type: "registry:page",
|
||||
target: "app/dashboard/page.tsx",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-01/components/app-sidebar.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-01/components/search-form.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-01/components/version-switcher.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
],
|
||||
categories: ["sidebar", "dashboard"],
|
||||
},
|
||||
{
|
||||
name: "sidebar-02",
|
||||
title: "Sidebar 02",
|
||||
description: "A sidebar with collapsible sections.",
|
||||
type: "registry:block",
|
||||
registryDependencies: [
|
||||
"sidebar",
|
||||
"breadcrumb",
|
||||
"separator",
|
||||
"label",
|
||||
"dropdown-menu",
|
||||
],
|
||||
files: [
|
||||
{
|
||||
path: "blocks/sidebar-02/page.tsx",
|
||||
type: "registry:page",
|
||||
target: "app/dashboard/page.tsx",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-02/components/app-sidebar.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-02/components/search-form.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-02/components/version-switcher.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
],
|
||||
categories: ["sidebar", "dashboard"],
|
||||
},
|
||||
{
|
||||
name: "sidebar-03",
|
||||
title: "Sidebar 03",
|
||||
description: "A sidebar with submenus.",
|
||||
type: "registry:block",
|
||||
registryDependencies: ["sidebar", "breadcrumb"],
|
||||
files: [
|
||||
{
|
||||
path: "blocks/sidebar-03/page.tsx",
|
||||
type: "registry:page",
|
||||
target: "app/dashboard/page.tsx",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-03/components/app-sidebar.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
],
|
||||
categories: ["sidebar", "dashboard"],
|
||||
},
|
||||
{
|
||||
name: "sidebar-04",
|
||||
title: "Sidebar 04",
|
||||
description: "A floating sidebar with submenus.",
|
||||
type: "registry:block",
|
||||
registryDependencies: ["sidebar", "breadcrumb", "separator"],
|
||||
files: [
|
||||
{
|
||||
path: "blocks/sidebar-04/page.tsx",
|
||||
type: "registry:page",
|
||||
target: "app/dashboard/page.tsx",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-04/components/app-sidebar.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
],
|
||||
categories: ["sidebar", "dashboard"],
|
||||
},
|
||||
{
|
||||
name: "sidebar-05",
|
||||
title: "Sidebar 05",
|
||||
description: "A sidebar with collapsible submenus.",
|
||||
type: "registry:block",
|
||||
registryDependencies: [
|
||||
"sidebar",
|
||||
"breadcrumb",
|
||||
"separator",
|
||||
"label",
|
||||
"collapsible",
|
||||
],
|
||||
files: [
|
||||
{
|
||||
path: "blocks/sidebar-05/page.tsx",
|
||||
type: "registry:page",
|
||||
target: "app/dashboard/page.tsx",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-05/components/app-sidebar.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-05/components/search-form.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
],
|
||||
categories: ["sidebar", "dashboard"],
|
||||
},
|
||||
{
|
||||
name: "sidebar-06",
|
||||
title: "Sidebar 06",
|
||||
description: "A sidebar with submenus as dropdowns.",
|
||||
type: "registry:block",
|
||||
registryDependencies: [
|
||||
"sidebar",
|
||||
"breadcrumb",
|
||||
"separator",
|
||||
"card",
|
||||
"dropdown-menu",
|
||||
],
|
||||
files: [
|
||||
{
|
||||
path: "blocks/sidebar-06/page.tsx",
|
||||
type: "registry:page",
|
||||
target: "app/dashboard/page.tsx",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-06/components/app-sidebar.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-06/components/nav-main.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-06/components/sidebar-opt-in-form.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
],
|
||||
categories: ["sidebar", "dashboard"],
|
||||
},
|
||||
{
|
||||
name: "sidebar-07",
|
||||
title: "Sidebar 07",
|
||||
type: "registry:block",
|
||||
description: "A sidebar that collapses to icons.",
|
||||
registryDependencies: [
|
||||
"sidebar",
|
||||
"breadcrumb",
|
||||
"separator",
|
||||
"collapsible",
|
||||
"dropdown-menu",
|
||||
"avatar",
|
||||
],
|
||||
files: [
|
||||
{
|
||||
path: "blocks/sidebar-07/page.tsx",
|
||||
type: "registry:page",
|
||||
target: "app/dashboard/page.tsx",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-07/components/app-sidebar.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-07/components/nav-main.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-07/components/nav-projects.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-07/components/nav-user.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-07/components/team-switcher.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
],
|
||||
categories: ["sidebar", "dashboard"],
|
||||
},
|
||||
{
|
||||
name: "sidebar-08",
|
||||
title: "Sidebar 08",
|
||||
description: "An inset sidebar with secondary navigation.",
|
||||
type: "registry:block",
|
||||
registryDependencies: [
|
||||
"sidebar",
|
||||
"breadcrumb",
|
||||
"separator",
|
||||
"collapsible",
|
||||
"dropdown-menu",
|
||||
"avatar",
|
||||
],
|
||||
files: [
|
||||
{
|
||||
path: "blocks/sidebar-08/page.tsx",
|
||||
type: "registry:page",
|
||||
target: "app/dashboard/page.tsx",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-08/components/app-sidebar.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-08/components/nav-main.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-08/components/nav-projects.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-08/components/nav-secondary.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-08/components/nav-user.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
],
|
||||
categories: ["sidebar", "dashboard"],
|
||||
},
|
||||
{
|
||||
name: "sidebar-09",
|
||||
title: "Sidebar 09",
|
||||
description: "Collapsible nested sidebars.",
|
||||
type: "registry:block",
|
||||
registryDependencies: [
|
||||
"sidebar",
|
||||
"breadcrumb",
|
||||
"separator",
|
||||
"collapsible",
|
||||
"dropdown-menu",
|
||||
"avatar",
|
||||
"switch",
|
||||
"label",
|
||||
],
|
||||
files: [
|
||||
{
|
||||
path: "blocks/sidebar-09/page.tsx",
|
||||
type: "registry:page",
|
||||
target: "app/dashboard/page.tsx",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-09/components/app-sidebar.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-09/components/nav-user.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
],
|
||||
categories: ["sidebar", "dashboard"],
|
||||
},
|
||||
{
|
||||
name: "sidebar-10",
|
||||
title: "Sidebar 10",
|
||||
description: "A sidebar in a popover.",
|
||||
type: "registry:block",
|
||||
registryDependencies: [
|
||||
"sidebar",
|
||||
"breadcrumb",
|
||||
"separator",
|
||||
"popover",
|
||||
"collapsible",
|
||||
"dropdown-menu",
|
||||
],
|
||||
files: [
|
||||
{
|
||||
path: "blocks/sidebar-10/page.tsx",
|
||||
type: "registry:page",
|
||||
target: "app/dashboard/page.tsx",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-10/components/app-sidebar.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-10/components/nav-actions.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-10/components/nav-favorites.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-10/components/nav-main.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-10/components/nav-secondary.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-10/components/nav-workspaces.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-10/components/team-switcher.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
],
|
||||
categories: ["sidebar", "dashboard"],
|
||||
},
|
||||
{
|
||||
name: "sidebar-11",
|
||||
title: "Sidebar 11",
|
||||
description: "A sidebar with a collapsible file tree.",
|
||||
type: "registry:block",
|
||||
registryDependencies: ["sidebar", "breadcrumb", "separator", "collapsible"],
|
||||
files: [
|
||||
{
|
||||
path: "blocks/sidebar-11/page.tsx",
|
||||
type: "registry:page",
|
||||
target: "app/dashboard/page.tsx",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-11/components/app-sidebar.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
],
|
||||
categories: ["sidebar", "dashboard"],
|
||||
},
|
||||
{
|
||||
name: "sidebar-12",
|
||||
title: "Sidebar 12",
|
||||
description: "A sidebar with a calendar.",
|
||||
type: "registry:block",
|
||||
registryDependencies: [
|
||||
"sidebar",
|
||||
"breadcrumb",
|
||||
"separator",
|
||||
"collapsible",
|
||||
"calendar",
|
||||
"dropdown-menu",
|
||||
"avatar",
|
||||
],
|
||||
files: [
|
||||
{
|
||||
path: "blocks/sidebar-12/page.tsx",
|
||||
type: "registry:page",
|
||||
target: "app/dashboard/page.tsx",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-12/components/app-sidebar.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-12/components/calendars.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-12/components/date-picker.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-12/components/nav-user.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
],
|
||||
categories: ["sidebar", "dashboard"],
|
||||
},
|
||||
{
|
||||
name: "sidebar-13",
|
||||
title: "Sidebar 13",
|
||||
description: "A sidebar in a dialog.",
|
||||
type: "registry:block",
|
||||
registryDependencies: ["sidebar", "breadcrumb", "button", "dialog"],
|
||||
files: [
|
||||
{
|
||||
path: "blocks/sidebar-13/page.tsx",
|
||||
type: "registry:page",
|
||||
target: "app/dashboard/page.tsx",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-13/components/settings-dialog.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
],
|
||||
categories: ["sidebar", "dashboard"],
|
||||
},
|
||||
{
|
||||
name: "sidebar-14",
|
||||
title: "Sidebar 14",
|
||||
description: "A sidebar on the right.",
|
||||
type: "registry:block",
|
||||
registryDependencies: ["sidebar", "breadcrumb"],
|
||||
files: [
|
||||
{
|
||||
path: "blocks/sidebar-14/page.tsx",
|
||||
type: "registry:page",
|
||||
target: "app/dashboard/page.tsx",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-14/components/app-sidebar.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
],
|
||||
categories: ["sidebar", "dashboard"],
|
||||
},
|
||||
{
|
||||
name: "sidebar-15",
|
||||
title: "Sidebar 15",
|
||||
description: "A left and right sidebar.",
|
||||
type: "registry:block",
|
||||
registryDependencies: [
|
||||
"sidebar",
|
||||
"breadcrumb",
|
||||
"separator",
|
||||
"popover",
|
||||
"collapsible",
|
||||
"dropdown-menu",
|
||||
"calendar",
|
||||
"avatar",
|
||||
],
|
||||
files: [
|
||||
{
|
||||
path: "blocks/sidebar-15/page.tsx",
|
||||
type: "registry:page",
|
||||
target: "app/dashboard/page.tsx",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-15/components/calendars.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-15/components/date-picker.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-15/components/nav-favorites.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-15/components/nav-main.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-15/components/nav-secondary.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-15/components/nav-user.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-15/components/nav-workspaces.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-15/components/sidebar-left.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-15/components/sidebar-right.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-15/components/team-switcher.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
],
|
||||
categories: ["sidebar", "dashboard"],
|
||||
},
|
||||
{
|
||||
name: "sidebar-16",
|
||||
title: "Sidebar 16",
|
||||
description: "A sidebar with a sticky site header.",
|
||||
type: "registry:block",
|
||||
registryDependencies: [
|
||||
"sidebar",
|
||||
"breadcrumb",
|
||||
"separator",
|
||||
"collapsible",
|
||||
"dropdown-menu",
|
||||
"avatar",
|
||||
"button",
|
||||
"label",
|
||||
],
|
||||
files: [
|
||||
{
|
||||
path: "blocks/sidebar-16/page.tsx",
|
||||
type: "registry:page",
|
||||
target: "app/dashboard/page.tsx",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-16/components/app-sidebar.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-16/components/nav-main.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-16/components/nav-projects.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-16/components/nav-secondary.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-16/components/nav-user.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-16/components/search-form.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-16/components/site-header.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
],
|
||||
categories: ["sidebar", "dashboard"],
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,281 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
|
||||
import { NavDocuments } from "@/registry/bases/base/blocks/dashboard-01/components/nav-documents"
|
||||
import { NavMain } from "@/registry/bases/base/blocks/dashboard-01/components/nav-main"
|
||||
import { NavSecondary } from "@/registry/bases/base/blocks/dashboard-01/components/nav-secondary"
|
||||
import { NavUser } from "@/registry/bases/base/blocks/dashboard-01/components/nav-user"
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
SidebarFooter,
|
||||
SidebarHeader,
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
} from "@/registry/bases/base/ui/sidebar"
|
||||
import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"
|
||||
|
||||
const data = {
|
||||
user: {
|
||||
name: "shadcn",
|
||||
email: "m@example.com",
|
||||
avatar: "/avatars/shadcn.jpg",
|
||||
},
|
||||
navMain: [
|
||||
{
|
||||
title: "Dashboard",
|
||||
url: "#",
|
||||
icon: (
|
||||
<IconPlaceholder
|
||||
lucide="LayoutDashboardIcon"
|
||||
tabler="IconDashboard"
|
||||
hugeicons="DashboardSquare01Icon"
|
||||
phosphor="SquaresFourIcon"
|
||||
remixicon="RiDashboardLine"
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Lifecycle",
|
||||
url: "#",
|
||||
icon: (
|
||||
<IconPlaceholder
|
||||
lucide="ListIcon"
|
||||
tabler="IconListDetails"
|
||||
hugeicons="Menu01Icon"
|
||||
phosphor="ListIcon"
|
||||
remixicon="RiListUnordered"
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Analytics",
|
||||
url: "#",
|
||||
icon: (
|
||||
<IconPlaceholder
|
||||
lucide="ChartBarIcon"
|
||||
tabler="IconChartBar"
|
||||
hugeicons="ChartHistogramIcon"
|
||||
phosphor="ChartBarIcon"
|
||||
remixicon="RiBarChartLine"
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Projects",
|
||||
url: "#",
|
||||
icon: (
|
||||
<IconPlaceholder
|
||||
lucide="FolderIcon"
|
||||
tabler="IconFolder"
|
||||
hugeicons="Folder01Icon"
|
||||
phosphor="FolderIcon"
|
||||
remixicon="RiFolderLine"
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Team",
|
||||
url: "#",
|
||||
icon: (
|
||||
<IconPlaceholder
|
||||
lucide="UsersIcon"
|
||||
tabler="IconUsers"
|
||||
hugeicons="UserGroupIcon"
|
||||
phosphor="UsersIcon"
|
||||
remixicon="RiGroupLine"
|
||||
/>
|
||||
),
|
||||
},
|
||||
],
|
||||
navClouds: [
|
||||
{
|
||||
title: "Capture",
|
||||
icon: (
|
||||
<IconPlaceholder
|
||||
lucide="CameraIcon"
|
||||
tabler="IconCamera"
|
||||
hugeicons="Camera01Icon"
|
||||
phosphor="CameraIcon"
|
||||
remixicon="RiCameraLine"
|
||||
/>
|
||||
),
|
||||
isActive: true,
|
||||
url: "#",
|
||||
items: [
|
||||
{
|
||||
title: "Active Proposals",
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
title: "Archived",
|
||||
url: "#",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Proposal",
|
||||
icon: (
|
||||
<IconPlaceholder
|
||||
lucide="FileTextIcon"
|
||||
tabler="IconFileDescription"
|
||||
hugeicons="File01Icon"
|
||||
phosphor="FileTextIcon"
|
||||
remixicon="RiFileTextLine"
|
||||
/>
|
||||
),
|
||||
url: "#",
|
||||
items: [
|
||||
{
|
||||
title: "Active Proposals",
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
title: "Archived",
|
||||
url: "#",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Prompts",
|
||||
icon: (
|
||||
<IconPlaceholder
|
||||
lucide="FileTextIcon"
|
||||
tabler="IconFileAi"
|
||||
hugeicons="File01Icon"
|
||||
phosphor="FileTextIcon"
|
||||
remixicon="RiFileTextLine"
|
||||
/>
|
||||
),
|
||||
url: "#",
|
||||
items: [
|
||||
{
|
||||
title: "Active Proposals",
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
title: "Archived",
|
||||
url: "#",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
navSecondary: [
|
||||
{
|
||||
title: "Settings",
|
||||
url: "#",
|
||||
icon: (
|
||||
<IconPlaceholder
|
||||
lucide="Settings2Icon"
|
||||
tabler="IconSettings"
|
||||
hugeicons="Settings05Icon"
|
||||
phosphor="GearIcon"
|
||||
remixicon="RiSettingsLine"
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Get Help",
|
||||
url: "#",
|
||||
icon: (
|
||||
<IconPlaceholder
|
||||
lucide="CircleHelpIcon"
|
||||
tabler="IconHelp"
|
||||
hugeicons="HelpCircleIcon"
|
||||
phosphor="QuestionIcon"
|
||||
remixicon="RiQuestionLine"
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Search",
|
||||
url: "#",
|
||||
icon: (
|
||||
<IconPlaceholder
|
||||
lucide="SearchIcon"
|
||||
tabler="IconSearch"
|
||||
hugeicons="SearchIcon"
|
||||
phosphor="MagnifyingGlassIcon"
|
||||
remixicon="RiSearchLine"
|
||||
/>
|
||||
),
|
||||
},
|
||||
],
|
||||
documents: [
|
||||
{
|
||||
name: "Data Library",
|
||||
url: "#",
|
||||
icon: (
|
||||
<IconPlaceholder
|
||||
lucide="DatabaseIcon"
|
||||
tabler="IconDatabase"
|
||||
hugeicons="Database01Icon"
|
||||
phosphor="DatabaseIcon"
|
||||
remixicon="RiDatabase2Line"
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "Reports",
|
||||
url: "#",
|
||||
icon: (
|
||||
<IconPlaceholder
|
||||
lucide="FileChartColumnIcon"
|
||||
tabler="IconReport"
|
||||
hugeicons="Analytics01Icon"
|
||||
phosphor="ChartLineIcon"
|
||||
remixicon="RiFileChartLine"
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "Word Assistant",
|
||||
url: "#",
|
||||
icon: (
|
||||
<IconPlaceholder
|
||||
lucide="FileIcon"
|
||||
tabler="IconFileWord"
|
||||
hugeicons="File01Icon"
|
||||
phosphor="FileIcon"
|
||||
remixicon="RiFileLine"
|
||||
/>
|
||||
),
|
||||
},
|
||||
],
|
||||
}
|
||||
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||
return (
|
||||
<Sidebar collapsible="offcanvas" {...props}>
|
||||
<SidebarHeader>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton
|
||||
className="data-[slot=sidebar-menu-button]:p-1.5!"
|
||||
render={<a href="#" />}
|
||||
>
|
||||
<IconPlaceholder
|
||||
lucide="CommandIcon"
|
||||
tabler="IconInnerShadowTop"
|
||||
hugeicons="CommandIcon"
|
||||
phosphor="CommandIcon"
|
||||
remixicon="RiCommandLine"
|
||||
className="size-5!"
|
||||
/>
|
||||
<span className="text-base font-semibold">Acme Inc.</span>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarHeader>
|
||||
<SidebarContent>
|
||||
<NavMain items={data.navMain} />
|
||||
<NavDocuments items={data.documents} />
|
||||
<NavSecondary items={data.navSecondary} className="mt-auto" />
|
||||
</SidebarContent>
|
||||
<SidebarFooter>
|
||||
<NavUser user={data.user} />
|
||||
</SidebarFooter>
|
||||
</Sidebar>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,300 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Area, AreaChart, CartesianGrid, XAxis } from "recharts"
|
||||
|
||||
import { useIsMobile } from "@/registry/bases/base/hooks/use-mobile"
|
||||
import {
|
||||
Card,
|
||||
CardAction,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/base/ui/card"
|
||||
import {
|
||||
ChartContainer,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
type ChartConfig,
|
||||
} from "@/registry/bases/base/ui/chart"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/registry/bases/base/ui/select"
|
||||
import {
|
||||
ToggleGroup,
|
||||
ToggleGroupItem,
|
||||
} from "@/registry/bases/base/ui/toggle-group"
|
||||
|
||||
export const description = "An interactive area chart"
|
||||
|
||||
const chartData = [
|
||||
{ date: "2024-04-01", desktop: 222, mobile: 150 },
|
||||
{ date: "2024-04-02", desktop: 97, mobile: 180 },
|
||||
{ date: "2024-04-03", desktop: 167, mobile: 120 },
|
||||
{ date: "2024-04-04", desktop: 242, mobile: 260 },
|
||||
{ date: "2024-04-05", desktop: 373, mobile: 290 },
|
||||
{ date: "2024-04-06", desktop: 301, mobile: 340 },
|
||||
{ date: "2024-04-07", desktop: 245, mobile: 180 },
|
||||
{ date: "2024-04-08", desktop: 409, mobile: 320 },
|
||||
{ date: "2024-04-09", desktop: 59, mobile: 110 },
|
||||
{ date: "2024-04-10", desktop: 261, mobile: 190 },
|
||||
{ date: "2024-04-11", desktop: 327, mobile: 350 },
|
||||
{ date: "2024-04-12", desktop: 292, mobile: 210 },
|
||||
{ date: "2024-04-13", desktop: 342, mobile: 380 },
|
||||
{ date: "2024-04-14", desktop: 137, mobile: 220 },
|
||||
{ date: "2024-04-15", desktop: 120, mobile: 170 },
|
||||
{ date: "2024-04-16", desktop: 138, mobile: 190 },
|
||||
{ date: "2024-04-17", desktop: 446, mobile: 360 },
|
||||
{ date: "2024-04-18", desktop: 364, mobile: 410 },
|
||||
{ date: "2024-04-19", desktop: 243, mobile: 180 },
|
||||
{ date: "2024-04-20", desktop: 89, mobile: 150 },
|
||||
{ date: "2024-04-21", desktop: 137, mobile: 200 },
|
||||
{ date: "2024-04-22", desktop: 224, mobile: 170 },
|
||||
{ date: "2024-04-23", desktop: 138, mobile: 230 },
|
||||
{ date: "2024-04-24", desktop: 387, mobile: 290 },
|
||||
{ date: "2024-04-25", desktop: 215, mobile: 250 },
|
||||
{ date: "2024-04-26", desktop: 75, mobile: 130 },
|
||||
{ date: "2024-04-27", desktop: 383, mobile: 420 },
|
||||
{ date: "2024-04-28", desktop: 122, mobile: 180 },
|
||||
{ date: "2024-04-29", desktop: 315, mobile: 240 },
|
||||
{ date: "2024-04-30", desktop: 454, mobile: 380 },
|
||||
{ date: "2024-05-01", desktop: 165, mobile: 220 },
|
||||
{ date: "2024-05-02", desktop: 293, mobile: 310 },
|
||||
{ date: "2024-05-03", desktop: 247, mobile: 190 },
|
||||
{ date: "2024-05-04", desktop: 385, mobile: 420 },
|
||||
{ date: "2024-05-05", desktop: 481, mobile: 390 },
|
||||
{ date: "2024-05-06", desktop: 498, mobile: 520 },
|
||||
{ date: "2024-05-07", desktop: 388, mobile: 300 },
|
||||
{ date: "2024-05-08", desktop: 149, mobile: 210 },
|
||||
{ date: "2024-05-09", desktop: 227, mobile: 180 },
|
||||
{ date: "2024-05-10", desktop: 293, mobile: 330 },
|
||||
{ date: "2024-05-11", desktop: 335, mobile: 270 },
|
||||
{ date: "2024-05-12", desktop: 197, mobile: 240 },
|
||||
{ date: "2024-05-13", desktop: 197, mobile: 160 },
|
||||
{ date: "2024-05-14", desktop: 448, mobile: 490 },
|
||||
{ date: "2024-05-15", desktop: 473, mobile: 380 },
|
||||
{ date: "2024-05-16", desktop: 338, mobile: 400 },
|
||||
{ date: "2024-05-17", desktop: 499, mobile: 420 },
|
||||
{ date: "2024-05-18", desktop: 315, mobile: 350 },
|
||||
{ date: "2024-05-19", desktop: 235, mobile: 180 },
|
||||
{ date: "2024-05-20", desktop: 177, mobile: 230 },
|
||||
{ date: "2024-05-21", desktop: 82, mobile: 140 },
|
||||
{ date: "2024-05-22", desktop: 81, mobile: 120 },
|
||||
{ date: "2024-05-23", desktop: 252, mobile: 290 },
|
||||
{ date: "2024-05-24", desktop: 294, mobile: 220 },
|
||||
{ date: "2024-05-25", desktop: 201, mobile: 250 },
|
||||
{ date: "2024-05-26", desktop: 213, mobile: 170 },
|
||||
{ date: "2024-05-27", desktop: 420, mobile: 460 },
|
||||
{ date: "2024-05-28", desktop: 233, mobile: 190 },
|
||||
{ date: "2024-05-29", desktop: 78, mobile: 130 },
|
||||
{ date: "2024-05-30", desktop: 340, mobile: 280 },
|
||||
{ date: "2024-05-31", desktop: 178, mobile: 230 },
|
||||
{ date: "2024-06-01", desktop: 178, mobile: 200 },
|
||||
{ date: "2024-06-02", desktop: 470, mobile: 410 },
|
||||
{ date: "2024-06-03", desktop: 103, mobile: 160 },
|
||||
{ date: "2024-06-04", desktop: 439, mobile: 380 },
|
||||
{ date: "2024-06-05", desktop: 88, mobile: 140 },
|
||||
{ date: "2024-06-06", desktop: 294, mobile: 250 },
|
||||
{ date: "2024-06-07", desktop: 323, mobile: 370 },
|
||||
{ date: "2024-06-08", desktop: 385, mobile: 320 },
|
||||
{ date: "2024-06-09", desktop: 438, mobile: 480 },
|
||||
{ date: "2024-06-10", desktop: 155, mobile: 200 },
|
||||
{ date: "2024-06-11", desktop: 92, mobile: 150 },
|
||||
{ date: "2024-06-12", desktop: 492, mobile: 420 },
|
||||
{ date: "2024-06-13", desktop: 81, mobile: 130 },
|
||||
{ date: "2024-06-14", desktop: 426, mobile: 380 },
|
||||
{ date: "2024-06-15", desktop: 307, mobile: 350 },
|
||||
{ date: "2024-06-16", desktop: 371, mobile: 310 },
|
||||
{ date: "2024-06-17", desktop: 475, mobile: 520 },
|
||||
{ date: "2024-06-18", desktop: 107, mobile: 170 },
|
||||
{ date: "2024-06-19", desktop: 341, mobile: 290 },
|
||||
{ date: "2024-06-20", desktop: 408, mobile: 450 },
|
||||
{ date: "2024-06-21", desktop: 169, mobile: 210 },
|
||||
{ date: "2024-06-22", desktop: 317, mobile: 270 },
|
||||
{ date: "2024-06-23", desktop: 480, mobile: 530 },
|
||||
{ date: "2024-06-24", desktop: 132, mobile: 180 },
|
||||
{ date: "2024-06-25", desktop: 141, mobile: 190 },
|
||||
{ date: "2024-06-26", desktop: 434, mobile: 380 },
|
||||
{ date: "2024-06-27", desktop: 448, mobile: 490 },
|
||||
{ date: "2024-06-28", desktop: 149, mobile: 200 },
|
||||
{ date: "2024-06-29", desktop: 103, mobile: 160 },
|
||||
{ date: "2024-06-30", desktop: 446, mobile: 400 },
|
||||
]
|
||||
|
||||
const chartConfig = {
|
||||
visitors: {
|
||||
label: "Visitors",
|
||||
},
|
||||
desktop: {
|
||||
label: "Desktop",
|
||||
color: "var(--primary)",
|
||||
},
|
||||
mobile: {
|
||||
label: "Mobile",
|
||||
color: "var(--primary)",
|
||||
},
|
||||
} satisfies ChartConfig
|
||||
|
||||
export function ChartAreaInteractive() {
|
||||
const isMobile = useIsMobile()
|
||||
const [timeRange, setTimeRange] = React.useState("90d")
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isMobile) {
|
||||
setTimeRange("7d")
|
||||
}
|
||||
}, [isMobile])
|
||||
|
||||
const filteredData = chartData.filter((item) => {
|
||||
const date = new Date(item.date)
|
||||
const referenceDate = new Date("2024-06-30")
|
||||
let daysToSubtract = 90
|
||||
if (timeRange === "30d") {
|
||||
daysToSubtract = 30
|
||||
} else if (timeRange === "7d") {
|
||||
daysToSubtract = 7
|
||||
}
|
||||
const startDate = new Date(referenceDate)
|
||||
startDate.setDate(startDate.getDate() - daysToSubtract)
|
||||
return date >= startDate
|
||||
})
|
||||
|
||||
return (
|
||||
<Card className="@container/card">
|
||||
<CardHeader>
|
||||
<CardTitle>Total Visitors</CardTitle>
|
||||
<CardDescription>
|
||||
<span className="hidden @[540px]/card:block">
|
||||
Total for the last 3 months
|
||||
</span>
|
||||
<span className="@[540px]/card:hidden">Last 3 months</span>
|
||||
</CardDescription>
|
||||
<CardAction>
|
||||
<ToggleGroup
|
||||
multiple={false}
|
||||
value={timeRange ? [timeRange] : []}
|
||||
onValueChange={(value) => {
|
||||
setTimeRange(value[0] ?? "90d")
|
||||
}}
|
||||
variant="outline"
|
||||
className="hidden *:data-[slot=toggle-group-item]:px-4! @[767px]/card:flex"
|
||||
>
|
||||
<ToggleGroupItem value="90d">Last 3 months</ToggleGroupItem>
|
||||
<ToggleGroupItem value="30d">Last 30 days</ToggleGroupItem>
|
||||
<ToggleGroupItem value="7d">Last 7 days</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
<Select
|
||||
value={timeRange}
|
||||
onValueChange={(value) => {
|
||||
if (value !== null) {
|
||||
setTimeRange(value)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SelectTrigger
|
||||
className="flex w-40 **:data-[slot=select-value]:block **:data-[slot=select-value]:truncate @[767px]/card:hidden"
|
||||
size="sm"
|
||||
aria-label="Select a value"
|
||||
>
|
||||
<SelectValue placeholder="Last 3 months" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="rounded-xl">
|
||||
<SelectItem value="90d" className="rounded-lg">
|
||||
Last 3 months
|
||||
</SelectItem>
|
||||
<SelectItem value="30d" className="rounded-lg">
|
||||
Last 30 days
|
||||
</SelectItem>
|
||||
<SelectItem value="7d" className="rounded-lg">
|
||||
Last 7 days
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
<CardContent className="px-2 pt-4 sm:px-6 sm:pt-6">
|
||||
<ChartContainer
|
||||
config={chartConfig}
|
||||
className="aspect-auto h-[250px] w-full"
|
||||
>
|
||||
<AreaChart data={filteredData}>
|
||||
<defs>
|
||||
<linearGradient id="fillDesktop" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop
|
||||
offset="5%"
|
||||
stopColor="var(--color-desktop)"
|
||||
stopOpacity={1.0}
|
||||
/>
|
||||
<stop
|
||||
offset="95%"
|
||||
stopColor="var(--color-desktop)"
|
||||
stopOpacity={0.1}
|
||||
/>
|
||||
</linearGradient>
|
||||
<linearGradient id="fillMobile" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop
|
||||
offset="5%"
|
||||
stopColor="var(--color-mobile)"
|
||||
stopOpacity={0.8}
|
||||
/>
|
||||
<stop
|
||||
offset="95%"
|
||||
stopColor="var(--color-mobile)"
|
||||
stopOpacity={0.1}
|
||||
/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<CartesianGrid vertical={false} />
|
||||
<XAxis
|
||||
dataKey="date"
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickMargin={8}
|
||||
minTickGap={32}
|
||||
tickFormatter={(value) => {
|
||||
const date = new Date(value)
|
||||
return date.toLocaleDateString("en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
})
|
||||
}}
|
||||
/>
|
||||
<ChartTooltip
|
||||
cursor={false}
|
||||
content={
|
||||
<ChartTooltipContent
|
||||
labelFormatter={(value) => {
|
||||
return new Date(value).toLocaleDateString("en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
})
|
||||
}}
|
||||
indicator="dot"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Area
|
||||
dataKey="mobile"
|
||||
type="natural"
|
||||
fill="url(#fillMobile)"
|
||||
stroke="var(--color-mobile)"
|
||||
stackId="a"
|
||||
/>
|
||||
<Area
|
||||
dataKey="desktop"
|
||||
type="natural"
|
||||
fill="url(#fillDesktop)"
|
||||
stroke="var(--color-desktop)"
|
||||
stackId="a"
|
||||
/>
|
||||
</AreaChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,953 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import {
|
||||
closestCenter,
|
||||
DndContext,
|
||||
KeyboardSensor,
|
||||
MouseSensor,
|
||||
TouchSensor,
|
||||
useSensor,
|
||||
useSensors,
|
||||
type DragEndEvent,
|
||||
type UniqueIdentifier,
|
||||
} from "@dnd-kit/core"
|
||||
import { restrictToVerticalAxis } from "@dnd-kit/modifiers"
|
||||
import {
|
||||
arrayMove,
|
||||
SortableContext,
|
||||
useSortable,
|
||||
verticalListSortingStrategy,
|
||||
} from "@dnd-kit/sortable"
|
||||
import { CSS } from "@dnd-kit/utilities"
|
||||
import {
|
||||
columnFilteringFeature,
|
||||
columnVisibilityFeature,
|
||||
createColumnHelper,
|
||||
createFilteredRowModel,
|
||||
createPaginatedRowModel,
|
||||
createSortedRowModel,
|
||||
FlexRender,
|
||||
rowPaginationFeature,
|
||||
rowSelectionFeature,
|
||||
rowSortingFeature,
|
||||
tableFeatures,
|
||||
useTable,
|
||||
type ColumnFiltersState,
|
||||
type ColumnVisibilityState,
|
||||
type Row,
|
||||
type SortingState,
|
||||
} from "@tanstack/react-table"
|
||||
import { Area, AreaChart, CartesianGrid, XAxis } from "recharts"
|
||||
import { toast } from "sonner"
|
||||
import { z } from "zod"
|
||||
|
||||
import { useIsMobile } from "@/registry/bases/base/hooks/use-mobile"
|
||||
import { Badge } from "@/registry/bases/base/ui/badge"
|
||||
import { Button } from "@/registry/bases/base/ui/button"
|
||||
import {
|
||||
ChartContainer,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
type ChartConfig,
|
||||
} from "@/registry/bases/base/ui/chart"
|
||||
import { Checkbox } from "@/registry/bases/base/ui/checkbox"
|
||||
import {
|
||||
Drawer,
|
||||
DrawerClose,
|
||||
DrawerContent,
|
||||
DrawerDescription,
|
||||
DrawerFooter,
|
||||
DrawerHeader,
|
||||
DrawerTitle,
|
||||
DrawerTrigger,
|
||||
} from "@/registry/bases/base/ui/drawer"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/registry/bases/base/ui/dropdown-menu"
|
||||
import { Input } from "@/registry/bases/base/ui/input"
|
||||
import { Label } from "@/registry/bases/base/ui/label"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/registry/bases/base/ui/select"
|
||||
import { Separator } from "@/registry/bases/base/ui/separator"
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/registry/bases/base/ui/table"
|
||||
import {
|
||||
Tabs,
|
||||
TabsContent,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
} from "@/registry/bases/base/ui/tabs"
|
||||
import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"
|
||||
|
||||
// New in v9: declare the features this table uses — anything you don't
|
||||
// register is tree-shaken out of the bundle.
|
||||
const features = tableFeatures({
|
||||
columnFilteringFeature,
|
||||
columnVisibilityFeature,
|
||||
rowPaginationFeature,
|
||||
rowSelectionFeature,
|
||||
rowSortingFeature,
|
||||
filteredRowModel: createFilteredRowModel(),
|
||||
paginatedRowModel: createPaginatedRowModel(),
|
||||
sortedRowModel: createSortedRowModel(),
|
||||
})
|
||||
|
||||
const columnHelper = createColumnHelper<
|
||||
typeof features,
|
||||
z.infer<typeof schema>
|
||||
>()
|
||||
|
||||
export const schema = z.object({
|
||||
id: z.number(),
|
||||
header: z.string(),
|
||||
type: z.string(),
|
||||
status: z.string(),
|
||||
target: z.string(),
|
||||
limit: z.string(),
|
||||
reviewer: z.string(),
|
||||
})
|
||||
|
||||
// Create a separate component for the drag handle
|
||||
function DragHandle({ id }: { id: number }) {
|
||||
const { attributes, listeners } = useSortable({
|
||||
id,
|
||||
})
|
||||
return (
|
||||
<Button
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-7 text-muted-foreground hover:bg-transparent"
|
||||
>
|
||||
<IconPlaceholder
|
||||
lucide="GripVerticalIcon"
|
||||
tabler="IconGripVertical"
|
||||
hugeicons="DragDropVerticalIcon"
|
||||
phosphor="DotsSixVerticalIcon"
|
||||
remixicon="RiDraggable"
|
||||
className="size-3 text-muted-foreground"
|
||||
/>
|
||||
<span className="sr-only">Drag to reorder</span>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
const columns = columnHelper.columns([
|
||||
columnHelper.display({
|
||||
id: "drag",
|
||||
header: () => null,
|
||||
cell: ({ row }) => <DragHandle id={row.original.id} />,
|
||||
}),
|
||||
columnHelper.display({
|
||||
id: "select",
|
||||
header: ({ table }) => (
|
||||
<div className="flex items-center justify-center">
|
||||
<Checkbox
|
||||
checked={table.getIsAllPageRowsSelected()}
|
||||
indeterminate={
|
||||
table.getIsSomePageRowsSelected() &&
|
||||
!table.getIsAllPageRowsSelected()
|
||||
}
|
||||
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
|
||||
aria-label="Select all"
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<div className="flex items-center justify-center">
|
||||
<Checkbox
|
||||
checked={row.getIsSelected()}
|
||||
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
||||
aria-label="Select row"
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
}),
|
||||
columnHelper.accessor("header", {
|
||||
header: "Header",
|
||||
cell: ({ row }) => {
|
||||
return <TableCellViewer item={row.original} />
|
||||
},
|
||||
enableHiding: false,
|
||||
}),
|
||||
columnHelper.accessor("type", {
|
||||
header: "Section Type",
|
||||
cell: ({ row }) => (
|
||||
<div className="w-32">
|
||||
<Badge variant="outline" className="px-1.5 text-muted-foreground">
|
||||
{row.original.type}
|
||||
</Badge>
|
||||
</div>
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor("status", {
|
||||
header: "Status",
|
||||
cell: ({ row }) => (
|
||||
<Badge variant="outline" className="px-1.5 text-muted-foreground">
|
||||
{row.original.status === "Done" ? (
|
||||
<IconPlaceholder
|
||||
lucide="CircleCheckIcon"
|
||||
tabler="IconCircleCheckFilled"
|
||||
hugeicons="CheckmarkCircle01Icon"
|
||||
phosphor="CheckCircleIcon"
|
||||
remixicon="RiCheckboxCircleFill"
|
||||
className="fill-green-500 dark:fill-green-400"
|
||||
/>
|
||||
) : (
|
||||
<IconPlaceholder
|
||||
lucide="LoaderIcon"
|
||||
tabler="IconLoader"
|
||||
hugeicons="Loading03Icon"
|
||||
phosphor="SpinnerIcon"
|
||||
remixicon="RiLoader4Line"
|
||||
/>
|
||||
)}
|
||||
{row.original.status}
|
||||
</Badge>
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor("target", {
|
||||
header: () => <div className="w-full text-right">Target</div>,
|
||||
cell: ({ row }) => (
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault()
|
||||
toast.promise(new Promise((resolve) => setTimeout(resolve, 1000)), {
|
||||
loading: `Saving ${row.original.header}`,
|
||||
success: "Done",
|
||||
error: "Error",
|
||||
})
|
||||
}}
|
||||
>
|
||||
<Label htmlFor={`${row.original.id}-target`} className="sr-only">
|
||||
Target
|
||||
</Label>
|
||||
<Input
|
||||
className="h-8 w-16 border-transparent bg-transparent text-right shadow-none hover:bg-input/30 focus-visible:border focus-visible:bg-background dark:bg-transparent dark:hover:bg-input/30 dark:focus-visible:bg-input/30"
|
||||
defaultValue={row.original.target}
|
||||
id={`${row.original.id}-target`}
|
||||
/>
|
||||
</form>
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor("limit", {
|
||||
header: () => <div className="w-full text-right">Limit</div>,
|
||||
cell: ({ row }) => (
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault()
|
||||
toast.promise(new Promise((resolve) => setTimeout(resolve, 1000)), {
|
||||
loading: `Saving ${row.original.header}`,
|
||||
success: "Done",
|
||||
error: "Error",
|
||||
})
|
||||
}}
|
||||
>
|
||||
<Label htmlFor={`${row.original.id}-limit`} className="sr-only">
|
||||
Limit
|
||||
</Label>
|
||||
<Input
|
||||
className="h-8 w-16 border-transparent bg-transparent text-right shadow-none hover:bg-input/30 focus-visible:border focus-visible:bg-background dark:bg-transparent dark:hover:bg-input/30 dark:focus-visible:bg-input/30"
|
||||
defaultValue={row.original.limit}
|
||||
id={`${row.original.id}-limit`}
|
||||
/>
|
||||
</form>
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor("reviewer", {
|
||||
header: "Reviewer",
|
||||
cell: ({ row }) => {
|
||||
const isAssigned = row.original.reviewer !== "Assign reviewer"
|
||||
if (isAssigned) {
|
||||
return row.original.reviewer
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<Label htmlFor={`${row.original.id}-reviewer`} className="sr-only">
|
||||
Reviewer
|
||||
</Label>
|
||||
<Select
|
||||
items={[
|
||||
{ label: "Eddie Lake", value: "Eddie Lake" },
|
||||
{ label: "Jamik Tashpulatov", value: "Jamik Tashpulatov" },
|
||||
]}
|
||||
>
|
||||
<SelectTrigger
|
||||
className="w-38 **:data-[slot=select-value]:block **:data-[slot=select-value]:truncate"
|
||||
size="sm"
|
||||
id={`${row.original.id}-reviewer`}
|
||||
>
|
||||
<SelectValue placeholder="Assign reviewer" />
|
||||
</SelectTrigger>
|
||||
<SelectContent align="end">
|
||||
<SelectGroup>
|
||||
<SelectItem value="Eddie Lake">Eddie Lake</SelectItem>
|
||||
<SelectItem value="Jamik Tashpulatov">
|
||||
Jamik Tashpulatov
|
||||
</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</>
|
||||
)
|
||||
},
|
||||
}),
|
||||
columnHelper.display({
|
||||
id: "actions",
|
||||
cell: () => (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
render={
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="flex size-8 text-muted-foreground data-open:bg-muted"
|
||||
size="icon"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<IconPlaceholder
|
||||
lucide="EllipsisVerticalIcon"
|
||||
tabler="IconDotsVertical"
|
||||
hugeicons="MoreVerticalCircle01Icon"
|
||||
phosphor="DotsThreeVerticalIcon"
|
||||
remixicon="RiMore2Line"
|
||||
/>
|
||||
<span className="sr-only">Open menu</span>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-32">
|
||||
<DropdownMenuItem>Edit</DropdownMenuItem>
|
||||
<DropdownMenuItem>Make a copy</DropdownMenuItem>
|
||||
<DropdownMenuItem>Favorite</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem variant="destructive">Delete</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
),
|
||||
}),
|
||||
])
|
||||
function DraggableRow({
|
||||
row,
|
||||
}: {
|
||||
row: Row<typeof features, z.infer<typeof schema>>
|
||||
}) {
|
||||
const { transform, transition, setNodeRef, isDragging } = useSortable({
|
||||
id: row.original.id,
|
||||
})
|
||||
return (
|
||||
<TableRow
|
||||
data-state={row.getIsSelected() && "selected"}
|
||||
data-dragging={isDragging}
|
||||
ref={setNodeRef}
|
||||
className="relative z-0 data-[dragging=true]:z-10 data-[dragging=true]:opacity-80"
|
||||
style={{
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition: transition,
|
||||
}}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id}>
|
||||
<FlexRender cell={cell} />
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
)
|
||||
}
|
||||
export function DataTable({
|
||||
data: initialData,
|
||||
}: {
|
||||
data: z.infer<typeof schema>[]
|
||||
}) {
|
||||
const [data, setData] = React.useState(() => initialData)
|
||||
const [rowSelection, setRowSelection] = React.useState({})
|
||||
const [columnVisibility, setColumnVisibility] =
|
||||
React.useState<ColumnVisibilityState>({})
|
||||
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
|
||||
[]
|
||||
)
|
||||
const [sorting, setSorting] = React.useState<SortingState>([])
|
||||
const [pagination, setPagination] = React.useState({
|
||||
pageIndex: 0,
|
||||
pageSize: 10,
|
||||
})
|
||||
const sortableId = React.useId()
|
||||
const sensors = useSensors(
|
||||
useSensor(MouseSensor, {}),
|
||||
useSensor(TouchSensor, {}),
|
||||
useSensor(KeyboardSensor, {})
|
||||
)
|
||||
const dataIds = React.useMemo<UniqueIdentifier[]>(
|
||||
() => data?.map(({ id }) => id) || [],
|
||||
[data]
|
||||
)
|
||||
const table = useTable({
|
||||
features,
|
||||
data,
|
||||
columns,
|
||||
state: {
|
||||
sorting,
|
||||
columnVisibility,
|
||||
rowSelection,
|
||||
columnFilters,
|
||||
pagination,
|
||||
},
|
||||
getRowId: (row) => row.id.toString(),
|
||||
enableRowSelection: true,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
onSortingChange: setSorting,
|
||||
onColumnFiltersChange: setColumnFilters,
|
||||
onColumnVisibilityChange: setColumnVisibility,
|
||||
onPaginationChange: setPagination,
|
||||
})
|
||||
function handleDragEnd(event: DragEndEvent) {
|
||||
const { active, over } = event
|
||||
if (active && over && active.id !== over.id) {
|
||||
setData((data) => {
|
||||
const oldIndex = dataIds.indexOf(active.id)
|
||||
const newIndex = dataIds.indexOf(over.id)
|
||||
return arrayMove(data, oldIndex, newIndex)
|
||||
})
|
||||
}
|
||||
}
|
||||
return (
|
||||
<Tabs
|
||||
defaultValue="outline"
|
||||
className="w-full flex-col justify-start gap-6"
|
||||
>
|
||||
<div className="flex items-center justify-between px-4 lg:px-6">
|
||||
<Label htmlFor="view-selector" className="sr-only">
|
||||
View
|
||||
</Label>
|
||||
<Select
|
||||
defaultValue="outline"
|
||||
items={[
|
||||
{ label: "Outline", value: "outline" },
|
||||
{ label: "Past Performance", value: "past-performance" },
|
||||
{ label: "Key Personnel", value: "key-personnel" },
|
||||
{ label: "Focus Documents", value: "focus-documents" },
|
||||
]}
|
||||
>
|
||||
<SelectTrigger
|
||||
className="flex w-fit @4xl/main:hidden"
|
||||
size="sm"
|
||||
id="view-selector"
|
||||
>
|
||||
<SelectValue placeholder="Select a view" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem value="outline">Outline</SelectItem>
|
||||
<SelectItem value="past-performance">Past Performance</SelectItem>
|
||||
<SelectItem value="key-personnel">Key Personnel</SelectItem>
|
||||
<SelectItem value="focus-documents">Focus Documents</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<TabsList className="hidden **:data-[slot=badge]:size-5 **:data-[slot=badge]:rounded-full **:data-[slot=badge]:bg-muted-foreground/30 **:data-[slot=badge]:px-1 @4xl/main:flex">
|
||||
<TabsTrigger value="outline">Outline</TabsTrigger>
|
||||
<TabsTrigger value="past-performance">
|
||||
Past Performance <Badge variant="secondary">3</Badge>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="key-personnel">
|
||||
Key Personnel <Badge variant="secondary">2</Badge>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="focus-documents">Focus Documents</TabsTrigger>
|
||||
</TabsList>
|
||||
<div className="flex items-center gap-2">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
render={<Button variant="outline" size="sm" />}
|
||||
>
|
||||
<IconPlaceholder
|
||||
lucide="Columns3Icon"
|
||||
tabler="IconLayoutColumns"
|
||||
hugeicons="LeftToRightListBulletIcon"
|
||||
phosphor="ColumnsIcon"
|
||||
remixicon="RiLayoutColumnLine"
|
||||
data-icon="inline-start"
|
||||
/>
|
||||
Columns
|
||||
<IconPlaceholder
|
||||
lucide="ChevronDownIcon"
|
||||
tabler="IconChevronDown"
|
||||
hugeicons="ArrowDown01Icon"
|
||||
phosphor="CaretDownIcon"
|
||||
remixicon="RiArrowDownSLine"
|
||||
data-icon="inline-end"
|
||||
/>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-32">
|
||||
{table
|
||||
.getAllColumns()
|
||||
.filter(
|
||||
(column) =>
|
||||
typeof column.accessorFn !== "undefined" &&
|
||||
column.getCanHide()
|
||||
)
|
||||
.map((column) => {
|
||||
return (
|
||||
<DropdownMenuCheckboxItem
|
||||
key={column.id}
|
||||
className="capitalize"
|
||||
checked={column.getIsVisible()}
|
||||
onCheckedChange={(value) =>
|
||||
column.toggleVisibility(!!value)
|
||||
}
|
||||
>
|
||||
{column.id}
|
||||
</DropdownMenuCheckboxItem>
|
||||
)
|
||||
})}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<Button variant="outline" size="sm">
|
||||
<IconPlaceholder
|
||||
lucide="PlusIcon"
|
||||
tabler="IconPlus"
|
||||
hugeicons="Add01Icon"
|
||||
phosphor="PlusIcon"
|
||||
remixicon="RiAddLine"
|
||||
/>
|
||||
<span className="hidden lg:inline">Add Section</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<TabsContent
|
||||
value="outline"
|
||||
className="relative flex flex-col gap-4 overflow-auto px-4 lg:px-6"
|
||||
>
|
||||
<div className="overflow-hidden rounded-lg border">
|
||||
<DndContext
|
||||
collisionDetection={closestCenter}
|
||||
modifiers={[restrictToVerticalAxis]}
|
||||
onDragEnd={handleDragEnd}
|
||||
sensors={sensors}
|
||||
id={sortableId}
|
||||
>
|
||||
<Table>
|
||||
<TableHeader className="sticky top-0 z-10 bg-muted">
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => {
|
||||
return (
|
||||
<TableHead key={header.id} colSpan={header.colSpan}>
|
||||
{header.isPlaceholder ? null : (
|
||||
<FlexRender header={header} />
|
||||
)}
|
||||
</TableHead>
|
||||
)
|
||||
})}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody className="**:data-[slot=table-cell]:first:w-8">
|
||||
{table.getRowModel().rows?.length ? (
|
||||
<SortableContext
|
||||
items={dataIds}
|
||||
strategy={verticalListSortingStrategy}
|
||||
>
|
||||
{table.getRowModel().rows.map((row) => (
|
||||
<DraggableRow key={row.id} row={row} />
|
||||
))}
|
||||
</SortableContext>
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={columns.length}
|
||||
className="h-24 text-center"
|
||||
>
|
||||
No results.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</DndContext>
|
||||
</div>
|
||||
<div className="flex items-center justify-between px-4">
|
||||
<div className="hidden flex-1 text-sm text-muted-foreground lg:flex">
|
||||
{table.getFilteredSelectedRowModel().rows.length} of{" "}
|
||||
{table.getFilteredRowModel().rows.length} row(s) selected.
|
||||
</div>
|
||||
<div className="flex w-full items-center gap-8 lg:w-fit">
|
||||
<div className="hidden items-center gap-2 lg:flex">
|
||||
<Label htmlFor="rows-per-page" className="text-sm font-medium">
|
||||
Rows per page
|
||||
</Label>
|
||||
<Select
|
||||
value={`${table.state.pagination.pageSize}`}
|
||||
onValueChange={(value) => {
|
||||
table.setPageSize(Number(value))
|
||||
}}
|
||||
items={[10, 20, 30, 40, 50].map((pageSize) => ({
|
||||
label: `${pageSize}`,
|
||||
value: `${pageSize}`,
|
||||
}))}
|
||||
>
|
||||
<SelectTrigger size="sm" className="w-20" id="rows-per-page">
|
||||
<SelectValue placeholder={table.state.pagination.pageSize} />
|
||||
</SelectTrigger>
|
||||
<SelectContent side="top">
|
||||
<SelectGroup>
|
||||
{[10, 20, 30, 40, 50].map((pageSize) => (
|
||||
<SelectItem key={pageSize} value={`${pageSize}`}>
|
||||
{pageSize}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex w-fit items-center justify-center text-sm font-medium">
|
||||
Page {table.state.pagination.pageIndex + 1} of{" "}
|
||||
{table.getPageCount()}
|
||||
</div>
|
||||
<div className="ml-auto flex items-center gap-2 lg:ml-0">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="hidden h-8 w-8 p-0 lg:flex"
|
||||
onClick={() => table.setPageIndex(0)}
|
||||
disabled={!table.getCanPreviousPage()}
|
||||
>
|
||||
<span className="sr-only">Go to first page</span>
|
||||
<IconPlaceholder
|
||||
lucide="ChevronsLeftIcon"
|
||||
tabler="IconChevronsLeft"
|
||||
hugeicons="ArrowLeftDoubleIcon"
|
||||
phosphor="CaretDoubleLeftIcon"
|
||||
remixicon="RiSkipLeftLine"
|
||||
/>
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="size-8"
|
||||
size="icon"
|
||||
onClick={() => table.previousPage()}
|
||||
disabled={!table.getCanPreviousPage()}
|
||||
>
|
||||
<span className="sr-only">Go to previous page</span>
|
||||
<IconPlaceholder
|
||||
lucide="ChevronLeftIcon"
|
||||
tabler="IconChevronLeft"
|
||||
hugeicons="ArrowLeft01Icon"
|
||||
phosphor="CaretLeftIcon"
|
||||
remixicon="RiArrowLeftSLine"
|
||||
/>
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="size-8"
|
||||
size="icon"
|
||||
onClick={() => table.nextPage()}
|
||||
disabled={!table.getCanNextPage()}
|
||||
>
|
||||
<span className="sr-only">Go to next page</span>
|
||||
<IconPlaceholder
|
||||
lucide="ChevronRightIcon"
|
||||
tabler="IconChevronRight"
|
||||
hugeicons="ArrowRight01Icon"
|
||||
phosphor="CaretRightIcon"
|
||||
remixicon="RiArrowRightSLine"
|
||||
/>
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="hidden size-8 lg:flex"
|
||||
size="icon"
|
||||
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
|
||||
disabled={!table.getCanNextPage()}
|
||||
>
|
||||
<span className="sr-only">Go to last page</span>
|
||||
<IconPlaceholder
|
||||
lucide="ChevronsRightIcon"
|
||||
tabler="IconChevronsRight"
|
||||
hugeicons="ArrowRightDoubleIcon"
|
||||
phosphor="CaretDoubleRightIcon"
|
||||
remixicon="RiSkipRightLine"
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent
|
||||
value="past-performance"
|
||||
className="flex flex-col px-4 lg:px-6"
|
||||
>
|
||||
<div className="aspect-video w-full flex-1 rounded-lg border border-dashed"></div>
|
||||
</TabsContent>
|
||||
<TabsContent value="key-personnel" className="flex flex-col px-4 lg:px-6">
|
||||
<div className="aspect-video w-full flex-1 rounded-lg border border-dashed"></div>
|
||||
</TabsContent>
|
||||
<TabsContent
|
||||
value="focus-documents"
|
||||
className="flex flex-col px-4 lg:px-6"
|
||||
>
|
||||
<div className="aspect-video w-full flex-1 rounded-lg border border-dashed"></div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
)
|
||||
}
|
||||
const chartData = [
|
||||
{
|
||||
month: "January",
|
||||
desktop: 186,
|
||||
mobile: 80,
|
||||
},
|
||||
{
|
||||
month: "February",
|
||||
desktop: 305,
|
||||
mobile: 200,
|
||||
},
|
||||
{
|
||||
month: "March",
|
||||
desktop: 237,
|
||||
mobile: 120,
|
||||
},
|
||||
{
|
||||
month: "April",
|
||||
desktop: 73,
|
||||
mobile: 190,
|
||||
},
|
||||
{
|
||||
month: "May",
|
||||
desktop: 209,
|
||||
mobile: 130,
|
||||
},
|
||||
{
|
||||
month: "June",
|
||||
desktop: 214,
|
||||
mobile: 140,
|
||||
},
|
||||
]
|
||||
const chartConfig = {
|
||||
desktop: {
|
||||
label: "Desktop",
|
||||
color: "var(--primary)",
|
||||
},
|
||||
mobile: {
|
||||
label: "Mobile",
|
||||
color: "var(--primary)",
|
||||
},
|
||||
} satisfies ChartConfig
|
||||
function TableCellViewer({ item }: { item: z.infer<typeof schema> }) {
|
||||
const isMobile = useIsMobile()
|
||||
return (
|
||||
<Drawer swipeDirection={isMobile ? "down" : "right"}>
|
||||
<DrawerTrigger
|
||||
render={
|
||||
<Button
|
||||
variant="link"
|
||||
className="w-fit px-0 text-left text-foreground"
|
||||
/>
|
||||
}
|
||||
>
|
||||
{item.header}
|
||||
</DrawerTrigger>
|
||||
<DrawerContent>
|
||||
<DrawerHeader className="gap-1">
|
||||
<DrawerTitle>{item.header}</DrawerTitle>
|
||||
<DrawerDescription>
|
||||
Showing total visitors for the last 6 months
|
||||
</DrawerDescription>
|
||||
</DrawerHeader>
|
||||
<div className="flex flex-col gap-4 overflow-y-auto px-4 text-sm">
|
||||
{!isMobile && (
|
||||
<>
|
||||
<ChartContainer config={chartConfig}>
|
||||
<AreaChart
|
||||
accessibilityLayer
|
||||
data={chartData}
|
||||
margin={{
|
||||
left: 0,
|
||||
right: 10,
|
||||
}}
|
||||
>
|
||||
<CartesianGrid vertical={false} />
|
||||
<XAxis
|
||||
dataKey="month"
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickMargin={8}
|
||||
tickFormatter={(value) => value.slice(0, 3)}
|
||||
hide
|
||||
/>
|
||||
<ChartTooltip
|
||||
cursor={false}
|
||||
content={<ChartTooltipContent indicator="dot" />}
|
||||
/>
|
||||
<Area
|
||||
dataKey="mobile"
|
||||
type="natural"
|
||||
fill="var(--color-mobile)"
|
||||
fillOpacity={0.6}
|
||||
stroke="var(--color-mobile)"
|
||||
stackId="a"
|
||||
/>
|
||||
<Area
|
||||
dataKey="desktop"
|
||||
type="natural"
|
||||
fill="var(--color-desktop)"
|
||||
fillOpacity={0.4}
|
||||
stroke="var(--color-desktop)"
|
||||
stackId="a"
|
||||
/>
|
||||
</AreaChart>
|
||||
</ChartContainer>
|
||||
<Separator />
|
||||
<div className="grid gap-2">
|
||||
<div className="flex gap-2 leading-none font-medium">
|
||||
Trending up by 5.2% this month{" "}
|
||||
<IconPlaceholder
|
||||
lucide="TrendingUpIcon"
|
||||
tabler="IconTrendingUp"
|
||||
hugeicons="ChartUpIcon"
|
||||
phosphor="TrendUpIcon"
|
||||
remixicon="RiArrowUpLine"
|
||||
className="size-4"
|
||||
/>
|
||||
</div>
|
||||
<div className="text-muted-foreground">
|
||||
Showing total visitors for the last 6 months. This is just
|
||||
some random text to test the layout. It spans multiple lines
|
||||
and should wrap around.
|
||||
</div>
|
||||
</div>
|
||||
<Separator />
|
||||
</>
|
||||
)}
|
||||
<form className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-3">
|
||||
<Label htmlFor="header">Header</Label>
|
||||
<Input id="header" defaultValue={item.header} />
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="flex flex-col gap-3">
|
||||
<Label htmlFor="type">Type</Label>
|
||||
<Select
|
||||
defaultValue={item.type}
|
||||
items={[
|
||||
{ label: "Table of Contents", value: "Table of Contents" },
|
||||
{ label: "Executive Summary", value: "Executive Summary" },
|
||||
{
|
||||
label: "Technical Approach",
|
||||
value: "Technical Approach",
|
||||
},
|
||||
{ label: "Design", value: "Design" },
|
||||
{ label: "Capabilities", value: "Capabilities" },
|
||||
{ label: "Focus Documents", value: "Focus Documents" },
|
||||
{ label: "Narrative", value: "Narrative" },
|
||||
{ label: "Cover Page", value: "Cover Page" },
|
||||
]}
|
||||
>
|
||||
<SelectTrigger id="type" className="w-full">
|
||||
<SelectValue placeholder="Select a type" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem value="Table of Contents">
|
||||
Table of Contents
|
||||
</SelectItem>
|
||||
<SelectItem value="Executive Summary">
|
||||
Executive Summary
|
||||
</SelectItem>
|
||||
<SelectItem value="Technical Approach">
|
||||
Technical Approach
|
||||
</SelectItem>
|
||||
<SelectItem value="Design">Design</SelectItem>
|
||||
<SelectItem value="Capabilities">Capabilities</SelectItem>
|
||||
<SelectItem value="Focus Documents">
|
||||
Focus Documents
|
||||
</SelectItem>
|
||||
<SelectItem value="Narrative">Narrative</SelectItem>
|
||||
<SelectItem value="Cover Page">Cover Page</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
<Label htmlFor="status">Status</Label>
|
||||
<Select
|
||||
defaultValue={item.status}
|
||||
items={[
|
||||
{ label: "Done", value: "Done" },
|
||||
{ label: "In Progress", value: "In Progress" },
|
||||
{ label: "Not Started", value: "Not Started" },
|
||||
]}
|
||||
>
|
||||
<SelectTrigger id="status" className="w-full">
|
||||
<SelectValue placeholder="Select a status" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem value="Done">Done</SelectItem>
|
||||
<SelectItem value="In Progress">In Progress</SelectItem>
|
||||
<SelectItem value="Not Started">Not Started</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="flex flex-col gap-3">
|
||||
<Label htmlFor="target">Target</Label>
|
||||
<Input id="target" defaultValue={item.target} />
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
<Label htmlFor="limit">Limit</Label>
|
||||
<Input id="limit" defaultValue={item.limit} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
<Label htmlFor="reviewer">Reviewer</Label>
|
||||
<Select
|
||||
defaultValue={item.reviewer}
|
||||
items={[
|
||||
{ label: "Eddie Lake", value: "Eddie Lake" },
|
||||
{ label: "Jamik Tashpulatov", value: "Jamik Tashpulatov" },
|
||||
{ label: "Emily Whalen", value: "Emily Whalen" },
|
||||
]}
|
||||
>
|
||||
<SelectTrigger id="reviewer" className="w-full">
|
||||
<SelectValue placeholder="Select a reviewer" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem value="Eddie Lake">Eddie Lake</SelectItem>
|
||||
<SelectItem value="Jamik Tashpulatov">
|
||||
Jamik Tashpulatov
|
||||
</SelectItem>
|
||||
<SelectItem value="Emily Whalen">Emily Whalen</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<DrawerFooter>
|
||||
<Button>Submit</Button>
|
||||
<DrawerClose render={<Button variant="outline" />}>Done</DrawerClose>
|
||||
</DrawerFooter>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
"use client"
|
||||
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/registry/bases/base/ui/dropdown-menu"
|
||||
import {
|
||||
SidebarGroup,
|
||||
SidebarGroupLabel,
|
||||
SidebarMenu,
|
||||
SidebarMenuAction,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
useSidebar,
|
||||
} from "@/registry/bases/base/ui/sidebar"
|
||||
import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"
|
||||
|
||||
export function NavDocuments({
|
||||
items,
|
||||
}: {
|
||||
items: {
|
||||
name: string
|
||||
url: string
|
||||
icon: React.ReactNode
|
||||
}[]
|
||||
}) {
|
||||
const { isMobile } = useSidebar()
|
||||
return (
|
||||
<SidebarGroup className="group-data-[collapsible=icon]:hidden">
|
||||
<SidebarGroupLabel>Documents</SidebarGroupLabel>
|
||||
<SidebarMenu>
|
||||
{items.map((item) => (
|
||||
<SidebarMenuItem key={item.name}>
|
||||
<SidebarMenuButton render={<a href={item.url} />}>
|
||||
{item.icon}
|
||||
<span>{item.name}</span>
|
||||
</SidebarMenuButton>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
render={
|
||||
<SidebarMenuAction
|
||||
showOnHover
|
||||
className="aria-expanded:bg-muted"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<IconPlaceholder
|
||||
lucide="MoreHorizontalIcon"
|
||||
tabler="IconDots"
|
||||
hugeicons="MoreHorizontalCircle01Icon"
|
||||
phosphor="DotsThreeOutlineIcon"
|
||||
remixicon="RiMoreLine"
|
||||
/>
|
||||
<span className="sr-only">More</span>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
className="w-24"
|
||||
side={isMobile ? "bottom" : "right"}
|
||||
align={isMobile ? "end" : "start"}
|
||||
>
|
||||
<DropdownMenuItem>
|
||||
<IconPlaceholder
|
||||
lucide="FolderIcon"
|
||||
tabler="IconFolder"
|
||||
hugeicons="Folder01Icon"
|
||||
phosphor="FolderIcon"
|
||||
remixicon="RiFolderLine"
|
||||
/>
|
||||
<span>Open</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<IconPlaceholder
|
||||
lucide="ShareIcon"
|
||||
tabler="IconShare3"
|
||||
hugeicons="Share01Icon"
|
||||
phosphor="ShareIcon"
|
||||
remixicon="RiShareLine"
|
||||
/>
|
||||
<span>Share</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem variant="destructive">
|
||||
<IconPlaceholder
|
||||
lucide="Trash2Icon"
|
||||
tabler="IconTrash"
|
||||
hugeicons="Delete02Icon"
|
||||
phosphor="TrashIcon"
|
||||
remixicon="RiDeleteBinLine"
|
||||
/>
|
||||
<span>Delete</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</SidebarMenuItem>
|
||||
))}
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton className="text-sidebar-foreground/70">
|
||||
<IconPlaceholder
|
||||
lucide="MoreHorizontalIcon"
|
||||
tabler="IconDots"
|
||||
hugeicons="MoreHorizontalCircle01Icon"
|
||||
phosphor="DotsThreeOutlineIcon"
|
||||
remixicon="RiMoreLine"
|
||||
className="text-sidebar-foreground/70"
|
||||
/>
|
||||
<span>More</span>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarGroup>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
"use client"
|
||||
|
||||
import { Button } from "@/registry/bases/base/ui/button"
|
||||
import {
|
||||
SidebarGroup,
|
||||
SidebarGroupContent,
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
} from "@/registry/bases/base/ui/sidebar"
|
||||
import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"
|
||||
|
||||
export function NavMain({
|
||||
items,
|
||||
}: {
|
||||
items: {
|
||||
title: string
|
||||
url: string
|
||||
icon?: React.ReactNode
|
||||
}[]
|
||||
}) {
|
||||
return (
|
||||
<SidebarGroup>
|
||||
<SidebarGroupContent className="flex flex-col gap-2">
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem className="flex items-center gap-2">
|
||||
<SidebarMenuButton
|
||||
tooltip="Quick Create"
|
||||
className="min-w-8 bg-primary text-primary-foreground duration-200 ease-linear hover:bg-primary/90 hover:text-primary-foreground active:bg-primary/90 active:text-primary-foreground"
|
||||
>
|
||||
<IconPlaceholder
|
||||
lucide="CirclePlusIcon"
|
||||
tabler="IconCirclePlusFilled"
|
||||
hugeicons="PlusSignCircleIcon"
|
||||
phosphor="PlusCircleIcon"
|
||||
remixicon="RiAddCircleFill"
|
||||
/>
|
||||
<span>Quick Create</span>
|
||||
</SidebarMenuButton>
|
||||
<Button
|
||||
size="icon"
|
||||
className="size-8 group-data-[collapsible=icon]:opacity-0"
|
||||
variant="outline"
|
||||
>
|
||||
<IconPlaceholder
|
||||
lucide="MailIcon"
|
||||
tabler="IconMail"
|
||||
hugeicons="Mail01Icon"
|
||||
phosphor="EnvelopeIcon"
|
||||
remixicon="RiMailLine"
|
||||
/>
|
||||
<span className="sr-only">Inbox</span>
|
||||
</Button>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
<SidebarMenu>
|
||||
{items.map((item) => (
|
||||
<SidebarMenuItem key={item.title}>
|
||||
<SidebarMenuButton tooltip={item.title}>
|
||||
{item.icon}
|
||||
<span>{item.title}</span>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
))}
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
|
||||
import {
|
||||
SidebarGroup,
|
||||
SidebarGroupContent,
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
} from "@/registry/bases/base/ui/sidebar"
|
||||
|
||||
export function NavSecondary({
|
||||
items,
|
||||
...props
|
||||
}: {
|
||||
items: {
|
||||
title: string
|
||||
url: string
|
||||
icon: React.ReactNode
|
||||
}[]
|
||||
} & React.ComponentPropsWithoutRef<typeof SidebarGroup>) {
|
||||
return (
|
||||
<SidebarGroup {...props}>
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu>
|
||||
{items.map((item) => (
|
||||
<SidebarMenuItem key={item.title}>
|
||||
<SidebarMenuButton render={<a href={item.url} />}>
|
||||
{item.icon}
|
||||
<span>{item.title}</span>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
))}
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
"use client"
|
||||
|
||||
import {
|
||||
Avatar,
|
||||
AvatarFallback,
|
||||
AvatarImage,
|
||||
} from "@/registry/bases/base/ui/avatar"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/registry/bases/base/ui/dropdown-menu"
|
||||
import {
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
useSidebar,
|
||||
} from "@/registry/bases/base/ui/sidebar"
|
||||
import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"
|
||||
|
||||
export function NavUser({
|
||||
user,
|
||||
}: {
|
||||
user: {
|
||||
name: string
|
||||
email: string
|
||||
avatar: string
|
||||
}
|
||||
}) {
|
||||
const { isMobile } = useSidebar()
|
||||
return (
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
render={
|
||||
<SidebarMenuButton size="lg" className="aria-expanded:bg-muted" />
|
||||
}
|
||||
>
|
||||
<Avatar className="size-8 rounded-lg grayscale">
|
||||
<AvatarImage src={user.avatar} alt={user.name} />
|
||||
<AvatarFallback className="rounded-lg">CN</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||
<span className="truncate font-medium">{user.name}</span>
|
||||
<span className="truncate text-xs text-foreground/70">
|
||||
{user.email}
|
||||
</span>
|
||||
</div>
|
||||
<IconPlaceholder
|
||||
lucide="EllipsisVerticalIcon"
|
||||
tabler="IconDotsVertical"
|
||||
hugeicons="MoreVerticalCircle01Icon"
|
||||
phosphor="DotsThreeVerticalIcon"
|
||||
remixicon="RiMore2Line"
|
||||
className="ml-auto size-4"
|
||||
/>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
className="min-w-56"
|
||||
side={isMobile ? "bottom" : "right"}
|
||||
align="end"
|
||||
sideOffset={4}
|
||||
>
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuLabel className="p-0 font-normal">
|
||||
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
|
||||
<Avatar className="size-8">
|
||||
<AvatarImage src={user.avatar} alt={user.name} />
|
||||
<AvatarFallback className="rounded-lg">CN</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||
<span className="truncate font-medium">{user.name}</span>
|
||||
<span className="truncate text-xs text-muted-foreground">
|
||||
{user.email}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</DropdownMenuLabel>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem>
|
||||
<IconPlaceholder
|
||||
lucide="CircleUserRoundIcon"
|
||||
tabler="IconUserCircle"
|
||||
hugeicons="UserCircle02Icon"
|
||||
phosphor="UserCircleIcon"
|
||||
remixicon="RiUserLine"
|
||||
/>
|
||||
Account
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<IconPlaceholder
|
||||
lucide="CreditCardIcon"
|
||||
tabler="IconCreditCard"
|
||||
hugeicons="CreditCardIcon"
|
||||
phosphor="CreditCardIcon"
|
||||
remixicon="RiBankCardLine"
|
||||
/>
|
||||
Billing
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<IconPlaceholder
|
||||
lucide="BellIcon"
|
||||
tabler="IconNotification"
|
||||
hugeicons="Notification03Icon"
|
||||
phosphor="BellIcon"
|
||||
remixicon="RiNotification3Line"
|
||||
/>
|
||||
Notifications
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>
|
||||
<IconPlaceholder
|
||||
lucide="LogOutIcon"
|
||||
tabler="IconLogout"
|
||||
hugeicons="Logout01Icon"
|
||||
phosphor="SignOutIcon"
|
||||
remixicon="RiLogoutBoxLine"
|
||||
/>
|
||||
Log out
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
"use client"
|
||||
|
||||
import { Badge } from "@/registry/bases/base/ui/badge"
|
||||
import {
|
||||
Card,
|
||||
CardAction,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/base/ui/card"
|
||||
import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"
|
||||
|
||||
export function SectionCards() {
|
||||
return (
|
||||
<div className="grid grid-cols-1 gap-4 px-4 *:data-[slot=card]:bg-linear-to-t *:data-[slot=card]:from-primary/5 *:data-[slot=card]:to-card *:data-[slot=card]:shadow-xs lg:px-6 @xl/main:grid-cols-2 @5xl/main:grid-cols-4 dark:*:data-[slot=card]:bg-card">
|
||||
<Card className="@container/card">
|
||||
<CardHeader>
|
||||
<CardDescription>Total Revenue</CardDescription>
|
||||
<CardTitle className="text-2xl font-semibold tabular-nums @[250px]/card:text-3xl">
|
||||
$1,250.00
|
||||
</CardTitle>
|
||||
<CardAction>
|
||||
<Badge variant="outline">
|
||||
<IconPlaceholder
|
||||
lucide="TrendingUpIcon"
|
||||
tabler="IconTrendingUp"
|
||||
hugeicons="ChartUpIcon"
|
||||
phosphor="TrendUpIcon"
|
||||
remixicon="RiArrowUpLine"
|
||||
/>
|
||||
+12.5%
|
||||
</Badge>
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
<CardFooter className="flex-col items-start gap-1.5 text-sm">
|
||||
<div className="line-clamp-1 flex gap-2 font-medium">
|
||||
Trending up this month{" "}
|
||||
<IconPlaceholder
|
||||
lucide="TrendingUpIcon"
|
||||
tabler="IconTrendingUp"
|
||||
hugeicons="ChartUpIcon"
|
||||
phosphor="TrendUpIcon"
|
||||
remixicon="RiArrowUpLine"
|
||||
className="size-4"
|
||||
/>
|
||||
</div>
|
||||
<div className="text-muted-foreground">
|
||||
Visitors for the last 6 months
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
<Card className="@container/card">
|
||||
<CardHeader>
|
||||
<CardDescription>New Customers</CardDescription>
|
||||
<CardTitle className="text-2xl font-semibold tabular-nums @[250px]/card:text-3xl">
|
||||
1,234
|
||||
</CardTitle>
|
||||
<CardAction>
|
||||
<Badge variant="outline">
|
||||
<IconPlaceholder
|
||||
lucide="TrendingDownIcon"
|
||||
tabler="IconTrendingDown"
|
||||
hugeicons="ChartDownIcon"
|
||||
phosphor="TrendDownIcon"
|
||||
remixicon="RiArrowDownLine"
|
||||
/>
|
||||
-20%
|
||||
</Badge>
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
<CardFooter className="flex-col items-start gap-1.5 text-sm">
|
||||
<div className="line-clamp-1 flex gap-2 font-medium">
|
||||
Down 20% this period{" "}
|
||||
<IconPlaceholder
|
||||
lucide="TrendingDownIcon"
|
||||
tabler="IconTrendingDown"
|
||||
hugeicons="ChartDownIcon"
|
||||
phosphor="TrendDownIcon"
|
||||
remixicon="RiArrowDownLine"
|
||||
className="size-4"
|
||||
/>
|
||||
</div>
|
||||
<div className="text-muted-foreground">
|
||||
Acquisition needs attention
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
<Card className="@container/card">
|
||||
<CardHeader>
|
||||
<CardDescription>Active Accounts</CardDescription>
|
||||
<CardTitle className="text-2xl font-semibold tabular-nums @[250px]/card:text-3xl">
|
||||
45,678
|
||||
</CardTitle>
|
||||
<CardAction>
|
||||
<Badge variant="outline">
|
||||
<IconPlaceholder
|
||||
lucide="TrendingUpIcon"
|
||||
tabler="IconTrendingUp"
|
||||
hugeicons="ChartUpIcon"
|
||||
phosphor="TrendUpIcon"
|
||||
remixicon="RiArrowUpLine"
|
||||
/>
|
||||
+12.5%
|
||||
</Badge>
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
<CardFooter className="flex-col items-start gap-1.5 text-sm">
|
||||
<div className="line-clamp-1 flex gap-2 font-medium">
|
||||
Strong user retention{" "}
|
||||
<IconPlaceholder
|
||||
lucide="TrendingUpIcon"
|
||||
tabler="IconTrendingUp"
|
||||
hugeicons="ChartUpIcon"
|
||||
phosphor="TrendUpIcon"
|
||||
remixicon="RiArrowUpLine"
|
||||
className="size-4"
|
||||
/>
|
||||
</div>
|
||||
<div className="text-muted-foreground">Engagement exceed targets</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
<Card className="@container/card">
|
||||
<CardHeader>
|
||||
<CardDescription>Growth Rate</CardDescription>
|
||||
<CardTitle className="text-2xl font-semibold tabular-nums @[250px]/card:text-3xl">
|
||||
4.5%
|
||||
</CardTitle>
|
||||
<CardAction>
|
||||
<Badge variant="outline">
|
||||
<IconPlaceholder
|
||||
lucide="TrendingUpIcon"
|
||||
tabler="IconTrendingUp"
|
||||
hugeicons="ChartUpIcon"
|
||||
phosphor="TrendUpIcon"
|
||||
remixicon="RiArrowUpLine"
|
||||
/>
|
||||
+4.5%
|
||||
</Badge>
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
<CardFooter className="flex-col items-start gap-1.5 text-sm">
|
||||
<div className="line-clamp-1 flex gap-2 font-medium">
|
||||
Steady performance increase{" "}
|
||||
<IconPlaceholder
|
||||
lucide="TrendingUpIcon"
|
||||
tabler="IconTrendingUp"
|
||||
hugeicons="ChartUpIcon"
|
||||
phosphor="TrendUpIcon"
|
||||
remixicon="RiArrowUpLine"
|
||||
className="size-4"
|
||||
/>
|
||||
</div>
|
||||
<div className="text-muted-foreground">Meets growth projections</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { Separator } from "@/registry/bases/base/ui/separator"
|
||||
import { SidebarTrigger } from "@/registry/bases/base/ui/sidebar"
|
||||
|
||||
export function SiteHeader() {
|
||||
return (
|
||||
<header className="flex h-(--header-height) shrink-0 items-center gap-2 border-b transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-(--header-height)">
|
||||
<div className="flex w-full items-center gap-1 px-4 lg:gap-2 lg:px-6">
|
||||
<SidebarTrigger className="-ml-1" />
|
||||
<Separator
|
||||
orientation="vertical"
|
||||
className="mx-2 h-4 data-vertical:self-auto"
|
||||
/>
|
||||
<h1 className="text-base font-medium">Documents</h1>
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
614
apps/v4/registry/bases/base/blocks/dashboard-01/data.json
Normal file
614
apps/v4/registry/bases/base/blocks/dashboard-01/data.json
Normal file
@@ -0,0 +1,614 @@
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"header": "Cover page",
|
||||
"type": "Cover page",
|
||||
"status": "In Process",
|
||||
"target": "18",
|
||||
"limit": "5",
|
||||
"reviewer": "Eddie Lake"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"header": "Table of contents",
|
||||
"type": "Table of contents",
|
||||
"status": "Done",
|
||||
"target": "29",
|
||||
"limit": "24",
|
||||
"reviewer": "Eddie Lake"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"header": "Executive summary",
|
||||
"type": "Narrative",
|
||||
"status": "Done",
|
||||
"target": "10",
|
||||
"limit": "13",
|
||||
"reviewer": "Eddie Lake"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"header": "Technical approach",
|
||||
"type": "Narrative",
|
||||
"status": "Done",
|
||||
"target": "27",
|
||||
"limit": "23",
|
||||
"reviewer": "Jamik Tashpulatov"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"header": "Design",
|
||||
"type": "Narrative",
|
||||
"status": "In Process",
|
||||
"target": "2",
|
||||
"limit": "16",
|
||||
"reviewer": "Jamik Tashpulatov"
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"header": "Capabilities",
|
||||
"type": "Narrative",
|
||||
"status": "In Process",
|
||||
"target": "20",
|
||||
"limit": "8",
|
||||
"reviewer": "Jamik Tashpulatov"
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"header": "Integration with existing systems",
|
||||
"type": "Narrative",
|
||||
"status": "In Process",
|
||||
"target": "19",
|
||||
"limit": "21",
|
||||
"reviewer": "Jamik Tashpulatov"
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"header": "Innovation and Advantages",
|
||||
"type": "Narrative",
|
||||
"status": "Done",
|
||||
"target": "25",
|
||||
"limit": "26",
|
||||
"reviewer": "Assign reviewer"
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"header": "Overview of EMR's Innovative Solutions",
|
||||
"type": "Technical content",
|
||||
"status": "Done",
|
||||
"target": "7",
|
||||
"limit": "23",
|
||||
"reviewer": "Assign reviewer"
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"header": "Advanced Algorithms and Machine Learning",
|
||||
"type": "Narrative",
|
||||
"status": "Done",
|
||||
"target": "30",
|
||||
"limit": "28",
|
||||
"reviewer": "Assign reviewer"
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"header": "Adaptive Communication Protocols",
|
||||
"type": "Narrative",
|
||||
"status": "Done",
|
||||
"target": "9",
|
||||
"limit": "31",
|
||||
"reviewer": "Assign reviewer"
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"header": "Advantages Over Current Technologies",
|
||||
"type": "Narrative",
|
||||
"status": "Done",
|
||||
"target": "12",
|
||||
"limit": "0",
|
||||
"reviewer": "Assign reviewer"
|
||||
},
|
||||
{
|
||||
"id": 13,
|
||||
"header": "Past Performance",
|
||||
"type": "Narrative",
|
||||
"status": "Done",
|
||||
"target": "22",
|
||||
"limit": "33",
|
||||
"reviewer": "Assign reviewer"
|
||||
},
|
||||
{
|
||||
"id": 14,
|
||||
"header": "Customer Feedback and Satisfaction Levels",
|
||||
"type": "Narrative",
|
||||
"status": "Done",
|
||||
"target": "15",
|
||||
"limit": "34",
|
||||
"reviewer": "Assign reviewer"
|
||||
},
|
||||
{
|
||||
"id": 15,
|
||||
"header": "Implementation Challenges and Solutions",
|
||||
"type": "Narrative",
|
||||
"status": "Done",
|
||||
"target": "3",
|
||||
"limit": "35",
|
||||
"reviewer": "Assign reviewer"
|
||||
},
|
||||
{
|
||||
"id": 16,
|
||||
"header": "Security Measures and Data Protection Policies",
|
||||
"type": "Narrative",
|
||||
"status": "In Process",
|
||||
"target": "6",
|
||||
"limit": "36",
|
||||
"reviewer": "Assign reviewer"
|
||||
},
|
||||
{
|
||||
"id": 17,
|
||||
"header": "Scalability and Future Proofing",
|
||||
"type": "Narrative",
|
||||
"status": "Done",
|
||||
"target": "4",
|
||||
"limit": "37",
|
||||
"reviewer": "Assign reviewer"
|
||||
},
|
||||
{
|
||||
"id": 18,
|
||||
"header": "Cost-Benefit Analysis",
|
||||
"type": "Plain language",
|
||||
"status": "Done",
|
||||
"target": "14",
|
||||
"limit": "38",
|
||||
"reviewer": "Assign reviewer"
|
||||
},
|
||||
{
|
||||
"id": 19,
|
||||
"header": "User Training and Onboarding Experience",
|
||||
"type": "Narrative",
|
||||
"status": "Done",
|
||||
"target": "17",
|
||||
"limit": "39",
|
||||
"reviewer": "Assign reviewer"
|
||||
},
|
||||
{
|
||||
"id": 20,
|
||||
"header": "Future Development Roadmap",
|
||||
"type": "Narrative",
|
||||
"status": "Done",
|
||||
"target": "11",
|
||||
"limit": "40",
|
||||
"reviewer": "Assign reviewer"
|
||||
},
|
||||
{
|
||||
"id": 21,
|
||||
"header": "System Architecture Overview",
|
||||
"type": "Technical content",
|
||||
"status": "In Process",
|
||||
"target": "24",
|
||||
"limit": "18",
|
||||
"reviewer": "Maya Johnson"
|
||||
},
|
||||
{
|
||||
"id": 22,
|
||||
"header": "Risk Management Plan",
|
||||
"type": "Narrative",
|
||||
"status": "Done",
|
||||
"target": "15",
|
||||
"limit": "22",
|
||||
"reviewer": "Carlos Rodriguez"
|
||||
},
|
||||
{
|
||||
"id": 23,
|
||||
"header": "Compliance Documentation",
|
||||
"type": "Legal",
|
||||
"status": "In Process",
|
||||
"target": "31",
|
||||
"limit": "27",
|
||||
"reviewer": "Sarah Chen"
|
||||
},
|
||||
{
|
||||
"id": 24,
|
||||
"header": "API Documentation",
|
||||
"type": "Technical content",
|
||||
"status": "Done",
|
||||
"target": "8",
|
||||
"limit": "12",
|
||||
"reviewer": "Raj Patel"
|
||||
},
|
||||
{
|
||||
"id": 25,
|
||||
"header": "User Interface Mockups",
|
||||
"type": "Visual",
|
||||
"status": "In Process",
|
||||
"target": "19",
|
||||
"limit": "25",
|
||||
"reviewer": "Leila Ahmadi"
|
||||
},
|
||||
{
|
||||
"id": 26,
|
||||
"header": "Database Schema",
|
||||
"type": "Technical content",
|
||||
"status": "Done",
|
||||
"target": "22",
|
||||
"limit": "20",
|
||||
"reviewer": "Thomas Wilson"
|
||||
},
|
||||
{
|
||||
"id": 27,
|
||||
"header": "Testing Methodology",
|
||||
"type": "Technical content",
|
||||
"status": "In Process",
|
||||
"target": "17",
|
||||
"limit": "14",
|
||||
"reviewer": "Assign reviewer"
|
||||
},
|
||||
{
|
||||
"id": 28,
|
||||
"header": "Deployment Strategy",
|
||||
"type": "Narrative",
|
||||
"status": "Done",
|
||||
"target": "26",
|
||||
"limit": "30",
|
||||
"reviewer": "Eddie Lake"
|
||||
},
|
||||
{
|
||||
"id": 29,
|
||||
"header": "Budget Breakdown",
|
||||
"type": "Financial",
|
||||
"status": "In Process",
|
||||
"target": "13",
|
||||
"limit": "16",
|
||||
"reviewer": "Jamik Tashpulatov"
|
||||
},
|
||||
{
|
||||
"id": 30,
|
||||
"header": "Market Analysis",
|
||||
"type": "Research",
|
||||
"status": "Done",
|
||||
"target": "29",
|
||||
"limit": "32",
|
||||
"reviewer": "Sophia Martinez"
|
||||
},
|
||||
{
|
||||
"id": 31,
|
||||
"header": "Competitor Comparison",
|
||||
"type": "Research",
|
||||
"status": "In Process",
|
||||
"target": "21",
|
||||
"limit": "19",
|
||||
"reviewer": "Assign reviewer"
|
||||
},
|
||||
{
|
||||
"id": 32,
|
||||
"header": "Maintenance Plan",
|
||||
"type": "Technical content",
|
||||
"status": "Done",
|
||||
"target": "16",
|
||||
"limit": "23",
|
||||
"reviewer": "Alex Thompson"
|
||||
},
|
||||
{
|
||||
"id": 33,
|
||||
"header": "User Personas",
|
||||
"type": "Research",
|
||||
"status": "In Process",
|
||||
"target": "27",
|
||||
"limit": "24",
|
||||
"reviewer": "Nina Patel"
|
||||
},
|
||||
{
|
||||
"id": 34,
|
||||
"header": "Accessibility Compliance",
|
||||
"type": "Legal",
|
||||
"status": "Done",
|
||||
"target": "18",
|
||||
"limit": "21",
|
||||
"reviewer": "Assign reviewer"
|
||||
},
|
||||
{
|
||||
"id": 35,
|
||||
"header": "Performance Metrics",
|
||||
"type": "Technical content",
|
||||
"status": "In Process",
|
||||
"target": "23",
|
||||
"limit": "26",
|
||||
"reviewer": "David Kim"
|
||||
},
|
||||
{
|
||||
"id": 36,
|
||||
"header": "Disaster Recovery Plan",
|
||||
"type": "Technical content",
|
||||
"status": "Done",
|
||||
"target": "14",
|
||||
"limit": "17",
|
||||
"reviewer": "Jamik Tashpulatov"
|
||||
},
|
||||
{
|
||||
"id": 37,
|
||||
"header": "Third-party Integrations",
|
||||
"type": "Technical content",
|
||||
"status": "In Process",
|
||||
"target": "25",
|
||||
"limit": "28",
|
||||
"reviewer": "Eddie Lake"
|
||||
},
|
||||
{
|
||||
"id": 38,
|
||||
"header": "User Feedback Summary",
|
||||
"type": "Research",
|
||||
"status": "Done",
|
||||
"target": "20",
|
||||
"limit": "15",
|
||||
"reviewer": "Assign reviewer"
|
||||
},
|
||||
{
|
||||
"id": 39,
|
||||
"header": "Localization Strategy",
|
||||
"type": "Narrative",
|
||||
"status": "In Process",
|
||||
"target": "12",
|
||||
"limit": "19",
|
||||
"reviewer": "Maria Garcia"
|
||||
},
|
||||
{
|
||||
"id": 40,
|
||||
"header": "Mobile Compatibility",
|
||||
"type": "Technical content",
|
||||
"status": "Done",
|
||||
"target": "28",
|
||||
"limit": "31",
|
||||
"reviewer": "James Wilson"
|
||||
},
|
||||
{
|
||||
"id": 41,
|
||||
"header": "Data Migration Plan",
|
||||
"type": "Technical content",
|
||||
"status": "In Process",
|
||||
"target": "19",
|
||||
"limit": "22",
|
||||
"reviewer": "Assign reviewer"
|
||||
},
|
||||
{
|
||||
"id": 42,
|
||||
"header": "Quality Assurance Protocols",
|
||||
"type": "Technical content",
|
||||
"status": "Done",
|
||||
"target": "30",
|
||||
"limit": "33",
|
||||
"reviewer": "Priya Singh"
|
||||
},
|
||||
{
|
||||
"id": 43,
|
||||
"header": "Stakeholder Analysis",
|
||||
"type": "Research",
|
||||
"status": "In Process",
|
||||
"target": "11",
|
||||
"limit": "14",
|
||||
"reviewer": "Eddie Lake"
|
||||
},
|
||||
{
|
||||
"id": 44,
|
||||
"header": "Environmental Impact Assessment",
|
||||
"type": "Research",
|
||||
"status": "Done",
|
||||
"target": "24",
|
||||
"limit": "27",
|
||||
"reviewer": "Assign reviewer"
|
||||
},
|
||||
{
|
||||
"id": 45,
|
||||
"header": "Intellectual Property Rights",
|
||||
"type": "Legal",
|
||||
"status": "In Process",
|
||||
"target": "17",
|
||||
"limit": "20",
|
||||
"reviewer": "Sarah Johnson"
|
||||
},
|
||||
{
|
||||
"id": 46,
|
||||
"header": "Customer Support Framework",
|
||||
"type": "Narrative",
|
||||
"status": "Done",
|
||||
"target": "22",
|
||||
"limit": "25",
|
||||
"reviewer": "Jamik Tashpulatov"
|
||||
},
|
||||
{
|
||||
"id": 47,
|
||||
"header": "Version Control Strategy",
|
||||
"type": "Technical content",
|
||||
"status": "In Process",
|
||||
"target": "15",
|
||||
"limit": "18",
|
||||
"reviewer": "Assign reviewer"
|
||||
},
|
||||
{
|
||||
"id": 48,
|
||||
"header": "Continuous Integration Pipeline",
|
||||
"type": "Technical content",
|
||||
"status": "Done",
|
||||
"target": "26",
|
||||
"limit": "29",
|
||||
"reviewer": "Michael Chen"
|
||||
},
|
||||
{
|
||||
"id": 49,
|
||||
"header": "Regulatory Compliance",
|
||||
"type": "Legal",
|
||||
"status": "In Process",
|
||||
"target": "13",
|
||||
"limit": "16",
|
||||
"reviewer": "Assign reviewer"
|
||||
},
|
||||
{
|
||||
"id": 50,
|
||||
"header": "User Authentication System",
|
||||
"type": "Technical content",
|
||||
"status": "Done",
|
||||
"target": "28",
|
||||
"limit": "31",
|
||||
"reviewer": "Eddie Lake"
|
||||
},
|
||||
{
|
||||
"id": 51,
|
||||
"header": "Data Analytics Framework",
|
||||
"type": "Technical content",
|
||||
"status": "In Process",
|
||||
"target": "21",
|
||||
"limit": "24",
|
||||
"reviewer": "Jamik Tashpulatov"
|
||||
},
|
||||
{
|
||||
"id": 52,
|
||||
"header": "Cloud Infrastructure",
|
||||
"type": "Technical content",
|
||||
"status": "Done",
|
||||
"target": "16",
|
||||
"limit": "19",
|
||||
"reviewer": "Assign reviewer"
|
||||
},
|
||||
{
|
||||
"id": 53,
|
||||
"header": "Network Security Measures",
|
||||
"type": "Technical content",
|
||||
"status": "In Process",
|
||||
"target": "29",
|
||||
"limit": "32",
|
||||
"reviewer": "Lisa Wong"
|
||||
},
|
||||
{
|
||||
"id": 54,
|
||||
"header": "Project Timeline",
|
||||
"type": "Planning",
|
||||
"status": "Done",
|
||||
"target": "14",
|
||||
"limit": "17",
|
||||
"reviewer": "Eddie Lake"
|
||||
},
|
||||
{
|
||||
"id": 55,
|
||||
"header": "Resource Allocation",
|
||||
"type": "Planning",
|
||||
"status": "In Process",
|
||||
"target": "27",
|
||||
"limit": "30",
|
||||
"reviewer": "Assign reviewer"
|
||||
},
|
||||
{
|
||||
"id": 56,
|
||||
"header": "Team Structure and Roles",
|
||||
"type": "Planning",
|
||||
"status": "Done",
|
||||
"target": "20",
|
||||
"limit": "23",
|
||||
"reviewer": "Jamik Tashpulatov"
|
||||
},
|
||||
{
|
||||
"id": 57,
|
||||
"header": "Communication Protocols",
|
||||
"type": "Planning",
|
||||
"status": "In Process",
|
||||
"target": "15",
|
||||
"limit": "18",
|
||||
"reviewer": "Assign reviewer"
|
||||
},
|
||||
{
|
||||
"id": 58,
|
||||
"header": "Success Metrics",
|
||||
"type": "Planning",
|
||||
"status": "Done",
|
||||
"target": "30",
|
||||
"limit": "33",
|
||||
"reviewer": "Eddie Lake"
|
||||
},
|
||||
{
|
||||
"id": 59,
|
||||
"header": "Internationalization Support",
|
||||
"type": "Technical content",
|
||||
"status": "In Process",
|
||||
"target": "23",
|
||||
"limit": "26",
|
||||
"reviewer": "Jamik Tashpulatov"
|
||||
},
|
||||
{
|
||||
"id": 60,
|
||||
"header": "Backup and Recovery Procedures",
|
||||
"type": "Technical content",
|
||||
"status": "Done",
|
||||
"target": "18",
|
||||
"limit": "21",
|
||||
"reviewer": "Assign reviewer"
|
||||
},
|
||||
{
|
||||
"id": 61,
|
||||
"header": "Monitoring and Alerting System",
|
||||
"type": "Technical content",
|
||||
"status": "In Process",
|
||||
"target": "25",
|
||||
"limit": "28",
|
||||
"reviewer": "Daniel Park"
|
||||
},
|
||||
{
|
||||
"id": 62,
|
||||
"header": "Code Review Guidelines",
|
||||
"type": "Technical content",
|
||||
"status": "Done",
|
||||
"target": "12",
|
||||
"limit": "15",
|
||||
"reviewer": "Eddie Lake"
|
||||
},
|
||||
{
|
||||
"id": 63,
|
||||
"header": "Documentation Standards",
|
||||
"type": "Technical content",
|
||||
"status": "In Process",
|
||||
"target": "27",
|
||||
"limit": "30",
|
||||
"reviewer": "Jamik Tashpulatov"
|
||||
},
|
||||
{
|
||||
"id": 64,
|
||||
"header": "Release Management Process",
|
||||
"type": "Planning",
|
||||
"status": "Done",
|
||||
"target": "22",
|
||||
"limit": "25",
|
||||
"reviewer": "Assign reviewer"
|
||||
},
|
||||
{
|
||||
"id": 65,
|
||||
"header": "Feature Prioritization Matrix",
|
||||
"type": "Planning",
|
||||
"status": "In Process",
|
||||
"target": "19",
|
||||
"limit": "22",
|
||||
"reviewer": "Emma Davis"
|
||||
},
|
||||
{
|
||||
"id": 66,
|
||||
"header": "Technical Debt Assessment",
|
||||
"type": "Technical content",
|
||||
"status": "Done",
|
||||
"target": "24",
|
||||
"limit": "27",
|
||||
"reviewer": "Eddie Lake"
|
||||
},
|
||||
{
|
||||
"id": 67,
|
||||
"header": "Capacity Planning",
|
||||
"type": "Planning",
|
||||
"status": "In Process",
|
||||
"target": "21",
|
||||
"limit": "24",
|
||||
"reviewer": "Jamik Tashpulatov"
|
||||
},
|
||||
{
|
||||
"id": 68,
|
||||
"header": "Service Level Agreements",
|
||||
"type": "Legal",
|
||||
"status": "Done",
|
||||
"target": "26",
|
||||
"limit": "29",
|
||||
"reviewer": "Assign reviewer"
|
||||
}
|
||||
]
|
||||
37
apps/v4/registry/bases/base/blocks/dashboard-01/page.tsx
Normal file
37
apps/v4/registry/bases/base/blocks/dashboard-01/page.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import { AppSidebar } from "@/registry/bases/base/blocks/dashboard-01/components/app-sidebar"
|
||||
import { ChartAreaInteractive } from "@/registry/bases/base/blocks/dashboard-01/components/chart-area-interactive"
|
||||
import { DataTable } from "@/registry/bases/base/blocks/dashboard-01/components/data-table"
|
||||
import { SectionCards } from "@/registry/bases/base/blocks/dashboard-01/components/section-cards"
|
||||
import { SiteHeader } from "@/registry/bases/base/blocks/dashboard-01/components/site-header"
|
||||
import { SidebarInset, SidebarProvider } from "@/registry/bases/base/ui/sidebar"
|
||||
|
||||
import data from "./data.json"
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<SidebarProvider
|
||||
style={
|
||||
{
|
||||
"--sidebar-width": "calc(var(--spacing) * 72)",
|
||||
"--header-height": "calc(var(--spacing) * 12)",
|
||||
} as React.CSSProperties
|
||||
}
|
||||
>
|
||||
<AppSidebar variant="inset" />
|
||||
<SidebarInset>
|
||||
<SiteHeader />
|
||||
<div className="flex flex-1 flex-col">
|
||||
<div className="@container/main flex flex-1 flex-col gap-2">
|
||||
<div className="flex flex-col gap-4 py-4 md:gap-6 md:py-6">
|
||||
<SectionCards />
|
||||
<div className="px-4 lg:px-6">
|
||||
<ChartAreaInteractive />
|
||||
</div>
|
||||
<DataTable data={data} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SidebarInset>
|
||||
</SidebarProvider>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import { cn } from "cn"
|
||||
|
||||
import { Button } from "@/registry/bases/base/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/base/ui/card"
|
||||
import {
|
||||
Field,
|
||||
FieldDescription,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
} from "@/registry/bases/base/ui/field"
|
||||
import { Input } from "@/registry/bases/base/ui/input"
|
||||
|
||||
export function LoginForm({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div className={cn("flex flex-col gap-6", className)} {...props}>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Login to your account</CardTitle>
|
||||
<CardDescription>
|
||||
Enter your email below to login to your account
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form>
|
||||
<FieldGroup>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="email">Email</FieldLabel>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="m@example.com"
|
||||
required
|
||||
/>
|
||||
</Field>
|
||||
<Field>
|
||||
<div className="flex items-center">
|
||||
<FieldLabel htmlFor="password">Password</FieldLabel>
|
||||
<a
|
||||
href="#"
|
||||
className="ml-auto inline-block text-sm underline-offset-4 hover:underline"
|
||||
>
|
||||
Forgot your password?
|
||||
</a>
|
||||
</div>
|
||||
<Input id="password" type="password" required />
|
||||
</Field>
|
||||
<Field>
|
||||
<Button type="submit">Login</Button>
|
||||
<Button variant="outline" type="button">
|
||||
Login with Google
|
||||
</Button>
|
||||
<FieldDescription className="text-center">
|
||||
Don't have an account? <a href="#">Sign up</a>
|
||||
</FieldDescription>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
11
apps/v4/registry/bases/base/blocks/login-01/page.tsx
Normal file
11
apps/v4/registry/bases/base/blocks/login-01/page.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import { LoginForm } from "@/registry/bases/base/blocks/login-01/components/login-form"
|
||||
|
||||
export default function Page() {
|
||||
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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import { cn } from "cn"
|
||||
|
||||
import { Button } from "@/registry/bases/base/ui/button"
|
||||
import {
|
||||
Field,
|
||||
FieldDescription,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
FieldSeparator,
|
||||
} from "@/registry/bases/base/ui/field"
|
||||
import { Input } from "@/registry/bases/base/ui/input"
|
||||
|
||||
export function LoginForm({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"form">) {
|
||||
return (
|
||||
<form className={cn("flex flex-col gap-6", className)} {...props}>
|
||||
<FieldGroup>
|
||||
<div className="flex flex-col items-center gap-1 text-center">
|
||||
<h1 className="text-2xl font-bold">Login to your account</h1>
|
||||
<p className="text-sm text-balance text-muted-foreground">
|
||||
Enter your email below to login to your account
|
||||
</p>
|
||||
</div>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="email">Email</FieldLabel>
|
||||
<Input id="email" type="email" placeholder="m@example.com" required />
|
||||
</Field>
|
||||
<Field>
|
||||
<div className="flex items-center">
|
||||
<FieldLabel htmlFor="password">Password</FieldLabel>
|
||||
<a
|
||||
href="#"
|
||||
className="ml-auto text-sm underline-offset-4 hover:underline"
|
||||
>
|
||||
Forgot your password?
|
||||
</a>
|
||||
</div>
|
||||
<Input id="password" type="password" required />
|
||||
</Field>
|
||||
<Field>
|
||||
<Button type="submit">Login</Button>
|
||||
</Field>
|
||||
<FieldSeparator>Or continue with</FieldSeparator>
|
||||
<Field>
|
||||
<Button variant="outline" type="button">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
Login with GitHub
|
||||
</Button>
|
||||
<FieldDescription className="text-center">
|
||||
Don't have an account?{" "}
|
||||
<a href="#" className="underline underline-offset-4">
|
||||
Sign up
|
||||
</a>
|
||||
</FieldDescription>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
40
apps/v4/registry/bases/base/blocks/login-02/page.tsx
Normal file
40
apps/v4/registry/bases/base/blocks/login-02/page.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
"use client"
|
||||
|
||||
import { LoginForm } from "@/registry/bases/base/blocks/login-02/components/login-form"
|
||||
import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"
|
||||
|
||||
export default function LoginPage() {
|
||||
return (
|
||||
<div className="grid min-h-svh lg:grid-cols-2">
|
||||
<div className="flex flex-col gap-4 p-6 md:p-10">
|
||||
<div className="flex justify-center gap-2 md:justify-start">
|
||||
<a href="#" className="flex items-center gap-2 font-medium">
|
||||
<div className="flex size-6 items-center justify-center rounded-md bg-primary text-primary-foreground">
|
||||
<IconPlaceholder
|
||||
lucide="GalleryVerticalEndIcon"
|
||||
tabler="IconLayoutRows"
|
||||
hugeicons="LayoutBottomIcon"
|
||||
phosphor="RowsIcon"
|
||||
remixicon="RiGalleryLine"
|
||||
className="size-4"
|
||||
/>
|
||||
</div>
|
||||
Acme Inc.
|
||||
</a>
|
||||
</div>
|
||||
<div className="flex flex-1 items-center justify-center">
|
||||
<div className="w-full max-w-xs">
|
||||
<LoginForm />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative hidden bg-muted lg:block">
|
||||
<img
|
||||
src="/placeholder.svg"
|
||||
alt="Image"
|
||||
className="absolute inset-0 h-full w-full object-cover dark:brightness-[0.2] dark:grayscale"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import { cn } from "cn"
|
||||
|
||||
import { Button } from "@/registry/bases/base/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/base/ui/card"
|
||||
import {
|
||||
Field,
|
||||
FieldDescription,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
FieldSeparator,
|
||||
} from "@/registry/bases/base/ui/field"
|
||||
import { Input } from "@/registry/bases/base/ui/input"
|
||||
|
||||
export function LoginForm({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div className={cn("flex flex-col gap-6", className)} {...props}>
|
||||
<Card>
|
||||
<CardHeader className="text-center">
|
||||
<CardTitle className="text-xl">Welcome back</CardTitle>
|
||||
<CardDescription>
|
||||
Login with your Apple or Google account
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form>
|
||||
<FieldGroup>
|
||||
<Field>
|
||||
<Button variant="outline" type="button">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M12.152 6.896c-.948 0-2.415-1.078-3.96-1.04-2.04.027-3.91 1.183-4.961 3.014-2.117 3.675-.546 9.103 1.519 12.09 1.013 1.454 2.208 3.09 3.792 3.039 1.52-.065 2.09-.987 3.935-.987 1.831 0 2.35.987 3.96.948 1.637-.026 2.676-1.48 3.676-2.948 1.156-1.688 1.636-3.325 1.662-3.415-.039-.013-3.182-1.221-3.22-4.857-.026-3.04 2.48-4.494 2.597-4.559-1.429-2.09-3.623-2.324-4.39-2.376-2-.156-3.675 1.09-4.61 1.09zM15.53 3.83c.843-1.012 1.4-2.427 1.245-3.83-1.207.052-2.662.805-3.532 1.818-.78.896-1.454 2.338-1.273 3.714 1.338.104 2.715-.688 3.559-1.701"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
Login with Apple
|
||||
</Button>
|
||||
<Button variant="outline" type="button">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
Login with Google
|
||||
</Button>
|
||||
</Field>
|
||||
<FieldSeparator className="*:data-[slot=field-separator-content]:bg-card">
|
||||
Or continue with
|
||||
</FieldSeparator>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="email">Email</FieldLabel>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="m@example.com"
|
||||
required
|
||||
/>
|
||||
</Field>
|
||||
<Field>
|
||||
<div className="flex items-center">
|
||||
<FieldLabel htmlFor="password">Password</FieldLabel>
|
||||
<a
|
||||
href="#"
|
||||
className="ml-auto text-sm underline-offset-4 hover:underline"
|
||||
>
|
||||
Forgot your password?
|
||||
</a>
|
||||
</div>
|
||||
<Input id="password" type="password" required />
|
||||
</Field>
|
||||
<Field>
|
||||
<Button type="submit">Login</Button>
|
||||
<FieldDescription className="text-center">
|
||||
Don't have an account? <a href="#">Sign up</a>
|
||||
</FieldDescription>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<FieldDescription className="px-6 text-center">
|
||||
By clicking continue, you agree to our <a href="#">Terms of Service</a>{" "}
|
||||
and <a href="#">Privacy Policy</a>.
|
||||
</FieldDescription>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
27
apps/v4/registry/bases/base/blocks/login-03/page.tsx
Normal file
27
apps/v4/registry/bases/base/blocks/login-03/page.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
"use client"
|
||||
|
||||
import { LoginForm } from "@/registry/bases/base/blocks/login-03/components/login-form"
|
||||
import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"
|
||||
|
||||
export default function LoginPage() {
|
||||
return (
|
||||
<div className="flex min-h-svh flex-col items-center justify-center gap-6 bg-muted p-6 md:p-10">
|
||||
<div className="flex w-full max-w-sm flex-col gap-6">
|
||||
<a href="#" className="flex items-center gap-2 self-center font-medium">
|
||||
<div className="flex size-6 items-center justify-center rounded-md bg-primary text-primary-foreground">
|
||||
<IconPlaceholder
|
||||
lucide="GalleryVerticalEndIcon"
|
||||
tabler="IconLayoutRows"
|
||||
hugeicons="LayoutBottomIcon"
|
||||
phosphor="RowsIcon"
|
||||
remixicon="RiGalleryLine"
|
||||
className="size-4"
|
||||
/>
|
||||
</div>
|
||||
Acme Inc.
|
||||
</a>
|
||||
<LoginForm />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import { cn } from "cn"
|
||||
|
||||
import { Button } from "@/registry/bases/base/ui/button"
|
||||
import { Card, CardContent } from "@/registry/bases/base/ui/card"
|
||||
import {
|
||||
Field,
|
||||
FieldDescription,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
FieldSeparator,
|
||||
} from "@/registry/bases/base/ui/field"
|
||||
import { Input } from "@/registry/bases/base/ui/input"
|
||||
|
||||
export function LoginForm({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div className={cn("flex flex-col gap-6", className)} {...props}>
|
||||
<Card className="overflow-hidden p-0">
|
||||
<CardContent className="grid p-0 md:grid-cols-2">
|
||||
<form className="p-6 md:p-8">
|
||||
<FieldGroup>
|
||||
<div className="flex flex-col items-center gap-2 text-center">
|
||||
<h1 className="text-2xl font-bold">Welcome back</h1>
|
||||
<p className="text-balance text-muted-foreground">
|
||||
Login to your Acme Inc account
|
||||
</p>
|
||||
</div>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="email">Email</FieldLabel>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="m@example.com"
|
||||
required
|
||||
/>
|
||||
</Field>
|
||||
<Field>
|
||||
<div className="flex items-center">
|
||||
<FieldLabel htmlFor="password">Password</FieldLabel>
|
||||
<a
|
||||
href="#"
|
||||
className="ml-auto text-sm underline-offset-2 hover:underline"
|
||||
>
|
||||
Forgot your password?
|
||||
</a>
|
||||
</div>
|
||||
<Input id="password" type="password" required />
|
||||
</Field>
|
||||
<Field>
|
||||
<Button type="submit">Login</Button>
|
||||
</Field>
|
||||
<FieldSeparator className="*:data-[slot=field-separator-content]:bg-card">
|
||||
Or continue with
|
||||
</FieldSeparator>
|
||||
<Field className="grid grid-cols-3 gap-4">
|
||||
<Button variant="outline" type="button">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M12.152 6.896c-.948 0-2.415-1.078-3.96-1.04-2.04.027-3.91 1.183-4.961 3.014-2.117 3.675-.546 9.103 1.519 12.09 1.013 1.454 2.208 3.09 3.792 3.039 1.52-.065 2.09-.987 3.935-.987 1.831 0 2.35.987 3.96.948 1.637-.026 2.676-1.48 3.676-2.948 1.156-1.688 1.636-3.325 1.662-3.415-.039-.013-3.182-1.221-3.22-4.857-.026-3.04 2.48-4.494 2.597-4.559-1.429-2.09-3.623-2.324-4.39-2.376-2-.156-3.675 1.09-4.61 1.09zM15.53 3.83c.843-1.012 1.4-2.427 1.245-3.83-1.207.052-2.662.805-3.532 1.818-.78.896-1.454 2.338-1.273 3.714 1.338.104 2.715-.688 3.559-1.701"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
<span className="sr-only">Login with Apple</span>
|
||||
</Button>
|
||||
<Button variant="outline" type="button">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
<span className="sr-only">Login with Google</span>
|
||||
</Button>
|
||||
<Button variant="outline" type="button">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M6.915 4.03c-1.968 0-3.683 1.28-4.871 3.113C.704 9.208 0 11.883 0 14.449c0 .706.07 1.369.21 1.973a6.624 6.624 0 0 0 .265.86 5.297 5.297 0 0 0 .371.761c.696 1.159 1.818 1.927 3.593 1.927 1.497 0 2.633-.671 3.965-2.444.76-1.012 1.144-1.626 2.663-4.32l.756-1.339.186-.325c.061.1.121.196.183.3l2.152 3.595c.724 1.21 1.665 2.556 2.47 3.314 1.046.987 1.992 1.22 3.06 1.22 1.075 0 1.876-.355 2.455-.843a3.743 3.743 0 0 0 .81-.973c.542-.939.861-2.127.861-3.745 0-2.72-.681-5.357-2.084-7.45-1.282-1.912-2.957-2.93-4.716-2.93-1.047 0-2.088.467-3.053 1.308-.652.57-1.257 1.29-1.82 2.05-.69-.875-1.335-1.547-1.958-2.056-1.182-.966-2.315-1.303-3.454-1.303zm10.16 2.053c1.147 0 2.188.758 2.992 1.999 1.132 1.748 1.647 4.195 1.647 6.4 0 1.548-.368 2.9-1.839 2.9-.58 0-1.027-.23-1.664-1.004-.496-.601-1.343-1.878-2.832-4.358l-.617-1.028a44.908 44.908 0 0 0-1.255-1.98c.07-.109.141-.224.211-.327 1.12-1.667 2.118-2.602 3.358-2.602zm-10.201.553c1.265 0 2.058.791 2.675 1.446.307.327.737.871 1.234 1.579l-1.02 1.566c-.757 1.163-1.882 3.017-2.837 4.338-1.191 1.649-1.81 1.817-2.486 1.817-.524 0-1.038-.237-1.383-.794-.263-.426-.464-1.13-.464-2.046 0-2.221.63-4.535 1.66-6.088.454-.687.964-1.226 1.533-1.533a2.264 2.264 0 0 1 1.088-.285z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
<span className="sr-only">Login with Meta</span>
|
||||
</Button>
|
||||
</Field>
|
||||
<FieldDescription className="text-center">
|
||||
Don't have an account? <a href="#">Sign up</a>
|
||||
</FieldDescription>
|
||||
</FieldGroup>
|
||||
</form>
|
||||
<div className="relative hidden bg-muted md:block">
|
||||
<img
|
||||
src="/placeholder.svg"
|
||||
alt="Image"
|
||||
className="absolute inset-0 h-full w-full object-cover dark:brightness-[0.2] dark:grayscale"
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<FieldDescription className="px-6 text-center">
|
||||
By clicking continue, you agree to our <a href="#">Terms of Service</a>{" "}
|
||||
and <a href="#">Privacy Policy</a>.
|
||||
</FieldDescription>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
11
apps/v4/registry/bases/base/blocks/login-04/page.tsx
Normal file
11
apps/v4/registry/bases/base/blocks/login-04/page.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import { LoginForm } from "@/registry/bases/base/blocks/login-04/components/login-form"
|
||||
|
||||
export default function LoginPage() {
|
||||
return (
|
||||
<div className="flex min-h-svh flex-col items-center justify-center bg-muted p-6 md:p-10">
|
||||
<div className="w-full max-w-sm md:max-w-4xl">
|
||||
<LoginForm />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
"use client"
|
||||
|
||||
import { cn } from "cn"
|
||||
|
||||
import { Button } from "@/registry/bases/base/ui/button"
|
||||
import {
|
||||
Field,
|
||||
FieldDescription,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
FieldSeparator,
|
||||
} from "@/registry/bases/base/ui/field"
|
||||
import { Input } from "@/registry/bases/base/ui/input"
|
||||
import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"
|
||||
|
||||
export function LoginForm({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div className={cn("flex flex-col gap-6", className)} {...props}>
|
||||
<form>
|
||||
<FieldGroup>
|
||||
<div className="flex flex-col items-center gap-2 text-center">
|
||||
<a
|
||||
href="#"
|
||||
className="flex flex-col items-center gap-2 font-medium"
|
||||
>
|
||||
<div className="flex size-8 items-center justify-center rounded-md">
|
||||
<IconPlaceholder
|
||||
lucide="GalleryVerticalEndIcon"
|
||||
tabler="IconLayoutRows"
|
||||
hugeicons="LayoutBottomIcon"
|
||||
phosphor="RowsIcon"
|
||||
remixicon="RiGalleryLine"
|
||||
className="size-6"
|
||||
/>
|
||||
</div>
|
||||
<span className="sr-only">Acme Inc.</span>
|
||||
</a>
|
||||
<h1 className="text-xl font-bold">Welcome to Acme Inc.</h1>
|
||||
<FieldDescription>
|
||||
Don't have an account? <a href="#">Sign up</a>
|
||||
</FieldDescription>
|
||||
</div>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="email">Email</FieldLabel>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="m@example.com"
|
||||
required
|
||||
/>
|
||||
</Field>
|
||||
<Field>
|
||||
<Button type="submit">Login</Button>
|
||||
</Field>
|
||||
<FieldSeparator>Or</FieldSeparator>
|
||||
<Field className="grid gap-4 sm:grid-cols-2">
|
||||
<Button variant="outline" type="button">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M12.152 6.896c-.948 0-2.415-1.078-3.96-1.04-2.04.027-3.91 1.183-4.961 3.014-2.117 3.675-.546 9.103 1.519 12.09 1.013 1.454 2.208 3.09 3.792 3.039 1.52-.065 2.09-.987 3.935-.987 1.831 0 2.35.987 3.96.948 1.637-.026 2.676-1.48 3.676-2.948 1.156-1.688 1.636-3.325 1.662-3.415-.039-.013-3.182-1.221-3.22-4.857-.026-3.04 2.48-4.494 2.597-4.559-1.429-2.09-3.623-2.324-4.39-2.376-2-.156-3.675 1.09-4.61 1.09zM15.53 3.83c.843-1.012 1.4-2.427 1.245-3.83-1.207.052-2.662.805-3.532 1.818-.78.896-1.454 2.338-1.273 3.714 1.338.104 2.715-.688 3.559-1.701"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
Continue with Apple
|
||||
</Button>
|
||||
<Button variant="outline" type="button">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
Continue with Google
|
||||
</Button>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</form>
|
||||
<FieldDescription className="px-6 text-center">
|
||||
By clicking continue, you agree to our <a href="#">Terms of Service</a>{" "}
|
||||
and <a href="#">Privacy Policy</a>.
|
||||
</FieldDescription>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
11
apps/v4/registry/bases/base/blocks/login-05/page.tsx
Normal file
11
apps/v4/registry/bases/base/blocks/login-05/page.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import { LoginForm } from "@/registry/bases/base/blocks/login-05/components/login-form"
|
||||
|
||||
export default function LoginPage() {
|
||||
return (
|
||||
<div className="flex min-h-svh flex-col items-center justify-center gap-6 bg-background p-6 md:p-10">
|
||||
<div className="w-full max-w-sm">
|
||||
<LoginForm />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
"use client"
|
||||
|
||||
import { Button } from "@/registry/bases/base/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/base/ui/card"
|
||||
import { Field, FieldGroup, FieldLabel } from "@/registry/bases/base/ui/field"
|
||||
import { Input } from "@/registry/bases/base/ui/input"
|
||||
import {
|
||||
Item,
|
||||
ItemContent,
|
||||
ItemDescription,
|
||||
ItemMedia,
|
||||
ItemTitle,
|
||||
} from "@/registry/bases/base/ui/item"
|
||||
import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"
|
||||
|
||||
export function AccountAccess() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Account Access</CardTitle>
|
||||
<CardDescription>
|
||||
Update your credentials or re-authenticate.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<FieldGroup>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="email-address">Email Address</FieldLabel>
|
||||
<Input
|
||||
id="email-address"
|
||||
type="email"
|
||||
defaultValue="artist@studio.inc"
|
||||
/>
|
||||
</Field>
|
||||
<Field>
|
||||
<div className="flex items-center justify-between">
|
||||
<FieldLabel htmlFor="current-password">
|
||||
Current Password
|
||||
</FieldLabel>
|
||||
<a
|
||||
href="#"
|
||||
className="text-xs font-medium tracking-wider text-muted-foreground uppercase hover:text-foreground"
|
||||
>
|
||||
Forgot?
|
||||
</a>
|
||||
</div>
|
||||
<Input
|
||||
id="current-password"
|
||||
type="password"
|
||||
defaultValue="password123"
|
||||
/>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</CardContent>
|
||||
<CardFooter className="flex-col gap-4">
|
||||
<Button className="w-full">
|
||||
<IconPlaceholder
|
||||
lucide="LockKeyholeIcon"
|
||||
tabler="IconLock"
|
||||
hugeicons="SquareLock02Icon"
|
||||
phosphor="LockKeyIcon"
|
||||
remixicon="RiLockLine"
|
||||
/>
|
||||
Update Security
|
||||
</Button>
|
||||
<Item variant="muted" render={<a href="#" />}>
|
||||
<ItemMedia variant="icon">
|
||||
<IconPlaceholder
|
||||
lucide="AlertCircleIcon"
|
||||
tabler="IconAlertCircle"
|
||||
hugeicons="AlertCircleIcon"
|
||||
phosphor="WarningCircleIcon"
|
||||
remixicon="RiErrorWarningLine"
|
||||
className="text-destructive"
|
||||
/>
|
||||
</ItemMedia>
|
||||
<ItemContent>
|
||||
<ItemTitle>Danger Zone</ItemTitle>
|
||||
<ItemDescription className="line-clamp-1">
|
||||
Archive account and remove catalog
|
||||
</ItemDescription>
|
||||
</ItemContent>
|
||||
<IconPlaceholder
|
||||
lucide="ArrowRightIcon"
|
||||
tabler="IconArrowRight"
|
||||
hugeicons="ArrowRight01Icon"
|
||||
phosphor="ArrowRightIcon"
|
||||
remixicon="RiArrowRightLine"
|
||||
className="size-4"
|
||||
/>
|
||||
</Item>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { Badge } from "@/registry/bases/base/ui/badge"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/base/ui/card"
|
||||
import { Separator } from "@/registry/bases/base/ui/separator"
|
||||
|
||||
export function AlbumCard() {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
<div className="relative overflow-hidden rounded-lg">
|
||||
<img
|
||||
src="https://images.unsplash.com/photo-1534088568595-a066f410bcda?w=800&h=800&fit=crop"
|
||||
alt="Synthetic Horizons EP cover art"
|
||||
className="aspect-square w-full object-cover"
|
||||
/>
|
||||
<Badge className="absolute top-3 right-3">$26,033.79</Badge>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<CardTitle>Synthetic Horizons EP</CardTitle>
|
||||
<CardDescription className="text-xs tracking-wider uppercase">
|
||||
Released Aug 14, 2023
|
||||
</CardDescription>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter className="flex-col gap-4">
|
||||
<Separator />
|
||||
<div className="grid w-full grid-cols-2 gap-4">
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<span className="text-xs tracking-wider text-muted-foreground uppercase">
|
||||
Tracks
|
||||
</span>
|
||||
<span className="text-lg font-medium tabular-nums">6 Tracks</span>
|
||||
</div>
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<span className="line-clamp-1 text-xs tracking-wider text-muted-foreground uppercase">
|
||||
Cumulative Streams
|
||||
</span>
|
||||
<span className="text-lg font-medium tabular-nums">6,198,524</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
"use client"
|
||||
|
||||
import { Bar, BarChart, XAxis } from "recharts"
|
||||
|
||||
import { Badge } from "@/registry/bases/base/ui/badge"
|
||||
import { Button } from "@/registry/bases/base/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/base/ui/card"
|
||||
import {
|
||||
ChartContainer,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
type ChartConfig,
|
||||
} from "@/registry/bases/base/ui/chart"
|
||||
|
||||
const activityData = [
|
||||
{ month: "Jan", amount: 40 },
|
||||
{ month: "Feb", amount: 55 },
|
||||
{ month: "Mar", amount: 35 },
|
||||
{ month: "Apr", amount: 60 },
|
||||
{ month: "May", amount: 45 },
|
||||
{ month: "Jun", amount: 50 },
|
||||
{ month: "Jul", amount: 65 },
|
||||
{ month: "Aug", amount: 40 },
|
||||
{ month: "Sep", amount: 55 },
|
||||
{ month: "Oct", amount: 70 },
|
||||
{ month: "Nov", amount: 45 },
|
||||
{ month: "Dec", amount: 80 },
|
||||
]
|
||||
|
||||
const chartConfig = {
|
||||
amount: {
|
||||
label: "Activity",
|
||||
color: "var(--chart-2)",
|
||||
},
|
||||
} satisfies ChartConfig
|
||||
|
||||
export function CardOverview() {
|
||||
return (
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<Card>
|
||||
<CardContent>
|
||||
<CardDescription>Card Balance</CardDescription>
|
||||
<CardTitle className="text-2xl tabular-nums">US$12.94</CardTitle>
|
||||
<CardDescription className="tabular-nums">
|
||||
US$11,337.06 Available
|
||||
</CardDescription>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="flex flex-col justify-between">
|
||||
<CardContent className="flex flex-1 flex-col justify-between">
|
||||
<div className="flex flex-col gap-1">
|
||||
<CardDescription>Payment Due</CardDescription>
|
||||
<CardTitle className="text-2xl">1 Apr</CardTitle>
|
||||
</div>
|
||||
<Button variant="outline" size="sm" className="mt-3 w-full">
|
||||
Pay Early
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="col-span-2">
|
||||
<CardContent className="flex flex-col gap-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<CardDescription>Yearly Activity</CardDescription>
|
||||
<Badge variant="secondary">+US$0.25 Daily Cash</Badge>
|
||||
</div>
|
||||
<ChartContainer config={chartConfig} className="h-20 w-full">
|
||||
<BarChart
|
||||
data={activityData}
|
||||
margin={{ top: 4, right: 0, bottom: 0, left: 0 }}
|
||||
>
|
||||
<XAxis
|
||||
dataKey="month"
|
||||
tickLine={false}
|
||||
tickMargin={4}
|
||||
axisLine={false}
|
||||
tickFormatter={(v) => String(v).slice(0, 1)}
|
||||
className="text-[10px]"
|
||||
/>
|
||||
<ChartTooltip
|
||||
cursor={false}
|
||||
content={<ChartTooltipContent hideLabel />}
|
||||
/>
|
||||
<Bar
|
||||
dataKey="amount"
|
||||
fill="var(--color-amount)"
|
||||
radius={[3, 3, 0, 0]}
|
||||
/>
|
||||
</BarChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
"use client"
|
||||
|
||||
import { Button } from "@/registry/bases/base/ui/button"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupInput,
|
||||
} from "@/registry/bases/base/ui/input-group"
|
||||
import {
|
||||
ToggleGroup,
|
||||
ToggleGroupItem,
|
||||
} from "@/registry/bases/base/ui/toggle-group"
|
||||
import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"
|
||||
|
||||
export function CatalogToolbar() {
|
||||
return (
|
||||
<div className="flex items-center gap-3">
|
||||
<InputGroup className="flex-1">
|
||||
<InputGroupAddon>
|
||||
<IconPlaceholder
|
||||
lucide="SearchIcon"
|
||||
tabler="IconSearch"
|
||||
hugeicons="Search01Icon"
|
||||
phosphor="MagnifyingGlassIcon"
|
||||
remixicon="RiSearchLine"
|
||||
/>
|
||||
</InputGroupAddon>
|
||||
<InputGroupInput placeholder="Search releases or catalog..." />
|
||||
</InputGroup>
|
||||
<Button>
|
||||
<IconPlaceholder
|
||||
lucide="PlusIcon"
|
||||
tabler="IconPlus"
|
||||
hugeicons="Add01Icon"
|
||||
phosphor="PlusIcon"
|
||||
remixicon="RiAddLine"
|
||||
/>
|
||||
Upload New Release
|
||||
</Button>
|
||||
<ToggleGroup defaultValue={["releases"]} variant="outline">
|
||||
<ToggleGroupItem value="all-tracks">All Tracks</ToggleGroupItem>
|
||||
<ToggleGroupItem value="releases">Releases</ToggleGroupItem>
|
||||
<ToggleGroupItem value="top-earners">Top Earners</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import { Badge } from "@/registry/bases/base/ui/badge"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/base/ui/card"
|
||||
import { Item, ItemContent } from "@/registry/bases/base/ui/item"
|
||||
import { Separator } from "@/registry/bases/base/ui/separator"
|
||||
|
||||
export function ClaimableBalance() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardDescription>Claimable Balance</CardDescription>
|
||||
<CardTitle className="text-5xl tabular-nums">$0.00</CardTitle>
|
||||
<Badge variant="outline">
|
||||
<span className="size-2 rounded-full bg-yellow-500" />
|
||||
Pending Setup
|
||||
</Badge>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-1 flex-col justify-end">
|
||||
<Item variant="muted" className="flex-col items-stretch">
|
||||
<ItemContent className="gap-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Net Royalties
|
||||
</span>
|
||||
<span className="text-sm font-medium tabular-nums">$0.00</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Processing Fee
|
||||
</span>
|
||||
<span className="text-sm font-medium tabular-nums">-$0.00</span>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Total Ready to Claim
|
||||
</span>
|
||||
<span className="text-sm font-semibold tabular-nums">
|
||||
$0.00 USD
|
||||
</span>
|
||||
</div>
|
||||
</ItemContent>
|
||||
</Item>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<CardDescription>
|
||||
Once your bank is connected, balances over $10.00 are automatically
|
||||
eligible for monthly distribution on the 15th of each month.
|
||||
</CardDescription>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
"use client"
|
||||
|
||||
import { Bar, BarChart, XAxis } from "recharts"
|
||||
|
||||
import { Badge } from "@/registry/bases/base/ui/badge"
|
||||
import { Button } from "@/registry/bases/base/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardAction,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/base/ui/card"
|
||||
import {
|
||||
ChartContainer,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
type ChartConfig,
|
||||
} from "@/registry/bases/base/ui/chart"
|
||||
import {
|
||||
Item,
|
||||
ItemContent,
|
||||
ItemDescription,
|
||||
} from "@/registry/bases/base/ui/item"
|
||||
import { useDesignSystemSearchParams } from "@/app/(app)/create/lib/search-params"
|
||||
|
||||
const chartData = [
|
||||
{ month: "Dec", amount: 800 },
|
||||
{ month: "Jan", amount: 1100 },
|
||||
{ month: "Feb", amount: 900 },
|
||||
{ month: "Mar", amount: 1300 },
|
||||
{ month: "Apr", amount: 750 },
|
||||
{ month: "May", amount: 1400 },
|
||||
]
|
||||
|
||||
const chartConfig = {
|
||||
amount: {
|
||||
label: "Contribution",
|
||||
color: "var(--chart-2)",
|
||||
},
|
||||
} satisfies ChartConfig
|
||||
|
||||
export function ContributionHistory() {
|
||||
const [params] = useDesignSystemSearchParams()
|
||||
const isRounded = !["lyra", "sera"].includes(params.style)
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Contribution History</CardTitle>
|
||||
<CardDescription>Last 6 months of activity</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ChartContainer config={chartConfig} className="h-[200px] w-full">
|
||||
<BarChart
|
||||
accessibilityLayer
|
||||
data={chartData}
|
||||
margin={{ left: 0, right: 0, top: 8, bottom: 0 }}
|
||||
>
|
||||
<XAxis
|
||||
dataKey="month"
|
||||
tickLine={false}
|
||||
tickMargin={8}
|
||||
axisLine={false}
|
||||
/>
|
||||
<ChartTooltip
|
||||
cursor={false}
|
||||
content={<ChartTooltipContent hideLabel className="min-w-40" />}
|
||||
/>
|
||||
<Bar
|
||||
dataKey="amount"
|
||||
fill="var(--color-amount)"
|
||||
radius={isRounded ? [6, 6, 0, 0] : 0}
|
||||
maxBarSize={40}
|
||||
/>
|
||||
</BarChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
<CardContent>
|
||||
<div className="grid w-full grid-cols-1 gap-3 md:grid-cols-2">
|
||||
<Item variant="muted" className="flex-col items-stretch">
|
||||
<ItemContent className="gap-1">
|
||||
<ItemDescription className="text-xs font-medium tracking-wider text-muted-foreground uppercase">
|
||||
Upcoming
|
||||
</ItemDescription>
|
||||
<span className="cn-font-heading text-lg font-semibold">
|
||||
May 25, 2024
|
||||
</span>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
$1,000 scheduled
|
||||
</span>
|
||||
</ItemContent>
|
||||
</Item>
|
||||
<Item variant="muted" className="flex-col items-stretch">
|
||||
<ItemContent className="gap-1">
|
||||
<ItemDescription className="text-xs font-medium tracking-wider text-muted-foreground uppercase">
|
||||
Auto-Save Plan
|
||||
</ItemDescription>
|
||||
<span className="cn-font-heading text-lg font-semibold">
|
||||
Accelerated
|
||||
</span>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Recurring weekly
|
||||
</span>
|
||||
</ItemContent>
|
||||
</Item>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button className="w-full">View Full Report</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import { Button } from "@/registry/bases/base/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
} from "@/registry/bases/base/ui/card"
|
||||
import { Item } from "@/registry/bases/base/ui/item"
|
||||
import { Label } from "@/registry/bases/base/ui/label"
|
||||
import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"
|
||||
|
||||
export function CoverArt() {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="flex flex-col gap-3">
|
||||
<Label
|
||||
htmlFor="cover-art"
|
||||
className="text-center text-xs font-normal tracking-wider text-muted-foreground uppercase"
|
||||
>
|
||||
Cover Art
|
||||
</Label>
|
||||
<Item className="aspect-square" variant="outline">
|
||||
<label
|
||||
htmlFor="cover-art"
|
||||
className="flex size-full cursor-pointer items-center justify-center"
|
||||
>
|
||||
<IconPlaceholder
|
||||
lucide="ImageIcon"
|
||||
tabler="IconPhoto"
|
||||
hugeicons="Image01Icon"
|
||||
phosphor="ImageIcon"
|
||||
remixicon="RiImageLine"
|
||||
className="size-10 text-muted-foreground/50"
|
||||
/>
|
||||
</label>
|
||||
</Item>
|
||||
<input
|
||||
id="cover-art"
|
||||
type="file"
|
||||
accept="image/jpeg,image/png"
|
||||
className="sr-only"
|
||||
/>
|
||||
</CardContent>
|
||||
<CardFooter className="flex-col gap-2">
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="w-full"
|
||||
render={<label htmlFor="cover-art" className="cursor-pointer" />}
|
||||
nativeButton={false}
|
||||
>
|
||||
Upload Artwork
|
||||
</Button>
|
||||
<CardDescription className="text-center text-xs">
|
||||
Minimum 3000 × 3000px
|
||||
<br />
|
||||
JPEG or PNG only
|
||||
</CardDescription>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
"use client"
|
||||
|
||||
import { Bar, BarChart, XAxis } from "recharts"
|
||||
|
||||
import { Button } from "@/registry/bases/base/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardAction,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/base/ui/card"
|
||||
import {
|
||||
ChartContainer,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
type ChartConfig,
|
||||
} from "@/registry/bases/base/ui/chart"
|
||||
import {
|
||||
Item,
|
||||
ItemContent,
|
||||
ItemDescription,
|
||||
ItemGroup,
|
||||
ItemTitle,
|
||||
} from "@/registry/bases/base/ui/item"
|
||||
import { useDesignSystemSearchParams } from "@/app/(app)/create/lib/search-params"
|
||||
import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"
|
||||
|
||||
const HOLDINGS = [
|
||||
{
|
||||
name: "Vanguard VIG",
|
||||
shares: "450 Shares",
|
||||
amount: "$1,842.10",
|
||||
data: [
|
||||
{ q: "Q1", value: 380 },
|
||||
{ q: "Q2", value: 420 },
|
||||
{ q: "Q3", value: 390 },
|
||||
{ q: "Q4", value: 652 },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "S&P 500 VOO",
|
||||
shares: "112 Shares",
|
||||
amount: "$928.40",
|
||||
data: [
|
||||
{ q: "Q1", value: 180 },
|
||||
{ q: "Q2", value: 210 },
|
||||
{ q: "Q3", value: 320 },
|
||||
{ q: "Q4", value: 218 },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Apple AAPL",
|
||||
shares: "85 Shares",
|
||||
amount: "$340.00",
|
||||
data: [
|
||||
{ q: "Q1", value: 60 },
|
||||
{ q: "Q2", value: 70 },
|
||||
{ q: "Q3", value: 120 },
|
||||
{ q: "Q4", value: 90 },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Realty Income",
|
||||
shares: "320 Shares",
|
||||
amount: "$1,139.50",
|
||||
data: [
|
||||
{ q: "Q1", value: 240 },
|
||||
{ q: "Q2", value: 260 },
|
||||
{ q: "Q3", value: 280 },
|
||||
{ q: "Q4", value: 360 },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const miniChartConfig = {
|
||||
value: {
|
||||
label: "Dividend",
|
||||
color: "var(--chart-2)",
|
||||
},
|
||||
} satisfies ChartConfig
|
||||
|
||||
export function DividendIncome() {
|
||||
const [params] = useDesignSystemSearchParams()
|
||||
const isRounded = !["lyra", "sera"].includes(params.style)
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Q2 Dividend Income</CardTitle>
|
||||
<CardDescription>
|
||||
Quarterly dividend payouts across your portfolio holdings.
|
||||
</CardDescription>
|
||||
<CardAction>
|
||||
<Button variant="ghost" size="icon-sm" className="bg-muted">
|
||||
<IconPlaceholder
|
||||
lucide="XIcon"
|
||||
tabler="IconX"
|
||||
hugeicons="Cancel01Icon"
|
||||
phosphor="XIcon"
|
||||
remixicon="RiCloseLine"
|
||||
/>
|
||||
</Button>
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ItemGroup>
|
||||
{HOLDINGS.map((holding) => (
|
||||
<Item key={holding.name} variant="muted">
|
||||
<ItemContent>
|
||||
<ItemTitle>{holding.name}</ItemTitle>
|
||||
<ItemDescription>{holding.shares}</ItemDescription>
|
||||
</ItemContent>
|
||||
<ChartContainer
|
||||
config={miniChartConfig}
|
||||
className="hidden h-8 w-24 md:block"
|
||||
>
|
||||
<BarChart
|
||||
data={holding.data}
|
||||
margin={{ top: 0, right: 0, bottom: 0, left: 0 }}
|
||||
>
|
||||
<ChartTooltip
|
||||
cursor={false}
|
||||
content={<ChartTooltipContent hideLabel />}
|
||||
/>
|
||||
<Bar
|
||||
dataKey="value"
|
||||
fill="var(--color-value)"
|
||||
radius={isRounded ? [3, 3, 0, 0] : 0}
|
||||
/>
|
||||
</BarChart>
|
||||
</ChartContainer>
|
||||
<span className="hidden text-sm font-semibold tabular-nums md:block">
|
||||
{holding.amount}
|
||||
</span>
|
||||
</Item>
|
||||
))}
|
||||
</ItemGroup>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { Button } from "@/registry/bases/base/ui/button"
|
||||
import { Card, CardContent } from "@/registry/bases/base/ui/card"
|
||||
import {
|
||||
Empty,
|
||||
EmptyContent,
|
||||
EmptyDescription,
|
||||
EmptyHeader,
|
||||
EmptyMedia,
|
||||
EmptyTitle,
|
||||
} from "@/registry/bases/base/ui/empty"
|
||||
import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"
|
||||
|
||||
export function EmptyConnectBank() {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Empty className="p-4">
|
||||
<EmptyMedia variant="icon">
|
||||
<IconPlaceholder
|
||||
lucide="CreditCardIcon"
|
||||
tabler="IconCreditCard"
|
||||
hugeicons="CreditCardIcon"
|
||||
phosphor="CreditCardIcon"
|
||||
remixicon="RiBankCardLine"
|
||||
/>
|
||||
</EmptyMedia>
|
||||
<EmptyHeader>
|
||||
<EmptyTitle>Connect Bank</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
Link your payout method to receive monthly royalty distributions
|
||||
automatically.
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
<EmptyContent>
|
||||
<Button>Set Up Payouts</Button>
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { Button } from "@/registry/bases/base/ui/button"
|
||||
import { Card, CardContent } from "@/registry/bases/base/ui/card"
|
||||
import {
|
||||
Empty,
|
||||
EmptyContent,
|
||||
EmptyDescription,
|
||||
EmptyHeader,
|
||||
EmptyMedia,
|
||||
EmptyTitle,
|
||||
} from "@/registry/bases/base/ui/empty"
|
||||
import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"
|
||||
|
||||
export function EmptyDistributeTrack() {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Empty className="p-4">
|
||||
<EmptyMedia variant="icon">
|
||||
<IconPlaceholder
|
||||
lucide="PlusIcon"
|
||||
tabler="IconPlus"
|
||||
hugeicons="Add01Icon"
|
||||
phosphor="PlusIcon"
|
||||
remixicon="RiAddLine"
|
||||
/>
|
||||
</EmptyMedia>
|
||||
<EmptyHeader>
|
||||
<EmptyTitle>Distribute Track</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
Upload your first master to start reaching listeners on Spotify,
|
||||
Apple Music, and more.
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
<EmptyContent>
|
||||
<Button>Create Release</Button>
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { Button } from "@/registry/bases/base/ui/button"
|
||||
import { Card, CardContent } from "@/registry/bases/base/ui/card"
|
||||
import {
|
||||
Empty,
|
||||
EmptyContent,
|
||||
EmptyDescription,
|
||||
EmptyHeader,
|
||||
EmptyMedia,
|
||||
EmptyTitle,
|
||||
} from "@/registry/bases/base/ui/empty"
|
||||
import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"
|
||||
|
||||
export function EmptyExploreCatalog() {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Empty className="p-4">
|
||||
<EmptyMedia variant="icon">
|
||||
<IconPlaceholder
|
||||
lucide="AudioLinesIcon"
|
||||
tabler="IconPlayerRecordFilled"
|
||||
hugeicons="AudioWave01Icon"
|
||||
phosphor="RecordIcon"
|
||||
remixicon="RiRecordCircleLine"
|
||||
/>
|
||||
</EmptyMedia>
|
||||
<EmptyHeader>
|
||||
<EmptyTitle>Explore Catalog</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
Check your ISRC codes, metadata, and visual assets before going
|
||||
live.
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
<EmptyContent>
|
||||
<Button>View Catalog</Button>
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
124
apps/v4/registry/bases/base/blocks/preview-02/cards/faq.tsx
Normal file
124
apps/v4/registry/bases/base/blocks/preview-02/cards/faq.tsx
Normal file
@@ -0,0 +1,124 @@
|
||||
"use client"
|
||||
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from "@/registry/bases/base/ui/accordion"
|
||||
import { Button } from "@/registry/bases/base/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/base/ui/card"
|
||||
import {
|
||||
Tabs,
|
||||
TabsContent,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
} from "@/registry/bases/base/ui/tabs"
|
||||
|
||||
const GENERAL_QUESTIONS = [
|
||||
{
|
||||
q: "How secure is my financial data with Ledger?",
|
||||
a: "We use bank-level AES-256 encryption, SOC 2 Type II certified infrastructure, and never store your credentials. All connections use read-only access tokens. We are a SEC registered investment advisor.",
|
||||
},
|
||||
{
|
||||
q: "How do I connect my bank or investment accounts?",
|
||||
a: "Go to Settings > Linked Accounts and search for your institution. We support over 12,000 banks and brokerages via Plaid and MX.",
|
||||
},
|
||||
{
|
||||
q: "Can I export my data for tax purposes?",
|
||||
a: "Yes. Navigate to Reports > Tax Export to download a CSV or PDF summary of your transactions, dividends, and capital gains for any tax year.",
|
||||
},
|
||||
]
|
||||
|
||||
const BILLING_QUESTIONS = [
|
||||
{
|
||||
q: "What is the difference between Basic and Pro pricing tiers?",
|
||||
a: "Basic includes budgeting, goal tracking, and up to 3 linked accounts. Pro adds unlimited accounts, dividend tracking, portfolio analysis, and priority support.",
|
||||
},
|
||||
{
|
||||
q: "How do I cancel my subscription?",
|
||||
a: "Go to Settings > Billing > Manage Plan and click Cancel. Your access continues until the end of your current billing period.",
|
||||
},
|
||||
{
|
||||
q: "Do you offer a free trial?",
|
||||
a: "Yes. All new accounts start with a 14-day Pro trial. No credit card required.",
|
||||
},
|
||||
]
|
||||
|
||||
const GOALS_QUESTIONS = [
|
||||
{
|
||||
q: "How do I set up a custom financial goal?",
|
||||
a: "Click New Goal from the Savings Targets card. Choose a category, set a target amount and date, and we'll calculate the monthly contribution needed.",
|
||||
},
|
||||
{
|
||||
q: "Can I track multiple goals at once?",
|
||||
a: "Yes. Pro accounts can track unlimited goals. Basic accounts support up to 3 active goals.",
|
||||
},
|
||||
{
|
||||
q: "How are monthly contributions calculated?",
|
||||
a: "We divide the remaining amount by the number of months until your target date, adjusted for your current savings rate and any auto-transfer schedules.",
|
||||
},
|
||||
]
|
||||
|
||||
function QuestionList({
|
||||
questions,
|
||||
}: {
|
||||
questions: { q: string; a: string }[]
|
||||
}) {
|
||||
return (
|
||||
<Accordion defaultValue={[0]}>
|
||||
{questions.map((item, index) => (
|
||||
<AccordionItem key={index} value={index}>
|
||||
<AccordionTrigger>{item.q}</AccordionTrigger>
|
||||
<AccordionContent>{item.a}</AccordionContent>
|
||||
</AccordionItem>
|
||||
))}
|
||||
</Accordion>
|
||||
)
|
||||
}
|
||||
|
||||
export function Faq() {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Tabs defaultValue="general">
|
||||
<TabsList className="w-full">
|
||||
<TabsTrigger value="general" className="flex-1">
|
||||
General
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="billing" className="flex-1">
|
||||
Billing
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="goals" className="flex-1">
|
||||
Goals
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="general">
|
||||
<QuestionList questions={GENERAL_QUESTIONS} />
|
||||
</TabsContent>
|
||||
<TabsContent value="billing">
|
||||
<QuestionList questions={BILLING_QUESTIONS} />
|
||||
</TabsContent>
|
||||
<TabsContent value="goals">
|
||||
<QuestionList questions={GOALS_QUESTIONS} />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button variant="outline" className="w-full">
|
||||
Contact Support
|
||||
</Button>
|
||||
<Button variant="link" className="w-full">
|
||||
Learn More
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { Badge } from "@/registry/bases/base/ui/badge"
|
||||
import {
|
||||
Card,
|
||||
CardAction,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/base/ui/card"
|
||||
import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"
|
||||
|
||||
export function FrontDoor() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Front Door</CardTitle>
|
||||
<CardDescription>Smart Lock Pro</CardDescription>
|
||||
<CardAction>
|
||||
<div className="flex items-center gap-1.5 text-sm text-muted-foreground">
|
||||
Locked
|
||||
<IconPlaceholder
|
||||
lucide="LockIcon"
|
||||
tabler="IconLock"
|
||||
hugeicons="SquareLock02Icon"
|
||||
phosphor="LockKeyIcon"
|
||||
remixicon="RiLockLine"
|
||||
className="size-4"
|
||||
/>
|
||||
</div>
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="relative flex aspect-video items-center justify-center overflow-hidden rounded-lg bg-muted bg-[repeating-linear-gradient(45deg,transparent,transparent_10px,var(--border)_10px,var(--border)_11px)]">
|
||||
<Badge variant="destructive" className="absolute top-2 right-2">
|
||||
Live
|
||||
</Badge>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { Button } from "@/registry/bases/base/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardAction,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/base/ui/card"
|
||||
import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"
|
||||
|
||||
export function IndexInvesting() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Dollar-Cost Averaging</CardTitle>
|
||||
<CardDescription>
|
||||
A strategy for building wealth over time.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<CardDescription className="mt-3 text-sm leading-relaxed style-sera:mt-0">
|
||||
<a
|
||||
href="#"
|
||||
className="underline underline-offset-4 hover:text-primary"
|
||||
>
|
||||
Over time
|
||||
</a>
|
||||
, this smooths out the average cost of your investments. When prices
|
||||
drop, your fixed amount buys more shares. When prices rise, you buy
|
||||
fewer. The result is a lower average cost per share compared to
|
||||
lump-sum investing during volatile periods.
|
||||
</CardDescription>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
|
||||
import {
|
||||
Card,
|
||||
CardAction,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/base/ui/card"
|
||||
import {
|
||||
Item,
|
||||
ItemActions,
|
||||
ItemContent,
|
||||
ItemGroup,
|
||||
ItemMedia,
|
||||
ItemTitle,
|
||||
} from "@/registry/bases/base/ui/item"
|
||||
import { Slider } from "@/registry/bases/base/ui/slider"
|
||||
import { Switch } from "@/registry/bases/base/ui/switch"
|
||||
import {
|
||||
ToggleGroup,
|
||||
ToggleGroupItem,
|
||||
} from "@/registry/bases/base/ui/toggle-group"
|
||||
import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"
|
||||
|
||||
const SCENES = {
|
||||
cooking: { brightness: [90], colorTemp: [70], volume: [30], fade: [0] },
|
||||
dining: { brightness: [50], colorTemp: [40], volume: [20], fade: [60] },
|
||||
nightlight: { brightness: [15], colorTemp: [20], volume: [0], fade: [80] },
|
||||
focus: { brightness: [100], colorTemp: [85], volume: [0], fade: [0] },
|
||||
} as const
|
||||
|
||||
export function KitchenIsland() {
|
||||
const [enabled, setEnabled] = React.useState(true)
|
||||
const [scene, setScene] = React.useState("cooking")
|
||||
const [brightness, setBrightness] = React.useState([90])
|
||||
const [colorTemp, setColorTemp] = React.useState([70])
|
||||
const [volume, setVolume] = React.useState([30])
|
||||
const [fade, setFade] = React.useState([0])
|
||||
|
||||
const handleSceneChange = (value: string) => {
|
||||
if (!value) return
|
||||
setScene(value)
|
||||
const preset = SCENES[value as keyof typeof SCENES]
|
||||
setBrightness([...preset.brightness])
|
||||
setColorTemp([...preset.colorTemp])
|
||||
setVolume([...preset.volume])
|
||||
setFade([...preset.fade])
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Kitchen Island</CardTitle>
|
||||
<CardDescription>Hue Color Ambient</CardDescription>
|
||||
<CardAction>
|
||||
<Switch checked={enabled} onCheckedChange={setEnabled} />
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
<span className="sr-only">Scenes</span>
|
||||
<ToggleGroup
|
||||
value={[scene]}
|
||||
onValueChange={(value) => handleSceneChange(value[0] ?? "cooking")}
|
||||
variant="outline"
|
||||
spacing={1}
|
||||
className="flex-wrap"
|
||||
>
|
||||
<ToggleGroupItem value="cooking" disabled={!enabled}>
|
||||
Cooking
|
||||
</ToggleGroupItem>
|
||||
<ToggleGroupItem value="dining" disabled={!enabled}>
|
||||
Dining
|
||||
</ToggleGroupItem>
|
||||
<ToggleGroupItem value="nightlight" disabled={!enabled}>
|
||||
Nightlight
|
||||
</ToggleGroupItem>
|
||||
<ToggleGroupItem value="focus" disabled={!enabled}>
|
||||
Focus
|
||||
</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
</div>
|
||||
<ItemGroup>
|
||||
<Item size="sm" variant="outline">
|
||||
<ItemMedia variant="icon">
|
||||
<IconPlaceholder
|
||||
lucide="SunIcon"
|
||||
tabler="IconSun"
|
||||
hugeicons="Sun03Icon"
|
||||
phosphor="SunIcon"
|
||||
remixicon="RiSunLine"
|
||||
/>
|
||||
</ItemMedia>
|
||||
<ItemContent className="flex-row items-center gap-3">
|
||||
<ItemTitle className="shrink-0">Brightness</ItemTitle>
|
||||
</ItemContent>
|
||||
<ItemActions className="flex-1">
|
||||
<Slider
|
||||
value={brightness}
|
||||
onValueChange={(value) =>
|
||||
setBrightness(Array.isArray(value) ? [...value] : [value])
|
||||
}
|
||||
max={100}
|
||||
disabled={!enabled}
|
||||
className="w-full"
|
||||
/>
|
||||
</ItemActions>
|
||||
</Item>
|
||||
<Item size="sm" variant="outline">
|
||||
<ItemMedia variant="icon">
|
||||
<IconPlaceholder
|
||||
lucide="ThermometerIcon"
|
||||
tabler="IconThermometer"
|
||||
hugeicons="ThermometerWarmIcon"
|
||||
phosphor="ThermometerIcon"
|
||||
remixicon="RiThermometerLine"
|
||||
/>
|
||||
</ItemMedia>
|
||||
<ItemContent className="flex-row items-center gap-3">
|
||||
<ItemTitle className="shrink-0">Color Temp</ItemTitle>
|
||||
</ItemContent>
|
||||
<ItemActions className="flex-1">
|
||||
<Slider
|
||||
value={colorTemp}
|
||||
onValueChange={(value) =>
|
||||
setColorTemp(Array.isArray(value) ? [...value] : [value])
|
||||
}
|
||||
max={100}
|
||||
disabled={!enabled}
|
||||
/>
|
||||
</ItemActions>
|
||||
</Item>
|
||||
<Item size="sm" variant="outline">
|
||||
<ItemMedia variant="icon">
|
||||
<IconPlaceholder
|
||||
lucide="Volume2Icon"
|
||||
tabler="IconVolume"
|
||||
hugeicons="VolumeHighIcon"
|
||||
phosphor="SpeakerHighIcon"
|
||||
remixicon="RiVolumeUpLine"
|
||||
/>
|
||||
</ItemMedia>
|
||||
<ItemContent className="flex-row items-center gap-3">
|
||||
<ItemTitle className="shrink-0">Volume</ItemTitle>
|
||||
</ItemContent>
|
||||
<ItemActions className="flex-1">
|
||||
<Slider
|
||||
value={volume}
|
||||
onValueChange={(value) =>
|
||||
setVolume(Array.isArray(value) ? [...value] : [value])
|
||||
}
|
||||
max={100}
|
||||
disabled={!enabled}
|
||||
/>
|
||||
</ItemActions>
|
||||
</Item>
|
||||
<Item size="sm" variant="outline">
|
||||
<ItemMedia variant="icon">
|
||||
<IconPlaceholder
|
||||
lucide="TimerIcon"
|
||||
tabler="IconClock"
|
||||
hugeicons="Clock03Icon"
|
||||
phosphor="TimerIcon"
|
||||
remixicon="RiTimerLine"
|
||||
/>
|
||||
</ItemMedia>
|
||||
<ItemContent className="flex-row items-center gap-3">
|
||||
<ItemTitle className="shrink-0">Fade</ItemTitle>
|
||||
</ItemContent>
|
||||
<ItemActions className="flex-1">
|
||||
<Slider
|
||||
value={fade}
|
||||
onValueChange={(value) =>
|
||||
setFade(Array.isArray(value) ? [...value] : [value])
|
||||
}
|
||||
max={100}
|
||||
disabled={!enabled}
|
||||
/>
|
||||
</ItemActions>
|
||||
</Item>
|
||||
</ItemGroup>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { Card, CardContent, CardHeader } from "@/registry/bases/base/ui/card"
|
||||
import { Skeleton } from "@/registry/bases/base/ui/skeleton"
|
||||
|
||||
export function LoadingCard() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Skeleton className="h-5 w-32" />
|
||||
<Skeleton className="h-4 w-48" />
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
<Skeleton className="h-32 w-full rounded-lg" />
|
||||
<div className="flex flex-col gap-2">
|
||||
<Skeleton className="h-4 w-full" />
|
||||
<Skeleton className="h-4 w-3/4" />
|
||||
<Skeleton className="h-4 w-1/2" />
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Skeleton className="h-9 flex-1 rounded-md" />
|
||||
<Skeleton className="h-9 flex-1 rounded-md" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
"use client"
|
||||
|
||||
import { Button } from "@/registry/bases/base/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/base/ui/card"
|
||||
import { Field, FieldGroup, FieldLabel } from "@/registry/bases/base/ui/field"
|
||||
import { Input } from "@/registry/bases/base/ui/input"
|
||||
|
||||
export function NewMilestone() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Set a new milestone</CardTitle>
|
||||
<CardDescription>
|
||||
Define your financial target and we'll help you pace your
|
||||
savings.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<FieldGroup>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="goal-name">Goal Name</FieldLabel>
|
||||
<Input
|
||||
id="goal-name"
|
||||
placeholder="e.g. New Car, Home Downpayment"
|
||||
/>
|
||||
</Field>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<Field>
|
||||
<FieldLabel htmlFor="target-amount">Target Amount</FieldLabel>
|
||||
<Input id="target-amount" defaultValue="$15,000" />
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="target-date">Target Date</FieldLabel>
|
||||
<Input id="target-date" defaultValue="Dec 2025" />
|
||||
</Field>
|
||||
</div>
|
||||
</FieldGroup>
|
||||
</CardContent>
|
||||
<CardFooter className="flex-col gap-2">
|
||||
<Button className="w-full">Create Goal</Button>
|
||||
<Button variant="outline" className="w-full">
|
||||
Cancel
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
|
||||
import { Button } from "@/registry/bases/base/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/base/ui/card"
|
||||
import { Checkbox } from "@/registry/bases/base/ui/checkbox"
|
||||
import {
|
||||
Field,
|
||||
FieldContent,
|
||||
FieldDescription,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
} from "@/registry/bases/base/ui/field"
|
||||
|
||||
const NOTIFICATIONS = [
|
||||
{
|
||||
id: "transactions",
|
||||
label: "Transaction alerts",
|
||||
description: "Deposits, withdrawals, and transfers.",
|
||||
defaultChecked: true,
|
||||
},
|
||||
{
|
||||
id: "security",
|
||||
label: "Security alerts",
|
||||
description: "Login attempts and account changes.",
|
||||
defaultChecked: true,
|
||||
},
|
||||
{
|
||||
id: "goals",
|
||||
label: "Goal milestones",
|
||||
description: "Updates at 25%, 50%, 75%, and 100%.",
|
||||
defaultChecked: false,
|
||||
},
|
||||
{
|
||||
id: "market",
|
||||
label: "Market updates",
|
||||
description: "Daily portfolio summary and price alerts.",
|
||||
defaultChecked: false,
|
||||
},
|
||||
]
|
||||
|
||||
export function NotificationSettings() {
|
||||
const [checked, setChecked] = React.useState<Record<string, boolean>>(
|
||||
Object.fromEntries(NOTIFICATIONS.map((n) => [n.id, n.defaultChecked]))
|
||||
)
|
||||
|
||||
const allChecked = NOTIFICATIONS.every((n) => checked[n.id])
|
||||
const someChecked = NOTIFICATIONS.some((n) => checked[n.id]) && !allChecked
|
||||
|
||||
const handleSelectAll = (value: boolean) => {
|
||||
setChecked(Object.fromEntries(NOTIFICATIONS.map((n) => [n.id, value])))
|
||||
}
|
||||
|
||||
const handleToggle = (id: string, value: boolean) => {
|
||||
setChecked((prev) => ({ ...prev, [id]: value }))
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Notifications</CardTitle>
|
||||
<CardDescription>
|
||||
Choose what you want to be notified about.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<FieldGroup>
|
||||
<Field orientation="horizontal">
|
||||
<Checkbox
|
||||
id="notify-all"
|
||||
checked={allChecked}
|
||||
indeterminate={someChecked}
|
||||
onCheckedChange={(v) => handleSelectAll(!!v)}
|
||||
/>
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor="notify-all">Select all</FieldLabel>
|
||||
</FieldContent>
|
||||
</Field>
|
||||
{NOTIFICATIONS.map((n) => (
|
||||
<Field key={n.id} orientation="horizontal">
|
||||
<Checkbox
|
||||
id={`notify-${n.id}`}
|
||||
checked={checked[n.id]}
|
||||
onCheckedChange={(v) => handleToggle(n.id, !!v)}
|
||||
/>
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor={`notify-${n.id}`}>{n.label}</FieldLabel>
|
||||
<FieldDescription>{n.description}</FieldDescription>
|
||||
</FieldContent>
|
||||
</Field>
|
||||
))}
|
||||
</FieldGroup>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button className="w-full">Save Preferences</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
176
apps/v4/registry/bases/base/blocks/preview-02/cards/payments.tsx
Normal file
176
apps/v4/registry/bases/base/blocks/preview-02/cards/payments.tsx
Normal file
@@ -0,0 +1,176 @@
|
||||
"use client"
|
||||
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbList,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
} from "@/registry/bases/base/ui/breadcrumb"
|
||||
import { Button } from "@/registry/bases/base/ui/button"
|
||||
import { Card, CardContent, CardHeader } from "@/registry/bases/base/ui/card"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/registry/bases/base/ui/dropdown-menu"
|
||||
import {
|
||||
Item,
|
||||
ItemContent,
|
||||
ItemDescription,
|
||||
ItemGroup,
|
||||
ItemMedia,
|
||||
ItemTitle,
|
||||
} from "@/registry/bases/base/ui/item"
|
||||
import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"
|
||||
|
||||
export function Payments() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="flex flex-col gap-3">
|
||||
<Breadcrumb>
|
||||
<BreadcrumbList>
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink href="#">Home</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator />
|
||||
<BreadcrumbItem>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
render={<Button size="icon-sm" variant="ghost" />}
|
||||
>
|
||||
<IconPlaceholder
|
||||
lucide="MoreHorizontalIcon"
|
||||
tabler="IconDots"
|
||||
hugeicons="MoreHorizontalCircle01Icon"
|
||||
phosphor="DotsThreeIcon"
|
||||
remixicon="RiMoreLine"
|
||||
/>
|
||||
<span className="sr-only">Account options</span>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start">
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem>Profile</DropdownMenuItem>
|
||||
<DropdownMenuItem>Statements</DropdownMenuItem>
|
||||
<DropdownMenuItem>Documents</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator />
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbPage>Payments</BreadcrumbPage>
|
||||
</BreadcrumbItem>
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ItemGroup>
|
||||
<Item variant="muted" render={<a href="#" />}>
|
||||
<ItemMedia variant="icon">
|
||||
<IconPlaceholder
|
||||
lucide="GaugeIcon"
|
||||
tabler="IconGauge"
|
||||
hugeicons="Settings01Icon"
|
||||
phosphor="GaugeIcon"
|
||||
remixicon="RiDashboardLine"
|
||||
/>
|
||||
</ItemMedia>
|
||||
<ItemContent>
|
||||
<ItemTitle>Change transfer limit</ItemTitle>
|
||||
<ItemDescription>
|
||||
Adjust how much you can send from your balance.
|
||||
</ItemDescription>
|
||||
</ItemContent>
|
||||
<IconPlaceholder
|
||||
lucide="ChevronRightIcon"
|
||||
tabler="IconChevronRight"
|
||||
hugeicons="ArrowRight01Icon"
|
||||
phosphor="CaretRightIcon"
|
||||
remixicon="RiArrowRightSLine"
|
||||
className="size-4 shrink-0 text-muted-foreground"
|
||||
/>
|
||||
</Item>
|
||||
<Item variant="muted" render={<a href="#" />}>
|
||||
<ItemMedia variant="icon">
|
||||
<IconPlaceholder
|
||||
lucide="CalendarIcon"
|
||||
tabler="IconCalendar"
|
||||
hugeicons="Calendar03Icon"
|
||||
phosphor="CalendarIcon"
|
||||
remixicon="RiCalendarLine"
|
||||
/>
|
||||
</ItemMedia>
|
||||
<ItemContent>
|
||||
<ItemTitle>Scheduled transfers</ItemTitle>
|
||||
<ItemDescription>
|
||||
Set up a transfer to send at a later date.
|
||||
</ItemDescription>
|
||||
</ItemContent>
|
||||
<IconPlaceholder
|
||||
lucide="ChevronRightIcon"
|
||||
tabler="IconChevronRight"
|
||||
hugeicons="ArrowRight01Icon"
|
||||
phosphor="CaretRightIcon"
|
||||
remixicon="RiArrowRightSLine"
|
||||
className="size-4 shrink-0 text-muted-foreground"
|
||||
/>
|
||||
</Item>
|
||||
<Item variant="muted" render={<a href="#" />}>
|
||||
<ItemMedia variant="icon">
|
||||
<IconPlaceholder
|
||||
lucide="RepeatIcon"
|
||||
tabler="IconRepeat"
|
||||
hugeicons="RepeatIcon"
|
||||
phosphor="RepeatIcon"
|
||||
remixicon="RiRepeatLine"
|
||||
/>
|
||||
</ItemMedia>
|
||||
<ItemContent>
|
||||
<ItemTitle>Direct Debits</ItemTitle>
|
||||
<ItemDescription>
|
||||
Set up and manage regular payments.
|
||||
</ItemDescription>
|
||||
</ItemContent>
|
||||
<IconPlaceholder
|
||||
lucide="ChevronRightIcon"
|
||||
tabler="IconChevronRight"
|
||||
hugeicons="ArrowRight01Icon"
|
||||
phosphor="CaretRightIcon"
|
||||
remixicon="RiArrowRightSLine"
|
||||
className="size-4 shrink-0 text-muted-foreground"
|
||||
/>
|
||||
</Item>
|
||||
<Item variant="muted" render={<a href="#" />}>
|
||||
<ItemMedia variant="icon">
|
||||
<IconPlaceholder
|
||||
lucide="RefreshCwIcon"
|
||||
tabler="IconRefresh"
|
||||
hugeicons="RepeatIcon"
|
||||
phosphor="ArrowsClockwiseIcon"
|
||||
remixicon="RiRefreshLine"
|
||||
/>
|
||||
</ItemMedia>
|
||||
<ItemContent>
|
||||
<ItemTitle>Recurring card payments</ItemTitle>
|
||||
<ItemDescription>
|
||||
Manage your repeated card transactions.
|
||||
</ItemDescription>
|
||||
</ItemContent>
|
||||
<IconPlaceholder
|
||||
lucide="ChevronRightIcon"
|
||||
tabler="IconChevronRight"
|
||||
hugeicons="ArrowRight01Icon"
|
||||
phosphor="CaretRightIcon"
|
||||
remixicon="RiArrowRightSLine"
|
||||
className="size-4 shrink-0 text-muted-foreground"
|
||||
/>
|
||||
</Item>
|
||||
</ItemGroup>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
|
||||
import { Button } from "@/registry/bases/base/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardAction,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/base/ui/card"
|
||||
import {
|
||||
Field,
|
||||
FieldDescription,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
} from "@/registry/bases/base/ui/field"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/registry/bases/base/ui/select"
|
||||
import { Slider } from "@/registry/bases/base/ui/slider"
|
||||
import { Textarea } from "@/registry/bases/base/ui/textarea"
|
||||
import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"
|
||||
|
||||
const CURRENCIES = [
|
||||
{ label: "USD — United States Dollar", value: "usd" },
|
||||
{ label: "EUR — Euro", value: "eur" },
|
||||
{ label: "GBP — British Pound", value: "gbp" },
|
||||
{ label: "JPY — Japanese Yen", value: "jpy" },
|
||||
]
|
||||
|
||||
export function PayoutThreshold() {
|
||||
const [amount, setAmount] = React.useState([2500])
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Payout Threshold</CardTitle>
|
||||
<CardDescription>
|
||||
Set the minimum balance required before a payout is triggered.
|
||||
</CardDescription>
|
||||
<CardAction>
|
||||
<Button variant="ghost" size="icon-sm" className="bg-muted">
|
||||
<IconPlaceholder
|
||||
lucide="XIcon"
|
||||
tabler="IconX"
|
||||
hugeicons="Cancel01Icon"
|
||||
phosphor="XIcon"
|
||||
remixicon="RiCloseLine"
|
||||
/>
|
||||
</Button>
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<FieldGroup>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="preferred-currency">
|
||||
Preferred Currency
|
||||
</FieldLabel>
|
||||
<Select items={CURRENCIES} defaultValue="usd">
|
||||
<SelectTrigger id="preferred-currency" className="w-full">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
{CURRENCIES.map((item) => (
|
||||
<SelectItem key={item.value} value={item.value}>
|
||||
{item.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
<Field>
|
||||
<div className="flex items-baseline justify-between">
|
||||
<FieldLabel htmlFor="min-payout">
|
||||
Minimum Payout Amount
|
||||
</FieldLabel>
|
||||
<span className="text-2xl font-semibold tabular-nums">
|
||||
${amount[0].toFixed(2)}
|
||||
</span>
|
||||
</div>
|
||||
<Slider
|
||||
id="min-payout"
|
||||
value={amount}
|
||||
onValueChange={(value) =>
|
||||
setAmount(Array.isArray(value) ? [...value] : [value])
|
||||
}
|
||||
min={50}
|
||||
max={10000}
|
||||
step={50}
|
||||
/>
|
||||
<div className="flex items-center justify-between">
|
||||
<FieldDescription>$50 (MIN)</FieldDescription>
|
||||
<FieldDescription>$10,000 (MAX)</FieldDescription>
|
||||
</div>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="payout-notes">Notes</FieldLabel>
|
||||
<Textarea
|
||||
id="payout-notes"
|
||||
placeholder="Add any notes for this payout configuration..."
|
||||
className="min-h-[100px]"
|
||||
/>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button className="w-full">Save Threshold</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
"use client"
|
||||
|
||||
import { Bar, BarChart, XAxis } from "recharts"
|
||||
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/base/ui/card"
|
||||
import {
|
||||
ChartContainer,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
type ChartConfig,
|
||||
} from "@/registry/bases/base/ui/chart"
|
||||
import { Progress } from "@/registry/bases/base/ui/progress"
|
||||
import { Separator } from "@/registry/bases/base/ui/separator"
|
||||
import { useDesignSystemSearchParams } from "@/app/(app)/create/lib/search-params"
|
||||
|
||||
const chartData = [
|
||||
{ hour: "6a", usage: 1.2 },
|
||||
{ hour: "8a", usage: 2.8 },
|
||||
{ hour: "10a", usage: 3.1 },
|
||||
{ hour: "12p", usage: 2.4 },
|
||||
{ hour: "2p", usage: 3.4 },
|
||||
{ hour: "4p", usage: 2.9 },
|
||||
{ hour: "6p", usage: 3.8 },
|
||||
{ hour: "8p", usage: 3.2 },
|
||||
]
|
||||
|
||||
const chartConfig = {
|
||||
usage: {
|
||||
label: "Usage (kW)",
|
||||
color: "var(--chart-2)",
|
||||
},
|
||||
} satisfies ChartConfig
|
||||
|
||||
export function PowerUsage() {
|
||||
const [params] = useDesignSystemSearchParams()
|
||||
const isRounded = !["lyra", "sera"].includes(params.style)
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Power Usage</CardTitle>
|
||||
<CardDescription>Whole Home</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
<ChartContainer config={chartConfig} className="h-[140px] w-full">
|
||||
<BarChart
|
||||
data={chartData}
|
||||
margin={{ left: 0, right: 0, top: 4, bottom: 0 }}
|
||||
>
|
||||
<XAxis
|
||||
dataKey="hour"
|
||||
tickLine={false}
|
||||
tickMargin={6}
|
||||
axisLine={false}
|
||||
className="text-xs"
|
||||
/>
|
||||
<ChartTooltip
|
||||
cursor={false}
|
||||
content={<ChartTooltipContent hideLabel />}
|
||||
/>
|
||||
<Bar
|
||||
dataKey="usage"
|
||||
fill="var(--color-usage)"
|
||||
radius={isRounded ? [4, 4, 0, 0] : 0}
|
||||
/>
|
||||
</BarChart>
|
||||
</ChartContainer>
|
||||
<Separator />
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Currently Using
|
||||
</span>
|
||||
<span className="text-lg font-semibold tabular-nums">3.4 kW</span>
|
||||
</div>
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<span className="text-sm text-muted-foreground">Solar Gen</span>
|
||||
<span className="text-lg font-semibold text-chart-1 tabular-nums">
|
||||
+1.2 kW
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter className="flex-col items-start gap-1">
|
||||
<span className="text-sm text-muted-foreground">Battery Level</span>
|
||||
<div className="flex w-full items-center gap-2">
|
||||
<Progress value={85} className="flex-1" />
|
||||
<span className="text-sm font-medium tabular-nums">85%</span>
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
import { Button } from "@/registry/bases/base/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardAction,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/base/ui/card"
|
||||
import {
|
||||
Field,
|
||||
FieldContent,
|
||||
FieldDescription,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
FieldSeparator,
|
||||
} from "@/registry/bases/base/ui/field"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/registry/bases/base/ui/select"
|
||||
import { Switch } from "@/registry/bases/base/ui/switch"
|
||||
import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"
|
||||
|
||||
const CURRENCIES = [
|
||||
{ label: "USD — United States Dollar", value: "usd" },
|
||||
{ label: "EUR — Euro", value: "eur" },
|
||||
{ label: "GBP — British Pound", value: "gbp" },
|
||||
{ label: "JPY — Japanese Yen", value: "jpy" },
|
||||
]
|
||||
|
||||
export function Preferences() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Preferences</CardTitle>
|
||||
<CardDescription>
|
||||
Manage your account settings and notifications.
|
||||
</CardDescription>
|
||||
<CardAction>
|
||||
<Button variant="ghost" size="icon-sm" className="bg-muted">
|
||||
<IconPlaceholder
|
||||
lucide="XIcon"
|
||||
tabler="IconX"
|
||||
hugeicons="Cancel01Icon"
|
||||
phosphor="XIcon"
|
||||
remixicon="RiCloseLine"
|
||||
/>
|
||||
</Button>
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<FieldGroup>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="default-currency">Default Currency</FieldLabel>
|
||||
<Select items={CURRENCIES} defaultValue="usd">
|
||||
<SelectTrigger id="default-currency" className="w-full">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
{CURRENCIES.map((item) => (
|
||||
<SelectItem key={item.value} value={item.value}>
|
||||
{item.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
<FieldSeparator className="-my-4 style-sera:hidden" />
|
||||
<Field orientation="horizontal">
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor="public-statistics">
|
||||
Public Statistics
|
||||
</FieldLabel>
|
||||
<FieldDescription>
|
||||
Allow others to see your total stream count and listening
|
||||
activity
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<Switch id="public-statistics" defaultChecked />
|
||||
</Field>
|
||||
<FieldSeparator className="-my-4 style-sera:hidden" />
|
||||
<Field orientation="horizontal">
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor="email-notifications">
|
||||
Email Notifications
|
||||
</FieldLabel>
|
||||
<FieldDescription>
|
||||
Monthly royalty reports and distribution updates
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<Switch id="email-notifications" defaultChecked />
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button variant="outline">Reset</Button>
|
||||
<Button className="ml-auto">Save Preferences</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
"use client"
|
||||
|
||||
import QRCode from "react-qr-code"
|
||||
|
||||
import { Button } from "@/registry/bases/base/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/base/ui/card"
|
||||
|
||||
export function QrConnect() {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="flex justify-center pt-6">
|
||||
<div className="rounded-xl border bg-white p-4">
|
||||
<QRCode
|
||||
value="https://ledger.app/connect/jd-4829"
|
||||
size={160}
|
||||
level="M"
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardHeader className="text-center">
|
||||
<CardTitle>Scan to connect your mobile device</CardTitle>
|
||||
<CardDescription>
|
||||
Open the Ledger mobile app and scan this code to link your device.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardFooter>
|
||||
<Button variant="secondary" className="w-full">
|
||||
Got it
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
"use client"
|
||||
|
||||
import { Button } from "@/registry/bases/base/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardAction,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/base/ui/card"
|
||||
import {
|
||||
Field,
|
||||
FieldContent,
|
||||
FieldDescription,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
FieldLegend,
|
||||
FieldSet,
|
||||
FieldTitle,
|
||||
} from "@/registry/bases/base/ui/field"
|
||||
import { Input } from "@/registry/bases/base/ui/input"
|
||||
import {
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
} from "@/registry/bases/base/ui/radio-group"
|
||||
import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"
|
||||
|
||||
export function ReceivingMethod() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardDescription>Payout Preferences</CardDescription>
|
||||
<CardTitle>Receiving Method</CardTitle>
|
||||
<CardAction>
|
||||
<Button variant="ghost" size="icon-sm" className="bg-muted">
|
||||
<IconPlaceholder
|
||||
lucide="XIcon"
|
||||
tabler="IconX"
|
||||
hugeicons="Cancel01Icon"
|
||||
phosphor="XIcon"
|
||||
remixicon="RiCloseLine"
|
||||
/>
|
||||
</Button>
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<FieldGroup>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="account-holder">
|
||||
Account Holder Name
|
||||
</FieldLabel>
|
||||
<Input
|
||||
id="account-holder"
|
||||
defaultValue="Synthetic Horizons Music LLC"
|
||||
/>
|
||||
</Field>
|
||||
<FieldSet>
|
||||
<FieldLegend variant="label">Receiving Method</FieldLegend>
|
||||
<RadioGroup
|
||||
defaultValue="bank"
|
||||
className="grid grid-cols-1 items-start gap-3 md:grid-cols-2 style-sera:grid-cols-1"
|
||||
>
|
||||
<FieldLabel htmlFor="method-bank">
|
||||
<Field orientation="horizontal" className="pb-2.5">
|
||||
<RadioGroupItem value="bank" id="method-bank" />
|
||||
<FieldContent>
|
||||
<FieldTitle>Bank Transfer</FieldTitle>
|
||||
<FieldDescription>SWIFT / IBAN</FieldDescription>
|
||||
</FieldContent>
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
<FieldLabel htmlFor="method-paypal">
|
||||
<Field orientation="horizontal" className="pb-2.5">
|
||||
<RadioGroupItem value="paypal" id="method-paypal" />
|
||||
<FieldContent>
|
||||
<FieldTitle>PayPal</FieldTitle>
|
||||
<FieldDescription className="line-clamp-1">
|
||||
Instant Payout
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
</RadioGroup>
|
||||
</FieldSet>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="iban">IBAN / Account Number</FieldLabel>
|
||||
<Input id="iban" placeholder="DE89 3704 0044 ...." />
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button className="w-full" disabled>
|
||||
Save Payout Settings
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,307 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
|
||||
import { Button } from "@/registry/bases/base/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardAction,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/base/ui/card"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/registry/bases/base/ui/dropdown-menu"
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableRow,
|
||||
} from "@/registry/bases/base/ui/table"
|
||||
import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"
|
||||
|
||||
export function RecentTransactions() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Recent Transactions</CardTitle>
|
||||
<CardDescription>Your latest account activity.</CardDescription>
|
||||
<CardAction>
|
||||
<Button variant="outline" size="sm">
|
||||
View All
|
||||
</Button>
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Table>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell className="w-10">
|
||||
<div className="flex size-10 items-center justify-center rounded-lg bg-muted">
|
||||
<IconPlaceholder
|
||||
className="size-4 shrink-0"
|
||||
lucide="CoffeeIcon"
|
||||
tabler="IconCoffee"
|
||||
hugeicons="CoffeeIcon"
|
||||
phosphor="CoffeeIcon"
|
||||
remixicon="RiCupLine"
|
||||
/>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium">Blue Bottle Coffee</span>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Food & Drink
|
||||
</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-sm text-muted-foreground">
|
||||
Today, 10:24 AM
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<span className="text-sm font-semibold tabular-nums">
|
||||
-$6.50
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell className="w-8">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
render={<Button variant="ghost" size="icon-sm" />}
|
||||
>
|
||||
<IconPlaceholder
|
||||
lucide="MoreHorizontalIcon"
|
||||
tabler="IconDotsVertical"
|
||||
hugeicons="MoreVerticalCircle01Icon"
|
||||
phosphor="DotsThreeIcon"
|
||||
remixicon="RiMore2Line"
|
||||
/>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem>View details</DropdownMenuItem>
|
||||
<DropdownMenuItem>Add note</DropdownMenuItem>
|
||||
<DropdownMenuItem>Categorize</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>Dispute</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell className="w-10">
|
||||
<div className="flex size-10 items-center justify-center rounded-lg bg-muted">
|
||||
<IconPlaceholder
|
||||
className="size-4 shrink-0"
|
||||
lucide="ShoppingCartIcon"
|
||||
tabler="IconShoppingCart"
|
||||
hugeicons="ShoppingCart01Icon"
|
||||
phosphor="ShoppingCartIcon"
|
||||
remixicon="RiShoppingCartLine"
|
||||
/>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium">Whole Foods Market</span>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Groceries
|
||||
</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-sm text-muted-foreground">
|
||||
Yesterday
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<span className="text-sm font-semibold tabular-nums">
|
||||
-$142.30
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell className="w-8">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
render={<Button variant="ghost" size="icon-sm" />}
|
||||
>
|
||||
<IconPlaceholder
|
||||
lucide="MoreHorizontalIcon"
|
||||
tabler="IconDotsVertical"
|
||||
hugeicons="MoreVerticalCircle01Icon"
|
||||
phosphor="DotsThreeIcon"
|
||||
remixicon="RiMore2Line"
|
||||
/>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem>View details</DropdownMenuItem>
|
||||
<DropdownMenuItem>Add note</DropdownMenuItem>
|
||||
<DropdownMenuItem>Categorize</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>Dispute</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell className="w-10">
|
||||
<div className="flex size-10 items-center justify-center rounded-lg bg-muted">
|
||||
<IconPlaceholder
|
||||
className="size-4 shrink-0"
|
||||
lucide="WalletIcon"
|
||||
tabler="IconWallet"
|
||||
hugeicons="Wallet01Icon"
|
||||
phosphor="WalletIcon"
|
||||
remixicon="RiWalletLine"
|
||||
/>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium">Stripe Payout</span>
|
||||
<span className="text-sm text-muted-foreground">Income</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-sm text-muted-foreground">
|
||||
Oct 12
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<span className="text-sm font-semibold text-emerald-500 tabular-nums">
|
||||
+$4,200.00
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell className="w-8">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
render={<Button variant="ghost" size="icon-sm" />}
|
||||
>
|
||||
<IconPlaceholder
|
||||
lucide="MoreHorizontalIcon"
|
||||
tabler="IconDotsVertical"
|
||||
hugeicons="MoreVerticalCircle01Icon"
|
||||
phosphor="DotsThreeIcon"
|
||||
remixicon="RiMore2Line"
|
||||
/>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem>View details</DropdownMenuItem>
|
||||
<DropdownMenuItem>Add note</DropdownMenuItem>
|
||||
<DropdownMenuItem>Categorize</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>Dispute</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell className="w-10">
|
||||
<div className="flex size-10 items-center justify-center rounded-lg bg-muted">
|
||||
<IconPlaceholder
|
||||
className="size-4 shrink-0"
|
||||
lucide="CarIcon"
|
||||
tabler="IconCar"
|
||||
hugeicons="Car01Icon"
|
||||
phosphor="CarIcon"
|
||||
remixicon="RiCarLine"
|
||||
/>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium">Uber Technologies</span>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Transport
|
||||
</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-sm text-muted-foreground">
|
||||
Oct 11
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<span className="text-sm font-semibold tabular-nums">
|
||||
-$24.10
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell className="w-8">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
render={<Button variant="ghost" size="icon-sm" />}
|
||||
>
|
||||
<IconPlaceholder
|
||||
lucide="MoreHorizontalIcon"
|
||||
tabler="IconDotsVertical"
|
||||
hugeicons="MoreVerticalCircle01Icon"
|
||||
phosphor="DotsThreeIcon"
|
||||
remixicon="RiMore2Line"
|
||||
/>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem>View details</DropdownMenuItem>
|
||||
<DropdownMenuItem>Add note</DropdownMenuItem>
|
||||
<DropdownMenuItem>Categorize</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>Dispute</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell className="w-10">
|
||||
<div className="flex size-10 items-center justify-center rounded-lg bg-muted">
|
||||
<IconPlaceholder
|
||||
className="size-4 shrink-0"
|
||||
lucide="TvIcon"
|
||||
tabler="IconDeviceTv"
|
||||
hugeicons="Tv01Icon"
|
||||
phosphor="TelevisionIcon"
|
||||
remixicon="RiTvLine"
|
||||
/>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium">Netflix Subscription</span>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Entertainment
|
||||
</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-sm text-muted-foreground">
|
||||
Oct 10
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<span className="text-sm font-semibold tabular-nums">
|
||||
-$19.99
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell className="w-8">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
render={<Button variant="ghost" size="icon-sm" />}
|
||||
>
|
||||
<IconPlaceholder
|
||||
lucide="MoreHorizontalIcon"
|
||||
tabler="IconDotsVertical"
|
||||
hugeicons="MoreVerticalCircle01Icon"
|
||||
phosphor="DotsThreeIcon"
|
||||
remixicon="RiMore2Line"
|
||||
/>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem>View details</DropdownMenuItem>
|
||||
<DropdownMenuItem>Add note</DropdownMenuItem>
|
||||
<DropdownMenuItem>Categorize</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>Dispute</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
"use client"
|
||||
|
||||
import { Badge } from "@/registry/bases/base/ui/badge"
|
||||
import { Card, CardContent, CardHeader } from "@/registry/bases/base/ui/card"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupInput,
|
||||
} from "@/registry/bases/base/ui/input-group"
|
||||
import {
|
||||
Item,
|
||||
ItemContent,
|
||||
ItemDescription,
|
||||
ItemGroup,
|
||||
ItemMedia,
|
||||
ItemTitle,
|
||||
} from "@/registry/bases/base/ui/item"
|
||||
import {
|
||||
ToggleGroup,
|
||||
ToggleGroupItem,
|
||||
} from "@/registry/bases/base/ui/toggle-group"
|
||||
import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"
|
||||
|
||||
const HOLDINGS = [
|
||||
{
|
||||
ticker: "VOO",
|
||||
name: "Vanguard S&P 500 ETF",
|
||||
type: "ETF",
|
||||
added: "Jan 2021",
|
||||
shares: "112",
|
||||
value: "$48,230.40",
|
||||
},
|
||||
{
|
||||
ticker: "VIG",
|
||||
name: "Vanguard Dividend Appreciation",
|
||||
type: "ETF",
|
||||
added: "Mar 2022",
|
||||
shares: "450",
|
||||
value: "$26,033.79",
|
||||
},
|
||||
{
|
||||
ticker: "AAPL",
|
||||
name: "Apple Inc.",
|
||||
type: "Stock",
|
||||
added: "Nov 2020",
|
||||
shares: "85",
|
||||
value: "$18,488.90",
|
||||
},
|
||||
{
|
||||
ticker: "O",
|
||||
name: "Realty Income Corp",
|
||||
type: "REIT",
|
||||
added: "Jun 2023",
|
||||
shares: "320",
|
||||
value: "$15,136.59",
|
||||
},
|
||||
]
|
||||
|
||||
export function ReleaseCatalog() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<InputGroup className="max-w-sm">
|
||||
<InputGroupAddon>
|
||||
<IconPlaceholder
|
||||
lucide="SearchIcon"
|
||||
tabler="IconSearch"
|
||||
hugeicons="Search01Icon"
|
||||
phosphor="MagnifyingGlassIcon"
|
||||
remixicon="RiSearchLine"
|
||||
/>
|
||||
</InputGroupAddon>
|
||||
<InputGroupInput placeholder="Search holdings or tickers..." />
|
||||
</InputGroup>
|
||||
<ToggleGroup defaultValue={["etfs"]} variant="outline" spacing={1}>
|
||||
<ToggleGroupItem value="stocks">Stocks</ToggleGroupItem>
|
||||
<ToggleGroupItem value="etfs">ETFs</ToggleGroupItem>
|
||||
<ToggleGroupItem value="reits">REITs</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ItemGroup>
|
||||
{HOLDINGS.map((holding) => (
|
||||
<Item key={holding.ticker} variant="muted">
|
||||
<ItemMedia>
|
||||
<div className="flex size-12 items-center justify-center rounded-lg border text-sm font-semibold">
|
||||
{holding.ticker}
|
||||
</div>
|
||||
</ItemMedia>
|
||||
<ItemContent>
|
||||
<ItemTitle>{holding.name}</ItemTitle>
|
||||
<ItemDescription className="text-xs tracking-wider uppercase">
|
||||
{holding.shares} Shares · {holding.added}
|
||||
</ItemDescription>
|
||||
</ItemContent>
|
||||
<div className="flex shrink-0 items-center gap-6">
|
||||
<Badge variant="outline">{holding.type}</Badge>
|
||||
<div className="flex flex-col items-end gap-0.5">
|
||||
<span className="text-xs tracking-wider text-muted-foreground uppercase">
|
||||
Value
|
||||
</span>
|
||||
<span className="font-medium tabular-nums">
|
||||
{holding.value}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</Item>
|
||||
))}
|
||||
</ItemGroup>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/base/ui/card"
|
||||
import { Slider } from "@/registry/bases/base/ui/slider"
|
||||
import {
|
||||
ToggleGroup,
|
||||
ToggleGroupItem,
|
||||
} from "@/registry/bases/base/ui/toggle-group"
|
||||
|
||||
export function RollerShades() {
|
||||
const [position, setPosition] = React.useState([50])
|
||||
|
||||
const preset =
|
||||
position[0] <= 10 ? "open" : position[0] >= 90 ? "closed" : "half"
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Living Room</CardTitle>
|
||||
<CardDescription>Roller Shades</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
<div className="flex h-32 flex-col overflow-hidden rounded-lg border bg-muted">
|
||||
<div
|
||||
className="bg-muted-foreground transition-all duration-300"
|
||||
style={{ height: `${position[0]}%` }}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-xs font-medium tracking-wider text-muted-foreground uppercase">
|
||||
Open
|
||||
</span>
|
||||
<Slider
|
||||
value={position}
|
||||
onValueChange={(value) =>
|
||||
setPosition(Array.isArray(value) ? [...value] : [value])
|
||||
}
|
||||
max={100}
|
||||
className="flex-1"
|
||||
/>
|
||||
<span className="text-xs font-medium tracking-wider text-muted-foreground uppercase">
|
||||
Close
|
||||
</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<ToggleGroup
|
||||
value={[preset]}
|
||||
onValueChange={(value) => {
|
||||
const v = value[0]
|
||||
if (v === "open") setPosition([0])
|
||||
if (v === "half") setPosition([50])
|
||||
if (v === "closed") setPosition([100])
|
||||
}}
|
||||
variant="outline"
|
||||
spacing={1}
|
||||
className="w-full"
|
||||
>
|
||||
<ToggleGroupItem value="open" className="flex-1">
|
||||
Open
|
||||
</ToggleGroupItem>
|
||||
<ToggleGroupItem value="half" className="flex-1">
|
||||
Half
|
||||
</ToggleGroupItem>
|
||||
<ToggleGroupItem value="closed" className="flex-1">
|
||||
Closed
|
||||
</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
"use client"
|
||||
|
||||
import { Label, Pie, PieChart } from "recharts"
|
||||
|
||||
import { Card, CardContent, CardFooter } from "@/registry/bases/base/ui/card"
|
||||
import {
|
||||
ChartContainer,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
type ChartConfig,
|
||||
} from "@/registry/bases/base/ui/chart"
|
||||
import { Separator } from "@/registry/bases/base/ui/separator"
|
||||
|
||||
const chartData = [
|
||||
{ name: "saved", value: 24000, fill: "var(--color-saved)" },
|
||||
{ name: "remaining", value: 6000, fill: "var(--color-remaining)" },
|
||||
]
|
||||
|
||||
const chartConfig = {
|
||||
saved: {
|
||||
label: "Saved",
|
||||
color: "var(--chart-2)",
|
||||
},
|
||||
remaining: {
|
||||
label: "Remaining",
|
||||
color: "var(--chart-1)",
|
||||
},
|
||||
} satisfies ChartConfig
|
||||
|
||||
export function SavingsProgress() {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent>
|
||||
<ChartContainer
|
||||
config={chartConfig}
|
||||
className="mx-auto aspect-square max-h-[220px]"
|
||||
>
|
||||
<PieChart>
|
||||
<ChartTooltip
|
||||
cursor={false}
|
||||
content={<ChartTooltipContent hideLabel />}
|
||||
/>
|
||||
<Pie
|
||||
data={chartData}
|
||||
dataKey="value"
|
||||
nameKey="name"
|
||||
innerRadius={70}
|
||||
outerRadius={95}
|
||||
strokeWidth={0}
|
||||
startAngle={90}
|
||||
endAngle={-270}
|
||||
>
|
||||
<Label
|
||||
content={({ viewBox }) => {
|
||||
if (viewBox && "cx" in viewBox && "cy" in viewBox) {
|
||||
return (
|
||||
<text
|
||||
x={viewBox.cx}
|
||||
y={viewBox.cy}
|
||||
textAnchor="middle"
|
||||
dominantBaseline="middle"
|
||||
>
|
||||
<tspan
|
||||
x={viewBox.cx}
|
||||
y={(viewBox.cy || 0) - 12}
|
||||
className="fill-foreground text-2xl font-bold"
|
||||
>
|
||||
$24,000
|
||||
</tspan>
|
||||
<tspan
|
||||
x={viewBox.cx}
|
||||
y={(viewBox.cy || 0) + 12}
|
||||
className="fill-muted-foreground text-xs"
|
||||
>
|
||||
80% of $30,000
|
||||
</tspan>
|
||||
</text>
|
||||
)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Pie>
|
||||
</PieChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
<CardFooter className="flex-col gap-0">
|
||||
<div className="flex w-full items-center justify-between py-3">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Projected Finish
|
||||
</span>
|
||||
<span className="text-sm font-semibold">October 2024</span>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="flex w-full items-center justify-between py-3">
|
||||
<span className="text-sm text-muted-foreground">Monthly Average</span>
|
||||
<span className="text-sm font-semibold tabular-nums">$1,250</span>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="flex w-full items-center justify-between py-3">
|
||||
<span className="text-sm text-muted-foreground">Top Contributor</span>
|
||||
<span className="text-sm font-semibold">Auto-Transfer</span>
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
import { Button } from "@/registry/bases/base/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardAction,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/base/ui/card"
|
||||
import {
|
||||
Field,
|
||||
FieldDescription,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
} from "@/registry/bases/base/ui/field"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupInput,
|
||||
InputGroupText,
|
||||
} from "@/registry/bases/base/ui/input-group"
|
||||
import {
|
||||
Item,
|
||||
ItemContent,
|
||||
ItemDescription,
|
||||
ItemFooter,
|
||||
ItemGroup,
|
||||
} from "@/registry/bases/base/ui/item"
|
||||
import {
|
||||
NativeSelect,
|
||||
NativeSelectOption,
|
||||
} from "@/registry/bases/base/ui/native-select"
|
||||
import { Progress } from "@/registry/bases/base/ui/progress"
|
||||
|
||||
export function SavingsTargets() {
|
||||
return (
|
||||
<div className="grid grid-cols-2 gap-(--gap)">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Savings Targets</CardTitle>
|
||||
<CardDescription>Active milestones for 2024</CardDescription>
|
||||
<CardAction>
|
||||
<Button variant="outline" size="sm">
|
||||
New Goal
|
||||
</Button>
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ItemGroup className="gap-3">
|
||||
<Item variant="muted" className="flex-col items-stretch">
|
||||
<ItemContent className="gap-3">
|
||||
<ItemDescription className="cn-font-heading text-xs font-medium tracking-wider text-muted-foreground uppercase">
|
||||
Retirement
|
||||
</ItemDescription>
|
||||
<span className="text-3xl font-semibold tabular-nums">
|
||||
$420,000
|
||||
</span>
|
||||
<Progress value={65} />
|
||||
</ItemContent>
|
||||
<ItemFooter>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
65% achieved
|
||||
</span>
|
||||
<span className="text-sm font-medium tabular-nums">
|
||||
$273,000
|
||||
</span>
|
||||
</ItemFooter>
|
||||
</Item>
|
||||
<Item variant="muted" className="flex-col items-stretch">
|
||||
<ItemContent className="gap-3">
|
||||
<ItemDescription className="cn-font-heading text-xs font-medium tracking-wider text-muted-foreground uppercase">
|
||||
Real Estate
|
||||
</ItemDescription>
|
||||
<span className="text-3xl font-semibold tabular-nums">
|
||||
$85,000
|
||||
</span>
|
||||
<Progress value={32} />
|
||||
</ItemContent>
|
||||
<ItemFooter>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
32% achieved
|
||||
</span>
|
||||
<span className="text-sm font-medium tabular-nums">
|
||||
$27,200
|
||||
</span>
|
||||
</ItemFooter>
|
||||
</Item>
|
||||
</ItemGroup>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<CardDescription className="text-center">
|
||||
You have not met your targets for this year.
|
||||
</CardDescription>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Buy Investment</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-1 flex-col gap-3">
|
||||
<FieldGroup className="flex-1">
|
||||
<Field>
|
||||
<FieldLabel htmlFor="invest-amount">Amount to Invest</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupAddon>
|
||||
<InputGroupText>$</InputGroupText>
|
||||
</InputGroupAddon>
|
||||
<InputGroupInput id="invest-amount" defaultValue="1,000.00" />
|
||||
</InputGroup>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="invest-type">Order Type</FieldLabel>
|
||||
<NativeSelect id="invest-type" defaultValue="market">
|
||||
<NativeSelectOption value="market">
|
||||
Market Order
|
||||
</NativeSelectOption>
|
||||
<NativeSelectOption value="limit">
|
||||
Limit Order
|
||||
</NativeSelectOption>
|
||||
<NativeSelectOption value="stop">Stop Order</NativeSelectOption>
|
||||
</NativeSelect>
|
||||
<FieldDescription>
|
||||
Market orders execute at the current price.
|
||||
</FieldDescription>
|
||||
</Field>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Estimated Shares
|
||||
</span>
|
||||
<span className="text-sm font-semibold tabular-nums">1.95</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Buying Power
|
||||
</span>
|
||||
<span className="text-sm font-semibold tabular-nums">
|
||||
$12,450.00
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</FieldGroup>
|
||||
</CardContent>
|
||||
<CardFooter className="flex-col gap-3">
|
||||
<Button className="w-full">Review Order</Button>
|
||||
<CardDescription className="text-center">
|
||||
Trades are typically executed within minutes during market hours.
|
||||
</CardDescription>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,285 @@
|
||||
"use client"
|
||||
|
||||
import { Card } from "@/registry/bases/base/ui/card"
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
SidebarGroup,
|
||||
SidebarGroupContent,
|
||||
SidebarGroupLabel,
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
SidebarProvider,
|
||||
SidebarSeparator,
|
||||
} from "@/registry/bases/base/ui/sidebar"
|
||||
import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"
|
||||
|
||||
export function SidebarNav() {
|
||||
return (
|
||||
<div className="grid grid-cols-2 items-start gap-6">
|
||||
<Card className="overflow-hidden py-0">
|
||||
<SidebarProvider className="min-h-0">
|
||||
<Sidebar collapsible="none" className="w-full bg-transparent">
|
||||
<SidebarContent className="gap-0">
|
||||
<SidebarGroup className="pb-1">
|
||||
<SidebarGroupLabel>Overview</SidebarGroupLabel>
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton isActive>
|
||||
<IconPlaceholder
|
||||
lucide="LayoutDashboardIcon"
|
||||
tabler="IconLayoutDashboard"
|
||||
hugeicons="DashboardSquare01Icon"
|
||||
phosphor="SquaresFourIcon"
|
||||
remixicon="RiDashboardLine"
|
||||
/>
|
||||
Dashboard
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton>
|
||||
<IconPlaceholder
|
||||
lucide="ArrowLeftRightIcon"
|
||||
tabler="IconArrowsLeftRight"
|
||||
hugeicons="ArrowDataTransferHorizontalIcon"
|
||||
phosphor="ArrowsLeftRightIcon"
|
||||
remixicon="RiArrowLeftRightLine"
|
||||
/>
|
||||
Transactions
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton>
|
||||
<IconPlaceholder
|
||||
lucide="TrendingUpIcon"
|
||||
tabler="IconTrendingUp"
|
||||
hugeicons="AnalyticsUpIcon"
|
||||
phosphor="TrendUpIcon"
|
||||
remixicon="RiLineChartLine"
|
||||
/>
|
||||
Investments
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton>
|
||||
<IconPlaceholder
|
||||
lucide="Building2Icon"
|
||||
tabler="IconBuildingBank"
|
||||
hugeicons="BankIcon"
|
||||
phosphor="BankIcon"
|
||||
remixicon="RiBankLine"
|
||||
/>
|
||||
Accounts
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton>
|
||||
<IconPlaceholder
|
||||
lucide="PieChartIcon"
|
||||
tabler="IconChartPie"
|
||||
hugeicons="PieChartIcon"
|
||||
phosphor="ChartPieIcon"
|
||||
remixicon="RiPieChartLine"
|
||||
/>
|
||||
Spending
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
<SidebarSeparator className="w-auto!" />
|
||||
<SidebarGroup className="pt-1">
|
||||
<SidebarGroupLabel>Planning</SidebarGroupLabel>
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton>
|
||||
<IconPlaceholder
|
||||
lucide="TargetIcon"
|
||||
tabler="IconTarget"
|
||||
hugeicons="Target02Icon"
|
||||
phosphor="TargetIcon"
|
||||
remixicon="RiFocus3Line"
|
||||
/>
|
||||
Goals
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton>
|
||||
<IconPlaceholder
|
||||
lucide="WalletIcon"
|
||||
tabler="IconWallet"
|
||||
hugeicons="Wallet01Icon"
|
||||
phosphor="WalletIcon"
|
||||
remixicon="RiWalletLine"
|
||||
/>
|
||||
Budget
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton>
|
||||
<IconPlaceholder
|
||||
lucide="FileBarChartIcon"
|
||||
tabler="IconReportAnalytics"
|
||||
hugeicons="ChartBarLineIcon"
|
||||
phosphor="ChartBarIcon"
|
||||
remixicon="RiBarChartLine"
|
||||
/>
|
||||
Reports
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton>
|
||||
<IconPlaceholder
|
||||
lucide="FileTextIcon"
|
||||
tabler="IconFileText"
|
||||
hugeicons="File02Icon"
|
||||
phosphor="FileTextIcon"
|
||||
remixicon="RiFileTextLine"
|
||||
/>
|
||||
Documents
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
</SidebarContent>
|
||||
</Sidebar>
|
||||
</SidebarProvider>
|
||||
</Card>
|
||||
<Card className="overflow-hidden py-0">
|
||||
<SidebarProvider className="min-h-0">
|
||||
<Sidebar collapsible="none" className="w-full bg-transparent">
|
||||
<SidebarContent className="gap-0">
|
||||
<SidebarGroup className="pb-1">
|
||||
<SidebarGroupLabel>Account</SidebarGroupLabel>
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton>
|
||||
<IconPlaceholder
|
||||
lucide="UserIcon"
|
||||
tabler="IconUser"
|
||||
hugeicons="UserIcon"
|
||||
phosphor="UserIcon"
|
||||
remixicon="RiUserLine"
|
||||
/>
|
||||
Profile
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton isActive>
|
||||
<IconPlaceholder
|
||||
lucide="CreditCardIcon"
|
||||
tabler="IconCreditCard"
|
||||
hugeicons="CreditCardIcon"
|
||||
phosphor="CreditCardIcon"
|
||||
remixicon="RiBankCardLine"
|
||||
/>
|
||||
Billing
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton>
|
||||
<IconPlaceholder
|
||||
lucide="BellIcon"
|
||||
tabler="IconBell"
|
||||
hugeicons="Notification03Icon"
|
||||
phosphor="BellIcon"
|
||||
remixicon="RiBellLine"
|
||||
/>
|
||||
Notifications
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton>
|
||||
<IconPlaceholder
|
||||
lucide="ShieldIcon"
|
||||
tabler="IconShield"
|
||||
hugeicons="ShieldIcon"
|
||||
phosphor="ShieldIcon"
|
||||
remixicon="RiShieldLine"
|
||||
/>
|
||||
Security
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton>
|
||||
<IconPlaceholder
|
||||
lucide="PaintbrushIcon"
|
||||
tabler="IconPalette"
|
||||
hugeicons="PaintBoardIcon"
|
||||
phosphor="PaletteIcon"
|
||||
remixicon="RiPaletteLine"
|
||||
/>
|
||||
Appearance
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
<SidebarSeparator className="w-auto!" />
|
||||
<SidebarGroup className="pt-1">
|
||||
<SidebarGroupLabel>Support</SidebarGroupLabel>
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton>
|
||||
<IconPlaceholder
|
||||
lucide="CircleHelpIcon"
|
||||
tabler="IconHelp"
|
||||
hugeicons="HelpCircleIcon"
|
||||
phosphor="QuestionIcon"
|
||||
remixicon="RiQuestionLine"
|
||||
/>
|
||||
Help Center
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton>
|
||||
<IconPlaceholder
|
||||
lucide="MessageSquareIcon"
|
||||
tabler="IconMessage"
|
||||
hugeicons="Message01Icon"
|
||||
phosphor="ChatIcon"
|
||||
remixicon="RiChat1Line"
|
||||
/>
|
||||
Contact Us
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton>
|
||||
<IconPlaceholder
|
||||
lucide="BookOpenIcon"
|
||||
tabler="IconBook"
|
||||
hugeicons="BookOpen02Icon"
|
||||
phosphor="BookOpenIcon"
|
||||
remixicon="RiBookOpenLine"
|
||||
/>
|
||||
Documentation
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton>
|
||||
<IconPlaceholder
|
||||
lucide="ActivityIcon"
|
||||
tabler="IconActivity"
|
||||
hugeicons="ActivityIcon"
|
||||
phosphor="ActivityIcon"
|
||||
remixicon="RiPulseLine"
|
||||
/>
|
||||
Status
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
</SidebarContent>
|
||||
</Sidebar>
|
||||
</SidebarProvider>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
import { Button } from "@/registry/bases/base/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/base/ui/card"
|
||||
import { Field, FieldGroup, FieldLabel } from "@/registry/bases/base/ui/field"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupInput,
|
||||
} from "@/registry/bases/base/ui/input-group"
|
||||
import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"
|
||||
|
||||
export function SocialLinks() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Social Links</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<FieldGroup>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="spotify-url">Spotify Artist URL</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupAddon>
|
||||
<IconPlaceholder
|
||||
lucide="CirclePlusIcon"
|
||||
tabler="IconCirclePlus"
|
||||
hugeicons="PlusSignCircleIcon"
|
||||
phosphor="PlusCircleIcon"
|
||||
remixicon="RiAddCircleLine"
|
||||
/>
|
||||
</InputGroupAddon>
|
||||
<InputGroupInput
|
||||
id="spotify-url"
|
||||
defaultValue="spotify.com/artist/3j...2k"
|
||||
/>
|
||||
</InputGroup>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="instagram-handle">Instagram Handle</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupAddon>
|
||||
<IconPlaceholder
|
||||
lucide="CameraIcon"
|
||||
tabler="IconCamera"
|
||||
hugeicons="Camera01Icon"
|
||||
phosphor="CameraIcon"
|
||||
remixicon="RiCameraLine"
|
||||
/>
|
||||
</InputGroupAddon>
|
||||
<InputGroupInput
|
||||
id="instagram-handle"
|
||||
defaultValue="@julianduryea_music"
|
||||
/>
|
||||
</InputGroup>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="soundcloud-url">SoundCloud URL</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupAddon>
|
||||
<IconPlaceholder
|
||||
lucide="CloudIcon"
|
||||
tabler="IconCloud"
|
||||
hugeicons="CloudUploadIcon"
|
||||
phosphor="CloudIcon"
|
||||
remixicon="RiCloudLine"
|
||||
/>
|
||||
</InputGroupAddon>
|
||||
<InputGroupInput
|
||||
id="soundcloud-url"
|
||||
placeholder="soundcloud.com/username"
|
||||
/>
|
||||
</InputGroup>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="website-url">Website</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupAddon>
|
||||
<IconPlaceholder
|
||||
lucide="GlobeIcon"
|
||||
tabler="IconWorld"
|
||||
hugeicons="Globe02Icon"
|
||||
phosphor="GlobeIcon"
|
||||
remixicon="RiGlobalLine"
|
||||
/>
|
||||
</InputGroupAddon>
|
||||
<InputGroupInput
|
||||
id="website-url"
|
||||
placeholder="https://yoursite.com"
|
||||
/>
|
||||
</InputGroup>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</CardContent>
|
||||
<CardFooter className="justify-end gap-2 style-sera:justify-center">
|
||||
<Button variant="secondary" className="style-sera:flex-1">
|
||||
Discard
|
||||
</Button>
|
||||
<Button className="style-sera:flex-1">Save Changes</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Area, AreaChart, CartesianGrid, XAxis } from "recharts"
|
||||
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/base/ui/card"
|
||||
import {
|
||||
ChartContainer,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
type ChartConfig,
|
||||
} from "@/registry/bases/base/ui/chart"
|
||||
import {
|
||||
Combobox,
|
||||
ComboboxContent,
|
||||
ComboboxEmpty,
|
||||
ComboboxInput,
|
||||
ComboboxItem,
|
||||
ComboboxList,
|
||||
} from "@/registry/bases/base/ui/combobox"
|
||||
import { Field, FieldGroup, FieldLabel } from "@/registry/bases/base/ui/field"
|
||||
import { Separator } from "@/registry/bases/base/ui/separator"
|
||||
|
||||
const TICKERS = ["VOO", "VIG", "AAPL", "MSFT", "GOOGL", "AMZN", "TSLA"]
|
||||
|
||||
const CHART_DATA: Record<string, { month: string; price: number }[]> = {
|
||||
VOO: [
|
||||
{ month: "Jan", price: 412 },
|
||||
{ month: "Feb", price: 438 },
|
||||
{ month: "Mar", price: 395 },
|
||||
{ month: "Apr", price: 450 },
|
||||
{ month: "May", price: 420 },
|
||||
{ month: "Jun", price: 462 },
|
||||
],
|
||||
AAPL: [
|
||||
{ month: "Jan", price: 185 },
|
||||
{ month: "Feb", price: 210 },
|
||||
{ month: "Mar", price: 172 },
|
||||
{ month: "Apr", price: 198 },
|
||||
{ month: "May", price: 178 },
|
||||
{ month: "Jun", price: 215 },
|
||||
],
|
||||
}
|
||||
|
||||
const DEFAULT_DATA = [
|
||||
{ month: "Jan", price: 100 },
|
||||
{ month: "Feb", price: 118 },
|
||||
{ month: "Mar", price: 95 },
|
||||
{ month: "Apr", price: 125 },
|
||||
{ month: "May", price: 108 },
|
||||
{ month: "Jun", price: 130 },
|
||||
]
|
||||
|
||||
const chartConfig = {
|
||||
price: {
|
||||
label: "Price",
|
||||
color: "var(--chart-1)",
|
||||
},
|
||||
} satisfies ChartConfig
|
||||
|
||||
export function StockPerformance() {
|
||||
const [ticker, setTicker] = React.useState("VOO")
|
||||
|
||||
const data = CHART_DATA[ticker] ?? DEFAULT_DATA
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Stock Performance</CardTitle>
|
||||
<CardDescription>6-month price history.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
<FieldGroup>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="ticker-select">Ticker</FieldLabel>
|
||||
<Combobox
|
||||
items={TICKERS}
|
||||
value={ticker}
|
||||
onValueChange={(value) => {
|
||||
if (value !== null) setTicker(value)
|
||||
}}
|
||||
>
|
||||
<ComboboxInput
|
||||
id="ticker-select"
|
||||
placeholder="Search ticker..."
|
||||
/>
|
||||
<ComboboxContent>
|
||||
<ComboboxEmpty>No tickers found.</ComboboxEmpty>
|
||||
<ComboboxList>
|
||||
{(item) => (
|
||||
<ComboboxItem key={item} value={item}>
|
||||
{item}
|
||||
</ComboboxItem>
|
||||
)}
|
||||
</ComboboxList>
|
||||
</ComboboxContent>
|
||||
</Combobox>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
<Separator className="style-sera:hidden" />
|
||||
<ChartContainer config={chartConfig} className="h-[200px] w-full">
|
||||
<AreaChart
|
||||
accessibilityLayer
|
||||
data={data}
|
||||
margin={{ left: 0, right: 0, top: 8, bottom: 0 }}
|
||||
>
|
||||
<defs>
|
||||
<linearGradient id="fillPrice" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop
|
||||
offset="0%"
|
||||
stopColor="var(--color-price)"
|
||||
stopOpacity={0.3}
|
||||
/>
|
||||
<stop
|
||||
offset="100%"
|
||||
stopColor="var(--color-price)"
|
||||
stopOpacity={0.05}
|
||||
/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<CartesianGrid vertical={false} strokeDasharray="3 3" />
|
||||
<ChartTooltip
|
||||
cursor={false}
|
||||
content={<ChartTooltipContent hideLabel />}
|
||||
/>
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="price"
|
||||
stroke="var(--color-price)"
|
||||
strokeWidth={2}
|
||||
fill="url(#fillPrice)"
|
||||
/>
|
||||
</AreaChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { Button } from "@/registry/bases/base/ui/button"
|
||||
import { Card, CardContent } from "@/registry/bases/base/ui/card"
|
||||
import {
|
||||
Empty,
|
||||
EmptyContent,
|
||||
EmptyDescription,
|
||||
EmptyHeader,
|
||||
EmptyMedia,
|
||||
EmptyTitle,
|
||||
} from "@/registry/bases/base/ui/empty"
|
||||
import { Spinner } from "@/registry/bases/base/ui/spinner"
|
||||
|
||||
export function SyncingState() {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="p-0">
|
||||
<Empty className="p-4">
|
||||
<EmptyHeader>
|
||||
<EmptyMedia variant="icon">
|
||||
<Spinner />
|
||||
</EmptyMedia>
|
||||
<EmptyTitle>Syncing your accounts</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
We're pulling in your latest transactions. This usually takes
|
||||
a few seconds.
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
<EmptyContent>
|
||||
<Button variant="outline">Cancel</Button>
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
import { Button } from "@/registry/bases/base/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardAction,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/base/ui/card"
|
||||
import { Field, FieldGroup, FieldLabel } from "@/registry/bases/base/ui/field"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupInput,
|
||||
InputGroupText,
|
||||
} from "@/registry/bases/base/ui/input-group"
|
||||
import { Item, ItemContent } from "@/registry/bases/base/ui/item"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/registry/bases/base/ui/select"
|
||||
import { Separator } from "@/registry/bases/base/ui/separator"
|
||||
import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"
|
||||
|
||||
const FROM_ACCOUNTS = [
|
||||
{ label: "Main Checking (··8402) — $12,450.00", value: "checking" },
|
||||
{ label: "Business (··7731) — $8,920.00", value: "business" },
|
||||
]
|
||||
|
||||
const TO_ACCOUNTS = [
|
||||
{ label: "High Yield Savings (··1192) — $42,100.00", value: "savings" },
|
||||
{ label: "Investment (··3349) — $18,200.00", value: "investment" },
|
||||
]
|
||||
|
||||
export function TransferFunds() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Transfer Funds</CardTitle>
|
||||
<CardDescription>
|
||||
Move money between your connected accounts.
|
||||
</CardDescription>
|
||||
<CardAction>
|
||||
<Button variant="ghost" size="icon-sm" className="bg-muted">
|
||||
<IconPlaceholder
|
||||
lucide="XIcon"
|
||||
tabler="IconX"
|
||||
hugeicons="Cancel01Icon"
|
||||
phosphor="XIcon"
|
||||
remixicon="RiCloseLine"
|
||||
/>
|
||||
</Button>
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<FieldGroup>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="transfer-amount">
|
||||
Amount to Transfer
|
||||
</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupAddon>
|
||||
<InputGroupText>$</InputGroupText>
|
||||
</InputGroupAddon>
|
||||
<InputGroupInput id="transfer-amount" defaultValue="1,200.00" />
|
||||
</InputGroup>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="from-account">From Account</FieldLabel>
|
||||
<Select items={FROM_ACCOUNTS} defaultValue="checking">
|
||||
<SelectTrigger id="from-account" className="w-full">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
{FROM_ACCOUNTS.map((item) => (
|
||||
<SelectItem key={item.value} value={item.value}>
|
||||
{item.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="to-account">To Account</FieldLabel>
|
||||
<Select items={TO_ACCOUNTS} defaultValue="savings">
|
||||
<SelectTrigger id="to-account" className="w-full">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
{TO_ACCOUNTS.map((item) => (
|
||||
<SelectItem key={item.value} value={item.value}>
|
||||
{item.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
<Item variant="muted" className="flex-col items-stretch">
|
||||
<ItemContent className="gap-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Estimated arrival
|
||||
</span>
|
||||
<span className="text-sm font-medium">Today, Apr 14</span>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Transaction fee
|
||||
</span>
|
||||
<span className="text-sm font-medium tabular-nums">$0.00</span>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-medium">Total amount</span>
|
||||
<span className="text-sm font-semibold tabular-nums">
|
||||
$1,200.00
|
||||
</span>
|
||||
</div>
|
||||
</ItemContent>
|
||||
</Item>
|
||||
</FieldGroup>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button className="w-full">Confirm Transfer</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
|
||||
import { Badge } from "@/registry/bases/base/ui/badge"
|
||||
import { Calendar } from "@/registry/bases/base/ui/calendar"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/base/ui/card"
|
||||
import {
|
||||
Item,
|
||||
ItemContent,
|
||||
ItemDescription,
|
||||
ItemGroup,
|
||||
ItemMedia,
|
||||
ItemTitle,
|
||||
} from "@/registry/bases/base/ui/item"
|
||||
|
||||
export function UpcomingPayments() {
|
||||
const [date, setDate] = React.useState<Date | undefined>(new Date())
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Upcoming Payments</CardTitle>
|
||||
<CardDescription>
|
||||
Select a date to view scheduled payments.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
<Item variant="outline" className="justify-center">
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={date}
|
||||
onSelect={setDate}
|
||||
className="w-full [--cell-size:--spacing(8)] md:[--cell-size:--spacing(10)] style-sera:md:[--cell-size:--spacing(9)]"
|
||||
/>
|
||||
</Item>
|
||||
<ItemGroup className="w-full">
|
||||
<Item variant="muted">
|
||||
<ItemContent>
|
||||
<ItemTitle>Netflix Subscription</ItemTitle>
|
||||
<ItemDescription>Apr 15, 2024</ItemDescription>
|
||||
</ItemContent>
|
||||
<Badge variant="secondary">$19.99</Badge>
|
||||
</Item>
|
||||
<Item variant="muted">
|
||||
<ItemContent>
|
||||
<ItemTitle>Rent Payment</ItemTitle>
|
||||
<ItemDescription>Apr 1, 2024</ItemDescription>
|
||||
</ItemContent>
|
||||
<Badge variant="secondary">$2,400.00</Badge>
|
||||
</Item>
|
||||
<Item variant="muted">
|
||||
<ItemContent>
|
||||
<ItemTitle>Auto Insurance</ItemTitle>
|
||||
<ItemDescription>Apr 22, 2024</ItemDescription>
|
||||
</ItemContent>
|
||||
<Badge variant="secondary">$186.00</Badge>
|
||||
</Item>
|
||||
</ItemGroup>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
98
apps/v4/registry/bases/base/blocks/preview-02/index.tsx
Normal file
98
apps/v4/registry/bases/base/blocks/preview-02/index.tsx
Normal file
@@ -0,0 +1,98 @@
|
||||
import { AccountAccess } from "@/registry/bases/base/blocks/preview-02/cards/account-access"
|
||||
import { CardOverview } from "@/registry/bases/base/blocks/preview-02/cards/card-overview"
|
||||
import { ClaimableBalance } from "@/registry/bases/base/blocks/preview-02/cards/claimable-balance"
|
||||
import { ContributionHistory } from "@/registry/bases/base/blocks/preview-02/cards/contribution-history"
|
||||
import { CoverArt } from "@/registry/bases/base/blocks/preview-02/cards/cover-art"
|
||||
import { DividendIncome } from "@/registry/bases/base/blocks/preview-02/cards/dividend-income"
|
||||
import { EmptyConnectBank } from "@/registry/bases/base/blocks/preview-02/cards/empty-connect-bank"
|
||||
import { EmptyDistributeTrack } from "@/registry/bases/base/blocks/preview-02/cards/empty-distribute-track"
|
||||
import { EmptyExploreCatalog } from "@/registry/bases/base/blocks/preview-02/cards/empty-explore-catalog"
|
||||
import { Faq } from "@/registry/bases/base/blocks/preview-02/cards/faq"
|
||||
import { FrontDoor } from "@/registry/bases/base/blocks/preview-02/cards/front-door"
|
||||
import { IndexInvesting } from "@/registry/bases/base/blocks/preview-02/cards/index-investing"
|
||||
import { KitchenIsland } from "@/registry/bases/base/blocks/preview-02/cards/kitchen-island"
|
||||
import { LoadingCard } from "@/registry/bases/base/blocks/preview-02/cards/loading-card"
|
||||
import { NewMilestone } from "@/registry/bases/base/blocks/preview-02/cards/new-milestone"
|
||||
import { NotificationSettings } from "@/registry/bases/base/blocks/preview-02/cards/notification-settings"
|
||||
import { Payments } from "@/registry/bases/base/blocks/preview-02/cards/payments"
|
||||
import { PayoutThreshold } from "@/registry/bases/base/blocks/preview-02/cards/payout-threshold"
|
||||
import { PowerUsage } from "@/registry/bases/base/blocks/preview-02/cards/power-usage"
|
||||
import { Preferences } from "@/registry/bases/base/blocks/preview-02/cards/preferences"
|
||||
import { QrConnect } from "@/registry/bases/base/blocks/preview-02/cards/qr-connect"
|
||||
import { ReceivingMethod } from "@/registry/bases/base/blocks/preview-02/cards/receiving-method"
|
||||
import { RecentTransactions } from "@/registry/bases/base/blocks/preview-02/cards/recent-transactions"
|
||||
import { ReleaseCatalog } from "@/registry/bases/base/blocks/preview-02/cards/release-catalog"
|
||||
import { RollerShades } from "@/registry/bases/base/blocks/preview-02/cards/roller-shades"
|
||||
import { SavingsProgress } from "@/registry/bases/base/blocks/preview-02/cards/savings-progress"
|
||||
import { SavingsTargets } from "@/registry/bases/base/blocks/preview-02/cards/savings-targets"
|
||||
import { SidebarNav } from "@/registry/bases/base/blocks/preview-02/cards/sidebar-nav"
|
||||
import { SocialLinks } from "@/registry/bases/base/blocks/preview-02/cards/social-links"
|
||||
import { StockPerformance } from "@/registry/bases/base/blocks/preview-02/cards/stock-performance"
|
||||
import { SyncingState } from "@/registry/bases/base/blocks/preview-02/cards/syncing-state"
|
||||
import { TransferFunds } from "@/registry/bases/base/blocks/preview-02/cards/transfer-funds"
|
||||
import { UpcomingPayments } from "@/registry/bases/base/blocks/preview-02/cards/upcoming-payments"
|
||||
|
||||
export default function Preview02Example() {
|
||||
return (
|
||||
<div className="overflow-x-auto overflow-y-hidden bg-muted contain-[paint] [--gap:--spacing(4)] 3xl:[--gap:--spacing(12)] md:[--gap:--spacing(10)] dark:bg-background style-lyra:md:[--gap:--spacing(6)] style-mira:md:[--gap:--spacing(6)]">
|
||||
<div className="flex w-full min-w-max justify-center">
|
||||
<div
|
||||
className="grid w-[2400px] grid-cols-7 items-start gap-(--gap) bg-muted p-(--gap) md:w-[3000px] dark:bg-background style-lyra:md:w-[2600px] style-mira:md:w-[2600px] *:[div]:gap-(--gap)"
|
||||
data-slot="capture-target"
|
||||
>
|
||||
<div className="flex flex-col p-1 [contain-intrinsic-size:380px_1200px] [content-visibility:auto]">
|
||||
<ContributionHistory />
|
||||
<EmptyDistributeTrack />
|
||||
<QrConnect />
|
||||
<DividendIncome />
|
||||
<IndexInvesting />
|
||||
<SyncingState />
|
||||
</div>
|
||||
<div className="flex flex-col p-1 [contain-intrinsic-size:380px_1200px] [content-visibility:auto]">
|
||||
<PayoutThreshold />
|
||||
<ClaimableBalance />
|
||||
<Preferences />
|
||||
<SavingsProgress />
|
||||
<KitchenIsland />
|
||||
</div>
|
||||
<div className="col-span-2 flex flex-col p-1 [contain-intrinsic-size:760px_1200px] [content-visibility:auto]">
|
||||
<SavingsTargets />
|
||||
<RecentTransactions />
|
||||
<div className="grid grid-cols-2 items-start gap-(--gap)">
|
||||
<div className="flex flex-col gap-(--gap)">
|
||||
<SidebarNav />
|
||||
<Faq />
|
||||
</div>
|
||||
<div className="flex flex-col gap-(--gap)">
|
||||
<Payments />
|
||||
<FrontDoor />
|
||||
</div>
|
||||
</div>
|
||||
<ReleaseCatalog />
|
||||
</div>
|
||||
<div className="flex flex-col p-1 [contain-intrinsic-size:380px_1200px] [content-visibility:auto]">
|
||||
<AccountAccess />
|
||||
<CardOverview />
|
||||
<TransferFunds />
|
||||
<CoverArt />
|
||||
<LoadingCard />
|
||||
</div>
|
||||
<div className="flex flex-col p-1 [contain-intrinsic-size:380px_1200px] [content-visibility:auto]">
|
||||
<ReceivingMethod />
|
||||
<PowerUsage />
|
||||
<EmptyConnectBank />
|
||||
<UpcomingPayments />
|
||||
<RollerShades />
|
||||
</div>
|
||||
<div className="flex flex-col p-1 [contain-intrinsic-size:380px_1200px] [content-visibility:auto]">
|
||||
<StockPerformance />
|
||||
<EmptyExploreCatalog />
|
||||
<NewMilestone />
|
||||
<SocialLinks />
|
||||
<NotificationSettings />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
3
apps/v4/registry/bases/base/blocks/preview-03/index.tsx
Normal file
3
apps/v4/registry/bases/base/blocks/preview-03/index.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function Preview03() {
|
||||
return <div>Preview 03</div>
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
"use client"
|
||||
|
||||
import { Alert, AlertDescription } from "@/registry/bases/base/ui/alert"
|
||||
import { Badge } from "@/registry/bases/base/ui/badge"
|
||||
import { Button } from "@/registry/bases/base/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/base/ui/card"
|
||||
import {
|
||||
Item,
|
||||
ItemContent,
|
||||
ItemGroup,
|
||||
ItemMedia,
|
||||
ItemTitle,
|
||||
} from "@/registry/bases/base/ui/item"
|
||||
import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"
|
||||
|
||||
// Agent feature descriptions.
|
||||
const agentFeatures = [
|
||||
{
|
||||
id: "code-reviews",
|
||||
content: (
|
||||
<>
|
||||
<strong>Code reviews</strong> with full codebase context to catch{" "}
|
||||
<strong>hard-to-find</strong> bugs.
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "code-suggestions",
|
||||
content: (
|
||||
<>
|
||||
<strong>Code suggestions</strong> validated in sandboxes before you
|
||||
merge.
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "root-cause",
|
||||
content: (
|
||||
<>
|
||||
<strong>Root-cause analysis</strong> for production issues with
|
||||
deployment context.{" "}
|
||||
<Badge variant="secondary" className="bg-chart-1 text-chart-5">
|
||||
Requires Observability Plus
|
||||
</Badge>
|
||||
</>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
export function ActivateAgentDialog() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Ship faster & safer with Vercel Agent</CardTitle>
|
||||
<CardDescription>
|
||||
Your use is subject to Vercel's{" "}
|
||||
<a href="#">Public Beta Agreement</a> and{" "}
|
||||
<a href="#">AI Product Terms</a>.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
<ItemGroup className="gap-0">
|
||||
{agentFeatures.map((feature) => (
|
||||
<Item key={feature.id} size="xs" className="px-0">
|
||||
<ItemMedia variant="icon" className="self-start">
|
||||
<IconPlaceholder
|
||||
lucide="CheckCircle2Icon"
|
||||
tabler="IconCircleCheckFilled"
|
||||
hugeicons="CheckmarkCircle02Icon"
|
||||
phosphor="CheckCircleIcon"
|
||||
remixicon="RiCheckboxCircleLine"
|
||||
className="size-5 fill-primary text-primary-foreground"
|
||||
/>
|
||||
</ItemMedia>
|
||||
<ItemContent>
|
||||
<ItemTitle className="inline leading-relaxed font-normal text-muted-foreground *:[strong]:font-medium *:[strong]:text-foreground">
|
||||
{feature.content}
|
||||
</ItemTitle>
|
||||
</ItemContent>
|
||||
</Item>
|
||||
))}
|
||||
</ItemGroup>
|
||||
<Alert>
|
||||
<AlertDescription>
|
||||
Pro teams get $100 in Vercel Agent trial credit for 2 weeks after
|
||||
activation.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</CardContent>
|
||||
<CardFooter className="justify-end gap-2">
|
||||
<Button variant="outline" className="style-sera:hidden">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button className="style-sera:w-full">Enable with $100 credits</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
"use client"
|
||||
|
||||
import { Area, AreaChart } from "recharts"
|
||||
|
||||
import { Badge } from "@/registry/bases/base/ui/badge"
|
||||
import { Button } from "@/registry/bases/base/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardAction,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/base/ui/card"
|
||||
import {
|
||||
ChartContainer,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
type ChartConfig,
|
||||
} from "@/registry/bases/base/ui/chart"
|
||||
|
||||
// Monthly visitor data for the area chart.
|
||||
const chartData = [
|
||||
{ month: "January", visitors: 186 },
|
||||
{ month: "February", visitors: 305 },
|
||||
{ month: "March", visitors: 237 },
|
||||
{ month: "April", visitors: 73 },
|
||||
{ month: "May", visitors: 209 },
|
||||
{ month: "June", visitors: 214 },
|
||||
]
|
||||
|
||||
// Chart configuration for the visitors dataset.
|
||||
const chartConfig = {
|
||||
visitors: {
|
||||
label: "Visitors",
|
||||
color: "var(--chart-1)",
|
||||
},
|
||||
} satisfies ChartConfig
|
||||
|
||||
export function AnalyticsCard() {
|
||||
return (
|
||||
<Card className="mx-auto w-full max-w-sm data-[size=sm]:pb-0" size="sm">
|
||||
<CardHeader>
|
||||
<CardTitle>Analytics</CardTitle>
|
||||
<CardDescription>
|
||||
418.2K Visitors <Badge>+10%</Badge>
|
||||
</CardDescription>
|
||||
<CardAction>
|
||||
<Button variant="outline" size="sm">
|
||||
View Analytics
|
||||
</Button>
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
<ChartContainer config={chartConfig} className="aspect-[1/0.35]">
|
||||
<AreaChart
|
||||
accessibilityLayer
|
||||
data={chartData}
|
||||
margin={{
|
||||
left: 0,
|
||||
right: 0,
|
||||
}}
|
||||
>
|
||||
<ChartTooltip
|
||||
cursor={false}
|
||||
content={<ChartTooltipContent indicator="line" hideLabel />}
|
||||
/>
|
||||
<Area
|
||||
dataKey="visitors"
|
||||
type="linear"
|
||||
fill="var(--color-visitors)"
|
||||
fillOpacity={0.4}
|
||||
stroke="var(--color-visitors)"
|
||||
/>
|
||||
</AreaChart>
|
||||
</ChartContainer>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
"use client"
|
||||
|
||||
import { Button } from "@/registry/bases/base/ui/button"
|
||||
import { Card, CardContent } from "@/registry/bases/base/ui/card"
|
||||
import {
|
||||
Empty,
|
||||
EmptyContent,
|
||||
EmptyDescription,
|
||||
EmptyHeader,
|
||||
EmptyTitle,
|
||||
} from "@/registry/bases/base/ui/empty"
|
||||
|
||||
export function AnomalyAlert() {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Empty className="h-48">
|
||||
<EmptyHeader>
|
||||
<EmptyTitle>Get alerted for anomalies</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
Automatically monitor your projects for anomalies and get
|
||||
notified.
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
<EmptyContent>
|
||||
<Button>Upgrade to Observability Plus</Button>
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
|
||||
import {
|
||||
Avatar,
|
||||
AvatarFallback,
|
||||
AvatarImage,
|
||||
} from "@/registry/bases/base/ui/avatar"
|
||||
import { Button } from "@/registry/bases/base/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardAction,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/base/ui/card"
|
||||
import {
|
||||
Combobox,
|
||||
ComboboxChip,
|
||||
ComboboxChips,
|
||||
ComboboxChipsInput,
|
||||
ComboboxContent,
|
||||
ComboboxEmpty,
|
||||
ComboboxItem,
|
||||
ComboboxList,
|
||||
ComboboxValue,
|
||||
useComboboxAnchor,
|
||||
} from "@/registry/bases/base/ui/combobox"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/registry/bases/base/ui/tooltip"
|
||||
import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"
|
||||
|
||||
// Users available for assignment.
|
||||
const users = [
|
||||
"shadcn",
|
||||
"maxleiter",
|
||||
"evilrabbit",
|
||||
"pranathip",
|
||||
"jorgezreik",
|
||||
"shuding",
|
||||
"rauchg",
|
||||
]
|
||||
|
||||
export function AssignIssue() {
|
||||
const anchor = useComboboxAnchor()
|
||||
return (
|
||||
<Card className="w-full max-w-sm" size="sm">
|
||||
<CardHeader className="border-b">
|
||||
<CardTitle className="text-sm">Assign Issue</CardTitle>
|
||||
<CardDescription className="text-sm">
|
||||
Select users to assign to this issue.
|
||||
</CardDescription>
|
||||
<CardAction>
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={<Button variant="outline" size="icon-xs" />}
|
||||
>
|
||||
<IconPlaceholder
|
||||
lucide="PlusIcon"
|
||||
tabler="IconPlus"
|
||||
hugeicons="PlusSignIcon"
|
||||
phosphor="PlusIcon"
|
||||
remixicon="RiAddLine"
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Add user</TooltipContent>
|
||||
</Tooltip>
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Combobox
|
||||
multiple
|
||||
autoHighlight
|
||||
items={users}
|
||||
defaultValue={[users[0]]}
|
||||
>
|
||||
<ComboboxChips ref={anchor}>
|
||||
<ComboboxValue>
|
||||
{(values) => (
|
||||
<React.Fragment>
|
||||
{values.map((username: string) => (
|
||||
<ComboboxChip key={username}>
|
||||
<Avatar className="size-4">
|
||||
<AvatarImage
|
||||
src={`https://github.com/${username}.png`}
|
||||
alt={username}
|
||||
/>
|
||||
<AvatarFallback>{username.charAt(0)}</AvatarFallback>
|
||||
</Avatar>
|
||||
{username}
|
||||
</ComboboxChip>
|
||||
))}
|
||||
<ComboboxChipsInput
|
||||
placeholder={
|
||||
values.length > 0 ? undefined : "Select a item..."
|
||||
}
|
||||
/>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</ComboboxValue>
|
||||
</ComboboxChips>
|
||||
<ComboboxContent anchor={anchor}>
|
||||
<ComboboxEmpty>No users found.</ComboboxEmpty>
|
||||
<ComboboxList>
|
||||
{(username: string) => (
|
||||
<ComboboxItem key={username} value={username}>
|
||||
<Avatar className="size-5">
|
||||
<AvatarImage
|
||||
src={`https://github.com/${username}.png`}
|
||||
alt={username}
|
||||
/>
|
||||
<AvatarFallback>{username.charAt(0)}</AvatarFallback>
|
||||
</Avatar>
|
||||
{username}
|
||||
</ComboboxItem>
|
||||
)}
|
||||
</ComboboxList>
|
||||
</ComboboxContent>
|
||||
</Combobox>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Bar, BarChart, CartesianGrid, XAxis } from "recharts"
|
||||
|
||||
import { Button } from "@/registry/bases/base/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/base/ui/card"
|
||||
import {
|
||||
ChartContainer,
|
||||
ChartLegend,
|
||||
ChartLegendContent,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
type ChartConfig,
|
||||
} from "@/registry/bases/base/ui/chart"
|
||||
import { useDesignSystemSearchParams } from "@/app/(app)/create/lib/search-params"
|
||||
|
||||
const barChartData = [
|
||||
{ month: "January", desktop: 186, mobile: 80 },
|
||||
{ month: "February", desktop: 305, mobile: 200 },
|
||||
{ month: "March", desktop: 237, mobile: 120 },
|
||||
{ month: "April", desktop: 73, mobile: 190 },
|
||||
{ month: "May", desktop: 209, mobile: 130 },
|
||||
{ month: "June", desktop: 214, mobile: 140 },
|
||||
]
|
||||
|
||||
const barChartConfig = {
|
||||
desktop: {
|
||||
label: "Desktop",
|
||||
color: "var(--chart-1)",
|
||||
},
|
||||
mobile: {
|
||||
label: "Mobile",
|
||||
color: "var(--chart-2)",
|
||||
},
|
||||
} satisfies ChartConfig
|
||||
|
||||
const desktopTotal = barChartData.reduce((sum, item) => sum + item.desktop, 0)
|
||||
const mobileTotal = barChartData.reduce((sum, item) => sum + item.mobile, 0)
|
||||
const desktopDelta = Math.round(
|
||||
((desktopTotal - mobileTotal) / mobileTotal) * 100
|
||||
)
|
||||
const desktopDeltaPrefix = desktopDelta > 0 ? "+" : ""
|
||||
|
||||
export function BarChartCard() {
|
||||
const [params] = useDesignSystemSearchParams()
|
||||
const isRounded = !["lyra", "sera"].includes(params.style)
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">Traffic channels</CardTitle>
|
||||
<CardDescription className="line-clamp-2 text-sm leading-snug">
|
||||
Monthly desktop and mobile traffic for the last six months—compare
|
||||
volume and mix across platforms and devices at a glance.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4 pt-0">
|
||||
<ChartContainer
|
||||
config={barChartConfig}
|
||||
className="max-h-[180px] w-full"
|
||||
>
|
||||
<BarChart
|
||||
accessibilityLayer
|
||||
data={barChartData}
|
||||
margin={{ left: 0, right: 0, top: 8, bottom: 0 }}
|
||||
>
|
||||
<CartesianGrid vertical={false} strokeDasharray="3 3" />
|
||||
<XAxis
|
||||
dataKey="month"
|
||||
tickLine={false}
|
||||
tickMargin={8}
|
||||
axisLine={false}
|
||||
tickFormatter={(value) => String(value).slice(0, 3)}
|
||||
/>
|
||||
<ChartTooltip
|
||||
cursor={false}
|
||||
content={<ChartTooltipContent indicator="dashed" />}
|
||||
/>
|
||||
<ChartLegend content={<ChartLegendContent />} />
|
||||
<Bar
|
||||
dataKey="desktop"
|
||||
fill="var(--color-desktop)"
|
||||
radius={isRounded ? [6, 6, 0, 0] : 0}
|
||||
/>
|
||||
<Bar
|
||||
dataKey="mobile"
|
||||
fill="var(--color-mobile)"
|
||||
radius={isRounded ? [6, 6, 0, 0] : 0}
|
||||
/>
|
||||
</BarChart>
|
||||
</ChartContainer>
|
||||
<div className="grid w-full grid-cols-3 divide-x divide-border/60">
|
||||
<div className="px-2 text-center">
|
||||
<div className="text-[0.65rem] text-muted-foreground uppercase">
|
||||
Desktop
|
||||
</div>
|
||||
<div className="text-sm font-medium tabular-nums">
|
||||
{desktopTotal.toLocaleString()}
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-2 text-center">
|
||||
<div className="text-[0.65rem] text-muted-foreground uppercase">
|
||||
Mobile
|
||||
</div>
|
||||
<div className="text-sm font-medium tabular-nums">
|
||||
{mobileTotal.toLocaleString()}
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-2 text-center">
|
||||
<div className="text-[0.65rem] text-muted-foreground uppercase">
|
||||
Mix Delta
|
||||
</div>
|
||||
<div className="text-sm font-medium tabular-nums">
|
||||
{desktopDeltaPrefix}
|
||||
{desktopDelta}%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button className="w-full">View report</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,513 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { cn } from "cn"
|
||||
|
||||
import { Button } from "@/registry/bases/base/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/base/ui/card"
|
||||
|
||||
type AgentState =
|
||||
| "connecting"
|
||||
| "initializing"
|
||||
| "listening"
|
||||
| "speaking"
|
||||
| "thinking"
|
||||
|
||||
type AnimationState = AgentState | undefined
|
||||
|
||||
interface AudioAnalyserOptions {
|
||||
fftSize?: number
|
||||
smoothingTimeConstant?: number
|
||||
minDecibels?: number
|
||||
maxDecibels?: number
|
||||
}
|
||||
|
||||
interface MultiBandVolumeOptions {
|
||||
bands?: number
|
||||
loPass?: number
|
||||
hiPass?: number
|
||||
updateInterval?: number
|
||||
analyserOptions?: AudioAnalyserOptions
|
||||
}
|
||||
|
||||
const multibandDefaults: MultiBandVolumeOptions = {
|
||||
bands: 5,
|
||||
loPass: 100,
|
||||
hiPass: 600,
|
||||
updateInterval: 32,
|
||||
analyserOptions: { fftSize: 2048 },
|
||||
}
|
||||
|
||||
const normalizeDb = (value: number) => {
|
||||
if (value === -Infinity) {
|
||||
return 0
|
||||
}
|
||||
const minDb = -100
|
||||
const maxDb = -10
|
||||
const db = 1 - (Math.max(minDb, Math.min(maxDb, value)) * -1) / 100
|
||||
return Math.sqrt(db)
|
||||
}
|
||||
|
||||
function createAudioAnalyser(
|
||||
mediaStream: MediaStream,
|
||||
options: AudioAnalyserOptions = {}
|
||||
) {
|
||||
const audioContext = new (window.AudioContext ||
|
||||
(window as unknown as { webkitAudioContext: typeof AudioContext })
|
||||
.webkitAudioContext)()
|
||||
const source = audioContext.createMediaStreamSource(mediaStream)
|
||||
const analyser = audioContext.createAnalyser()
|
||||
|
||||
if (options.fftSize) {
|
||||
analyser.fftSize = options.fftSize
|
||||
}
|
||||
if (options.smoothingTimeConstant !== undefined) {
|
||||
analyser.smoothingTimeConstant = options.smoothingTimeConstant
|
||||
}
|
||||
if (options.minDecibels !== undefined) {
|
||||
analyser.minDecibels = options.minDecibels
|
||||
}
|
||||
if (options.maxDecibels !== undefined) {
|
||||
analyser.maxDecibels = options.maxDecibels
|
||||
}
|
||||
|
||||
source.connect(analyser)
|
||||
|
||||
const cleanup = () => {
|
||||
source.disconnect()
|
||||
audioContext.close()
|
||||
}
|
||||
|
||||
return { analyser, audioContext, cleanup }
|
||||
}
|
||||
|
||||
function useMultibandVolume(
|
||||
mediaStream?: MediaStream | null,
|
||||
options: MultiBandVolumeOptions = {}
|
||||
) {
|
||||
const bands = options.bands ?? multibandDefaults.bands ?? 5
|
||||
const loPass = options.loPass ?? multibandDefaults.loPass ?? 100
|
||||
const hiPass = options.hiPass ?? multibandDefaults.hiPass ?? 600
|
||||
const updateInterval =
|
||||
options.updateInterval ?? multibandDefaults.updateInterval ?? 32
|
||||
const fftSize =
|
||||
options.analyserOptions?.fftSize ??
|
||||
multibandDefaults.analyserOptions?.fftSize
|
||||
const smoothingTimeConstant =
|
||||
options.analyserOptions?.smoothingTimeConstant ??
|
||||
multibandDefaults.analyserOptions?.smoothingTimeConstant
|
||||
const minDecibels =
|
||||
options.analyserOptions?.minDecibels ??
|
||||
multibandDefaults.analyserOptions?.minDecibels
|
||||
const maxDecibels =
|
||||
options.analyserOptions?.maxDecibels ??
|
||||
multibandDefaults.analyserOptions?.maxDecibels
|
||||
|
||||
const [frequencyBands, setFrequencyBands] = React.useState<number[]>(() =>
|
||||
new Array(bands).fill(0)
|
||||
)
|
||||
const bandsRef = React.useRef<number[]>(new Array(bands).fill(0))
|
||||
const frameId = React.useRef<number | undefined>(undefined)
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!mediaStream) {
|
||||
const emptyBands = new Array(bands).fill(0)
|
||||
bandsRef.current = emptyBands
|
||||
setFrequencyBands(emptyBands)
|
||||
return
|
||||
}
|
||||
|
||||
const { analyser, cleanup } = createAudioAnalyser(mediaStream, {
|
||||
fftSize,
|
||||
smoothingTimeConstant,
|
||||
minDecibels,
|
||||
maxDecibels,
|
||||
})
|
||||
|
||||
const bufferLength = analyser.frequencyBinCount
|
||||
const dataArray = new Float32Array(bufferLength)
|
||||
const sliceStart = loPass
|
||||
const sliceEnd = hiPass
|
||||
const sliceLength = sliceEnd - sliceStart
|
||||
const chunkSize = Math.ceil(sliceLength / bands)
|
||||
|
||||
let lastUpdate = 0
|
||||
|
||||
const updateVolume = (timestamp: number) => {
|
||||
if (timestamp - lastUpdate >= updateInterval) {
|
||||
analyser.getFloatFrequencyData(dataArray)
|
||||
|
||||
const chunks = new Array(bands)
|
||||
|
||||
for (let i = 0; i < bands; i++) {
|
||||
let sum = 0
|
||||
let count = 0
|
||||
const startIdx = sliceStart + i * chunkSize
|
||||
const endIdx = Math.min(sliceStart + (i + 1) * chunkSize, sliceEnd)
|
||||
|
||||
for (let j = startIdx; j < endIdx; j++) {
|
||||
sum += normalizeDb(dataArray[j])
|
||||
count++
|
||||
}
|
||||
|
||||
chunks[i] = count > 0 ? sum / count : 0
|
||||
}
|
||||
|
||||
let hasChanged = false
|
||||
for (let i = 0; i < chunks.length; i++) {
|
||||
if (Math.abs(chunks[i] - bandsRef.current[i]) > 0.01) {
|
||||
hasChanged = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (hasChanged) {
|
||||
bandsRef.current = chunks
|
||||
setFrequencyBands(chunks)
|
||||
}
|
||||
|
||||
lastUpdate = timestamp
|
||||
}
|
||||
|
||||
frameId.current = requestAnimationFrame(updateVolume)
|
||||
}
|
||||
|
||||
frameId.current = requestAnimationFrame(updateVolume)
|
||||
|
||||
return () => {
|
||||
cleanup()
|
||||
if (frameId.current) {
|
||||
cancelAnimationFrame(frameId.current)
|
||||
}
|
||||
}
|
||||
}, [
|
||||
bands,
|
||||
fftSize,
|
||||
hiPass,
|
||||
loPass,
|
||||
maxDecibels,
|
||||
mediaStream,
|
||||
minDecibels,
|
||||
smoothingTimeConstant,
|
||||
updateInterval,
|
||||
])
|
||||
|
||||
return frequencyBands
|
||||
}
|
||||
|
||||
const generateConnectingSequenceBar = (columns: number): number[][] => {
|
||||
const seq = []
|
||||
for (let x = 0; x < columns; x++) {
|
||||
seq.push([x, columns - 1 - x])
|
||||
}
|
||||
return seq
|
||||
}
|
||||
|
||||
const generateListeningSequenceBar = (columns: number): number[][] => {
|
||||
const center = Math.floor(columns / 2)
|
||||
const noIndex = -1
|
||||
return [[center], [noIndex]]
|
||||
}
|
||||
|
||||
const useBarAnimator = (
|
||||
state: AnimationState,
|
||||
columns: number,
|
||||
interval: number
|
||||
): number[] => {
|
||||
const indexRef = React.useRef(0)
|
||||
const [currentFrame, setCurrentFrame] = React.useState<number[]>([])
|
||||
const animationFrameId = React.useRef<number | null>(null)
|
||||
|
||||
const sequence = React.useMemo(() => {
|
||||
if (state === "thinking" || state === "listening") {
|
||||
return generateListeningSequenceBar(columns)
|
||||
} else if (state === "connecting" || state === "initializing") {
|
||||
return generateConnectingSequenceBar(columns)
|
||||
} else if (state === undefined || state === "speaking") {
|
||||
return [new Array(columns).fill(0).map((_, idx) => idx)]
|
||||
} else {
|
||||
return [[]]
|
||||
}
|
||||
}, [state, columns])
|
||||
|
||||
React.useEffect(() => {
|
||||
indexRef.current = 0
|
||||
setCurrentFrame(sequence[0] || [])
|
||||
}, [sequence])
|
||||
|
||||
React.useEffect(() => {
|
||||
let startTime = performance.now()
|
||||
|
||||
const animate = (time: DOMHighResTimeStamp) => {
|
||||
const timeElapsed = time - startTime
|
||||
|
||||
if (timeElapsed >= interval) {
|
||||
indexRef.current = (indexRef.current + 1) % sequence.length
|
||||
setCurrentFrame(sequence[indexRef.current] || [])
|
||||
startTime = time
|
||||
}
|
||||
|
||||
animationFrameId.current = requestAnimationFrame(animate)
|
||||
}
|
||||
|
||||
animationFrameId.current = requestAnimationFrame(animate)
|
||||
|
||||
return () => {
|
||||
if (animationFrameId.current !== null) {
|
||||
cancelAnimationFrame(animationFrameId.current)
|
||||
}
|
||||
}
|
||||
}, [interval, sequence])
|
||||
|
||||
return currentFrame
|
||||
}
|
||||
|
||||
// Memoized Bar component to prevent unnecessary re-renders.
|
||||
const Bar = React.memo<{
|
||||
heightPct: number
|
||||
isHighlighted: boolean
|
||||
state?: AgentState
|
||||
}>(({ heightPct, isHighlighted, state }) => (
|
||||
<div
|
||||
data-highlighted={isHighlighted}
|
||||
className={cn(
|
||||
"max-w-[12px] min-w-[8px] flex-1 transition-all duration-150",
|
||||
"rounded-full",
|
||||
"bg-border data-[highlighted=true]:bg-chart-3",
|
||||
state === "speaking" && "bg-chart-3",
|
||||
state === "thinking" && isHighlighted && "animate-pulse"
|
||||
)}
|
||||
style={{
|
||||
height: `${heightPct}%`,
|
||||
animationDuration: state === "thinking" ? "300ms" : undefined,
|
||||
}}
|
||||
/>
|
||||
))
|
||||
|
||||
Bar.displayName = "Bar"
|
||||
|
||||
const BarVisualizerComponent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
{
|
||||
state?: AgentState
|
||||
barCount?: number
|
||||
mediaStream?: MediaStream | null
|
||||
minHeight?: number
|
||||
maxHeight?: number
|
||||
demo?: boolean
|
||||
centerAlign?: boolean
|
||||
} & React.HTMLAttributes<HTMLDivElement>
|
||||
>(
|
||||
(
|
||||
{
|
||||
state,
|
||||
barCount = 15,
|
||||
mediaStream,
|
||||
minHeight = 20,
|
||||
maxHeight = 100,
|
||||
demo = false,
|
||||
centerAlign = false,
|
||||
className,
|
||||
style,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const realVolumeBands = useMultibandVolume(mediaStream, {
|
||||
bands: barCount,
|
||||
loPass: 100,
|
||||
hiPass: 200,
|
||||
})
|
||||
|
||||
const fakeVolumeBandsRef = React.useRef<number[]>(
|
||||
new Array(barCount).fill(0.2)
|
||||
)
|
||||
const [fakeVolumeBands, setFakeVolumeBands] = React.useState<number[]>(() =>
|
||||
new Array(barCount).fill(0.2)
|
||||
)
|
||||
const fakeAnimationRef = React.useRef<number | undefined>(undefined)
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!demo) {
|
||||
return
|
||||
}
|
||||
|
||||
if (state !== "speaking" && state !== "listening") {
|
||||
const bands = new Array(barCount).fill(0.2)
|
||||
fakeVolumeBandsRef.current = bands
|
||||
setFakeVolumeBands(bands)
|
||||
return
|
||||
}
|
||||
|
||||
let lastUpdate = 0
|
||||
const updateInterval = 50
|
||||
const startTime = Date.now() / 1000
|
||||
|
||||
const updateFakeVolume = (timestamp: number) => {
|
||||
if (timestamp - lastUpdate >= updateInterval) {
|
||||
const time = Date.now() / 1000 - startTime
|
||||
const newBands = new Array(barCount)
|
||||
|
||||
for (let i = 0; i < barCount; i++) {
|
||||
const waveOffset = i * 0.5
|
||||
const baseVolume = Math.sin(time * 2 + waveOffset) * 0.3 + 0.5
|
||||
const randomNoise = Math.random() * 0.2
|
||||
newBands[i] = Math.max(0.1, Math.min(1, baseVolume + randomNoise))
|
||||
}
|
||||
|
||||
let hasChanged = false
|
||||
for (let i = 0; i < barCount; i++) {
|
||||
if (Math.abs(newBands[i] - fakeVolumeBandsRef.current[i]) > 0.05) {
|
||||
hasChanged = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (hasChanged) {
|
||||
fakeVolumeBandsRef.current = newBands
|
||||
setFakeVolumeBands(newBands)
|
||||
}
|
||||
|
||||
lastUpdate = timestamp
|
||||
}
|
||||
|
||||
fakeAnimationRef.current = requestAnimationFrame(updateFakeVolume)
|
||||
}
|
||||
|
||||
fakeAnimationRef.current = requestAnimationFrame(updateFakeVolume)
|
||||
|
||||
return () => {
|
||||
if (fakeAnimationRef.current) {
|
||||
cancelAnimationFrame(fakeAnimationRef.current)
|
||||
}
|
||||
}
|
||||
}, [demo, state, barCount])
|
||||
|
||||
const volumeBands = React.useMemo(
|
||||
() => (demo ? fakeVolumeBands : realVolumeBands),
|
||||
[demo, fakeVolumeBands, realVolumeBands]
|
||||
)
|
||||
|
||||
const highlightedIndices = useBarAnimator(
|
||||
state,
|
||||
barCount,
|
||||
state === "connecting"
|
||||
? 2000 / barCount
|
||||
: state === "thinking"
|
||||
? 150
|
||||
: state === "listening"
|
||||
? 500
|
||||
: 1000
|
||||
)
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
data-state={state}
|
||||
className={cn(
|
||||
"relative flex justify-center gap-1.5",
|
||||
centerAlign ? "items-center" : "items-end",
|
||||
"h-32 w-full overflow-hidden rounded-lg bg-muted p-4",
|
||||
className
|
||||
)}
|
||||
style={{
|
||||
...style,
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
{volumeBands.map((volume, index) => {
|
||||
const heightPct = Math.min(
|
||||
maxHeight,
|
||||
Math.max(minHeight, volume * 100 + 5)
|
||||
)
|
||||
const isHighlighted = highlightedIndices?.includes(index) ?? false
|
||||
|
||||
return (
|
||||
<Bar
|
||||
key={index}
|
||||
heightPct={heightPct}
|
||||
isHighlighted={isHighlighted}
|
||||
state={state}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
BarVisualizerComponent.displayName = "BarVisualizerComponent"
|
||||
|
||||
const BarVisualizer = React.memo(
|
||||
BarVisualizerComponent,
|
||||
(prevProps, nextProps) => {
|
||||
return (
|
||||
prevProps.state === nextProps.state &&
|
||||
prevProps.barCount === nextProps.barCount &&
|
||||
prevProps.mediaStream === nextProps.mediaStream &&
|
||||
prevProps.minHeight === nextProps.minHeight &&
|
||||
prevProps.maxHeight === nextProps.maxHeight &&
|
||||
prevProps.demo === nextProps.demo &&
|
||||
prevProps.centerAlign === nextProps.centerAlign &&
|
||||
prevProps.className === nextProps.className &&
|
||||
JSON.stringify(prevProps.style) === JSON.stringify(nextProps.style)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
BarVisualizer.displayName = "BarVisualizer"
|
||||
|
||||
export function BarVisualizerCard() {
|
||||
const [state, setState] = React.useState<AgentState>("speaking")
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Audio Frequency Visualizer</CardTitle>
|
||||
<CardDescription>
|
||||
Real-time frequency band visualization with animated state transitions
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<BarVisualizer
|
||||
state={state}
|
||||
demo={true}
|
||||
barCount={20}
|
||||
minHeight={15}
|
||||
maxHeight={90}
|
||||
className="h-40 max-w-full"
|
||||
/>
|
||||
</CardContent>
|
||||
<CardFooter className="gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant={state === "connecting" ? "default" : "outline"}
|
||||
onClick={() => setState("connecting")}
|
||||
>
|
||||
Connecting
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant={state === "listening" ? "default" : "outline"}
|
||||
onClick={() => setState("listening")}
|
||||
>
|
||||
Listening
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant={state === "speaking" ? "default" : "outline"}
|
||||
onClick={() => setState("speaking")}
|
||||
>
|
||||
Speaking
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
"use client"
|
||||
|
||||
import {
|
||||
Alert,
|
||||
AlertDescription,
|
||||
AlertTitle,
|
||||
} from "@/registry/bases/base/ui/alert"
|
||||
import { Button } from "@/registry/bases/base/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/base/ui/card"
|
||||
import { Field, FieldGroup, FieldLabel } from "@/registry/bases/base/ui/field"
|
||||
import {
|
||||
ToggleGroup,
|
||||
ToggleGroupItem,
|
||||
} from "@/registry/bases/base/ui/toggle-group"
|
||||
import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"
|
||||
|
||||
export function BookAppointment() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Book Appointment</CardTitle>
|
||||
<CardDescription>Dr. Sarah Chen · Cardiology</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
<FieldGroup>
|
||||
<Field>
|
||||
<FieldLabel>Available on March 18, 2026</FieldLabel>
|
||||
<ToggleGroup spacing={2} defaultValue={["slot-0"]}>
|
||||
{["9:00 AM", "10:30 AM", "11:00 AM", "1:30 PM"].map(
|
||||
(time, index) => (
|
||||
<ToggleGroupItem key={time} value={`slot-${index}`}>
|
||||
{time}
|
||||
</ToggleGroupItem>
|
||||
)
|
||||
)}
|
||||
</ToggleGroup>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
<Alert>
|
||||
<AlertTitle>New patient?</AlertTitle>
|
||||
<AlertDescription>Please arrive 15 minutes early.</AlertDescription>
|
||||
</Alert>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button className="w-full">Book Appointment</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,390 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
|
||||
import { Button } from "@/registry/bases/base/ui/button"
|
||||
import { Card, CardContent } from "@/registry/bases/base/ui/card"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/registry/bases/base/ui/dropdown-menu"
|
||||
import {
|
||||
Empty,
|
||||
EmptyContent,
|
||||
EmptyDescription,
|
||||
EmptyHeader,
|
||||
EmptyMedia,
|
||||
EmptyTitle,
|
||||
} from "@/registry/bases/base/ui/empty"
|
||||
import {
|
||||
Field,
|
||||
FieldDescription,
|
||||
FieldLabel,
|
||||
} from "@/registry/bases/base/ui/field"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupButton,
|
||||
InputGroupInput,
|
||||
} from "@/registry/bases/base/ui/input-group"
|
||||
import {
|
||||
Item,
|
||||
ItemActions,
|
||||
ItemContent,
|
||||
ItemDescription,
|
||||
ItemTitle,
|
||||
} from "@/registry/bases/base/ui/item"
|
||||
import { Separator } from "@/registry/bases/base/ui/separator"
|
||||
import { Spinner } from "@/registry/bases/base/ui/spinner"
|
||||
import {
|
||||
Tabs,
|
||||
TabsContent,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
} from "@/registry/bases/base/ui/tabs"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/registry/bases/base/ui/tooltip"
|
||||
import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"
|
||||
|
||||
export function CodespacesCard() {
|
||||
const [isCreatingCodespace, setIsCreatingCodespace] = React.useState(false)
|
||||
return (
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Tabs defaultValue="codespaces">
|
||||
<TabsList className="w-full">
|
||||
<TabsTrigger value="codespaces">Codespaces</TabsTrigger>
|
||||
<TabsTrigger value="local">Local</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="codespaces">
|
||||
<Item size="sm" className="px-1 pt-2">
|
||||
<ItemContent>
|
||||
<ItemTitle>Codespaces</ItemTitle>
|
||||
<ItemDescription>Your workspaces in the cloud</ItemDescription>
|
||||
</ItemContent>
|
||||
<ItemActions>
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={<Button variant="ghost" size="icon-sm" />}
|
||||
>
|
||||
<IconPlaceholder
|
||||
lucide="PlusIcon"
|
||||
tabler="IconPlus"
|
||||
hugeicons="PlusSignIcon"
|
||||
phosphor="PlusIcon"
|
||||
remixicon="RiAddLine"
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">
|
||||
Create a codespace on main
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
render={<Button variant="ghost" size="icon-sm" />}
|
||||
>
|
||||
<IconPlaceholder
|
||||
lucide="MoreHorizontalIcon"
|
||||
tabler="IconDots"
|
||||
hugeicons="MoreHorizontalCircle01Icon"
|
||||
phosphor="DotsThreeOutlineIcon"
|
||||
remixicon="RiMoreLine"
|
||||
/>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-56">
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem>
|
||||
<IconPlaceholder
|
||||
lucide="PlusIcon"
|
||||
tabler="IconPlus"
|
||||
hugeicons="PlusSignIcon"
|
||||
phosphor="PlusIcon"
|
||||
remixicon="RiAddLine"
|
||||
/>
|
||||
New with options...
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<IconPlaceholder
|
||||
lucide="ContainerIcon"
|
||||
tabler="IconBox"
|
||||
hugeicons="CubeIcon"
|
||||
phosphor="CubeIcon"
|
||||
remixicon="RiBox1Line"
|
||||
/>
|
||||
Configure container
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<IconPlaceholder
|
||||
lucide="ZapIcon"
|
||||
tabler="IconBolt"
|
||||
hugeicons="ZapIcon"
|
||||
phosphor="LightningIcon"
|
||||
remixicon="RiFlashlightLine"
|
||||
/>
|
||||
Set up prebuilds
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem>
|
||||
<IconPlaceholder
|
||||
lucide="ServerIcon"
|
||||
tabler="IconServer"
|
||||
hugeicons="ServerStackIcon"
|
||||
phosphor="HardDrivesIcon"
|
||||
remixicon="RiHardDriveLine"
|
||||
/>
|
||||
Manage codespaces
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<IconPlaceholder
|
||||
lucide="ShareIcon"
|
||||
tabler="IconShare2"
|
||||
hugeicons="Share03Icon"
|
||||
phosphor="ShareIcon"
|
||||
remixicon="RiShareLine"
|
||||
/>
|
||||
Share deep link
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<IconPlaceholder
|
||||
lucide="InfoIcon"
|
||||
tabler="IconInfoCircle"
|
||||
hugeicons="AlertCircleIcon"
|
||||
phosphor="InfoIcon"
|
||||
remixicon="RiInformationLine"
|
||||
/>
|
||||
What are codespaces?
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</ItemActions>
|
||||
</Item>
|
||||
<Separator className="-mx-2 my-2 w-auto!" />
|
||||
<Empty className="p-4">
|
||||
<EmptyHeader>
|
||||
<EmptyMedia variant="icon">
|
||||
<IconPlaceholder
|
||||
lucide="ServerIcon"
|
||||
tabler="IconServer"
|
||||
hugeicons="ServerStackIcon"
|
||||
phosphor="HardDrivesIcon"
|
||||
remixicon="RiHardDriveLine"
|
||||
/>
|
||||
</EmptyMedia>
|
||||
<EmptyTitle>No codespaces</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
You don't have any codespaces with this repository
|
||||
checked out
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
<EmptyContent>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setIsCreatingCodespace(true)
|
||||
setTimeout(() => {
|
||||
setIsCreatingCodespace(false)
|
||||
}, 2000)
|
||||
}}
|
||||
disabled={isCreatingCodespace}
|
||||
>
|
||||
{isCreatingCodespace ? (
|
||||
<Spinner data-icon="inline-start" />
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
Create Codespace
|
||||
</Button>
|
||||
<a
|
||||
href="#learn-more"
|
||||
className="text-xs text-muted-foreground underline underline-offset-4"
|
||||
>
|
||||
Learn more about codespaces
|
||||
</a>
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
<Separator className="-mx-2 my-2 w-auto!" />
|
||||
<div className="p-1.5 text-xs text-muted-foreground">
|
||||
Codespace usage for this repository is paid for by{" "}
|
||||
<span className="font-medium">shadcn</span>.
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="local">
|
||||
<Item size="sm" className="hidden p-0">
|
||||
<ItemContent>
|
||||
<ItemTitle className="gap-2">
|
||||
<IconPlaceholder
|
||||
lucide="TerminalIcon"
|
||||
tabler="IconTerminal"
|
||||
hugeicons="ComputerTerminal01Icon"
|
||||
phosphor="TerminalIcon"
|
||||
remixicon="RiTerminalBoxLine"
|
||||
className="size-4"
|
||||
/>
|
||||
Clone
|
||||
</ItemTitle>
|
||||
</ItemContent>
|
||||
<ItemActions>
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={<Button variant="ghost" size="icon" />}
|
||||
>
|
||||
<IconPlaceholder
|
||||
lucide="InfoIcon"
|
||||
tabler="IconInfoCircle"
|
||||
hugeicons="AlertCircleIcon"
|
||||
phosphor="InfoIcon"
|
||||
remixicon="RiInformationLine"
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="left">
|
||||
Which remote URL should I use?
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</ItemActions>
|
||||
</Item>
|
||||
<Tabs defaultValue="https">
|
||||
<TabsList
|
||||
variant="line"
|
||||
className="w-full justify-start border-b *:[button]:flex-0"
|
||||
>
|
||||
<TabsTrigger value="https">HTTPS</TabsTrigger>
|
||||
<TabsTrigger value="ssh">SSH</TabsTrigger>
|
||||
<TabsTrigger value="cli">GitHub CLI</TabsTrigger>
|
||||
</TabsList>
|
||||
<div className="rounded-md border bg-muted/30 p-2">
|
||||
<TabsContent value="https">
|
||||
<Field className="gap-2">
|
||||
<FieldLabel htmlFor="https-url" className="sr-only">
|
||||
HTTPS URL
|
||||
</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupAddon align="inline-end">
|
||||
<InputGroupButton variant="ghost" size="icon-xs">
|
||||
<IconPlaceholder
|
||||
lucide="CopyIcon"
|
||||
tabler="IconCopy"
|
||||
hugeicons="Copy01Icon"
|
||||
phosphor="CopyIcon"
|
||||
remixicon="RiFileCopyLine"
|
||||
/>
|
||||
</InputGroupButton>
|
||||
</InputGroupAddon>
|
||||
<InputGroupInput
|
||||
id="https-url"
|
||||
defaultValue="https://github.com/shadcn-ui/ui.git"
|
||||
readOnly
|
||||
/>
|
||||
</InputGroup>
|
||||
<FieldDescription>
|
||||
Clone using the web URL.
|
||||
</FieldDescription>
|
||||
</Field>
|
||||
</TabsContent>
|
||||
<TabsContent value="ssh">
|
||||
<Field className="gap-2">
|
||||
<FieldLabel htmlFor="ssh-url" className="sr-only">
|
||||
SSH URL
|
||||
</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupAddon align="inline-end">
|
||||
<InputGroupButton variant="ghost" size="icon-xs">
|
||||
<IconPlaceholder
|
||||
lucide="CopyIcon"
|
||||
tabler="IconCopy"
|
||||
hugeicons="Copy01Icon"
|
||||
phosphor="CopyIcon"
|
||||
remixicon="RiFileCopyLine"
|
||||
/>
|
||||
</InputGroupButton>
|
||||
</InputGroupAddon>
|
||||
<InputGroupInput
|
||||
id="ssh-url"
|
||||
defaultValue="git@github.com:shadcn-ui/ui.git"
|
||||
readOnly
|
||||
/>
|
||||
</InputGroup>
|
||||
<FieldDescription>
|
||||
Use a password-protected SSH key.
|
||||
</FieldDescription>
|
||||
</Field>
|
||||
</TabsContent>
|
||||
<TabsContent value="cli">
|
||||
<Field className="gap-2">
|
||||
<FieldLabel htmlFor="cli-command" className="sr-only">
|
||||
CLI Command
|
||||
</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupAddon align="inline-end">
|
||||
<InputGroupButton variant="ghost" size="icon-xs">
|
||||
<IconPlaceholder
|
||||
lucide="CopyIcon"
|
||||
tabler="IconCopy"
|
||||
hugeicons="Copy01Icon"
|
||||
phosphor="CopyIcon"
|
||||
remixicon="RiFileCopyLine"
|
||||
/>
|
||||
</InputGroupButton>
|
||||
</InputGroupAddon>
|
||||
<InputGroupInput
|
||||
id="cli-command"
|
||||
defaultValue="gh repo clone shadcn-ui/ui"
|
||||
readOnly
|
||||
/>
|
||||
</InputGroup>
|
||||
<FieldDescription>
|
||||
Work fast with our official CLI.{" "}
|
||||
<a href="#learn-more">Learn more</a>
|
||||
</FieldDescription>
|
||||
</Field>
|
||||
</TabsContent>
|
||||
</div>
|
||||
</Tabs>
|
||||
<Separator className="-mx-2 my-2 w-auto!" />
|
||||
<div className="flex flex-col">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="justify-start gap-1.5"
|
||||
>
|
||||
<IconPlaceholder
|
||||
lucide="MonitorIcon"
|
||||
tabler="IconDeviceDesktop"
|
||||
hugeicons="ComputerIcon"
|
||||
phosphor="MonitorIcon"
|
||||
remixicon="RiComputerLine"
|
||||
data-icon="inline-start"
|
||||
/>
|
||||
Open with GitHub Desktop
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="justify-start gap-1.5"
|
||||
>
|
||||
<IconPlaceholder
|
||||
lucide="DownloadIcon"
|
||||
tabler="IconDownload"
|
||||
hugeicons="DownloadIcon"
|
||||
phosphor="DownloadIcon"
|
||||
remixicon="RiDownloadLine"
|
||||
data-icon="inline-start"
|
||||
/>
|
||||
Download ZIP
|
||||
</Button>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
"use client"
|
||||
|
||||
import { Button } from "@/registry/bases/base/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/base/ui/card"
|
||||
import { Checkbox } from "@/registry/bases/base/ui/checkbox"
|
||||
import {
|
||||
Field,
|
||||
FieldContent,
|
||||
FieldDescription,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
FieldLegend,
|
||||
FieldSet,
|
||||
} from "@/registry/bases/base/ui/field"
|
||||
|
||||
export function ContributionsActivity() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Contributions & Activity</CardTitle>
|
||||
<CardDescription>
|
||||
Manage your contributions and activity visibility.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form id="contributions-activity">
|
||||
<FieldGroup>
|
||||
<FieldSet>
|
||||
<FieldLegend className="sr-only">
|
||||
Contributions & activity
|
||||
</FieldLegend>
|
||||
<FieldGroup>
|
||||
<Field orientation="horizontal">
|
||||
<Checkbox id="activity-private-profile" />
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor="activity-private-profile">
|
||||
Make profile private and hide activity
|
||||
</FieldLabel>
|
||||
<FieldDescription>
|
||||
Enabling this will hide your contributions and activity
|
||||
from your GitHub profile and from social features like
|
||||
followers, stars, feeds, leaderboards and releases.
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</FieldSet>
|
||||
</FieldGroup>
|
||||
</form>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button form="contributions-activity" className="style-sera:w-full">
|
||||
Save Changes
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
"use client"
|
||||
|
||||
import {
|
||||
Avatar,
|
||||
AvatarFallback,
|
||||
AvatarImage,
|
||||
} from "@/registry/bases/base/ui/avatar"
|
||||
import { Badge } from "@/registry/bases/base/ui/badge"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/base/ui/card"
|
||||
|
||||
// GitHub usernames displayed as contributor avatars.
|
||||
const usernames = [
|
||||
"shadcn",
|
||||
"vercel",
|
||||
"nextjs",
|
||||
"tailwindlabs",
|
||||
"typescript-lang",
|
||||
"eslint",
|
||||
"prettier",
|
||||
"babel",
|
||||
"webpack",
|
||||
"rollup",
|
||||
"parcel",
|
||||
"vite",
|
||||
"react",
|
||||
"vue",
|
||||
"angular",
|
||||
"solid",
|
||||
]
|
||||
|
||||
export function Contributors() {
|
||||
return (
|
||||
<Card className="max-w-sm">
|
||||
<CardHeader>
|
||||
<CardTitle>
|
||||
Contributors <Badge variant="secondary">312</Badge>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{usernames.map((username) => (
|
||||
<Avatar key={username} className="grayscale">
|
||||
<AvatarImage
|
||||
src={`https://github.com/${username}.png`}
|
||||
alt={username}
|
||||
/>
|
||||
<AvatarFallback>{username.charAt(0)}</AvatarFallback>
|
||||
</Avatar>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<a href="#" className="text-sm underline underline-offset-4">
|
||||
+ 810 contributors
|
||||
</a>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
"use client"
|
||||
|
||||
import { Button } from "@/registry/bases/base/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/base/ui/card"
|
||||
|
||||
export function EnvironmentVariables() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Environment Variables</CardTitle>
|
||||
<CardDescription>Production · 8 variables</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-2">
|
||||
{[
|
||||
{ key: "DATABASE_URL", masked: true },
|
||||
{ key: "NEXT_PUBLIC_API", masked: false },
|
||||
{ key: "STRIPE_SECRET", masked: true },
|
||||
].map((env) => (
|
||||
<div
|
||||
key={env.key}
|
||||
className="flex items-center gap-2 rounded-md px-2.5 py-2 font-mono text-xs ring ring-border"
|
||||
>
|
||||
<span className="font-medium">{env.key}</span>
|
||||
<span className="ml-auto text-muted-foreground">
|
||||
{env.masked ? "••••••••" : "https://api.example.com"}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button variant="outline">Edit</Button>
|
||||
<Button className="ml-auto">Deploy</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
"use client"
|
||||
|
||||
import { Button } from "@/registry/bases/base/ui/button"
|
||||
import { Card, CardContent, CardFooter } from "@/registry/bases/base/ui/card"
|
||||
import { Field, FieldGroup, FieldLabel } from "@/registry/bases/base/ui/field"
|
||||
import {
|
||||
NativeSelect,
|
||||
NativeSelectOption,
|
||||
} from "@/registry/bases/base/ui/native-select"
|
||||
import { Textarea } from "@/registry/bases/base/ui/textarea"
|
||||
|
||||
export function FeedbackForm() {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent>
|
||||
<form id="feedback-form">
|
||||
<FieldGroup>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="topic">Topic</FieldLabel>
|
||||
<NativeSelect id="topic">
|
||||
<NativeSelectOption value="">Select a topic</NativeSelectOption>
|
||||
<NativeSelectOption value="ai">AI</NativeSelectOption>
|
||||
<NativeSelectOption value="accounts-and-access-controls">
|
||||
Accounts and Access Controls
|
||||
</NativeSelectOption>
|
||||
<NativeSelectOption value="billing">Billing</NativeSelectOption>
|
||||
<NativeSelectOption value="cdn">
|
||||
CDN (Firewall, Caching)
|
||||
</NativeSelectOption>
|
||||
<NativeSelectOption value="ci-cd">
|
||||
CI/CD (Builds, Deployments, Environment Variables)
|
||||
</NativeSelectOption>
|
||||
<NativeSelectOption value="dashboard-interface">
|
||||
Dashboard Interface (Navigation, UI Issues)
|
||||
</NativeSelectOption>
|
||||
<NativeSelectOption value="domains">Domains</NativeSelectOption>
|
||||
<NativeSelectOption value="frameworks">
|
||||
Frameworks
|
||||
</NativeSelectOption>
|
||||
<NativeSelectOption value="marketplace-and-integrations">
|
||||
Marketplace and Integrations
|
||||
</NativeSelectOption>
|
||||
<NativeSelectOption value="observability">
|
||||
Observability (Observability, Logs, Monitoring)
|
||||
</NativeSelectOption>
|
||||
<NativeSelectOption value="storage">Storage</NativeSelectOption>
|
||||
</NativeSelect>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="feedback">Feedback</FieldLabel>
|
||||
<Textarea
|
||||
id="feedback"
|
||||
placeholder="Your feedback helps us improve..."
|
||||
/>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</form>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button
|
||||
type="submit"
|
||||
form="feedback-form"
|
||||
className="style-sera:w-full"
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
"use client"
|
||||
|
||||
import { Button } from "@/registry/bases/base/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/base/ui/card"
|
||||
import {
|
||||
Empty,
|
||||
EmptyContent,
|
||||
EmptyDescription,
|
||||
EmptyHeader,
|
||||
EmptyMedia,
|
||||
EmptyTitle,
|
||||
} from "@/registry/bases/base/ui/empty"
|
||||
import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"
|
||||
|
||||
export function FileUpload() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>File Upload</CardTitle>
|
||||
<CardDescription>Drag and drop or browse</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Empty className="border">
|
||||
<EmptyHeader>
|
||||
<EmptyMedia variant="icon">
|
||||
<IconPlaceholder
|
||||
lucide="UploadCloudIcon"
|
||||
tabler="IconCloudUpload"
|
||||
hugeicons="CloudUploadIcon"
|
||||
phosphor="CloudArrowUpIcon"
|
||||
remixicon="RiUploadCloudLine"
|
||||
/>
|
||||
</EmptyMedia>
|
||||
<EmptyTitle>Upload files</EmptyTitle>
|
||||
<EmptyDescription>PNG, JPG, PDF up to 10MB</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
<EmptyContent>
|
||||
<Button>Browse Files</Button>
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
"use client"
|
||||
|
||||
import { Button } from "@/registry/bases/base/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/base/ui/card"
|
||||
import {
|
||||
Field,
|
||||
FieldDescription,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
} from "@/registry/bases/base/ui/field"
|
||||
import { Input } from "@/registry/bases/base/ui/input"
|
||||
import {
|
||||
NativeSelect,
|
||||
NativeSelectOption,
|
||||
} from "@/registry/bases/base/ui/native-select"
|
||||
import { Textarea } from "@/registry/bases/base/ui/textarea"
|
||||
|
||||
export function GithubProfile() {
|
||||
return (
|
||||
<Card className="mx-auto w-full max-w-md">
|
||||
<CardHeader>
|
||||
<CardTitle>Profile</CardTitle>
|
||||
<CardDescription>Manage your profile information.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form id="profile">
|
||||
<FieldGroup>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="name">Name</FieldLabel>
|
||||
<Input id="name" placeholder="shadcn" />
|
||||
<FieldDescription>
|
||||
Your name may appear around GitHub where you contribute or are
|
||||
mentioned. You can remove it at any time.
|
||||
</FieldDescription>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="email">Public Email</FieldLabel>
|
||||
<NativeSelect id="email">
|
||||
<NativeSelectOption value="m@shadcn.com">
|
||||
m@shadcn.com
|
||||
</NativeSelectOption>
|
||||
<NativeSelectOption value="m@gmail.com">
|
||||
m@gmail.com
|
||||
</NativeSelectOption>
|
||||
</NativeSelect>
|
||||
<FieldDescription>
|
||||
You can manage verified email addresses in your{" "}
|
||||
<a href="#email-settings">email settings</a>.
|
||||
</FieldDescription>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="bio">Bio</FieldLabel>
|
||||
<Textarea
|
||||
id="bio"
|
||||
placeholder="Tell us a little bit about yourself"
|
||||
/>
|
||||
<FieldDescription>
|
||||
You can <span>@mention</span> other users and organizations to
|
||||
link to them.
|
||||
</FieldDescription>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</form>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button form="profile" className="style-sera:w-full">
|
||||
Save Profile
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
"use client"
|
||||
|
||||
import { type IconLibraryName } from "shadcn/icons"
|
||||
|
||||
import { Card, CardContent } from "@/registry/bases/base/ui/card"
|
||||
import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"
|
||||
|
||||
const PREVIEW_ICONS = [
|
||||
{
|
||||
lucide: "CopyIcon",
|
||||
tabler: "IconCopy",
|
||||
hugeicons: "Copy01Icon",
|
||||
phosphor: "CopyIcon",
|
||||
remixicon: "RiFileCopyLine",
|
||||
},
|
||||
{
|
||||
lucide: "CircleAlertIcon",
|
||||
tabler: "IconExclamationCircle",
|
||||
hugeicons: "AlertCircleIcon",
|
||||
phosphor: "WarningCircleIcon",
|
||||
remixicon: "RiErrorWarningLine",
|
||||
},
|
||||
{
|
||||
lucide: "TrashIcon",
|
||||
tabler: "IconTrash",
|
||||
hugeicons: "Delete02Icon",
|
||||
phosphor: "TrashIcon",
|
||||
remixicon: "RiDeleteBinLine",
|
||||
},
|
||||
{
|
||||
lucide: "ShareIcon",
|
||||
tabler: "IconShare",
|
||||
hugeicons: "Share03Icon",
|
||||
phosphor: "ShareIcon",
|
||||
remixicon: "RiShareLine",
|
||||
},
|
||||
{
|
||||
lucide: "ShoppingBagIcon",
|
||||
tabler: "IconShoppingBag",
|
||||
hugeicons: "ShoppingBag01Icon",
|
||||
phosphor: "BagIcon",
|
||||
remixicon: "RiShoppingBagLine",
|
||||
},
|
||||
{
|
||||
lucide: "MoreHorizontalIcon",
|
||||
tabler: "IconDots",
|
||||
hugeicons: "MoreHorizontalCircle01Icon",
|
||||
phosphor: "DotsThreeIcon",
|
||||
remixicon: "RiMoreLine",
|
||||
},
|
||||
{
|
||||
lucide: "Loader2Icon",
|
||||
tabler: "IconLoader",
|
||||
hugeicons: "Loading03Icon",
|
||||
phosphor: "SpinnerIcon",
|
||||
remixicon: "RiLoaderLine",
|
||||
},
|
||||
{
|
||||
lucide: "PlusIcon",
|
||||
tabler: "IconPlus",
|
||||
hugeicons: "PlusSignIcon",
|
||||
phosphor: "PlusIcon",
|
||||
remixicon: "RiAddLine",
|
||||
},
|
||||
{
|
||||
lucide: "MinusIcon",
|
||||
tabler: "IconMinus",
|
||||
hugeicons: "MinusSignIcon",
|
||||
phosphor: "MinusIcon",
|
||||
remixicon: "RiSubtractLine",
|
||||
},
|
||||
{
|
||||
lucide: "ArrowLeftIcon",
|
||||
tabler: "IconArrowLeft",
|
||||
hugeicons: "ArrowLeft02Icon",
|
||||
phosphor: "ArrowLeftIcon",
|
||||
remixicon: "RiArrowLeftLine",
|
||||
},
|
||||
{
|
||||
lucide: "ArrowRightIcon",
|
||||
tabler: "IconArrowRight",
|
||||
hugeicons: "ArrowRight02Icon",
|
||||
phosphor: "ArrowRightIcon",
|
||||
remixicon: "RiArrowRightLine",
|
||||
},
|
||||
{
|
||||
lucide: "CheckIcon",
|
||||
tabler: "IconCheck",
|
||||
hugeicons: "Tick02Icon",
|
||||
phosphor: "CheckIcon",
|
||||
remixicon: "RiCheckLine",
|
||||
},
|
||||
{
|
||||
lucide: "ChevronDownIcon",
|
||||
tabler: "IconChevronDown",
|
||||
hugeicons: "ArrowDown01Icon",
|
||||
phosphor: "CaretDownIcon",
|
||||
remixicon: "RiArrowDownSLine",
|
||||
},
|
||||
{
|
||||
lucide: "ChevronRightIcon",
|
||||
tabler: "IconChevronRight",
|
||||
hugeicons: "ArrowRight01Icon",
|
||||
phosphor: "CaretRightIcon",
|
||||
remixicon: "RiArrowRightSLine",
|
||||
},
|
||||
{
|
||||
lucide: "SearchIcon",
|
||||
tabler: "IconSearch",
|
||||
hugeicons: "Search01Icon",
|
||||
phosphor: "MagnifyingGlassIcon",
|
||||
remixicon: "RiSearchLine",
|
||||
},
|
||||
{
|
||||
lucide: "SettingsIcon",
|
||||
tabler: "IconSettings",
|
||||
hugeicons: "Settings01Icon",
|
||||
phosphor: "GearIcon",
|
||||
remixicon: "RiSettingsLine",
|
||||
},
|
||||
] satisfies Record<IconLibraryName, string>[]
|
||||
|
||||
export function IconPreviewGrid() {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-8 place-items-center gap-4">
|
||||
{PREVIEW_ICONS.map((icon, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex size-8 items-center justify-center rounded-md ring ring-border style-sera:rounded-none *:[svg]:size-4"
|
||||
>
|
||||
<IconPlaceholder
|
||||
lucide={icon.lucide}
|
||||
tabler={icon.tabler}
|
||||
hugeicons={icon.hugeicons}
|
||||
phosphor={icon.phosphor}
|
||||
remixicon={icon.remixicon}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
107
apps/v4/registry/bases/base/blocks/preview/cards/invite-team.tsx
Normal file
107
apps/v4/registry/bases/base/blocks/preview/cards/invite-team.tsx
Normal file
@@ -0,0 +1,107 @@
|
||||
"use client"
|
||||
|
||||
import { Button } from "@/registry/bases/base/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/base/ui/card"
|
||||
import { Field, FieldLabel } from "@/registry/bases/base/ui/field"
|
||||
import { Input } from "@/registry/bases/base/ui/input"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupButton,
|
||||
InputGroupInput,
|
||||
} from "@/registry/bases/base/ui/input-group"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/registry/bases/base/ui/select"
|
||||
import { Separator } from "@/registry/bases/base/ui/separator"
|
||||
import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"
|
||||
|
||||
export function InviteTeam() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Invite Team</CardTitle>
|
||||
<CardDescription>Add members to your workspace</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-3">
|
||||
{[
|
||||
{ email: "alex@example.com", role: "Editor" },
|
||||
{ email: "sam@example.com", role: "Viewer" },
|
||||
].map((invite) => (
|
||||
<div key={invite.email} className="flex items-center gap-2">
|
||||
<Input defaultValue={invite.email} className="flex-1" />
|
||||
<Select
|
||||
items={[
|
||||
{ label: "Admin", value: "admin" },
|
||||
{ label: "Editor", value: "editor" },
|
||||
{ label: "Viewer", value: "viewer" },
|
||||
]}
|
||||
defaultValue={invite.role.toLowerCase()}
|
||||
>
|
||||
<SelectTrigger className="w-24">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent alignItemWithTrigger={false} align="end">
|
||||
<SelectGroup>
|
||||
<SelectItem value="admin">Admin</SelectItem>
|
||||
<SelectItem value="editor">Editor</SelectItem>
|
||||
<SelectItem value="viewer">Viewer</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<Button variant="outline">
|
||||
<IconPlaceholder
|
||||
lucide="PlusIcon"
|
||||
tabler="IconPlus"
|
||||
hugeicons="PlusSignIcon"
|
||||
phosphor="PlusIcon"
|
||||
remixicon="RiAddLine"
|
||||
data-icon="inline-start"
|
||||
/>
|
||||
Add another
|
||||
</Button>
|
||||
<Separator />
|
||||
<Field>
|
||||
<FieldLabel htmlFor="invite-link">Or share invite link</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupInput
|
||||
id="invite-link"
|
||||
defaultValue="https://app.co/invite/x8f2k"
|
||||
readOnly
|
||||
/>
|
||||
<InputGroupAddon align="inline-end">
|
||||
<InputGroupButton size="icon-xs" aria-label="Copy link">
|
||||
<IconPlaceholder
|
||||
lucide="CopyIcon"
|
||||
tabler="IconCopy"
|
||||
hugeicons="Copy01Icon"
|
||||
phosphor="CopyIcon"
|
||||
remixicon="RiFileCopyLine"
|
||||
/>
|
||||
</InputGroupButton>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</Field>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button className="w-full">Send Invites</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
115
apps/v4/registry/bases/base/blocks/preview/cards/invoice.tsx
Normal file
115
apps/v4/registry/bases/base/blocks/preview/cards/invoice.tsx
Normal file
@@ -0,0 +1,115 @@
|
||||
"use client"
|
||||
|
||||
import { Badge } from "@/registry/bases/base/ui/badge"
|
||||
import { Button } from "@/registry/bases/base/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardAction,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/base/ui/card"
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableFooter,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/registry/bases/base/ui/table"
|
||||
|
||||
const INVOICE_ITEMS = [
|
||||
{ item: "Design System License", qty: 1, unitPrice: 499 },
|
||||
{ item: "Priority Support", qty: 12, unitPrice: 99 },
|
||||
{ item: "Custom Components", qty: 3, unitPrice: 250 },
|
||||
] as const
|
||||
|
||||
const subtotal = INVOICE_ITEMS.reduce(
|
||||
(sum, row) => sum + row.qty * row.unitPrice,
|
||||
0
|
||||
)
|
||||
const tax = 0
|
||||
const totalDue = subtotal + tax
|
||||
|
||||
function formatCurrency(value: number) {
|
||||
return new Intl.NumberFormat("en-US", {
|
||||
style: "currency",
|
||||
currency: "USD",
|
||||
maximumFractionDigits: 2,
|
||||
}).format(value)
|
||||
}
|
||||
|
||||
export function Invoice() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Invoice #INV-2847</CardTitle>
|
||||
<CardDescription>Due March 30, 2026</CardDescription>
|
||||
<CardAction>
|
||||
<Badge variant="secondary">Pending</Badge>
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Item</TableHead>
|
||||
<TableHead className="text-right">Qty</TableHead>
|
||||
<TableHead className="text-right">Rate</TableHead>
|
||||
<TableHead className="text-right">Amount</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{INVOICE_ITEMS.map((row) => (
|
||||
<TableRow key={row.item}>
|
||||
<TableCell>{row.item}</TableCell>
|
||||
<TableCell className="text-right tabular-nums">
|
||||
{row.qty}
|
||||
</TableCell>
|
||||
<TableCell className="text-right tabular-nums">
|
||||
{formatCurrency(row.unitPrice)}
|
||||
</TableCell>
|
||||
<TableCell className="text-right tabular-nums">
|
||||
{formatCurrency(row.qty * row.unitPrice)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
<TableRow>
|
||||
<TableCell colSpan={3} className="text-right">
|
||||
Subtotal
|
||||
</TableCell>
|
||||
<TableCell className="text-right tabular-nums">
|
||||
{formatCurrency(subtotal)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell colSpan={3} className="text-right">
|
||||
Tax
|
||||
</TableCell>
|
||||
<TableCell className="text-right tabular-nums">$0.00</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell colSpan={3} className="text-right">
|
||||
Total Due
|
||||
</TableCell>
|
||||
<TableCell className="text-right tabular-nums">
|
||||
{formatCurrency(totalDue)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button variant="outline" size="sm">
|
||||
Download PDF
|
||||
</Button>
|
||||
<Button size="sm" className="ml-auto">
|
||||
Pay Now
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,556 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { useEffect, useRef, useState } from "react"
|
||||
import { cn } from "cn"
|
||||
|
||||
import { Button } from "@/registry/bases/base/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/base/ui/card"
|
||||
|
||||
const LiveWaveform = ({
|
||||
active = false,
|
||||
processing = false,
|
||||
deviceId,
|
||||
barWidth = 3,
|
||||
barGap = 1,
|
||||
barRadius = 1.5,
|
||||
barColor,
|
||||
fadeEdges = true,
|
||||
fadeWidth = 24,
|
||||
barHeight: baseBarHeight = 4,
|
||||
height = 64,
|
||||
sensitivity = 1,
|
||||
smoothingTimeConstant = 0.8,
|
||||
fftSize = 256,
|
||||
historySize = 60,
|
||||
updateRate = 30,
|
||||
mode = "static",
|
||||
onError,
|
||||
onStreamReady,
|
||||
onStreamEnd,
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement> & {
|
||||
active?: boolean
|
||||
processing?: boolean
|
||||
deviceId?: string
|
||||
barWidth?: number
|
||||
barHeight?: number
|
||||
barGap?: number
|
||||
barRadius?: number
|
||||
barColor?: string
|
||||
fadeEdges?: boolean
|
||||
fadeWidth?: number
|
||||
height?: string | number
|
||||
sensitivity?: number
|
||||
smoothingTimeConstant?: number
|
||||
fftSize?: number
|
||||
historySize?: number
|
||||
updateRate?: number
|
||||
mode?: "scrolling" | "static"
|
||||
onError?: (error: Error) => void
|
||||
onStreamReady?: (stream: MediaStream) => void
|
||||
onStreamEnd?: () => void
|
||||
}) => {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null)
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const historyRef = useRef<number[]>([])
|
||||
const analyserRef = useRef<AnalyserNode | null>(null)
|
||||
const audioContextRef = useRef<AudioContext | null>(null)
|
||||
const streamRef = useRef<MediaStream | null>(null)
|
||||
const animationRef = useRef<number>(0)
|
||||
const lastUpdateRef = useRef<number>(0)
|
||||
const processingAnimationRef = useRef<number | null>(null)
|
||||
const lastActiveDataRef = useRef<number[]>([])
|
||||
const transitionProgressRef = useRef(0)
|
||||
const staticBarsRef = useRef<number[]>([])
|
||||
const needsRedrawRef = useRef(true)
|
||||
const gradientCacheRef = useRef<CanvasGradient | null>(null)
|
||||
const lastWidthRef = useRef(0)
|
||||
const heightStyle = typeof height === "number" ? `${height}px` : height
|
||||
|
||||
// Handle canvas resizing.
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current
|
||||
const container = containerRef.current
|
||||
if (!canvas || !container) return
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
const rect = container.getBoundingClientRect()
|
||||
const dpr = window.devicePixelRatio || 1
|
||||
canvas.width = rect.width * dpr
|
||||
canvas.height = rect.height * dpr
|
||||
canvas.style.width = `${rect.width}px`
|
||||
canvas.style.height = `${rect.height}px`
|
||||
const ctx = canvas.getContext("2d")
|
||||
if (ctx) {
|
||||
ctx.scale(dpr, dpr)
|
||||
}
|
||||
gradientCacheRef.current = null
|
||||
lastWidthRef.current = rect.width
|
||||
needsRedrawRef.current = true
|
||||
})
|
||||
resizeObserver.observe(container)
|
||||
return () => resizeObserver.disconnect()
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (processing && !active) {
|
||||
let time = 0
|
||||
transitionProgressRef.current = 0
|
||||
const animateProcessing = () => {
|
||||
time += 0.03
|
||||
transitionProgressRef.current = Math.min(
|
||||
1,
|
||||
transitionProgressRef.current + 0.02
|
||||
)
|
||||
const processingData = []
|
||||
const barCount = Math.floor(
|
||||
(containerRef.current?.getBoundingClientRect().width || 200) /
|
||||
(barWidth + barGap)
|
||||
)
|
||||
if (mode === "static") {
|
||||
const halfCount = Math.floor(barCount / 2)
|
||||
for (let i = 0; i < barCount; i++) {
|
||||
const normalizedPosition = (i - halfCount) / halfCount
|
||||
const centerWeight = 1 - Math.abs(normalizedPosition) * 0.4
|
||||
const wave1 = Math.sin(time * 1.5 + normalizedPosition * 3) * 0.25
|
||||
const wave2 = Math.sin(time * 0.8 - normalizedPosition * 2) * 0.2
|
||||
const wave3 = Math.cos(time * 2 + normalizedPosition) * 0.15
|
||||
const combinedWave = wave1 + wave2 + wave3
|
||||
const processingValue = (0.2 + combinedWave) * centerWeight
|
||||
let finalValue = processingValue
|
||||
if (
|
||||
lastActiveDataRef.current.length > 0 &&
|
||||
transitionProgressRef.current < 1
|
||||
) {
|
||||
const lastDataIndex = Math.min(
|
||||
i,
|
||||
lastActiveDataRef.current.length - 1
|
||||
)
|
||||
const lastValue = lastActiveDataRef.current[lastDataIndex] || 0
|
||||
finalValue =
|
||||
lastValue * (1 - transitionProgressRef.current) +
|
||||
processingValue * transitionProgressRef.current
|
||||
}
|
||||
processingData.push(Math.max(0.05, Math.min(1, finalValue)))
|
||||
}
|
||||
} else {
|
||||
for (let i = 0; i < barCount; i++) {
|
||||
const normalizedPosition = (i - barCount / 2) / (barCount / 2)
|
||||
const centerWeight = 1 - Math.abs(normalizedPosition) * 0.4
|
||||
const wave1 = Math.sin(time * 1.5 + i * 0.15) * 0.25
|
||||
const wave2 = Math.sin(time * 0.8 - i * 0.1) * 0.2
|
||||
const wave3 = Math.cos(time * 2 + i * 0.05) * 0.15
|
||||
const combinedWave = wave1 + wave2 + wave3
|
||||
const processingValue = (0.2 + combinedWave) * centerWeight
|
||||
let finalValue = processingValue
|
||||
if (
|
||||
lastActiveDataRef.current.length > 0 &&
|
||||
transitionProgressRef.current < 1
|
||||
) {
|
||||
const lastDataIndex = Math.floor(
|
||||
(i / barCount) * lastActiveDataRef.current.length
|
||||
)
|
||||
const lastValue = lastActiveDataRef.current[lastDataIndex] || 0
|
||||
finalValue =
|
||||
lastValue * (1 - transitionProgressRef.current) +
|
||||
processingValue * transitionProgressRef.current
|
||||
}
|
||||
processingData.push(Math.max(0.05, Math.min(1, finalValue)))
|
||||
}
|
||||
}
|
||||
if (mode === "static") {
|
||||
staticBarsRef.current = processingData
|
||||
} else {
|
||||
historyRef.current = processingData
|
||||
}
|
||||
needsRedrawRef.current = true
|
||||
processingAnimationRef.current =
|
||||
requestAnimationFrame(animateProcessing)
|
||||
}
|
||||
animateProcessing()
|
||||
return () => {
|
||||
if (processingAnimationRef.current) {
|
||||
cancelAnimationFrame(processingAnimationRef.current)
|
||||
}
|
||||
}
|
||||
} else if (!active && !processing) {
|
||||
const hasData =
|
||||
mode === "static"
|
||||
? staticBarsRef.current.length > 0
|
||||
: historyRef.current.length > 0
|
||||
if (hasData) {
|
||||
let fadeProgress = 0
|
||||
const fadeToIdle = () => {
|
||||
fadeProgress += 0.03
|
||||
if (fadeProgress < 1) {
|
||||
if (mode === "static") {
|
||||
staticBarsRef.current = staticBarsRef.current.map(
|
||||
(value) => value * (1 - fadeProgress)
|
||||
)
|
||||
} else {
|
||||
historyRef.current = historyRef.current.map(
|
||||
(value) => value * (1 - fadeProgress)
|
||||
)
|
||||
}
|
||||
needsRedrawRef.current = true
|
||||
requestAnimationFrame(fadeToIdle)
|
||||
} else {
|
||||
if (mode === "static") {
|
||||
staticBarsRef.current = []
|
||||
} else {
|
||||
historyRef.current = []
|
||||
}
|
||||
}
|
||||
}
|
||||
fadeToIdle()
|
||||
}
|
||||
}
|
||||
}, [processing, active, barWidth, barGap, mode])
|
||||
|
||||
// Handle microphone setup and teardown.
|
||||
useEffect(() => {
|
||||
if (!active) {
|
||||
if (streamRef.current) {
|
||||
streamRef.current.getTracks().forEach((track) => track.stop())
|
||||
streamRef.current = null
|
||||
onStreamEnd?.()
|
||||
}
|
||||
if (
|
||||
audioContextRef.current &&
|
||||
audioContextRef.current.state !== "closed"
|
||||
) {
|
||||
audioContextRef.current.close()
|
||||
audioContextRef.current = null
|
||||
}
|
||||
if (animationRef.current) {
|
||||
cancelAnimationFrame(animationRef.current)
|
||||
animationRef.current = 0
|
||||
}
|
||||
return
|
||||
}
|
||||
const setupMicrophone = async () => {
|
||||
try {
|
||||
const stream = await navigator.mediaDevices.getUserMedia({
|
||||
audio: deviceId
|
||||
? {
|
||||
deviceId: { exact: deviceId },
|
||||
echoCancellation: true,
|
||||
noiseSuppression: true,
|
||||
autoGainControl: true,
|
||||
}
|
||||
: {
|
||||
echoCancellation: true,
|
||||
noiseSuppression: true,
|
||||
autoGainControl: true,
|
||||
},
|
||||
})
|
||||
streamRef.current = stream
|
||||
onStreamReady?.(stream)
|
||||
const AudioContextConstructor =
|
||||
window.AudioContext ||
|
||||
(window as unknown as { webkitAudioContext: typeof AudioContext })
|
||||
.webkitAudioContext
|
||||
const audioContext = new AudioContextConstructor()
|
||||
const analyser = audioContext.createAnalyser()
|
||||
analyser.fftSize = fftSize
|
||||
analyser.smoothingTimeConstant = smoothingTimeConstant
|
||||
const source = audioContext.createMediaStreamSource(stream)
|
||||
source.connect(analyser)
|
||||
audioContextRef.current = audioContext
|
||||
analyserRef.current = analyser
|
||||
historyRef.current = []
|
||||
} catch (error) {
|
||||
onError?.(error as Error)
|
||||
}
|
||||
}
|
||||
setupMicrophone()
|
||||
return () => {
|
||||
if (streamRef.current) {
|
||||
streamRef.current.getTracks().forEach((track) => track.stop())
|
||||
streamRef.current = null
|
||||
onStreamEnd?.()
|
||||
}
|
||||
if (
|
||||
audioContextRef.current &&
|
||||
audioContextRef.current.state !== "closed"
|
||||
) {
|
||||
audioContextRef.current.close()
|
||||
audioContextRef.current = null
|
||||
}
|
||||
if (animationRef.current) {
|
||||
cancelAnimationFrame(animationRef.current)
|
||||
animationRef.current = 0
|
||||
}
|
||||
}
|
||||
}, [
|
||||
active,
|
||||
deviceId,
|
||||
fftSize,
|
||||
smoothingTimeConstant,
|
||||
onError,
|
||||
onStreamReady,
|
||||
onStreamEnd,
|
||||
])
|
||||
|
||||
// Animation loop.
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current
|
||||
if (!canvas) return
|
||||
const ctx = canvas.getContext("2d")
|
||||
if (!ctx) return
|
||||
let rafId: number
|
||||
const animate = (currentTime: number) => {
|
||||
const rect = canvas.getBoundingClientRect()
|
||||
if (active && currentTime - lastUpdateRef.current > updateRate) {
|
||||
lastUpdateRef.current = currentTime
|
||||
if (analyserRef.current) {
|
||||
const dataArray = new Uint8Array(
|
||||
analyserRef.current.frequencyBinCount
|
||||
)
|
||||
analyserRef.current.getByteFrequencyData(dataArray)
|
||||
if (mode === "static") {
|
||||
const startFreq = Math.floor(dataArray.length * 0.05)
|
||||
const endFreq = Math.floor(dataArray.length * 0.4)
|
||||
const relevantData = dataArray.slice(startFreq, endFreq)
|
||||
const barCount = Math.floor(rect.width / (barWidth + barGap))
|
||||
const halfCount = Math.floor(barCount / 2)
|
||||
const newBars: number[] = []
|
||||
for (let i = halfCount - 1; i >= 0; i--) {
|
||||
const dataIndex = Math.floor(
|
||||
(i / halfCount) * relevantData.length
|
||||
)
|
||||
const value = Math.min(
|
||||
1,
|
||||
(relevantData[dataIndex] / 255) * sensitivity
|
||||
)
|
||||
newBars.push(Math.max(0.05, value))
|
||||
}
|
||||
for (let i = 0; i < halfCount; i++) {
|
||||
const dataIndex = Math.floor(
|
||||
(i / halfCount) * relevantData.length
|
||||
)
|
||||
const value = Math.min(
|
||||
1,
|
||||
(relevantData[dataIndex] / 255) * sensitivity
|
||||
)
|
||||
newBars.push(Math.max(0.05, value))
|
||||
}
|
||||
staticBarsRef.current = newBars
|
||||
lastActiveDataRef.current = newBars
|
||||
} else {
|
||||
let sum = 0
|
||||
const startFreq = Math.floor(dataArray.length * 0.05)
|
||||
const endFreq = Math.floor(dataArray.length * 0.4)
|
||||
const relevantData = dataArray.slice(startFreq, endFreq)
|
||||
for (let i = 0; i < relevantData.length; i++) {
|
||||
sum += relevantData[i]
|
||||
}
|
||||
const average = (sum / relevantData.length / 255) * sensitivity
|
||||
historyRef.current.push(Math.min(1, Math.max(0.05, average)))
|
||||
lastActiveDataRef.current = [...historyRef.current]
|
||||
if (historyRef.current.length > historySize) {
|
||||
historyRef.current.shift()
|
||||
}
|
||||
}
|
||||
needsRedrawRef.current = true
|
||||
}
|
||||
}
|
||||
if (!needsRedrawRef.current && !active) {
|
||||
rafId = requestAnimationFrame(animate)
|
||||
return
|
||||
}
|
||||
needsRedrawRef.current = active
|
||||
ctx.clearRect(0, 0, rect.width, rect.height)
|
||||
const computedBarColor =
|
||||
barColor ||
|
||||
(() => {
|
||||
const style = getComputedStyle(canvas)
|
||||
const color = style.color
|
||||
return color || "#000"
|
||||
})()
|
||||
const step = barWidth + barGap
|
||||
const barCount = Math.floor(rect.width / step)
|
||||
const centerY = rect.height / 2
|
||||
if (mode === "static") {
|
||||
const dataToRender = processing
|
||||
? staticBarsRef.current
|
||||
: active
|
||||
? staticBarsRef.current
|
||||
: staticBarsRef.current.length > 0
|
||||
? staticBarsRef.current
|
||||
: []
|
||||
for (let i = 0; i < barCount && i < dataToRender.length; i++) {
|
||||
const value = dataToRender[i] || 0.1
|
||||
const x = i * step
|
||||
const bh = Math.max(baseBarHeight, value * rect.height * 0.8)
|
||||
const y = centerY - bh / 2
|
||||
ctx.fillStyle = computedBarColor
|
||||
ctx.globalAlpha = 0.4 + value * 0.6
|
||||
if (barRadius > 0) {
|
||||
ctx.beginPath()
|
||||
ctx.roundRect(x, y, barWidth, bh, barRadius)
|
||||
ctx.fill()
|
||||
} else {
|
||||
ctx.fillRect(x, y, barWidth, bh)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (let i = 0; i < barCount && i < historyRef.current.length; i++) {
|
||||
const dataIndex = historyRef.current.length - 1 - i
|
||||
const value = historyRef.current[dataIndex] || 0.1
|
||||
const x = rect.width - (i + 1) * step
|
||||
const bh = Math.max(baseBarHeight, value * rect.height * 0.8)
|
||||
const y = centerY - bh / 2
|
||||
ctx.fillStyle = computedBarColor
|
||||
ctx.globalAlpha = 0.4 + value * 0.6
|
||||
if (barRadius > 0) {
|
||||
ctx.beginPath()
|
||||
ctx.roundRect(x, y, barWidth, bh, barRadius)
|
||||
ctx.fill()
|
||||
} else {
|
||||
ctx.fillRect(x, y, barWidth, bh)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (fadeEdges && fadeWidth > 0 && rect.width > 0) {
|
||||
if (!gradientCacheRef.current || lastWidthRef.current !== rect.width) {
|
||||
const gradient = ctx.createLinearGradient(0, 0, rect.width, 0)
|
||||
const fadePercent = Math.min(0.3, fadeWidth / rect.width)
|
||||
gradient.addColorStop(0, "rgba(255,255,255,1)")
|
||||
gradient.addColorStop(fadePercent, "rgba(255,255,255,0)")
|
||||
gradient.addColorStop(1 - fadePercent, "rgba(255,255,255,0)")
|
||||
gradient.addColorStop(1, "rgba(255,255,255,1)")
|
||||
gradientCacheRef.current = gradient
|
||||
lastWidthRef.current = rect.width
|
||||
}
|
||||
ctx.globalCompositeOperation = "destination-out"
|
||||
ctx.fillStyle = gradientCacheRef.current
|
||||
ctx.fillRect(0, 0, rect.width, rect.height)
|
||||
ctx.globalCompositeOperation = "source-over"
|
||||
}
|
||||
ctx.globalAlpha = 1
|
||||
rafId = requestAnimationFrame(animate)
|
||||
}
|
||||
rafId = requestAnimationFrame(animate)
|
||||
return () => {
|
||||
if (rafId) {
|
||||
cancelAnimationFrame(rafId)
|
||||
}
|
||||
}
|
||||
}, [
|
||||
active,
|
||||
processing,
|
||||
sensitivity,
|
||||
updateRate,
|
||||
historySize,
|
||||
barWidth,
|
||||
baseBarHeight,
|
||||
barGap,
|
||||
barRadius,
|
||||
barColor,
|
||||
fadeEdges,
|
||||
fadeWidth,
|
||||
mode,
|
||||
])
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn("relative h-full w-full", className)}
|
||||
ref={containerRef}
|
||||
style={{ height: heightStyle }}
|
||||
aria-label={
|
||||
active
|
||||
? "Live audio waveform"
|
||||
: processing
|
||||
? "Processing audio"
|
||||
: "Audio waveform idle"
|
||||
}
|
||||
role="img"
|
||||
{...props}
|
||||
>
|
||||
{!active && !processing && (
|
||||
<div className="absolute top-1/2 right-0 left-0 -translate-y-1/2 border-t-2 border-dotted border-muted-foreground/20" />
|
||||
)}
|
||||
<canvas
|
||||
className="block h-full w-full"
|
||||
ref={canvasRef}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function LiveWaveformCard() {
|
||||
const [active, setActive] = useState(false)
|
||||
const [processing, setProcessing] = useState(true)
|
||||
const [mode, setMode] = useState<"static" | "scrolling">("static")
|
||||
|
||||
const handleToggleActive = () => {
|
||||
setActive(!active)
|
||||
if (!active) {
|
||||
setProcessing(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleToggleProcessing = () => {
|
||||
setProcessing(!processing)
|
||||
if (!processing) {
|
||||
setActive(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Live Audio Waveform</CardTitle>
|
||||
<CardDescription>
|
||||
Real-time microphone input visualization with audio reactivity
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<LiveWaveform
|
||||
active={active}
|
||||
processing={processing}
|
||||
height={80}
|
||||
barWidth={3}
|
||||
barGap={2}
|
||||
mode={mode}
|
||||
fadeEdges={true}
|
||||
barColor="gray"
|
||||
historySize={120}
|
||||
/>
|
||||
</CardContent>
|
||||
<CardFooter className="gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant={active ? "default" : "outline"}
|
||||
onClick={handleToggleActive}
|
||||
>
|
||||
{active ? "Stop" : "Start"} Listening
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant={processing ? "default" : "outline"}
|
||||
onClick={handleToggleProcessing}
|
||||
>
|
||||
{processing ? "Stop" : "Start"} Processing
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => setMode(mode === "static" ? "scrolling" : "static")}
|
||||
>
|
||||
{mode === "static" ? "Static" : "Scrolling"}
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
"use client"
|
||||
|
||||
import {
|
||||
Avatar,
|
||||
AvatarFallback,
|
||||
AvatarGroup,
|
||||
AvatarImage,
|
||||
} from "@/registry/bases/base/ui/avatar"
|
||||
import { Button } from "@/registry/bases/base/ui/button"
|
||||
import { Card, CardContent } from "@/registry/bases/base/ui/card"
|
||||
import {
|
||||
Empty,
|
||||
EmptyContent,
|
||||
EmptyDescription,
|
||||
EmptyHeader,
|
||||
EmptyMedia,
|
||||
EmptyTitle,
|
||||
} from "@/registry/bases/base/ui/empty"
|
||||
|
||||
export function NoTeamMembers() {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Empty className="h-56 border">
|
||||
<EmptyHeader>
|
||||
<EmptyMedia>
|
||||
<AvatarGroup className="grayscale">
|
||||
<Avatar size="lg">
|
||||
<AvatarImage
|
||||
src="https://github.com/shadcn.png"
|
||||
alt="@shadcn"
|
||||
/>
|
||||
<AvatarFallback>CN</AvatarFallback>
|
||||
</Avatar>
|
||||
<Avatar size="lg">
|
||||
<AvatarImage
|
||||
src="https://github.com/maxleiter.png"
|
||||
alt="@maxleiter"
|
||||
/>
|
||||
<AvatarFallback>LR</AvatarFallback>
|
||||
</Avatar>
|
||||
<Avatar size="lg">
|
||||
<AvatarImage
|
||||
src="https://github.com/evilrabbit.png"
|
||||
alt="@evilrabbit"
|
||||
/>
|
||||
<AvatarFallback>ER</AvatarFallback>
|
||||
</Avatar>
|
||||
</AvatarGroup>
|
||||
</EmptyMedia>
|
||||
<EmptyTitle>No Team Members</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
Invite your team to collaborate on this project.
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
<EmptyContent>
|
||||
<Button size="sm">Invite Members</Button>
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
"use client"
|
||||
|
||||
import { Button } from "@/registry/bases/base/ui/button"
|
||||
import { Card, CardContent } from "@/registry/bases/base/ui/card"
|
||||
import {
|
||||
Empty,
|
||||
EmptyContent,
|
||||
EmptyDescription,
|
||||
EmptyHeader,
|
||||
EmptyTitle,
|
||||
} from "@/registry/bases/base/ui/empty"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupInput,
|
||||
} from "@/registry/bases/base/ui/input-group"
|
||||
import { Kbd } from "@/registry/bases/base/ui/kbd"
|
||||
import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"
|
||||
|
||||
export function NotFound() {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Empty className="h-72">
|
||||
<EmptyHeader>
|
||||
<EmptyTitle>404 - Not Found</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
The page you're looking for doesn't exist. Try searching
|
||||
for what you need below.
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
<EmptyContent>
|
||||
<InputGroup className="w-3/4">
|
||||
<InputGroupInput placeholder="Try searching for pages..." />
|
||||
<InputGroupAddon>
|
||||
<IconPlaceholder
|
||||
lucide="SearchIcon"
|
||||
tabler="IconSearch"
|
||||
hugeicons="Search01Icon"
|
||||
phosphor="MagnifyingGlassIcon"
|
||||
remixicon="RiSearchLine"
|
||||
/>
|
||||
</InputGroupAddon>
|
||||
<InputGroupAddon align="inline-end">
|
||||
<Kbd>/</Kbd>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
<Button variant="link">Go to homepage</Button>
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
"use client"
|
||||
|
||||
import { Badge } from "@/registry/bases/base/ui/badge"
|
||||
import { Button } from "@/registry/bases/base/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/base/ui/card"
|
||||
import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"
|
||||
|
||||
export function ObservabilityCard() {
|
||||
return (
|
||||
<Card className="relative w-full max-w-md overflow-hidden pt-0">
|
||||
<div className="absolute inset-0 z-30 aspect-video bg-primary opacity-50 mix-blend-color" />
|
||||
<img
|
||||
src="https://images.unsplash.com/photo-1604076850742-4c7221f3101b?q=80&w=1887&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
|
||||
alt="Photo by mymind on Unsplash"
|
||||
title="Photo by mymind on Unsplash"
|
||||
className="relative z-20 aspect-video w-full object-cover brightness-60 grayscale"
|
||||
/>
|
||||
<CardHeader>
|
||||
<CardTitle>Observability Plus is replacing Monitoring</CardTitle>
|
||||
<CardDescription>
|
||||
Switch to the improved way to explore your data, with natural
|
||||
language. Monitoring will no longer be available on the Pro plan in
|
||||
November, 2025
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardFooter>
|
||||
<Button>
|
||||
Create Query{" "}
|
||||
<IconPlaceholder
|
||||
lucide="PlusIcon"
|
||||
tabler="IconPlus"
|
||||
hugeicons="PlusSignIcon"
|
||||
phosphor="PlusIcon"
|
||||
remixicon="RiAddLine"
|
||||
data-icon="inline-end"
|
||||
/>
|
||||
</Button>
|
||||
<Badge variant="secondary" className="ml-auto">
|
||||
Warning
|
||||
</Badge>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
"use client"
|
||||
|
||||
import { Label, Pie, PieChart } from "recharts"
|
||||
|
||||
import { Badge } from "@/registry/bases/base/ui/badge"
|
||||
import {
|
||||
Card,
|
||||
CardAction,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/base/ui/card"
|
||||
import {
|
||||
ChartContainer,
|
||||
ChartLegend,
|
||||
ChartLegendContent,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
type ChartConfig,
|
||||
} from "@/registry/bases/base/ui/chart"
|
||||
import { Progress } from "@/registry/bases/base/ui/progress"
|
||||
|
||||
const pieChartData = [
|
||||
{ browser: "chrome", visitors: 275, fill: "var(--color-chrome)" },
|
||||
{ browser: "safari", visitors: 200, fill: "var(--color-safari)" },
|
||||
{ browser: "firefox", visitors: 287, fill: "var(--color-firefox)" },
|
||||
{ browser: "edge", visitors: 173, fill: "var(--color-edge)" },
|
||||
]
|
||||
|
||||
const pieChartConfig = {
|
||||
visitors: {
|
||||
label: "Visitors",
|
||||
},
|
||||
chrome: {
|
||||
label: "Chrome",
|
||||
color: "var(--chart-1)",
|
||||
},
|
||||
safari: {
|
||||
label: "Safari",
|
||||
color: "var(--chart-2)",
|
||||
},
|
||||
firefox: {
|
||||
label: "Firefox",
|
||||
color: "var(--chart-3)",
|
||||
},
|
||||
edge: {
|
||||
label: "Edge",
|
||||
color: "var(--chart-4)",
|
||||
},
|
||||
} satisfies ChartConfig
|
||||
|
||||
export function PieChartCard() {
|
||||
const totalVisitors = pieChartData.reduce(
|
||||
(sum, item) => sum + item.visitors,
|
||||
0
|
||||
)
|
||||
const topBrowser = pieChartData.reduce((max, item) =>
|
||||
item.visitors > max.visitors ? item : max
|
||||
)
|
||||
const topBrowserShare = Math.round(
|
||||
(topBrowser.visitors / totalVisitors) * 100
|
||||
)
|
||||
const topBrowserLabel =
|
||||
pieChartConfig[topBrowser.browser as keyof typeof pieChartConfig]?.label ??
|
||||
"Top"
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="pb-0">
|
||||
<CardTitle>Browser Share</CardTitle>
|
||||
<CardDescription>January - June 2026</CardDescription>
|
||||
<CardAction>
|
||||
<Badge variant="outline">{topBrowserLabel}</Badge>
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-0">
|
||||
<ChartContainer
|
||||
config={pieChartConfig}
|
||||
className="mx-auto aspect-square max-h-[190px]"
|
||||
>
|
||||
<PieChart>
|
||||
<ChartTooltip
|
||||
cursor={false}
|
||||
content={<ChartTooltipContent hideLabel />}
|
||||
/>
|
||||
<Pie
|
||||
data={pieChartData}
|
||||
dataKey="visitors"
|
||||
nameKey="browser"
|
||||
innerRadius={50}
|
||||
strokeWidth={5}
|
||||
>
|
||||
<Label
|
||||
content={({ viewBox }) => {
|
||||
if (viewBox && "cx" in viewBox && "cy" in viewBox) {
|
||||
return (
|
||||
<text
|
||||
x={viewBox.cx}
|
||||
y={viewBox.cy}
|
||||
textAnchor="middle"
|
||||
dominantBaseline="middle"
|
||||
fill="red"
|
||||
>
|
||||
<tspan
|
||||
x={viewBox.cx}
|
||||
y={viewBox.cy - 16}
|
||||
className="fill-foreground text-2xl font-bold"
|
||||
>
|
||||
{totalVisitors.toLocaleString()}
|
||||
</tspan>
|
||||
<tspan
|
||||
x={viewBox.cx}
|
||||
y={(viewBox.cy || 0) + 4}
|
||||
className="fill-muted-foreground text-xs"
|
||||
>
|
||||
Visitors
|
||||
</tspan>
|
||||
</text>
|
||||
)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Pie>
|
||||
<ChartLegend
|
||||
content={<ChartLegendContent nameKey="browser" />}
|
||||
className="translate-y-2"
|
||||
/>
|
||||
</PieChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
<CardFooter className="flex-col items-stretch gap-2">
|
||||
<div className="flex items-center text-xs">
|
||||
<span className="font-medium">{topBrowserLabel}</span>
|
||||
<span className="ml-auto text-muted-foreground tabular-nums">
|
||||
{topBrowserShare}%
|
||||
</span>
|
||||
</div>
|
||||
<Progress
|
||||
value={topBrowserShare}
|
||||
className="**:data-[slot=progress-indicator]:bg-chart-3"
|
||||
/>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
113
apps/v4/registry/bases/base/blocks/preview/cards/report-bug.tsx
Normal file
113
apps/v4/registry/bases/base/blocks/preview/cards/report-bug.tsx
Normal file
@@ -0,0 +1,113 @@
|
||||
"use client"
|
||||
|
||||
import { Button } from "@/registry/bases/base/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/base/ui/card"
|
||||
import { Field, FieldGroup, FieldLabel } from "@/registry/bases/base/ui/field"
|
||||
import { Input } from "@/registry/bases/base/ui/input"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/registry/bases/base/ui/select"
|
||||
import { Textarea } from "@/registry/bases/base/ui/textarea"
|
||||
|
||||
export function ReportBug() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Report Bug</CardTitle>
|
||||
<CardDescription>Help us fix issues faster.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<FieldGroup>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="bug-title">Title</FieldLabel>
|
||||
<Input
|
||||
id="bug-title"
|
||||
placeholder="Brief description of the issue"
|
||||
/>
|
||||
</Field>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<Field>
|
||||
<FieldLabel htmlFor="bug-severity">Severity</FieldLabel>
|
||||
<Select
|
||||
items={[
|
||||
{ label: "Critical", value: "critical" },
|
||||
{ label: "High", value: "high" },
|
||||
{ label: "Medium", value: "medium" },
|
||||
{ label: "Low", value: "low" },
|
||||
]}
|
||||
defaultValue="medium"
|
||||
>
|
||||
<SelectTrigger id="bug-severity" className="w-full">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem value="critical">Critical</SelectItem>
|
||||
<SelectItem value="high">High</SelectItem>
|
||||
<SelectItem value="medium">Medium</SelectItem>
|
||||
<SelectItem value="low">Low</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="bug-component">Component</FieldLabel>
|
||||
<Select
|
||||
items={[
|
||||
{ label: "Dashboard", value: "dashboard" },
|
||||
{ label: "Auth", value: "auth" },
|
||||
{ label: "API", value: "api" },
|
||||
{ label: "Billing", value: "billing" },
|
||||
]}
|
||||
defaultValue="dashboard"
|
||||
>
|
||||
<SelectTrigger id="bug-component" className="w-full">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem value="dashboard">Dashboard</SelectItem>
|
||||
<SelectItem value="auth">Auth</SelectItem>
|
||||
<SelectItem value="api">API</SelectItem>
|
||||
<SelectItem value="billing">Billing</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
</div>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="bug-steps">Steps to reproduce</FieldLabel>
|
||||
<Textarea
|
||||
id="bug-steps"
|
||||
placeholder="1. Go to 2. Click on 3. Observe..."
|
||||
className="min-h-24 resize-none"
|
||||
/>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Field
|
||||
orientation="horizontal"
|
||||
className="justify-end style-sera:justify-center"
|
||||
>
|
||||
<Button variant="outline" className="style-sera:flex-1">
|
||||
Attach File
|
||||
</Button>
|
||||
<Button className="style-sera:flex-1">Submit Bug</Button>
|
||||
</Field>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
"use client"
|
||||
|
||||
import { Button } from "@/registry/bases/base/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/base/ui/card"
|
||||
import { Checkbox } from "@/registry/bases/base/ui/checkbox"
|
||||
import { Field, FieldGroup, FieldLabel } from "@/registry/bases/base/ui/field"
|
||||
import { Input } from "@/registry/bases/base/ui/input"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/registry/bases/base/ui/select"
|
||||
|
||||
export function ShippingAddress() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Shipping Address</CardTitle>
|
||||
<CardDescription>Where should we deliver?</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<FieldGroup>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="shipping-street">Street address</FieldLabel>
|
||||
<Input id="shipping-street" placeholder="123 Main Street" />
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="shipping-apt">Apt / Suite</FieldLabel>
|
||||
<Input id="shipping-apt" placeholder="Apt 4B" />
|
||||
</Field>
|
||||
<FieldGroup className="grid grid-cols-2">
|
||||
<Field>
|
||||
<FieldLabel htmlFor="shipping-city">City</FieldLabel>
|
||||
<Input id="shipping-city" placeholder="San Francisco" />
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="shipping-state">State</FieldLabel>
|
||||
<Select
|
||||
items={[
|
||||
{ label: "California", value: "CA" },
|
||||
{ label: "New York", value: "NY" },
|
||||
{ label: "Texas", value: "TX" },
|
||||
]}
|
||||
defaultValue="CA"
|
||||
>
|
||||
<SelectTrigger id="shipping-state" className="w-full">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem value="CA">California</SelectItem>
|
||||
<SelectItem value="NY">New York</SelectItem>
|
||||
<SelectItem value="TX">Texas</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
<FieldGroup className="grid grid-cols-2">
|
||||
<Field>
|
||||
<FieldLabel htmlFor="shipping-zip">ZIP Code</FieldLabel>
|
||||
<Input id="shipping-zip" placeholder="94102" />
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="shipping-country">Country</FieldLabel>
|
||||
<Select
|
||||
items={[
|
||||
{ label: "United States", value: "US" },
|
||||
{ label: "Canada", value: "CA" },
|
||||
{ label: "United Kingdom", value: "UK" },
|
||||
]}
|
||||
defaultValue="US"
|
||||
>
|
||||
<SelectTrigger id="shipping-country" className="w-full">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem value="US">United States</SelectItem>
|
||||
<SelectItem value="CA">Canada</SelectItem>
|
||||
<SelectItem value="UK">United Kingdom</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
<Field orientation="horizontal">
|
||||
<Checkbox id="shipping-save" defaultChecked />
|
||||
<FieldLabel htmlFor="shipping-save" className="font-normal">
|
||||
Save as default address
|
||||
</FieldLabel>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button variant="outline" size="sm">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button size="sm" className="ml-auto">
|
||||
Save Address
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { Card, CardContent } from "@/registry/bases/base/ui/card"
|
||||
import {
|
||||
Item,
|
||||
ItemActions,
|
||||
ItemGroup,
|
||||
ItemHeader,
|
||||
ItemSeparator,
|
||||
ItemTitle,
|
||||
} from "@/registry/bases/base/ui/item"
|
||||
import { Kbd } from "@/registry/bases/base/ui/kbd"
|
||||
|
||||
const shortcuts = [
|
||||
{ label: "Search", keys: ["⌘", "K"] },
|
||||
{ label: "Quick Actions", keys: ["⌘", "J"] },
|
||||
{ label: "New File", keys: ["⌘", "N"] },
|
||||
{ label: "Save", keys: ["⌘", "S"] },
|
||||
{ label: "Toggle Sidebar", keys: ["⌘", "B"] },
|
||||
] as const
|
||||
|
||||
export function Shortcuts() {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent>
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="text-sm font-medium">Shortcuts</div>
|
||||
<ItemGroup className="gap-2 text-muted-foreground" data-size="xs">
|
||||
{shortcuts.map(({ label, keys }, i) => (
|
||||
<React.Fragment key={label}>
|
||||
{i > 0 && <ItemSeparator />}
|
||||
<Item
|
||||
variant="default"
|
||||
size="xs"
|
||||
className="border-0 px-0 py-0"
|
||||
>
|
||||
<ItemHeader>
|
||||
<ItemTitle className="font-normal">{label}</ItemTitle>
|
||||
<ItemActions>
|
||||
<div className="flex gap-1">
|
||||
{keys.map((key) => (
|
||||
<Kbd key={key}>{key}</Kbd>
|
||||
))}
|
||||
</div>
|
||||
</ItemActions>
|
||||
</ItemHeader>
|
||||
</Item>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</ItemGroup>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
"use client"
|
||||
|
||||
import { Card, CardContent } from "@/registry/bases/base/ui/card"
|
||||
import { Skeleton } from "@/registry/bases/base/ui/skeleton"
|
||||
|
||||
export function SkeletonLoading() {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<Skeleton className="size-10 rounded-full" />
|
||||
<div className="flex flex-1 flex-col gap-2">
|
||||
<Skeleton className="h-4 w-3/4" />
|
||||
<Skeleton className="h-3 w-1/2" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Skeleton className="h-3 w-full" />
|
||||
<Skeleton className="h-3 w-full" />
|
||||
<Skeleton className="h-3 w-4/5" />
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Skeleton className="h-8 w-20" />
|
||||
<Skeleton className="h-8 w-20" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
"use client"
|
||||
|
||||
import { Bar, BarChart } from "recharts"
|
||||
|
||||
import { Badge } from "@/registry/bases/base/ui/badge"
|
||||
import { Button } from "@/registry/bases/base/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/base/ui/card"
|
||||
import {
|
||||
ChartContainer,
|
||||
type ChartConfig,
|
||||
} from "@/registry/bases/base/ui/chart"
|
||||
|
||||
const sleepChartData = [
|
||||
{ hour: "10pm", deep: 0, light: 30, rem: 0 },
|
||||
{ hour: "11pm", deep: 20, light: 10, rem: 0 },
|
||||
{ hour: "12am", deep: 40, light: 0, rem: 10 },
|
||||
{ hour: "1am", deep: 30, light: 5, rem: 15 },
|
||||
{ hour: "2am", deep: 10, light: 20, rem: 30 },
|
||||
{ hour: "3am", deep: 25, light: 10, rem: 20 },
|
||||
{ hour: "4am", deep: 15, light: 25, rem: 10 },
|
||||
{ hour: "5am", deep: 5, light: 35, rem: 15 },
|
||||
{ hour: "6am", deep: 0, light: 20, rem: 25 },
|
||||
]
|
||||
|
||||
const sleepChartConfig = {
|
||||
deep: {
|
||||
label: "Deep",
|
||||
color: "var(--chart-1)",
|
||||
},
|
||||
light: {
|
||||
label: "Light",
|
||||
color: "var(--chart-2)",
|
||||
},
|
||||
rem: {
|
||||
label: "REM",
|
||||
color: "var(--chart-3)",
|
||||
},
|
||||
} satisfies ChartConfig
|
||||
|
||||
export function SleepReport() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Sleep Report</CardTitle>
|
||||
<CardDescription>Last night · 7h 24m</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-3">
|
||||
<ChartContainer config={sleepChartConfig} className="h-32 w-full">
|
||||
<BarChart
|
||||
accessibilityLayer
|
||||
data={sleepChartData}
|
||||
margin={{ left: 0, right: 0, top: 0, bottom: 0 }}
|
||||
barSize={16}
|
||||
>
|
||||
<Bar
|
||||
dataKey="deep"
|
||||
stackId="a"
|
||||
fill="var(--color-deep)"
|
||||
radius={0}
|
||||
/>
|
||||
<Bar
|
||||
dataKey="light"
|
||||
stackId="a"
|
||||
fill="var(--color-light)"
|
||||
radius={0}
|
||||
/>
|
||||
<Bar
|
||||
dataKey="rem"
|
||||
stackId="a"
|
||||
fill="var(--color-rem)"
|
||||
radius={[2, 2, 0, 0]}
|
||||
/>
|
||||
</BarChart>
|
||||
</ChartContainer>
|
||||
<div className="grid grid-cols-4 gap-2">
|
||||
{[
|
||||
{ label: "Deep", value: "2h 10m" },
|
||||
{ label: "Light", value: "3h 48m" },
|
||||
{ label: "REM", value: "1h 26m" },
|
||||
{ label: "Score", value: "84" },
|
||||
].map((s) => (
|
||||
<div key={s.label} className="text-center">
|
||||
<div className="text-sm font-medium tabular-nums">{s.value}</div>
|
||||
<div className="text-xs text-muted-foreground">{s.label}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Badge variant="outline">Good</Badge>
|
||||
<Button variant="outline" size="sm" className="ml-auto">
|
||||
Details
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
|
||||
import { Card, CardContent } from "@/registry/bases/base/ui/card"
|
||||
import { STYLES } from "@/registry/styles"
|
||||
import { FONTS } from "@/app/(create)/lib/fonts"
|
||||
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
||||
|
||||
export function StyleOverview() {
|
||||
const [params] = useDesignSystemSearchParams()
|
||||
|
||||
const currentFont = React.useMemo(
|
||||
() => FONTS.find((font) => font.value === params.font),
|
||||
[params.font]
|
||||
)
|
||||
|
||||
const currentFontHeading = React.useMemo(
|
||||
() => FONTS.find((font) => font.value === params.fontHeading),
|
||||
[params.fontHeading]
|
||||
)
|
||||
|
||||
const currentStyle = React.useMemo(
|
||||
() => STYLES.find((style) => style.name === params.style),
|
||||
[params.style]
|
||||
)
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="flex flex-col gap-6 style-lyra:gap-4 style-mira:gap-4">
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="cn-font-heading text-2xl font-medium style-lyra:text-lg style-mira:text-lg style-sera:text-lg style-sera:font-semibold style-sera:tracking-wide style-sera:uppercase">
|
||||
{currentStyle?.title} -{" "}
|
||||
{currentFontHeading?.name &&
|
||||
currentFontHeading.name !== currentFont?.name
|
||||
? currentFontHeading.name
|
||||
: currentFont?.name}
|
||||
</div>
|
||||
<div className="line-clamp-2 text-base text-muted-foreground style-lyra:text-sm style-mira:text-sm style-sera:text-sm style-sera:leading-relaxed">
|
||||
Designers love packing quirky glyphs into test phrases. This is a
|
||||
preview of the typography styles.
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-6 gap-3">
|
||||
{[
|
||||
"--background",
|
||||
"--foreground",
|
||||
"--primary",
|
||||
"--secondary",
|
||||
"--muted",
|
||||
"--accent",
|
||||
"--border",
|
||||
"--chart-1",
|
||||
"--chart-2",
|
||||
"--chart-3",
|
||||
"--chart-4",
|
||||
"--chart-5",
|
||||
].map((variant) => (
|
||||
<div
|
||||
key={variant}
|
||||
className="flex flex-col flex-wrap items-center gap-2"
|
||||
>
|
||||
<div
|
||||
className="relative aspect-square w-full rounded-lg bg-(--color) after:absolute after:inset-0 after:rounded-lg after:border after:border-border after:mix-blend-darken dark:after:mix-blend-lighten style-sera:rounded-none style-sera:after:rounded-none"
|
||||
style={
|
||||
{
|
||||
"--color": `var(${variant})`,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
/>
|
||||
<div className="hidden max-w-14 truncate font-mono text-[0.60rem] md:block style-lyra:max-w-10 style-mira:max-w-10">
|
||||
{variant}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
|
||||
import { Button } from "@/registry/bases/base/ui/button"
|
||||
import { Card, CardContent, CardFooter } from "@/registry/bases/base/ui/card"
|
||||
import {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/registry/bases/base/ui/dialog"
|
||||
import { Field, FieldGroup, FieldLabel } from "@/registry/bases/base/ui/field"
|
||||
import { Input } from "@/registry/bases/base/ui/input"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/registry/bases/base/ui/select"
|
||||
import { Textarea } from "@/registry/bases/base/ui/textarea"
|
||||
import { FONTS } from "@/app/(create)/lib/fonts"
|
||||
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
||||
|
||||
export function TypographySpecimen() {
|
||||
const [params] = useDesignSystemSearchParams()
|
||||
|
||||
const currentFont = React.useMemo(
|
||||
() => FONTS.find((font) => font.value === params.font),
|
||||
[params.font]
|
||||
)
|
||||
|
||||
const currentFontHeading = React.useMemo(
|
||||
() => FONTS.find((font) => font.value === params.fontHeading),
|
||||
[params.fontHeading]
|
||||
)
|
||||
const headingLabel =
|
||||
currentFontHeading?.name && currentFontHeading.name !== currentFont?.name
|
||||
? currentFontHeading.name
|
||||
: "Inherit"
|
||||
const bodyLabel = currentFont?.name ?? "Default"
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="flex flex-col gap-2">
|
||||
<div className="text-xs font-medium text-muted-foreground uppercase">
|
||||
{headingLabel} - {bodyLabel}
|
||||
</div>
|
||||
<p className="cn-font-heading text-2xl font-medium style-sera:text-lg style-sera:font-semibold style-sera:tracking-wider style-sera:uppercase">
|
||||
Designing with rhythm and hierarchy.
|
||||
</p>
|
||||
<p className="text-sm leading-relaxed text-muted-foreground">
|
||||
A strong body style keeps long-form content readable and balances the
|
||||
visual weight of headings.
|
||||
</p>
|
||||
<p className="text-sm leading-relaxed text-muted-foreground">
|
||||
Thoughtful spacing and cadence help paragraphs scan quickly without
|
||||
feeling dense.
|
||||
</p>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Dialog>
|
||||
<DialogTrigger
|
||||
render={<Button variant="outline" className="w-full" />}
|
||||
>
|
||||
Share Feedback
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Share Feedback</DialogTitle>
|
||||
<DialogDescription>
|
||||
Let us know how we can improve your experience.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<FieldGroup>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<Field>
|
||||
<FieldLabel htmlFor="feedback-name">Name</FieldLabel>
|
||||
<Input id="feedback-name" placeholder="Your name" />
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="feedback-email">Email</FieldLabel>
|
||||
<Input
|
||||
id="feedback-email"
|
||||
type="email"
|
||||
placeholder="you@example.com"
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="feedback-category">Category</FieldLabel>
|
||||
<Select
|
||||
items={[
|
||||
{ label: "General", value: "general" },
|
||||
{ label: "Bug Report", value: "bug" },
|
||||
{ label: "Feature Request", value: "feature" },
|
||||
{ label: "Improvement", value: "improvement" },
|
||||
]}
|
||||
defaultValue="general"
|
||||
>
|
||||
<SelectTrigger id="feedback-category" className="w-full">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem value="general">General</SelectItem>
|
||||
<SelectItem value="bug">Bug Report</SelectItem>
|
||||
<SelectItem value="feature">Feature Request</SelectItem>
|
||||
<SelectItem value="improvement">Improvement</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="feedback-message">Message</FieldLabel>
|
||||
<Textarea
|
||||
id="feedback-message"
|
||||
placeholder="Tell us what's on your mind..."
|
||||
className="min-h-24 resize-none"
|
||||
/>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
<DialogFooter>
|
||||
<DialogClose render={<Button variant="outline" />}>
|
||||
Cancel
|
||||
</DialogClose>
|
||||
<Button>Submit</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
202
apps/v4/registry/bases/base/blocks/preview/cards/ui-elements.tsx
Normal file
202
apps/v4/registry/bases/base/blocks/preview/cards/ui-elements.tsx
Normal file
@@ -0,0 +1,202 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from "@/registry/bases/base/ui/alert-dialog"
|
||||
import { Badge } from "@/registry/bases/base/ui/badge"
|
||||
import { Button } from "@/registry/bases/base/ui/button"
|
||||
import { ButtonGroup } from "@/registry/bases/base/ui/button-group"
|
||||
import { Card, CardContent } from "@/registry/bases/base/ui/card"
|
||||
import { Checkbox } from "@/registry/bases/base/ui/checkbox"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/registry/bases/base/ui/dropdown-menu"
|
||||
import { Field, FieldGroup } from "@/registry/bases/base/ui/field"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupInput,
|
||||
InputGroupText,
|
||||
} from "@/registry/bases/base/ui/input-group"
|
||||
import {
|
||||
Item,
|
||||
ItemActions,
|
||||
ItemContent,
|
||||
ItemDescription,
|
||||
ItemTitle,
|
||||
} from "@/registry/bases/base/ui/item"
|
||||
import {
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
} from "@/registry/bases/base/ui/radio-group"
|
||||
import { Slider } from "@/registry/bases/base/ui/slider"
|
||||
import { Switch } from "@/registry/bases/base/ui/switch"
|
||||
import { Textarea } from "@/registry/bases/base/ui/textarea"
|
||||
import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"
|
||||
|
||||
export function UIElements() {
|
||||
const [sliderValue, setSliderValue] = React.useState<number[]>([500])
|
||||
const handleSliderValueChange = React.useCallback(
|
||||
(value: number | readonly number[]) => {
|
||||
if (typeof value === "number") {
|
||||
setSliderValue([value])
|
||||
} else {
|
||||
setSliderValue([...value])
|
||||
}
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
return (
|
||||
<Card className="w-full">
|
||||
<CardContent className="flex flex-col gap-6">
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button>Button</Button>
|
||||
<Button variant="secondary">Secondary</Button>
|
||||
<Button variant="outline">Outline</Button>
|
||||
<Button variant="ghost">Ghost</Button>
|
||||
</div>
|
||||
<Item variant="outline">
|
||||
<ItemContent>
|
||||
<ItemTitle>Two-factor authentication</ItemTitle>
|
||||
<ItemDescription className="text-pretty xl:hidden 2xl:block">
|
||||
Verify via email or phone number.
|
||||
</ItemDescription>
|
||||
</ItemContent>
|
||||
<ItemActions className="hidden md:flex">
|
||||
<Button size="sm" variant="secondary">
|
||||
Enable
|
||||
</Button>
|
||||
</ItemActions>
|
||||
</Item>
|
||||
</div>
|
||||
<Slider
|
||||
value={sliderValue}
|
||||
onValueChange={handleSliderValueChange}
|
||||
max={1000}
|
||||
min={0}
|
||||
step={10}
|
||||
className="flex-1"
|
||||
aria-label="Slider"
|
||||
/>
|
||||
<FieldGroup>
|
||||
<Field>
|
||||
<InputGroup>
|
||||
<InputGroupInput placeholder="Name" />
|
||||
<InputGroupAddon align="inline-end">
|
||||
<InputGroupText>
|
||||
<IconPlaceholder
|
||||
lucide="SearchIcon"
|
||||
tabler="IconSearch"
|
||||
hugeicons="Search01Icon"
|
||||
phosphor="MagnifyingGlassIcon"
|
||||
remixicon="RiSearchLine"
|
||||
/>
|
||||
</InputGroupText>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</Field>
|
||||
<Field className="flex-1">
|
||||
<Textarea placeholder="Message" className="resize-none" />
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex gap-2">
|
||||
<Badge>Badge</Badge>
|
||||
<Badge variant="secondary">Secondary</Badge>
|
||||
<Badge variant="outline">Outline</Badge>
|
||||
</div>
|
||||
<RadioGroup defaultValue="apple" className="ml-auto flex w-fit gap-3">
|
||||
<RadioGroupItem value="apple" />
|
||||
<RadioGroupItem value="banana" />
|
||||
</RadioGroup>
|
||||
<div className="flex gap-3">
|
||||
<Checkbox defaultChecked />
|
||||
<Checkbox />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger render={<Button variant="outline" />}>
|
||||
<span className="hidden md:flex style-sera:md:hidden">
|
||||
Alert Dialog
|
||||
</span>
|
||||
<span className="flex md:hidden style-sera:md:flex">Dialog</span>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent size="sm">
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Allow accessory to connect?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Do you want to allow the USB accessory to connect to this
|
||||
device and your data?
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Don't allow</AlertDialogCancel>
|
||||
<AlertDialogAction>Allow</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
<ButtonGroup>
|
||||
<Button variant="outline">
|
||||
<span className="style-sera:hidden">Button Group</span>
|
||||
<span className="hidden style-sera:block">Group</span>
|
||||
</Button>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
render={<Button variant="outline" size="icon" />}
|
||||
>
|
||||
<IconPlaceholder
|
||||
lucide="ChevronUpIcon"
|
||||
tabler="IconChevronUp"
|
||||
hugeicons="ArrowUp01Icon"
|
||||
phosphor="CaretUpIcon"
|
||||
remixicon="RiArrowUpSLine"
|
||||
/>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" side="top" className="w-fit">
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuLabel>Quick Actions</DropdownMenuLabel>
|
||||
<DropdownMenuItem>Mute Conversation</DropdownMenuItem>
|
||||
<DropdownMenuItem>Mark as Read</DropdownMenuItem>
|
||||
<DropdownMenuItem>Block User</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuLabel>Conversation</DropdownMenuLabel>
|
||||
<DropdownMenuItem>Share Conversation</DropdownMenuItem>
|
||||
<DropdownMenuItem>Copy Conversation</DropdownMenuItem>
|
||||
<DropdownMenuItem>Report Conversation</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem variant="destructive">
|
||||
Delete Conversation
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</ButtonGroup>
|
||||
<Switch defaultChecked className="ml-auto" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
134
apps/v4/registry/bases/base/blocks/preview/cards/usage-card.tsx
Normal file
134
apps/v4/registry/bases/base/blocks/preview/cards/usage-card.tsx
Normal file
@@ -0,0 +1,134 @@
|
||||
"use client"
|
||||
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/base/ui/card"
|
||||
import {
|
||||
Item,
|
||||
ItemActions,
|
||||
ItemContent,
|
||||
ItemGroup,
|
||||
ItemMedia,
|
||||
ItemTitle,
|
||||
} from "@/registry/bases/base/ui/item"
|
||||
|
||||
// Usage items with percentage and value.
|
||||
const items = [
|
||||
{
|
||||
name: "Edge Requests",
|
||||
value: "$1.83K",
|
||||
percentage: 67.34,
|
||||
},
|
||||
{
|
||||
name: "Fast Data Transfer",
|
||||
percentage: 52.18,
|
||||
value: "$952.51",
|
||||
},
|
||||
{
|
||||
name: "Monitoring data points",
|
||||
percentage: 89.42,
|
||||
value: "$901.20",
|
||||
},
|
||||
{
|
||||
name: "Web Analytics Events",
|
||||
percentage: 45.67,
|
||||
value: "$603.71",
|
||||
},
|
||||
{
|
||||
name: "ISR Writes",
|
||||
percentage: 26.23,
|
||||
value: "524.52K / 2M",
|
||||
},
|
||||
{
|
||||
name: "Function Duration",
|
||||
percentage: 5.11,
|
||||
value: "5.11 GB Hrs / 1K GB Hrs",
|
||||
},
|
||||
]
|
||||
|
||||
// Circular gauge SVG component for displaying a percentage.
|
||||
function CircularGauge({ percentage }: { percentage: number }) {
|
||||
const normalizedPercentage = Math.min(Math.max(percentage, 0), 100)
|
||||
const circumference = 2 * Math.PI * 42.5
|
||||
const strokePercent = (normalizedPercentage / 100) * circumference
|
||||
|
||||
return (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="none"
|
||||
height="16"
|
||||
width="16"
|
||||
strokeWidth="2"
|
||||
viewBox="0 0 100 100"
|
||||
className="-rotate-90"
|
||||
>
|
||||
<circle
|
||||
cx="50"
|
||||
cy="50"
|
||||
r="42.5"
|
||||
strokeWidth="12"
|
||||
strokeDashoffset="0"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="opacity-20"
|
||||
stroke="currentColor"
|
||||
style={{
|
||||
strokeDasharray: `${circumference} ${circumference}`,
|
||||
}}
|
||||
/>
|
||||
<circle
|
||||
cx="50"
|
||||
cy="50"
|
||||
r="42.5"
|
||||
strokeWidth="12"
|
||||
strokeDashoffset="0"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
stroke="currentColor"
|
||||
className="transition-all duration-300"
|
||||
style={{
|
||||
strokeDasharray: `${strokePercent} ${circumference}`,
|
||||
}}
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function UsageCard() {
|
||||
return (
|
||||
<Card className="w-full max-w-sm gap-4">
|
||||
<CardHeader>
|
||||
<CardTitle className="px-1 text-sm">
|
||||
5 days remaining in cycle
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ItemGroup className="gap-0">
|
||||
{items.map((item) => (
|
||||
<Item
|
||||
key={item.name}
|
||||
size="xs"
|
||||
className="px-0 group-hover/item-group:bg-transparent"
|
||||
render={<a href="#" />}
|
||||
>
|
||||
<ItemMedia variant="icon" className="text-primary">
|
||||
<CircularGauge percentage={item.percentage} />
|
||||
</ItemMedia>
|
||||
<ItemContent className="inline-block truncate">
|
||||
<ItemTitle className="inline">{item.name}</ItemTitle>
|
||||
</ItemContent>
|
||||
<ItemActions>
|
||||
<span className="font-mono text-xs font-medium text-muted-foreground tabular-nums">
|
||||
{item.value}
|
||||
</span>
|
||||
</ItemActions>
|
||||
</Item>
|
||||
))}
|
||||
</ItemGroup>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
"use client"
|
||||
|
||||
import { Area, AreaChart, XAxis } from "recharts"
|
||||
|
||||
import { Badge } from "@/registry/bases/base/ui/badge"
|
||||
import {
|
||||
Card,
|
||||
CardAction,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/base/ui/card"
|
||||
import {
|
||||
ChartContainer,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
type ChartConfig,
|
||||
} from "@/registry/bases/base/ui/chart"
|
||||
|
||||
const areaChartData = [
|
||||
{ month: "January", desktop: 186 },
|
||||
{ month: "February", desktop: 305 },
|
||||
{ month: "March", desktop: 237 },
|
||||
{ month: "April", desktop: 73 },
|
||||
{ month: "May", desktop: 209 },
|
||||
{ month: "June", desktop: 214 },
|
||||
]
|
||||
|
||||
const areaChartConfig = {
|
||||
desktop: {
|
||||
label: "Desktop",
|
||||
color: "var(--chart-1)",
|
||||
},
|
||||
} satisfies ChartConfig
|
||||
|
||||
const latestVisitors = areaChartData[areaChartData.length - 1]?.desktop ?? 0
|
||||
const previousVisitors =
|
||||
areaChartData[areaChartData.length - 2]?.desktop ?? latestVisitors
|
||||
const trendPercent =
|
||||
previousVisitors === 0
|
||||
? 0
|
||||
: Math.round(((latestVisitors - previousVisitors) / previousVisitors) * 100)
|
||||
const trendPrefix = trendPercent > 0 ? "+" : ""
|
||||
|
||||
export function Visitors() {
|
||||
return (
|
||||
<Card className="pb-0">
|
||||
<CardHeader>
|
||||
<CardTitle>Visitors</CardTitle>
|
||||
<CardDescription>Last 6 months </CardDescription>
|
||||
<CardAction>
|
||||
<Badge variant={trendPercent >= 0 ? "secondary" : "destructive"}>
|
||||
{trendPrefix}
|
||||
{trendPercent}% vs last month
|
||||
</Badge>
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
<CardContent className="px-0">
|
||||
<ChartContainer config={areaChartConfig} className="h-48 w-full">
|
||||
<AreaChart
|
||||
accessibilityLayer
|
||||
data={areaChartData}
|
||||
margin={{ left: 0, right: 0, top: 6, bottom: 0 }}
|
||||
>
|
||||
<XAxis
|
||||
dataKey="month"
|
||||
tickLine={false}
|
||||
hide
|
||||
axisLine={false}
|
||||
tickMargin={6}
|
||||
tickFormatter={(value) => String(value).slice(0, 3)}
|
||||
/>
|
||||
<ChartTooltip
|
||||
cursor={false}
|
||||
content={<ChartTooltipContent indicator="line" />}
|
||||
/>
|
||||
<Area
|
||||
dataKey="desktop"
|
||||
type="natural"
|
||||
fill="var(--color-desktop)"
|
||||
fillOpacity={0.15}
|
||||
stroke="var(--color-desktop)"
|
||||
strokeWidth={2}
|
||||
/>
|
||||
</AreaChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
|
||||
import { Button } from "@/registry/bases/base/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/base/ui/card"
|
||||
|
||||
const FITNESS_WEEKLY_LOAD = [
|
||||
{ day: "M", load: 84 },
|
||||
{ day: "T", load: 52 },
|
||||
{ day: "W", load: 73 },
|
||||
{ day: "T", load: 66 },
|
||||
{ day: "F", load: 91 },
|
||||
{ day: "S", load: 48 },
|
||||
{ day: "S", load: 61 },
|
||||
]
|
||||
|
||||
export function WeeklyFitnessSummary() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Weekly Fitness Summary</CardTitle>
|
||||
<CardDescription>Calories and workout load by day</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
<div className="grid grid-cols-7 gap-1.5">
|
||||
{FITNESS_WEEKLY_LOAD.map((day, index) => (
|
||||
<div
|
||||
key={`${day.day}-${index}`}
|
||||
className="rounded-md p-1.5 text-center ring ring-border"
|
||||
>
|
||||
<div className="text-sm text-muted-foreground">{day.day}</div>
|
||||
<div className="relative mt-1 h-16 overflow-hidden rounded-sm bg-muted">
|
||||
<div
|
||||
className="absolute inset-x-0 bottom-0 rounded-sm bg-chart-3"
|
||||
style={
|
||||
{
|
||||
height: `${day.load}%`,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button className="w-full">View details</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
97
apps/v4/registry/bases/base/blocks/preview/index.tsx
Normal file
97
apps/v4/registry/bases/base/blocks/preview/index.tsx
Normal file
@@ -0,0 +1,97 @@
|
||||
"use client"
|
||||
|
||||
import { ActivateAgentDialog } from "@/registry/bases/base/blocks/preview/cards/activate-agent-dialog"
|
||||
import { AnalyticsCard } from "@/registry/bases/base/blocks/preview/cards/analytics-card"
|
||||
import { AnomalyAlert } from "@/registry/bases/base/blocks/preview/cards/anomaly-alert"
|
||||
import { BarChartCard } from "@/registry/bases/base/blocks/preview/cards/bar-chart-card"
|
||||
import { BookAppointment } from "@/registry/bases/base/blocks/preview/cards/book-appointment"
|
||||
import { CodespacesCard } from "@/registry/bases/base/blocks/preview/cards/codespaces-card"
|
||||
import { ContributionsActivity } from "@/registry/bases/base/blocks/preview/cards/contributions-activity"
|
||||
import { Contributors } from "@/registry/bases/base/blocks/preview/cards/contributors"
|
||||
import { EnvironmentVariables } from "@/registry/bases/base/blocks/preview/cards/environment-variables"
|
||||
import { FeedbackForm } from "@/registry/bases/base/blocks/preview/cards/feedback-form"
|
||||
import { FileUpload } from "@/registry/bases/base/blocks/preview/cards/file-upload"
|
||||
import { GithubProfile } from "@/registry/bases/base/blocks/preview/cards/github-profile"
|
||||
import { IconPreviewGrid } from "@/registry/bases/base/blocks/preview/cards/icon-preview-grid"
|
||||
import { InviteTeam } from "@/registry/bases/base/blocks/preview/cards/invite-team"
|
||||
import { Invoice } from "@/registry/bases/base/blocks/preview/cards/invoice"
|
||||
import { LiveWaveformCard } from "@/registry/bases/base/blocks/preview/cards/live-waveform"
|
||||
import { NoTeamMembers } from "@/registry/bases/base/blocks/preview/cards/no-team-members"
|
||||
import { NotFound } from "@/registry/bases/base/blocks/preview/cards/not-found"
|
||||
import { ObservabilityCard } from "@/registry/bases/base/blocks/preview/cards/observability-card"
|
||||
import { PieChartCard } from "@/registry/bases/base/blocks/preview/cards/pie-chart-card"
|
||||
import { ReportBug } from "@/registry/bases/base/blocks/preview/cards/report-bug"
|
||||
import { ShippingAddress } from "@/registry/bases/base/blocks/preview/cards/shipping-address"
|
||||
import { Shortcuts } from "@/registry/bases/base/blocks/preview/cards/shortcuts"
|
||||
import { SkeletonLoading } from "@/registry/bases/base/blocks/preview/cards/skeleton-loading"
|
||||
import { SleepReport } from "@/registry/bases/base/blocks/preview/cards/sleep-report"
|
||||
import { StyleOverview } from "@/registry/bases/base/blocks/preview/cards/style-overview"
|
||||
import { TypographySpecimen } from "@/registry/bases/base/blocks/preview/cards/typography-specimen"
|
||||
import { UIElements } from "@/registry/bases/base/blocks/preview/cards/ui-elements"
|
||||
import { UsageCard } from "@/registry/bases/base/blocks/preview/cards/usage-card"
|
||||
import { Visitors } from "@/registry/bases/base/blocks/preview/cards/visitors"
|
||||
import { WeeklyFitnessSummary } from "@/registry/bases/base/blocks/preview/cards/weekly-fitness-summary"
|
||||
|
||||
export default function PreviewExample() {
|
||||
return (
|
||||
<div className="overflow-x-auto overflow-y-hidden bg-muted contain-[paint] [--gap:--spacing(4)] 3xl:[--gap:--spacing(12)] md:[--gap:--spacing(10)] dark:bg-background style-lyra:md:[--gap:--spacing(6)] style-mira:md:[--gap:--spacing(6)]">
|
||||
<div className="flex w-full min-w-max justify-center">
|
||||
<div
|
||||
className="grid w-[2400px] grid-cols-7 items-start gap-(--gap) bg-muted p-(--gap) md:w-[3000px] dark:bg-background style-lyra:md:w-[2600px] style-mira:md:w-[2600px] *:[div]:gap-(--gap)"
|
||||
data-slot="capture-target"
|
||||
>
|
||||
<div className="flex flex-col p-px [contain-intrinsic-size:380px_1200px] [content-visibility:auto]">
|
||||
<StyleOverview />
|
||||
<TypographySpecimen />
|
||||
<div className="md:hidden">
|
||||
<UIElements />
|
||||
</div>
|
||||
<CodespacesCard />
|
||||
<Invoice />
|
||||
</div>
|
||||
<div className="flex flex-col p-px [contain-intrinsic-size:380px_1200px] [content-visibility:auto]">
|
||||
<IconPreviewGrid />
|
||||
<div className="hidden w-full md:flex">
|
||||
<UIElements />
|
||||
</div>
|
||||
<ObservabilityCard />
|
||||
<ShippingAddress />
|
||||
</div>
|
||||
<div className="flex flex-col p-px [contain-intrinsic-size:380px_1200px] [content-visibility:auto]">
|
||||
<EnvironmentVariables />
|
||||
<BarChartCard />
|
||||
<InviteTeam />
|
||||
<ActivateAgentDialog />
|
||||
</div>
|
||||
<div className="flex flex-col p-px [contain-intrinsic-size:380px_1200px] [content-visibility:auto]">
|
||||
<SkeletonLoading />
|
||||
<PieChartCard />
|
||||
<NoTeamMembers />
|
||||
<ReportBug />
|
||||
<Contributors />
|
||||
</div>
|
||||
<div className="flex flex-col p-px [contain-intrinsic-size:380px_1200px] [content-visibility:auto]">
|
||||
<FeedbackForm />
|
||||
<BookAppointment />
|
||||
<SleepReport />
|
||||
<GithubProfile />
|
||||
</div>
|
||||
<div className="flex flex-col p-px [contain-intrinsic-size:380px_1200px] [content-visibility:auto]">
|
||||
<WeeklyFitnessSummary />
|
||||
<FileUpload />
|
||||
<AnalyticsCard />
|
||||
<UsageCard />
|
||||
<Shortcuts />
|
||||
</div>
|
||||
<div className="flex flex-col p-px [contain-intrinsic-size:380px_1200px] [content-visibility:auto]">
|
||||
<AnomalyAlert />
|
||||
<LiveWaveformCard />
|
||||
<Visitors />
|
||||
<ContributionsActivity />
|
||||
<NotFound />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { SearchForm } from "@/registry/bases/base/blocks/sidebar-01/components/search-form"
|
||||
import { VersionSwitcher } from "@/registry/bases/base/blocks/sidebar-01/components/version-switcher"
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
SidebarGroup,
|
||||
SidebarGroupContent,
|
||||
SidebarGroupLabel,
|
||||
SidebarHeader,
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
SidebarRail,
|
||||
} from "@/registry/bases/base/ui/sidebar"
|
||||
|
||||
// This is sample data.
|
||||
const data = {
|
||||
versions: ["1.0.1", "1.1.0-alpha", "2.0.0-beta1"],
|
||||
navMain: [
|
||||
{
|
||||
title: "Getting Started",
|
||||
url: "#",
|
||||
items: [
|
||||
{
|
||||
title: "Installation",
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
title: "Project Structure",
|
||||
url: "#",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Build Your Application",
|
||||
url: "#",
|
||||
items: [
|
||||
{
|
||||
title: "Routing",
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
title: "Data Fetching",
|
||||
url: "#",
|
||||
isActive: true,
|
||||
},
|
||||
{
|
||||
title: "Rendering",
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
title: "Caching",
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
title: "Styling",
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
title: "Optimizing",
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
title: "Configuring",
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
title: "Testing",
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
title: "Authentication",
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
title: "Deploying",
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
title: "Upgrading",
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
title: "Examples",
|
||||
url: "#",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "API Reference",
|
||||
url: "#",
|
||||
items: [
|
||||
{
|
||||
title: "Components",
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
title: "File Conventions",
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
title: "Functions",
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
title: "next.config.js Options",
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
title: "CLI",
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
title: "Edge Runtime",
|
||||
url: "#",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Architecture",
|
||||
url: "#",
|
||||
items: [
|
||||
{
|
||||
title: "Accessibility",
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
title: "Fast Refresh",
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
title: "Next.js Compiler",
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
title: "Supported Browsers",
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
title: "Turbopack",
|
||||
url: "#",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||
return (
|
||||
<Sidebar {...props}>
|
||||
<SidebarHeader>
|
||||
<VersionSwitcher
|
||||
versions={data.versions}
|
||||
defaultVersion={data.versions[0]}
|
||||
/>
|
||||
<SearchForm />
|
||||
</SidebarHeader>
|
||||
<SidebarContent>
|
||||
{data.navMain.map((item) => (
|
||||
<SidebarGroup key={item.title}>
|
||||
<SidebarGroupLabel>{item.title}</SidebarGroupLabel>
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu>
|
||||
{item.items.map((item) => (
|
||||
<SidebarMenuItem key={item.title}>
|
||||
<SidebarMenuButton
|
||||
isActive={item.isActive}
|
||||
render={<a href={item.url} />}
|
||||
>
|
||||
{item.title}
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
))}
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
))}
|
||||
</SidebarContent>
|
||||
<SidebarRail />
|
||||
</Sidebar>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
"use client"
|
||||
|
||||
import { Label } from "@/registry/bases/base/ui/label"
|
||||
import {
|
||||
SidebarGroup,
|
||||
SidebarGroupContent,
|
||||
SidebarInput,
|
||||
} from "@/registry/bases/base/ui/sidebar"
|
||||
import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"
|
||||
|
||||
export function SearchForm({ ...props }: React.ComponentProps<"form">) {
|
||||
return (
|
||||
<form {...props}>
|
||||
<SidebarGroup className="py-0">
|
||||
<SidebarGroupContent className="relative">
|
||||
<Label htmlFor="search" className="sr-only">
|
||||
Search
|
||||
</Label>
|
||||
<SidebarInput
|
||||
id="search"
|
||||
placeholder="Search the docs..."
|
||||
className="pl-8"
|
||||
/>
|
||||
<IconPlaceholder
|
||||
lucide="SearchIcon"
|
||||
tabler="IconSearch"
|
||||
hugeicons="SearchIcon"
|
||||
phosphor="MagnifyingGlassIcon"
|
||||
remixicon="RiSearchLine"
|
||||
className="pointer-events-none absolute top-1/2 left-2 size-4 -translate-y-1/2 opacity-50 select-none"
|
||||
/>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/registry/bases/base/ui/dropdown-menu"
|
||||
import {
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
} from "@/registry/bases/base/ui/sidebar"
|
||||
import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"
|
||||
|
||||
export function VersionSwitcher({
|
||||
versions,
|
||||
defaultVersion,
|
||||
}: {
|
||||
versions: string[]
|
||||
defaultVersion: string
|
||||
}) {
|
||||
const [selectedVersion, setSelectedVersion] = React.useState(defaultVersion)
|
||||
return (
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
render={
|
||||
<SidebarMenuButton
|
||||
size="lg"
|
||||
className="data-open:bg-sidebar-accent data-open:text-sidebar-accent-foreground"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground">
|
||||
<IconPlaceholder
|
||||
lucide="GalleryVerticalEndIcon"
|
||||
tabler="IconLayoutRows"
|
||||
hugeicons="LayoutBottomIcon"
|
||||
phosphor="RowsIcon"
|
||||
remixicon="RiGalleryLine"
|
||||
className="size-4"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-0.5 leading-none">
|
||||
<span className="font-medium">Documentation</span>
|
||||
<span className="">v{selectedVersion}</span>
|
||||
</div>
|
||||
<IconPlaceholder
|
||||
lucide="ChevronsUpDownIcon"
|
||||
tabler="IconSelector"
|
||||
hugeicons="UnfoldMoreIcon"
|
||||
phosphor="CaretUpDownIcon"
|
||||
remixicon="RiArrowUpDownLine"
|
||||
className="ml-auto"
|
||||
/>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start">
|
||||
{versions.map((version) => (
|
||||
<DropdownMenuItem
|
||||
key={version}
|
||||
onSelect={() => setSelectedVersion(version)}
|
||||
>
|
||||
v{version}{" "}
|
||||
{version === selectedVersion && (
|
||||
<IconPlaceholder
|
||||
lucide="CheckIcon"
|
||||
tabler="IconCheck"
|
||||
hugeicons="Tick02Icon"
|
||||
phosphor="CheckIcon"
|
||||
remixicon="RiCheckLine"
|
||||
className="ml-auto"
|
||||
/>
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
)
|
||||
}
|
||||
51
apps/v4/registry/bases/base/blocks/sidebar-01/page.tsx
Normal file
51
apps/v4/registry/bases/base/blocks/sidebar-01/page.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import { AppSidebar } from "@/registry/bases/base/blocks/sidebar-01/components/app-sidebar"
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbList,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
} from "@/registry/bases/base/ui/breadcrumb"
|
||||
import { Separator } from "@/registry/bases/base/ui/separator"
|
||||
import {
|
||||
SidebarInset,
|
||||
SidebarProvider,
|
||||
SidebarTrigger,
|
||||
} from "@/registry/bases/base/ui/sidebar"
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<SidebarProvider>
|
||||
<AppSidebar />
|
||||
<SidebarInset>
|
||||
<header className="flex h-16 shrink-0 items-center gap-2 border-b px-4">
|
||||
<SidebarTrigger className="-ml-1" />
|
||||
<Separator
|
||||
orientation="vertical"
|
||||
className="mr-2 data-vertical:h-4 data-vertical:self-auto"
|
||||
/>
|
||||
<Breadcrumb>
|
||||
<BreadcrumbList>
|
||||
<BreadcrumbItem className="hidden md:block">
|
||||
<BreadcrumbLink href="#">Build Your Application</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator className="hidden md:block" />
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbPage>Data Fetching</BreadcrumbPage>
|
||||
</BreadcrumbItem>
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
</header>
|
||||
<div className="flex flex-1 flex-col gap-4 p-4">
|
||||
<div className="grid auto-rows-min gap-4 md:grid-cols-3">
|
||||
<div className="aspect-video rounded-xl bg-muted/50" />
|
||||
<div className="aspect-video rounded-xl bg-muted/50" />
|
||||
<div className="aspect-video rounded-xl bg-muted/50" />
|
||||
</div>
|
||||
<div className="min-h-screen flex-1 rounded-xl bg-muted/50 md:min-h-min" />
|
||||
</div>
|
||||
</SidebarInset>
|
||||
</SidebarProvider>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
|
||||
import { SearchForm } from "@/registry/bases/base/blocks/sidebar-02/components/search-form"
|
||||
import { VersionSwitcher } from "@/registry/bases/base/blocks/sidebar-02/components/version-switcher"
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from "@/registry/bases/base/ui/collapsible"
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
SidebarGroup,
|
||||
SidebarGroupContent,
|
||||
SidebarGroupLabel,
|
||||
SidebarHeader,
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
SidebarRail,
|
||||
} from "@/registry/bases/base/ui/sidebar"
|
||||
import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"
|
||||
|
||||
// This is sample data.
|
||||
const data = {
|
||||
versions: ["1.0.1", "1.1.0-alpha", "2.0.0-beta1"],
|
||||
navMain: [
|
||||
{
|
||||
title: "Getting Started",
|
||||
url: "#",
|
||||
items: [
|
||||
{
|
||||
title: "Installation",
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
title: "Project Structure",
|
||||
url: "#",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Build Your Application",
|
||||
url: "#",
|
||||
items: [
|
||||
{
|
||||
title: "Routing",
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
title: "Data Fetching",
|
||||
url: "#",
|
||||
isActive: true,
|
||||
},
|
||||
{
|
||||
title: "Rendering",
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
title: "Caching",
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
title: "Styling",
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
title: "Optimizing",
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
title: "Configuring",
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
title: "Testing",
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
title: "Authentication",
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
title: "Deploying",
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
title: "Upgrading",
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
title: "Examples",
|
||||
url: "#",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "API Reference",
|
||||
url: "#",
|
||||
items: [
|
||||
{
|
||||
title: "Components",
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
title: "File Conventions",
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
title: "Functions",
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
title: "next.config.js Options",
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
title: "CLI",
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
title: "Edge Runtime",
|
||||
url: "#",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Architecture",
|
||||
url: "#",
|
||||
items: [
|
||||
{
|
||||
title: "Accessibility",
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
title: "Fast Refresh",
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
title: "Next.js Compiler",
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
title: "Supported Browsers",
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
title: "Turbopack",
|
||||
url: "#",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Community",
|
||||
url: "#",
|
||||
items: [
|
||||
{
|
||||
title: "Contribution Guide",
|
||||
url: "#",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||
return (
|
||||
<Sidebar {...props}>
|
||||
<SidebarHeader>
|
||||
<VersionSwitcher
|
||||
versions={data.versions}
|
||||
defaultVersion={data.versions[0]}
|
||||
/>
|
||||
<SearchForm />
|
||||
</SidebarHeader>
|
||||
<SidebarContent className="gap-0">
|
||||
{/* We create a collapsible SidebarGroup for each parent. */}
|
||||
{data.navMain.map((item) => (
|
||||
<Collapsible
|
||||
key={item.title}
|
||||
title={item.title}
|
||||
defaultOpen
|
||||
className="group/collapsible"
|
||||
>
|
||||
<SidebarGroup>
|
||||
<SidebarGroupLabel
|
||||
className="group/label text-sm text-sidebar-foreground hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"
|
||||
render={<CollapsibleTrigger />}
|
||||
>
|
||||
{item.title}{" "}
|
||||
<IconPlaceholder
|
||||
lucide="ChevronRightIcon"
|
||||
tabler="IconChevronRight"
|
||||
hugeicons="ArrowRight01Icon"
|
||||
phosphor="CaretRightIcon"
|
||||
remixicon="RiArrowRightSLine"
|
||||
className="ml-auto transition-transform group-data-open/collapsible:rotate-90"
|
||||
/>
|
||||
</SidebarGroupLabel>
|
||||
<CollapsibleContent>
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu>
|
||||
{item.items.map((item) => (
|
||||
<SidebarMenuItem key={item.title}>
|
||||
<SidebarMenuButton
|
||||
isActive={item.isActive}
|
||||
render={<a href={item.url} />}
|
||||
>
|
||||
{item.title}
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
))}
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</CollapsibleContent>
|
||||
</SidebarGroup>
|
||||
</Collapsible>
|
||||
))}
|
||||
</SidebarContent>
|
||||
<SidebarRail />
|
||||
</Sidebar>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
"use client"
|
||||
|
||||
import { Label } from "@/registry/bases/base/ui/label"
|
||||
import {
|
||||
SidebarGroup,
|
||||
SidebarGroupContent,
|
||||
SidebarInput,
|
||||
} from "@/registry/bases/base/ui/sidebar"
|
||||
import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"
|
||||
|
||||
export function SearchForm({ ...props }: React.ComponentProps<"form">) {
|
||||
return (
|
||||
<form {...props}>
|
||||
<SidebarGroup className="py-0">
|
||||
<SidebarGroupContent className="relative">
|
||||
<Label htmlFor="search" className="sr-only">
|
||||
Search
|
||||
</Label>
|
||||
<SidebarInput
|
||||
id="search"
|
||||
placeholder="Search the docs..."
|
||||
className="pl-8"
|
||||
/>
|
||||
<IconPlaceholder
|
||||
lucide="SearchIcon"
|
||||
tabler="IconSearch"
|
||||
hugeicons="SearchIcon"
|
||||
phosphor="MagnifyingGlassIcon"
|
||||
remixicon="RiSearchLine"
|
||||
className="pointer-events-none absolute top-1/2 left-2 size-4 -translate-y-1/2 opacity-50 select-none"
|
||||
/>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/registry/bases/base/ui/dropdown-menu"
|
||||
import {
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
} from "@/registry/bases/base/ui/sidebar"
|
||||
import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"
|
||||
|
||||
export function VersionSwitcher({
|
||||
versions,
|
||||
defaultVersion,
|
||||
}: {
|
||||
versions: string[]
|
||||
defaultVersion: string
|
||||
}) {
|
||||
const [selectedVersion, setSelectedVersion] = React.useState(defaultVersion)
|
||||
return (
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
render={
|
||||
<SidebarMenuButton
|
||||
size="lg"
|
||||
className="data-open:bg-sidebar-accent data-open:text-sidebar-accent-foreground"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground">
|
||||
<IconPlaceholder
|
||||
lucide="GalleryVerticalEndIcon"
|
||||
tabler="IconLayoutRows"
|
||||
hugeicons="LayoutBottomIcon"
|
||||
phosphor="RowsIcon"
|
||||
remixicon="RiGalleryLine"
|
||||
className="size-4"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-0.5 leading-none">
|
||||
<span className="font-medium">Documentation</span>
|
||||
<span className="">v{selectedVersion}</span>
|
||||
</div>
|
||||
<IconPlaceholder
|
||||
lucide="ChevronsUpDownIcon"
|
||||
tabler="IconSelector"
|
||||
hugeicons="UnfoldMoreIcon"
|
||||
phosphor="CaretUpDownIcon"
|
||||
remixicon="RiArrowUpDownLine"
|
||||
className="ml-auto"
|
||||
/>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start">
|
||||
{versions.map((version) => (
|
||||
<DropdownMenuItem
|
||||
key={version}
|
||||
onSelect={() => setSelectedVersion(version)}
|
||||
>
|
||||
v{version}{" "}
|
||||
{version === selectedVersion && (
|
||||
<IconPlaceholder
|
||||
lucide="CheckIcon"
|
||||
tabler="IconCheck"
|
||||
hugeicons="Tick02Icon"
|
||||
phosphor="CheckIcon"
|
||||
remixicon="RiCheckLine"
|
||||
className="ml-auto"
|
||||
/>
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user