frontend page fixes v1
This commit is contained in:
@@ -46,8 +46,8 @@
|
||||
{i18n.t("pricing.calc.lead")}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-8">
|
||||
<div class="grid gap-6 sm:grid-cols-3">
|
||||
<CardContent class="grid gap-8 lg:grid-cols-2">
|
||||
<div class="grid gap-6">
|
||||
<div class="space-y-2 text-sm">
|
||||
<label class="font-medium text-text" for="plan-calc-feeds"
|
||||
>{i18n.t("pricing.calc.feeds")}</label
|
||||
@@ -102,7 +102,7 @@
|
||||
</div>
|
||||
|
||||
{#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="mt-2 text-sm text-text-muted">{i18n.t("pricing.calc.enterpriseBody")}</p>
|
||||
<a
|
||||
@@ -117,8 +117,8 @@
|
||||
{:else if result.recommended}
|
||||
{@const rec = result.recommended}
|
||||
{@const budget = result.budget}
|
||||
<div class="grid gap-4 lg:grid-cols-2">
|
||||
<div class="rounded-lg border border-primary/30 bg-primary/5 px-4 py-5">
|
||||
<div class={budget ? "grid h-full gap-4" : "h-full"}>
|
||||
<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">
|
||||
{i18n.t("pricing.calc.recommendedLabel")}
|
||||
</p>
|
||||
@@ -132,7 +132,7 @@
|
||||
{formatMoney(rec.monthlyListUSD)} / {i18n.t("pricing.card.perMonth")}
|
||||
{/if}
|
||||
</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>
|
||||
{i18n.t("pricing.calc.coversCredits", {
|
||||
count: result.creditsNeeded.toLocaleString()
|
||||
@@ -152,7 +152,7 @@
|
||||
</ul>
|
||||
<a
|
||||
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 })}
|
||||
</a>
|
||||
@@ -195,14 +195,8 @@
|
||||
{i18n.t("pricing.calc.ctaPlan", { name: budget.plan.name })}
|
||||
</a>
|
||||
</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}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<p class="text-center text-xs text-text-muted sm:text-left">{i18n.t("pricing.calc.disclaimer")}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { Check, Zap, Sparkles, ChevronDown, ChevronUp } from "@lucide/svelte";
|
||||
import { slide } from "svelte/transition";
|
||||
import {
|
||||
Badge,
|
||||
Card,
|
||||
@@ -17,12 +18,18 @@
|
||||
type Props = {
|
||||
plan: PricingPlan;
|
||||
isYearly?: boolean;
|
||||
expanded?: boolean;
|
||||
onToggle?: () => void;
|
||||
class?: string;
|
||||
};
|
||||
|
||||
let { plan, isYearly = false, class: className = "" }: Props = $props();
|
||||
|
||||
let showAllFeatures = $state(false);
|
||||
let {
|
||||
plan,
|
||||
isYearly = false,
|
||||
expanded = false,
|
||||
onToggle = () => {},
|
||||
class: className = ""
|
||||
}: Props = $props();
|
||||
|
||||
const yearlyPrice = $derived(Math.round(plan.pricePerMonth * 12 * (1 - ANNUAL_DISCOUNT)));
|
||||
const displayPrice = $derived(isYearly ? yearlyPrice : plan.pricePerMonth);
|
||||
@@ -37,7 +44,7 @@
|
||||
|
||||
const initialFeaturesCount = 5;
|
||||
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);
|
||||
|
||||
@@ -98,7 +105,7 @@
|
||||
</script>
|
||||
|
||||
<!-- 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">
|
||||
{#if plan.popular}
|
||||
<Badge
|
||||
@@ -111,7 +118,7 @@
|
||||
</div>
|
||||
|
||||
<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-border shadow-md'} {className}"
|
||||
>
|
||||
@@ -150,9 +157,9 @@
|
||||
</div>
|
||||
</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)}
|
||||
<div class="flex items-start space-x-3">
|
||||
<div class="flex items-start space-x-3" transition:slide={{ duration: 220 }}>
|
||||
<Check
|
||||
class="mt-0.5 h-4 w-4 shrink-0 {feature.included
|
||||
? 'text-success'
|
||||
@@ -171,32 +178,35 @@
|
||||
{/each}
|
||||
|
||||
{#if hasMoreFeatures}
|
||||
<div class="mt-auto pt-2">
|
||||
<div class="mt-auto flex justify-center pt-4">
|
||||
<button
|
||||
type="button"
|
||||
class={buttonClasses(
|
||||
"ghost",
|
||||
"sm",
|
||||
"w-full text-xs text-text-muted hover:text-text"
|
||||
)}
|
||||
onclick={() => (showAllFeatures = !showAllFeatures)}
|
||||
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]"
|
||||
aria-expanded={expanded}
|
||||
onclick={onToggle}
|
||||
>
|
||||
{#if showAllFeatures}
|
||||
<ChevronUp class="mr-1 h-3 w-3" />
|
||||
{#if expanded}
|
||||
{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}
|
||||
<ChevronDown class="mr-1 h-3 w-3" />
|
||||
{@const moreCount = plan.features.length - initialFeaturesCount}
|
||||
{moreCount === 1
|
||||
? i18n.t("pricing.card.showMoreFeature", { 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}
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</CardContent>
|
||||
|
||||
<CardFooter class="pt-4">
|
||||
<CardFooter class="pt-3">
|
||||
<a
|
||||
href={plan.ctaHref}
|
||||
target={isExternal ? "_blank" : undefined}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import { slide } from "svelte/transition";
|
||||
import { CreditCard, Check, Calendar } from "@lucide/svelte";
|
||||
import {
|
||||
Badge,
|
||||
@@ -44,6 +45,7 @@
|
||||
|
||||
let isYearly = $state(false);
|
||||
let openFaq = $state<number | null>(null);
|
||||
let openPlanId = $state<number | null>(null);
|
||||
let plans = $state<PricingPlan[]>(PRICING_PLANS);
|
||||
|
||||
onMount(() => {
|
||||
@@ -118,15 +120,22 @@
|
||||
</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)}
|
||||
<PlanCard {plan} {isYearly} />
|
||||
<PlanCard
|
||||
{plan}
|
||||
{isYearly}
|
||||
expanded={openPlanId === plan.id}
|
||||
onToggle={() => (openPlanId = openPlanId === plan.id ? null : plan.id)}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class="mt-12">
|
||||
{#if hideIntro}
|
||||
<div class="mt-12 mb-2">
|
||||
<PlanCalculator {plans} />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<p class="mx-auto mt-6 max-w-3xl text-center text-sm text-text-muted">
|
||||
{i18n.t("pricing.section.guestPublicBefore")}
|
||||
@@ -169,7 +178,7 @@
|
||||
{i18n.t("pricing.section.faqTitle")}
|
||||
</h2>
|
||||
<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)}
|
||||
<div>
|
||||
@@ -185,7 +194,10 @@
|
||||
</span>
|
||||
</button>
|
||||
{#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}
|
||||
</div>
|
||||
{/if}
|
||||
@@ -194,7 +206,7 @@
|
||||
</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">
|
||||
<h3 class="mb-2 text-2xl font-bold text-text">
|
||||
{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.byokAddon": "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). */
|
||||
@@ -36,12 +36,12 @@ const PRICING_FEATURE_CAPABILITY_BY_NAME: Record<string, string> = {
|
||||
"Bring your own AI key": "capability.byok",
|
||||
"Bring-your-own-key add-on": "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. */
|
||||
export function resolvePricingFeatureCapabilityKey(
|
||||
feature: Pick<PricingFeature, "name" | "nameKey">
|
||||
feature: Pick<PricingFeature, "name" | "nameKey">,
|
||||
): string | undefined {
|
||||
if (feature.nameKey) {
|
||||
const byKey = PRICING_FEATURE_CAPABILITY_BY_NAME_KEY[feature.nameKey];
|
||||
@@ -57,7 +57,7 @@ export function resolvePricingFeatureCapabilityKey(
|
||||
export function applyResolvedFeaturesToPricingFeatures(
|
||||
features: PricingFeature[],
|
||||
originals: PricingFeature[],
|
||||
resolved: Record<string, boolean> | null | undefined
|
||||
resolved: Record<string, boolean> | null | undefined,
|
||||
): PricingFeature[] {
|
||||
if (!resolved || typeof resolved !== "object") return features;
|
||||
return features.map((f, index) => {
|
||||
@@ -65,7 +65,10 @@ export function applyResolvedFeaturesToPricingFeatures(
|
||||
const capabilityKey =
|
||||
resolvePricingFeatureCapabilityKey(original) ??
|
||||
resolvePricingFeatureCapabilityKey(f);
|
||||
if (!capabilityKey || !Object.prototype.hasOwnProperty.call(resolved, capabilityKey)) {
|
||||
if (
|
||||
!capabilityKey ||
|
||||
!Object.prototype.hasOwnProperty.call(resolved, capabilityKey)
|
||||
) {
|
||||
return f;
|
||||
}
|
||||
return { ...f, included: resolved[capabilityKey] === true };
|
||||
@@ -111,7 +114,8 @@ export const PRICING_PLANS: PricingPlan[] = [
|
||||
{
|
||||
id: 1,
|
||||
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",
|
||||
pricePerMonth: 0,
|
||||
maxProducts: 50,
|
||||
@@ -119,34 +123,74 @@ export const PRICING_PLANS: PricingPlan[] = [
|
||||
monthlyCredits: 0,
|
||||
extraCreditCost: 0,
|
||||
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",
|
||||
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",
|
||||
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",
|
||||
ctaHref: "/register"
|
||||
ctaHref: "/register",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
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",
|
||||
pricePerMonth: 49,
|
||||
maxProducts: 100,
|
||||
@@ -154,29 +198,70 @@ export const PRICING_PLANS: PricingPlan[] = [
|
||||
monthlyCredits: 100,
|
||||
extraCreditCost: 0.009,
|
||||
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)",
|
||||
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",
|
||||
ctaHref: "/register"
|
||||
ctaHref: "/register",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
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",
|
||||
pricePerMonth: 199,
|
||||
maxProducts: 400,
|
||||
@@ -184,33 +269,70 @@ export const PRICING_PLANS: PricingPlan[] = [
|
||||
monthlyCredits: 400,
|
||||
extraCreditCost: 0.009,
|
||||
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)",
|
||||
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",
|
||||
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",
|
||||
ctaHref: "/register"
|
||||
ctaHref: "/register",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
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",
|
||||
pricePerMonth: 299,
|
||||
maxProducts: 1200,
|
||||
@@ -219,29 +341,69 @@ export const PRICING_PLANS: PricingPlan[] = [
|
||||
extraCreditCost: 0.009,
|
||||
popular: true,
|
||||
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)",
|
||||
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",
|
||||
ctaHref: "/register"
|
||||
ctaHref: "/register",
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
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",
|
||||
pricePerMonth: 499,
|
||||
maxProducts: 4000,
|
||||
@@ -249,36 +411,64 @@ export const PRICING_PLANS: PricingPlan[] = [
|
||||
monthlyCredits: 4000,
|
||||
extraCreditCost: 0.009,
|
||||
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)",
|
||||
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",
|
||||
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",
|
||||
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",
|
||||
ctaHref: "/register"
|
||||
ctaHref: "/register",
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
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",
|
||||
pricePerMonth: 999,
|
||||
maxProducts: 12_000,
|
||||
@@ -286,31 +476,59 @@ export const PRICING_PLANS: PricingPlan[] = [
|
||||
monthlyCredits: 12000,
|
||||
extraCreditCost: 0.009,
|
||||
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)",
|
||||
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",
|
||||
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",
|
||||
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",
|
||||
ctaHref: "/register"
|
||||
ctaHref: "/register",
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
@@ -324,28 +542,40 @@ export const PRICING_PLANS: PricingPlan[] = [
|
||||
extraCreditCost: 0,
|
||||
customPrice: true,
|
||||
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",
|
||||
nameKey: "pricing.feature.unlimitedAiOwnKey",
|
||||
included: true
|
||||
included: true,
|
||||
},
|
||||
{
|
||||
name: "SSO, dedicated account manager",
|
||||
nameKey: "pricing.feature.ssoWebhooksAm",
|
||||
included: true
|
||||
included: true,
|
||||
},
|
||||
{
|
||||
name: "Custom integrations",
|
||||
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",
|
||||
ctaHref: "/contact-sales?source=pricing"
|
||||
}
|
||||
ctaHref: "/contact-sales?source=pricing",
|
||||
},
|
||||
];
|
||||
|
||||
export const PRICING_CAPABILITIES = [
|
||||
@@ -357,19 +587,34 @@ export const PRICING_CAPABILITIES = [
|
||||
{ text: "Field mapping & validation", key: "pricing.cap.feeds.f2" },
|
||||
{ text: "Multi-supplier merge", key: "pricing.cap.feeds.f3" },
|
||||
{ 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",
|
||||
categoryKey: "pricing.cap.processing",
|
||||
features: [
|
||||
{ text: "Data cleanup & attribute fill (all plans)", key: "pricing.cap.processing.f1" },
|
||||
{ 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" }
|
||||
]
|
||||
{
|
||||
text: "Data cleanup & attribute fill (all plans)",
|
||||
key: "pricing.cap.processing.f1",
|
||||
},
|
||||
{
|
||||
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",
|
||||
@@ -379,74 +624,81 @@ export const PRICING_CAPABILITIES = [
|
||||
{ text: "WooCommerce / Shopify sync", key: "pricing.cap.export.f2" },
|
||||
{ text: "Full API", key: "pricing.cap.export.f3" },
|
||||
{ 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",
|
||||
categoryKey: "pricing.cap.limits",
|
||||
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: "Enterprise SLA options", key: "pricing.cap.limits.f4" }
|
||||
]
|
||||
}
|
||||
{ text: "Enterprise SLA options", key: "pricing.cap.limits.f4" },
|
||||
],
|
||||
},
|
||||
] as const;
|
||||
|
||||
export const PRICING_FAQ: PricingFaqItem[] = [
|
||||
{
|
||||
question: "What are AI credits?",
|
||||
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",
|
||||
answerKey: "pricing.faq.credits.a"
|
||||
answerKey: "pricing.faq.credits.a",
|
||||
},
|
||||
{
|
||||
question: "Can I buy extra AI credits?",
|
||||
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",
|
||||
answerKey: "pricing.faq.packs.a"
|
||||
answerKey: "pricing.faq.packs.a",
|
||||
},
|
||||
{
|
||||
question: "What happens if I hit my product or credit limit?",
|
||||
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+).",
|
||||
questionKey: "pricing.faq.limits.q",
|
||||
answerKey: "pricing.faq.limits.a"
|
||||
answerKey: "pricing.faq.limits.a",
|
||||
},
|
||||
{
|
||||
question: "Can I upgrade or downgrade?",
|
||||
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.",
|
||||
questionKey: "pricing.faq.change.q",
|
||||
answerKey: "pricing.faq.change.a"
|
||||
answerKey: "pricing.faq.change.a",
|
||||
},
|
||||
{
|
||||
question: "What does Free include?",
|
||||
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",
|
||||
answerKey: "pricing.faq.free.a"
|
||||
answerKey: "pricing.faq.free.a",
|
||||
},
|
||||
{
|
||||
question: "Why not pay per product like content-only tools?",
|
||||
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",
|
||||
answerKey: "pricing.faq.why.a"
|
||||
answerKey: "pricing.faq.why.a",
|
||||
},
|
||||
{
|
||||
question: "How does annual billing work?",
|
||||
answer:
|
||||
"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",
|
||||
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). */
|
||||
export const CONTACT_SALES_HREF = "/contact-sales";
|
||||
@@ -466,7 +718,10 @@ export type ApiPublicPlan = {
|
||||
};
|
||||
|
||||
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 {
|
||||
@@ -479,12 +734,15 @@ function isCreditsFeature(feature: PricingFeature): boolean {
|
||||
/** Overlay DB meters onto marketing shells (price/CTA/feature matrix stay marketing). */
|
||||
export function applyApiMetersToPricingPlan(
|
||||
marketing: PricingPlan,
|
||||
api: ApiPublicPlan | undefined
|
||||
api: ApiPublicPlan | undefined,
|
||||
): PricingPlan {
|
||||
if (!api) return marketing;
|
||||
const monthlyCredits =
|
||||
typeof api.monthly_credits === "number" ? api.monthly_credits : marketing.monthlyCredits;
|
||||
const cover = typeof api.ai_cover_percent === "number" ? api.ai_cover_percent : 0;
|
||||
typeof api.monthly_credits === "number"
|
||||
? 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 maxProducts = marketing.customPrice
|
||||
? marketing.maxProducts
|
||||
@@ -493,11 +751,16 @@ export function applyApiMetersToPricingPlan(
|
||||
: marketing.maxProducts;
|
||||
|
||||
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 {
|
||||
...f,
|
||||
name: `Up to ${maxProducts.toLocaleString()} SKUs`,
|
||||
nameKey: undefined
|
||||
nameKey: undefined,
|
||||
};
|
||||
}
|
||||
if (isCreditsFeature(f)) {
|
||||
@@ -505,7 +768,7 @@ export function applyApiMetersToPricingPlan(
|
||||
return {
|
||||
...f,
|
||||
name: "0 AI credits / month",
|
||||
nameKey: "pricing.feature.zeroCredits"
|
||||
nameKey: "pricing.feature.zeroCredits",
|
||||
};
|
||||
}
|
||||
if (marketing.customPrice) return f;
|
||||
@@ -513,7 +776,7 @@ export function applyApiMetersToPricingPlan(
|
||||
return {
|
||||
...f,
|
||||
name: `${monthlyCredits.toLocaleString()} AI credits / month${coverLabel}`,
|
||||
nameKey: undefined
|
||||
nameKey: undefined,
|
||||
};
|
||||
}
|
||||
return f;
|
||||
@@ -523,16 +786,20 @@ export function applyApiMetersToPricingPlan(
|
||||
...marketing,
|
||||
id: api.id > 0 ? api.id : marketing.id,
|
||||
description,
|
||||
maxProducts: typeof maxProducts === "number" ? maxProducts : marketing.maxProducts,
|
||||
maxProducts:
|
||||
typeof maxProducts === "number" ? maxProducts : marketing.maxProducts,
|
||||
monthlyCredits,
|
||||
features
|
||||
features,
|
||||
};
|
||||
}
|
||||
|
||||
export function mergePricingPlansFromApi(apiPlans: ApiPublicPlan[]): PricingPlan[] {
|
||||
export function mergePricingPlansFromApi(
|
||||
apiPlans: ApiPublicPlan[],
|
||||
): PricingPlan[] {
|
||||
return PRICING_PLANS.map((marketing) => {
|
||||
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);
|
||||
});
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
const featureKeys = ["home.cta.f1", "home.cta.f2", "home.cta.f3"] as const;
|
||||
</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="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">
|
||||
|
||||
@@ -27,33 +27,33 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<footer class={`w-full border-t border-border bg-background ${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="grid grid-cols-1 gap-12 md:grid-cols-2 lg:grid-cols-4">
|
||||
<div class="lg:col-span-2">
|
||||
<a href="/" class="mb-6 flex items-center space-x-3" aria-label={i18n.t("app.home")}>
|
||||
<div class="relative flex h-10 w-10 items-center justify-center">
|
||||
<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-12 sm:py-16 md:px-6 lg:py-20">
|
||||
<div class="grid grid-cols-1 gap-10 md:grid-cols-3 md:gap-12 lg:gap-20">
|
||||
<div class="md:col-span-2">
|
||||
<a href="/" class="mb-5 inline-flex items-center space-x-3" aria-label={i18n.t("app.home")}>
|
||||
<div class="relative flex h-11 w-11 items-center justify-center rounded-xl bg-background shadow-sm ring-1 ring-border/80">
|
||||
<img
|
||||
src="/descrybe_logo.png"
|
||||
alt=""
|
||||
width="40"
|
||||
height="40"
|
||||
class="h-auto w-auto"
|
||||
class="h-9 w-9"
|
||||
/>
|
||||
</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>
|
||||
|
||||
<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")}
|
||||
</p>
|
||||
|
||||
{#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}
|
||||
<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
|
||||
class="mt-0.5 h-5 w-5 text-text-muted"
|
||||
class="h-4 w-4 text-primary"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
@@ -68,7 +68,7 @@
|
||||
</svg>
|
||||
<a
|
||||
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}
|
||||
</a>
|
||||
@@ -79,18 +79,18 @@
|
||||
</div>
|
||||
|
||||
{#each FOOTER_DATA.columns as column}
|
||||
<div>
|
||||
<h2 class="mb-5 text-base font-semibold text-text">
|
||||
<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-4 text-sm font-semibold tracking-wide text-text uppercase">
|
||||
{columnTitleKeys[column.title]
|
||||
? i18n.t(columnTitleKeys[column.title])
|
||||
: column.title}
|
||||
</h2>
|
||||
<ul class="space-y-3">
|
||||
<ul class="grid grid-cols-2 gap-2 md:grid-cols-1">
|
||||
{#each column.links as link}
|
||||
<li>
|
||||
<a
|
||||
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)}
|
||||
</a>
|
||||
@@ -102,10 +102,10 @@
|
||||
</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="flex flex-col items-center justify-between md:flex-row">
|
||||
<div class="mb-4 text-sm text-text-muted md:mb-0">
|
||||
<div class="flex flex-col items-start justify-between gap-5 md:flex-row md:items-center">
|
||||
<div class="text-sm text-text-muted">
|
||||
{i18n.t("site.footer.rightsLine", {
|
||||
year: currentYear,
|
||||
name: i18n.t("app.name")
|
||||
@@ -113,18 +113,18 @@
|
||||
</div>
|
||||
|
||||
{#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}
|
||||
<a
|
||||
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)}
|
||||
</a>
|
||||
{/each}
|
||||
<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()}
|
||||
>
|
||||
{i18n.t("site.cookieSettings")}
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
aria-hidden="true"
|
||||
></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%);"
|
||||
aria-hidden="true"
|
||||
></div>
|
||||
@@ -46,9 +46,21 @@
|
||||
</a>
|
||||
<a
|
||||
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>
|
||||
</div>
|
||||
<div class="mt-10 flex flex-col items-center justify-center">
|
||||
|
||||
@@ -23,9 +23,9 @@
|
||||
});
|
||||
|
||||
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 === "/docs") return i18n.t("site.nav.apiDocs");
|
||||
return href;
|
||||
}
|
||||
|
||||
@@ -212,20 +212,24 @@
|
||||
</div>
|
||||
|
||||
{#if mobileMenuOpen}
|
||||
<div class="absolute top-full right-0 left-0 bg-background/95 shadow-md backdrop-blur-md lg:hidden">
|
||||
<div class="border-t border-border/80 px-4 pt-2 pb-6">
|
||||
<nav class="mt-2 mb-4 flex flex-col space-y-1" aria-label={i18n.t("site.nav.mobile")}>
|
||||
<div
|
||||
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"
|
||||
>
|
||||
<div class="p-3">
|
||||
<nav class="flex flex-col gap-1" aria-label={i18n.t("site.nav.mobile")}>
|
||||
{#each NAV_ITEMS as item}
|
||||
<a
|
||||
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)}
|
||||
>
|
||||
{navTitle(item.href)}
|
||||
</a>
|
||||
{/each}
|
||||
</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}
|
||||
<p class="truncate text-center text-sm font-medium text-text-muted">
|
||||
{accountLabel}
|
||||
@@ -242,7 +246,7 @@
|
||||
<a
|
||||
href="/login"
|
||||
class={buttonClasses(
|
||||
"ghost",
|
||||
"outline",
|
||||
"default",
|
||||
"box-border h-10 w-full min-h-10 text-sm font-medium"
|
||||
)}>{i18n.t("site.logIn")}</a
|
||||
|
||||
@@ -41,7 +41,7 @@ export {
|
||||
export const PRODUCT_EXPLANATION = {
|
||||
id: "product",
|
||||
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:
|
||||
"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: [
|
||||
@@ -56,12 +56,12 @@ export const PRODUCT_EXPLANATION = {
|
||||
export const PROCESS_STEPS: ProcessStep[] = [
|
||||
{
|
||||
number: 1,
|
||||
title: "Import Your Taxonomy",
|
||||
description: "Set up the categories and attributes your store already uses",
|
||||
title: "Set up your catalog",
|
||||
description: "Organize and enrich products for your store",
|
||||
features: [
|
||||
{ text: "Define required attributes for each category" },
|
||||
{ text: "Choose product details for each category" },
|
||||
{ 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"
|
||||
},
|
||||
@@ -104,7 +104,7 @@ export const PROCESS_STEPS: ProcessStep[] = [
|
||||
export const MAIN_BENEFIT = {
|
||||
title: "Faster path from feed to listing",
|
||||
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: [
|
||||
{
|
||||
label: "Manual Process",
|
||||
@@ -144,7 +144,7 @@ export const STANDARD_BENEFITS: Benefit[] = [
|
||||
icon: "chart",
|
||||
title: "Clearer listings, stronger conversion",
|
||||
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",
|
||||
@@ -162,7 +162,7 @@ export const STANDARD_BENEFITS: Benefit[] = [
|
||||
icon: "cost",
|
||||
title: "Less manual data entry",
|
||||
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",
|
||||
@@ -217,7 +217,7 @@ export const PRICING_ENABLED = true;
|
||||
export const FOOTER_DATA = {
|
||||
companyName: "Descrybe",
|
||||
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: [
|
||||
{
|
||||
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).
|
||||
*/
|
||||
export const NAV_ITEMS = [
|
||||
{ title: "Home", href: "/" },
|
||||
...(PRICING_ENABLED ? [{ title: "Pricing", href: "/pricing" }] : []),
|
||||
{ title: "API Docs", href: "/docs" }
|
||||
{ title: "How it works", href: "/#how-it-works" },
|
||||
{ title: "Benefits", href: "/#benefits" },
|
||||
...(PRICING_ENABLED ? [{ title: "Pricing", href: "/pricing" }] : [])
|
||||
] as Array<{ title: string; href: string }>;
|
||||
|
||||
+2430
-1227
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Reference in New Issue
Block a user