MDX/Addac120sPriceTable
Default
English
addac120s-price-table.tsx
import { productsMapping } from '@/lib/data/products-mapping';
import { computeBundleComponentSum } from '@/lib/data/products/compute-bundle-component-sum';
import { productDetailHref } from '@/lib/utils/product-detail-href';
import { Table } from '@components/article/table';
import { H2 } from '@components/article/h2';
import { H3 } from '@components/article/h3';
import { ADDAC120S_SERIES_ORDER } from '@data/addac120s-series-order.mjs';
import type { Locale } from '@/lib/i18n/types';
interface Addac120sPriceTableProps {
locale?: Locale;
}
const HEADING = {
ja: 'ADDAC120s Four Stringsシリーズ 価格一覧',
en: 'ADDAC120s Four Strings Series — Price List',
} as const;
/**
* Value message — the on-page "a bundle is the reasonable choice" guidance the
* shop owner asked for. The bundle systems are priced below the sum of their
* component modules; the dedicated bundle-discount table makes that explicit.
*/
const CAPTION = {
ja: 'セット(システム)は、モジュールを単品でそろえるよりも割引価格でお得です。',
en: 'The bundle systems cost less than buying the modules individually.',
} as const;
const GROUP = {
ja: { modules: 'モジュール / フレーム', bundles: 'セット(システム)' },
en: { modules: 'Modules / Frames', bundles: 'Bundle systems' },
} as const;
const MODULE_COL = {
ja: { product: '商品名', price: '価格(税込)' },
en: { product: 'Product', price: 'Price (incl. tax)' },
} as const;
const BUNDLE_COL = {
ja: {
product: '商品名',
setPrice: 'セット価格(税込)',
sum: '単価合計(税込)',
discount: 'バンドルディスカウント',
},
en: {
product: 'Product',
setPrice: 'Set price (incl. tax)',
sum: 'Total of unit prices (incl. tax)',
discount: 'Bundle discount',
},
} as const;
/**
* Futura numerals — matches the price styling used across the site (PurchaseNav:
* `non-clickable-status-item.tsx`, `purchase-nav/constants.ts`), which is
* `font-futura` WITHOUT `font-bold`. Adding `font-bold` makes the numerals fall
* back off Futura to a generic bold sans (the `Futura` system face the stack
* resolves to has no usable 700 weight), so keep every price cell non-bold.
*/
const PRICE_CLASS = 'font-futura whitespace-nowrap';
/** Bundle-discount accent (orange `zd-strong`) — same token discount-note.tsx uses. */
const DISCOUNT_CLASS = 'font-futura whitespace-nowrap text-zd-strong';
function yen(n: number): string {
return `¥${n.toLocaleString()}`;
}
interface BundleEconomics {
referencePrice: number;
savings: number;
discountPercent: number;
}
/**
* Mirror of the PurchaseNav bundle-discount derivation
* (src/lib/resolve-purchase-nav-client-props.ts): referencePrice = Σ parts,
* savings = Σ − price, discountPercent = round((Σ − price)/Σ * 1000)/10.
* Returns null unless the component sum is fully resolved and exceeds the price
* — so a row never shows a discount figure it can't back up.
*/
function bundleEconomics(componentSlugs: string[], price: number): BundleEconomics | null {
const { sum, allPriced } = computeBundleComponentSum(componentSlugs);
if (!allPriced || sum <= price) return null;
return {
referencePrice: sum,
savings: sum - price,
discountPercent: Math.round(((sum - price) / sum) * 1000) / 10,
};
}
function NameCell({ slug, locale }: { slug: string; locale: Locale }) {
const product = productsMapping[slug];
if (!product) return null;
const href = productDetailHref(product.detailHref, locale);
const label = (
<>
<span className="font-bold">{product.name}</span>
{product.subtitle ? <span> {product.subtitle}</span> : null}
</>
);
// addac901m (detailHref: null) and any untranslated EN page resolve to null → plain text, no 404 link.
return href ? (
<a href={href} className="underline">
{label}
</a>
) : (
label
);
}
/**
* Shared, data-driven price list for the ADDAC120s "Four Strings" series.
*
* Renders the series as two tables, each in the canonical order (modules/frames
* by module number, then bundle systems by price ascending — see
* `src/data/addac120s-series-order.mjs`):
* 1. Modules / Frames — name + plain price.
* 2. Bundle systems — a bundle-discount table with the set price, the Σ-parts
* unit total, and the savings (バンドルディスカウント) in the accent color,
* making each bundle's value explicit.
*
* Prices render in the Futura style used by the PurchaseNav UI.
*
* Plain SSR component (no island wrapper) — safe to import productsMapping /
* computeBundleComponentSum directly.
*/
export function Addac120sPriceTable({ locale = 'ja' }: Addac120sPriceTableProps) {
const group = GROUP[locale];
const moduleCol = MODULE_COL[locale];
const bundleCol = BUNDLE_COL[locale];
const moduleSlugs: string[] = [];
const bundleSlugs: string[] = [];
for (const slug of ADDAC120S_SERIES_ORDER) {
const product = productsMapping[slug];
if (!product) continue;
if (product.bundle != null) {
bundleSlugs.push(slug);
} else {
moduleSlugs.push(slug);
}
}
return (
<section className="mb-vgap-md">
<H2>{HEADING[locale]}</H2>
<p className="mb-vgap-sm text-sm">{CAPTION[locale]}</p>
<H3>{group.modules}</H3>
<Table className="mb-vgap-md">
<thead>
<tr>
<th className="text-left">{moduleCol.product}</th>
<th className="text-left">{moduleCol.price}</th>
</tr>
</thead>
<tbody>
{moduleSlugs.map((slug) => {
const product = productsMapping[slug];
const price = typeof product.price === 'number' ? product.price : null;
return (
<tr key={slug}>
<td>
<NameCell slug={slug} locale={locale} />
</td>
<td className={PRICE_CLASS}>{price != null ? yen(price) : '—'}</td>
</tr>
);
})}
</tbody>
</Table>
<H3>{group.bundles}</H3>
<Table>
<thead>
<tr>
<th className="text-left">{bundleCol.product}</th>
<th className="text-left">{bundleCol.setPrice}</th>
<th className="text-left">{bundleCol.sum}</th>
<th className="text-left text-zd-strong">{bundleCol.discount}</th>
</tr>
</thead>
<tbody>
{bundleSlugs.map((slug) => {
const product = productsMapping[slug];
const price = typeof product.price === 'number' ? product.price : null;
const econ =
price != null && product.bundle != null
? bundleEconomics(product.bundle.componentSlugs, price)
: null;
return (
<tr key={slug}>
<td>
<NameCell slug={slug} locale={locale} />
</td>
<td className={PRICE_CLASS}>{price != null ? yen(price) : '—'}</td>
<td className={PRICE_CLASS}>{econ ? yen(econ.referencePrice) : '—'}</td>
<td className={DISCOUNT_CLASS}>{econ ? yen(econ.savings) : '—'}</td>
</tr>
);
})}
</tbody>
</Table>
</section>
);
}
addac120s-price-table.stories.tsx
import { Addac120sPriceTable } from './addac120s-price-table';
/**
* Addac120sPriceTable
*
* Shared, data-driven price list for the ADDAC120s "Four Strings" series,
* rendered as two tables: a Modules/Frames table (name + plain price) and a
* bundle-discount table (set price, Σ-parts unit total, and the savings —
* バンドルディスカウント — in the accent color). Prices use the site's Futura
* style. Rows follow the canonical order (modules by №, then bundles by price ↑).
*/
export const meta = {
title: 'MDX/Addac120sPriceTable',
};
/** Japanese (default) */
export const Default = () => <Addac120sPriceTable locale="ja" />;
/** English */
export const English = () => <Addac120sPriceTable locale="en" />;