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")}
</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">
<PlanCalculator {plans} />
</div>
{#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")}
File diff suppressed because it is too large Load Diff
@@ -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">
+23 -23
View File
@@ -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
+12 -12
View File
@@ -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 }>;
File diff suppressed because it is too large Load Diff