TikTok V6

This commit is contained in:
2026-07-13 18:51:52 +02:00
parent 5b4a924de9
commit 48e9e2992c
9 changed files with 747 additions and 41 deletions

View File

@@ -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) => (

View 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>
);
}

View File

@@ -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}