feat: German as the main language, English under /en, with a language switch
The site now exists in German (at the root, with /impressum and /datenschutz) and English (/en). A request without a language follows the visitor's choice from the language switch, then the browser language, then German; the old English legal addresses move to /en for good. All visible texts are data in src/content/de.ts and en.ts, so components hold none of their own. The header carries a DE/EN switch whose bit curtain covers the page while it is swapped and reopens at the same reading position. Title, description, canonical, hreflang, Open Graph, the share image, the sitemap and the structured data are per language. The privacy notice now describes the cookie that remembers the chosen language. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -3,13 +3,9 @@
|
||||
import { BitAura } from "@/src/components/bits/BitAura";
|
||||
import * as React from "react";
|
||||
import { useNarrow } from "./useNarrow";
|
||||
import {
|
||||
KIND_LABEL,
|
||||
WORKFLOWS,
|
||||
type FlowNode,
|
||||
type NodeKind,
|
||||
type Workflow,
|
||||
} from "@/src/content/automation";
|
||||
import { useContent } from "@/src/components/ContentProvider";
|
||||
import { workflowsFor, type FlowNode } from "@/src/content/automation";
|
||||
import type { NodeKind } from "@/src/content/types";
|
||||
|
||||
/**
|
||||
* AutomationFlow: a workflow drawn as connected systems, with the work visibly
|
||||
@@ -95,6 +91,9 @@ const KIND_STYLE: Readonly<
|
||||
human: { stroke: "#f8fafc", dash: "2 4", tag: "#f8fafc" },
|
||||
};
|
||||
|
||||
/** The order of the legend below the drawing. */
|
||||
const KINDS: readonly NodeKind[] = ["trigger", "rule", "ai", "action", "human"];
|
||||
|
||||
type Clock = {
|
||||
/** Which run of the workflow is playing. */
|
||||
readonly run: number;
|
||||
@@ -107,7 +106,12 @@ type Clock = {
|
||||
const MONO = "font-mono text-[11px] uppercase tracking-[0.2em]";
|
||||
|
||||
export const AutomationFlow: React.FC = () => {
|
||||
const [flowId, setFlowId] = React.useState(WORKFLOWS[0]?.id ?? "");
|
||||
const { automation } = useContent();
|
||||
const workflows = React.useMemo(
|
||||
() => workflowsFor(automation.workflows),
|
||||
[automation.workflows],
|
||||
);
|
||||
const [flowId, setFlowId] = React.useState(workflows[0]?.id ?? "");
|
||||
const [clock, setClock] = React.useState<Clock>({ run: 0, step: 0, t: 0 });
|
||||
const ref = React.useRef<HTMLDivElement>(null);
|
||||
const [inView, setInView] = React.useState(false);
|
||||
@@ -116,7 +120,7 @@ export const AutomationFlow: React.FC = () => {
|
||||
const tall = useNarrow();
|
||||
const canvas = tall ? TALL : WIDE;
|
||||
|
||||
const flow = WORKFLOWS.find((item) => item.id === flowId) ?? WORKFLOWS[0];
|
||||
const flow = workflows.find((item) => item.id === flowId) ?? workflows[0];
|
||||
|
||||
React.useEffect(() => {
|
||||
const node = ref.current;
|
||||
@@ -195,10 +199,15 @@ export const AutomationFlow: React.FC = () => {
|
||||
}
|
||||
});
|
||||
|
||||
const logLines: string[] = [];
|
||||
const logLines: { text: string; kind: NodeKind }[] = [];
|
||||
const log = (node: Placed) => {
|
||||
if (logLines.every((line) => line.text !== node.log)) {
|
||||
logLines.push({ text: node.log, kind: node.kind });
|
||||
}
|
||||
};
|
||||
if (firstNode) {
|
||||
const first = nodeOf(firstNode);
|
||||
if (first) logLines.push(first.log);
|
||||
if (first) log(first);
|
||||
}
|
||||
steps.forEach((edges, index) => {
|
||||
const arrived =
|
||||
@@ -206,7 +215,7 @@ export const AutomationFlow: React.FC = () => {
|
||||
if (!arrived) return;
|
||||
edges.forEach(([, to]) => {
|
||||
const node = nodeOf(to);
|
||||
if (node && !logLines.includes(node.log)) logLines.push(node.log);
|
||||
if (node) log(node);
|
||||
});
|
||||
});
|
||||
const finished = clock.step >= steps.length;
|
||||
@@ -215,10 +224,10 @@ export const AutomationFlow: React.FC = () => {
|
||||
<div ref={ref}>
|
||||
<div
|
||||
role="tablist"
|
||||
aria-label="Example workflows"
|
||||
aria-label={automation.examplesLabel}
|
||||
className="flex flex-wrap gap-2"
|
||||
>
|
||||
{WORKFLOWS.map((item) => {
|
||||
{workflows.map((item) => {
|
||||
const selected = item.id === flow.id;
|
||||
return (
|
||||
<button
|
||||
@@ -251,7 +260,7 @@ export const AutomationFlow: React.FC = () => {
|
||||
<span
|
||||
className={`h-1.5 w-1.5 rounded-full ${finished ? "bg-emerald-400" : "bg-amber-300 animate-pulse"}`}
|
||||
/>
|
||||
{finished ? "run complete" : "running"}
|
||||
{finished ? automation.runComplete : automation.running}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -383,53 +392,51 @@ export const AutomationFlow: React.FC = () => {
|
||||
|
||||
<div className="grid grid-cols-1 border-t border-slate-800 md:grid-cols-12">
|
||||
<div className="border-b border-slate-800 px-5 py-5 md:col-span-7 md:border-b-0 md:border-r md:px-7">
|
||||
<p className={`${MONO} m-0 text-slate-500`}>Log</p>
|
||||
<p className={`${MONO} m-0 text-slate-500`}>{automation.log}</p>
|
||||
<ol
|
||||
className="m-0 mt-3 min-h-[7.5rem] list-none p-0 font-mono text-[12px] leading-relaxed text-slate-300 md:text-[13px]"
|
||||
aria-live="off"
|
||||
>
|
||||
{logLines.map((line, index) => (
|
||||
<li key={line} className="flex gap-3">
|
||||
<li key={line.text} className="flex gap-3">
|
||||
<span className="text-slate-600">
|
||||
{String(index + 1).padStart(2, "0")}
|
||||
</span>
|
||||
<span
|
||||
className={
|
||||
line.startsWith("unclear")
|
||||
? "text-amber-200"
|
||||
: undefined
|
||||
line.kind === "human" ? "text-amber-200" : undefined
|
||||
}
|
||||
>
|
||||
{line}
|
||||
{line.text}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</div>
|
||||
<ul className="m-0 flex list-none flex-col justify-center gap-2 px-5 py-5 md:col-span-5 md:px-7">
|
||||
{(Object.keys(KIND_LABEL) as NodeKind[])
|
||||
.filter((kind) => flow.nodes.some((node) => node.kind === kind))
|
||||
.map((kind) => (
|
||||
<li
|
||||
key={kind}
|
||||
className="flex items-center gap-3 font-mono text-[11px] text-slate-400"
|
||||
>
|
||||
<svg width="22" height="12" aria-hidden="true">
|
||||
<rect
|
||||
x="1"
|
||||
y="1"
|
||||
width="20"
|
||||
height="10"
|
||||
rx="3"
|
||||
fill="none"
|
||||
stroke={KIND_STYLE[kind].stroke}
|
||||
strokeWidth="1.5"
|
||||
strokeDasharray={KIND_STYLE[kind].dash}
|
||||
/>
|
||||
</svg>
|
||||
{KIND_LABEL[kind]}
|
||||
</li>
|
||||
))}
|
||||
{KINDS.filter((kind) =>
|
||||
flow.nodes.some((node) => node.kind === kind),
|
||||
).map((kind) => (
|
||||
<li
|
||||
key={kind}
|
||||
className="flex items-center gap-3 font-mono text-[11px] text-slate-400"
|
||||
>
|
||||
<svg width="22" height="12" aria-hidden="true">
|
||||
<rect
|
||||
x="1"
|
||||
y="1"
|
||||
width="20"
|
||||
height="10"
|
||||
rx="3"
|
||||
fill="none"
|
||||
stroke={KIND_STYLE[kind].stroke}
|
||||
strokeWidth="1.5"
|
||||
strokeDasharray={KIND_STYLE[kind].dash}
|
||||
/>
|
||||
</svg>
|
||||
{automation.kinds[kind]}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -437,13 +444,13 @@ export const AutomationFlow: React.FC = () => {
|
||||
|
||||
<div className="mt-6 grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||
<div className="rounded-3xl border border-slate-200 p-6 md:p-8">
|
||||
<p className={`${MONO} m-0 text-slate-400`}>By hand</p>
|
||||
<p className={`${MONO} m-0 text-slate-400`}>{automation.byHand}</p>
|
||||
<p className="m-0 mt-3 font-sans text-lg leading-relaxed text-slate-500 md:text-xl">
|
||||
{flow.byHand}
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-3xl border border-slate-900 p-6 md:p-8">
|
||||
<p className={`${MONO} m-0 text-slate-900`}>Automated</p>
|
||||
<p className={`${MONO} m-0 text-slate-900`}>{automation.automated}</p>
|
||||
<p className="m-0 mt-3 font-sans text-lg leading-relaxed text-slate-900 md:text-xl">
|
||||
{flow.automated}
|
||||
</p>
|
||||
|
||||
Reference in New Issue
Block a user