Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .storybook/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,18 @@ const config: StorybookConfig = {
// Ensure proper alias resolution
config.resolve = config.resolve || {};
config.resolve.alias = {
// Mock DB-coupled server actions so components that import them can render
// in Storybook without bundling the Postgres client or next/cache. The
// more specific subpath must be listed before the barrel.
"@/lib/db_actions/props": new URL(
"./mocks/db_actions-props.ts",
import.meta.url,
).pathname,
"@/lib/db_actions": new URL("./mocks/db_actions.ts", import.meta.url)
.pathname,
// next/link's app-router internals trip a circular-import TDZ under Vite
// dev; render a plain anchor instead.
"next/link": new URL("./mocks/next-link.tsx", import.meta.url).pathname,
...config.resolve.alias,
"@": new URL("../", import.meta.url).pathname,
};
Expand Down
4 changes: 4 additions & 0 deletions .storybook/mocks/db_actions-props.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Storybook mock for `@/lib/db_actions/props` (used by PropEditDialog).
import { success } from "@/lib/server-action-result";

export const updateProp = async () => success(undefined);
6 changes: 6 additions & 0 deletions .storybook/mocks/db_actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// Storybook mock for `@/lib/db_actions`. Stories never touch the database;
// these stubs just return a success result so server-action hooks resolve.
import { success } from "@/lib/server-action-result";

export const createForecast = async () => success(undefined);
export const updateForecast = async () => success(undefined);
28 changes: 28 additions & 0 deletions .storybook/mocks/next-link.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import * as React from "react";

/**
* Storybook stand-in for `next/link`: render a plain anchor.
*
* The real module pulls in Next's app-router client internals, whose circular
* ESM imports trip a "Cannot access 'default' before initialization" TDZ error
* under Vite's on-demand dev transform (production rollup hoists around it).
*/
type LinkProps = React.PropsWithChildren<
React.AnchorHTMLAttributes<HTMLAnchorElement> & {
href: string | { pathname?: string };
}
>;

const Link = React.forwardRef<HTMLAnchorElement, LinkProps>(function Link(
{ href, children, ...props },
ref,
) {
const hrefStr = typeof href === "string" ? href : (href?.pathname ?? "#");
return (
<a ref={ref} href={hrefStr} {...props}>
{children}
</a>
);
});

export default Link;
90 changes: 90 additions & 0 deletions components/forecast-card/editable-forecast-card.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { EditableForecastCard } from "./editable-forecast-card";
import { makeProp } from "./forecast-card.fixtures";

// Note: in Storybook there's no logged-in user (the /api/me fetch returns
// null), so the admin "edit prop" pencil is hidden. The Save / Cancel buttons
// only appear once you change the forecast (type a new value in the % box).
// Saving is mocked — see .storybook/mocks/db_actions.ts.
const meta = {
title: "Forecast/EditableForecastCard",
component: EditableForecastCard,
parameters: {
layout: "padded",
},
tags: ["autodocs"],
decorators: [
(Story) => (
<div className="w-[640px] max-w-full">
<Story />
</div>
),
],
argTypes: {
prop: {
control: false,
description: "The prop plus the user's forecast and community average",
},
onForecastUpdate: {
control: false,
description: "Called after a forecast is created or updated",
},
},
} satisfies Meta<typeof EditableForecastCard>;

export default meta;
type Story = StoryObj<typeof meta>;

// Has a forecast: needle (with community-average ghost) and the editable % box.
export const Default: Story = {
args: {
prop: makeProp({ user_forecast: 0.6, user_forecast_id: 10 }),
},
};

// No forecast yet: placeholder + empty % box; type a value to set it.
export const NoForecast: Story = {
args: {
prop: makeProp({ user_forecast: null, user_forecast_id: null }),
},
};

// Low probability -> needle points left, into the red.
export const LowProbability: Story = {
args: {
prop: makeProp({ user_forecast: 0.12, user_forecast_id: 10 }),
},
};

// High probability -> needle points right, into the green.
export const HighProbability: Story = {
args: {
prop: makeProp({ user_forecast: 0.91, user_forecast_id: 10 }),
},
};

// Long prop text wraps; the edit pencil stays beside it.
export const LongPropText: Story = {
args: {
prop: makeProp({
user_forecast: 0.45,
user_forecast_id: 10,
prop_text:
"Will at least three of the five largest economies report **negative** quarterly GDP growth in the same quarter before the end of the year?",
prop_notes:
"Measured by nominal GDP; figures from each country's official statistics agency.",
}),
},
};

// Long notes wrap rather than spilling off the side.
export const LongNotes: Story = {
args: {
prop: makeProp({
user_forecast: 0.6,
user_forecast_id: 10,
prop_notes:
"Resolution uses the seasonally adjusted figures published by each country's primary national statistics office; preliminary estimates count, and later revisions will not change a resolution once it has been finalized by the competition admins.",
}),
},
};
Loading
Loading