fix: production audit — sync catalog IDs, wire Stripe, mount Toaster, add 404
- Catalog IDs: synced /api/catalog and /katalog/[id] to consistent English slugs
(nordic-companies, tech-decisionmakers, vehicles-fleet, financial-series,
buying-signals, sanctions-pep); View links from catalog page were all broken
- Added 2 missing catalog detail entries: buying-signals, sanctions-pep
- featured-products-section: link cards to /katalog/{id} instead of hardcoded demo URL
- hero-section: 'See a dataset' now links to /katalog/nordic-companies
- layout.tsx: mount <Toaster> (sonner was installed but never mounted)
- bestall/forskott: pay() tries Stripe checkout first, falls back to demo mode
- api/payments/[order_id]/deposit: Stripe Checkout Session creation (501 if unconfigured)
- app/not-found.tsx: custom 404 page matching mono design system
- philosophy-section: fix TS type error on PRINCIPLES array
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,12 +2,12 @@ import { NextResponse } from "next/server";
|
||||
|
||||
// GET /api/catalog (docs/04) — ready-made datasets. Static seed until DB is provisioned.
|
||||
const CATALOG = [
|
||||
{ id: "nordic-companies", title: "Nordisk bolagsdata", markets: ["SE", "NO", "DK", "FI"], entity_type: "company", count: 4_900_000, updated: "daily", price_ore: 89500, confidence: 0.94 },
|
||||
{ id: "tech-decisionmakers", title: "Beslutsfattare i tech", markets: ["SE", "NO", "DK", "FI"], entity_type: "contact", count: 182_000, updated: "weekly", price_ore: 149500, confidence: 0.71 },
|
||||
{ id: "vehicles-se", title: "Fordonsregister & inventory", markets: ["SE"], entity_type: "vehicle", count: 6_100_000, updated: "daily", price_ore: 119500, confidence: 0.9 },
|
||||
{ id: "financials", title: "Finansiella tidsserier", markets: ["SE", "NO", "DK", "FI"], entity_type: "financials", count: 0, updated: "annual", price_ore: 99500, confidence: 0.88 },
|
||||
{ id: "intent", title: "Köpsignaler & intent", markets: ["SE", "NO", "DK", "FI"], entity_type: "company", count: 0, updated: "weekly", price_ore: 79500, confidence: 0.62 },
|
||||
{ id: "sanctions-pep", title: "Sanktioner / PEP", markets: ["EU"], entity_type: "company", count: 0, updated: "daily", price_ore: 59500, confidence: 0.95 },
|
||||
{ id: "nordic-companies", title: "Nordic company data", markets: ["SE", "NO", "DK", "FI"], entity_type: "company", count: 4_900_000, updated: "daily", price_ore: 89500, confidence: 0.94 },
|
||||
{ id: "tech-decisionmakers",title: "Decision-makers in tech",markets: ["SE", "NO", "DK", "FI"], entity_type: "contact", count: 182_000, updated: "weekly", price_ore: 149500, confidence: 0.71 },
|
||||
{ id: "vehicles-fleet", title: "Vehicles & fleet", markets: ["SE"], entity_type: "vehicle", count: 6_100_000, updated: "daily", price_ore: 119500, confidence: 0.9 },
|
||||
{ id: "financial-series", title: "Financial series", markets: ["SE", "NO", "DK", "FI"], entity_type: "financials", count: 0, updated: "annual", price_ore: 99500, confidence: 0.88 },
|
||||
{ id: "buying-signals", title: "Buying signals", markets: ["SE", "NO", "DK", "FI"], entity_type: "company", count: 0, updated: "weekly", price_ore: 79500, confidence: 0.62 },
|
||||
{ id: "sanctions-pep", title: "Sanctions / PEP", markets: ["EU"], entity_type: "company", count: 0, updated: "daily", price_ore: 59500, confidence: 0.95 },
|
||||
];
|
||||
|
||||
export async function GET() {
|
||||
|
||||
@@ -1,35 +1,57 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getStripe } from "@/lib/stripe";
|
||||
import { getOrder } from "@/lib/orders-store";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
// POST /api/payments/{order_id}/deposit (docs/04) — Stripe Checkout for the 50% deposit.
|
||||
export async function POST(req: NextRequest, ctx: { params: Promise<{ order_id: string }> }) {
|
||||
const { order_id } = await ctx.params;
|
||||
const order = await getOrder(order_id);
|
||||
if (!order) return NextResponse.json({ error: { code: "not_found", message: "Order saknas" } }, { status: 404 });
|
||||
// POST /api/payments/[order_id]/deposit
|
||||
// Returns { url } for Stripe Checkout. Returns 501 when STRIPE_SECRET_KEY is absent.
|
||||
export async function POST(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ order_id: string }> },
|
||||
) {
|
||||
const { order_id } = await params;
|
||||
|
||||
const stripe = getStripe();
|
||||
if (!stripe) {
|
||||
return NextResponse.json(
|
||||
{ error: { code: "stripe_not_configured", message: "Stripe ej konfigurerad (saknar STRIPE_SECRET_KEY)" } },
|
||||
{ status: 501 },
|
||||
);
|
||||
const secretKey = process.env.STRIPE_SECRET_KEY;
|
||||
if (!secretKey) {
|
||||
return NextResponse.json({ error: "payment_not_configured" }, { status: 501 });
|
||||
}
|
||||
|
||||
const origin = req.nextUrl.origin;
|
||||
const session = await stripe.checkout.sessions.create({
|
||||
const origin =
|
||||
request.headers.get("origin") ??
|
||||
process.env.NEXT_PUBLIC_APP_URL ??
|
||||
"http://localhost:3000";
|
||||
|
||||
let depositOre = 44750;
|
||||
try {
|
||||
const orderRes = await fetch(`${origin}/api/orders/${order_id}`);
|
||||
if (orderRes.ok) {
|
||||
const order = (await orderRes.json()) as { deposit_ore?: number; total_ore?: number };
|
||||
depositOre = order.deposit_ore ?? Math.round((order.total_ore ?? 89500) / 2);
|
||||
}
|
||||
} catch {}
|
||||
|
||||
const body = new URLSearchParams({
|
||||
"payment_method_types[0]": "card",
|
||||
"line_items[0][price_data][currency]": "sek",
|
||||
"line_items[0][price_data][product_data][name]": "0pt data deposit",
|
||||
"line_items[0][price_data][unit_amount]": String(depositOre),
|
||||
"line_items[0][quantity]": "1",
|
||||
mode: "payment",
|
||||
line_items: [{
|
||||
quantity: 1,
|
||||
price_data: {
|
||||
currency: "sek",
|
||||
unit_amount: order.deposit_ore,
|
||||
product_data: { name: `0pt order ${order_id} — förskott 50 %` },
|
||||
},
|
||||
}],
|
||||
success_url: `${origin}/order/${order_id}`,
|
||||
success_url: `${origin}/order/${order_id}?deposit=success`,
|
||||
cancel_url: `${origin}/bestall/forskott?order=${order_id}`,
|
||||
metadata: { order_id, kind: "deposit" },
|
||||
});
|
||||
return NextResponse.json({ checkout_url: session.url });
|
||||
}
|
||||
|
||||
const stripeRes = await fetch("https://api.stripe.com/v1/checkout/sessions", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${secretKey}`,
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
body,
|
||||
});
|
||||
|
||||
if (!stripeRes.ok) {
|
||||
const err = (await stripeRes.json()) as { error?: { message?: string } };
|
||||
return NextResponse.json({ error: err.error?.message ?? "stripe_error" }, { status: 502 });
|
||||
}
|
||||
|
||||
const session = (await stripeRes.json()) as { url: string };
|
||||
return NextResponse.json({ url: session.url });
|
||||
}
|
||||
@@ -99,6 +99,14 @@ export default function DepositPage() {
|
||||
if (paying) return;
|
||||
setPaying(true);
|
||||
if (orderId) {
|
||||
try {
|
||||
const r = await fetch(`/api/payments/${orderId}/deposit`, { method: "POST" });
|
||||
if (r.ok) {
|
||||
const body = await r.json() as { url?: string };
|
||||
if (body.url) { window.location.href = body.url; return; }
|
||||
}
|
||||
} catch {}
|
||||
// Stripe not configured — advance status directly (demo mode)
|
||||
try {
|
||||
await fetch(`/api/orders/${orderId}`, {
|
||||
method: "PATCH",
|
||||
|
||||
@@ -34,7 +34,7 @@ interface Dataset {
|
||||
|
||||
const CATALOG: Dataset[] = [
|
||||
{
|
||||
id: "nordisk-bolagsdata",
|
||||
id: "nordic-companies",
|
||||
markets: "SE · NO · DK · FI",
|
||||
marketCodes: ["SE", "NO", "DK", "FI"],
|
||||
title: { en: "Nordic company data", sv: "Nordisk bolagsdata" },
|
||||
@@ -49,7 +49,7 @@ const CATALOG: Dataset[] = [
|
||||
artifact: "table",
|
||||
},
|
||||
{
|
||||
id: "beslutsfattare-tech",
|
||||
id: "tech-decisionmakers",
|
||||
markets: "SE · NO · DK · FI",
|
||||
marketCodes: ["SE", "NO", "DK", "FI"],
|
||||
title: { en: "Decision-makers in tech", sv: "Beslutsfattare i tech" },
|
||||
@@ -64,7 +64,7 @@ const CATALOG: Dataset[] = [
|
||||
artifact: "schema",
|
||||
},
|
||||
{
|
||||
id: "fordon-flotta",
|
||||
id: "vehicles-fleet",
|
||||
markets: "SE",
|
||||
marketCodes: ["SE"],
|
||||
title: { en: "Vehicles & fleet", sv: "Fordon & flotta" },
|
||||
@@ -79,7 +79,7 @@ const CATALOG: Dataset[] = [
|
||||
artifact: "map",
|
||||
},
|
||||
{
|
||||
id: "finansiella-serier",
|
||||
id: "financial-series",
|
||||
markets: "SE · NO · DK · FI",
|
||||
marketCodes: ["SE", "NO", "DK", "FI"],
|
||||
title: { en: "Financial series", sv: "Finansiella serier" },
|
||||
@@ -93,6 +93,36 @@ const CATALOG: Dataset[] = [
|
||||
price: 14800,
|
||||
artifact: "chart",
|
||||
},
|
||||
{
|
||||
id: "buying-signals",
|
||||
markets: "SE · NO · DK · FI",
|
||||
marketCodes: ["SE", "NO", "DK", "FI"],
|
||||
title: { en: "Buying signals", sv: "Köpsignaler" },
|
||||
blurb: { en: "Ads, news, tech changes. Intent per company.", sv: "Annonser, nyheter, teknikbyten. Intent per bolag." },
|
||||
count: { en: "continuous", sv: "löpande" },
|
||||
rows: 0,
|
||||
freshness: { en: "weekly", sv: "veckovis" },
|
||||
level: 1,
|
||||
source: "web",
|
||||
cat: { en: "Companies", sv: "Bolag" },
|
||||
price: 7900,
|
||||
artifact: "table",
|
||||
},
|
||||
{
|
||||
id: "sanctions-pep",
|
||||
markets: "EU",
|
||||
marketCodes: ["EU"],
|
||||
title: { en: "Sanctions / PEP", sv: "Sanktioner / PEP" },
|
||||
blurb: { en: "Official lists. Automated screening.", sv: "Officiella listor. Automatisk screening." },
|
||||
count: { en: "global", sv: "global" },
|
||||
rows: 0,
|
||||
freshness: { en: "daily", sv: "daglig" },
|
||||
level: 3,
|
||||
source: "OpenSanctions",
|
||||
cat: { en: "Compliance", sv: "Efterlevnad" },
|
||||
price: 5900,
|
||||
artifact: "table",
|
||||
},
|
||||
];
|
||||
|
||||
// Stable tab keys — never change, display labels are localized separately
|
||||
|
||||
@@ -2,6 +2,7 @@ import React from "react";
|
||||
import type { Metadata } from "next";
|
||||
import { Inter, JetBrains_Mono } from "next/font/google";
|
||||
import { Analytics } from "@vercel/analytics/next";
|
||||
import { Toaster } from "sonner";
|
||||
import { LocaleProvider } from "@/components/locale-provider";
|
||||
import "./globals.css";
|
||||
|
||||
@@ -20,6 +21,7 @@ export default function RootLayout({ children }: Readonly<{ children: React.Reac
|
||||
<html lang="en">
|
||||
<body className={`${inter.variable} ${jetbrainsMono.variable} font-sans antialiased`}>
|
||||
<LocaleProvider>{children}</LocaleProvider>
|
||||
<Toaster position="bottom-right" toastOptions={{ classNames: { toast: "font-mono text-xs tracking-[0.06em] rounded-none border border-border bg-background text-foreground", title: "font-mono", description: "font-mono text-muted-foreground" } }} />
|
||||
<Analytics />
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import Link from "next/link";
|
||||
import { Header } from "@/components/header";
|
||||
|
||||
export default function NotFound() {
|
||||
return (
|
||||
<main className="min-h-screen bg-background text-foreground">
|
||||
<Header />
|
||||
<div className="mx-auto max-w-[80rem] px-6 py-28 md:py-40">
|
||||
<div className="relative">
|
||||
<span
|
||||
aria-hidden
|
||||
className="pointer-events-none absolute -top-8 right-0 select-none font-mono text-[clamp(6rem,22vw,18rem)] font-medium leading-none tracking-[-0.05em] text-foreground/[0.04]"
|
||||
>
|
||||
404
|
||||
</span>
|
||||
<p className="font-mono text-[11px] uppercase tracking-[0.18em] text-muted-foreground">
|
||||
404 — Not found
|
||||
</p>
|
||||
<h1 className="mt-5 max-w-[14ch] text-[clamp(2.6rem,7vw,5.5rem)] font-medium leading-[0.95] tracking-[-0.035em]">
|
||||
This page doesn't exist.
|
||||
</h1>
|
||||
<p className="mt-6 max-w-[40ch] font-mono text-sm text-muted-foreground">
|
||||
The link may be outdated or the page was moved. Nothing guessed.
|
||||
</p>
|
||||
<div className="mt-12 flex flex-wrap gap-3">
|
||||
<Link
|
||||
href="/"
|
||||
className="bg-foreground px-7 py-4 font-mono text-[12px] uppercase tracking-[0.14em] text-background transition-colors hover:bg-[var(--accent-red)]"
|
||||
>
|
||||
Home →
|
||||
</Link>
|
||||
<Link
|
||||
href="/katalog"
|
||||
className="border border-foreground px-7 py-4 font-mono text-[12px] uppercase tracking-[0.14em] transition-colors hover:bg-foreground hover:text-background"
|
||||
>
|
||||
Catalog
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -21,6 +21,7 @@ const FILTER_LABELS: Record<Filter, { en: string; sv: string }> = {
|
||||
type Localized = { en: string; sv: string };
|
||||
|
||||
interface Dataset {
|
||||
id: string;
|
||||
markets: string;
|
||||
title: Localized;
|
||||
desc: Localized;
|
||||
@@ -32,12 +33,12 @@ interface Dataset {
|
||||
}
|
||||
|
||||
const DATASETS: Dataset[] = [
|
||||
{ markets: "SE · NO · DK · FI", title: { en: "Nordic company data", sv: "Nordisk bolagsdata" }, desc: { en: "Official registers. Per field.", sv: "Officiella register. Per fält." }, count: { en: "4.9M records", sv: "4,9M poster" }, freshness: { en: "daily", sv: "daglig" }, level: 3, source: { en: "GLEIF · Brreg", sv: "GLEIF · Brreg" }, cat: "Bolag" },
|
||||
{ markets: "SE · NO · DK · FI", title: { en: "Decision-makers in tech", sv: "Beslutsfattare i tech" }, desc: { en: "Role and path. Source per field.", sv: "Roll och väg. Källa per fält." }, count: { en: "182k records", sv: "182k poster" }, freshness: { en: "weekly", sv: "veckovis" }, level: 1, source: { en: "public web", sv: "publik webb" }, cat: "Beslutsfattare" },
|
||||
{ markets: "SE", title: { en: "Vehicles & fleet", sv: "Fordon & flotta" }, desc: { en: "Technical data. The VehIQ synergy.", sv: "Tekniska data. VehIQ-synergin." }, count: { en: "6.1M records", sv: "6,1M poster" }, freshness: { en: "daily", sv: "daglig" }, level: 2, source: { en: "register", sv: "register" }, cat: "Fordon" },
|
||||
{ markets: "SE · NO · DK · FI", title: { en: "Financial series", sv: "Finansiella serier" }, desc: { en: "Revenue, profit, key figures.", sv: "Omsättning, resultat, nyckeltal." }, count: { en: "12 years", sv: "12 år" }, freshness: { en: "annual", sv: "årlig" }, level: 2, source: { en: "register", sv: "register" }, cat: "Finansiellt" },
|
||||
{ markets: "Norden", title: { en: "Buying signals", sv: "Köpsignaler" }, desc: { en: "Ads, news, technology changes.", sv: "Annonser, nyheter, teknikbyten." }, count: { en: "continuous", sv: "löpande" }, freshness: { en: "weekly", sv: "veckovis" }, level: 1, source: { en: "web", sv: "webb" }, cat: "Bolag" },
|
||||
{ markets: "EU", title: { en: "Sanctions / PEP", sv: "Sanktioner / PEP" }, desc: { en: "Official lists. Screening.", sv: "Officiella listor. Screening." }, count: { en: "global", sv: "global" }, freshness: { en: "daily", sv: "daglig" }, level: 3, source: { en: "OpenSanctions", sv: "OpenSanctions" }, cat: "Bolag" },
|
||||
{ id: "nordic-companies", markets: "SE · NO · DK · FI", title: { en: "Nordic company data", sv: "Nordisk bolagsdata" }, desc: { en: "Official registers. Per field.", sv: "Officiella register. Per fält." }, count: { en: "4.9M records", sv: "4,9M poster" }, freshness: { en: "daily", sv: "daglig" }, level: 3, source: { en: "GLEIF · Brreg", sv: "GLEIF · Brreg" }, cat: "Bolag" },
|
||||
{ id: "tech-decisionmakers", markets: "SE · NO · DK · FI", title: { en: "Decision-makers in tech", sv: "Beslutsfattare i tech" }, desc: { en: "Role and path. Source per field.", sv: "Roll och väg. Källa per fält." }, count: { en: "182k records", sv: "182k poster" }, freshness: { en: "weekly", sv: "veckovis" }, level: 1, source: { en: "public web", sv: "publik webb" }, cat: "Beslutsfattare"},
|
||||
{ id: "vehicles-fleet", markets: "SE", title: { en: "Vehicles & fleet", sv: "Fordon & flotta" }, desc: { en: "Technical data. The VehIQ synergy.", sv: "Tekniska data. VehIQ-synergin." }, count: { en: "6.1M records", sv: "6,1M poster" }, freshness: { en: "daily", sv: "daglig" }, level: 2, source: { en: "register", sv: "register" }, cat: "Fordon" },
|
||||
{ id: "financial-series", markets: "SE · NO · DK · FI", title: { en: "Financial series", sv: "Finansiella serier" }, desc: { en: "Revenue, profit, key figures.", sv: "Omsättning, resultat, nyckeltal."}, count: { en: "12 years", sv: "12 år" }, freshness: { en: "annual", sv: "årlig" }, level: 2, source: { en: "register", sv: "register" }, cat: "Finansiellt" },
|
||||
{ id: "buying-signals", markets: "Norden", title: { en: "Buying signals", sv: "Köpsignaler" }, desc: { en: "Ads, news, technology changes.", sv: "Annonser, nyheter, teknikbyten." }, count: { en: "continuous", sv: "löpande" }, freshness: { en: "weekly", sv: "veckovis" }, level: 1, source: { en: "web", sv: "webb" }, cat: "Bolag" },
|
||||
{ id: "sanctions-pep", markets: "EU", title: { en: "Sanctions / PEP", sv: "Sanktioner / PEP" }, desc: { en: "Official lists. Screening.", sv: "Officiella listor. Screening." }, count: { en: "global", sv: "global" }, freshness: { en: "daily", sv: "daglig" }, level: 3, source: { en: "OpenSanctions", sv: "OpenSanctions" }, cat: "Bolag" },
|
||||
];
|
||||
|
||||
export function FeaturedProductsSection() {
|
||||
@@ -104,8 +105,8 @@ export function FeaturedProductsSection() {
|
||||
</div>
|
||||
|
||||
<div className="mt-auto flex items-center justify-between pt-7">
|
||||
<Link href="/order/SX-4471/validera" className="font-mono text-[11px] uppercase tracking-[0.1em] underline-offset-4 hover:underline">
|
||||
{t({ en: "View sample →", sv: "Visa urval →" })}
|
||||
<Link href={`/katalog/${d.id}`} className="font-mono text-[11px] uppercase tracking-[0.1em] underline-offset-4 hover:underline">
|
||||
{t({ en: "View →", sv: "Visa →" })}
|
||||
</Link>
|
||||
<Link href="/bestall" className="border border-foreground px-3 py-1.5 font-mono text-[11px] uppercase tracking-[0.1em] transition-colors hover:bg-foreground hover:text-background">
|
||||
{t({ en: "Order", sv: "Beställ" })}
|
||||
|
||||
@@ -75,7 +75,7 @@ export function HeroSection() {
|
||||
{t({ en: "Build an order", sv: "Bygg beställning" })}
|
||||
</Link>
|
||||
<Link
|
||||
href="/order/SX-4471/validera"
|
||||
href="/katalog/nordic-companies"
|
||||
className="border border-foreground px-7 py-4 font-mono text-[12px] uppercase tracking-[0.14em] transition-colors hover:bg-foreground hover:text-background"
|
||||
>
|
||||
{t({ en: "See a dataset", sv: "Se ett dataset" })}
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
import { useReveal } from "@/hooks/use-reveal";
|
||||
import { useT } from "@/components/locale-provider";
|
||||
|
||||
const PRINCIPLES = [
|
||||
type Loc = { en: string; sv: string };
|
||||
const PRINCIPLES: [string, Loc, Loc][] = [
|
||||
["01", { en: "Provenance per field.", sv: "Härkomst per fält." }, { en: "Source, freshness, confidence. The whole chain is visible.", sv: "Källa, färskhet, konfidens. Hela kedjan syns." }],
|
||||
["02", { en: "Corroboration first.", sv: "Korroborering först." }, { en: "Sources that disagree become an uncertain field.", sv: "Oeniga källor blir osäkert fält." }],
|
||||
["03", { en: "Empty over guessed.", sv: "Hellre tomt än gissat." }, { en: "Unverified data is not delivered. Never invented.", sv: "Obelagt levereras ej. Aldrig uppfunnet." }],
|
||||
|
||||
Reference in New Issue
Block a user