diff --git a/apps/portal/.storybook/main.ts b/apps/portal/.storybook/main.ts new file mode 100644 index 00000000..b98c0f89 --- /dev/null +++ b/apps/portal/.storybook/main.ts @@ -0,0 +1,48 @@ +import type { StorybookConfig } from "@storybook/react-vite"; +import tailwindcss from "@tailwindcss/vite"; +import path from "path"; + +const config: StorybookConfig = { + stories: ["../src/**/*.stories.@(ts|tsx)"], + addons: ["@storybook/addon-essentials"], + framework: { + name: "@storybook/react-vite", + options: {}, + }, + staticDirs: ["../public"], + viteFinal: async config => { + config.plugins = config.plugins || []; + config.plugins.push(tailwindcss()); + + // Ensure JSX runtime is available (auto-imports React) + config.esbuild = { + ...config.esbuild, + jsx: "automatic", + }; + + config.resolve = config.resolve || {}; + config.resolve.alias = { + ...config.resolve.alias, + "@": path.resolve(__dirname, "../src"), + "next/link": path.resolve(__dirname, "mocks/next-link.tsx"), + "next/image": path.resolve(__dirname, "mocks/next-image.tsx"), + "next/navigation": path.resolve(__dirname, "mocks/next-navigation.tsx"), + }; + + // Disable PostCSS — @tailwindcss/vite handles CSS directly + config.css = config.css || {}; + config.css.postcss = { plugins: [] }; + + // Polyfill process.env for Next.js code that uses it + config.define = { + ...config.define, + "process.env": JSON.stringify({ + NODE_ENV: "development", + }), + }; + + return config; + }, +}; + +export default config; diff --git a/apps/portal/.storybook/mocks/next-image.tsx b/apps/portal/.storybook/mocks/next-image.tsx new file mode 100644 index 00000000..519ddad7 --- /dev/null +++ b/apps/portal/.storybook/mocks/next-image.tsx @@ -0,0 +1,24 @@ +import React from "react"; + +interface ImageProps extends React.ImgHTMLAttributes { + src: string; + alt: string; + width?: number; + height?: number; + fill?: boolean; + priority?: boolean; +} + +const Image = ({ src, alt, width, height, fill, priority: _priority, ...props }: ImageProps) => ( + // eslint-disable-next-line @next/next/no-img-element + {alt} +); + +export default Image; diff --git a/apps/portal/.storybook/mocks/next-link.tsx b/apps/portal/.storybook/mocks/next-link.tsx new file mode 100644 index 00000000..ff16f4f3 --- /dev/null +++ b/apps/portal/.storybook/mocks/next-link.tsx @@ -0,0 +1,13 @@ +import React from "react"; + +const Link = React.forwardRef< + HTMLAnchorElement, + React.AnchorHTMLAttributes & { href: string } +>(({ href, children, ...props }, ref) => ( + + {children} + +)); +Link.displayName = "Link"; + +export default Link; diff --git a/apps/portal/.storybook/mocks/next-navigation.tsx b/apps/portal/.storybook/mocks/next-navigation.tsx new file mode 100644 index 00000000..76aa6a7f --- /dev/null +++ b/apps/portal/.storybook/mocks/next-navigation.tsx @@ -0,0 +1,30 @@ +export function useRouter() { + return { + push: (url: string) => { + window.location.hash = url; + }, + replace: (url: string) => { + window.location.hash = url; + }, + back: () => { + window.history.back(); + }, + forward: () => { + window.history.forward(); + }, + refresh: () => {}, + prefetch: () => Promise.resolve(), + }; +} + +export function usePathname() { + return "/"; +} + +export function useSearchParams() { + return new URLSearchParams(); +} + +export function useParams() { + return {}; +} diff --git a/apps/portal/.storybook/preview.ts b/apps/portal/.storybook/preview.ts new file mode 100644 index 00000000..8cd71b2e --- /dev/null +++ b/apps/portal/.storybook/preview.ts @@ -0,0 +1,18 @@ +import type { Preview } from "@storybook/react"; +import "../src/app/globals.css"; + +const preview: Preview = { + parameters: { + layout: "centered", + backgrounds: { + default: "light", + values: [ + { name: "light", value: "#f9f9f9" }, + { name: "white", value: "#ffffff" }, + { name: "dark", value: "#1a1a2e" }, + ], + }, + }, +}; + +export default preview;