From 34f77c2e24259d0c4f42f8335e0ba2dbea7cf790 Mon Sep 17 00:00:00 2001 From: Anton BV Date: Fri, 14 Aug 2026 23:45:26 +0200 Subject: [PATCH 1/6] restore standalone Basket surface for POP recovery --- src/components/basket/basket-landing.tsx | 600 +++++++++++++++++++++++ src/lib/basket/data.ts | 276 +++++++++++ src/lib/basket/formulas.ts | 98 ++++ src/lib/basket/products.ts | 118 +++++ src/lib/basket/types.ts | 80 +++ 5 files changed, 1172 insertions(+) create mode 100644 src/components/basket/basket-landing.tsx create mode 100644 src/lib/basket/data.ts create mode 100644 src/lib/basket/formulas.ts create mode 100644 src/lib/basket/products.ts create mode 100644 src/lib/basket/types.ts diff --git a/src/components/basket/basket-landing.tsx b/src/components/basket/basket-landing.tsx new file mode 100644 index 00000000..006e72a3 --- /dev/null +++ b/src/components/basket/basket-landing.tsx @@ -0,0 +1,600 @@ +"use client"; + +import Image from "next/image"; +import Link from "next/link"; +import { useMemo, useState, type ReactNode } from "react"; +import { BASKET_MARKETS, BASKET_PRODUCTS } from "@/lib/basket/products"; +import { + getBasketCompare, + getBasketHistory, + getBasketLatest, + getBasketMonthlyReport, +} from "@/lib/basket/data"; +import type { BasketChartSeries, BasketLatestItem, BasketMarket } from "@/lib/basket/types"; + +export function BasketLanding({ embed = false, initialMarket = "GLOBAL" }: { embed?: boolean; initialMarket?: BasketMarket }) { + const [market, setMarket] = useState(initialMarket); + const latest = useMemo(() => getBasketLatest(market), [market]); + const history = useMemo(() => getBasketHistory(market), [market]); + const compare = useMemo(() => getBasketCompare(market), [market]); + const report = getBasketMonthlyReport(); + + return ( +
+ + {!embed ? : null} +
+
+

Consumer basket indices by 1D3X

+

+ + 1D3X Basket + + + Consumer basket indices for the real world. + +

+

+ Big Mac. Starbucks Latte. iPhone. Everyday prices turned into global signals. +

+ +
+ Independent fan project. Data sources: +
+ The Economist + McDonald's + Starbucks + Apple +
+
+
+ +
+ +
+ {latest.products.map((item) => ( + + ))} +
+
+ Baseline: United States (US) + Values show USD prices and percent change vs baseline + Last update: {new Date(latest.updatedAt).toUTCString()} + Coverage: {latest.composite.coverage.label} +
+ +
+
+
+
+

Analytics Lab

+

+ Compare consumer basket indices with currencies, commodities and SPIKE Spot Index positions. +

+
+
+ {BASKET_MARKETS.map((item) => ( + + ))} +
+
+
+ + +
+

Normalized to 100. Ukraine mode adds selected SPIKE Spot Index overlays.

+
+
+ +
+ +
    +
  • Prices are collected from credited public and monitored sources.
  • +
  • Local prices are converted to USD using daily FX rates.
  • +
  • Each index shows price level vs US baseline and movement over time.
  • +
  • Basket Composite averages available verified or monitored components.
  • +
+
+ +
+ API access visual +
+
    +
  • Latest values and history
  • +
  • Cross-index comparison
  • +
  • Source metadata and confidence
  • +
  • Embeddable widgets
  • +
+

Access available on request.

+
+ + Monthly Basket Review visual +

{report.month}

+

{report.title}

+

{report.summary}

+
+
+ + {!embed ? : null} +
+ ); +} + +export function BasketCardsEmbed({ market = "GLOBAL" }: { market?: BasketMarket }) { + const latest = getBasketLatest(market); + + return ( +
+ +
+ {latest.products.map((item) => ( + + ))} +
+
+ ); +} + +export function BasketChartEmbed({ market = "GLOBAL" }: { market?: BasketMarket }) { + return ( +
+ +
+

1D3X Basket Analytics

+ +
+
+ ); +} + +export function BasketHeroEmbed({ market = "GLOBAL" }: { market?: BasketMarket }) { + const latest = getBasketLatest(market); + + return ( +
+ +
+
+

+ 1D3X Basket +

+

Consumer basket indices for the real world.

+

Coverage: {latest.composite.coverage.label}

+
+ +
+
+ ); +} + +function BasketHeader({ market, setMarket }: { market: BasketMarket; setMarket: (market: BasketMarket) => void }) { + return ( +
+ +
+ ); +} + +function ProductStage({ compact = false }: { compact?: boolean }) { + return ( +
+
+
+
+
+
+
+
+
+
+
+ Big Mac product visual +
+
+ +
+ +
+ ); +} + +function BasketIndexCard({ item, compact = false }: { item: BasketLatestItem; compact?: boolean }) { + const product = BASKET_PRODUCTS[item.product]; + const value = item.valueUsd === null ? "Unavailable" : item.valueUsd > 100 ? item.valueUsd.toLocaleString("en-US") : item.valueUsd.toFixed(2); + const trend = item.changeYoY === null ? "Source pending" : `${item.changeYoY > 0 ? "+" : ""}${item.changeYoY}% vs US baseline`; + + return ( +
+
+

{product.name}

+ + {item.confidence} + +
+
+ {value} + {item.valueUsd !== null ? USD : null} +
+

{trend}

+ +

Source: {item.source.label}

+ {item.note ?

{item.note}

: null} +
+ ); +} + +function BasketLineChart({ series, compact = false }: { series: BasketChartSeries[]; compact?: boolean }) { + const visible = series.slice(0, compact ? 6 : 10); + const values = visible.flatMap((item) => item.points.map((point) => point.value)); + const min = Math.min(...values) - 4; + const max = Math.max(...values) + 4; + + return ( +
+
+ {visible.map((item) => ( + + + {item.label} + + ))} +
+ + {[20, 40, 60, 80].map((y) => ( + + ))} + {visible.map((item) => ( + point.value), min, max)} + stroke={item.color} + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth={item.id === "basket" ? "2.6" : "1.7"} + vectorEffect="non-scaling-stroke" + /> + ))} + +
+ ); +} + +function CorrelationPanel({ correlations }: { correlations: Array<{ id: string; label: string; correlationToBasket: number | null }> }) { + return ( + + ); +} + +function InfoPanel({ id, title, cta, children }: { id: string; title: string; cta: string; children: ReactNode }) { + return ( +
+

{title}

+
{children}
+ +
+ ); +} + +function BasketFooter() { + return ( +
+
+
+

1D3X

+

+ 1D3X Basket is an independent fan and research project. Big Mac, McDonald's, Starbucks, iPhone, Apple and The Economist are trademarks or properties of their respective owners. No partnership, sponsorship or endorsement is implied. Data for informational purposes only. +

+
+
+ About + Contact +
+
+
+ ); +} + +function Sparkline({ values, color }: { values: number[]; color: string }) { + return ( + + + + ); +} + +function ArrowIcon() { + return ( + + ); +} + +function toPoints(values: number[], min: number, max: number) { + const range = Math.max(max - min, 1); + return values + .map((value, index) => { + const x = values.length === 1 ? 0 : (index / (values.length - 1)) * 100; + const y = 88 - ((value - min) / range) * 76; + return `${x},${y}`; + }) + .join(" "); +} + +function BasketStyles() { + return ( + + ); +} diff --git a/src/lib/basket/data.ts b/src/lib/basket/data.ts new file mode 100644 index 00000000..b366c686 --- /dev/null +++ b/src/lib/basket/data.ts @@ -0,0 +1,276 @@ +import { BASKET_PRODUCTS, BASKET_SOURCES } from "@/lib/basket/products"; +import { + calculateComposite, + calculateCorrelation, + enrichObservation, + rebaseSeriesTo100, +} from "@/lib/basket/formulas"; +import type { + BasketChartSeries, + BasketLatestResponse, + BasketMarket, + BasketObservation, + BasketProductId, +} from "@/lib/basket/types"; + +const updatedAt = "2026-06-30T09:30:00.000Z"; + +const observations: BasketObservation[] = [ + { + product: "bigmac", + market: "GLOBAL", + date: "2026-06-30", + valueUsd: 6.12, + baselineUsd: 5.21, + source: BASKET_SOURCES.economistBigMac, + confidence: "verified", + status: "published", + }, + { + product: "latte", + market: "GLOBAL", + date: "2026-06-30", + valueUsd: 5.45, + baselineUsd: 4.86, + source: BASKET_SOURCES.starbucksMonitor, + confidence: "monitored", + status: "monitored", + }, + { + product: "iphone", + market: "GLOBAL", + date: "2026-06-30", + valueUsd: 1099, + baselineUsd: 1011, + source: BASKET_SOURCES.appleStore, + confidence: "seed", + status: "monitored", + }, + { + product: "bigmac", + market: "US", + date: "2026-06-30", + valueUsd: 5.21, + baselineUsd: 5.21, + source: BASKET_SOURCES.economistBigMac, + confidence: "verified", + status: "published", + }, + { + product: "latte", + market: "US", + date: "2026-06-30", + valueUsd: 4.86, + baselineUsd: 4.86, + source: BASKET_SOURCES.starbucksMonitor, + confidence: "monitored", + status: "monitored", + }, + { + product: "iphone", + market: "US", + date: "2026-06-30", + valueUsd: 999, + baselineUsd: 999, + source: BASKET_SOURCES.appleStore, + confidence: "verified", + status: "published", + }, + { + product: "bigmac", + market: "UA", + date: "2026-06-30", + valueUsd: 4.34, + baselineUsd: 5.21, + source: BASKET_SOURCES.economistBigMac, + confidence: "verified", + status: "published", + }, + { + product: "latte", + market: "UA", + date: "2026-06-30", + valueUsd: null, + baselineUsd: 4.86, + source: BASKET_SOURCES.starbucksMonitor, + confidence: "unavailable", + status: "unavailable", + note: "Ukraine Starbucks Latte source is not verified for MVP.", + }, + { + product: "iphone", + market: "UA", + date: "2026-06-30", + valueUsd: 1180, + baselineUsd: 999, + source: BASKET_SOURCES.appleStore, + confidence: "seed", + status: "monitored", + }, +]; + +const productSparklines: Record = { + bigmac: [97, 98, 96, 99, 101, 103, 102, 106, 108, 111, 113, 117], + latte: [94, 96, 99, 98, 101, 104, 103, 106, 108, 110, 111, 112], + iphone: [100, 101, 103, 105, 104, 106, 108, 109, 110, 111, 112, 113], +}; + +const yoy: Record = { + bigmac: 17.4, + latte: 12.1, + iphone: 8.7, +}; + +const baseHistory = [ + { date: "2025-07-01", basket: 100, bigmac: 100, latte: 100, iphone: 100, usd: 100, brent: 100, gold: 100 }, + { date: "2025-08-01", basket: 104, bigmac: 103, latte: 101, iphone: 107, usd: 101, brent: 95, gold: 102 }, + { date: "2025-09-01", basket: 108, bigmac: 105, latte: 103, iphone: 116, usd: 102, brent: 91, gold: 105 }, + { date: "2025-10-01", basket: 106, bigmac: 102, latte: 105, iphone: 111, usd: 103, brent: 86, gold: 107 }, + { date: "2025-11-01", basket: 109, bigmac: 108, latte: 106, iphone: 114, usd: 101, brent: 82, gold: 110 }, + { date: "2025-12-01", basket: 111, bigmac: 110, latte: 108, iphone: 116, usd: 100, brent: 88, gold: 113 }, + { date: "2026-01-01", basket: 114, bigmac: 112, latte: 110, iphone: 121, usd: 102, brent: 92, gold: 116 }, + { date: "2026-02-01", basket: 116, bigmac: 115, latte: 111, iphone: 123, usd: 103, brent: 96, gold: 119 }, + { date: "2026-03-01", basket: 119, bigmac: 118, latte: 114, iphone: 126, usd: 104, brent: 93, gold: 123 }, + { date: "2026-04-01", basket: 121, bigmac: 120, latte: 116, iphone: 129, usd: 105, brent: 97, gold: 127 }, + { date: "2026-05-01", basket: 124, bigmac: 123, latte: 118, iphone: 132, usd: 104, brent: 99, gold: 130 }, + { date: "2026-06-01", basket: 127, bigmac: 126, latte: 121, iphone: 134, usd: 106, brent: 101, gold: 133 }, +] as const; + +const spikeUaHistory = [ + { date: "2025-07-01", corn: 100, wheat: 100, sunflower: 100 }, + { date: "2025-08-01", corn: 97, wheat: 99, sunflower: 104 }, + { date: "2025-09-01", corn: 92, wheat: 95, sunflower: 108 }, + { date: "2025-10-01", corn: 89, wheat: 93, sunflower: 106 }, + { date: "2025-11-01", corn: 91, wheat: 94, sunflower: 109 }, + { date: "2025-12-01", corn: 96, wheat: 98, sunflower: 112 }, + { date: "2026-01-01", corn: 103, wheat: 104, sunflower: 115 }, + { date: "2026-02-01", corn: 108, wheat: 107, sunflower: 117 }, + { date: "2026-03-01", corn: 111, wheat: 110, sunflower: 121 }, + { date: "2026-04-01", corn: 114, wheat: 113, sunflower: 124 }, + { date: "2026-05-01", corn: 118, wheat: 116, sunflower: 127 }, + { date: "2026-06-01", corn: 121, wheat: 119, sunflower: 131 }, +] as const; + +export function normalizeBasketMarket(value: string | undefined | null): BasketMarket { + const upper = value?.toUpperCase(); + return upper === "US" || upper === "UA" || upper === "GLOBAL" ? upper : "GLOBAL"; +} + +export function getBasketLatest(market: BasketMarket = "GLOBAL"): BasketLatestResponse { + const marketObservations = observations.filter((item) => item.market === market); + const products = marketObservations.map((observation) => + enrichObservation(observation, { + changeYoY: observation.status === "unavailable" ? null : yoy[observation.product], + sparkline: productSparklines[observation.product], + }), + ); + const composite = calculateComposite(products); + + return { + market, + updatedAt, + products, + composite, + }; +} + +export function getBasketSources() { + return Object.values(BASKET_SOURCES); +} + +export function getBasketHistory(market: BasketMarket = "GLOBAL"): BasketChartSeries[] { + const productAvailability = getBasketLatest(market).products; + const unavailable = new Set( + productAvailability.filter((item) => item.status === "unavailable").map((item) => item.product), + ); + const productSeries = [ + makeSeries("basket", "Basket Composite", "#ffc42e", "1D3X Basket composite", "basket"), + makeSeries("bigmac", BASKET_PRODUCTS.bigmac.name, BASKET_PRODUCTS.bigmac.accent, BASKET_SOURCES.economistBigMac.label, "bigmac"), + !unavailable.has("latte") + ? makeSeries("latte", BASKET_PRODUCTS.latte.name, BASKET_PRODUCTS.latte.accent, BASKET_SOURCES.starbucksMonitor.label, "latte") + : null, + makeSeries("iphone", BASKET_PRODUCTS.iphone.name, BASKET_PRODUCTS.iphone.accent, BASKET_SOURCES.appleStore.label, "iphone"), + makeSeries("usd", "USD Broad Index", "#4aa3ff", BASKET_SOURCES.fredUsd.label, "usd"), + makeSeries("brent", "Brent Oil", "#ff7043", BASKET_SOURCES.commodityProxy.label, "brent"), + makeSeries("gold", "Gold proxy", "#ffd166", BASKET_SOURCES.commodityProxy.label, "gold"), + ].filter((item): item is BasketChartSeries => Boolean(item)); + + if (market !== "UA") return productSeries; + + return [ + ...productSeries, + makeSpikeSeries("spike-corn", "SPIKE Corn CPT Odesa", "#20d47b", "corn"), + makeSpikeSeries("spike-wheat", "SPIKE Milling Wheat CPT Odesa", "#25d6d0", "wheat"), + makeSpikeSeries("spike-sunflower", "SPIKE Sunflower Seed", "#ffb000", "sunflower"), + ]; +} + +export function getBasketCompare(market: BasketMarket = "GLOBAL") { + const series = getBasketHistory(market); + const basket = series.find((item) => item.id === "basket"); + const correlations = series.map((item) => ({ + id: item.id, + label: item.label, + correlationToBasket: + basket && item.id !== "basket" + ? calculateCorrelation( + basket.points.map((point) => point.value), + item.points.map((point) => point.value), + ) + : item.id === "basket" + ? 1 + : null, + })); + + return { + market, + mode: "rebasedTo100", + series, + correlations, + generatedAt: new Date().toISOString(), + }; +} + +export function getBasketMonthlyReport() { + return { + id: "basket-review-2026-06", + title: "Global Consumer Basket Review", + month: "June 2026", + status: "seed", + summary: + "Big Mac and iPhone signals remain above the US baseline while Starbucks Latte coverage is monitored and source-dependent by market.", + sourceGroup: "basket-global", + cadence: "monthly", + }; +} + +function makeSeries( + id: string, + label: string, + color: string, + source: string, + key: keyof (typeof baseHistory)[number], +): BasketChartSeries { + return { + id, + label, + color, + source, + points: rebaseSeriesTo100(baseHistory.map((point) => ({ date: point.date, value: Number(point[key]) }))), + }; +} + +function makeSpikeSeries( + id: string, + label: string, + keyColor: string, + key: keyof (typeof spikeUaHistory)[number], +): BasketChartSeries { + return { + id, + label, + color: keyColor, + source: BASKET_SOURCES.spikePublic.label, + points: rebaseSeriesTo100(spikeUaHistory.map((point) => ({ date: point.date, value: Number(point[key]) }))), + }; +} diff --git a/src/lib/basket/formulas.ts b/src/lib/basket/formulas.ts new file mode 100644 index 00000000..d1e46eaf --- /dev/null +++ b/src/lib/basket/formulas.ts @@ -0,0 +1,98 @@ +import type { BasketCoverage, BasketLatestItem, BasketObservation, BasketSeriesPoint } from "@/lib/basket/types"; + +export function toUsdPrice(localPrice: number, usdFxRate: number) { + if (usdFxRate <= 0) throw new Error("FX rate must be positive"); + return roundMoney(localPrice / usdFxRate); +} + +export function calculateIndexVsBaseline(valueUsd: number | null, baselineUsd: number) { + if (valueUsd === null || baselineUsd <= 0) return null; + return roundPercent(((valueUsd - baselineUsd) / baselineUsd) * 100); +} + +export function calculateComposite(items: BasketLatestItem[]) { + const values = items + .map((item) => item.indexVsBaseline) + .filter((value): value is number => typeof value === "number" && Number.isFinite(value)); + + const coverage = getCoverage(values.length, items.length); + + return { + value: values.length > 0 ? roundPercent(values.reduce((sum, value) => sum + value, 0) / values.length) : null, + coverage, + }; +} + +export function getCoverage(available: number, total: number): BasketCoverage { + return { + available, + total, + label: `${available} / ${total} components`, + }; +} + +export function enrichObservation( + observation: BasketObservation, + options: { changeYoY: number | null; sparkline: number[] }, +): BasketLatestItem { + return { + ...observation, + changeYoY: options.changeYoY, + indexVsBaseline: calculateIndexVsBaseline(observation.valueUsd, observation.baselineUsd), + sparkline: options.sparkline, + }; +} + +export function rebaseSeriesTo100(points: Array<{ date: string; value: number }>) { + const first = points.find((point) => Number.isFinite(point.value) && point.value > 0); + + if (!first) return []; + + return points.map((point) => ({ + date: point.date, + value: roundPercent((point.value / first.value) * 100), + })); +} + +export function calculateCorrelation(a: number[], b: number[]) { + const length = Math.min(a.length, b.length); + if (length < 2) return null; + + const left = a.slice(-length); + const right = b.slice(-length); + const avgLeft = average(left); + const avgRight = average(right); + const numerator = left.reduce((sum, value, index) => sum + (value - avgLeft) * (right[index] - avgRight), 0); + const leftVariance = left.reduce((sum, value) => sum + (value - avgLeft) ** 2, 0); + const rightVariance = right.reduce((sum, value) => sum + (value - avgRight) ** 2, 0); + const denominator = Math.sqrt(leftVariance * rightVariance); + + return denominator === 0 ? null : Number((numerator / denominator).toFixed(2)); +} + +export function alignSeriesByDate(series: BasketSeriesPoint[]) { + const grouped = new Map>(); + + for (const point of series) { + grouped.set(point.date, { + ...(grouped.get(point.date) ?? {}), + [point.seriesId]: point.value, + }); + } + + return [...grouped.entries()] + .sort(([a], [b]) => a.localeCompare(b)) + .map(([date, values]) => ({ date, values })); +} + +function average(values: number[]) { + return values.reduce((sum, value) => sum + value, 0) / values.length; +} + +function roundMoney(value: number) { + return Number(value.toFixed(2)); +} + +function roundPercent(value: number) { + return Number(value.toFixed(2)); +} diff --git a/src/lib/basket/products.ts b/src/lib/basket/products.ts new file mode 100644 index 00000000..6f132d56 --- /dev/null +++ b/src/lib/basket/products.ts @@ -0,0 +1,118 @@ +import type { BasketMarket, BasketProduct, BasketProductId, BasketSource } from "@/lib/basket/types"; + +export const BASKET_PRODUCTS: Record = { + bigmac: { + id: "bigmac", + name: "Big Mac Index", + shortName: "Big Mac", + accent: "#ffc42e", + unit: "item", + }, + latte: { + id: "latte", + name: "Starbucks Latte Index", + shortName: "Latte", + accent: "#70f2bd", + unit: "item", + }, + iphone: { + id: "iphone", + name: "iPhone Index", + shortName: "iPhone", + accent: "#b96cff", + unit: "item", + }, +}; + +export const BASKET_MARKETS: Array<{ id: BasketMarket; label: string }> = [ + { id: "GLOBAL", label: "Global" }, + { id: "US", label: "United States" }, + { id: "UA", label: "Ukraine" }, +]; + +export const BASKET_SOURCES: Record = { + economistBigMac: { + id: "economist-bigmac", + kind: "price_dataset", + label: "The Economist Big Mac Index dataset", + url: "https://github.com/TheEconomist/big-mac-data", + }, + starbucksMonitor: { + id: "starbucks-menu-monitor", + kind: "brand_menu", + label: "Monitored Starbucks menu prices / public menu datasets", + }, + gcpdexStarbucks: { + id: "gcpdex-starbucks", + kind: "price_dataset", + label: "GCPDex Starbucks Latte Index reference dataset", + url: "https://www.gcpdex.com/", + }, + appleStore: { + id: "apple-store-retail", + kind: "retail_price", + label: "Apple Store / authorized retailer price monitoring", + }, + picodiIphone: { + id: "picodi-iphone-index", + kind: "price_dataset", + label: "Picodi iPhone Index annual affordability report", + url: "https://www.picodi.com/", + }, + tenscopeIphone: { + id: "tenscope-iphone-affordability", + kind: "price_dataset", + label: "Tenscope iPhone Affordability Index", + url: "https://tenscope.com/", + }, + applePriceCompare: { + id: "apple-price-compare", + kind: "retail_price", + label: "ApplePriceCompare Apple Store country price tables", + url: "https://applepricecompare.com/", + }, + fredUsd: { + id: "fred-usd-broad", + kind: "external_market_series", + label: "FRED broad USD proxy", + url: "https://fred.stlouisfed.org/series/DTWEXBGS", + }, + fredBrent: { + id: "fred-brent", + kind: "external_market_series", + label: "FRED Brent crude oil spot price", + url: "https://fred.stlouisfed.org/series/DCOILBRENTEU", + }, + fredWti: { + id: "fred-wti", + kind: "external_market_series", + label: "FRED WTI crude oil spot price", + url: "https://fred.stlouisfed.org/series/DCOILWTICO", + }, + commodityProxy: { + id: "commodity-proxy", + kind: "external_market_series", + label: "Public commodity market proxy", + }, + spikePublic: { + id: "spike-public-history", + kind: "external_market_series", + label: "SPIKE Spot Index public history", + }, + economistMedia: { + id: "economist-burgernomics", + kind: "news", + label: "The Economist Burgernomics coverage", + url: "https://www.economist.com/big-mac-index", + }, + consumerIndexMedia: { + id: "consumer-index-media", + kind: "rss", + label: "Consumer index media monitoring: Visual Capitalist, Investopedia, SwitchOnBusiness, Yahoo Finance, CNBC, Reuters, Bloomberg", + }, + communityDataSignals: { + id: "community-data-signals", + kind: "news", + label: "Community data signals for non-official consumer index leads", + }, +}; diff --git a/src/lib/basket/types.ts b/src/lib/basket/types.ts new file mode 100644 index 00000000..a13a6d90 --- /dev/null +++ b/src/lib/basket/types.ts @@ -0,0 +1,80 @@ +export type BasketProductId = "bigmac" | "latte" | "iphone"; +export type BasketMarket = "GLOBAL" | "US" | "UA"; +export type BasketConfidence = "verified" | "monitored" | "seed" | "unavailable"; +export type BasketObservationStatus = "published" | "monitored" | "unavailable"; + +export type BasketSourceKind = + | "price_dataset" + | "brand_menu" + | "retail_price" + | "fx" + | "external_market_series" + | "news" + | "rss" + | "telegram" + | "manual_material"; + +export type BasketSource = { + id: string; + label: string; + kind: BasketSourceKind; + url?: string; +}; + +export type BasketProduct = { + id: BasketProductId; + name: string; + shortName: string; + accent: string; + unit: "item"; +}; + +export type BasketObservation = { + product: BasketProductId; + market: BasketMarket; + date: string; + valueUsd: number | null; + baselineUsd: number; + localPrice?: number | null; + currencyCode?: string | null; + source: BasketSource; + confidence: BasketConfidence; + status: BasketObservationStatus; + note?: string; +}; + +export type BasketLatestItem = BasketObservation & { + indexVsBaseline: number | null; + changeYoY: number | null; + sparkline: number[]; +}; + +export type BasketSeriesPoint = { + date: string; + seriesId: string; + value: number; +}; + +export type BasketChartSeries = { + id: string; + label: string; + color: string; + points: Array<{ date: string; value: number }>; + source: string; +}; + +export type BasketCoverage = { + available: number; + total: number; + label: string; +}; + +export type BasketLatestResponse = { + market: BasketMarket; + updatedAt: string; + products: BasketLatestItem[]; + composite: { + value: number | null; + coverage: BasketCoverage; + }; +}; From cec2a1bfa4ef2181389c96940dd60e47650796db Mon Sep 17 00:00:00 2001 From: Anton BV Date: Fri, 14 Aug 2026 23:46:04 +0200 Subject: [PATCH 2/6] route POP host to restored Basket surface --- src/app/page.tsx | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/app/page.tsx b/src/app/page.tsx index be9fb3d0..d123ae8b 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,6 +1,10 @@ import { cookies } from "next/headers"; import { headers } from "next/headers"; import { redirect } from "next/navigation"; +import { + BasketHeroEmbed, + BasketLanding, +} from "@/components/basket/basket-landing"; import { PlatformLanding } from "@/components/platform/platform-landing"; import { detectLocaleFromCountry, @@ -16,6 +20,11 @@ export default async function HomeRedirect() { headerStore.get("host") ?? undefined; + if (isPopHost(requestHost)) { + const isIframe = headerStore.get("sec-fetch-dest") === "iframe"; + return isIframe ? : ; + } + if (isPlatformSite(requestHost ?? undefined)) { return ; } @@ -35,6 +44,23 @@ export default async function HomeRedirect() { redirect(`/${locale}`); } +function isPopHost(value?: string) { + if (!value) return false; + + const host = value + .split(",") + .map((item) => item.trim().toLowerCase()) + .find(Boolean); + + return Boolean( + host && + (host === "pop.1d3x.com" || + host.startsWith("pop.1d3x.com:") || + host.includes("1d3x-basket") || + host.includes("basket")), + ); +} + function getCountryHeader(headerStore: Headers) { return ( headerStore.get("x-vercel-ip-country") ?? From 7bab68fbcc2f6095028938010d6f3c1257942e53 Mon Sep 17 00:00:00 2001 From: Anton BV Date: Fri, 14 Aug 2026 23:46:40 +0200 Subject: [PATCH 3/6] restore Basket visual assets --- public/basket/assets/api-cube.webp | Bin 0 -> 7588 bytes public/basket/assets/hero-bigmac.webp | Bin 0 -> 105728 bytes public/basket/assets/hero-iphone.webp | Bin 0 -> 56950 bytes public/basket/assets/hero-latte.webp | Bin 0 -> 72616 bytes public/basket/assets/media-monthly-review.webp | Bin 0 -> 11904 bytes public/basket/assets/og-basket.webp | Bin 0 -> 249838 bytes 6 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 public/basket/assets/api-cube.webp create mode 100644 public/basket/assets/hero-bigmac.webp create mode 100644 public/basket/assets/hero-iphone.webp create mode 100644 public/basket/assets/hero-latte.webp create mode 100644 public/basket/assets/media-monthly-review.webp create mode 100644 public/basket/assets/og-basket.webp diff --git a/public/basket/assets/api-cube.webp b/public/basket/assets/api-cube.webp new file mode 100644 index 0000000000000000000000000000000000000000..8ae2845eb1e5e66b1bf02ba2f74cc2958ac45cee GIT binary patch literal 7588 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 G6*vH@^7PvP literal 0 HcmV?d00001 diff --git a/public/basket/assets/hero-bigmac.webp b/public/basket/assets/hero-bigmac.webp new file mode 100644 index 0000000000000000000000000000000000000000..2b87a32c35df0cd570672904ae0511f882863a53 GIT binary patch literal 105728 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