Adds apps/video, a standalone Remotion package (not part of the pnpm workspace, so it never reaches the web build or the Docker image). It renders six looping films, one per service, in German and English, as 4:5 for LinkedIn and 16:9 for YouTube and ads, together with the text to post and an overview page. Each film is one continuous shot: close on something abstract, the camera pulls back to show what it really is, the form changes, and the picture folds back into its opening. The slide deck code from the earlier attempts is still in the package and no longer used. Also adds the Google Ads concept, brings the relaunch concept in line with the site as built, and a project skill that describes how to make further films. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
312 lines
7.9 KiB
TypeScript
312 lines
7.9 KiB
TypeScript
import type { FC, ReactNode } from "react";
|
|
import { FONT } from "../brand/fonts";
|
|
import { useT } from "../content/CopyContext";
|
|
import { COLOR, DRAW } from "../brand/tokens";
|
|
|
|
/** Shared drawing parts for illustrations. Everything is SVG, drawn in stage coordinates. */
|
|
|
|
export type IconName =
|
|
| "cart"
|
|
| "invoice"
|
|
| "people"
|
|
| "mail"
|
|
| "check"
|
|
| "book"
|
|
| "form"
|
|
| "chat"
|
|
| "bell"
|
|
| "tasks";
|
|
|
|
const ICON_PATHS: Readonly<Record<IconName, ReactNode>> = {
|
|
cart: (
|
|
<>
|
|
<path d="M3 4h2l2.4 10.2a1 1 0 0 0 1 .8h8.9a1 1 0 0 0 1-.8L20 7H6.2" />
|
|
<circle cx="9.5" cy="19" r="1.3" />
|
|
<circle cx="17" cy="19" r="1.3" />
|
|
</>
|
|
),
|
|
invoice: (
|
|
<>
|
|
<path d="M7 3h7l4 4v14H7z" />
|
|
<path d="M14 3v4h4" />
|
|
<path d="M10 12h5" />
|
|
<path d="M10 16h5" />
|
|
</>
|
|
),
|
|
people: (
|
|
<>
|
|
<circle cx="12" cy="8" r="3.5" />
|
|
<path d="M5 20c0-3.6 3.1-6 7-6s7 2.4 7 6" />
|
|
</>
|
|
),
|
|
mail: (
|
|
<>
|
|
<rect x="3" y="6" width="18" height="13" rx="2" />
|
|
<path d="m3.5 7.5 8.5 6 8.5-6" />
|
|
</>
|
|
),
|
|
check: (
|
|
<>
|
|
<circle cx="12" cy="12" r="9" />
|
|
<path d="m8 12.5 3 3 5-6" />
|
|
</>
|
|
),
|
|
book: (
|
|
<>
|
|
<path d="M5 4h11a2 2 0 0 1 2 2v14H7a2 2 0 0 1-2-2z" />
|
|
<path d="M5 18a2 2 0 0 1 2-2h11" />
|
|
<path d="M9 8h6" />
|
|
</>
|
|
),
|
|
form: (
|
|
<>
|
|
<rect x="4" y="4" width="16" height="16" rx="2" />
|
|
<path d="M8 9h8" />
|
|
<path d="M8 13h8" />
|
|
<path d="M8 17h4" />
|
|
</>
|
|
),
|
|
chat: <path d="M4 5h16v11H10l-4 4v-4H4z" />,
|
|
bell: (
|
|
<>
|
|
<path d="M6 16v-5a6 6 0 0 1 12 0v5l1.5 2h-15z" />
|
|
<path d="M10 20a2 2 0 0 0 4 0" />
|
|
</>
|
|
),
|
|
tasks: (
|
|
<>
|
|
<rect x="4" y="4" width="16" height="16" rx="2" />
|
|
<path d="m8 12 3 3 5-6" />
|
|
</>
|
|
),
|
|
};
|
|
|
|
/** A line icon from a 24 x 24 grid, centred on (cx, cy). */
|
|
export const Icon: FC<{ name: IconName; cx: number; cy: number; size: number; color: string }> = ({
|
|
name,
|
|
cx,
|
|
cy,
|
|
size,
|
|
color,
|
|
}) => (
|
|
<g
|
|
transform={`translate(${cx - size / 2} ${cy - size / 2}) scale(${size / 24})`}
|
|
fill="none"
|
|
stroke={color}
|
|
strokeWidth={1.4}
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
>
|
|
{ICON_PATHS[name]}
|
|
</g>
|
|
);
|
|
|
|
export const TILE_SIZE = 150;
|
|
|
|
/** One tool: a dark rounded tile with an icon and a mono label below. */
|
|
export const Tile: FC<{
|
|
x: number;
|
|
y: number;
|
|
icon: IconName;
|
|
label: string;
|
|
/** Coloured outline, e.g. while someone works in this tool. */
|
|
ring?: string;
|
|
scale?: number;
|
|
opacity?: number;
|
|
}> = ({ x, y, icon, label, ring, scale = 1, opacity = 1 }) => {
|
|
const centre = TILE_SIZE / 2;
|
|
return (
|
|
<g transform={`translate(${x} ${y})`} opacity={opacity}>
|
|
<g transform={`translate(${centre} ${centre}) scale(${scale}) translate(${-centre} ${-centre})`}>
|
|
{ring && (
|
|
<rect
|
|
x={-10}
|
|
y={-10}
|
|
width={TILE_SIZE + 20}
|
|
height={TILE_SIZE + 20}
|
|
rx={42}
|
|
fill="none"
|
|
stroke={ring}
|
|
strokeWidth={4}
|
|
style={{ filter: `drop-shadow(0 0 12px ${ring})` }}
|
|
/>
|
|
)}
|
|
<rect width={TILE_SIZE} height={TILE_SIZE} rx={34} fill={DRAW.tint} stroke={DRAW.fg} strokeWidth={2.5} />
|
|
<Icon name={icon} cx={centre} cy={centre} size={78} color={DRAW.fg} />
|
|
</g>
|
|
<text
|
|
x={centre}
|
|
y={TILE_SIZE + 44}
|
|
textAnchor="middle"
|
|
fontFamily={FONT.mono}
|
|
fontWeight={500}
|
|
fontSize={19}
|
|
letterSpacing="0.16em"
|
|
fill={DRAW.label}
|
|
>
|
|
{useT()(label).toUpperCase()}
|
|
</text>
|
|
</g>
|
|
);
|
|
};
|
|
|
|
/** Round badge with a tick, centred on (cx, cy). */
|
|
export const DoneBadge: FC<{ cx: number; cy: number; r: number; scale?: number }> = ({ cx, cy, r, scale = 1 }) => (
|
|
<g transform={`translate(${cx} ${cy}) scale(${scale})`}>
|
|
<circle r={r} fill={COLOR.done} style={{ filter: `drop-shadow(0 0 ${r * 0.5}px ${COLOR.done})` }} />
|
|
<path
|
|
d={`M${-r * 0.42} ${r * 0.04} ${-r * 0.1} ${r * 0.36} ${r * 0.44} ${-r * 0.3}`}
|
|
fill="none"
|
|
stroke={DRAW.bg}
|
|
strokeWidth={r * 0.2}
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
/>
|
|
</g>
|
|
);
|
|
|
|
/** Round amber badge with one character: something needs attention. */
|
|
export const WarnBadge: FC<{ cx: number; cy: number; r: number; scale?: number; char?: string }> = ({
|
|
cx,
|
|
cy,
|
|
r,
|
|
scale = 1,
|
|
char = "!",
|
|
}) => (
|
|
<g transform={`translate(${cx} ${cy}) scale(${scale})`}>
|
|
<circle r={r} fill={DRAW.mark} style={{ filter: `drop-shadow(0 0 ${r * 0.5}px ${DRAW.mark})` }} />
|
|
<text y={r * 0.42} textAnchor="middle" fontFamily={FONT.sans} fontWeight={900} fontSize={r * 1.2} fill={DRAW.bg}>
|
|
{char}
|
|
</text>
|
|
</g>
|
|
);
|
|
|
|
/** Mono caption inside a drawing. */
|
|
export const Label: FC<{ x: number; y: number; text: string; anchor?: "start" | "middle" | "end"; color?: string; size?: number }> = ({
|
|
x,
|
|
y,
|
|
text,
|
|
anchor = "middle",
|
|
color = DRAW.label,
|
|
size = 19,
|
|
}) => (
|
|
<text
|
|
x={x}
|
|
y={y}
|
|
textAnchor={anchor}
|
|
fontFamily={FONT.mono}
|
|
fontWeight={500}
|
|
fontSize={size}
|
|
letterSpacing="0.16em"
|
|
fill={color}
|
|
>
|
|
{useT()(text).toUpperCase()}
|
|
</text>
|
|
);
|
|
|
|
/** Mouse pointer with its tip at (x, y): stands for a person doing the step by hand. */
|
|
export const Cursor: FC<{ x: number; y: number; scale?: number }> = ({ x, y, scale = 1.5 }) => (
|
|
<path
|
|
transform={`translate(${x} ${y}) scale(${scale})`}
|
|
d="M0 0 0 30 8 23 14 36 19 34 13 21 24 21Z"
|
|
fill={DRAW.fg}
|
|
stroke={DRAW.bg}
|
|
strokeWidth={2.5}
|
|
strokeLinejoin="round"
|
|
/>
|
|
);
|
|
|
|
/** A piece of data: dark pill with binary digits. */
|
|
export const DataChip: FC<{ x: number; y: number; width?: number; digits?: string; color?: string }> = ({
|
|
x,
|
|
y,
|
|
width = 150,
|
|
digits = "0110",
|
|
color = DRAW.fg,
|
|
}) => (
|
|
<g transform={`translate(${x} ${y})`}>
|
|
<rect width={width} height={44} rx={22} fill={color} />
|
|
<text
|
|
x={width / 2}
|
|
y={30}
|
|
textAnchor="middle"
|
|
fontFamily={FONT.mono}
|
|
fontWeight={700}
|
|
fontSize={22}
|
|
letterSpacing="0.2em"
|
|
fill={DRAW.bg}
|
|
>
|
|
{digits}
|
|
</text>
|
|
</g>
|
|
);
|
|
|
|
/** An app window: white card with a title bar. Children are drawn in its coordinates. */
|
|
export const AppWindow: FC<{
|
|
x: number;
|
|
y: number;
|
|
width: number;
|
|
height: number;
|
|
title: string;
|
|
children?: ReactNode;
|
|
}> = ({ x, y, width, height, title, children }) => (
|
|
<g transform={`translate(${x} ${y})`}>
|
|
<rect width={width} height={height} rx={30} fill={DRAW.glass} stroke={DRAW.fg} strokeWidth={2.5} />
|
|
<path d={`M0 64H${width}`} stroke={DRAW.rim} strokeWidth={2} />
|
|
{[30, 56, 82].map((cx) => (
|
|
<circle key={cx} cx={cx} cy={33} r={8} fill={DRAW.soft} />
|
|
))}
|
|
<text
|
|
x={width - 28}
|
|
y={41}
|
|
textAnchor="end"
|
|
fontFamily={FONT.mono}
|
|
fontWeight={500}
|
|
fontSize={19}
|
|
letterSpacing="0.16em"
|
|
fill={DRAW.label}
|
|
>
|
|
{useT()(title).toUpperCase()}
|
|
</text>
|
|
{children}
|
|
</g>
|
|
);
|
|
|
|
/** A box for one day of a week or a countdown, centred on cx. */
|
|
export const DayBox: FC<{
|
|
cx: number;
|
|
y: number;
|
|
width: number;
|
|
label: string;
|
|
filled: boolean;
|
|
scale?: number;
|
|
}> = ({ cx, y, width, label, filled, scale = 1 }) => (
|
|
<g transform={`translate(${cx} ${y + 55}) scale(${scale})`}>
|
|
<rect
|
|
x={-width / 2}
|
|
y={-55}
|
|
width={width}
|
|
height={110}
|
|
rx={26}
|
|
fill={filled ? DRAW.fg : DRAW.glass}
|
|
stroke={filled ? DRAW.fg : DRAW.rim}
|
|
strokeWidth={2.5}
|
|
/>
|
|
<Label x={0} y={8} text={label} color={filled ? DRAW.bg : DRAW.fg} size={24} />
|
|
</g>
|
|
);
|
|
|
|
/** A sheet of paper. Children are drawn in its coordinates. */
|
|
export const Page: FC<{ x: number; y: number; width: number; height: number; children?: ReactNode }> = ({
|
|
x,
|
|
y,
|
|
width,
|
|
height,
|
|
children,
|
|
}) => (
|
|
<g transform={`translate(${x} ${y})`}>
|
|
<rect width={width} height={height} rx={24} fill={DRAW.glass} stroke={DRAW.fg} strokeWidth={2.5} />
|
|
{children}
|
|
</g>
|
|
);
|