TikTok V6
This commit is contained in:
@@ -19,22 +19,41 @@ interface AnswerFirstBlockProps {
|
||||
howTo: {
|
||||
steps: string[]; // 3 Steps: "So funktioniert's"
|
||||
};
|
||||
labels?: {
|
||||
summaryTitle: string;
|
||||
whenToUseTitle: string;
|
||||
comparisonTitle: string;
|
||||
howItWorksTitle: string;
|
||||
supportedLabel: string;
|
||||
unavailableLabel: string;
|
||||
};
|
||||
className?: string; // Add className prop
|
||||
}
|
||||
|
||||
const defaultLabels = {
|
||||
summaryTitle: 'Quick Summary',
|
||||
whenToUseTitle: 'When to use this?',
|
||||
comparisonTitle: 'Comparison',
|
||||
howItWorksTitle: 'How it works',
|
||||
supportedLabel: 'Supported',
|
||||
unavailableLabel: 'No',
|
||||
};
|
||||
|
||||
export const AnswerFirstBlock: React.FC<AnswerFirstBlockProps> = ({
|
||||
whatIsIt,
|
||||
whenToUse,
|
||||
comparison,
|
||||
howTo,
|
||||
labels: customLabels,
|
||||
className,
|
||||
}) => {
|
||||
const labels = customLabels ?? defaultLabels;
|
||||
const leftValueFor = (item: ComparisonItem) => item.text ?? 'No';
|
||||
|
||||
return (
|
||||
<section className={`my-12 space-y-10 ${className || ''}`} aria-label="Quick answer">
|
||||
<div className="max-w-4xl space-y-4">
|
||||
<h2 className="text-2xl font-semibold text-slate-900 tracking-tight -tracking-[0.02em]">Quick Summary</h2>
|
||||
<h2 className="text-2xl font-semibold text-slate-900 tracking-tight -tracking-[0.02em]">{labels.summaryTitle}</h2>
|
||||
<p className="text-lg text-slate-600 leading-relaxed font-light">
|
||||
{whatIsIt}
|
||||
</p>
|
||||
@@ -45,7 +64,7 @@ export const AnswerFirstBlock: React.FC<AnswerFirstBlockProps> = ({
|
||||
<Card className="p-6 bg-white border-slate-200/60 shadow-sm rounded-xl flex flex-col h-full">
|
||||
<h3 className="font-semibold text-lg text-slate-900 mb-5 flex items-center gap-2">
|
||||
<HelpCircle className="w-5 h-5 text-slate-400 shrink-0" />
|
||||
When to use this?
|
||||
{labels.whenToUseTitle}
|
||||
</h3>
|
||||
<ul className="space-y-4 flex-1">
|
||||
{whenToUse.map((item, idx) => (
|
||||
@@ -61,7 +80,7 @@ export const AnswerFirstBlock: React.FC<AnswerFirstBlockProps> = ({
|
||||
<Card className="p-6 bg-white border-slate-200/60 shadow-sm rounded-xl flex flex-col h-full">
|
||||
<h3 className="font-semibold text-lg text-slate-900 mb-5 flex items-center gap-2">
|
||||
<ArrowRight className="w-5 h-5 text-slate-400 shrink-0 rotate-[-45deg]" />
|
||||
Comparison
|
||||
{labels.comparisonTitle}
|
||||
</h3>
|
||||
<div className="space-y-3.5 flex-1">
|
||||
{comparison.items.map((item, idx) => (
|
||||
@@ -82,12 +101,12 @@ export const AnswerFirstBlock: React.FC<AnswerFirstBlockProps> = ({
|
||||
{item.value ? (
|
||||
<>
|
||||
<Check className="w-3.5 h-3.5 text-emerald-600 shrink-0" aria-hidden="true" />
|
||||
<span className="text-indigo-650">Supported</span>
|
||||
<span className="text-indigo-650">{labels.supportedLabel}</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<X className="w-3.5 h-3.5 text-red-400 shrink-0" aria-hidden="true" />
|
||||
<span className="text-slate-500">No</span>
|
||||
<span className="text-slate-500">{labels.unavailableLabel}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
@@ -102,7 +121,7 @@ export const AnswerFirstBlock: React.FC<AnswerFirstBlockProps> = ({
|
||||
<Card className="p-6 bg-white border-slate-200/60 shadow-sm rounded-xl flex flex-col h-full">
|
||||
<h3 className="font-semibold text-lg text-slate-900 mb-5 flex items-center gap-2">
|
||||
<Play className="w-5 h-5 text-slate-400 shrink-0" />
|
||||
How it works
|
||||
{labels.howItWorksTitle}
|
||||
</h3>
|
||||
<ol className="space-y-6 flex-1">
|
||||
{howTo.steps.map((step, idx) => (
|
||||
|
||||
35
src/components/marketing/ScrollReveal.tsx
Normal file
35
src/components/marketing/ScrollReveal.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
'use client';
|
||||
|
||||
import type { ReactNode } from 'react';
|
||||
import { motion, useReducedMotion } from 'framer-motion';
|
||||
|
||||
type ScrollRevealProps = {
|
||||
children: ReactNode;
|
||||
delay?: number;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* A small, optional entrance animation for content that benefits from a
|
||||
* progressive reading order. Reduced-motion users receive the final state
|
||||
* immediately.
|
||||
*/
|
||||
export function ScrollReveal({
|
||||
children,
|
||||
delay = 0,
|
||||
className,
|
||||
}: ScrollRevealProps) {
|
||||
const prefersReducedMotion = useReducedMotion();
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={prefersReducedMotion ? false : { opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, amount: 0.01 }}
|
||||
transition={{ duration: 0.4, delay, ease: 'easeOut' }}
|
||||
className={className}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { FAQItem } from "@/lib/types";
|
||||
import type { Metadata } from "next";
|
||||
|
||||
import dynamic from "next/dynamic";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import {
|
||||
@@ -32,6 +33,10 @@ import { Card } from "@/components/ui/Card";
|
||||
import { HeroSpotlight } from "@/components/marketing/HeroSpotlight";
|
||||
import { breadcrumbSchema, faqPageSchema } from "@/lib/schema";
|
||||
|
||||
const ScrollReveal = dynamic(() =>
|
||||
import("@/components/marketing/ScrollReveal").then((module) => module.ScrollReveal),
|
||||
);
|
||||
|
||||
type LinkCard = {
|
||||
href: string;
|
||||
title: string;
|
||||
@@ -50,6 +55,14 @@ export type UseCaseTemplateLabels = {
|
||||
faqTitle?: string;
|
||||
nextStepEyebrow: string;
|
||||
ctaBannerTitle: string;
|
||||
answerFirst?: {
|
||||
summaryTitle: string;
|
||||
whenToUseTitle: string;
|
||||
comparisonTitle: string;
|
||||
howItWorksTitle: string;
|
||||
supportedLabel: string;
|
||||
unavailableLabel: string;
|
||||
};
|
||||
};
|
||||
|
||||
const defaultLabels: UseCaseTemplateLabels = {
|
||||
@@ -68,6 +81,14 @@ const defaultLabels: UseCaseTemplateLabels = {
|
||||
relatedResourcesTitle: "Related resources",
|
||||
nextStepEyebrow: "Next step",
|
||||
ctaBannerTitle: "Start with a QR setup that keeps your print permanent.",
|
||||
answerFirst: {
|
||||
summaryTitle: "Quick Summary",
|
||||
whenToUseTitle: "When to use this?",
|
||||
comparisonTitle: "Comparison",
|
||||
howItWorksTitle: "How it works",
|
||||
supportedLabel: "Supported",
|
||||
unavailableLabel: "No",
|
||||
},
|
||||
};
|
||||
|
||||
const subBenefitIcons = [ShieldCheck, RefreshCw, BarChart3, Zap];
|
||||
@@ -112,6 +133,7 @@ type UseCasePageTemplateProps = {
|
||||
authoritySignals?: string[];
|
||||
directAnswer?: string;
|
||||
schemaData?: Record<string, unknown>[];
|
||||
enableScrollReveal?: boolean;
|
||||
labels?: UseCaseTemplateLabels;
|
||||
};
|
||||
|
||||
@@ -433,6 +455,7 @@ export function UseCasePageTemplate({
|
||||
authoritySignals = [],
|
||||
directAnswer,
|
||||
schemaData = [],
|
||||
enableScrollReveal = false,
|
||||
labels = defaultLabels,
|
||||
}: UseCasePageTemplateProps) {
|
||||
return (
|
||||
@@ -566,19 +589,39 @@ export function UseCasePageTemplate({
|
||||
|
||||
{/* AnswerFirst block - summary and quick details */}
|
||||
<div className="container mx-auto max-w-7xl px-4 py-16 sm:px-6 lg:px-8">
|
||||
<AnswerFirstBlock
|
||||
whatIsIt={answer}
|
||||
whenToUse={whenToUse}
|
||||
comparison={{
|
||||
leftTitle: labels.comparisonLeftTitle,
|
||||
rightTitle: labels.comparisonRightTitle,
|
||||
items: comparisonItems,
|
||||
}}
|
||||
howTo={{
|
||||
steps: howToSteps,
|
||||
}}
|
||||
className="mt-0"
|
||||
/>
|
||||
{enableScrollReveal ? (
|
||||
<ScrollReveal>
|
||||
<AnswerFirstBlock
|
||||
whatIsIt={answer}
|
||||
whenToUse={whenToUse}
|
||||
comparison={{
|
||||
leftTitle: labels.comparisonLeftTitle,
|
||||
rightTitle: labels.comparisonRightTitle,
|
||||
items: comparisonItems,
|
||||
}}
|
||||
howTo={{
|
||||
steps: howToSteps,
|
||||
}}
|
||||
labels={labels.answerFirst}
|
||||
className="mt-0"
|
||||
/>
|
||||
</ScrollReveal>
|
||||
) : (
|
||||
<AnswerFirstBlock
|
||||
whatIsIt={answer}
|
||||
whenToUse={whenToUse}
|
||||
comparison={{
|
||||
leftTitle: labels.comparisonLeftTitle,
|
||||
rightTitle: labels.comparisonRightTitle,
|
||||
items: comparisonItems,
|
||||
}}
|
||||
howTo={{
|
||||
steps: howToSteps,
|
||||
}}
|
||||
labels={labels.answerFirst}
|
||||
className="mt-0"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{directAnswer ? (
|
||||
@@ -617,9 +660,8 @@ export function UseCasePageTemplate({
|
||||
<div className="grid gap-8 lg:grid-cols-3">
|
||||
{workflowCards.map((card, idx) => {
|
||||
const Icon = getWorkflowIcon(idx);
|
||||
return (
|
||||
const cardContent = (
|
||||
<Card
|
||||
key={card.title}
|
||||
className="rounded-xl border-slate-200 bg-white p-8 shadow-sm transition-all duration-250 hover:border-slate-300 hover:shadow-md"
|
||||
>
|
||||
<div className="mb-6 flex h-11 w-11 items-center justify-center rounded-lg bg-slate-50 border border-slate-100 text-slate-655">
|
||||
@@ -633,6 +675,14 @@ export function UseCasePageTemplate({
|
||||
</p>
|
||||
</Card>
|
||||
);
|
||||
|
||||
return enableScrollReveal ? (
|
||||
<ScrollReveal key={card.title} delay={idx * 0.08}>
|
||||
{cardContent}
|
||||
</ScrollReveal>
|
||||
) : (
|
||||
<div key={card.title}>{cardContent}</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
@@ -718,7 +768,7 @@ export function UseCasePageTemplate({
|
||||
<div className="container mx-auto max-w-5xl px-4 sm:px-6 lg:px-8">
|
||||
<div className="relative rounded-2xl bg-slate-50 border border-slate-200 p-8 sm:p-12 text-slate-900 shadow-sm overflow-hidden">
|
||||
<div className="relative z-10 flex flex-col gap-8 lg:flex-row lg:items-center lg:justify-between">
|
||||
<div className="max-w-2xl space-y-3">
|
||||
<div className="max-w-2xl min-w-0 flex-1 space-y-3">
|
||||
<div className="text-xs font-semibold uppercase tracking-wider text-slate-450">
|
||||
{labels.nextStepEyebrow}
|
||||
</div>
|
||||
@@ -730,7 +780,7 @@ export function UseCasePageTemplate({
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-3.5 sm:flex-row shrink-0">
|
||||
<div className="flex flex-col gap-3.5 shrink-0">
|
||||
<TrackedCtaLink
|
||||
href={primaryCta.href}
|
||||
ctaLabel={primaryCta.label}
|
||||
|
||||
Reference in New Issue
Block a user