From a707a44c46c63fa12bd42ec2836237e3158279e1 Mon Sep 17 00:00:00 2001 From: NiclasDev63 Date: Thu, 27 Aug 2026 23:22:09 +0200 Subject: [PATCH] implemented new onboarding-offboarding landing page --- .../hero => hero-shell}/hero-glass-frame.tsx | 0 .../hero-open-source-chip.tsx | 2 +- .../components/hero-shell/hero-shell.tsx | 58 ++++ .../home-page-section/cta/calendly-embed.tsx | 8 +- .../components/home-page-section/cta/cta.tsx | 6 +- .../home-page-section/hero/hero.tsx | 64 ++--- .../knowledge-sources/artifacts-canvas.tsx | 123 +++++++++ .../knowledge-sources/captured-chip.tsx | 15 ++ .../knowledge-sources/card-canvas.tsx | 26 ++ .../knowledge-sources/interview-canvas.tsx | 73 +++++ .../knowledge-source-card.tsx | 57 ++++ .../knowledge-sources-section.tsx | 85 ++++++ .../knowledge-sources-tones.ts | 6 + .../knowledge-sources/lesson-canvas.tsx | 169 ++++++++++++ .../components/mock/mock-ground-fade.tsx | 21 ++ .../components/mock/mock-quiz-card.tsx | 63 +++++ .../components/mock/mock-theme.ts | 17 ++ .../components/mock/mock-window-bar.tsx | 14 + .../components/mock/mock-window.tsx | 20 ++ .../onboarding-flow/checklist-mock.tsx | 84 ++++++ .../onboarding-flow/course-mock.tsx | 56 ++++ .../onboarding-flow/flow-mock-bar.tsx | 32 +++ .../components/onboarding-flow/flow-well.tsx | 31 +++ .../onboarding-flow/onboarding-flow-blurb.tsx | 36 +++ .../onboarding-flow-section.tsx | 88 ++++++ .../onboarding-flow/onboarding-flow-tone.ts | 3 + .../onboarding-flow/progress-mock.tsx | 70 +++++ .../onboarding-flow/questions-mock.tsx | 102 +++++++ .../problem-numbers-section.tsx | 89 +++++++ .../problem-numbers/sliding-number.test.tsx | 67 +++++ .../problem-numbers/sliding-number.tsx | 125 +++++++++ .../i18n/onboarding-offboarding.i18n.de.json | 250 ++++++++++++++++++ .../i18n/onboarding-offboarding.i18n.en.json | 250 ++++++++++++++++++ .../i18n/onboarding-offboarding.types.ts | 141 ++++++++++ .../onboarding-offboarding-view.tsx | 79 ++++++ .../[lang]/onboarding-offboarding/page.tsx | 39 +++ .../src/app/[lang]/use-cases/[slug]/page.tsx | 22 +- .../use-cases/components/use-case-faq.tsx | 6 +- .../use-cases/components/use-case-hero.tsx | 34 ++- .../[lang]/use-cases/content/ai-enablement.ts | 2 - .../[lang]/use-cases/content/healthcare.ts | 2 - .../[lang]/use-cases/content/hospitality.ts | 2 - .../[lang]/use-cases/content/it-companies.ts | 2 - .../app/[lang]/use-cases/content/logistics.ts | 2 - .../app/[lang]/use-cases/content/retail.ts | 2 - .../content/trades-and-construction.ts | 2 - apps/web/src/app/[lang]/use-cases/types.ts | 1 - apps/web/src/app/sitemap.ts | 18 ++ apps/web/src/components/faq/faq-json-ld.tsx | 23 ++ apps/web/src/components/faq/faq-list.tsx | 2 +- apps/web/src/i18n/types.ts | 2 + apps/web/src/styles/marketing-motion.css | 21 ++ 52 files changed, 2402 insertions(+), 110 deletions(-) rename apps/web/src/app/[lang]/components/{home-page-section/hero => hero-shell}/hero-glass-frame.tsx (100%) rename apps/web/src/app/[lang]/components/{home-page-section/hero => hero-shell}/hero-open-source-chip.tsx (94%) create mode 100644 apps/web/src/app/[lang]/components/hero-shell/hero-shell.tsx create mode 100644 apps/web/src/app/[lang]/onboarding-offboarding/components/knowledge-sources/artifacts-canvas.tsx create mode 100644 apps/web/src/app/[lang]/onboarding-offboarding/components/knowledge-sources/captured-chip.tsx create mode 100644 apps/web/src/app/[lang]/onboarding-offboarding/components/knowledge-sources/card-canvas.tsx create mode 100644 apps/web/src/app/[lang]/onboarding-offboarding/components/knowledge-sources/interview-canvas.tsx create mode 100644 apps/web/src/app/[lang]/onboarding-offboarding/components/knowledge-sources/knowledge-source-card.tsx create mode 100644 apps/web/src/app/[lang]/onboarding-offboarding/components/knowledge-sources/knowledge-sources-section.tsx create mode 100644 apps/web/src/app/[lang]/onboarding-offboarding/components/knowledge-sources/knowledge-sources-tones.ts create mode 100644 apps/web/src/app/[lang]/onboarding-offboarding/components/knowledge-sources/lesson-canvas.tsx create mode 100644 apps/web/src/app/[lang]/onboarding-offboarding/components/mock/mock-ground-fade.tsx create mode 100644 apps/web/src/app/[lang]/onboarding-offboarding/components/mock/mock-quiz-card.tsx create mode 100644 apps/web/src/app/[lang]/onboarding-offboarding/components/mock/mock-theme.ts create mode 100644 apps/web/src/app/[lang]/onboarding-offboarding/components/mock/mock-window-bar.tsx create mode 100644 apps/web/src/app/[lang]/onboarding-offboarding/components/mock/mock-window.tsx create mode 100644 apps/web/src/app/[lang]/onboarding-offboarding/components/onboarding-flow/checklist-mock.tsx create mode 100644 apps/web/src/app/[lang]/onboarding-offboarding/components/onboarding-flow/course-mock.tsx create mode 100644 apps/web/src/app/[lang]/onboarding-offboarding/components/onboarding-flow/flow-mock-bar.tsx create mode 100644 apps/web/src/app/[lang]/onboarding-offboarding/components/onboarding-flow/flow-well.tsx create mode 100644 apps/web/src/app/[lang]/onboarding-offboarding/components/onboarding-flow/onboarding-flow-blurb.tsx create mode 100644 apps/web/src/app/[lang]/onboarding-offboarding/components/onboarding-flow/onboarding-flow-section.tsx create mode 100644 apps/web/src/app/[lang]/onboarding-offboarding/components/onboarding-flow/onboarding-flow-tone.ts create mode 100644 apps/web/src/app/[lang]/onboarding-offboarding/components/onboarding-flow/progress-mock.tsx create mode 100644 apps/web/src/app/[lang]/onboarding-offboarding/components/onboarding-flow/questions-mock.tsx create mode 100644 apps/web/src/app/[lang]/onboarding-offboarding/components/problem-numbers/problem-numbers-section.tsx create mode 100644 apps/web/src/app/[lang]/onboarding-offboarding/components/problem-numbers/sliding-number.test.tsx create mode 100644 apps/web/src/app/[lang]/onboarding-offboarding/components/problem-numbers/sliding-number.tsx create mode 100644 apps/web/src/app/[lang]/onboarding-offboarding/i18n/onboarding-offboarding.i18n.de.json create mode 100644 apps/web/src/app/[lang]/onboarding-offboarding/i18n/onboarding-offboarding.i18n.en.json create mode 100644 apps/web/src/app/[lang]/onboarding-offboarding/i18n/onboarding-offboarding.types.ts create mode 100644 apps/web/src/app/[lang]/onboarding-offboarding/onboarding-offboarding-view.tsx create mode 100644 apps/web/src/app/[lang]/onboarding-offboarding/page.tsx create mode 100644 apps/web/src/components/faq/faq-json-ld.tsx diff --git a/apps/web/src/app/[lang]/components/home-page-section/hero/hero-glass-frame.tsx b/apps/web/src/app/[lang]/components/hero-shell/hero-glass-frame.tsx similarity index 100% rename from apps/web/src/app/[lang]/components/home-page-section/hero/hero-glass-frame.tsx rename to apps/web/src/app/[lang]/components/hero-shell/hero-glass-frame.tsx diff --git a/apps/web/src/app/[lang]/components/home-page-section/hero/hero-open-source-chip.tsx b/apps/web/src/app/[lang]/components/hero-shell/hero-open-source-chip.tsx similarity index 94% rename from apps/web/src/app/[lang]/components/home-page-section/hero/hero-open-source-chip.tsx rename to apps/web/src/app/[lang]/components/hero-shell/hero-open-source-chip.tsx index 40990bc..5751496 100644 --- a/apps/web/src/app/[lang]/components/home-page-section/hero/hero-open-source-chip.tsx +++ b/apps/web/src/app/[lang]/components/hero-shell/hero-open-source-chip.tsx @@ -4,7 +4,7 @@ import { ArrowDown, Github } from "lucide-react"; import { PILLARS, tint } from "@/app/[lang]/components/marketing-tokens"; -import { type HeroDictionary } from "./i18n/hero.types"; +import { type HeroDictionary } from "../home-page-section/hero/i18n/hero.types"; export function HeroOpenSourceChip({ t }: { t: HeroDictionary["openSource"] }) { return ( diff --git a/apps/web/src/app/[lang]/components/hero-shell/hero-shell.tsx b/apps/web/src/app/[lang]/components/hero-shell/hero-shell.tsx new file mode 100644 index 0000000..57e2d6f --- /dev/null +++ b/apps/web/src/app/[lang]/components/hero-shell/hero-shell.tsx @@ -0,0 +1,58 @@ +import { type ReactNode } from "react"; + +import { type HeroDictionary } from "../home-page-section/hero/i18n/hero.types"; +import { HeroGlassFrame } from "./hero-glass-frame"; +import { HeroOpenSourceChip } from "./hero-open-source-chip"; + +const CONTENT_INSET = + "md:top-[calc(clamp(88px,11.5vh,126px)+14px)] md:right-[calc(clamp(18px,5vw,112px)+14px)] md:bottom-[calc(clamp(28px,5.5vh,76px)+14px)] md:left-[calc(clamp(18px,5vw,112px)+14px)]"; + +// German compounds need the wider `md` measure to keep the headline off a fourth line. +const TITLE_CLASS = + "m-0 max-w-[19ch] text-[clamp(32px,min(6.6vw,9vh),68px)] leading-[1.1] font-medium tracking-[-0.026em] text-balance text-[#131c46] [text-shadow:0_2px_24px_rgba(255,255,255,0.6)] md:max-w-[21ch]"; + +const SUBTITLE_CLASS = + "mt-[clamp(16px,2.8vh,26px)] max-w-[440px] text-[clamp(15px,4vw,16.5px)] leading-[1.5] text-pretty text-[#2b3a58] [text-shadow:0_2px_14px_rgba(255,255,255,0.65)]"; + +export function HeroShell({ + alt, + openSource, + title, + subtitle, + children, +}: { + alt: string; + openSource: HeroDictionary["openSource"]; + title: ReactNode; + subtitle: ReactNode; + children?: ReactNode; +}) { + return ( +
+
+
+ + + +
+
+ +
+ +

{title}

+ +

{subtitle}

+ + {children} +
+
+ ); +} diff --git a/apps/web/src/app/[lang]/components/home-page-section/cta/calendly-embed.tsx b/apps/web/src/app/[lang]/components/home-page-section/cta/calendly-embed.tsx index 4dcfe12..49f3721 100644 --- a/apps/web/src/app/[lang]/components/home-page-section/cta/calendly-embed.tsx +++ b/apps/web/src/app/[lang]/components/home-page-section/cta/calendly-embed.tsx @@ -5,7 +5,7 @@ import { useCallback, useState } from "react"; const CALENDLY_CSS = "https://assets.calendly.com/assets/external/widget.css"; const CALENDLY_JS = "https://assets.calendly.com/assets/external/widget.js"; -const CALENDLY_DEMO_URL = "https://calendly.com/felix-kuennecke/scibly-demo"; +const DEFAULT_DEMO_URL = "https://calendly.com/felix-kuennecke/scibly-demo"; declare global { interface Window { @@ -24,13 +24,13 @@ function ensureCalendlyStylesheet() { document.head.appendChild(link); } -export function useCalendlyPopup() { +export function useCalendlyPopup(url: string = DEFAULT_DEMO_URL) { const [scriptReady, setScriptReady] = useState(false); const openCalendly = useCallback(() => { if (!window.Calendly) return; - window.Calendly.initPopupWidget({ url: CALENDLY_DEMO_URL }); - }, []); + window.Calendly.initPopupWidget({ url }); + }, [url]); // Stylesheet is injected when the lazyOnload script arrives: after the // landing page's own bytes, but before any human can click — injecting at diff --git a/apps/web/src/app/[lang]/components/home-page-section/cta/cta.tsx b/apps/web/src/app/[lang]/components/home-page-section/cta/cta.tsx index b9f6a40..88a6563 100644 --- a/apps/web/src/app/[lang]/components/home-page-section/cta/cta.tsx +++ b/apps/web/src/app/[lang]/components/home-page-section/cta/cta.tsx @@ -21,6 +21,7 @@ import { type CtaDictionary } from "./i18n/cta.types"; interface CtaSectionProps { t: CtaDictionary; + calendlyUrl?: string; } const FLOATING_CHAPTERS: Array<{ @@ -54,10 +55,11 @@ const FLOATING_CHAPTERS: Array<{ }, ]; -export function CtaSection({ t }: CtaSectionProps) { +export function CtaSection({ t, calendlyUrl }: CtaSectionProps) { const params = useParams(); const locale = typeof params.lang === "string" ? params.lang : "de"; - const { openCalendly, scriptReady, calendlyScript } = useCalendlyPopup(); + const { openCalendly, scriptReady, calendlyScript } = + useCalendlyPopup(calendlyUrl); useEffect(() => { if (window.location.hash !== "#cta-section") return; diff --git a/apps/web/src/app/[lang]/components/home-page-section/hero/hero.tsx b/apps/web/src/app/[lang]/components/home-page-section/hero/hero.tsx index d3e65c3..3aa9483 100644 --- a/apps/web/src/app/[lang]/components/home-page-section/hero/hero.tsx +++ b/apps/web/src/app/[lang]/components/home-page-section/hero/hero.tsx @@ -1,7 +1,6 @@ import { type ReactNode } from "react"; -import { HeroGlassFrame } from "./hero-glass-frame"; -import { HeroOpenSourceChip } from "./hero-open-source-chip"; +import { HeroShell } from "../../hero-shell/hero-shell"; import { type HeroDictionary } from "./i18n/hero.types"; interface HeroProps { @@ -9,58 +8,27 @@ interface HeroProps { children?: ReactNode; } -/** - * Copy sits inside the frame's own padding, so it clears the bezel without a - * second set of hard-coded offsets. - */ -const CONTENT_INSET = - "md:top-[calc(clamp(88px,11.5vh,126px)+14px)] md:right-[calc(clamp(18px,5vw,112px)+14px)] md:bottom-[calc(clamp(28px,5.5vh,76px)+14px)] md:left-[calc(clamp(18px,5vw,112px)+14px)]"; - export function Hero({ t, children }: HeroProps) { return ( -
- {/* Faint blueprint grid, faded out top and bottom so it never meets an edge */} -
- {/* Contact shadow — grounds the frame on the page */} -
- - - -
-
- -
- - {/* Measured in characters so the line length holds at every size. German - compounds need the wider measure to keep the headline off a fourth - line; phones stay tighter so it never runs edge to edge. */} -

+ {t.title} - {/* The one word the sentence turns on, in the serif accent. */} {t.titleHighlight} {t.titleSuffix} -

- -

- {t.subtitle} -

- - {children !== undefined ? ( -
- {children} -
- ) : null} -
-
+ + } + subtitle={t.subtitle} + > + {children !== undefined ? ( +
+ {children} +
+ ) : null} + ); } diff --git a/apps/web/src/app/[lang]/onboarding-offboarding/components/knowledge-sources/artifacts-canvas.tsx b/apps/web/src/app/[lang]/onboarding-offboarding/components/knowledge-sources/artifacts-canvas.tsx new file mode 100644 index 0000000..4f0af94 --- /dev/null +++ b/apps/web/src/app/[lang]/onboarding-offboarding/components/knowledge-sources/artifacts-canvas.tsx @@ -0,0 +1,123 @@ +import { GitPullRequest, Ticket } from "lucide-react"; + +import { CORRECT_KEY } from "@/app/[lang]/components/marketing-tokens"; +import { BrandLogo } from "@/components/brand-logo"; + +import { type ArtifactsCopy } from "../../i18n/onboarding-offboarding.types"; +import { enter, FAINT, HAIRLINE, MUTED } from "../mock/mock-theme"; +import { MockWindow } from "../mock/mock-window"; +import { MockWindowBar } from "../mock/mock-window-bar"; +import { CapturedChip } from "./captured-chip"; +import { CardCanvas } from "./card-canvas"; +import { SLACK_TONE, WORK_TONE } from "./knowledge-sources-tones"; + +export function ArtifactsCanvas({ + t, + capturedLabel, +}: { + t: ArtifactsCopy; + capturedLabel: string; +}) { + return ( + +
+ + + + {t.slackChannel} + + {t.slackTime} + + + +
+ + {t.slackAuthor.charAt(0)} + +

+ {t.slackAuthor}{" "} + {t.slackText} +

+
+
+ + + + + {t.codeFile} + + {t.codeReview} + + + +
+
+ - + {t.codeRemoved} +
+
+ + + {t.codeAdded} +
+
+
+ + TK + +

+ {t.codeComment} +

+
+
+ + + + + {t.ticketId} + + {t.ticketStatus} + + + +
+ + {t.ticketTitle} + + + {t.ticketCause} + +
+
+
+
+ ); +} diff --git a/apps/web/src/app/[lang]/onboarding-offboarding/components/knowledge-sources/captured-chip.tsx b/apps/web/src/app/[lang]/onboarding-offboarding/components/knowledge-sources/captured-chip.tsx new file mode 100644 index 0000000..252993c --- /dev/null +++ b/apps/web/src/app/[lang]/onboarding-offboarding/components/knowledge-sources/captured-chip.tsx @@ -0,0 +1,15 @@ +import { Check } from "lucide-react"; + +import { type Pillar } from "@/app/[lang]/components/marketing-tokens"; + +export function CapturedChip({ tone, label }: { tone: Pillar; label: string }) { + return ( + + + {label} + + ); +} diff --git a/apps/web/src/app/[lang]/onboarding-offboarding/components/knowledge-sources/card-canvas.tsx b/apps/web/src/app/[lang]/onboarding-offboarding/components/knowledge-sources/card-canvas.tsx new file mode 100644 index 0000000..7b52326 --- /dev/null +++ b/apps/web/src/app/[lang]/onboarding-offboarding/components/knowledge-sources/card-canvas.tsx @@ -0,0 +1,26 @@ +import { type ReactNode } from "react"; + +import { type Pillar, tint } from "@/app/[lang]/components/marketing-tokens"; + +import { MockGroundFade } from "../mock/mock-ground-fade"; +import { dottedGround } from "../mock/mock-theme"; + +export function CardCanvas({ + tone, + children, +}: { + tone: Pillar; + children: ReactNode; +}) { + const ground = tint(tone.softColor, 42); + return ( +
+ {children} + +
+ ); +} diff --git a/apps/web/src/app/[lang]/onboarding-offboarding/components/knowledge-sources/interview-canvas.tsx b/apps/web/src/app/[lang]/onboarding-offboarding/components/knowledge-sources/interview-canvas.tsx new file mode 100644 index 0000000..27f0994 --- /dev/null +++ b/apps/web/src/app/[lang]/onboarding-offboarding/components/knowledge-sources/interview-canvas.tsx @@ -0,0 +1,73 @@ +import { Mic, X } from "lucide-react"; + +import { PRODUCT_INK } from "@/app/[lang]/components/marketing-tokens"; +import { SciblyMark } from "@/components/brand-logo"; + +import { type InterviewCopy } from "../../i18n/onboarding-offboarding.types"; +import { enter } from "../mock/mock-theme"; +import { INTERVIEW_TONE } from "./knowledge-sources-tones"; + +const WAVEFORM_HEIGHTS = [5, 10, 14, 8, 12, 6, 9]; + +export function InterviewCanvas({ t }: { t: InterviewCopy }) { + return ( +
+ + + {t.timer} + + +
+ + + + +
+ +
+ {t.aiName} + + {t.listeningLabel} + +
+ +
+ {WAVEFORM_HEIGHTS.map((height, index) => ( + + ))} +
+ +

+ {t.question} +

+ +
+ + + + + + +
+
+ ); +} diff --git a/apps/web/src/app/[lang]/onboarding-offboarding/components/knowledge-sources/knowledge-source-card.tsx b/apps/web/src/app/[lang]/onboarding-offboarding/components/knowledge-sources/knowledge-source-card.tsx new file mode 100644 index 0000000..602b4a7 --- /dev/null +++ b/apps/web/src/app/[lang]/onboarding-offboarding/components/knowledge-sources/knowledge-source-card.tsx @@ -0,0 +1,57 @@ +import { type ReactNode } from "react"; + +import { + keyStyle, + panelKey, + type Pillar, +} from "@/app/[lang]/components/marketing-tokens"; + +import { enter, MUTED } from "../mock/mock-theme"; + +export function KnowledgeSourceCard({ + tone, + icon, + title, + description, + canvas, + delayMs, +}: { + tone: Pillar; + icon: ReactNode; + title: string; + description: string; + canvas: ReactNode; + delayMs: number; +}) { + return ( +
+ {/* Only the illustration animates in, so the title and description stay readable if the observer never fires. */} +
+ {canvas} +
+
+
+ + {icon} + +

+ {title} +

+
+

+ {description} +

+
+
+ ); +} diff --git a/apps/web/src/app/[lang]/onboarding-offboarding/components/knowledge-sources/knowledge-sources-section.tsx b/apps/web/src/app/[lang]/onboarding-offboarding/components/knowledge-sources/knowledge-sources-section.tsx new file mode 100644 index 0000000..b90cbb9 --- /dev/null +++ b/apps/web/src/app/[lang]/onboarding-offboarding/components/knowledge-sources/knowledge-sources-section.tsx @@ -0,0 +1,85 @@ +"use client"; + +import { cn } from "@scibly/ui/utils"; +import { BookOpen, GitPullRequest, Mic } from "lucide-react"; + +import { + MarketingSection, + MarketingSectionHeader, +} from "@/app/[lang]/components/marketing-section-content"; +import { useInViewOnce } from "@/components/in-view-reveal"; + +import { type KnowledgeSourcesCopy } from "../../i18n/onboarding-offboarding.types"; +import { ArtifactsCanvas } from "./artifacts-canvas"; +import { InterviewCanvas } from "./interview-canvas"; +import { KnowledgeSourceCard } from "./knowledge-source-card"; +import { + INTERVIEW_TONE, + LESSON_TONE, + WORK_TONE, +} from "./knowledge-sources-tones"; +import { LessonCanvas } from "./lesson-canvas"; + +export function KnowledgeSourcesSection({ t }: { t: KnowledgeSourcesCopy }) { + const { ref, inView } = useInViewOnce(0.15); + + const cards = [ + { + key: "interview", + tone: INTERVIEW_TONE, + icon: , + title: t.interview.title, + description: t.interview.description, + canvas: , + }, + { + key: "artifacts", + tone: WORK_TONE, + icon: , + title: t.artifacts.title, + description: t.artifacts.description, + canvas: ( + + ), + }, + { + key: "lesson", + tone: LESSON_TONE, + icon: , + title: t.lesson.title, + description: t.lesson.description, + canvas: , + }, + ]; + + return ( + + + +
+ {cards.map((card, index) => ( + + ))} +
+
+ ); +} diff --git a/apps/web/src/app/[lang]/onboarding-offboarding/components/knowledge-sources/knowledge-sources-tones.ts b/apps/web/src/app/[lang]/onboarding-offboarding/components/knowledge-sources/knowledge-sources-tones.ts new file mode 100644 index 0000000..f5004c9 --- /dev/null +++ b/apps/web/src/app/[lang]/onboarding-offboarding/components/knowledge-sources/knowledge-sources-tones.ts @@ -0,0 +1,6 @@ +import { PILLARS } from "@/app/[lang]/components/marketing-tokens"; + +export const INTERVIEW_TONE = PILLARS.byoai; +export const SLACK_TONE = PILLARS.channels; +export const WORK_TONE = PILLARS.analytics; +export const LESSON_TONE = PILLARS.import; diff --git a/apps/web/src/app/[lang]/onboarding-offboarding/components/knowledge-sources/lesson-canvas.tsx b/apps/web/src/app/[lang]/onboarding-offboarding/components/knowledge-sources/lesson-canvas.tsx new file mode 100644 index 0000000..ce70585 --- /dev/null +++ b/apps/web/src/app/[lang]/onboarding-offboarding/components/knowledge-sources/lesson-canvas.tsx @@ -0,0 +1,169 @@ +import { + Database, + GitPullRequest, + Mic, + ShieldCheck, + Sparkles, + Ticket, +} from "lucide-react"; + +import { CORRECT_KEY, tint } from "@/app/[lang]/components/marketing-tokens"; +import { BrandLogo } from "@/components/brand-logo"; + +import { + type KnowledgeCopy, + type LessonCopy, +} from "../../i18n/onboarding-offboarding.types"; +import { MockQuizCard } from "../mock/mock-quiz-card"; +import { enter, FAINT, MUTED } from "../mock/mock-theme"; +import { MockWindow } from "../mock/mock-window"; +import { MockWindowBar } from "../mock/mock-window-bar"; +import { CardCanvas } from "./card-canvas"; +import { + INTERVIEW_TONE, + LESSON_TONE, + SLACK_TONE, + WORK_TONE, +} from "./knowledge-sources-tones"; + +export function LessonCanvas({ + knowledge, + t, +}: { + knowledge: KnowledgeCopy; + t: LessonCopy; +}) { + const facts = [ + { + text: knowledge.fact1, + sources: [ + { + key: "slack", + tone: SLACK_TONE, + icon: , + }, + { + key: "git", + tone: WORK_TONE, + icon: , + }, + ], + }, + { + text: knowledge.fact2, + sources: [ + { + key: "interview", + tone: INTERVIEW_TONE, + icon: , + }, + { + key: "ticket", + tone: WORK_TONE, + icon: , + }, + ], + }, + ]; + + return ( + + + + + {knowledge.eyebrow} + + + {knowledge.verifiedLabel} + + + +
+

+ {knowledge.title} +

+
    + {facts.map((fact, index) => ( +
  • + + + {fact.text}{" "} + + {fact.sources.map((source) => ( + + {source.icon} + + ))} + + +
  • + ))} +
+
+
+ + + + + {t.lessonEyebrow} + + + + {t.derivedLabel} + + + +
+

+ {t.lessonTitle} +

+ +
+
+
+ ); +} diff --git a/apps/web/src/app/[lang]/onboarding-offboarding/components/mock/mock-ground-fade.tsx b/apps/web/src/app/[lang]/onboarding-offboarding/components/mock/mock-ground-fade.tsx new file mode 100644 index 0000000..c949cc6 --- /dev/null +++ b/apps/web/src/app/[lang]/onboarding-offboarding/components/mock/mock-ground-fade.tsx @@ -0,0 +1,21 @@ +import { cn } from "@scibly/ui/utils"; + +export function MockGroundFade({ + ground, + className, +}: { + ground: string; + className?: string; +}) { + return ( + + ); +} diff --git a/apps/web/src/app/[lang]/onboarding-offboarding/components/mock/mock-quiz-card.tsx b/apps/web/src/app/[lang]/onboarding-offboarding/components/mock/mock-quiz-card.tsx new file mode 100644 index 0000000..9fc96dc --- /dev/null +++ b/apps/web/src/app/[lang]/onboarding-offboarding/components/mock/mock-quiz-card.tsx @@ -0,0 +1,63 @@ +import { cn } from "@scibly/ui/utils"; +import { Check } from "lucide-react"; +import { type ComponentPropsWithoutRef } from "react"; + +import { + CORRECT_KEY, + type Pillar, + tint, +} from "@/app/[lang]/components/marketing-tokens"; + +import { MUTED } from "./mock-theme"; + +interface MockQuizCardProps extends ComponentPropsWithoutRef<"div"> { + tone: Pillar; + question: string; + wrongOption: string; + rightOption: string; + truncateOptions?: boolean; +} + +export function MockQuizCard({ + tone, + question, + wrongOption, + rightOption, + truncateOptions = false, + className, + style, + ...rest +}: MockQuizCardProps) { + const optionLabel = cn("text-[11px]", truncateOptions && "min-w-0 truncate"); + + return ( +
+

{question}

+
+ + + {wrongOption} + +
+
+ + + + {rightOption} +
+
+ ); +} diff --git a/apps/web/src/app/[lang]/onboarding-offboarding/components/mock/mock-theme.ts b/apps/web/src/app/[lang]/onboarding-offboarding/components/mock/mock-theme.ts new file mode 100644 index 0000000..bd9bee0 --- /dev/null +++ b/apps/web/src/app/[lang]/onboarding-offboarding/components/mock/mock-theme.ts @@ -0,0 +1,17 @@ +import { type CSSProperties } from "react"; + +import { BENTO_EASE } from "@/app/[lang]/components/home-page-section/keyframes"; +import { type Pillar, tint } from "@/app/[lang]/components/marketing-tokens"; + +export const MUTED = "#3b4574"; +export const FAINT = "#8b93b8"; +export const HAIRLINE = "#e6e9f4"; + +export const enter = (name: string, delayMs: number): CSSProperties => ({ + animation: `${name} 700ms ${BENTO_EASE} ${delayMs}ms both`, +}); + +export const dottedGround = (tone: Pillar): CSSProperties => ({ + backgroundImage: `radial-gradient(${tint(tone.lipColor, 55)} 1px, transparent 1.2px)`, + backgroundSize: "14px 14px", +}); diff --git a/apps/web/src/app/[lang]/onboarding-offboarding/components/mock/mock-window-bar.tsx b/apps/web/src/app/[lang]/onboarding-offboarding/components/mock/mock-window-bar.tsx new file mode 100644 index 0000000..0cc78f6 --- /dev/null +++ b/apps/web/src/app/[lang]/onboarding-offboarding/components/mock/mock-window-bar.tsx @@ -0,0 +1,14 @@ +import { type ReactNode } from "react"; + +import { HAIRLINE, MUTED } from "./mock-theme"; + +export function MockWindowBar({ children }: { children: ReactNode }) { + return ( +
+ {children} +
+ ); +} diff --git a/apps/web/src/app/[lang]/onboarding-offboarding/components/mock/mock-window.tsx b/apps/web/src/app/[lang]/onboarding-offboarding/components/mock/mock-window.tsx new file mode 100644 index 0000000..e55c877 --- /dev/null +++ b/apps/web/src/app/[lang]/onboarding-offboarding/components/mock/mock-window.tsx @@ -0,0 +1,20 @@ +import { cn } from "@scibly/ui/utils"; +import { type ComponentPropsWithoutRef } from "react"; + +export function MockWindow({ + className, + children, + ...rest +}: ComponentPropsWithoutRef<"div">) { + return ( +
+ {children} +
+ ); +} diff --git a/apps/web/src/app/[lang]/onboarding-offboarding/components/onboarding-flow/checklist-mock.tsx b/apps/web/src/app/[lang]/onboarding-offboarding/components/onboarding-flow/checklist-mock.tsx new file mode 100644 index 0000000..3389a8e --- /dev/null +++ b/apps/web/src/app/[lang]/onboarding-offboarding/components/onboarding-flow/checklist-mock.tsx @@ -0,0 +1,84 @@ +import { Check, ListChecks } from "lucide-react"; + +import { CORRECT_KEY } from "@/app/[lang]/components/marketing-tokens"; + +import { type ChecklistCopy } from "../../i18n/onboarding-offboarding.types"; +import { enter, FAINT, HAIRLINE, MUTED } from "../mock/mock-theme"; +import { MockWindow } from "../mock/mock-window"; +import { FlowMockBar } from "./flow-mock-bar"; + +function numberStepsAcrossGroups(groups: ChecklistCopy["groups"]) { + let stepsBefore = 0; + return groups.map((group) => { + const items = group.items.map((item, index) => ({ + ...item, + step: stepsBefore + index, + })); + stepsBefore += group.items.length; + return { label: group.label, items }; + }); +} + +export function ChecklistMock({ t }: { t: ChecklistCopy }) { + const groups = numberStepsAcrossGroups(t.groups); + + return ( + + } + title={t.windowTitle} + chip={t.stepCount} + /> +
+ {groups.map((group) => ( +
+ + {group.label} + + {group.items.map((item) => ( +
+ + {item.done ? : null} + + + {item.label} + + + {item.meta} + +
+ ))} +
+ ))} +
+
+ ); +} diff --git a/apps/web/src/app/[lang]/onboarding-offboarding/components/onboarding-flow/course-mock.tsx b/apps/web/src/app/[lang]/onboarding-offboarding/components/onboarding-flow/course-mock.tsx new file mode 100644 index 0000000..93a745e --- /dev/null +++ b/apps/web/src/app/[lang]/onboarding-offboarding/components/onboarding-flow/course-mock.tsx @@ -0,0 +1,56 @@ +import { GraduationCap } from "lucide-react"; + +import { tint } from "@/app/[lang]/components/marketing-tokens"; + +import { type CourseCopy } from "../../i18n/onboarding-offboarding.types"; +import { MockQuizCard } from "../mock/mock-quiz-card"; +import { enter, MUTED } from "../mock/mock-theme"; +import { MockWindow } from "../mock/mock-window"; +import { FlowMockBar } from "./flow-mock-bar"; +import { FLOW_TONE } from "./onboarding-flow-tone"; + +export function CourseMock({ t }: { t: CourseCopy }) { + return ( + + } + title={t.lessonTitle} + chip={t.lessonEyebrow} + /> +
+
+ + {t.whyLabel} + +

+ {t.whyText} +

+
+ + +
+
+ ); +} diff --git a/apps/web/src/app/[lang]/onboarding-offboarding/components/onboarding-flow/flow-mock-bar.tsx b/apps/web/src/app/[lang]/onboarding-offboarding/components/onboarding-flow/flow-mock-bar.tsx new file mode 100644 index 0000000..7ce93cc --- /dev/null +++ b/apps/web/src/app/[lang]/onboarding-offboarding/components/onboarding-flow/flow-mock-bar.tsx @@ -0,0 +1,32 @@ +import { type ReactNode } from "react"; + +import { MockWindowBar } from "../mock/mock-window-bar"; +import { FLOW_TONE } from "./onboarding-flow-tone"; + +export function FlowMockBar({ + icon, + title, + chip, +}: { + icon: ReactNode; + title: string; + chip?: string; +}) { + return ( + + {icon} + {title} + {chip ? ( + + {chip} + + ) : null} + + ); +} diff --git a/apps/web/src/app/[lang]/onboarding-offboarding/components/onboarding-flow/flow-well.tsx b/apps/web/src/app/[lang]/onboarding-offboarding/components/onboarding-flow/flow-well.tsx new file mode 100644 index 0000000..620045c --- /dev/null +++ b/apps/web/src/app/[lang]/onboarding-offboarding/components/onboarding-flow/flow-well.tsx @@ -0,0 +1,31 @@ +import { cn } from "@scibly/ui/utils"; +import { type ReactNode } from "react"; + +import { tint, washStyle } from "@/app/[lang]/components/marketing-tokens"; + +import { MockGroundFade } from "../mock/mock-ground-fade"; +import { dottedGround } from "../mock/mock-theme"; +import { FLOW_TONE } from "./onboarding-flow-tone"; + +export function FlowWell({ + className, + children, +}: { + className?: string; + children: ReactNode; +}) { + const ground = tint(FLOW_TONE.softColor, 38); + return ( +
+ {children} + +
+ ); +} diff --git a/apps/web/src/app/[lang]/onboarding-offboarding/components/onboarding-flow/onboarding-flow-blurb.tsx b/apps/web/src/app/[lang]/onboarding-offboarding/components/onboarding-flow/onboarding-flow-blurb.tsx new file mode 100644 index 0000000..c703e29 --- /dev/null +++ b/apps/web/src/app/[lang]/onboarding-offboarding/components/onboarding-flow/onboarding-flow-blurb.tsx @@ -0,0 +1,36 @@ +import { chosenKey, keyStyle } from "@/app/[lang]/components/marketing-tokens"; + +import { HAIRLINE, MUTED } from "../mock/mock-theme"; +import { FLOW_TONE } from "./onboarding-flow-tone"; + +export function OnboardingFlowBlurb({ + index, + title, + description, +}: { + index: number; + title: string; + description: string; +}) { + return ( +
+
+ + {String(index).padStart(2, "0")} + +

+ {title} +

+
+

+ {description} +

+
+ ); +} diff --git a/apps/web/src/app/[lang]/onboarding-offboarding/components/onboarding-flow/onboarding-flow-section.tsx b/apps/web/src/app/[lang]/onboarding-offboarding/components/onboarding-flow/onboarding-flow-section.tsx new file mode 100644 index 0000000..f5db374 --- /dev/null +++ b/apps/web/src/app/[lang]/onboarding-offboarding/components/onboarding-flow/onboarding-flow-section.tsx @@ -0,0 +1,88 @@ +"use client"; + +import { cn } from "@scibly/ui/utils"; + +import { + MarketingSection, + MarketingSectionHeader, +} from "@/app/[lang]/components/marketing-section-content"; +import { useInViewOnce } from "@/components/in-view-reveal"; + +import { type OnboardingFlowCopy } from "../../i18n/onboarding-offboarding.types"; +import { ChecklistMock } from "./checklist-mock"; +import { CourseMock } from "./course-mock"; +import { FlowWell } from "./flow-well"; +import { OnboardingFlowBlurb } from "./onboarding-flow-blurb"; +import { ProgressMock } from "./progress-mock"; +import { QuestionsMock } from "./questions-mock"; + +export function OnboardingFlowSection({ t }: { t: OnboardingFlowCopy }) { + const { ref, inView } = useInViewOnce(0.12); + + return ( + + + +
+
+ +
+ +
+
+ +
+
+ +
+ + +
+
+ +
+ +
+ +
+
+ +
+
+ +
+ + +
+
+
+
+ ); +} diff --git a/apps/web/src/app/[lang]/onboarding-offboarding/components/onboarding-flow/onboarding-flow-tone.ts b/apps/web/src/app/[lang]/onboarding-offboarding/components/onboarding-flow/onboarding-flow-tone.ts new file mode 100644 index 0000000..b98c9e0 --- /dev/null +++ b/apps/web/src/app/[lang]/onboarding-offboarding/components/onboarding-flow/onboarding-flow-tone.ts @@ -0,0 +1,3 @@ +import { PILLARS } from "@/app/[lang]/components/marketing-tokens"; + +export const FLOW_TONE = PILLARS.import; diff --git a/apps/web/src/app/[lang]/onboarding-offboarding/components/onboarding-flow/progress-mock.tsx b/apps/web/src/app/[lang]/onboarding-offboarding/components/onboarding-flow/progress-mock.tsx new file mode 100644 index 0000000..8bff51e --- /dev/null +++ b/apps/web/src/app/[lang]/onboarding-offboarding/components/onboarding-flow/progress-mock.tsx @@ -0,0 +1,70 @@ +import { ChartNoAxesColumn } from "lucide-react"; + +import { CORRECT_KEY } from "@/app/[lang]/components/marketing-tokens"; + +import { type ProgressCopy } from "../../i18n/onboarding-offboarding.types"; +import { enter, HAIRLINE, MUTED } from "../mock/mock-theme"; +import { MockWindow } from "../mock/mock-window"; +import { FlowMockBar } from "./flow-mock-bar"; +import { FLOW_TONE } from "./onboarding-flow-tone"; + +export function ProgressMock({ t }: { t: ProgressCopy }) { + return ( + + } + title={t.windowTitle} + chip={t.assignedLabel} + /> +
    + {t.people.map((person, index) => { + const done = person.progress >= 100; + return ( +
  • + + {person.initials} + + +
    +
    + + {person.name} + + + {person.status} + +
    + + + +
    +
  • + ); + })} +
+
+ ); +} diff --git a/apps/web/src/app/[lang]/onboarding-offboarding/components/onboarding-flow/questions-mock.tsx b/apps/web/src/app/[lang]/onboarding-offboarding/components/onboarding-flow/questions-mock.tsx new file mode 100644 index 0000000..0a49bf8 --- /dev/null +++ b/apps/web/src/app/[lang]/onboarding-offboarding/components/onboarding-flow/questions-mock.tsx @@ -0,0 +1,102 @@ +import { MessageCircleQuestion, Sparkles } from "lucide-react"; + +import { + CORRECT_KEY, + NOTICE_KEY, + tint, +} from "@/app/[lang]/components/marketing-tokens"; +import { SciblyMark } from "@/components/brand-logo"; + +import { type QuestionsCopy } from "../../i18n/onboarding-offboarding.types"; +import { enter, HAIRLINE, MUTED } from "../mock/mock-theme"; +import { MockWindow } from "../mock/mock-window"; +import { FlowMockBar } from "./flow-mock-bar"; +import { FLOW_TONE } from "./onboarding-flow-tone"; + +export function QuestionsMock({ t }: { t: QuestionsCopy }) { + return ( + + } + title={t.windowTitle} + /> +
+
+ + {t.learnerInitials} + +

+ {t.learnerName}{" "} + {t.question} +

+
+ +
+ + + +

+ {t.aiName} {t.answer} +

+
+ +
+
+ + {t.feedbackLabel} + + + {t.feedbackText} + +
+ + + {t.improvedLabel} + +
+
+
+ ); +} diff --git a/apps/web/src/app/[lang]/onboarding-offboarding/components/problem-numbers/problem-numbers-section.tsx b/apps/web/src/app/[lang]/onboarding-offboarding/components/problem-numbers/problem-numbers-section.tsx new file mode 100644 index 0000000..6a9d373 --- /dev/null +++ b/apps/web/src/app/[lang]/onboarding-offboarding/components/problem-numbers/problem-numbers-section.tsx @@ -0,0 +1,89 @@ +"use client"; + +import { cn } from "@scibly/ui/utils"; +import { UserMinus } from "lucide-react"; + +import { + MarketingSection, + MarketingSectionHeader, +} from "@/app/[lang]/components/marketing-section-content"; +import { PILLARS, PRODUCT_INK } from "@/app/[lang]/components/marketing-tokens"; +import { useInViewOnce } from "@/components/in-view-reveal"; + +import { type ProblemNumbersCopy } from "../../i18n/onboarding-offboarding.types"; +import { MUTED } from "../mock/mock-theme"; +import { SlidingNumber } from "./sliding-number"; + +const TONE = PILLARS.byoai; + +export function ProblemNumbersSection({ t }: { t: ProblemNumbersCopy }) { + const { ref, inView } = useInViewOnce(0.25); + // The section is in view long before the stats are, so the counters get their own later threshold. + const { ref: statsRef, inView: statsInView } = + useInViewOnce(0.6, "0px 0px -12% 0px"); + + return ( + + + +
+
+ + + +
+

+ {t.problemLabel} +

+

+ {t.problemBody} +

+
+
+ +
+ {t.stats.map((stat) => ( +
+
+ +
+
+ {stat.label} +
+
+ ))} +
+
+
+ ); +} diff --git a/apps/web/src/app/[lang]/onboarding-offboarding/components/problem-numbers/sliding-number.test.tsx b/apps/web/src/app/[lang]/onboarding-offboarding/components/problem-numbers/sliding-number.test.tsx new file mode 100644 index 0000000..c061356 --- /dev/null +++ b/apps/web/src/app/[lang]/onboarding-offboarding/components/problem-numbers/sliding-number.test.tsx @@ -0,0 +1,67 @@ +import { render } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; + +import { SlidingNumber } from "./sliding-number"; + +function columns(container: HTMLElement) { + return container.querySelectorAll("span.inline-block.overflow-hidden"); +} + +describe("SlidingNumber", () => { + it("gives a two-digit number two columns and keeps the suffix as literal text", () => { + const { container } = render(); + + expect(columns(container)).toHaveLength(2); + expect(container.querySelector("span.whitespace-pre")?.textContent).toBe( + "%", + ); + }); + + it("gives a single digit one column", () => { + const { container } = render(); + + expect(columns(container)).toHaveLength(1); + }); + + it("keeps every digit of a grouped thousand", () => { + // German groups thousands with a dot, so "1.000" is four digits, not "1.0". + const { container } = render(); + + expect(columns(container)).toHaveLength(4); + expect(container.querySelector("span.whitespace-pre")?.textContent).toBe( + ".", + ); + }); + + it("keeps leading zeroes", () => { + const { container } = render(); + + expect(columns(container)).toHaveLength(3); + }); + + it("always exposes the untouched value to screen readers", () => { + const { container } = render(); + + expect(container.querySelector(".sr-only")?.textContent).toBe("1.000"); + }); + + it("offers the plain value as the no-script stand-in for the wheels", () => { + const { container } = render(); + + expect(container.querySelector(".sc-roll-plain")?.textContent).toBe("48%"); + }); + + it("reveals the wheels only once activated", () => { + const { container: idle } = render( + , + ); + expect(idle.querySelector(".sc-roll")?.className).not.toContain( + "sc-roll-ready", + ); + + const { container: running } = render(); + expect(running.querySelector(".sc-roll")?.className).toContain( + "sc-roll-ready", + ); + }); +}); diff --git a/apps/web/src/app/[lang]/onboarding-offboarding/components/problem-numbers/sliding-number.tsx b/apps/web/src/app/[lang]/onboarding-offboarding/components/problem-numbers/sliding-number.tsx new file mode 100644 index 0000000..e6b436d --- /dev/null +++ b/apps/web/src/app/[lang]/onboarding-offboarding/components/problem-numbers/sliding-number.tsx @@ -0,0 +1,125 @@ +"use client"; + +import { cn } from "@scibly/ui/utils"; +import { + motion, + type MotionValue, + useSpring, + useTransform, +} from "framer-motion"; +import { useEffect } from "react"; + +import { usePrefersReducedMotion } from "@/components/in-view-reveal"; + +const DIGITS = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; + +const SPRING = { stiffness: 150, damping: 26, mass: 0.7 }; + +function Glyph({ + column, + digit, +}: { + column: MotionValue; + digit: number; +}) { + const y = useTransform(column, (latest) => { + // Wrapped so a wheel never travels more than five steps: 9 -> 1 rolls forward two, not back eight. + const offset = (10 + digit - (latest % 10)) % 10; + return `${(offset > 5 ? offset - 10 : offset) * 100}%`; + }); + + return ( + + {digit} + + ); +} + +function Digit({ + shiftedValue, + active, +}: { + shiftedValue: number; + active: boolean; +}) { + const column = useSpring(0, SPRING); + + useEffect(() => { + if (active) column.set(shiftedValue); + }, [active, shiftedValue, column]); + + return ( + + {DIGITS.map((digit) => ( + + ))} + + ); +} + +function Rolling({ digits, active }: { digits: string; active: boolean }) { + const value = Number.parseInt(digits, 10); + + return ( + <> + {[...digits].map((_, index) => { + const place = 10 ** (digits.length - 1 - index); + return ( + + ); + })} + + ); +} + +export function SlidingNumber({ + value, + active, + className, +}: { + value: string; + active: boolean; + className?: string; +}) { + const reducedMotion = usePrefersReducedMotion(); + + return ( + + + {value} + + + + {reducedMotion + ? value + : value + .split(/(\d+)/) + .filter(Boolean) + .map((part, index) => + /^\d+$/.test(part) ? ( + + ) : ( + + {part} + + ), + )} + + + {value} + + ); +} diff --git a/apps/web/src/app/[lang]/onboarding-offboarding/i18n/onboarding-offboarding.i18n.de.json b/apps/web/src/app/[lang]/onboarding-offboarding/i18n/onboarding-offboarding.i18n.de.json new file mode 100644 index 0000000..da52c51 --- /dev/null +++ b/apps/web/src/app/[lang]/onboarding-offboarding/i18n/onboarding-offboarding.i18n.de.json @@ -0,0 +1,250 @@ +{ + "onboardingOffboarding": { + "meta": { + "title": "Onboarding & Offboarding Software | Scibly", + "description": "Onboarding-Kurs einmal erstellen, mit einem Klick jeder neuen Person zuweisen. Wissen von ausscheidenden Mitarbeitenden als Kurs festhalten, statt zu verlieren. Scibly für Onboarding & Offboarding.", + "keywords": [ + "Onboarding Software", + "Offboarding Software", + "Mitarbeiter Onboarding LMS", + "Wissenstransfer Software", + "Wissen sichern beim Austritt" + ] + }, + "hero": { + "headline": "Besserer Wissenstransfer", + "subheadline": "Bewahre das wertvolle Wissen deines Teams, indem du es mit Scibly erfasst, weitergibst und aufbewahrst.", + "ctaLabel": "Demo anfordern" + }, + "featureShowcaseHeader": { + "eyebrow": "Für den gesamten Mitarbeiterzyklus gebaut", + "title": "Eine Plattform, vom ersten bis zum letzten Tag.", + "subtitle": "Ein Ort, um Onboarding aufzubauen, austretendes Wissen festzuhalten und beides prüfungssicher zu dokumentieren." + }, + "comparisonRows": [ + { + "kind": "learner", + "label": "Onboarding", + "legacy": "Checkliste in Tabelle oder PDF", + "scibly": "Geführtes Onboarding mit Fortschritt und Rückfragen, das mit jedem Teammitglied besser wird." + }, + { + "kind": "import", + "label": "Wissenstransfer", + "legacy": "Wissen geht mit dem Mitarbeiter", + "scibly": "Wissen wird kontinuierlich gesichert" + } + ], + "knowledgeSources": { + "eyebrow": "Wissenstransfer mit Scibly", + "title": "Undokumentiertes Expertenwissen wird erfasst und für alle zugänglich und lernbar gemacht.", + "subtitle": "Das wertvollste Wissen steht in keinem Dokument. Es steckt in den Köpfen der Experten und geht mit ihnen. Scibly hält es fest, bevor es verloren geht.", + "capturedLabel": "Erfasst", + "interview": { + "title": "Interviews", + "description": "Dokumentationen halten fest, wie etwas funktioniert. Scibly fragt nach dem Rest: Ausnahmen, Faustregeln, Gründe hinter Entscheidungen.", + "timer": "14:32", + "aiName": "Scibly KI", + "listeningLabel": "Hört zu …", + "question": "Du hast Payment-Retries erwähnt. Was passiert bei einem Timeout?" + }, + "artifacts": { + "title": "Die tägliche Arbeit liefert den Beleg", + "description": "Scibly extrahiert Wissen aus Slack-Nachrichten, Code-Reviews, Tickets und Docs und speichert jede Erkenntnis mit ihrer Quelle.", + "slackChannel": "#it-support", + "slackTime": "09:41", + "slackAuthor": "Mia", + "slackText": "Nie doppelt retryen, das übernimmt das Gateway.", + "codeFile": "payments/client.ts", + "codeReview": "Review", + "codeRemoved": "await retry(book, 3)", + "codeAdded": "await book()", + "codeComment": "Das Gateway retryt schon serverseitig.", + "ticketId": "INFRA-482", + "ticketStatus": "Gelöst", + "ticketTitle": "Doppelte Buchung nach Timeout", + "ticketCause": "Ursache: Client-Retry nach Timeout" + }, + "lesson": { + "title": "Mehr als nur Wissensextraktion", + "description": "Das extrahierte Wissen wird nicht nur zentral gespeichert, sondern kann mit nur einem Knopfdruck automatisch in interaktive Lerneinheiten verwandelt werden.", + "lessonEyebrow": "Lektion 4 von 9", + "lessonTitle": "Payment-Retries richtig behandeln", + "quizQuestion": "Ein Request läuft in einen Timeout. Was jetzt?", + "quizOptionWrong": "Sofort neu buchen", + "quizOptionRight": "Buchungsstatus prüfen, nicht retryen", + "derivedLabel": "Daraus generiert" + }, + "knowledge": { + "eyebrow": "Wissensbasis", + "title": "Payment-Retries", + "fact1": "Retries übernimmt ausschließlich das Gateway.", + "fact2": "Ein Timeout heißt nicht abgebrochen.", + "verifiedLabel": "3 Quellen · verifiziert" + } + }, + "problemNumbers": { + "eyebrow": "Das Problem in Zahlen", + "title": "Verlässt ein Mitarbeiter das Unternehmen, geht sein Wissen mit ihm.", + "problemLabel": "Das Problem", + "problemBody": "Jeder Mitarbeiter, der geht, nimmt undokumentiertes Wissen mit: die Ausnahmen, die Faustregeln, die Gründe hinter Entscheidungen. Was bleibt, sind längere Einarbeitung, wiederholte Fehler und Fragen, die niemand mehr beantworten kann.", + "stats": [ + { + "value": "31 Mrd. $", + "label": "Kosten durch Wissensverlust pro Jahr" + }, + { + "value": "92 %", + "label": "der Unternehmen sichern das Wissen ausscheidender Mitarbeiter nicht systematisch" + }, + { + "value": "3–6 Monate", + "label": "bis neue Mitarbeiter auf dem Produktivitätsniveau ihrer Kollegen arbeiten" + }, + { + "value": "50–200 %", + "label": "eines Jahresgehalts kostet es, eine Stelle neu zu besetzen" + } + ] + }, + "onboardingFlow": { + "eyebrow": "Onboarding mit Scibly", + "title": "Der schnellste Weg zur Produktivität", + "subtitle": "Struktur, Fortschritt, Rückfragen und Kontext an einem Ort. Vom Zugang am ersten Tag bis zum Verständnis dafür, wie Dinge im Unternehmen funktionieren und warum sie so gehandhabt werden.", + "checklist": { + "title": "Strukturierte Onboarding-Liste", + "description": "Jeder Schritt steht fest, mit Ansprechpartner, Frist und allen Informationen, die dazugehören. Niemand muss raten, was als Nächstes kommt.", + "windowTitle": "Onboarding · Vertrieb", + "stepCount": "12 Schritte", + "groups": [ + { + "label": "Vor dem ersten Tag", + "items": [ + { + "label": "Laptop und Zugänge bestellt", + "meta": "IT", + "done": true + }, + { + "label": "Arbeitsplatz eingerichtet", + "meta": "Office", + "done": true + } + ] + }, + { + "label": "Tag 1", + "items": [ + { + "label": "Willkommen im Team", + "meta": "Mia", + "done": true + }, + { + "label": "VPN und SSO einrichten", + "meta": "IT · bis 17:00", + "done": false + } + ] + }, + { + "label": "Woche 1", + "items": [ + { + "label": "Kurs: Sicherheitsrichtlinie", + "meta": "8 Lektionen", + "done": false + } + ] + } + ] + }, + "progress": { + "title": "Fortschritt wird getrackt", + "description": "Team Leads und Admins haben jederzeit den Überblick über den Stand des Onboardings und erkennen direkt, wenn jemand Unterstützung benötigt.", + "windowTitle": "Team-Übersicht · Vertrieb", + "assignedLabel": "3 im Onboarding", + "people": [ + { + "name": "Lena M.", + "initials": "LM", + "status": "12 von 12 Schritten", + "progress": 100 + }, + { + "name": "Jonas B.", + "initials": "JB", + "status": "5 von 12 Schritten", + "progress": 42 + }, + { + "name": "Aylin K.", + "initials": "AK", + "status": "0 von 12 Schritten", + "progress": 0 + } + ] + }, + "questions": { + "title": "Rückfragen und Feedback", + "description": "Neue Mitarbeiter können direkt im Onboarding Flow Fragen zu einzelnen Schritten stellen oder festhalten, was noch unklar ist. So verbessert sich das Onboarding mit jedem neuen Teammitglied.", + "windowTitle": "Schritt 4 · VPN einrichten", + "learnerName": "Jonas", + "learnerInitials": "JB", + "question": "Wo finde ich den VPN-Zugang für macOS?", + "aiName": "Scibly KI", + "answer": "Im Self-Service-Portal unter Geräte. Die IT gibt den Antrag automatisch frei.", + "feedbackLabel": "Feedback", + "feedbackText": "Der Schritt sagt nicht, wo der Zugang liegt.", + "improvedLabel": "Schritt ergänzt" + }, + "course": { + "title": "Kurse statt Schritt-für-Schritt-Anleitung", + "description": "Scibly erklärt nicht nur, was zu tun ist, sondern warum. Neue Mitarbeiter verstehen die Werkzeuge und Entscheidungen des Unternehmens, statt eine Anleitung abzuarbeiten.", + "lessonEyebrow": "Lektion 2 von 8", + "lessonTitle": "Warum Angebote zuerst ins CRM gehen", + "whyLabel": "Warum", + "whyText": "Ohne CRM-Eintrag fehlt der Forecast, und das Team sieht nicht, welche Deals wirklich laufen.", + "quizQuestion": "Ein Kunde sagt am Telefon zu. Was zuerst?", + "quizOptionWrong": "Vertrag direkt rausschicken", + "quizOptionRight": "Deal im CRM anlegen, dann Vertrag" + } + }, + "cta": { + "eyebrow": "Bereit, Wissenstransfer neu zu denken?", + "title": "Bereit, Wissen besser zu teilen und neue Mitarbeiter schneller einzuarbeiten?", + "button": "Demo anfordern", + "hint": "Unverbindlich · Wir melden uns persönlich" + }, + "faq": { + "title": "Häufige Fragen", + "questions": [ + { + "id": "what-is-scibly", + "q": "Was ist Scibly?", + "a": "Scibly macht Wissen aus den Köpfen eurer Experten für das ganze Unternehmen zugänglich. Aus bestehendem Wissen entstehen automatisch aktuelle Dokumentationen und Kurse sowie rollenbasierte Onboarding-Programme. So wird Wissen besser verteilt und neue Mitarbeitende schneller eingearbeitet." + }, + { + "id": "how-does-scibly-work", + "q": "Wie funktioniert Scibly?", + "a": "Scibly führt geführte Interviews mit euren Fachleuten und erschließt Wissen aus Quellen wie Slack, Code Reviews, Tickets und Dokumenten. Daraus entsteht eine Wissensbasis mit Quellenangaben, aus der sich Kurse und rollenbasierte Onboardings erstellen lassen. Zusätzlich können neue Teammitglieder direkt im Onboarding Fragen stellen und Feedback geben, wodurch sich das Onboarding automatisiert kontinuierlich verbessert." + }, + { + "id": "data-security", + "q": "Wie sicher sind meine Daten?", + "a": "Scibly ist DSGVO-konform und nutzt ausschließlich in der EU gehostete Modelle. Deine Daten werden weder für das Training von KI-Modellen verwendet noch an Dritte weitergegeben und sind sowohl bei der Übertragung als auch bei der Speicherung verschlüsselt. Für maximale Kontrolle kannst du Scibly außerdem selbst hosten, denn Scibly ist Open Source." + }, + { + "id": "getting-started", + "q": "Wie schnell sind wir startklar?", + "a": "Nachdem Scibly mit bestehenden Tools wie Slack, GitHub oder Notion verbunden wurde, beginnt die Wissensextraktion automatisch. Aus dem gewonnenen Wissen lassen sich zusätzlich mit unserem KI-Agenten innerhalb weniger Minuten interaktive Kurse und Onboardings erstellen." + }, + { + "id": "open-source", + "q": "Ist Scibly Open Source?", + "a": "Scibly ist Open Core: Der Großteil des Codes steht unter AGPLv3 auf GitHub, nur einzelne Enterprise-Funktionen sind davon ausgenommen. Du kannst das Repository klonen, den Stack auf deiner eigenen Infrastruktur starten und Scibly selbst hosten." + } + ] + } + } +} diff --git a/apps/web/src/app/[lang]/onboarding-offboarding/i18n/onboarding-offboarding.i18n.en.json b/apps/web/src/app/[lang]/onboarding-offboarding/i18n/onboarding-offboarding.i18n.en.json new file mode 100644 index 0000000..aa38f67 --- /dev/null +++ b/apps/web/src/app/[lang]/onboarding-offboarding/i18n/onboarding-offboarding.i18n.en.json @@ -0,0 +1,250 @@ +{ + "onboardingOffboarding": { + "meta": { + "title": "Onboarding & Offboarding Software | Scibly", + "description": "Build onboarding once, assign it to every new hire in one click. Capture a departing expert's knowledge as a course, not a lost handover. Scibly for onboarding & offboarding.", + "keywords": [ + "onboarding software", + "offboarding software", + "employee onboarding LMS", + "knowledge transfer software", + "exit knowledge capture" + ] + }, + "hero": { + "headline": "Better Knowledge Transfer", + "subheadline": "Preserve your team’s most valuable knowledge by capturing, transferring, and retaining it with Scibly.", + "ctaLabel": "Request demo" + }, + "featureShowcaseHeader": { + "eyebrow": "Built for the full employee lifecycle", + "title": "The same platform, from first day to last.", + "subtitle": "One place to build onboarding, capture outgoing knowledge, and keep both audit-ready." + }, + "comparisonRows": [ + { + "kind": "learner", + "label": "Onboarding", + "legacy": "Checklist in a spreadsheet or PDF", + "scibly": "Guided onboarding with progress and questions that improves with every team member." + }, + { + "kind": "import", + "label": "Knowledge Transfer", + "legacy": "Knowledge leaves with the employee", + "scibly": "Knowledge is captured continuously" + } + ], + "knowledgeSources": { + "eyebrow": "Knowledge transfer with Scibly", + "title": "Undocumented expertise gets captured and made accessible and learnable for everyone.", + "subtitle": "The most valuable knowledge is written down nowhere. It sits in your experts' heads and leaves with them. Scibly captures it before it is lost.", + "capturedLabel": "Captured", + "interview": { + "title": "Interviews", + "description": "Documentation records how something works. Scibly asks about the rest: the exceptions, the rules of thumb, the reasons behind decisions.", + "timer": "14:32", + "aiName": "Scibly AI", + "listeningLabel": "Listening…", + "question": "You mentioned payment retries. What happens on a timeout?" + }, + "artifacts": { + "title": "Everyday work supplies the evidence", + "description": "Scibly extracts knowledge from Slack messages, code reviews, tickets, and docs, and stores every insight with its source.", + "slackChannel": "#it-support", + "slackTime": "09:41", + "slackAuthor": "Mia", + "slackText": "Never retry twice, the gateway already does.", + "codeFile": "payments/client.ts", + "codeReview": "Review", + "codeRemoved": "await retry(book, 3)", + "codeAdded": "await book()", + "codeComment": "The gateway already retries server-side.", + "ticketId": "INFRA-482", + "ticketStatus": "Resolved", + "ticketTitle": "Duplicate booking after timeout", + "ticketCause": "Cause: client retry after timeout" + }, + "lesson": { + "title": "More than knowledge extraction", + "description": "The extracted knowledge is not just stored centrally, it can be turned into interactive learning units automatically, at the push of a button.", + "lessonEyebrow": "Lesson 4 of 9", + "lessonTitle": "Handling payment retries", + "quizQuestion": "A request hits a timeout. Now what?", + "quizOptionWrong": "Book again right away", + "quizOptionRight": "Check the booking status, don't retry", + "derivedLabel": "Generated from it" + }, + "knowledge": { + "eyebrow": "Knowledge base", + "title": "Payment retries", + "fact1": "Retries are handled by the gateway only.", + "fact2": "A timeout doesn't mean cancelled.", + "verifiedLabel": "3 sources · verified" + } + }, + "problemNumbers": { + "eyebrow": "The problem in numbers", + "title": "When an employee leaves the company, their knowledge leaves with them.", + "problemLabel": "The problem", + "problemBody": "Every employee who leaves takes undocumented knowledge with them: the exceptions, the rules of thumb, the reasons behind decisions. What stays behind is longer ramp-up, repeated mistakes, and questions nobody can answer any more.", + "stats": [ + { + "value": "$31B", + "label": "annual cost of knowledge loss" + }, + { + "value": "92%", + "label": "of organizations do not consistently capture knowledge from departing employees" + }, + { + "value": "3–6 months", + "label": "until new hires reach the productivity of their peers" + }, + { + "value": "50–200%", + "label": "of annual salary is the cost of replacing a single employee" + } + ] + }, + "onboardingFlow": { + "eyebrow": "Onboarding with Scibly", + "title": "The fastest path to productivity", + "subtitle": "Structure, progress, questions and context in one place. From day-one access to understanding how things work in the company and why they are done that way.", + "checklist": { + "title": "A structured onboarding list", + "description": "Every step is defined, with an owner, a deadline and all the information that belongs to it. Nobody has to guess what comes next.", + "windowTitle": "Onboarding · Sales", + "stepCount": "12 steps", + "groups": [ + { + "label": "Before day one", + "items": [ + { + "label": "Laptop and accounts ordered", + "meta": "IT", + "done": true + }, + { + "label": "Desk set up", + "meta": "Office", + "done": true + } + ] + }, + { + "label": "Day 1", + "items": [ + { + "label": "Welcome to the team", + "meta": "Mia", + "done": true + }, + { + "label": "Set up VPN and SSO", + "meta": "IT · by 5pm", + "done": false + } + ] + }, + { + "label": "Week 1", + "items": [ + { + "label": "Course: security policy", + "meta": "8 lessons", + "done": false + } + ] + } + ] + }, + "progress": { + "title": "Progress is tracked", + "description": "Team leads and admins have a complete view of onboarding progress at any time, and see immediately when someone needs support.", + "windowTitle": "Team overview · Sales", + "assignedLabel": "3 onboarding", + "people": [ + { + "name": "Lena M.", + "initials": "LM", + "status": "12 of 12 steps", + "progress": 100 + }, + { + "name": "Jonas B.", + "initials": "JB", + "status": "5 of 12 steps", + "progress": 42 + }, + { + "name": "Aylin K.", + "initials": "AK", + "status": "0 of 12 steps", + "progress": 0 + } + ] + }, + "questions": { + "title": "Questions and feedback", + "description": "New hires can ask questions about individual steps right inside the onboarding flow, or note down what is still unclear. That way onboarding improves with every new team member.", + "windowTitle": "Step 4 · Set up VPN", + "learnerName": "Jonas", + "learnerInitials": "JB", + "question": "Where do I find the macOS VPN profile?", + "aiName": "Scibly AI", + "answer": "In the self-service portal under Devices. IT approves the request automatically.", + "feedbackLabel": "Feedback", + "feedbackText": "The step doesn't say where the profile lives.", + "improvedLabel": "Step updated" + }, + "course": { + "title": "Courses, not a step-by-step manual", + "description": "Scibly explains not just what to do but why. New hires understand the company's tools and decisions instead of working through a checklist blind.", + "lessonEyebrow": "Lesson 2 of 8", + "lessonTitle": "Why deals go into the CRM first", + "whyLabel": "Why", + "whyText": "Without a CRM entry the forecast is wrong, and the team cannot see which deals are actually live.", + "quizQuestion": "A customer says yes on the phone. What first?", + "quizOptionWrong": "Send the contract right away", + "quizOptionRight": "Create the deal in the CRM, then the contract" + } + }, + "cta": { + "eyebrow": "Ready to rethink knowledge transfer?", + "title": "Ready to share knowledge better and onboard new hires faster?", + "button": "Request demo", + "hint": "No commitment · We'll reach out personally" + }, + "faq": { + "title": "Frequently asked questions", + "questions": [ + { + "id": "what-is-scibly", + "q": "What is Scibly?", + "a": "Scibly makes the knowledge inside your experts' heads accessible to the entire company. Existing knowledge automatically becomes up-to-date documentation and courses as well as role-based onboarding programs. Knowledge spreads further and new hires get up to speed faster." + }, + { + "id": "how-does-scibly-work", + "q": "How does Scibly work?", + "a": "Scibly runs guided interviews with your experts and taps knowledge from sources like Slack, code reviews, tickets and documents. That builds a knowledge base with cited sources, from which courses and role-based onboardings are created. New team members can also ask questions and give feedback right inside the onboarding, so it keeps improving automatically." + }, + { + "id": "data-security", + "q": "How secure is my data?", + "a": "Scibly is GDPR compliant and uses only models hosted in the EU. Your data is never used to train AI models nor passed on to third parties, and it is encrypted both in transit and at rest. For maximum control you can also self-host Scibly, because Scibly is open source." + }, + { + "id": "getting-started", + "q": "How quickly can we get started?", + "a": "Once Scibly is connected to your existing tools like Slack, GitHub or Notion, knowledge extraction starts automatically. From the knowledge gathered, our AI agent additionally creates interactive courses and onboardings within minutes." + }, + { + "id": "open-source", + "q": "Is Scibly open source?", + "a": "Scibly is open core: most of the code is on GitHub under AGPLv3, with only a few enterprise features excluded. You can clone the repository, boot the stack on your own infrastructure and self-host Scibly." + } + ] + } + } +} diff --git a/apps/web/src/app/[lang]/onboarding-offboarding/i18n/onboarding-offboarding.types.ts b/apps/web/src/app/[lang]/onboarding-offboarding/i18n/onboarding-offboarding.types.ts new file mode 100644 index 0000000..19fdb0e --- /dev/null +++ b/apps/web/src/app/[lang]/onboarding-offboarding/i18n/onboarding-offboarding.types.ts @@ -0,0 +1,141 @@ +import type { ComparisonDictionary } from "@/app/[lang]/components/home-page-section/comparison/i18n/comparison.types"; +import type { CtaDictionary } from "@/app/[lang]/components/home-page-section/cta/i18n/cta.types"; +import type { FaqItem } from "@/components/faq/faq-list"; + +export type OnboardingOffboardingPage = { + meta: { title: string; description: string; keywords: string[] }; + hero: { + headline: string; + subheadline: string; + ctaLabel: string; + }; + featureShowcaseHeader: { + eyebrow: string; + title: string; + subtitle: string; + }; + comparisonRows: ComparisonDictionary["table"]["rows"]; + knowledgeSources: { + eyebrow: string; + title: string; + subtitle: string; + capturedLabel: string; + interview: { + title: string; + description: string; + timer: string; + aiName: string; + listeningLabel: string; + question: string; + }; + artifacts: { + title: string; + description: string; + slackChannel: string; + slackTime: string; + slackAuthor: string; + slackText: string; + codeFile: string; + codeReview: string; + codeRemoved: string; + codeAdded: string; + codeComment: string; + ticketId: string; + ticketStatus: string; + ticketTitle: string; + ticketCause: string; + }; + knowledge: { + eyebrow: string; + title: string; + fact1: string; + fact2: string; + verifiedLabel: string; + }; + lesson: { + title: string; + derivedLabel: string; + description: string; + lessonEyebrow: string; + lessonTitle: string; + quizQuestion: string; + quizOptionWrong: string; + quizOptionRight: string; + }; + }; + problemNumbers: { + eyebrow: string; + title: string; + problemLabel: string; + problemBody: string; + stats: { value: string; label: string }[]; + }; + onboardingFlow: { + eyebrow: string; + title: string; + subtitle: string; + checklist: { + title: string; + description: string; + windowTitle: string; + stepCount: string; + groups: { + label: string; + items: { label: string; meta: string; done: boolean }[]; + }[]; + }; + progress: { + title: string; + description: string; + windowTitle: string; + assignedLabel: string; + people: { + name: string; + initials: string; + status: string; + progress: number; + }[]; + }; + questions: { + title: string; + description: string; + windowTitle: string; + learnerName: string; + learnerInitials: string; + question: string; + aiName: string; + answer: string; + feedbackLabel: string; + feedbackText: string; + improvedLabel: string; + }; + course: { + title: string; + description: string; + lessonEyebrow: string; + lessonTitle: string; + whyLabel: string; + whyText: string; + quizQuestion: string; + quizOptionWrong: string; + quizOptionRight: string; + }; + }; + cta: CtaDictionary; + faq: { title: string; questions: FaqItem[] }; +}; + +export type KnowledgeSourcesCopy = + OnboardingOffboardingPage["knowledgeSources"]; +export type InterviewCopy = KnowledgeSourcesCopy["interview"]; +export type ArtifactsCopy = KnowledgeSourcesCopy["artifacts"]; +export type KnowledgeCopy = KnowledgeSourcesCopy["knowledge"]; +export type LessonCopy = KnowledgeSourcesCopy["lesson"]; + +export type ProblemNumbersCopy = OnboardingOffboardingPage["problemNumbers"]; + +export type OnboardingFlowCopy = OnboardingOffboardingPage["onboardingFlow"]; +export type ChecklistCopy = OnboardingFlowCopy["checklist"]; +export type ProgressCopy = OnboardingFlowCopy["progress"]; +export type QuestionsCopy = OnboardingFlowCopy["questions"]; +export type CourseCopy = OnboardingFlowCopy["course"]; diff --git a/apps/web/src/app/[lang]/onboarding-offboarding/onboarding-offboarding-view.tsx b/apps/web/src/app/[lang]/onboarding-offboarding/onboarding-offboarding-view.tsx new file mode 100644 index 0000000..ccd9861 --- /dev/null +++ b/apps/web/src/app/[lang]/onboarding-offboarding/onboarding-offboarding-view.tsx @@ -0,0 +1,79 @@ +import type { Locale } from "@scibly/i18n/constants"; + +import dynamic from "next/dynamic"; + +import { FaqJsonLd } from "@/components/faq/faq-json-ld"; +import { getFullDictionary } from "@/i18n/dictionaries"; + +import { ComparisonSection } from "../components/home-page-section/comparison/comparison"; +import { CtaSection } from "../components/home-page-section/cta/cta"; +import { HeroComplianceStrip } from "../components/home-page-section/hero/hero-compliance-strip"; +import { UseCaseFaq } from "../use-cases/components/use-case-faq"; +import { UseCaseHero } from "../use-cases/components/use-case-hero"; +import { KnowledgeSourcesSection } from "./components/knowledge-sources/knowledge-sources-section"; +import { OnboardingFlowSection } from "./components/onboarding-flow/onboarding-flow-section"; +import { ProblemNumbersSection } from "./components/problem-numbers/problem-numbers-section"; + +const FeatureShowcase = dynamic(() => + import("../components/home-page-section/feature-showcase").then( + (m) => m.FeatureShowcase, + ), +); +const OpenSourceSection = dynamic(() => + import("../components/home-page-section/open-source/open-source").then( + (m) => m.OpenSourceSection, + ), +); +const ComplianceSection = dynamic(() => + import("../components/home-page-section/compliance/compliance").then( + (m) => m.ComplianceSection, + ), +); + +export async function OnboardingOffboardingView({ lang }: { lang: Locale }) { + const dict = await getFullDictionary(lang); + const content = dict.onboardingOffboarding; + + return ( +
+ + + + + + + + + + + + +
+ ); +} diff --git a/apps/web/src/app/[lang]/onboarding-offboarding/page.tsx b/apps/web/src/app/[lang]/onboarding-offboarding/page.tsx new file mode 100644 index 0000000..cdb9bec --- /dev/null +++ b/apps/web/src/app/[lang]/onboarding-offboarding/page.tsx @@ -0,0 +1,39 @@ +import type { Locale } from "@scibly/i18n/constants"; +import type { Metadata } from "next"; + +import { constructMetadata } from "@scibly/lib"; + +import { getFullDictionary } from "@/i18n/dictionaries"; +import { buildLocaleAlternates } from "@/lib/metadata"; + +import { OnboardingOffboardingView } from "./onboarding-offboarding-view"; + +export async function generateMetadata(props: { + params: Promise<{ lang: Locale }>; +}): Promise { + const { lang } = await props.params; + const dict = await getFullDictionary(lang); + const content = dict.onboardingOffboarding; + const { canonicalUrl, languages } = buildLocaleAlternates( + "/onboarding-offboarding", + lang, + ); + + return { + ...constructMetadata({ + fullTitle: content.meta.title, + description: content.meta.description, + url: canonicalUrl, + keywords: content.meta.keywords, + locale: lang, + }), + alternates: { canonical: canonicalUrl, languages }, + }; +} + +export default async function OnboardingOffboardingPage(props: { + params: Promise<{ lang: Locale }>; +}) { + const { lang } = await props.params; + return ; +} diff --git a/apps/web/src/app/[lang]/use-cases/[slug]/page.tsx b/apps/web/src/app/[lang]/use-cases/[slug]/page.tsx index 2bd0eb7..db2b513 100644 --- a/apps/web/src/app/[lang]/use-cases/[slug]/page.tsx +++ b/apps/web/src/app/[lang]/use-cases/[slug]/page.tsx @@ -6,6 +6,7 @@ import { constructMetadata } from "@scibly/lib"; import { routes } from "@scibly/routes"; import { notFound } from "next/navigation"; +import { FaqJsonLd } from "@/components/faq/faq-json-ld"; import { getFullDictionary } from "@/i18n/dictionaries"; import { CtaSection } from "../../components/home-page-section/cta/cta"; @@ -64,30 +65,15 @@ export default async function UseCasePage(props: { const content = useCase[lang]; const dict = await getFullDictionary(lang); - const faqJsonLd = { - "@context": "https://schema.org", - "@type": "FAQPage", - mainEntity: content.faq.questions.map((q) => ({ - "@type": "Question", - name: q.q, - acceptedAnswer: { - "@type": "Answer", - text: q.a, - }, - })), - }; - return (
-