├── .editorconfig
├── .gitignore
├── .storybook
├── main.ts
├── preview.tsx
└── vitest.setup.ts
├── .vscode
├── launch.json
└── settings.json
├── README.md
├── components.json
├── eslint.config.mjs
├── next.config.ts
├── package.json
├── pnpm-lock.yaml
├── postcss.config.mjs
├── public
├── next.svg
├── placeholder.svg
└── vercel.svg
├── src
├── app
│ ├── globals.css
│ ├── layout.tsx
│ └── page.tsx
├── components
│ ├── property-card
│ │ ├── basic
│ │ │ ├── spread-props
│ │ │ │ ├── index.tsx
│ │ │ │ └── spread-props.stories.tsx
│ │ │ └── value-prop
│ │ │ │ ├── index.tsx
│ │ │ │ └── value-prop.stories.tsx
│ │ ├── children-array
│ │ │ ├── children-array.stories.tsx
│ │ │ └── index.tsx
│ │ ├── compound
│ │ │ ├── managed
│ │ │ │ ├── index.tsx
│ │ │ │ └── managed.stories.tsx
│ │ │ └── presentational
│ │ │ │ ├── index.tsx
│ │ │ │ └── presentational.stories.tsx
│ │ ├── container-presentational
│ │ │ ├── container
│ │ │ │ ├── container.stories.tsx
│ │ │ │ └── index.tsx
│ │ │ └── presentational
│ │ │ │ ├── index.tsx
│ │ │ │ └── presentational.stories.tsx
│ │ ├── controlled-uncontrolled
│ │ │ ├── controlled
│ │ │ │ ├── controlled.stories.tsx
│ │ │ │ └── index.tsx
│ │ │ └── uncontrolled
│ │ │ │ ├── index.tsx
│ │ │ │ └── uncontrolled.stories.tsx
│ │ └── render-props
│ │ │ ├── index.tsx
│ │ │ └── render-props.stories.tsx
│ └── ui
│ │ └── button.tsx
└── lib
│ └── utils.ts
├── tsconfig.json
├── vitest.config.ts
└── vitest.shims.d.ts
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | insert_final_newline = true
5 | charset = utf-8
6 | indent_style = space
7 | indent_size = 2
8 | trim_trailing_whitespace = true
9 | max_line_length = 80
10 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 |
27 | # local env files
28 | .env*.local
29 |
30 | # vercel
31 | .vercel
32 |
33 | # typescript
34 | *.tsbuildinfo
35 | next-env.d.ts
36 |
37 | *storybook.log
38 | storybook-static
39 |
--------------------------------------------------------------------------------
/.storybook/main.ts:
--------------------------------------------------------------------------------
1 | import type {StorybookConfig} from "@storybook/nextjs-vite";
2 |
3 | const config: StorybookConfig = {
4 | stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
5 | addons: [
6 | "@chromatic-com/storybook",
7 | "@storybook/addon-docs",
8 | "@storybook/addon-onboarding",
9 | "@storybook/addon-a11y",
10 | "@storybook/addon-vitest",
11 | ],
12 | framework: {
13 | name: "@storybook/nextjs-vite",
14 | options: {},
15 | },
16 | staticDirs: ["..\\public"],
17 | };
18 |
19 | export default config;
20 |
--------------------------------------------------------------------------------
/.storybook/preview.tsx:
--------------------------------------------------------------------------------
1 | import type {Preview} from "@storybook/nextjs-vite";
2 |
3 | import "../src/app/globals.css";
4 |
5 | const preview: Preview = {
6 | parameters: {
7 | controls: {
8 | matchers: {
9 | color: /(background|color)$/i,
10 | date: /Date$/i,
11 | },
12 | },
13 |
14 | a11y: {
15 | // 'todo' - show a11y violations in the test UI only
16 | // 'error' - fail CI on a11y violations
17 | // 'off' - skip a11y checks entirely
18 | test: "todo",
19 | },
20 | },
21 | };
22 |
23 | export default preview;
24 |
--------------------------------------------------------------------------------
/.storybook/vitest.setup.ts:
--------------------------------------------------------------------------------
1 | import * as a11yAddonAnnotations from "@storybook/addon-a11y/preview";
2 | import {setProjectAnnotations} from "@storybook/nextjs-vite";
3 |
4 | import * as projectAnnotations from "./preview";
5 |
6 | // This is an important step to apply the right configuration when testing your stories.
7 | // More info at: https://storybook.js.org/docs/api/portable-stories/portable-stories-vitest#setprojectannotations
8 | setProjectAnnotations([a11yAddonAnnotations, projectAnnotations]);
9 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.2.0",
3 | "configurations": [
4 | {
5 | "name": "Next.js: debug server-side",
6 | "type": "node-terminal",
7 | "request": "launch",
8 | "command": "pnpm dev"
9 | },
10 | {
11 | "name": "Next.js: debug client-side",
12 | "type": "chrome",
13 | "request": "launch",
14 | "url": "http://localhost:3000"
15 | },
16 | {
17 | "name": "Next.js: debug full stack",
18 | "type": "node-terminal",
19 | "request": "launch",
20 | "command": "pnpm dev",
21 | "serverReadyAction": {
22 | "pattern": "started server on .+, url: (https?://.+)",
23 | "uriFormat": "%s",
24 | "action": "debugWithChrome"
25 | }
26 | }
27 | ]
28 | }
29 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "editor.formatOnSave": false,
3 | "editor.codeActionsOnSave": {
4 | "source.fixAll.eslint": "always"
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## Patrones
2 |
3 | - Container / presentational components
4 | - Uncontrolled / controlled components
5 | - High order components
6 | - Render props pattern / Function as child
7 | - Compound components
8 |
9 | ## Correrlo
10 |
11 | `pnpm i` y luego `pnpm storybook`
12 |
--------------------------------------------------------------------------------
/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "new-york",
4 | "rsc": true,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "",
8 | "css": "src/app/globals.css",
9 | "baseColor": "zinc",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "@/lib/utils",
16 | "ui": "@/components/ui",
17 | "lib": "@/lib",
18 | "hooks": "@/hooks"
19 | },
20 | "iconLibrary": "lucide"
21 | }
22 |
--------------------------------------------------------------------------------
/eslint.config.mjs:
--------------------------------------------------------------------------------
1 | // For more info, see https://github.com/storybookjs/eslint-plugin-storybook#configuration-flat-config-format
2 | import storybook from "eslint-plugin-storybook";
3 |
4 | import globals from "globals";
5 | import tseslint from "typescript-eslint";
6 | import eslintPluginReact from "eslint-plugin-react";
7 | import eslintPluginReactHooks from "eslint-plugin-react-hooks";
8 | import {fixupPluginRules} from "@eslint/compat";
9 | import eslintPluginPrettier from "eslint-plugin-prettier/recommended";
10 | import eslintPluginImport from "eslint-plugin-import";
11 | import eslintPluginReactCompiler from "eslint-plugin-react-compiler";
12 | import eslintPluginNext from "@next/eslint-plugin-next";
13 | import eslintPluginJsxA11y from "eslint-plugin-jsx-a11y";
14 |
15 | export default [// Ignores configuration
16 | // General configuration
17 | {
18 | ignores: ["node_modules", ".next", "out", "coverage", ".idea"],
19 | }, // React configuration
20 | {
21 | rules: {
22 | "padding-line-between-statements": [
23 | "warn",
24 | {blankLine: "always", prev: "*", next: ["return", "export"]},
25 | {blankLine: "always", prev: ["const", "let", "var"], next: "*"},
26 | {blankLine: "any", prev: ["const", "let", "var"], next: ["const", "let", "var"]},
27 | ],
28 | "no-console": ["warn", {allow: ["error"]}],
29 | },
30 | }, // TypeScript configuration
31 | {
32 | plugins: {
33 | react: fixupPluginRules(eslintPluginReact),
34 | "react-hooks": fixupPluginRules(eslintPluginReactHooks),
35 | "react-compiler": fixupPluginRules(eslintPluginReactCompiler),
36 | "jsx-a11y": fixupPluginRules(eslintPluginJsxA11y),
37 | },
38 | languageOptions: {
39 | parserOptions: {
40 | ecmaFeatures: {
41 | jsx: true,
42 | },
43 | },
44 | globals: {
45 | ...globals.browser,
46 | ...globals.serviceworker,
47 | },
48 | },
49 | settings: {
50 | react: {
51 | version: "detect",
52 | },
53 | },
54 | rules: {
55 | ...eslintPluginReact.configs.recommended.rules,
56 | ...eslintPluginJsxA11y.configs.recommended.rules,
57 | ...eslintPluginReactHooks.configs.recommended.rules,
58 | "react/jsx-boolean-value": ["error", "never"],
59 | "react/jsx-curly-brace-presence": ["error", {props: "never", children: "never"}],
60 | "react/jsx-no-useless-fragment": "error",
61 | "react/prop-types": "off",
62 | "react/jsx-uses-react": "off",
63 | "react/no-array-index-key": "off",
64 | "react/react-in-jsx-scope": "off",
65 | "react/self-closing-comp": "warn",
66 | "react/jsx-sort-props": [
67 | "warn",
68 | {
69 | callbacksLast: true,
70 | shorthandFirst: true,
71 | noSortAlphabetically: false,
72 | reservedFirst: true,
73 | },
74 | ],
75 | "react-compiler/react-compiler": "error",
76 | "react/jsx-no-leaked-render": "off",
77 | "jsx-a11y/no-static-element-interactions": "off",
78 | "jsx-a11y/click-events-have-key-events": "off",
79 | "jsx-a11y/html-has-lang": "off",
80 | },
81 | }, // Prettier configuration
82 | ...[
83 | ...tseslint.configs.recommended,
84 | {
85 | rules: {
86 | "@typescript-eslint/ban-ts-comment": "off",
87 | "@typescript-eslint/no-empty-object-type": "error",
88 | "@typescript-eslint/no-unsafe-function-type": "error",
89 | "@typescript-eslint/no-wrapper-object-types": "error",
90 | "@typescript-eslint/no-empty-function": "off",
91 | "@typescript-eslint/no-explicit-any": "off",
92 | "@typescript-eslint/no-inferrable-types": "off",
93 | "@typescript-eslint/no-namespace": "off",
94 | "@typescript-eslint/no-non-null-assertion": "off",
95 | "@typescript-eslint/no-shadow": "off",
96 | "@typescript-eslint/explicit-function-return-type": "off",
97 | "@typescript-eslint/require-await": "off",
98 | "@typescript-eslint/no-floating-promises": "off",
99 | "@typescript-eslint/no-confusing-void-expression": "off",
100 | "@typescript-eslint/no-unused-vars": [
101 | "warn",
102 | {
103 | args: "after-used",
104 | ignoreRestSiblings: false,
105 | argsIgnorePattern: "^_.*?$",
106 | caughtErrorsIgnorePattern: "^_.*?$",
107 | },
108 | ],
109 | },
110 | },
111 | ], // Import configuration
112 | ...[
113 | eslintPluginPrettier,
114 | {
115 | rules: {
116 | "prettier/prettier": [
117 | "warn",
118 | {
119 | printWidth: 100,
120 | trailingComma: "all",
121 | tabWidth: 2,
122 | semi: true,
123 | singleQuote: false,
124 | bracketSpacing: false,
125 | arrowParens: "always",
126 | endOfLine: "auto",
127 | plugins: ["prettier-plugin-tailwindcss"],
128 | },
129 | ],
130 | },
131 | },
132 | ], // Next configuration
133 | {
134 | plugins: {
135 | import: fixupPluginRules(eslintPluginImport),
136 | },
137 | rules: {
138 | "import/no-default-export": "off",
139 | "import/order": [
140 | "warn",
141 | {
142 | groups: [
143 | "type",
144 | "builtin",
145 | "object",
146 | "external",
147 | "internal",
148 | "parent",
149 | "sibling",
150 | "index",
151 | ],
152 | pathGroups: [
153 | {
154 | pattern: "@/*",
155 | group: "external",
156 | position: "after",
157 | },
158 | ],
159 | "newlines-between": "always",
160 | },
161 | ],
162 | },
163 | }, {
164 | plugins: {
165 | "@next/next": fixupPluginRules(eslintPluginNext),
166 | },
167 | languageOptions: {
168 | globals: {
169 | ...globals.node,
170 | ...globals.browser,
171 | },
172 | },
173 | rules: {
174 | ...eslintPluginNext.configs.recommended.rules,
175 | "@next/next/no-img-element": "off",
176 | "@next/next/no-html-link-for-pages": "off",
177 | },
178 | }, ...storybook.configs["flat/recommended"], ...storybook.configs["flat/recommended"]];
179 |
--------------------------------------------------------------------------------
/next.config.ts:
--------------------------------------------------------------------------------
1 | import type {NextConfig} from "next";
2 |
3 | const nextConfig: NextConfig = {
4 | experimental: {
5 | reactCompiler: true,
6 | dynamicIO: false,
7 | },
8 | logging: {
9 | fetches: {
10 | fullUrl: true,
11 | },
12 | },
13 | };
14 |
15 | export default nextConfig;
16 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "test-storybook-next",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev --turbopack",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint",
10 | "storybook": "storybook dev -p 6006",
11 | "build-storybook": "storybook build"
12 | },
13 | "dependencies": {
14 | "@radix-ui/react-slot": "^1.2.3",
15 | "@tailwindcss/postcss": "4.0.9",
16 | "class-variance-authority": "^0.7.1",
17 | "clsx": "^2.1.1",
18 | "lucide-react": "^0.475.0",
19 | "next": "^15.3.5",
20 | "postcss": "^8.5.6",
21 | "react": "^19.1.0",
22 | "react-dom": "^19.1.0",
23 | "tailwind-merge": "^3.3.1",
24 | "tailwindcss": "4.0.9",
25 | "tailwindcss-animate": "^1.0.7"
26 | },
27 | "devDependencies": {
28 | "@chromatic-com/storybook": "4.0.1",
29 | "@eslint/compat": "^1.3.1",
30 | "@next/eslint-plugin-next": "^15.3.5",
31 | "@storybook/addon-a11y": "9.0.16",
32 | "@storybook/addon-docs": "9.0.16",
33 | "@storybook/addon-onboarding": "9.0.16",
34 | "@storybook/addon-vitest": "9.0.16",
35 | "@storybook/nextjs-vite": "9.0.16",
36 | "@types/node": "^22.16.1",
37 | "@types/react": "^19.1.8",
38 | "@types/react-dom": "^19.1.6",
39 | "@vitest/browser": "^3.2.4",
40 | "@vitest/coverage-v8": "^3.2.4",
41 | "babel-plugin-react-compiler": "19.0.0-beta-e1e972c-20250221",
42 | "eslint": "^9.30.1",
43 | "eslint-config-next": "^15.3.5",
44 | "eslint-config-prettier": "^10.1.5",
45 | "eslint-plugin-import": "^2.32.0",
46 | "eslint-plugin-jsx-a11y": "^6.10.2",
47 | "eslint-plugin-prettier": "^5.5.1",
48 | "eslint-plugin-react": "^7.37.5",
49 | "eslint-plugin-react-compiler": "0.0.0-experimental-c8b3f72-20240517",
50 | "eslint-plugin-react-hooks": "^5.2.0",
51 | "eslint-plugin-storybook": "9.0.16",
52 | "globals": "^15.15.0",
53 | "playwright": "^1.53.2",
54 | "prettier": "^3.6.2",
55 | "prettier-plugin-tailwindcss": "^0.6.13",
56 | "storybook": "9.0.16",
57 | "typescript": "^5.8.3",
58 | "typescript-eslint": "^8.36.0",
59 | "vitest": "^3.2.4"
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/postcss.config.mjs:
--------------------------------------------------------------------------------
1 | export default {
2 | plugins: {
3 | "@tailwindcss/postcss": {},
4 | },
5 | };
6 |
--------------------------------------------------------------------------------
/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/placeholder.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/app/globals.css:
--------------------------------------------------------------------------------
1 | @import "tailwindcss";
2 |
3 | @plugin "tailwindcss-animate";
4 |
5 | :root {
6 | --radius: 0.5rem;
7 |
8 | --background: oklch(1 0 0);
9 | --foreground: oklch(0.141 0.005 285.823);
10 |
11 | --card: oklch(1 0 0);
12 | --card-foreground: oklch(0.141 0.005 285.823);
13 |
14 | --popover: oklch(1 0 0);
15 | --popover-foreground: oklch(0.141 0.005 285.823);
16 |
17 | --primary: oklch(0.21 0.006 285.885);
18 | --primary-foreground: oklch(0.985 0 0);
19 | --secondary: oklch(0.967 0.001 286.375);
20 | --secondary-foreground: oklch(0.21 0.006 285.885);
21 | --muted: oklch(0.967 0.001 286.375);
22 | --muted-foreground: oklch(0.552 0.016 285.938);
23 | --accent: oklch(0.967 0.001 286.375);
24 | --accent-foreground: oklch(0.21 0.006 285.885);
25 | --destructive: oklch(0.577 0.245 27.325);
26 |
27 | --border: oklch(0.92 0.004 286.32);
28 | --input: oklch(0.92 0.004 286.32);
29 | --ring: oklch(0.705 0.015 286.067);
30 |
31 | --chart-1: oklch(0.646 0.222 41.116);
32 | --chart-2: oklch(0.6 0.118 184.704);
33 | --chart-3: oklch(0.398 0.07 227.392);
34 | --chart-4: oklch(0.828 0.189 84.429);
35 | --chart-5: oklch(0.769 0.188 70.08);
36 |
37 | --sidebar: oklch(0.985 0 0);
38 | --sidebar-foreground: oklch(0.141 0.005 285.823);
39 | --sidebar-primary: oklch(0.21 0.006 285.885);
40 | --sidebar-primary-foreground: oklch(0.985 0 0);
41 | --sidebar-accent: oklch(0.967 0.001 286.375);
42 | --sidebar-accent-foreground: oklch(0.21 0.006 285.885);
43 | --sidebar-border: oklch(0.92 0.004 286.32);
44 | --sidebar-ring: oklch(0.705 0.015 286.067);
45 | }
46 |
47 | @media (prefers-color-scheme: dark) {
48 | :root {
49 | --background: oklch(0.141 0.005 285.823);
50 | --foreground: oklch(0.985 0 0);
51 |
52 | --card: oklch(0.21 0.006 285.885);
53 | --card-foreground: oklch(0.985 0 0);
54 |
55 | --popover: oklch(0.21 0.006 285.885);
56 | --popover-foreground: oklch(0.985 0 0);
57 |
58 | --primary: oklch(0.92 0.004 286.32);
59 | --primary-foreground: oklch(0.21 0.006 285.885);
60 | --secondary: oklch(0.274 0.006 286.033);
61 | --secondary-foreground: oklch(0.985 0 0);
62 | --muted: oklch(0.274 0.006 286.033);
63 | --muted-foreground: oklch(0.705 0.015 286.067);
64 | --accent: oklch(0.274 0.006 286.033);
65 | --accent-foreground: oklch(0.985 0 0);
66 | --destructive: oklch(0.704 0.191 22.216);
67 |
68 | --border: oklch(1 0 0 / 10%);
69 | --input: oklch(1 0 0 / 15%);
70 | --ring: oklch(0.552 0.016 285.938);
71 |
72 | --chart-1: oklch(0.488 0.243 264.376);
73 | --chart-2: oklch(0.696 0.17 162.48);
74 | --chart-3: oklch(0.769 0.188 70.08);
75 | --chart-4: oklch(0.627 0.265 303.9);
76 | --chart-5: oklch(0.645 0.246 16.439);
77 |
78 | --sidebar: oklch(0.21 0.006 285.885);
79 | --sidebar-foreground: oklch(0.985 0 0);
80 | --sidebar-primary: oklch(0.488 0.243 264.376);
81 | --sidebar-primary-foreground: oklch(0.985 0 0);
82 | --sidebar-accent: oklch(0.274 0.006 286.033);
83 | --sidebar-accent-foreground: oklch(0.985 0 0);
84 | --sidebar-border: oklch(1 0 0 / 10%);
85 | --sidebar-ring: oklch(0.552 0.016 285.938);
86 | }
87 | }
88 |
89 | @theme inline {
90 | --radius-sm: calc(var(--radius) - 4px);
91 | --radius-md: calc(var(--radius) - 2px);
92 | --radius-lg: var(--radius);
93 | --radius-xl: calc(var(--radius) + 4px);
94 |
95 | --color-background: var(--background);
96 | --color-foreground: var(--foreground);
97 |
98 | --color-card: var(--card);
99 | --color-card-foreground: var(--card-foreground);
100 |
101 | --color-popover: var(--popover);
102 | --color-popover-foreground: var(--popover-foreground);
103 |
104 | --color-primary: var(--primary);
105 | --color-primary-foreground: var(--primary-foreground);
106 | --color-secondary: var(--secondary);
107 | --color-secondary-foreground: var(--secondary-foreground);
108 | --color-muted: var(--muted);
109 | --color-muted-foreground: var(--muted-foreground);
110 | --color-accent: var(--accent);
111 | --color-accent-foreground: var(--accent-foreground);
112 | --color-destructive: var(--destructive);
113 | --color-destructive-foreground: var(--destructive-foreground);
114 |
115 | --color-border: var(--border);
116 | --color-input: var(--input);
117 | --color-ring: var(--ring);
118 |
119 | --color-chart-1: var(--chart-1);
120 | --color-chart-2: var(--chart-2);
121 | --color-chart-3: var(--chart-3);
122 | --color-chart-4: var(--chart-4);
123 | --color-chart-5: var(--chart-5);
124 |
125 | --color-sidebar: var(--sidebar);
126 | --color-sidebar-foreground: var(--sidebar-foreground);
127 | --color-sidebar-primary: var(--sidebar-primary);
128 | --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
129 | --color-sidebar-accent: var(--sidebar-accent);
130 | --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
131 | --color-sidebar-border: var(--sidebar-border);
132 | --color-sidebar-ring: var(--sidebar-ring);
133 | }
134 |
135 | @layer base {
136 | * {
137 | @apply border-border outline-ring/50;
138 | }
139 | body {
140 | @apply bg-background text-foreground;
141 | }
142 | }
143 |
144 | @keyframes fadeInUp {
145 | from {
146 | opacity: 0;
147 | transform: translateY(10px);
148 | }
149 | to {
150 | opacity: 1;
151 | transform: translateY(0);
152 | }
153 | }
154 |
--------------------------------------------------------------------------------
/src/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import type {Metadata} from "next";
2 |
3 | import Link from "next/link";
4 |
5 | import "./globals.css";
6 |
7 | export const metadata: Metadata = {
8 | title: "test-storybook-next",
9 | description: "Generated by appncy",
10 | };
11 |
12 | export default async function RootLayout({children}: {children: React.ReactNode}) {
13 | return (
14 |
15 |
16 |
17 | test-storybook-next
18 |
19 | {children}
20 |
21 |
22 |
23 | );
24 | }
25 |
--------------------------------------------------------------------------------
/src/app/page.tsx:
--------------------------------------------------------------------------------
1 | import {Button} from "@/components/ui/button";
2 |
3 | export default async function HomePage() {
4 | return (
5 |
6 |
7 |
8 | );
9 | }
10 |
--------------------------------------------------------------------------------
/src/components/property-card/basic/spread-props/index.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import {useState} from "react";
4 | import Image from "next/image";
5 | import {MapPin, Bed, Bath, Square} from "lucide-react";
6 |
7 | interface Property {
8 | id: number;
9 | title: string;
10 | location: string;
11 | price: number;
12 | bedrooms: number;
13 | bathrooms: number;
14 | sqft: number;
15 | images: string[];
16 | features: string[];
17 | description: string;
18 | agent: {
19 | name: string;
20 | image: string;
21 | phone: string;
22 | };
23 | }
24 |
25 | export default function PropertyCard(data: Property) {
26 | const [currentImageIndex, setCurrentImageIndex] = useState(0);
27 |
28 | const formatPrice = (price: number) => {
29 | return new Intl.NumberFormat("en-US", {
30 | style: "currency",
31 | currency: "USD",
32 | minimumFractionDigits: 0,
33 | maximumFractionDigits: 0,
34 | }).format(price);
35 | };
36 |
37 | const formatNumber = (num: number) => {
38 | return new Intl.NumberFormat("en-US").format(num);
39 | };
40 |
41 | return (
42 |
43 | {/* Image Gallery Section */}
44 |
45 | {data.images?.length ? (
46 |
52 | ) : null}
53 |
54 | {/* Image Navigation Dots */}
55 |
56 | {data.images?.length
57 | ? data.images.map((_, index) => (
58 |
70 |
71 | {/* Price Badge */}
72 | {data.price && (
73 |
74 | {formatPrice(data.price)}
75 |
76 | )}
77 |
78 |
79 | {/* Content Section */}
80 |
81 | {/* Title and Location */}
82 |
83 |
{data.title}
84 |
85 |
86 | {data.location}
87 |
88 |
89 |
90 | {/* Property Stats */}
91 |
92 |
93 |
94 | {data.bedrooms}
95 |
96 |
97 |
98 | {data.bathrooms}
99 |
100 |
101 |
102 |
103 | {data.sqft ? formatNumber(data.sqft) : "N/A"} ft²
104 |
105 |
106 |
107 |
108 | {/* Description */}
109 |
{data.description}
110 |
111 | {/* Features */}
112 | {data.features && (
113 |
114 | {data.features.map((feature, index) => (
115 |
119 | {feature}
120 |
121 | ))}
122 |
123 | )}
124 |
125 |
126 | );
127 | }
128 |
--------------------------------------------------------------------------------
/src/components/property-card/basic/spread-props/spread-props.stories.tsx:
--------------------------------------------------------------------------------
1 | import type {Meta} from "@storybook/nextjs-vite";
2 |
3 | import PropertyCard from ".";
4 |
5 | const props: React.ComponentProps = {
6 | id: 1,
7 | description: "This is a modern architectural marvel located in Beverly Hills, CA.",
8 | agent: {
9 | name: "John Doe",
10 | image: "/placeholder.svg?height=300&width=400",
11 | phone: "+1234567890",
12 | },
13 | title: "Modern Architectural Marvel",
14 | location: "Beverly Hills, CA",
15 | price: 2850000,
16 | bedrooms: 4,
17 | bathrooms: 3,
18 | sqft: 3200,
19 | images: ["/placeholder.svg?height=300&width=400", "/placeholder.svg?height=300&width=400"],
20 | features: ["Pool", "Garden", "Smart Home"],
21 | };
22 |
23 | export const SpreadProps = (props: typeof meta.args) => ;
24 |
25 | export const ManualProps = (props: typeof meta.args) => (
26 |
39 | );
40 |
41 | const meta = {
42 | title: "Basic Components/Spread Props",
43 | component: PropertyCard,
44 | parameters: {
45 | layout: "centered",
46 | },
47 | args: props,
48 | } satisfies Meta;
49 |
50 | export default meta;
51 |
--------------------------------------------------------------------------------
/src/components/property-card/basic/value-prop/index.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import {useState} from "react";
4 | import Image from "next/image";
5 | import {MapPin, Bed, Bath, Square} from "lucide-react";
6 |
7 | export interface Property {
8 | id: number;
9 | title: string;
10 | location: string;
11 | price: number;
12 | bedrooms: number;
13 | bathrooms: number;
14 | sqft: number;
15 | images: string[];
16 | features: string[];
17 | description: string;
18 | agent: {
19 | name: string;
20 | image: string;
21 | phone: string;
22 | };
23 | }
24 |
25 | export default function PropertyCard({value}: {value: Property}) {
26 | const [currentImageIndex, setCurrentImageIndex] = useState(0);
27 |
28 | const formatPrice = (price: number) => {
29 | return new Intl.NumberFormat("en-US", {
30 | style: "currency",
31 | currency: "USD",
32 | minimumFractionDigits: 0,
33 | maximumFractionDigits: 0,
34 | }).format(price);
35 | };
36 |
37 | const formatNumber = (num: number) => {
38 | return new Intl.NumberFormat("en-US").format(num);
39 | };
40 |
41 | return (
42 |
43 | {/* Image Gallery Section */}
44 |
45 |
51 |
52 | {/* Image Navigation Dots */}
53 |
54 | {value.images.map((_, index) => (
55 |
64 |
65 | {/* Price Badge */}
66 |
67 | {formatPrice(value.price)}
68 |
69 |
70 |
71 | {/* Content Section */}
72 |
73 | {/* Title and Location */}
74 |
75 |
{value.title}
76 |
77 |
78 | {value.location}
79 |
80 |
81 |
82 | {/* Property Stats */}
83 |
84 |
85 |
86 | {value.bedrooms}
87 |
88 |
89 |
90 | {value.bathrooms}
91 |
92 |
93 |
94 |
95 | {formatNumber(value.sqft)} ft²
96 |
97 |
98 |
99 |
100 | {/* Description */}
101 |
{value.description}
102 |
103 | {/* Features */}
104 |
105 | {value.features.map((feature, index) => (
106 |
110 | {feature}
111 |
112 | ))}
113 |
114 |
115 |
116 | );
117 | }
118 |
--------------------------------------------------------------------------------
/src/components/property-card/basic/value-prop/value-prop.stories.tsx:
--------------------------------------------------------------------------------
1 | import type {Meta} from "@storybook/nextjs-vite";
2 |
3 | import PropertyCard from ".";
4 |
5 | const props: React.ComponentProps = {
6 | value: {
7 | id: 1,
8 | description: "This is a modern architectural marvel located in Beverly Hills, CA.",
9 | agent: {
10 | name: "John Doe",
11 | image: "/placeholder.svg?height=300&width=400",
12 | phone: "+1234567890",
13 | },
14 | title: "Modern Architectural Marvel",
15 | location: "Beverly Hills, CA",
16 | price: 2850000,
17 | bedrooms: 4,
18 | bathrooms: 3,
19 | sqft: 3200,
20 | images: ["/placeholder.svg?height=300&width=400", "/placeholder.svg?height=300&width=400"],
21 | features: ["Pool", "Garden", "Smart Home"],
22 | },
23 | };
24 |
25 | export const ValueProp = (props: typeof meta.args) => ;
26 |
27 | const meta = {
28 | title: "Basic Components/Value Prop",
29 | component: PropertyCard,
30 | parameters: {
31 | layout: "centered",
32 | },
33 | args: props,
34 | } satisfies Meta;
35 |
36 | export default meta;
37 |
--------------------------------------------------------------------------------
/src/components/property-card/children-array/children-array.stories.tsx:
--------------------------------------------------------------------------------
1 | import PropertyCard, {type Property} from "../basic/value-prop";
2 |
3 | import FadeInGrid from ".";
4 |
5 | const props: {properties: Property[]} = {
6 | properties: [
7 | {
8 | id: 1,
9 | description: "This is a modern architectural marvel located in Beverly Hills, CA.",
10 | agent: {
11 | name: "John Doe",
12 | image: "/placeholder.svg?height=300&width=400",
13 | phone: "+1234567890",
14 | },
15 | title: "Modern Architectural Marvel",
16 | location: "Beverly Hills, CA",
17 | price: 2850000,
18 | bedrooms: 4,
19 | bathrooms: 3,
20 | sqft: 3200,
21 | images: ["/placeholder.svg?height=300&width=400", "/placeholder.svg?height=300&width=300"],
22 | features: ["Pool", "Garden", "Smart Home"],
23 | },
24 | {
25 | id: 2,
26 | description: "This is a modern architectural marvel located in Beverly Hills, CA.",
27 | agent: {
28 | name: "John Doe",
29 | image: "/placeholder.svg?height=300&width=400",
30 | phone: "+1234567890",
31 | },
32 | title: "Barbie House",
33 | location: "Malibu, CA",
34 | price: 2850000,
35 | bedrooms: 4,
36 | bathrooms: 3,
37 | sqft: 3200,
38 | images: ["/placeholder.svg?height=300&width=400", "/placeholder.svg?height=300&width=300"],
39 | features: ["Pool", "Garden", "Smart Home"],
40 | },
41 | {
42 | id: 3,
43 | description: "This is a modern architectural marvel located in Beverly Hills, CA.",
44 | agent: {
45 | name: "John Doe",
46 | image: "/placeholder.svg?height=300&width=400",
47 | phone: "+1234567890",
48 | },
49 | title: "The White House",
50 | location: "Washington, DC",
51 | price: 2850000,
52 | bedrooms: 4,
53 | bathrooms: 3,
54 | sqft: 3200,
55 | images: ["/placeholder.svg?height=300&width=400", "/placeholder.svg?height=300&width=300"],
56 | features: ["Pool", "Garden", "Smart Home"],
57 | },
58 | {
59 | id: 4,
60 | description: "This is a modern architectural marvel located in Beverly Hills, CA.",
61 | agent: {
62 | name: "John Doe",
63 | image: "/placeholder.svg?height=300&width=400",
64 | phone: "+1234567890",
65 | },
66 | title: "Penthouse",
67 | location: "New York, NY",
68 | price: 2850000,
69 | bedrooms: 4,
70 | bathrooms: 3,
71 | sqft: 3200,
72 | images: ["/placeholder.svg?height=300&width=400", "/placeholder.svg?height=300&width=300"],
73 | features: ["Pool", "Garden", "Smart Home"],
74 | },
75 | {
76 | id: 5,
77 | description: "This is a modern architectural marvel located in Beverly Hills, CA.",
78 | agent: {
79 | name: "John Doe",
80 | image: "/placeholder.svg?height=300&width=400",
81 | phone: "+1234567890",
82 | },
83 | title: "Pentagon House",
84 | location: "Washington, DC",
85 | price: 2850000,
86 | bedrooms: 4,
87 | bathrooms: 3,
88 | sqft: 3200,
89 | images: ["/placeholder.svg?height=300&width=400", "/placeholder.svg?height=300&width=300"],
90 | features: ["Pool", "Garden", "Smart Home"],
91 | },
92 | {
93 | id: 6,
94 | description: "This is a modern architectural marvel located in Beverly Hills, CA.",
95 | agent: {
96 | name: "John Doe",
97 | image: "/placeholder.svg?height=300&width=400",
98 | phone: "+1234567890",
99 | },
100 | title: "Some other house",
101 | location: "Washington, DC",
102 | price: 2850000,
103 | bedrooms: 4,
104 | bathrooms: 3,
105 | sqft: 3200,
106 | images: ["/placeholder.svg?height=300&width=400", "/placeholder.svg?height=300&width=300"],
107 | features: ["Pool", "Garden", "Smart Home"],
108 | },
109 | ],
110 | };
111 |
112 | export const FadeIn = ({properties}: typeof meta.args) => (
113 |
114 | Hola
115 | Mundo
116 | Soy
117 | Goncy
118 | {properties.map((property) => (
119 |
120 | ))}
121 |
122 | );
123 |
124 | const meta = {
125 | title: "Children Array/Fade In",
126 | component: FadeInGrid,
127 | args: props,
128 | };
129 |
130 | export default meta;
131 |
--------------------------------------------------------------------------------
/src/components/property-card/children-array/index.tsx:
--------------------------------------------------------------------------------
1 | import {Children} from "react";
2 |
3 | export default function FadeInGrid({children}: {children: React.ReactNode[]}) {
4 | return (
5 |
6 | {Children.toArray(children).map((child, idx) => (
7 |
15 | {child}
16 |
17 | ))}
18 |
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/src/components/property-card/compound/managed/index.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import {createContext, use, useState} from "react";
4 | import Image from "next/image";
5 | import {MapPin, Bed, Bath, Square} from "lucide-react";
6 |
7 | interface Property {
8 | id: number;
9 | title: string;
10 | location: string;
11 | price: number;
12 | bedrooms: number;
13 | bathrooms: number;
14 | sqft: number;
15 | images: string[];
16 | features: string[];
17 | description: string;
18 | agent: {
19 | name: string;
20 | image: string;
21 | phone: string;
22 | };
23 | }
24 |
25 | const PropertyCardContext = createContext(null);
26 |
27 | export function Root({children, value}: {children: React.ReactNode; value: Property}) {
28 | return {children};
29 | }
30 |
31 | export function Gallery() {
32 | const {images} = use(PropertyCardContext)!;
33 | const [currentImageIndex, setCurrentImageIndex] = useState(0);
34 |
35 | return (
36 |
37 |
38 |
39 |
40 | {/* Image Navigation Dots */}
41 |
42 | {images.map((_, index) => (
43 |
52 |
53 |
54 | );
55 | }
56 |
57 | export function Features() {
58 | const {features} = use(PropertyCardContext)!;
59 |
60 | return (
61 |
62 | {features.map((feature, index) => (
63 |
67 | {feature}
68 |
69 | ))}
70 |
71 | );
72 | }
73 |
74 | export function Description() {
75 | const {description} = use(PropertyCardContext)!;
76 |
77 | return {description}
;
78 | }
79 |
80 | export function Title() {
81 | const {title} = use(PropertyCardContext)!;
82 |
83 | return {title}
;
84 | }
85 |
86 | export function Location() {
87 | const {location} = use(PropertyCardContext)!;
88 |
89 | return (
90 |
91 |
92 | {location}
93 |
94 | );
95 | }
96 |
97 | export function Price() {
98 | const {price} = use(PropertyCardContext)!;
99 |
100 | return (
101 |
102 | {new Intl.NumberFormat("en-US", {
103 | style: "currency",
104 | currency: "USD",
105 | minimumFractionDigits: 0,
106 | maximumFractionDigits: 0,
107 | }).format(price)}
108 |
109 | );
110 | }
111 |
112 | export function Stats() {
113 | const {bedrooms, bathrooms, sqft} = use(PropertyCardContext)!;
114 | const formatNumber = (num: number) => {
115 | return new Intl.NumberFormat("en-US").format(num);
116 | };
117 |
118 | return (
119 |
120 |
121 |
122 | {bedrooms}
123 |
124 |
125 |
126 | {bathrooms}
127 |
128 |
129 |
130 |
131 | {sqft ? formatNumber(sqft) : "N/A"} ft²
132 |
133 |
134 |
135 | );
136 | }
137 |
138 | export function Overlay({children}: {children: React.ReactNode}) {
139 | return {children}
;
140 | }
141 |
142 | export function Card({children}: {children: React.ReactNode}) {
143 | return (
144 |
145 | {children}
146 |
147 | );
148 | }
149 |
150 | export function Content({children}: {children: React.ReactNode}) {
151 | return {children}
;
152 | }
153 |
154 | export default {
155 | Root,
156 | Card,
157 | Gallery,
158 | Features,
159 | Description,
160 | Title,
161 | Location,
162 | Price,
163 | Stats,
164 | Overlay,
165 | Content,
166 | };
167 |
--------------------------------------------------------------------------------
/src/components/property-card/compound/managed/managed.stories.tsx:
--------------------------------------------------------------------------------
1 | import type {Meta} from "@storybook/nextjs-vite";
2 |
3 | import PropertyCard from ".";
4 |
5 | const props: Omit, "children"> = {
6 | value: {
7 | id: 1,
8 | description: "This is a modern architectural marvel located in Beverly Hills, CA.",
9 | agent: {
10 | name: "John Doe",
11 | image: "/placeholder.svg?height=300&width=400",
12 | phone: "+1234567890",
13 | },
14 | title: "Modern Architectural Marvel",
15 | location: "Beverly Hills, CA",
16 | price: 2850000,
17 | bedrooms: 4,
18 | bathrooms: 3,
19 | sqft: 3200,
20 | images: ["/placeholder.svg?height=300&width=400", "/placeholder.svg?height=300&width=300"],
21 | features: ["Pool", "Garden", "Smart Home"],
22 | },
23 | };
24 |
25 | export const Managed = (props: typeof meta.args) => (
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 | );
41 |
42 | const meta = {
43 | title: "Compound Components/Managed",
44 | component: PropertyCard.Root,
45 | parameters: {
46 | layout: "centered",
47 | },
48 | args: props,
49 | } satisfies Meta;
50 |
51 | export default meta;
52 |
--------------------------------------------------------------------------------
/src/components/property-card/compound/presentational/index.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import {useState} from "react";
4 | import Image from "next/image";
5 | import {MapPin, Bed, Bath, Square} from "lucide-react";
6 |
7 | export interface Property {
8 | id: number;
9 | title: string;
10 | location: string;
11 | price: number;
12 | bedrooms: number;
13 | bathrooms: number;
14 | sqft: number;
15 | images: string[];
16 | features: string[];
17 | description: string;
18 | agent: {
19 | name: string;
20 | image: string;
21 | phone: string;
22 | };
23 | }
24 |
25 | export function Gallery({images}: {images: string[]}) {
26 | const [currentImageIndex, setCurrentImageIndex] = useState(0);
27 |
28 | return (
29 |
30 |
31 |
32 |
33 | {/* Image Navigation Dots */}
34 |
35 | {images.map((_, index) => (
36 |
45 |
46 |
47 | );
48 | }
49 |
50 | export function Features({features}: {features: string[]}) {
51 | return (
52 |
53 | {features.map((feature, index) => (
54 |
58 | {feature}
59 |
60 | ))}
61 |
62 | );
63 | }
64 |
65 | export function Description({description}: {description: string}) {
66 | return {description}
;
67 | }
68 |
69 | export function Title({title}: {title: string}) {
70 | return {title}
;
71 | }
72 |
73 | export function Location({location}: {location: string}) {
74 | return (
75 |
76 |
77 | {location}
78 |
79 | );
80 | }
81 |
82 | export function Price({price}: {price: number}) {
83 | return (
84 |
85 | {new Intl.NumberFormat("en-US", {
86 | style: "currency",
87 | currency: "USD",
88 | minimumFractionDigits: 0,
89 | maximumFractionDigits: 0,
90 | }).format(price)}
91 |
92 | );
93 | }
94 |
95 | export function Stats({
96 | bedrooms,
97 | bathrooms,
98 | sqft,
99 | }: {
100 | bedrooms: number;
101 | bathrooms: number;
102 | sqft: number;
103 | }) {
104 | const formatNumber = (num: number) => {
105 | return new Intl.NumberFormat("en-US").format(num);
106 | };
107 |
108 | return (
109 |
110 |
111 |
112 | {bedrooms}
113 |
114 |
115 |
116 | {bathrooms}
117 |
118 |
119 |
120 |
121 | {sqft ? formatNumber(sqft) : "N/A"} ft²
122 |
123 |
124 |
125 | );
126 | }
127 |
128 | export function Overlay({children}: {children: React.ReactNode}) {
129 | return {children}
;
130 | }
131 |
132 | export function Card({children}: {children: React.ReactNode}) {
133 | return (
134 |
135 | {children}
136 |
137 | );
138 | }
139 |
140 | export function Content({children}: {children: React.ReactNode}) {
141 | return {children}
;
142 | }
143 |
144 | export default {
145 | Card,
146 | Gallery,
147 | Features,
148 | Content,
149 | Description,
150 | Title,
151 | Location,
152 | Price,
153 | Stats,
154 | Overlay,
155 | };
156 |
--------------------------------------------------------------------------------
/src/components/property-card/compound/presentational/presentational.stories.tsx:
--------------------------------------------------------------------------------
1 | import PropertyCard, {type Property} from ".";
2 |
3 | const props: Property = {
4 | id: 1,
5 | description: "This is a modern architectural marvel located in Beverly Hills, CA.",
6 | agent: {
7 | name: "John Doe",
8 | image: "/placeholder.svg?height=300&width=400",
9 | phone: "+1234567890",
10 | },
11 | title: "Modern Architectural Marvel",
12 | location: "Beverly Hills, CA",
13 | price: 2850000,
14 | bedrooms: 4,
15 | bathrooms: 3,
16 | sqft: 3200,
17 | images: ["/placeholder.svg?height=300&width=400", "/placeholder.svg?height=300&width=300"],
18 | features: ["Pool", "Garden", "Smart Home"],
19 | };
20 |
21 | export const Default = (props: typeof meta.args) => (
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 | );
35 |
36 | export const NoGallery = (props: typeof meta.args) => (
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 | );
46 |
47 | export const SwitchedOrder = (props: typeof meta.args) => (
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 | );
59 |
60 | const meta = {
61 | title: "Compound Components/Presentational",
62 | component: PropertyCard,
63 | parameters: {
64 | layout: "centered",
65 | },
66 | args: props,
67 | };
68 |
69 | export default meta;
70 |
--------------------------------------------------------------------------------
/src/components/property-card/container-presentational/container/container.stories.tsx:
--------------------------------------------------------------------------------
1 | import type {Meta} from "@storybook/nextjs-vite";
2 |
3 | import {PropertyCardContainer} from ".";
4 |
5 | const props: React.ComponentProps = {
6 | id: 1,
7 | };
8 |
9 | export const Container = (props: typeof meta.args) => ;
10 |
11 | const meta = {
12 | title: "Container & Presentational/Container",
13 | component: PropertyCardContainer,
14 | parameters: {
15 | layout: "centered",
16 | },
17 | args: props,
18 | } satisfies Meta;
19 |
20 | export default meta;
21 |
--------------------------------------------------------------------------------
/src/components/property-card/container-presentational/container/index.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import {useEffect, useState} from "react";
4 | import Image from "next/image";
5 | import {MapPin, Bed, Bath, Square, Loader2} from "lucide-react";
6 |
7 | interface Property {
8 | id: number;
9 | title: string;
10 | location: string;
11 | price: number;
12 | bedrooms: number;
13 | bathrooms: number;
14 | sqft: number;
15 | images: string[];
16 | features: string[];
17 | description: string;
18 | agent: {
19 | name: string;
20 | image: string;
21 | phone: string;
22 | };
23 | }
24 |
25 | interface PropertyCardProps {
26 | data: Property;
27 | }
28 |
29 | const DATA: Property = {
30 | id: 1,
31 | description: "This is a modern architectural marvel located in Beverly Hills, CA.",
32 | agent: {
33 | name: "John Doe",
34 | image: "/placeholder.svg?height=300&width=400",
35 | phone: "+1234567890",
36 | },
37 | title: "Modern Architectural Marvel",
38 | location: "Beverly Hills, CA",
39 | price: 2850000,
40 | bedrooms: 4,
41 | bathrooms: 3,
42 | sqft: 3200,
43 | images: ["/placeholder.svg?height=300&width=400", "/placeholder.svg?height=300&width=400"],
44 | features: ["Pool", "Garden", "Smart Home"],
45 | };
46 |
47 | export function PropertyCardContainer({id: _id}: {id: Property["id"]}) {
48 | const [data, setData] = useState(null);
49 |
50 | useEffect(() => {
51 | setTimeout(() => {
52 | setData(DATA);
53 | }, 1000);
54 | }, []);
55 |
56 | if (!data)
57 | return (
58 |
59 |
60 |
61 | );
62 |
63 | return ;
64 | }
65 |
66 | export default function PropertyCard({data}: PropertyCardProps) {
67 | const [currentImageIndex, setCurrentImageIndex] = useState(0);
68 |
69 | const formatPrice = (price: number) => {
70 | return new Intl.NumberFormat("en-US", {
71 | style: "currency",
72 | currency: "USD",
73 | minimumFractionDigits: 0,
74 | maximumFractionDigits: 0,
75 | }).format(price);
76 | };
77 |
78 | const formatNumber = (num: number) => {
79 | return new Intl.NumberFormat("en-US").format(num);
80 | };
81 |
82 | return (
83 |
84 | {/* Image Gallery Section */}
85 |
86 |
92 |
93 | {/* Image Navigation Dots */}
94 |
95 | {data.images.map((_, index) => (
96 |
105 |
106 | {/* Price Badge */}
107 |
108 | {formatPrice(data.price)}
109 |
110 |
111 |
112 | {/* Content Section */}
113 |
114 | {/* Title and Location */}
115 |
116 |
{data.title}
117 |
118 |
119 | {data.location}
120 |
121 |
122 |
123 | {/* Property Stats */}
124 |
125 |
126 |
127 | {data.bedrooms}
128 |
129 |
130 |
131 | {data.bathrooms}
132 |
133 |
134 |
135 |
136 | {formatNumber(data.sqft)} ft²
137 |
138 |
139 |
140 |
141 | {/* Description */}
142 |
{data.description}
143 |
144 | {/* Features */}
145 |
146 | {data.features.map((feature, index) => (
147 |
151 | {feature}
152 |
153 | ))}
154 |
155 |
156 |
157 | );
158 | }
159 |
--------------------------------------------------------------------------------
/src/components/property-card/container-presentational/presentational/index.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import {useState} from "react";
4 | import Image from "next/image";
5 | import {MapPin, Bed, Bath, Square} from "lucide-react";
6 |
7 | interface Property {
8 | id: number;
9 | title: string;
10 | location: string;
11 | price: number;
12 | bedrooms: number;
13 | bathrooms: number;
14 | sqft: number;
15 | images: string[];
16 | features: string[];
17 | description: string;
18 | agent: {
19 | name: string;
20 | image: string;
21 | phone: string;
22 | };
23 | }
24 |
25 | interface PropertyCardProps {
26 | value: Property;
27 | }
28 |
29 | export default function PropertyCard({value}: PropertyCardProps) {
30 | const [currentImageIndex, setCurrentImageIndex] = useState(0);
31 |
32 | const formatPrice = (price: number) => {
33 | return new Intl.NumberFormat("en-US", {
34 | style: "currency",
35 | currency: "USD",
36 | minimumFractionDigits: 0,
37 | maximumFractionDigits: 0,
38 | }).format(price);
39 | };
40 |
41 | const formatNumber = (num: number) => {
42 | return new Intl.NumberFormat("en-US").format(num);
43 | };
44 |
45 | return (
46 |
47 | {/* Image Gallery Section */}
48 |
49 |
55 |
56 | {/* Image Navigation Dots */}
57 |
58 | {value.images.map((_, index) => (
59 |
68 |
69 | {/* Price Badge */}
70 |
71 | {formatPrice(value.price)}
72 |
73 |
74 |
75 | {/* Content Section */}
76 |
77 | {/* Title and Location */}
78 |
79 |
{value.title}
80 |
81 |
82 | {value.location}
83 |
84 |
85 |
86 | {/* Property Stats */}
87 |
88 |
89 |
90 | {value.bedrooms}
91 |
92 |
93 |
94 | {value.bathrooms}
95 |
96 |
97 |
98 |
99 | {formatNumber(value.sqft)} ft²
100 |
101 |
102 |
103 |
104 | {/* Description */}
105 |
{value.description}
106 |
107 | {/* Features */}
108 |
109 | {value.features.map((feature, index) => (
110 |
114 | {feature}
115 |
116 | ))}
117 |
118 |
119 |
120 | );
121 | }
122 |
--------------------------------------------------------------------------------
/src/components/property-card/container-presentational/presentational/presentational.stories.tsx:
--------------------------------------------------------------------------------
1 | import type {Meta} from "@storybook/nextjs-vite";
2 |
3 | import PropertyCard from ".";
4 |
5 | const props: React.ComponentProps = {
6 | value: {
7 | id: 1,
8 | description: "This is a modern architectural marvel located in Beverly Hills, CA.",
9 | agent: {
10 | name: "John Doe",
11 | image: "/placeholder.svg?height=300&width=400",
12 | phone: "+1234567890",
13 | },
14 | title: "Modern Architectural Marvel",
15 | location: "Beverly Hills, CA",
16 | price: 2850000,
17 | bedrooms: 4,
18 | bathrooms: 3,
19 | sqft: 3200,
20 | images: ["/placeholder.svg?height=300&width=400", "/placeholder.svg?height=300&width=400"],
21 | features: ["Pool", "Garden", "Smart Home"],
22 | },
23 | };
24 |
25 | export const Presentational = (props: typeof meta.args) => ;
26 |
27 | const meta = {
28 | title: "Container & Presentational/Presentational",
29 | component: PropertyCard,
30 | parameters: {
31 | layout: "centered",
32 | },
33 | args: props,
34 | } satisfies Meta;
35 |
36 | export default meta;
37 |
--------------------------------------------------------------------------------
/src/components/property-card/controlled-uncontrolled/controlled/controlled.stories.tsx:
--------------------------------------------------------------------------------
1 | import {useState} from "react";
2 |
3 | import PropertyCard, {type Property} from "../../basic/value-prop";
4 |
5 | import Tabs from ".";
6 |
7 | const PROPERTIES: Property[] = [
8 | {
9 | id: 1,
10 | description: "This is a modern architectural marvel located in Beverly Hills, CA.",
11 | agent: {
12 | name: "John Doe",
13 | image: "/placeholder.svg?height=300&width=400",
14 | phone: "+1234567890",
15 | },
16 | title: "Modern Architectural Marvel",
17 | location: "Beverly Hills, CA",
18 | price: 2850000,
19 | bedrooms: 4,
20 | bathrooms: 3,
21 | sqft: 3200,
22 | images: ["/placeholder.svg?height=300&width=400", "/placeholder.svg?height=300&width=300"],
23 | features: ["Pool", "Garden", "Smart Home"],
24 | },
25 | {
26 | id: 2,
27 | description: "This is a modern architectural marvel located in Beverly Hills, CA.",
28 | agent: {
29 | name: "John Doe",
30 | image: "/placeholder.svg?height=300&width=400",
31 | phone: "+1234567890",
32 | },
33 | title: "Barbie House",
34 | location: "Malibu, CA",
35 | price: 2850000,
36 | bedrooms: 4,
37 | bathrooms: 3,
38 | sqft: 3200,
39 | images: ["/placeholder.svg?height=300&width=400", "/placeholder.svg?height=300&width=300"],
40 | features: ["Pool", "Garden", "Smart Home"],
41 | },
42 | {
43 | id: 3,
44 | description: "This is a modern architectural marvel located in Beverly Hills, CA.",
45 | agent: {
46 | name: "John Doe",
47 | image: "/placeholder.svg?height=300&width=400",
48 | phone: "+1234567890",
49 | },
50 | title: "The White House",
51 | location: "Washington, DC",
52 | price: 2850000,
53 | bedrooms: 4,
54 | bathrooms: 3,
55 | sqft: 3200,
56 | images: ["/placeholder.svg?height=300&width=400", "/placeholder.svg?height=300&width=300"],
57 | features: ["Pool", "Garden", "Smart Home"],
58 | },
59 | ];
60 |
61 | const props: React.ComponentProps = {
62 | value: [
63 | {
64 | title: "Featured",
65 | content: PROPERTIES.slice(0, 2).map((property) => (
66 |
67 | )),
68 | },
69 | {
70 | title: "All",
71 | content: PROPERTIES.map((property) => ),
72 | },
73 | ],
74 | selected: 0,
75 | onChange: () => {},
76 | };
77 |
78 | export const Controlled = ({value}: typeof meta.args) => {
79 | const [selected, setSelected] = useState(0);
80 |
81 | return ;
82 | };
83 |
84 | const meta = {
85 | title: "Controlled & Uncontrolled/Controlled",
86 | component: Tabs,
87 | args: props,
88 | parameters: {
89 | layout: "centered",
90 | },
91 | };
92 |
93 | export default meta;
94 |
--------------------------------------------------------------------------------
/src/components/property-card/controlled-uncontrolled/controlled/index.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | export interface TabItem {
4 | title: string;
5 | content: React.ReactNode;
6 | }
7 |
8 | export default function Tabs({
9 | value,
10 | selected,
11 | onChange,
12 | }: {
13 | value: TabItem[];
14 | selected: number;
15 | onChange: (selected: number) => void;
16 | }) {
17 | return (
18 |
19 | {/* Tab Titles */}
20 |
21 | {value.map((tab, idx) => (
22 |
34 | ))}
35 |
36 | {/* Tab Content */}
37 |
{value[selected]?.content}
38 |
39 | );
40 | }
41 |
--------------------------------------------------------------------------------
/src/components/property-card/controlled-uncontrolled/uncontrolled/index.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import {useState} from "react";
4 |
5 | export interface TabItem {
6 | title: string;
7 | content: React.ReactNode;
8 | }
9 |
10 | export default function Tabs({value}: {value: TabItem[]}) {
11 | const [selected, setSelected] = useState(0);
12 |
13 | return (
14 |
15 | {/* Tab Titles */}
16 |
17 | {value.map((tab, idx) => (
18 |
30 | ))}
31 |
32 | {/* Tab Content */}
33 |
{value[selected]?.content}
34 |
35 | );
36 | }
37 |
--------------------------------------------------------------------------------
/src/components/property-card/controlled-uncontrolled/uncontrolled/uncontrolled.stories.tsx:
--------------------------------------------------------------------------------
1 | import PropertyCard, {type Property} from "../../basic/value-prop";
2 |
3 | import Tabs from ".";
4 |
5 | const PROPERTIES: Property[] = [
6 | {
7 | id: 1,
8 | description: "This is a modern architectural marvel located in Beverly Hills, CA.",
9 | agent: {
10 | name: "John Doe",
11 | image: "/placeholder.svg?height=300&width=400",
12 | phone: "+1234567890",
13 | },
14 | title: "Modern Architectural Marvel",
15 | location: "Beverly Hills, CA",
16 | price: 2850000,
17 | bedrooms: 4,
18 | bathrooms: 3,
19 | sqft: 3200,
20 | images: ["/placeholder.svg?height=300&width=400", "/placeholder.svg?height=300&width=300"],
21 | features: ["Pool", "Garden", "Smart Home"],
22 | },
23 | {
24 | id: 2,
25 | description: "This is a modern architectural marvel located in Beverly Hills, CA.",
26 | agent: {
27 | name: "John Doe",
28 | image: "/placeholder.svg?height=300&width=400",
29 | phone: "+1234567890",
30 | },
31 | title: "Barbie House",
32 | location: "Malibu, CA",
33 | price: 2850000,
34 | bedrooms: 4,
35 | bathrooms: 3,
36 | sqft: 3200,
37 | images: ["/placeholder.svg?height=300&width=400", "/placeholder.svg?height=300&width=300"],
38 | features: ["Pool", "Garden", "Smart Home"],
39 | },
40 | {
41 | id: 3,
42 | description: "This is a modern architectural marvel located in Beverly Hills, CA.",
43 | agent: {
44 | name: "John Doe",
45 | image: "/placeholder.svg?height=300&width=400",
46 | phone: "+1234567890",
47 | },
48 | title: "The White House",
49 | location: "Washington, DC",
50 | price: 2850000,
51 | bedrooms: 4,
52 | bathrooms: 3,
53 | sqft: 3200,
54 | images: ["/placeholder.svg?height=300&width=400", "/placeholder.svg?height=300&width=300"],
55 | features: ["Pool", "Garden", "Smart Home"],
56 | },
57 | ];
58 |
59 | const props: React.ComponentProps = {
60 | value: [
61 | {
62 | title: "Featured",
63 | content: PROPERTIES.slice(0, 2).map((property) => (
64 |
65 | )),
66 | },
67 | {
68 | title: "All",
69 | content: PROPERTIES.map((property) => ),
70 | },
71 | ],
72 | };
73 |
74 | export const Uncontrolled = ({value}: typeof meta.args) => ;
75 |
76 | const meta = {
77 | title: "Controlled & Uncontrolled/Uncontrolled",
78 | component: Tabs,
79 | parameters: {
80 | layout: "centered",
81 | },
82 | args: props,
83 | };
84 |
85 | export default meta;
86 |
--------------------------------------------------------------------------------
/src/components/property-card/render-props/index.tsx:
--------------------------------------------------------------------------------
1 | import {useState} from "react";
2 |
3 | export default function Search({
4 | children,
5 | placeholder = "Search...",
6 | }: {
7 | children: (query: string) => React.ReactNode;
8 | placeholder?: string;
9 | }) {
10 | const [query, setQuery] = useState("");
11 |
12 | return (
13 |
14 | setQuery(e.target.value)}
20 | />
21 | {children(query)}
22 |
23 | );
24 | }
25 |
--------------------------------------------------------------------------------
/src/components/property-card/render-props/render-props.stories.tsx:
--------------------------------------------------------------------------------
1 | import PropertyCard, {type Property} from "../basic/value-prop";
2 |
3 | import WithSearch from ".";
4 |
5 | const props: {properties: Property[]} = {
6 | properties: [
7 | {
8 | id: 1,
9 | description: "This is a modern architectural marvel located in Beverly Hills, CA.",
10 | agent: {
11 | name: "John Doe",
12 | image: "/placeholder.svg?height=300&width=400",
13 | phone: "+1234567890",
14 | },
15 | title: "Modern Architectural Marvel",
16 | location: "Beverly Hills, CA",
17 | price: 2850000,
18 | bedrooms: 4,
19 | bathrooms: 3,
20 | sqft: 3200,
21 | images: ["/placeholder.svg?height=300&width=400", "/placeholder.svg?height=300&width=300"],
22 | features: ["Pool", "Garden", "Smart Home"],
23 | },
24 | {
25 | id: 2,
26 | description: "This is a modern architectural marvel located in Beverly Hills, CA.",
27 | agent: {
28 | name: "John Doe",
29 | image: "/placeholder.svg?height=300&width=400",
30 | phone: "+1234567890",
31 | },
32 | title: "Barbie House",
33 | location: "Malibu, CA",
34 | price: 2850000,
35 | bedrooms: 4,
36 | bathrooms: 3,
37 | sqft: 3200,
38 | images: ["/placeholder.svg?height=300&width=400", "/placeholder.svg?height=300&width=300"],
39 | features: ["Pool", "Garden", "Smart Home"],
40 | },
41 | {
42 | id: 3,
43 | description: "This is a modern architectural marvel located in Beverly Hills, CA.",
44 | agent: {
45 | name: "John Doe",
46 | image: "/placeholder.svg?height=300&width=400",
47 | phone: "+1234567890",
48 | },
49 | title: "The White House",
50 | location: "Washington, DC",
51 | price: 2850000,
52 | bedrooms: 4,
53 | bathrooms: 3,
54 | sqft: 3200,
55 | images: ["/placeholder.svg?height=300&width=400", "/placeholder.svg?height=300&width=300"],
56 | features: ["Pool", "Garden", "Smart Home"],
57 | },
58 | ],
59 | };
60 |
61 | export const Search = ({properties}: typeof meta.args) => (
62 |
63 | {(query) => {
64 | const filteredProperties = properties.filter((property) =>
65 | property.title.toLowerCase().includes(query.toLowerCase()),
66 | );
67 |
68 | return (
69 |
70 | {filteredProperties.map((property) => (
71 |
72 | ))}
73 |
74 | );
75 | }}
76 |
77 | );
78 |
79 | const meta = {
80 | title: "Render Props/Search",
81 | component: WithSearch,
82 | args: props,
83 | };
84 |
85 | export default meta;
86 |
--------------------------------------------------------------------------------
/src/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import {Slot} from "@radix-ui/react-slot";
3 | import {cva, type VariantProps} from "class-variance-authority";
4 |
5 | import {cn} from "@/lib/utils";
6 |
7 | const buttonVariants = cva(
8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
9 | {
10 | variants: {
11 | variant: {
12 | default: "bg-primary text-primary-foreground hover:bg-primary/90",
13 | destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
14 | outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
15 | secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
16 | ghost: "hover:bg-accent hover:text-accent-foreground",
17 | link: "text-primary underline-offset-4 hover:underline",
18 | },
19 | size: {
20 | default: "h-10 px-4 py-2",
21 | sm: "h-9 rounded-md px-3",
22 | lg: "h-11 rounded-md px-8",
23 | icon: "h-10 w-10",
24 | },
25 | },
26 | defaultVariants: {
27 | variant: "default",
28 | size: "default",
29 | },
30 | },
31 | );
32 |
33 | export interface ButtonProps
34 | extends React.ButtonHTMLAttributes,
35 | VariantProps {
36 | asChild?: boolean;
37 | }
38 |
39 | const Button = React.forwardRef(
40 | ({className, variant, size, asChild = false, ...props}, ref) => {
41 | const Comp = asChild ? Slot : "button";
42 |
43 | return ;
44 | },
45 | );
46 |
47 | Button.displayName = "Button";
48 |
49 | export {Button, buttonVariants};
50 |
--------------------------------------------------------------------------------
/src/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import {type ClassValue, clsx} from "clsx";
2 | import {twMerge} from "tailwind-merge";
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs));
6 | }
7 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es6",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "noEmit": true,
9 | "esModuleInterop": true,
10 | "module": "esnext",
11 | "moduleResolution": "bundler",
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "jsx": "preserve",
15 | "incremental": true,
16 | "plugins": [
17 | {
18 | "name": "next"
19 | }
20 | ],
21 | "paths": {
22 | "@/*": ["./src/*"]
23 | }
24 | },
25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
26 | "exclude": ["node_modules"]
27 | }
28 |
--------------------------------------------------------------------------------
/vitest.config.ts:
--------------------------------------------------------------------------------
1 | import path from "node:path";
2 | import { fileURLToPath } from "node:url";
3 |
4 | import { defineConfig } from "vitest/config";
5 |
6 | import { storybookTest } from "@storybook/addon-vitest/vitest-plugin";
7 |
8 | const dirname =
9 | typeof __dirname !== "undefined"
10 | ? __dirname
11 | : path.dirname(fileURLToPath(import.meta.url));
12 |
13 | // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon
14 | export default defineConfig({
15 | test: {
16 | workspace: [
17 | {
18 | extends: true,
19 | plugins: [
20 | // The plugin will run tests for the stories defined in your Storybook config
21 | // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest
22 | storybookTest({ configDir: path.join(dirname, ".storybook") }),
23 | ],
24 | test: {
25 | name: "storybook",
26 | browser: {
27 | enabled: true,
28 | headless: true,
29 | provider: "playwright",
30 | instances: [{ browser: "chromium" }],
31 | },
32 | setupFiles: [".storybook/vitest.setup.ts"],
33 | },
34 | },
35 | ],
36 | },
37 | });
38 |
--------------------------------------------------------------------------------
/vitest.shims.d.ts:
--------------------------------------------------------------------------------
1 | ///
--------------------------------------------------------------------------------