frontend page fixes v1

This commit is contained in:
MatejGhub
2026-09-12 21:07:43 +02:00
parent 65e1fdd55b
commit 32c6c828b1
11 changed files with 7946 additions and 6444 deletions
@@ -46,8 +46,8 @@
{i18n.t("pricing.calc.lead")} {i18n.t("pricing.calc.lead")}
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent class="space-y-8"> <CardContent class="grid gap-8 lg:grid-cols-2">
<div class="grid gap-6 sm:grid-cols-3"> <div class="grid gap-6">
<div class="space-y-2 text-sm"> <div class="space-y-2 text-sm">
<label class="font-medium text-text" for="plan-calc-feeds" <label class="font-medium text-text" for="plan-calc-feeds"
>{i18n.t("pricing.calc.feeds")}</label >{i18n.t("pricing.calc.feeds")}</label
@@ -102,7 +102,7 @@
</div> </div>
{#if result.enterprise} {#if result.enterprise}
<div class="rounded-lg border border-border bg-background px-4 py-5 text-center sm:text-left"> <div class="h-full rounded-lg border border-border bg-background px-4 py-5 text-center sm:text-left">
<p class="text-lg font-semibold text-text">{i18n.t("pricing.calc.enterpriseTitle")}</p> <p class="text-lg font-semibold text-text">{i18n.t("pricing.calc.enterpriseTitle")}</p>
<p class="mt-2 text-sm text-text-muted">{i18n.t("pricing.calc.enterpriseBody")}</p> <p class="mt-2 text-sm text-text-muted">{i18n.t("pricing.calc.enterpriseBody")}</p>
<a <a
@@ -117,8 +117,8 @@
{:else if result.recommended} {:else if result.recommended}
{@const rec = result.recommended} {@const rec = result.recommended}
{@const budget = result.budget} {@const budget = result.budget}
<div class="grid gap-4 lg:grid-cols-2"> <div class={budget ? "grid h-full gap-4" : "h-full"}>
<div class="rounded-lg border border-primary/30 bg-primary/5 px-4 py-5"> <div class="flex h-full flex-col rounded-lg border border-primary/30 bg-primary/5 px-4 py-5">
<p class="text-xs font-semibold uppercase tracking-wide text-primary"> <p class="text-xs font-semibold uppercase tracking-wide text-primary">
{i18n.t("pricing.calc.recommendedLabel")} {i18n.t("pricing.calc.recommendedLabel")}
</p> </p>
@@ -132,7 +132,7 @@
{formatMoney(rec.monthlyListUSD)} / {i18n.t("pricing.card.perMonth")} {formatMoney(rec.monthlyListUSD)} / {i18n.t("pricing.card.perMonth")}
{/if} {/if}
</p> </p>
<ul class="mt-3 space-y-1 text-sm text-text-muted"> <ul class="mt-3 mb-4 space-y-1 text-sm text-text-muted">
<li> <li>
{i18n.t("pricing.calc.coversCredits", { {i18n.t("pricing.calc.coversCredits", {
count: result.creditsNeeded.toLocaleString() count: result.creditsNeeded.toLocaleString()
@@ -152,7 +152,7 @@
</ul> </ul>
<a <a
href={rec.plan.ctaHref} href={rec.plan.ctaHref}
class={buttonClasses("default", "default", "mt-4 inline-flex w-full sm:w-auto")} class={buttonClasses("default", "default", "mt-auto inline-flex w-full sm:w-auto")}
> >
{i18n.t("pricing.calc.ctaPlan", { name: rec.plan.name })} {i18n.t("pricing.calc.ctaPlan", { name: rec.plan.name })}
</a> </a>
@@ -195,14 +195,8 @@
{i18n.t("pricing.calc.ctaPlan", { name: budget.plan.name })} {i18n.t("pricing.calc.ctaPlan", { name: budget.plan.name })}
</a> </a>
</div> </div>
{:else}
<div class="rounded-lg border border-dashed border-border bg-background/50 px-4 py-5">
<p class="text-sm text-text-muted">{i18n.t("pricing.calc.noBudget")}</p>
</div>
{/if} {/if}
</div> </div>
{/if} {/if}
<p class="text-center text-xs text-text-muted sm:text-left">{i18n.t("pricing.calc.disclaimer")}</p>
</CardContent> </CardContent>
</Card> </Card>
@@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import { Check, Zap, Sparkles, ChevronDown, ChevronUp } from "@lucide/svelte"; import { Check, Zap, Sparkles, ChevronDown, ChevronUp } from "@lucide/svelte";
import { slide } from "svelte/transition";
import { import {
Badge, Badge,
Card, Card,
@@ -17,12 +18,18 @@
type Props = { type Props = {
plan: PricingPlan; plan: PricingPlan;
isYearly?: boolean; isYearly?: boolean;
expanded?: boolean;
onToggle?: () => void;
class?: string; class?: string;
}; };
let { plan, isYearly = false, class: className = "" }: Props = $props(); let {
plan,
let showAllFeatures = $state(false); isYearly = false,
expanded = false,
onToggle = () => {},
class: className = ""
}: Props = $props();
const yearlyPrice = $derived(Math.round(plan.pricePerMonth * 12 * (1 - ANNUAL_DISCOUNT))); const yearlyPrice = $derived(Math.round(plan.pricePerMonth * 12 * (1 - ANNUAL_DISCOUNT)));
const displayPrice = $derived(isYearly ? yearlyPrice : plan.pricePerMonth); const displayPrice = $derived(isYearly ? yearlyPrice : plan.pricePerMonth);
@@ -37,7 +44,7 @@
const initialFeaturesCount = 5; const initialFeaturesCount = 5;
const displayedFeatures = $derived( const displayedFeatures = $derived(
showAllFeatures ? plan.features : plan.features.slice(0, initialFeaturesCount) expanded ? plan.features : plan.features.slice(0, initialFeaturesCount)
); );
const hasMoreFeatures = $derived(plan.features.length > initialFeaturesCount); const hasMoreFeatures = $derived(plan.features.length > initialFeaturesCount);
@@ -98,7 +105,7 @@
</script> </script>
<!-- In-flow badge row keeps “Most Popular” clear of the plan title; spacer aligns siblings. --> <!-- In-flow badge row keeps “Most Popular” clear of the plan title; spacer aligns siblings. -->
<div class="flex h-full flex-col"> <div class="flex flex-col">
<div class="mb-4 flex min-h-8 shrink-0 items-center justify-center"> <div class="mb-4 flex min-h-8 shrink-0 items-center justify-center">
{#if plan.popular} {#if plan.popular}
<Badge <Badge
@@ -111,7 +118,7 @@
</div> </div>
<Card <Card
class="flex h-full flex-col {plan.popular class="flex flex-col sm:min-h-[32rem] {plan.popular
? 'border-primary shadow-xl ring-2 ring-primary/20' ? 'border-primary shadow-xl ring-2 ring-primary/20'
: 'border-border shadow-md'} {className}" : 'border-border shadow-md'} {className}"
> >
@@ -150,9 +157,9 @@
</div> </div>
</CardHeader> </CardHeader>
<CardContent class="flex flex-grow flex-col space-y-3"> <CardContent class="flex flex-grow flex-col space-y-3 pb-0">
{#each displayedFeatures as feature, index (index)} {#each displayedFeatures as feature, index (index)}
<div class="flex items-start space-x-3"> <div class="flex items-start space-x-3" transition:slide={{ duration: 220 }}>
<Check <Check
class="mt-0.5 h-4 w-4 shrink-0 {feature.included class="mt-0.5 h-4 w-4 shrink-0 {feature.included
? 'text-success' ? 'text-success'
@@ -171,32 +178,35 @@
{/each} {/each}
{#if hasMoreFeatures} {#if hasMoreFeatures}
<div class="mt-auto pt-2"> <div class="mt-auto flex justify-center pt-4">
<button <button
type="button" type="button"
class={buttonClasses( class="group inline-flex min-h-8 items-center justify-center gap-1.5 rounded-full border border-border/80 bg-background px-3.5 py-1.5 text-xs font-medium text-text-muted shadow-sm transition-all duration-200 hover:border-primary/30 hover:bg-accent hover:text-text hover:shadow focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 active:scale-[0.98]"
"ghost", aria-expanded={expanded}
"sm", onclick={onToggle}
"w-full text-xs text-text-muted hover:text-text"
)}
onclick={() => (showAllFeatures = !showAllFeatures)}
> >
{#if showAllFeatures} {#if expanded}
<ChevronUp class="mr-1 h-3 w-3" />
{i18n.t("pricing.card.showLess")} {i18n.t("pricing.card.showLess")}
<ChevronUp
class="h-3.5 w-3.5 transition-transform duration-200 group-hover:-translate-y-0.5"
aria-hidden="true"
/>
{:else} {:else}
<ChevronDown class="mr-1 h-3 w-3" />
{@const moreCount = plan.features.length - initialFeaturesCount} {@const moreCount = plan.features.length - initialFeaturesCount}
{moreCount === 1 {moreCount === 1
? i18n.t("pricing.card.showMoreFeature", { count: moreCount }) ? i18n.t("pricing.card.showMoreFeature", { count: moreCount })
: i18n.t("pricing.card.showMoreFeatures", { count: moreCount })} : i18n.t("pricing.card.showMoreFeatures", { count: moreCount })}
<ChevronDown
class="h-3.5 w-3.5 transition-transform duration-200 group-hover:translate-y-0.5"
aria-hidden="true"
/>
{/if} {/if}
</button> </button>
</div> </div>
{/if} {/if}
</CardContent> </CardContent>
<CardFooter class="pt-4"> <CardFooter class="pt-3">
<a <a
href={plan.ctaHref} href={plan.ctaHref}
target={isExternal ? "_blank" : undefined} target={isExternal ? "_blank" : undefined}
@@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import { onMount } from "svelte"; import { onMount } from "svelte";
import { slide } from "svelte/transition";
import { CreditCard, Check, Calendar } from "@lucide/svelte"; import { CreditCard, Check, Calendar } from "@lucide/svelte";
import { import {
Badge, Badge,
@@ -44,6 +45,7 @@
let isYearly = $state(false); let isYearly = $state(false);
let openFaq = $state<number | null>(null); let openFaq = $state<number | null>(null);
let openPlanId = $state<number | null>(null);
let plans = $state<PricingPlan[]>(PRICING_PLANS); let plans = $state<PricingPlan[]>(PRICING_PLANS);
onMount(() => { onMount(() => {
@@ -118,15 +120,22 @@
</div> </div>
</div> </div>
<div class="grid items-stretch gap-6 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4"> <div class="grid items-start gap-6 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
{#each plans as plan (plan.id)} {#each plans as plan (plan.id)}
<PlanCard {plan} {isYearly} /> <PlanCard
{plan}
{isYearly}
expanded={openPlanId === plan.id}
onToggle={() => (openPlanId = openPlanId === plan.id ? null : plan.id)}
/>
{/each} {/each}
</div> </div>
<div class="mt-12"> {#if hideIntro}
<div class="mt-12 mb-2">
<PlanCalculator {plans} /> <PlanCalculator {plans} />
</div> </div>
{/if}
<p class="mx-auto mt-6 max-w-3xl text-center text-sm text-text-muted"> <p class="mx-auto mt-6 max-w-3xl text-center text-sm text-text-muted">
{i18n.t("pricing.section.guestPublicBefore")} {i18n.t("pricing.section.guestPublicBefore")}
@@ -169,7 +178,7 @@
{i18n.t("pricing.section.faqTitle")} {i18n.t("pricing.section.faqTitle")}
</h2> </h2>
<div <div
class="mx-auto w-full max-w-3xl divide-y divide-border rounded-lg border border-border bg-surface" class="mx-auto w-full max-w-3xl overflow-hidden divide-y divide-border rounded-lg border border-border bg-surface"
> >
{#each PRICING_FAQ as item, index (index)} {#each PRICING_FAQ as item, index (index)}
<div> <div>
@@ -185,7 +194,10 @@
</span> </span>
</button> </button>
{#if openFaq === index} {#if openFaq === index}
<div class="px-4 pb-4 text-base text-text-muted"> <div
class="px-4 pt-3 pb-5 text-base leading-7 text-text-muted"
transition:slide={{ duration: 220 }}
>
{item.answerKey ? i18n.t(item.answerKey) : item.answer} {item.answerKey ? i18n.t(item.answerKey) : item.answer}
</div> </div>
{/if} {/if}
@@ -194,7 +206,7 @@
</div> </div>
</div> </div>
<div class="mt-16 text-center"> <div class="mt-20 text-center">
<div class="rounded-2xl border border-border bg-surface px-6 py-10 shadow-sm sm:px-10"> <div class="rounded-2xl border border-border bg-surface px-6 py-10 shadow-sm sm:px-10">
<h3 class="mb-2 text-2xl font-bold text-text"> <h3 class="mb-2 text-2xl font-bold text-text">
{i18n.t("pricing.section.readyTitle")} {i18n.t("pricing.section.readyTitle")}
@@ -21,7 +21,7 @@ export const PRICING_FEATURE_CAPABILITY_BY_NAME_KEY: Record<string, string> = {
"pricing.feature.byok": "capability.byok", "pricing.feature.byok": "capability.byok",
"pricing.feature.byokAddon": "capability.byok", "pricing.feature.byokAddon": "capability.byok",
"pricing.feature.byokIncluded": "capability.byok", "pricing.feature.byokIncluded": "capability.byok",
"pricing.feature.unlimitedAiOwnKey": "capability.byok" "pricing.feature.unlimitedAiOwnKey": "capability.byok",
}; };
/** English-name fallbacks when nameKey is absent (or stripped by meter overlay). */ /** English-name fallbacks when nameKey is absent (or stripped by meter overlay). */
@@ -36,12 +36,12 @@ const PRICING_FEATURE_CAPABILITY_BY_NAME: Record<string, string> = {
"Bring your own AI key": "capability.byok", "Bring your own AI key": "capability.byok",
"Bring-your-own-key add-on": "capability.byok", "Bring-your-own-key add-on": "capability.byok",
"Bring your own AI key included": "capability.byok", "Bring your own AI key included": "capability.byok",
"Unlimited AI credits / own key": "capability.byok" "Unlimited AI credits / own key": "capability.byok",
}; };
/** Resolve the backend capability key for a marketing pricing feature bullet. */ /** Resolve the backend capability key for a marketing pricing feature bullet. */
export function resolvePricingFeatureCapabilityKey( export function resolvePricingFeatureCapabilityKey(
feature: Pick<PricingFeature, "name" | "nameKey"> feature: Pick<PricingFeature, "name" | "nameKey">,
): string | undefined { ): string | undefined {
if (feature.nameKey) { if (feature.nameKey) {
const byKey = PRICING_FEATURE_CAPABILITY_BY_NAME_KEY[feature.nameKey]; const byKey = PRICING_FEATURE_CAPABILITY_BY_NAME_KEY[feature.nameKey];
@@ -57,7 +57,7 @@ export function resolvePricingFeatureCapabilityKey(
export function applyResolvedFeaturesToPricingFeatures( export function applyResolvedFeaturesToPricingFeatures(
features: PricingFeature[], features: PricingFeature[],
originals: PricingFeature[], originals: PricingFeature[],
resolved: Record<string, boolean> | null | undefined resolved: Record<string, boolean> | null | undefined,
): PricingFeature[] { ): PricingFeature[] {
if (!resolved || typeof resolved !== "object") return features; if (!resolved || typeof resolved !== "object") return features;
return features.map((f, index) => { return features.map((f, index) => {
@@ -65,7 +65,10 @@ export function applyResolvedFeaturesToPricingFeatures(
const capabilityKey = const capabilityKey =
resolvePricingFeatureCapabilityKey(original) ?? resolvePricingFeatureCapabilityKey(original) ??
resolvePricingFeatureCapabilityKey(f); resolvePricingFeatureCapabilityKey(f);
if (!capabilityKey || !Object.prototype.hasOwnProperty.call(resolved, capabilityKey)) { if (
!capabilityKey ||
!Object.prototype.hasOwnProperty.call(resolved, capabilityKey)
) {
return f; return f;
} }
return { ...f, included: resolved[capabilityKey] === true }; return { ...f, included: resolved[capabilityKey] === true };
@@ -111,7 +114,8 @@ export const PRICING_PLANS: PricingPlan[] = [
{ {
id: 1, id: 1,
name: "Free", name: "Free",
description: "Map a sample feed and clean product data — no card required", description:
"Map a sample feed and clean your product data. No card required.",
descriptionKey: "pricing.plan.free.description", descriptionKey: "pricing.plan.free.description",
pricePerMonth: 0, pricePerMonth: 0,
maxProducts: 50, maxProducts: 50,
@@ -119,34 +123,74 @@ export const PRICING_PLANS: PricingPlan[] = [
monthlyCredits: 0, monthlyCredits: 0,
extraCreditCost: 0, extraCreditCost: 0,
features: [ features: [
{ name: "Up to 50 SKUs", nameKey: "pricing.feature.upTo50Skus", included: true }, {
{ name: "1 feed source", nameKey: "pricing.feature.oneFeedSource", included: true }, name: "Up to 50 SKUs",
nameKey: "pricing.feature.upTo50Skus",
included: true,
},
{
name: "1 feed source",
nameKey: "pricing.feature.oneFeedSource",
included: true,
},
{ {
name: "Clean data, parse specs, and fill fields", name: "Clean data, parse specs, and fill fields",
nameKey: "pricing.feature.cleanData", nameKey: "pricing.feature.cleanData",
included: true included: true,
},
{
name: "EU energy labels (EPREL)",
nameKey: "pricing.feature.eprel",
included: true,
},
{
name: "0 AI credits / month",
nameKey: "pricing.feature.zeroCredits",
included: true,
},
{
name: "1 manual export feed",
nameKey: "pricing.feature.oneManualExport",
included: true,
}, },
{ name: "EU energy labels (EPREL)", nameKey: "pricing.feature.eprel", included: true },
{ name: "0 AI credits / month", nameKey: "pricing.feature.zeroCredits", included: true },
{ name: "1 manual export feed", nameKey: "pricing.feature.oneManualExport", included: true },
{ {
name: "WooCommerce connection test only", name: "WooCommerce connection test only",
nameKey: "pricing.feature.wooTestOnly", nameKey: "pricing.feature.wooTestOnly",
included: true included: true,
},
{
name: "Up to 2 seats",
nameKey: "pricing.feature.upTo2Seats",
included: true,
},
{
name: "AI titles & descriptions",
nameKey: "pricing.feature.aiTitles",
included: false,
},
{
name: "Live store sync",
nameKey: "pricing.feature.liveStoreSync",
included: false,
},
{
name: "API access",
nameKey: "pricing.feature.apiAccess",
included: false,
},
{
name: "Bring your own AI key",
nameKey: "pricing.feature.byok",
included: false,
}, },
{ name: "Up to 2 seats", nameKey: "pricing.feature.upTo2Seats", included: true },
{ name: "AI titles & descriptions", nameKey: "pricing.feature.aiTitles", included: false },
{ name: "Live store sync", nameKey: "pricing.feature.liveStoreSync", included: false },
{ name: "API access", nameKey: "pricing.feature.apiAccess", included: false },
{ name: "Bring your own AI key", nameKey: "pricing.feature.byok", included: false }
], ],
ctaText: "Get started", ctaText: "Get started",
ctaHref: "/register" ctaHref: "/register",
}, },
{ {
id: 2, id: 2,
name: "Starter", name: "Starter",
description: "Up to 100 SKUs, entry AI (~100 credits), Woo sync — packs for more AI", description: "For small catalogs that need AI content and WooCommerce sync",
descriptionKey: "pricing.plan.starter.description", descriptionKey: "pricing.plan.starter.description",
pricePerMonth: 49, pricePerMonth: 49,
maxProducts: 100, maxProducts: 100,
@@ -154,29 +198,70 @@ export const PRICING_PLANS: PricingPlan[] = [
monthlyCredits: 100, monthlyCredits: 100,
extraCreditCost: 0.009, extraCreditCost: 0.009,
features: [ features: [
{ name: "Up to 100 SKUs", nameKey: "pricing.feature.upTo100Skus", included: true }, {
{ name: "2 feed sources", nameKey: "pricing.feature.twoFeedSources", included: true }, name: "Up to 100 SKUs",
nameKey: "pricing.feature.upTo100Skus",
included: true,
},
{
name: "2 feed sources",
nameKey: "pricing.feature.twoFeedSources",
included: true,
},
{ {
name: "100 AI credits / month (~50% catalog cover)", name: "100 AI credits / month (~50% catalog cover)",
nameKey: "pricing.feature.credits100starter", nameKey: "pricing.feature.credits100starter",
included: true included: true,
},
{
name: "AI titles & descriptions",
nameKey: "pricing.feature.aiTitles",
included: true,
},
{
name: "EU energy labels (EPREL)",
nameKey: "pricing.feature.eprel",
included: true,
},
{
name: "2 export feeds",
nameKey: "pricing.feature.twoExports",
included: true,
},
{
name: "Full WooCommerce sync",
nameKey: "pricing.feature.fullWooSync",
included: true,
},
{
name: "Read API access",
nameKey: "pricing.feature.readApi",
included: true,
},
{
name: "Email support",
nameKey: "pricing.feature.emailSupport",
included: true,
},
{
name: "Buy extra AI credit packs",
nameKey: "pricing.feature.creditPacks",
included: true,
},
{
name: "Bring your own AI key",
nameKey: "pricing.feature.byok",
included: false,
}, },
{ name: "AI titles & descriptions", nameKey: "pricing.feature.aiTitles", included: true },
{ name: "EU energy labels (EPREL)", nameKey: "pricing.feature.eprel", included: true },
{ name: "2 export feeds", nameKey: "pricing.feature.twoExports", included: true },
{ name: "Full WooCommerce sync", nameKey: "pricing.feature.fullWooSync", included: true },
{ name: "Read API access", nameKey: "pricing.feature.readApi", included: true },
{ name: "Email support", nameKey: "pricing.feature.emailSupport", included: true },
{ name: "Buy extra AI credit packs", nameKey: "pricing.feature.creditPacks", included: true },
{ name: "Bring your own AI key", nameKey: "pricing.feature.byok", included: false }
], ],
ctaText: "Get started", ctaText: "Get started",
ctaHref: "/register" ctaHref: "/register",
}, },
{ {
id: 3, id: 3,
name: "Plus", name: "Plus",
description: "Up to 400 SKUs, Woo + Shopify, AI (~400 credits)", description:
"More products, more AI credits, and support for two store platforms",
descriptionKey: "pricing.plan.plus.description", descriptionKey: "pricing.plan.plus.description",
pricePerMonth: 199, pricePerMonth: 199,
maxProducts: 400, maxProducts: 400,
@@ -184,33 +269,70 @@ export const PRICING_PLANS: PricingPlan[] = [
monthlyCredits: 400, monthlyCredits: 400,
extraCreditCost: 0.009, extraCreditCost: 0.009,
features: [ features: [
{ name: "Up to 400 SKUs", nameKey: "pricing.feature.upTo400Skus", included: true }, {
{ name: "3 feed sources", nameKey: "pricing.feature.threeFeedSources", included: true }, name: "Up to 400 SKUs",
nameKey: "pricing.feature.upTo400Skus",
included: true,
},
{
name: "3 feed sources",
nameKey: "pricing.feature.threeFeedSources",
included: true,
},
{ {
name: "400 AI credits / month (~50% catalog cover)", name: "400 AI credits / month (~50% catalog cover)",
nameKey: "pricing.feature.credits400", nameKey: "pricing.feature.credits400",
included: true included: true,
},
{
name: "AI titles & descriptions",
nameKey: "pricing.feature.aiTitles",
included: true,
},
{
name: "EU energy labels (EPREL)",
nameKey: "pricing.feature.eprel",
included: true,
},
{
name: "4 export feeds",
nameKey: "pricing.feature.fourExports",
included: true,
}, },
{ name: "AI titles & descriptions", nameKey: "pricing.feature.aiTitles", included: true },
{ name: "EU energy labels (EPREL)", nameKey: "pricing.feature.eprel", included: true },
{ name: "4 export feeds", nameKey: "pricing.feature.fourExports", included: true },
{ {
name: "Full WooCommerce + Shopify sync", name: "Full WooCommerce + Shopify sync",
nameKey: "pricing.feature.wooShopifySync", nameKey: "pricing.feature.wooShopifySync",
included: true included: true,
},
{
name: "Read API access",
nameKey: "pricing.feature.readApi",
included: true,
},
{
name: "Email support",
nameKey: "pricing.feature.emailSupport",
included: true,
},
{
name: "Buy extra AI credit packs",
nameKey: "pricing.feature.creditPacks",
included: true,
},
{
name: "Bring your own AI key",
nameKey: "pricing.feature.byok",
included: false,
}, },
{ name: "Read API access", nameKey: "pricing.feature.readApi", included: true },
{ name: "Email support", nameKey: "pricing.feature.emailSupport", included: true },
{ name: "Buy extra AI credit packs", nameKey: "pricing.feature.creditPacks", included: true },
{ name: "Bring your own AI key", nameKey: "pricing.feature.byok", included: false }
], ],
ctaText: "Get started", ctaText: "Get started",
ctaHref: "/register" ctaHref: "/register",
}, },
{ {
id: 4, id: 4,
name: "Growth", name: "Growth",
description: "Up to 1,200 SKUs, stores, BYOK, AI (~1,200 credits)", description:
"For growing catalogs that need flexible AI and store connections",
descriptionKey: "pricing.plan.growth.description", descriptionKey: "pricing.plan.growth.description",
pricePerMonth: 299, pricePerMonth: 299,
maxProducts: 1200, maxProducts: 1200,
@@ -219,29 +341,69 @@ export const PRICING_PLANS: PricingPlan[] = [
extraCreditCost: 0.009, extraCreditCost: 0.009,
popular: true, popular: true,
features: [ features: [
{ name: "Up to 1,200 SKUs", nameKey: "pricing.feature.upTo1200Skus", included: true }, {
{ name: "5 feed sources", nameKey: "pricing.feature.fiveFeedSources", included: true }, name: "Up to 1,200 SKUs",
nameKey: "pricing.feature.upTo1200Skus",
included: true,
},
{
name: "5 feed sources",
nameKey: "pricing.feature.fiveFeedSources",
included: true,
},
{ {
name: "1,200 AI credits / month (~50% catalog cover)", name: "1,200 AI credits / month (~50% catalog cover)",
nameKey: "pricing.feature.credits1200", nameKey: "pricing.feature.credits1200",
included: true included: true,
},
{
name: "AI titles & descriptions",
nameKey: "pricing.feature.aiTitles",
included: true,
},
{
name: "EU energy labels (EPREL)",
nameKey: "pricing.feature.eprel",
included: true,
},
{
name: "8 export feeds",
nameKey: "pricing.feature.eightExports",
included: true,
},
{
name: "Full formulas and variables",
nameKey: "pricing.feature.fullFormulas",
included: true,
},
{
name: "Full API access",
nameKey: "pricing.feature.fullApi",
included: true,
},
{
name: "Buy extra AI credit packs",
nameKey: "pricing.feature.creditPacks",
included: true,
},
{
name: "Bring-your-own-key add-on",
nameKey: "pricing.feature.byokAddon",
included: true,
},
{
name: "Email support (24h)",
nameKey: "pricing.feature.emailSupport24h",
included: true,
}, },
{ name: "AI titles & descriptions", nameKey: "pricing.feature.aiTitles", included: true },
{ name: "EU energy labels (EPREL)", nameKey: "pricing.feature.eprel", included: true },
{ name: "8 export feeds", nameKey: "pricing.feature.eightExports", included: true },
{ name: "Full formulas and variables", nameKey: "pricing.feature.fullFormulas", included: true },
{ name: "Full API access", nameKey: "pricing.feature.fullApi", included: true },
{ name: "Buy extra AI credit packs", nameKey: "pricing.feature.creditPacks", included: true },
{ name: "Bring-your-own-key add-on", nameKey: "pricing.feature.byokAddon", included: true },
{ name: "Email support (24h)", nameKey: "pricing.feature.emailSupport24h", included: true }
], ],
ctaText: "Get started", ctaText: "Get started",
ctaHref: "/register" ctaHref: "/register",
}, },
{ {
id: 5, id: 5,
name: "Business", name: "Business",
description: "Up to 4,000 SKUs, BYOK, AI (~4,000 credits)", description: "Higher capacity, unlimited exports, and priority support",
descriptionKey: "pricing.plan.business.description", descriptionKey: "pricing.plan.business.description",
pricePerMonth: 499, pricePerMonth: 499,
maxProducts: 4000, maxProducts: 4000,
@@ -249,36 +411,64 @@ export const PRICING_PLANS: PricingPlan[] = [
monthlyCredits: 4000, monthlyCredits: 4000,
extraCreditCost: 0.009, extraCreditCost: 0.009,
features: [ features: [
{ name: "Up to 4,000 SKUs", nameKey: "pricing.feature.upTo4kSkus", included: true }, {
{ name: "8 feed sources", nameKey: "pricing.feature.eightFeedSources", included: true }, name: "Up to 4,000 SKUs",
nameKey: "pricing.feature.upTo4kSkus",
included: true,
},
{
name: "8 feed sources",
nameKey: "pricing.feature.eightFeedSources",
included: true,
},
{ {
name: "4,000 AI credits / month (~50% catalog cover)", name: "4,000 AI credits / month (~50% catalog cover)",
nameKey: "pricing.feature.credits4000", nameKey: "pricing.feature.credits4000",
included: true included: true,
},
{
name: "AI titles & descriptions",
nameKey: "pricing.feature.aiTitles",
included: true,
},
{
name: "EU energy labels (EPREL)",
nameKey: "pricing.feature.eprel",
included: true,
}, },
{ name: "AI titles & descriptions", nameKey: "pricing.feature.aiTitles", included: true },
{ name: "EU energy labels (EPREL)", nameKey: "pricing.feature.eprel", included: true },
{ {
name: "Unlimited export feeds", name: "Unlimited export feeds",
nameKey: "pricing.feature.unlimitedExports", nameKey: "pricing.feature.unlimitedExports",
included: true included: true,
},
{
name: "Full API",
nameKey: "pricing.feature.fullApiWebhooks",
included: true,
},
{
name: "Buy extra AI credit packs",
nameKey: "pricing.feature.creditPacks",
included: true,
}, },
{ name: "Full API", nameKey: "pricing.feature.fullApiWebhooks", included: true },
{ name: "Buy extra AI credit packs", nameKey: "pricing.feature.creditPacks", included: true },
{ {
name: "Bring your own AI key included", name: "Bring your own AI key included",
nameKey: "pricing.feature.byokIncluded", nameKey: "pricing.feature.byokIncluded",
included: true included: true,
},
{
name: "Priority email support",
nameKey: "pricing.feature.priorityEmail",
included: true,
}, },
{ name: "Priority email support", nameKey: "pricing.feature.priorityEmail", included: true }
], ],
ctaText: "Get started", ctaText: "Get started",
ctaHref: "/register" ctaHref: "/register",
}, },
{ {
id: 6, id: 6,
name: "Scale", name: "Scale",
description: "Up to 12,000 SKUs, AI (~12k credits), packs/BYOK for more", description: "Built for large catalogs with demanding automation needs",
descriptionKey: "pricing.plan.scale.description", descriptionKey: "pricing.plan.scale.description",
pricePerMonth: 999, pricePerMonth: 999,
maxProducts: 12_000, maxProducts: 12_000,
@@ -286,31 +476,59 @@ export const PRICING_PLANS: PricingPlan[] = [
monthlyCredits: 12000, monthlyCredits: 12000,
extraCreditCost: 0.009, extraCreditCost: 0.009,
features: [ features: [
{ name: "Up to 12,000 SKUs", nameKey: "pricing.feature.upTo12kSkus", included: true }, {
{ name: "12 feed sources", nameKey: "pricing.feature.twelveFeedSources", included: true }, name: "Up to 12,000 SKUs",
nameKey: "pricing.feature.upTo12kSkus",
included: true,
},
{
name: "12 feed sources",
nameKey: "pricing.feature.twelveFeedSources",
included: true,
},
{ {
name: "12,000 AI credits / month (~50% catalog cover)", name: "12,000 AI credits / month (~50% catalog cover)",
nameKey: "pricing.feature.credits12000", nameKey: "pricing.feature.credits12000",
included: true included: true,
},
{
name: "AI titles & descriptions",
nameKey: "pricing.feature.aiTitles",
included: true,
},
{
name: "EU energy labels (EPREL)",
nameKey: "pricing.feature.eprel",
included: true,
}, },
{ name: "AI titles & descriptions", nameKey: "pricing.feature.aiTitles", included: true },
{ name: "EU energy labels (EPREL)", nameKey: "pricing.feature.eprel", included: true },
{ {
name: "Unlimited export feeds", name: "Unlimited export feeds",
nameKey: "pricing.feature.unlimitedExports", nameKey: "pricing.feature.unlimitedExports",
included: true included: true,
},
{
name: "Full API",
nameKey: "pricing.feature.fullApiWebhooks",
included: true,
},
{
name: "Buy extra AI credit packs",
nameKey: "pricing.feature.creditPacks",
included: true,
}, },
{ name: "Full API", nameKey: "pricing.feature.fullApiWebhooks", included: true },
{ name: "Buy extra AI credit packs", nameKey: "pricing.feature.creditPacks", included: true },
{ {
name: "Bring your own AI key included", name: "Bring your own AI key included",
nameKey: "pricing.feature.byokIncluded", nameKey: "pricing.feature.byokIncluded",
included: true included: true,
},
{
name: "Priority support + Slack",
nameKey: "pricing.feature.prioritySlack",
included: true,
}, },
{ name: "Priority support + Slack", nameKey: "pricing.feature.prioritySlack", included: true }
], ],
ctaText: "Get started", ctaText: "Get started",
ctaHref: "/register" ctaHref: "/register",
}, },
{ {
id: 7, id: 7,
@@ -324,28 +542,40 @@ export const PRICING_PLANS: PricingPlan[] = [
extraCreditCost: 0, extraCreditCost: 0,
customPrice: true, customPrice: true,
features: [ features: [
{ name: "Unlimited SKUs", nameKey: "pricing.feature.unlimitedSkus", included: true }, {
{ name: "Unlimited feed sources", nameKey: "pricing.feature.unlimitedFeeds", included: true }, name: "Unlimited SKUs",
nameKey: "pricing.feature.unlimitedSkus",
included: true,
},
{
name: "Unlimited feed sources",
nameKey: "pricing.feature.unlimitedFeeds",
included: true,
},
{ {
name: "Unlimited AI credits / own key", name: "Unlimited AI credits / own key",
nameKey: "pricing.feature.unlimitedAiOwnKey", nameKey: "pricing.feature.unlimitedAiOwnKey",
included: true included: true,
}, },
{ {
name: "SSO, dedicated account manager", name: "SSO, dedicated account manager",
nameKey: "pricing.feature.ssoWebhooksAm", nameKey: "pricing.feature.ssoWebhooksAm",
included: true included: true,
}, },
{ {
name: "Custom integrations", name: "Custom integrations",
nameKey: "pricing.feature.customIntegrations", nameKey: "pricing.feature.customIntegrations",
included: true included: true,
},
{
name: "SLA & priority support",
nameKey: "pricing.feature.slaPriority",
included: true,
}, },
{ name: "SLA & priority support", nameKey: "pricing.feature.slaPriority", included: true }
], ],
ctaText: "Contact sales", ctaText: "Contact sales",
ctaHref: "/contact-sales?source=pricing" ctaHref: "/contact-sales?source=pricing",
} },
]; ];
export const PRICING_CAPABILITIES = [ export const PRICING_CAPABILITIES = [
@@ -357,19 +587,34 @@ export const PRICING_CAPABILITIES = [
{ text: "Field mapping & validation", key: "pricing.cap.feeds.f2" }, { text: "Field mapping & validation", key: "pricing.cap.feeds.f2" },
{ text: "Multi-supplier merge", key: "pricing.cap.feeds.f3" }, { text: "Multi-supplier merge", key: "pricing.cap.feeds.f3" },
{ text: "Scheduled sync", key: "pricing.cap.feeds.f4" }, { text: "Scheduled sync", key: "pricing.cap.feeds.f4" },
{ text: "Category-aware transforms", key: "pricing.cap.feeds.f5" } { text: "Category-aware transforms", key: "pricing.cap.feeds.f5" },
] ],
}, },
{ {
category: "Processing & AI", category: "Processing & AI",
categoryKey: "pricing.cap.processing", categoryKey: "pricing.cap.processing",
features: [ features: [
{ text: "Data cleanup & attribute fill (all plans)", key: "pricing.cap.processing.f1" }, {
{ text: "AI titles & descriptions (paid)", key: "pricing.cap.processing.f2" }, text: "Data cleanup & attribute fill (all plans)",
{ text: "EU energy labels / EPREL (all plans)", key: "pricing.cap.processing.f3" }, key: "pricing.cap.processing.f1",
{ text: "Formulas, brand voice, variables", key: "pricing.cap.processing.f4" }, },
{ text: "Managed credits or your own AI key", key: "pricing.cap.processing.f5" } {
] text: "AI titles & descriptions (paid)",
key: "pricing.cap.processing.f2",
},
{
text: "EU energy labels / EPREL (all plans)",
key: "pricing.cap.processing.f3",
},
{
text: "Formulas, brand voice, variables",
key: "pricing.cap.processing.f4",
},
{
text: "Managed credits or your own AI key",
key: "pricing.cap.processing.f5",
},
],
}, },
{ {
category: "Export & channels", category: "Export & channels",
@@ -379,74 +624,81 @@ export const PRICING_CAPABILITIES = [
{ text: "WooCommerce / Shopify sync", key: "pricing.cap.export.f2" }, { text: "WooCommerce / Shopify sync", key: "pricing.cap.export.f2" },
{ text: "Full API", key: "pricing.cap.export.f3" }, { text: "Full API", key: "pricing.cap.export.f3" },
{ text: "Channel-specific formats", key: "pricing.cap.export.f4" }, { text: "Channel-specific formats", key: "pricing.cap.export.f4" },
{ text: "Bulk updates", key: "pricing.cap.export.f5" } { text: "Bulk updates", key: "pricing.cap.export.f5" },
] ],
}, },
{ {
category: "Limits & control", category: "Limits & control",
categoryKey: "pricing.cap.limits", categoryKey: "pricing.cap.limits",
features: [ features: [
{ text: "SKU caps by plan (Scale reaches 55k; Enterprise for huge catalogs)", key: "pricing.cap.limits.f1" }, {
{ text: "Monthly AI credits + buyable top-up packs", key: "pricing.cap.limits.f2" }, text: "SKU caps by plan (Scale reaches 55k; Enterprise for huge catalogs)",
key: "pricing.cap.limits.f1",
},
{
text: "Monthly AI credits + buyable top-up packs",
key: "pricing.cap.limits.f2",
},
{ text: "Team roles & invites", key: "pricing.cap.limits.f3" }, { text: "Team roles & invites", key: "pricing.cap.limits.f3" },
{ text: "Enterprise SLA options", key: "pricing.cap.limits.f4" } { text: "Enterprise SLA options", key: "pricing.cap.limits.f4" },
] ],
} },
] as const; ] as const;
export const PRICING_FAQ: PricingFaqItem[] = [ export const PRICING_FAQ: PricingFaqItem[] = [
{ {
question: "What are AI credits?", question: "What are AI credits?",
answer: answer:
"AI credits pay for steps like generating titles and descriptions (~2 credits per product enhance). Free includes 0 AI. Paid plans have per-tier SKU caps (Starter 1.5k → Scale 55k; Enterprise unlimited); included AI is sized from a credit base (not the full SKU cap) and rises by tier: Starter ~4% (100) · Plus ~10% (600) · Growth ~15% (2,700) · Business ~20% (7,000) · Scale ~16% (16,000) credits/month for 1 primary language. Finish more AI with packs or BYOK (Growth+). Plan price is for platform + included AI — not a full-catalog AI bundle.", "AI credits are used when Descrybe generates content such as product titles and descriptions. Enhancing one product usually costs about two credits. Each paid plan includes a monthly allowance, and you can buy more credits when needed. Growth plans and above can also use their own AI key.",
questionKey: "pricing.faq.credits.q", questionKey: "pricing.faq.credits.q",
answerKey: "pricing.faq.credits.a" answerKey: "pricing.faq.credits.a",
}, },
{ {
question: "Can I buy extra AI credits?", question: "Can I buy extra AI credits?",
answer: answer:
"Yes. One-time AI packs from Billing (Checkout mode=payment — not subscription add-ons): Nano 25 credits / $29 · Starter 65 / $59 · Plus 200 / $149 · Growth 500 / $299 · Catalog 1,200 / $599 · Business 3,000 / $1,299 · Scale 8,000 / $2,999. Packs do not raise SKU limits.", "Yes. You can buy a one-time credit pack from Billing whenever you need more AI capacity. Credit packs do not change your plan's SKU limit.",
questionKey: "pricing.faq.packs.q", questionKey: "pricing.faq.packs.q",
answerKey: "pricing.faq.packs.a" answerKey: "pricing.faq.packs.a",
}, },
{ {
question: "What happens if I hit my product or credit limit?", question: "What happens if I hit my product or credit limit?",
answer: answer:
"We warn you as you get close. Each plan has a SKU cap that grows by tier (Scale reaches 55k; 1M+ catalogs are Enterprise). AI credits are often the binding limit for enrichment. When you run out of AI credits, buy a credit pack, wait for the next monthly grant, upgrade for a larger grant, or use your own AI key (Growth+).", "We warn you as you get close. Each plan has a SKU cap that grows by tier (Scale reaches 55k; 1M+ catalogs are Enterprise). AI credits are often the binding limit for enrichment. When you run out of AI credits, buy a credit pack, wait for the next monthly grant, upgrade for a larger grant, or use your own AI key (Growth+).",
questionKey: "pricing.faq.limits.q", questionKey: "pricing.faq.limits.q",
answerKey: "pricing.faq.limits.a" answerKey: "pricing.faq.limits.a",
}, },
{ {
question: "Can I upgrade or downgrade?", question: "Can I upgrade or downgrade?",
answer: answer:
"Yes. Start on Free, then upgrade to Starter, Plus, Growth, Business, or Scale from Plans or Billing (Stripe Checkout). Enterprise is always sales-led.", "Yes. Start on Free, then upgrade to Starter, Plus, Growth, Business, or Scale from Plans or Billing (Stripe Checkout). Enterprise is always sales-led.",
questionKey: "pricing.faq.change.q", questionKey: "pricing.faq.change.q",
answerKey: "pricing.faq.change.a" answerKey: "pricing.faq.change.a",
}, },
{ {
question: "What does Free include?", question: "What does Free include?",
answer: answer:
"Forever Free: 50 products, one feed, data cleanup and attribute fill, EU energy labels (EPREL — public data, no credits), plus one manual export — with 0 AI credits. No credit card. Upgrade when you need AI titles, descriptions, or more capacity.", "The Free plan includes 50 products, one supplier feed, data cleanup, attribute filling, EU energy labels, and one manual export. It does not include AI credits, and no credit card is required.",
questionKey: "pricing.faq.free.q", questionKey: "pricing.faq.free.q",
answerKey: "pricing.faq.free.a" answerKey: "pricing.faq.free.a",
}, },
{ {
question: "Why not pay per product like content-only tools?", question: "Why not pay per product like content-only tools?",
answer: answer:
"Descrybe is built for the full path from supplier feed to catalog to WooCommerce or export — not only AI copy. You pay for platform capacity (products and feeds); AI is a usage layer on top.", "Descrybe handles the full workflow from supplier feed to finished catalog, including mapping, enrichment, exports, and store sync. Your plan covers the products and feeds you manage, while AI usage is measured separately.",
questionKey: "pricing.faq.why.q", questionKey: "pricing.faq.why.q",
answerKey: "pricing.faq.why.a" answerKey: "pricing.faq.why.a",
}, },
{ {
question: "How does annual billing work?", question: "How does annual billing work?",
answer: answer:
"Yearly billing is about 20% off the monthly list price. Start free, then choose yearly in Checkout when you upgrade (or contact sales).", "Yearly billing is about 20% off the monthly list price. Start free, then choose yearly in Checkout when you upgrade (or contact sales).",
questionKey: "pricing.faq.annual.q", questionKey: "pricing.faq.annual.q",
answerKey: "pricing.faq.annual.a" answerKey: "pricing.faq.annual.a",
} },
]; ];
export const SALES_CALENDLY_URL = "https://calendly.com/tim-berce/descrybe-demo"; export const SALES_CALENDLY_URL =
"https://calendly.com/tim-berce/descrybe-demo";
/** In-app contact sales form (preferred CTA; Calendly remains optional on the form). */ /** In-app contact sales form (preferred CTA; Calendly remains optional on the form). */
export const CONTACT_SALES_HREF = "/contact-sales"; export const CONTACT_SALES_HREF = "/contact-sales";
@@ -466,7 +718,10 @@ export type ApiPublicPlan = {
}; };
function isSkuFeature(feature: PricingFeature): boolean { function isSkuFeature(feature: PricingFeature): boolean {
return Boolean(feature.nameKey?.toLowerCase().includes("skus")) || /SKU/i.test(feature.name); return (
Boolean(feature.nameKey?.toLowerCase().includes("skus")) ||
/SKU/i.test(feature.name)
);
} }
function isCreditsFeature(feature: PricingFeature): boolean { function isCreditsFeature(feature: PricingFeature): boolean {
@@ -479,12 +734,15 @@ function isCreditsFeature(feature: PricingFeature): boolean {
/** Overlay DB meters onto marketing shells (price/CTA/feature matrix stay marketing). */ /** Overlay DB meters onto marketing shells (price/CTA/feature matrix stay marketing). */
export function applyApiMetersToPricingPlan( export function applyApiMetersToPricingPlan(
marketing: PricingPlan, marketing: PricingPlan,
api: ApiPublicPlan | undefined api: ApiPublicPlan | undefined,
): PricingPlan { ): PricingPlan {
if (!api) return marketing; if (!api) return marketing;
const monthlyCredits = const monthlyCredits =
typeof api.monthly_credits === "number" ? api.monthly_credits : marketing.monthlyCredits; typeof api.monthly_credits === "number"
const cover = typeof api.ai_cover_percent === "number" ? api.ai_cover_percent : 0; ? api.monthly_credits
: marketing.monthlyCredits;
const cover =
typeof api.ai_cover_percent === "number" ? api.ai_cover_percent : 0;
const description = api.description?.trim() || marketing.description; const description = api.description?.trim() || marketing.description;
const maxProducts = marketing.customPrice const maxProducts = marketing.customPrice
? marketing.maxProducts ? marketing.maxProducts
@@ -493,11 +751,16 @@ export function applyApiMetersToPricingPlan(
: marketing.maxProducts; : marketing.maxProducts;
const features = marketing.features.map((f) => { const features = marketing.features.map((f) => {
if (isSkuFeature(f) && !marketing.customPrice && typeof maxProducts === "number" && Number.isFinite(maxProducts)) { if (
isSkuFeature(f) &&
!marketing.customPrice &&
typeof maxProducts === "number" &&
Number.isFinite(maxProducts)
) {
return { return {
...f, ...f,
name: `Up to ${maxProducts.toLocaleString()} SKUs`, name: `Up to ${maxProducts.toLocaleString()} SKUs`,
nameKey: undefined nameKey: undefined,
}; };
} }
if (isCreditsFeature(f)) { if (isCreditsFeature(f)) {
@@ -505,7 +768,7 @@ export function applyApiMetersToPricingPlan(
return { return {
...f, ...f,
name: "0 AI credits / month", name: "0 AI credits / month",
nameKey: "pricing.feature.zeroCredits" nameKey: "pricing.feature.zeroCredits",
}; };
} }
if (marketing.customPrice) return f; if (marketing.customPrice) return f;
@@ -513,7 +776,7 @@ export function applyApiMetersToPricingPlan(
return { return {
...f, ...f,
name: `${monthlyCredits.toLocaleString()} AI credits / month${coverLabel}`, name: `${monthlyCredits.toLocaleString()} AI credits / month${coverLabel}`,
nameKey: undefined nameKey: undefined,
}; };
} }
return f; return f;
@@ -523,16 +786,20 @@ export function applyApiMetersToPricingPlan(
...marketing, ...marketing,
id: api.id > 0 ? api.id : marketing.id, id: api.id > 0 ? api.id : marketing.id,
description, description,
maxProducts: typeof maxProducts === "number" ? maxProducts : marketing.maxProducts, maxProducts:
typeof maxProducts === "number" ? maxProducts : marketing.maxProducts,
monthlyCredits, monthlyCredits,
features features,
}; };
} }
export function mergePricingPlansFromApi(apiPlans: ApiPublicPlan[]): PricingPlan[] { export function mergePricingPlansFromApi(
apiPlans: ApiPublicPlan[],
): PricingPlan[] {
return PRICING_PLANS.map((marketing) => { return PRICING_PLANS.map((marketing) => {
const api = apiPlans.find( const api = apiPlans.find(
(p) => p.name.trim().toLowerCase() === marketing.name.trim().toLowerCase() (p) =>
p.name.trim().toLowerCase() === marketing.name.trim().toLowerCase(),
); );
return applyApiMetersToPricingPlan(marketing, api); return applyApiMetersToPricingPlan(marketing, api);
}); });
@@ -13,7 +13,7 @@
const featureKeys = ["home.cta.f1", "home.cta.f2", "home.cta.f3"] as const; const featureKeys = ["home.cta.f1", "home.cta.f2", "home.cta.f3"] as const;
</script> </script>
<section {id} class={`w-full bg-muted py-16 md:py-24 ${className}`}> <section {id} class={`w-full bg-muted pb-16 md:pb-24 ${className}`}>
<div class="container mx-auto max-w-[1216px] px-4 md:px-6"> <div class="container mx-auto max-w-[1216px] px-4 md:px-6">
<div class="overflow-hidden rounded-2xl bg-secondary text-secondary-foreground shadow-xl"> <div class="overflow-hidden rounded-2xl bg-secondary text-secondary-foreground shadow-xl">
<div class="grid grid-cols-1 items-center overflow-hidden lg:grid-cols-2"> <div class="grid grid-cols-1 items-center overflow-hidden lg:grid-cols-2">
+23 -23
View File
@@ -27,33 +27,33 @@
} }
</script> </script>
<footer class={`w-full border-t border-border bg-background ${className}`} aria-label={i18n.t("site.footer.aria")}> <footer class={`w-full border-t border-border/80 bg-surface/30 ${className}`} aria-label={i18n.t("site.footer.aria")}>
<div class="container mx-auto max-w-[1216px] px-4 py-16 md:px-6"> <div class="container mx-auto max-w-[1216px] px-4 py-12 sm:py-16 md:px-6 lg:py-20">
<div class="grid grid-cols-1 gap-12 md:grid-cols-2 lg:grid-cols-4"> <div class="grid grid-cols-1 gap-10 md:grid-cols-3 md:gap-12 lg:gap-20">
<div class="lg:col-span-2"> <div class="md:col-span-2">
<a href="/" class="mb-6 flex items-center space-x-3" aria-label={i18n.t("app.home")}> <a href="/" class="mb-5 inline-flex items-center space-x-3" aria-label={i18n.t("app.home")}>
<div class="relative flex h-10 w-10 items-center justify-center"> <div class="relative flex h-11 w-11 items-center justify-center rounded-xl bg-background shadow-sm ring-1 ring-border/80">
<img <img
src="/descrybe_logo.png" src="/descrybe_logo.png"
alt="" alt=""
width="40" width="40"
height="40" height="40"
class="h-auto w-auto" class="h-9 w-9"
/> />
</div> </div>
<span class="text-xl font-bold text-text">{i18n.t("app.name")}</span> <span class="text-xl font-bold tracking-tight text-text">{i18n.t("app.name")}</span>
</a> </a>
<p class="mb-6 max-w-md text-text-muted"> <p class="mb-6 max-w-xl text-sm leading-7 text-text-muted sm:text-base">
{i18n.t("site.footer.description")} {i18n.t("site.footer.description")}
</p> </p>
{#if FOOTER_DATA.contactInfo} {#if FOOTER_DATA.contactInfo}
<div class="flex flex-col space-y-3"> <div class="flex flex-col items-start space-y-3">
{#if FOOTER_DATA.contactInfo.email} {#if FOOTER_DATA.contactInfo.email}
<div class="flex items-start gap-3"> <div class="inline-flex items-center gap-2.5 rounded-full border border-border/80 bg-background px-4 py-2.5 shadow-sm">
<svg <svg
class="mt-0.5 h-5 w-5 text-text-muted" class="h-4 w-4 text-primary"
fill="none" fill="none"
viewBox="0 0 24 24" viewBox="0 0 24 24"
stroke="currentColor" stroke="currentColor"
@@ -68,7 +68,7 @@
</svg> </svg>
<a <a
href={`mailto:${FOOTER_DATA.contactInfo.email}`} href={`mailto:${FOOTER_DATA.contactInfo.email}`}
class="text-text-muted transition-colors hover:text-link" class="text-sm font-medium text-text-muted transition-colors hover:text-link focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring"
> >
{FOOTER_DATA.contactInfo.email} {FOOTER_DATA.contactInfo.email}
</a> </a>
@@ -79,18 +79,18 @@
</div> </div>
{#each FOOTER_DATA.columns as column} {#each FOOTER_DATA.columns as column}
<div> <div class="border-t border-border/80 pt-8 md:border-t-0 md:border-l md:pt-1 md:pl-10">
<h2 class="mb-5 text-base font-semibold text-text"> <h2 class="mb-4 text-sm font-semibold tracking-wide text-text uppercase">
{columnTitleKeys[column.title] {columnTitleKeys[column.title]
? i18n.t(columnTitleKeys[column.title]) ? i18n.t(columnTitleKeys[column.title])
: column.title} : column.title}
</h2> </h2>
<ul class="space-y-3"> <ul class="grid grid-cols-2 gap-2 md:grid-cols-1">
{#each column.links as link} {#each column.links as link}
<li> <li>
<a <a
href={link.href} href={link.href}
class="text-text-muted transition-colors hover:text-link" class="block rounded-lg border border-transparent px-3 py-2.5 text-sm font-medium text-text-muted transition hover:border-border/80 hover:bg-background hover:text-link hover:shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring"
> >
{linkLabel(link.text)} {linkLabel(link.text)}
</a> </a>
@@ -102,10 +102,10 @@
</div> </div>
</div> </div>
<div class="w-full border-t border-border"> <div class="w-full border-t border-border/80 bg-background/60">
<div class="container mx-auto max-w-[1216px] px-4 py-6 md:px-6"> <div class="container mx-auto max-w-[1216px] px-4 py-6 md:px-6">
<div class="flex flex-col items-center justify-between md:flex-row"> <div class="flex flex-col items-start justify-between gap-5 md:flex-row md:items-center">
<div class="mb-4 text-sm text-text-muted md:mb-0"> <div class="text-sm text-text-muted">
{i18n.t("site.footer.rightsLine", { {i18n.t("site.footer.rightsLine", {
year: currentYear, year: currentYear,
name: i18n.t("app.name") name: i18n.t("app.name")
@@ -113,18 +113,18 @@
</div> </div>
{#if FOOTER_DATA.legalLinks.length > 0} {#if FOOTER_DATA.legalLinks.length > 0}
<div class="flex flex-wrap gap-6 text-sm"> <div class="flex flex-wrap gap-x-5 gap-y-3 text-sm md:justify-end">
{#each FOOTER_DATA.legalLinks as link} {#each FOOTER_DATA.legalLinks as link}
<a <a
href={link.href} href={link.href}
class="text-text-muted transition-colors hover:text-link" class="text-text-muted transition-colors hover:text-link focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring"
> >
{linkLabel(link.text)} {linkLabel(link.text)}
</a> </a>
{/each} {/each}
<button <button
type="button" type="button"
class="text-text-muted transition-colors hover:text-link" class="text-text-muted transition-colors hover:text-link focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring"
onclick={() => cookieConsent.openPreferences()} onclick={() => cookieConsent.openPreferences()}
> >
{i18n.t("site.cookieSettings")} {i18n.t("site.cookieSettings")}
@@ -10,7 +10,7 @@
aria-hidden="true" aria-hidden="true"
></div> ></div>
<div <div
class="bg-tiled-pattern dark:bg-tiled-pattern-dark pointer-events-none absolute inset-x-0 bottom-0 bg-repeat opacity-90" class="bg-tiled-pattern dark:bg-tiled-pattern-dark pointer-events-none absolute inset-x-0 bottom-0 bg-repeat opacity-90 dark:opacity-20"
style="top: 4rem; mask-image: linear-gradient(to bottom, black 0%, transparent 55%, transparent 100%); -webkit-mask-image: linear-gradient(to bottom, black 0%, transparent 55%, transparent 100%);" style="top: 4rem; mask-image: linear-gradient(to bottom, black 0%, transparent 55%, transparent 100%); -webkit-mask-image: linear-gradient(to bottom, black 0%, transparent 55%, transparent 100%);"
aria-hidden="true" aria-hidden="true"
></div> ></div>
@@ -46,9 +46,21 @@
</a> </a>
<a <a
href="/register" href="/register"
class="rounded-md border border-border bg-surface/70 px-4 py-2.5 text-sm leading-6 font-semibold text-text backdrop-blur-sm transition hover:bg-accent focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring" class="inline-flex items-center gap-2 rounded-md border border-border bg-surface/70 px-4 py-2.5 text-sm leading-6 font-semibold text-text backdrop-blur-sm transition hover:bg-accent focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring"
> >
{i18n.t("home.hero.apply")} <span aria-hidden="true"></span> {i18n.t("home.hero.apply")}
<svg
class="h-4 w-4 shrink-0"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<path d="M5 12h14M13 6l6 6-6 6" />
</svg>
</a> </a>
</div> </div>
<div class="mt-10 flex flex-col items-center justify-center"> <div class="mt-10 flex flex-col items-center justify-center">
@@ -23,9 +23,9 @@
}); });
function navTitle(href: string): string { function navTitle(href: string): string {
if (href === "/") return i18n.t("site.nav.home"); if (href === "/#how-it-works") return i18n.t("site.nav.howItWorks");
if (href === "/#benefits") return i18n.t("site.nav.benefits");
if (href === "/pricing") return i18n.t("site.nav.pricing"); if (href === "/pricing") return i18n.t("site.nav.pricing");
if (href === "/docs") return i18n.t("site.nav.apiDocs");
return href; return href;
} }
@@ -212,20 +212,24 @@
</div> </div>
{#if mobileMenuOpen} {#if mobileMenuOpen}
<div class="absolute top-full right-0 left-0 bg-background/95 shadow-md backdrop-blur-md lg:hidden"> <div
<div class="border-t border-border/80 px-4 pt-2 pb-6"> class="absolute top-full right-3 left-3 mt-2 overflow-hidden rounded-xl border border-border/80 bg-background/95 shadow-xl backdrop-blur-md sm:right-4 sm:left-auto sm:w-80 lg:hidden"
<nav class="mt-2 mb-4 flex flex-col space-y-1" aria-label={i18n.t("site.nav.mobile")}> >
<div class="p-3">
<nav class="flex flex-col gap-1" aria-label={i18n.t("site.nav.mobile")}>
{#each NAV_ITEMS as item} {#each NAV_ITEMS as item}
<a <a
href={item.href} href={item.href}
class="block py-2.5 text-base font-medium text-text transition-colors hover:text-link" class="rounded-lg px-3 py-2.5 text-sm font-medium text-text transition-colors hover:bg-accent hover:text-link"
onclick={(e) => onNavClick(e, item.href)} onclick={(e) => onNavClick(e, item.href)}
> >
{navTitle(item.href)} {navTitle(item.href)}
</a> </a>
{/each} {/each}
</nav> </nav>
<div class="mt-4 space-y-3"> <div
class={`mt-3 border-t border-border/80 pt-3 ${loggedIn ? "space-y-3" : "grid grid-cols-2 gap-2"}`}
>
{#if loggedIn} {#if loggedIn}
<p class="truncate text-center text-sm font-medium text-text-muted"> <p class="truncate text-center text-sm font-medium text-text-muted">
{accountLabel} {accountLabel}
@@ -242,7 +246,7 @@
<a <a
href="/login" href="/login"
class={buttonClasses( class={buttonClasses(
"ghost", "outline",
"default", "default",
"box-border h-10 w-full min-h-10 text-sm font-medium" "box-border h-10 w-full min-h-10 text-sm font-medium"
)}>{i18n.t("site.logIn")}</a )}>{i18n.t("site.logIn")}</a
+12 -12
View File
@@ -41,7 +41,7 @@ export {
export const PRODUCT_EXPLANATION = { export const PRODUCT_EXPLANATION = {
id: "product", id: "product",
eyebrow: "What Descrybe does", eyebrow: "What Descrybe does",
title: "Supplier feeds in. Ready listings out — export, WooCommerce, or API.", title: "Turn supplier feeds into listings you can publish anywhere.",
description: description:
"Descrybe helps ecommerce teams turn supplier CSV and XML feeds into clean product catalogs. Map fields to your categories, enrich attributes and listing copy, then ship data through export feeds, WooCommerce sync, or the public API.", "Descrybe helps ecommerce teams turn supplier CSV and XML feeds into clean product catalogs. Map fields to your categories, enrich attributes and listing copy, then ship data through export feeds, WooCommerce sync, or the public API.",
pipeline: [ pipeline: [
@@ -56,12 +56,12 @@ export const PRODUCT_EXPLANATION = {
export const PROCESS_STEPS: ProcessStep[] = [ export const PROCESS_STEPS: ProcessStep[] = [
{ {
number: 1, number: 1,
title: "Import Your Taxonomy", title: "Set up your catalog",
description: "Set up the categories and attributes your store already uses", description: "Organize and enrich products for your store",
features: [ features: [
{ text: "Define required attributes for each category" }, { text: "Choose product details for each category" },
{ text: "Set per-category title formulas" }, { text: "Set per-category title formulas" },
{ text: "Set product description and search-snippet templates" } { text: "Decide how product descriptions should be written" }
], ],
image: "/import_taxonomy.png" image: "/import_taxonomy.png"
}, },
@@ -104,7 +104,7 @@ export const PROCESS_STEPS: ProcessStep[] = [
export const MAIN_BENEFIT = { export const MAIN_BENEFIT = {
title: "Faster path from feed to listing", title: "Faster path from feed to listing",
description: description:
"Descrybe cuts the manual work of turning raw supplier data into store-ready product pages — categorize, fill attributes, and write listing copy at catalog scale.", "Descrybe cuts the manual work of turning raw supplier data into store-ready product pages. Categorize products, fill attributes, and write listing copy at catalog scale.",
stats: [ stats: [
{ {
label: "Manual Process", label: "Manual Process",
@@ -144,7 +144,7 @@ export const STANDARD_BENEFITS: Benefit[] = [
icon: "chart", icon: "chart",
title: "Clearer listings, stronger conversion", title: "Clearer listings, stronger conversion",
description: description:
"Titles and descriptions that highlight what shoppers care about — benefits, specs, and search terms that match your catalog." "Create titles and descriptions around the benefits, specifications, and search terms your customers actually care about."
}, },
{ {
icon: "database", icon: "database",
@@ -162,7 +162,7 @@ export const STANDARD_BENEFITS: Benefit[] = [
icon: "cost", icon: "cost",
title: "Less manual data entry", title: "Less manual data entry",
description: description:
"Automate mapping, attribute fill, and listing copy so your team spends time on merchandising — not spreadsheet cleanup." "Automate mapping, attribute filling, and listing copy so your team can focus on merchandising instead of cleaning spreadsheets."
}, },
{ {
icon: "scale", icon: "scale",
@@ -217,7 +217,7 @@ export const PRICING_ENABLED = true;
export const FOOTER_DATA = { export const FOOTER_DATA = {
companyName: "Descrybe", companyName: "Descrybe",
companyDescription: companyDescription:
"Descrybe turns supplier feeds into clean, channel-ready product catalogs — map fields, enrich attributes and listing copy, then export or sync to WooCommerce.", "Descrybe turns supplier feeds into clean, channel-ready product catalogs. Map fields, enrich product data, then export your catalog or sync it to WooCommerce.",
columns: [ columns: [
{ {
title: "Navigation", title: "Navigation",
@@ -245,7 +245,7 @@ export const FOOTER_DATA = {
* Log in / Get started live in header actions (email auth in v2; Clerk in legacy). * Log in / Get started live in header actions (email auth in v2; Clerk in legacy).
*/ */
export const NAV_ITEMS = [ export const NAV_ITEMS = [
{ title: "Home", href: "/" }, { title: "How it works", href: "/#how-it-works" },
...(PRICING_ENABLED ? [{ title: "Pricing", href: "/pricing" }] : []), { title: "Benefits", href: "/#benefits" },
{ title: "API Docs", href: "/docs" } ...(PRICING_ENABLED ? [{ title: "Pricing", href: "/pricing" }] : [])
] as Array<{ title: string; href: string }>; ] as Array<{ title: string; href: string }>;
File diff suppressed because it is too large Load Diff
Binary file not shown.