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:
@@ -1,71 +0,0 @@
|
||||
import type { CareerStation } from "@/src/components/illustrations/CareerLine";
|
||||
|
||||
/** The career in four stations. Facts from the owner's CV, deliberately summarised. */
|
||||
export const CAREER: readonly CareerStation[] = [
|
||||
{ years: "2011", role: "Started in the web", place: "Apprenticeship" },
|
||||
{
|
||||
years: "2014 – 2020",
|
||||
role: "Agencies",
|
||||
place: "Frontend developer",
|
||||
},
|
||||
{
|
||||
years: "2020 – 2023",
|
||||
role: "Product companies",
|
||||
place: "Senior software developer",
|
||||
},
|
||||
{
|
||||
years: "Today",
|
||||
role: "Independent",
|
||||
place: "Working directly with clients",
|
||||
},
|
||||
];
|
||||
|
||||
/** Which projects I am a good fit for, and which not. */
|
||||
export const FIT = [
|
||||
{
|
||||
title: "A good fit",
|
||||
items: [
|
||||
"Web apps: dashboards, portals, internal tools",
|
||||
"Interfaces where design and usability matter",
|
||||
"Company websites, also in several languages",
|
||||
"Web3 frontends: wallets, live data, dashboards",
|
||||
"Automation: connecting systems and their APIs",
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Also possible",
|
||||
items: [
|
||||
"The backend an app needs: APIs, login, integrations",
|
||||
"Command-line tools",
|
||||
"Taking over and tidying up existing code",
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Not my field",
|
||||
items: [
|
||||
"Mobile apps for the app stores",
|
||||
"Windows software",
|
||||
"Running servers, specialised database work",
|
||||
],
|
||||
},
|
||||
] as const;
|
||||
|
||||
/** Technologies I work with. Only what I have actually used in projects. */
|
||||
export const TECH = [
|
||||
{
|
||||
area: "Interface",
|
||||
items: ["TypeScript", "React", "Next.js", "Vue.js", "Tailwind CSS"],
|
||||
},
|
||||
{
|
||||
area: "Behind it",
|
||||
items: ["Node.js", "REST APIs", "PostgreSQL", "Headless CMS"],
|
||||
},
|
||||
{
|
||||
area: "Web3 and systems",
|
||||
items: ["Solidity", "Rust"],
|
||||
},
|
||||
{
|
||||
area: "Delivery",
|
||||
items: ["Automated tests", "Docker", "CI/CD"],
|
||||
},
|
||||
] as const;
|
||||
@@ -1,11 +1,12 @@
|
||||
import type { FlowNodeCopy, NodeKind, WorkflowCopy } from "./types";
|
||||
|
||||
/**
|
||||
* Example workflows for the automation section. They are illustrations of typical
|
||||
* cases, not descriptions of specific client projects.
|
||||
* cases, not descriptions of specific client projects. This file holds what the
|
||||
* drawing needs (nodes, positions, runs); the texts are in en.ts and de.ts.
|
||||
*/
|
||||
|
||||
export type NodeKind = "trigger" | "rule" | "ai" | "action" | "human";
|
||||
|
||||
export type FlowNode = {
|
||||
type NodeLayout = {
|
||||
readonly id: string;
|
||||
readonly kind: NodeKind;
|
||||
/** Position of the node centre on the wide 920 x 400 canvas. */
|
||||
@@ -14,38 +15,28 @@ export type FlowNode = {
|
||||
/** Position of the node centre on the tall 400 x 540 canvas for phones. */
|
||||
readonly nx: number;
|
||||
readonly ny: number;
|
||||
readonly system: string;
|
||||
readonly label: string;
|
||||
/** What appears in the log when a run reaches this node. */
|
||||
readonly log: string;
|
||||
};
|
||||
|
||||
export type FlowNode = NodeLayout & FlowNodeCopy;
|
||||
|
||||
/** One step of a run: the connections travelled at the same time. */
|
||||
export type FlowStep = readonly (readonly [from: string, to: string])[];
|
||||
|
||||
export type Workflow = {
|
||||
type WorkflowLayout = {
|
||||
readonly id: string;
|
||||
readonly name: string;
|
||||
readonly summary: string;
|
||||
readonly byHand: string;
|
||||
readonly automated: string;
|
||||
readonly nodes: readonly FlowNode[];
|
||||
readonly nodes: readonly NodeLayout[];
|
||||
/** Every connection that exists, for drawing. */
|
||||
readonly edges: readonly (readonly [from: string, to: string])[];
|
||||
/** The runs that play in turn. Each run is a list of steps. */
|
||||
readonly runs: readonly (readonly FlowStep[])[];
|
||||
};
|
||||
|
||||
export const WORKFLOWS: readonly Workflow[] = [
|
||||
export type Workflow = Omit<WorkflowLayout, "nodes"> &
|
||||
Omit<WorkflowCopy, "nodes"> & { readonly nodes: readonly FlowNode[] };
|
||||
|
||||
const LAYOUTS: readonly WorkflowLayout[] = [
|
||||
{
|
||||
id: "order",
|
||||
name: "Order to invoice",
|
||||
summary:
|
||||
"A new order arrives in the shop. The invoice, the customer record and the confirmation follow without anyone typing.",
|
||||
byHand:
|
||||
"Someone opens the order, writes the invoice, updates the customer record and sends an email.",
|
||||
automated:
|
||||
"The order triggers everything. People only see the finished result.",
|
||||
nodes: [
|
||||
{
|
||||
id: "shop",
|
||||
@@ -54,9 +45,6 @@ export const WORKFLOWS: readonly Workflow[] = [
|
||||
y: 200,
|
||||
nx: 200,
|
||||
ny: 60,
|
||||
system: "Shop",
|
||||
label: "New order",
|
||||
log: "order received",
|
||||
},
|
||||
{
|
||||
id: "stock",
|
||||
@@ -65,9 +53,6 @@ export const WORKFLOWS: readonly Workflow[] = [
|
||||
y: 200,
|
||||
nx: 200,
|
||||
ny: 190,
|
||||
system: "Rule",
|
||||
label: "In stock?",
|
||||
log: "stock checked: available",
|
||||
},
|
||||
{
|
||||
id: "invoice",
|
||||
@@ -76,9 +61,6 @@ export const WORKFLOWS: readonly Workflow[] = [
|
||||
y: 110,
|
||||
nx: 105,
|
||||
ny: 330,
|
||||
system: "Accounting",
|
||||
label: "Create invoice",
|
||||
log: "invoice created",
|
||||
},
|
||||
{
|
||||
id: "crm",
|
||||
@@ -87,9 +69,6 @@ export const WORKFLOWS: readonly Workflow[] = [
|
||||
y: 290,
|
||||
nx: 295,
|
||||
ny: 330,
|
||||
system: "CRM",
|
||||
label: "Update customer",
|
||||
log: "customer record updated",
|
||||
},
|
||||
{
|
||||
id: "mail",
|
||||
@@ -98,9 +77,6 @@ export const WORKFLOWS: readonly Workflow[] = [
|
||||
y: 200,
|
||||
nx: 200,
|
||||
ny: 470,
|
||||
system: "Email",
|
||||
label: "Send confirmation",
|
||||
log: "confirmation sent",
|
||||
},
|
||||
],
|
||||
edges: [
|
||||
@@ -126,13 +102,6 @@ export const WORKFLOWS: readonly Workflow[] = [
|
||||
},
|
||||
{
|
||||
id: "documents",
|
||||
name: "Incoming documents",
|
||||
summary:
|
||||
"Invoices arrive as PDF by email. An AI step reads them, a rule checks the result, and a person only sees the unclear ones.",
|
||||
byHand:
|
||||
"Someone opens every PDF, reads supplier, amount and date, and types them into the accounting system.",
|
||||
automated:
|
||||
"Clear cases are booked directly. Unclear ones go to a person with the document attached.",
|
||||
nodes: [
|
||||
{
|
||||
id: "inbox",
|
||||
@@ -141,9 +110,6 @@ export const WORKFLOWS: readonly Workflow[] = [
|
||||
y: 200,
|
||||
nx: 200,
|
||||
ny: 60,
|
||||
system: "Inbox",
|
||||
label: "Email with PDF",
|
||||
log: "document received",
|
||||
},
|
||||
{
|
||||
id: "read",
|
||||
@@ -152,9 +118,6 @@ export const WORKFLOWS: readonly Workflow[] = [
|
||||
y: 200,
|
||||
nx: 200,
|
||||
ny: 190,
|
||||
system: "AI step",
|
||||
label: "Read the document",
|
||||
log: "supplier, amount and date extracted",
|
||||
},
|
||||
{
|
||||
id: "check",
|
||||
@@ -163,9 +126,6 @@ export const WORKFLOWS: readonly Workflow[] = [
|
||||
y: 200,
|
||||
nx: 200,
|
||||
ny: 320,
|
||||
system: "Rule",
|
||||
label: "Plausible?",
|
||||
log: "values checked",
|
||||
},
|
||||
{
|
||||
id: "book",
|
||||
@@ -174,9 +134,6 @@ export const WORKFLOWS: readonly Workflow[] = [
|
||||
y: 110,
|
||||
nx: 105,
|
||||
ny: 460,
|
||||
system: "Accounting",
|
||||
label: "Book it",
|
||||
log: "booked automatically",
|
||||
},
|
||||
{
|
||||
id: "ask",
|
||||
@@ -185,9 +142,6 @@ export const WORKFLOWS: readonly Workflow[] = [
|
||||
y: 290,
|
||||
nx: 295,
|
||||
ny: 460,
|
||||
system: "A person",
|
||||
label: "Review and decide",
|
||||
log: "unclear: sent to a person for review",
|
||||
},
|
||||
],
|
||||
edges: [
|
||||
@@ -204,12 +158,6 @@ export const WORKFLOWS: readonly Workflow[] = [
|
||||
},
|
||||
{
|
||||
id: "lead",
|
||||
name: "Enquiry to follow-up",
|
||||
summary:
|
||||
"Someone fills in the contact form. The contact is created, the right person is told, and a reminder makes sure nobody forgets.",
|
||||
byHand:
|
||||
"Someone copies the enquiry into the CRM, forwards it by email and hopes the follow-up is not forgotten.",
|
||||
automated: "The enquiry is filed, assigned and followed up on schedule.",
|
||||
nodes: [
|
||||
{
|
||||
id: "form",
|
||||
@@ -218,9 +166,6 @@ export const WORKFLOWS: readonly Workflow[] = [
|
||||
y: 200,
|
||||
nx: 200,
|
||||
ny: 60,
|
||||
system: "Website",
|
||||
label: "Contact form",
|
||||
log: "enquiry received",
|
||||
},
|
||||
{
|
||||
id: "contact",
|
||||
@@ -229,9 +174,6 @@ export const WORKFLOWS: readonly Workflow[] = [
|
||||
y: 200,
|
||||
nx: 200,
|
||||
ny: 190,
|
||||
system: "CRM",
|
||||
label: "Create contact",
|
||||
log: "contact created",
|
||||
},
|
||||
{
|
||||
id: "notify",
|
||||
@@ -240,9 +182,6 @@ export const WORKFLOWS: readonly Workflow[] = [
|
||||
y: 110,
|
||||
nx: 105,
|
||||
ny: 330,
|
||||
system: "Team chat",
|
||||
label: "Notify sales",
|
||||
log: "sales notified",
|
||||
},
|
||||
{
|
||||
id: "wait",
|
||||
@@ -251,9 +190,6 @@ export const WORKFLOWS: readonly Workflow[] = [
|
||||
y: 290,
|
||||
nx: 295,
|
||||
ny: 330,
|
||||
system: "Rule",
|
||||
label: "No reply in 3 days?",
|
||||
log: "follow-up scheduled",
|
||||
},
|
||||
{
|
||||
id: "remind",
|
||||
@@ -262,9 +198,6 @@ export const WORKFLOWS: readonly Workflow[] = [
|
||||
y: 290,
|
||||
nx: 295,
|
||||
ny: 470,
|
||||
system: "Tasks",
|
||||
label: "Create reminder",
|
||||
log: "reminder created",
|
||||
},
|
||||
],
|
||||
edges: [
|
||||
@@ -286,37 +219,26 @@ export const WORKFLOWS: readonly Workflow[] = [
|
||||
},
|
||||
];
|
||||
|
||||
export const KIND_LABEL: Readonly<Record<NodeKind, string>> = {
|
||||
trigger: "Trigger: something happens",
|
||||
rule: "Rule: a fixed check",
|
||||
ai: "AI step: reads or sorts",
|
||||
action: "Action: a system does something",
|
||||
human: "Person: decides the unclear cases",
|
||||
};
|
||||
const NO_TEXT: FlowNodeCopy = { system: "", label: "", log: "" };
|
||||
|
||||
export const AUTOMATION_NOTES = [
|
||||
{
|
||||
title: "Worth automating when",
|
||||
items: [
|
||||
"The same steps repeat every day or every week",
|
||||
"Data is typed from one tool into another",
|
||||
"Mistakes happen because the work is monotonous",
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "What stays with people",
|
||||
items: [
|
||||
"Decisions and exceptions",
|
||||
"Anything a customer should hear from a person",
|
||||
"The final say when the automation is unsure",
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "What I need from you",
|
||||
items: [
|
||||
"Access to the tools and their interfaces (APIs)",
|
||||
"Someone who knows the process as it is done today",
|
||||
"Real examples to test with",
|
||||
],
|
||||
},
|
||||
] as const;
|
||||
/** The example workflows with their texts in one language. */
|
||||
export const workflowsFor = (
|
||||
copy: Readonly<Record<string, WorkflowCopy>>,
|
||||
): readonly Workflow[] =>
|
||||
LAYOUTS.flatMap((layout) => {
|
||||
const text = copy[layout.id];
|
||||
if (text === undefined) return [];
|
||||
return [
|
||||
{
|
||||
...layout,
|
||||
name: text.name,
|
||||
summary: text.summary,
|
||||
byHand: text.byHand,
|
||||
automated: text.automated,
|
||||
nodes: layout.nodes.map((node) => ({
|
||||
...node,
|
||||
...(text.nodes[node.id] ?? NO_TEXT),
|
||||
})),
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
import type { MailDraft } from "@/src/domain/email";
|
||||
|
||||
/**
|
||||
* Message starters for the contact section. The visitor picks what they need and
|
||||
* gets a prepared email to finish in their own mail app. The square brackets are
|
||||
* blanks for the visitor to fill in.
|
||||
*/
|
||||
export type ContactTopic = {
|
||||
readonly id: string;
|
||||
readonly label: string;
|
||||
readonly draft: MailDraft;
|
||||
};
|
||||
|
||||
const SIGN_OFF = "Best regards";
|
||||
|
||||
export const CONTACT_TOPICS: readonly ContactTopic[] = [
|
||||
{
|
||||
id: "developer",
|
||||
label: "A developer",
|
||||
draft: {
|
||||
subject: "A developer for our product",
|
||||
body: `Hi Marc,\n\nwe are working on [what you are building] and need help with [what is next on your list].\n\nA first call would suit us around [date or week].\n\n${SIGN_OFF}`,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "rescue",
|
||||
label: "A reliable app",
|
||||
draft: {
|
||||
subject: "Our app needs to become reliable",
|
||||
body: `Hi Marc,\n\nour app [name or short description] works, but [what keeps going wrong].\n\nCould you take a look?\n\n${SIGN_OFF}`,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "automation",
|
||||
label: "Automation",
|
||||
draft: {
|
||||
subject: "We want to automate a process",
|
||||
body: `Hi Marc,\n\nwe would like to automate [the process]. Today it is done by hand in [the tools involved].\n\nCan we talk about it?\n\n${SIGN_OFF}`,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "website",
|
||||
label: "A website",
|
||||
draft: {
|
||||
subject: "A website for my business",
|
||||
body: `Hi Marc,\n\nI need a website for [your business]. It should [what it is for].\n\nWhen could we talk?\n\n${SIGN_OFF}`,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "other",
|
||||
label: "Something else",
|
||||
draft: {
|
||||
subject: "A question",
|
||||
body: `Hi Marc,\n\n[what you have in mind]\n\n${SIGN_OFF}`,
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,731 @@
|
||||
import type { Content } from "./types";
|
||||
|
||||
const SIGN_OFF = "Viele Grüße";
|
||||
const GREETING = "Hallo Herr Mintel,";
|
||||
const FIRST_CALL = "Ein erstes Gespräch, etwa 20 Minuten";
|
||||
const GET_IN_TOUCH = "Kontakt aufnehmen";
|
||||
|
||||
/** The German texts of the site, in the formal "Sie". en.ts holds the same in English. */
|
||||
export const de: Content = {
|
||||
site: {
|
||||
title: "Freelance-Webentwickler für Web-Apps & Websites | Marc Mintel",
|
||||
description:
|
||||
"Freelance-Webentwickler mit 15 Jahren Erfahrung. Ich baue Web-Apps, Oberflächen und Websites mit React, Next.js und TypeScript und automatisiere Abläufe.",
|
||||
jobTitle: "Freelance-Webentwickler",
|
||||
ogLocale: "de_DE",
|
||||
shareImageAlt:
|
||||
"Web-Apps und Websites. Marc Mintel, Freelance-Webentwickler mit 15 Jahren Erfahrung.",
|
||||
knowsAbout: [
|
||||
"Webanwendungen",
|
||||
"Benutzeroberflächen",
|
||||
"Websites",
|
||||
"Prozessautomatisierung",
|
||||
"Web3-Frontends",
|
||||
],
|
||||
catalogName: "Leistungen",
|
||||
},
|
||||
nav: {
|
||||
contact: "Kontakt",
|
||||
contactLong: GET_IN_TOUCH,
|
||||
logoLabel: "Marc Mintel, nach oben",
|
||||
imprint: "Impressum",
|
||||
privacy: "Datenschutz",
|
||||
backToTop: "Nach oben",
|
||||
language: "Sprache",
|
||||
languageNames: { en: "English", de: "Deutsch" },
|
||||
},
|
||||
hero: {
|
||||
headline: [
|
||||
{ text: "WEB-APPS", tone: "solid" },
|
||||
{ text: "& WEBSITES.", tone: "bits" },
|
||||
],
|
||||
lead: "Freelance-Webentwickler mit 15 Jahren Erfahrung.",
|
||||
leadStrong: "Ich baue Web-Apps, Oberflächen und Websites.",
|
||||
primaryCta: GET_IN_TOUCH,
|
||||
secondaryCta: "Was ich mache",
|
||||
trust: ["15 Jahre im Web", "Remote, EU-Kernzeiten"],
|
||||
builtAndRunning: "Gebaut und in Betrieb:",
|
||||
},
|
||||
hello: {
|
||||
label: "Hallo",
|
||||
title: "Ich bin Marc.",
|
||||
lead: "Ich baue seit 15 Jahren für das Web: zuerst in Agenturen, später als Senior-Entwickler und heute selbstständig.",
|
||||
paragraphs: [
|
||||
"Meine Stärke ist der Teil, den Menschen sehen und benutzen: Design und Oberfläche. Ich kenne mich auch mit dem aus, was dahinter liegt, und kann deshalb eine vollständige Anwendung bauen und nicht nur ihre Oberfläche.",
|
||||
"Sie arbeiten direkt mit mir. Egal, ob Sie ein Produktteam leiten oder so etwas zum ersten Mal in Auftrag geben.",
|
||||
],
|
||||
portraitAlt: "Marc Mintel, Freelance-Webentwickler",
|
||||
careerLabel: "Der Weg bisher",
|
||||
career: [
|
||||
{ years: "2011", role: "Einstieg ins Web", place: "Ausbildung" },
|
||||
{ years: "2014 – 2020", role: "Agenturen", place: "Frontend-Entwickler" },
|
||||
{
|
||||
years: "2020 – 2023",
|
||||
role: "Produktunternehmen",
|
||||
place: "Senior-Softwareentwickler",
|
||||
},
|
||||
{
|
||||
years: "Heute",
|
||||
role: "Selbstständig",
|
||||
place: "Direkte Zusammenarbeit mit Kunden",
|
||||
},
|
||||
],
|
||||
},
|
||||
services: {
|
||||
label: "Leistungen",
|
||||
title: { lead: "Was brauchen Sie?", rest: "Vier typische Fälle." },
|
||||
tablistLabel: "Was brauchen Sie?",
|
||||
youGet: "Das bekommen Sie",
|
||||
howItRuns: "So läuft es ab",
|
||||
whatItCosts: "Das kostet es",
|
||||
paths: [
|
||||
{
|
||||
id: "developer",
|
||||
service: "Entwicklung von Web-Apps und Frontends",
|
||||
need: "Wir brauchen einen Entwickler für unser Produkt",
|
||||
forWhom: "Start-ups, Produktteams, Agenturen, Web3-Teams",
|
||||
headline: "Ein erfahrener Entwickler, der in Ihr Projekt einsteigt.",
|
||||
intro:
|
||||
"Sie haben ein Produkt und mehr Arbeit, als Ihr Team schafft. Ich arbeite an vereinbarten Wochentagen an dem, was als Nächstes auf Ihrer Liste steht: neue Funktionen, die Oberfläche oder die Teile, zu denen niemand kommt.",
|
||||
youGet: [
|
||||
"Web-Apps, Dashboards, Kundenportale und interne Werkzeuge",
|
||||
"Oberflächen mit Blick für Design und Bedienbarkeit",
|
||||
"Web3-Frontends: Wallet-Anbindung, Live-Daten, Dashboards",
|
||||
"Kleine Werkzeuge und Automatisierung für wiederkehrende Arbeit",
|
||||
],
|
||||
steps: [
|
||||
{
|
||||
title: FIRST_CALL,
|
||||
text: "Sie erzählen mir, was Sie bauen und wo Sie Unterstützung brauchen. Wenn ich dafür nicht der Richtige bin, sage ich das.",
|
||||
},
|
||||
{
|
||||
title: "Wir vereinbaren die Tage",
|
||||
text: "Wie viele Tage pro Woche ich für Sie arbeite und was zuerst drankommt.",
|
||||
},
|
||||
{
|
||||
title: "Ich arbeite die Liste ab",
|
||||
text: "Fertige Teile gehen zur Durchsicht an Sie, sobald sie fertig sind. Ich erfasse meine Stunden, und Sie sehen, wofür sie angefallen sind.",
|
||||
},
|
||||
{
|
||||
title: "Zwei Wochen Kündigungsfrist",
|
||||
text: "Die Vereinbarung kann mit zwei Wochen Frist zum Monatsende beendet werden.",
|
||||
},
|
||||
],
|
||||
pricingModel: "Nach Aufwand, solange Ihr Projekt läuft",
|
||||
pricingText:
|
||||
"Ich arbeite an vereinbarten Wochentagen an Ihrem Projekt und rechne die Stunden ab, die ich tatsächlich gearbeitet habe. Es gibt kein Paket zu kaufen und keine Mindestlaufzeit.",
|
||||
prices: [
|
||||
{ label: "Pro Stunde", amount: "100 EUR", note: "netto" },
|
||||
{ label: "Pro Tag", amount: "800 EUR", note: "8 Stunden, netto" },
|
||||
],
|
||||
smallPrint:
|
||||
"Eine Rechnung pro Monat, mit den erfassten Stunden. Beide Seiten können mit zwei Wochen Frist zum Monatsende kündigen.",
|
||||
cta: GET_IN_TOUCH,
|
||||
},
|
||||
{
|
||||
id: "rescue",
|
||||
service: "Code-Review und Stabilisierung von Web-Apps",
|
||||
need: "Unsere App soll zuverlässig werden",
|
||||
forWhom:
|
||||
"Teams, deren App schnell gebaut wurde und sich jetzt schwer ändern lässt",
|
||||
headline:
|
||||
"Eine Woche, um Ihre App durchzusehen und das Dringendste zu beheben.",
|
||||
intro:
|
||||
"Die App läuft, aber Fehler kommen wieder, und kleine Änderungen dauern länger, als sie sollten. Ich nehme mir eine Woche, gehe den Code durch, behebe, was in dieser Zeit möglich ist, und schreibe auf, was ich als Nächstes tun würde.",
|
||||
youGet: [
|
||||
"Eine ehrliche Einschätzung: was solide ist und was wackelt",
|
||||
"Automatisierte Tests für die wichtigsten Teile",
|
||||
"Die dringendsten Probleme behoben, soweit eine Woche reicht",
|
||||
"Eine schriftliche Liste der nächsten Schritte, in verständlicher Sprache",
|
||||
],
|
||||
steps: [
|
||||
{
|
||||
title: FIRST_CALL,
|
||||
text: "Sie zeigen mir die App und erzählen, wo sie Ärger macht.",
|
||||
},
|
||||
{
|
||||
title: "Eine Woche Arbeit",
|
||||
text: "Ich lese den Code, ergänze Tests, wo sie am meisten bringen, und behebe die dringendsten Probleme.",
|
||||
},
|
||||
{
|
||||
title: "Eine schriftliche Zusammenfassung",
|
||||
text: "Was ich gefunden habe, was ich geändert habe und was ich empfehle. So geschrieben, dass Sie es nachvollziehen können, ohne selbst zu entwickeln.",
|
||||
},
|
||||
{
|
||||
title: "Sie entscheiden, wie es weitergeht",
|
||||
text: "Mit mir, mit Ihrem eigenen Team oder gar nicht.",
|
||||
},
|
||||
],
|
||||
pricingModel: "Eine feste Woche zum festen Preis",
|
||||
pricingText:
|
||||
"Der Umfang ist eine Woche meiner Zeit. Sie kennen die Kosten, bevor ich anfange.",
|
||||
prices: [
|
||||
{
|
||||
label: "Eine Woche, 35 Stunden",
|
||||
amount: "3.500 EUR",
|
||||
note: "netto",
|
||||
},
|
||||
],
|
||||
smallPrint:
|
||||
"Eine Woche behebt nicht alles. Wenn ich im ersten Gespräch sehe, dass sie nicht reicht, um Ihnen zu nützen, sage ich Ihnen das.",
|
||||
cta: GET_IN_TOUCH,
|
||||
},
|
||||
{
|
||||
id: "automation",
|
||||
service: "Prozessautomatisierung und Systemintegration",
|
||||
need: "Wir wollen einen Ablauf automatisieren",
|
||||
forWhom:
|
||||
"Unternehmen, in denen Menschen Daten zwischen Programmen kopieren oder dieselben Schritte wiederholen",
|
||||
headline:
|
||||
"Systeme, die Arbeit aneinander weitergeben, damit Menschen es nicht tun müssen.",
|
||||
intro:
|
||||
"Jemand exportiert hier eine Liste, tippt sie dort ein und schreibt eine E-Mail dazu. Ich verbinde die Programme, die Sie schon nutzen, sodass wiederkehrende Schritte von selbst laufen und Ihre Leute die Arbeit machen können, für die man sie braucht.",
|
||||
youGet: [
|
||||
"Verbindungen zwischen Ihren Systemen über deren Schnittstellen (APIs)",
|
||||
"Wiederkehrende Abläufe, die ohne Handarbeit laufen",
|
||||
"Wo es hilft, Schritte mit KI: sortieren, zusammenfassen, Dokumente lesen",
|
||||
"Eine kurze Beschreibung, wie es funktioniert, damit es gewartet werden kann",
|
||||
],
|
||||
steps: [
|
||||
{
|
||||
title: FIRST_CALL,
|
||||
text: "Sie beschreiben den Ablauf und welche Programme beteiligt sind.",
|
||||
},
|
||||
{
|
||||
title: "Ich sehe mir an, wie es heute läuft",
|
||||
text: "Schritt für Schritt, mit den Menschen, die es machen. Oft lässt sich ein Teil automatisieren, nicht alles.",
|
||||
},
|
||||
{
|
||||
title: "Ich verbinde die Systeme",
|
||||
text: "Einen Schritt nach dem anderen. Sie testen es mit echten Fällen, bevor es die Handarbeit ersetzt.",
|
||||
},
|
||||
{
|
||||
title: "Es läuft von selbst",
|
||||
text: "Sie bekommen eine Notiz dazu, was es tut und was zu prüfen ist, wenn sich eines der Programme ändert.",
|
||||
},
|
||||
],
|
||||
pricingModel: "Erst eine Schätzung, dann ein Budget",
|
||||
pricingText:
|
||||
"Sobald ich den Ablauf gesehen habe, schätze ich die Stunden. Diese Schätzung wird zum Budget. Ich rechne die Stunden ab, die ich tatsächlich gebraucht habe, und sage früh Bescheid, wenn es nicht reicht.",
|
||||
prices: [{ label: "Pro Stunde", amount: "100 EUR", note: "netto" }],
|
||||
smallPrint:
|
||||
"Wie lange es dauert, hängt vom Ablauf ab. Deshalb gibt es keinen Preis, bevor ich ihn gesehen habe. Gebühren für die Programme selbst zahlen Sie.",
|
||||
cta: GET_IN_TOUCH,
|
||||
},
|
||||
{
|
||||
id: "website",
|
||||
service: "Gestaltung und Entwicklung von Websites",
|
||||
need: "Ich brauche einfach eine Website",
|
||||
forWhom:
|
||||
"Unternehmen und Selbstständige, die sich nicht selbst darum kümmern wollen",
|
||||
headline: "Eine Website, für Sie gebaut und betreut.",
|
||||
intro:
|
||||
"Sie wollen keinen Website-Baukasten lernen und sich nicht mit Hosting beschäftigen. Sie sagen mir, was Ihr Unternehmen macht und was die Seite erreichen soll. Ich gestalte sie, baue sie und bringe sie online.",
|
||||
youGet: [
|
||||
"Ein Design für Ihr Unternehmen, keine Vorlage",
|
||||
"Funktioniert auf Handy, Tablet und Desktop",
|
||||
"Um das Hosting kümmere ich mich, Sie betreuen keinen Server",
|
||||
"Änderungen per E-Mail: Sie schreiben, was Sie brauchen, ich setze es um",
|
||||
],
|
||||
steps: [
|
||||
{
|
||||
title: FIRST_CALL,
|
||||
text: "Sie erzählen mir von Ihrem Unternehmen und wofür die Website da ist.",
|
||||
},
|
||||
{
|
||||
title: "Ein Entwurf zum Ansehen",
|
||||
text: "Sie bekommen eine erste Version zum Durchklicken. Wir passen sie an, bis sie passt.",
|
||||
},
|
||||
{
|
||||
title: "Ich baue sie und bringe sie online",
|
||||
text: "Mit Ihren Texten und Bildern und einem Kontaktformular. Sie geben sie frei, bevor sie live geht.",
|
||||
},
|
||||
{
|
||||
title: "Änderungen danach",
|
||||
text: "Ein neues Foto oder ein geänderter Preis: Sie schreiben mir eine E-Mail.",
|
||||
},
|
||||
],
|
||||
pricingModel: "Erst eine Schätzung, dann ein Budget",
|
||||
pricingText:
|
||||
"Nach dem ersten Gespräch bekommen Sie eine Schätzung. Sie wird Ihr Budget: Ich rechne die Stunden ab, die tatsächlich angefallen sind. Die Schätzung hält in der Regel, und wenn nicht, sage ich es Ihnen, bevor das Budget aufgebraucht ist.",
|
||||
prices: [
|
||||
{
|
||||
label: "Eine typische Unternehmenswebsite",
|
||||
amount: "ab 3.800 EUR",
|
||||
note: "einmalig, netto",
|
||||
},
|
||||
{
|
||||
label: "Alles Weitere, pro Stunde",
|
||||
amount: "100 EUR",
|
||||
note: "netto",
|
||||
},
|
||||
],
|
||||
smallPrint:
|
||||
"Hosting und kleine Änderungen danach kosten einen monatlichen Betrag, den wir vor dem Start vereinbaren. E-Mail-Postfächer verwalte ich nicht; sie bleiben bei Ihrem Anbieter.",
|
||||
cta: GET_IN_TOUCH,
|
||||
},
|
||||
],
|
||||
scenes: {
|
||||
you: "Sie",
|
||||
me: "Ich",
|
||||
talkLength: "etwa 20 Minuten",
|
||||
days: ["Mo", "Di", "Mi", "Do", "Fr"],
|
||||
daysNote: "zum Beispiel drei Tage pro Woche",
|
||||
review: "fertige Teile gehen an Sie zur Durchsicht",
|
||||
week: "Woche {n}",
|
||||
monthEnd: "Monatsende",
|
||||
notice: "zwei Wochen Frist",
|
||||
repair: "Wackliges wird getestet und behoben",
|
||||
summaryParts: ["gefunden", "geändert", "als Nächstes"],
|
||||
summaryNote: "so geschrieben, dass Sie folgen können",
|
||||
options: ["weiter mit mir", "Ihr eigenes Team", "hier aufhören"],
|
||||
online: "online",
|
||||
exampleDomain: "ihrefirma.de",
|
||||
firstVersion: "eine erste Version zum Durchklicken",
|
||||
approve: "Sie geben frei, bevor es live geht",
|
||||
changeByEmail: "Sie schreiben, ich setze es um",
|
||||
tools: ["Shop", "Buchhaltung", "E-Mail", "CRM"],
|
||||
byHand: "heute: von Hand kopiert",
|
||||
wiring: "Schritt für Schritt verbunden",
|
||||
running: "die Schritte laufen ohne Handarbeit",
|
||||
},
|
||||
},
|
||||
automation: {
|
||||
label: "Prozessautomatisierung",
|
||||
title: { lead: "Lassen Sie die Systeme", rest: "das Kopieren übernehmen." },
|
||||
intro:
|
||||
"In den meisten Unternehmen tragen Menschen Informationen von einem Programm ins andere: hier lesen, dort eintippen, eine E-Mail dazu schreiben. Automatisierung verbindet diese Programme, sodass die Arbeit von selbst vom einen zum nächsten wandert. Drei typische Beispiele:",
|
||||
examplesLabel: "Beispielabläufe",
|
||||
running: "läuft",
|
||||
runComplete: "Durchlauf fertig",
|
||||
log: "Protokoll",
|
||||
byHand: "Von Hand",
|
||||
automated: "Automatisiert",
|
||||
kinds: {
|
||||
trigger: "Auslöser: etwas passiert",
|
||||
rule: "Regel: eine feste Prüfung",
|
||||
ai: "KI-Schritt: liest oder sortiert",
|
||||
action: "Aktion: ein System tut etwas",
|
||||
human: "Mensch: entscheidet die unklaren Fälle",
|
||||
},
|
||||
workflows: {
|
||||
order: {
|
||||
name: "Bestellung bis Rechnung",
|
||||
summary:
|
||||
"Im Shop geht eine neue Bestellung ein. Rechnung, Kundendatensatz und Bestätigung folgen, ohne dass jemand tippt.",
|
||||
byHand:
|
||||
"Jemand öffnet die Bestellung, schreibt die Rechnung, aktualisiert den Kundendatensatz und verschickt eine E-Mail.",
|
||||
automated:
|
||||
"Die Bestellung stößt alles an. Menschen sehen nur das fertige Ergebnis.",
|
||||
nodes: {
|
||||
shop: {
|
||||
system: "Shop",
|
||||
label: "Neue Bestellung",
|
||||
log: "Bestellung eingegangen",
|
||||
},
|
||||
stock: {
|
||||
system: "Regel",
|
||||
label: "Auf Lager?",
|
||||
log: "Bestand geprüft: verfügbar",
|
||||
},
|
||||
invoice: {
|
||||
system: "Buchhaltung",
|
||||
label: "Rechnung erstellen",
|
||||
log: "Rechnung erstellt",
|
||||
},
|
||||
crm: {
|
||||
system: "CRM",
|
||||
label: "Kunde aktualisieren",
|
||||
log: "Kundendatensatz aktualisiert",
|
||||
},
|
||||
mail: {
|
||||
system: "E-Mail",
|
||||
label: "Bestätigung senden",
|
||||
log: "Bestätigung verschickt",
|
||||
},
|
||||
},
|
||||
},
|
||||
documents: {
|
||||
name: "Eingehende Dokumente",
|
||||
summary:
|
||||
"Rechnungen kommen als PDF per E-Mail. Ein KI-Schritt liest sie, eine Regel prüft das Ergebnis, und ein Mensch sieht nur die unklaren Fälle.",
|
||||
byHand:
|
||||
"Jemand öffnet jedes PDF, liest Lieferant, Betrag und Datum ab und tippt sie in die Buchhaltung.",
|
||||
automated:
|
||||
"Klare Fälle werden direkt gebucht. Unklare gehen mit dem Dokument im Anhang an einen Menschen.",
|
||||
nodes: {
|
||||
inbox: {
|
||||
system: "Posteingang",
|
||||
label: "E-Mail mit PDF",
|
||||
log: "Dokument eingegangen",
|
||||
},
|
||||
read: {
|
||||
system: "KI-Schritt",
|
||||
label: "Dokument lesen",
|
||||
log: "Lieferant, Betrag und Datum ausgelesen",
|
||||
},
|
||||
check: { system: "Regel", label: "Plausibel?", log: "Werte geprüft" },
|
||||
book: {
|
||||
system: "Buchhaltung",
|
||||
label: "Buchen",
|
||||
log: "automatisch gebucht",
|
||||
},
|
||||
ask: {
|
||||
system: "Ein Mensch",
|
||||
label: "Prüfen, entscheiden",
|
||||
log: "unklar: zur Prüfung an einen Menschen",
|
||||
},
|
||||
},
|
||||
},
|
||||
lead: {
|
||||
name: "Anfrage bis Nachfassen",
|
||||
summary:
|
||||
"Jemand füllt das Kontaktformular aus. Der Kontakt wird angelegt, die richtige Person erfährt davon, und eine Erinnerung sorgt dafür, dass niemand es vergisst.",
|
||||
byHand:
|
||||
"Jemand kopiert die Anfrage ins CRM, leitet sie per E-Mail weiter und hofft, dass das Nachfassen nicht vergessen wird.",
|
||||
automated:
|
||||
"Die Anfrage wird abgelegt, zugewiesen und zum geplanten Zeitpunkt nachverfolgt.",
|
||||
nodes: {
|
||||
form: {
|
||||
system: "Website",
|
||||
label: "Kontaktformular",
|
||||
log: "Anfrage eingegangen",
|
||||
},
|
||||
contact: {
|
||||
system: "CRM",
|
||||
label: "Kontakt anlegen",
|
||||
log: "Kontakt angelegt",
|
||||
},
|
||||
notify: {
|
||||
system: "Team-Chat",
|
||||
label: "Vertrieb informieren",
|
||||
log: "Vertrieb informiert",
|
||||
},
|
||||
wait: {
|
||||
system: "Regel",
|
||||
label: "Nach 3 Tagen offen?",
|
||||
log: "Nachfassen eingeplant",
|
||||
},
|
||||
remind: {
|
||||
system: "Aufgaben",
|
||||
label: "Erinnerung anlegen",
|
||||
log: "Erinnerung angelegt",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
notes: [
|
||||
{
|
||||
title: "Wann es sich lohnt",
|
||||
items: [
|
||||
"Dieselben Schritte wiederholen sich täglich oder wöchentlich",
|
||||
"Daten werden von einem Programm ins andere getippt",
|
||||
"Fehler passieren, weil die Arbeit eintönig ist",
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Was bei Menschen bleibt",
|
||||
items: [
|
||||
"Entscheidungen und Ausnahmen",
|
||||
"Alles, was ein Kunde von einem Menschen hören sollte",
|
||||
"Das letzte Wort, wenn die Automatisierung unsicher ist",
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Was ich von Ihnen brauche",
|
||||
items: [
|
||||
"Zugang zu den Programmen und ihren Schnittstellen (APIs)",
|
||||
"Jemanden, der den Ablauf so kennt, wie er heute gemacht wird",
|
||||
"Echte Beispiele zum Testen",
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
technology: {
|
||||
label: "Technik",
|
||||
title: { lead: "Für welche Projekte", rest: "ich der Richtige bin." },
|
||||
fit: [
|
||||
{
|
||||
title: "Passt gut",
|
||||
items: [
|
||||
"Web-Apps: Dashboards, Portale, interne Werkzeuge",
|
||||
"Oberflächen, bei denen Design und Bedienbarkeit zählen",
|
||||
"Unternehmenswebsites, auch in mehreren Sprachen",
|
||||
"Web3-Frontends: Wallets, Live-Daten, Dashboards",
|
||||
"Automatisierung: Systeme und ihre APIs verbinden",
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Auch möglich",
|
||||
items: [
|
||||
"Das Backend, das eine App braucht: APIs, Login, Integrationen",
|
||||
"Werkzeuge für die Kommandozeile",
|
||||
"Bestehenden Code übernehmen und aufräumen",
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Nicht mein Gebiet",
|
||||
items: [
|
||||
"Mobile Apps für die App-Stores",
|
||||
"Windows-Software",
|
||||
"Serverbetrieb, spezialisierte Datenbankarbeit",
|
||||
],
|
||||
},
|
||||
],
|
||||
techLabel: "Womit ich arbeite",
|
||||
tech: [
|
||||
{
|
||||
area: "Oberfläche",
|
||||
items: ["TypeScript", "React", "Next.js", "Vue.js", "Tailwind CSS"],
|
||||
},
|
||||
{
|
||||
area: "Dahinter",
|
||||
items: ["Node.js", "REST APIs", "PostgreSQL", "Headless CMS"],
|
||||
},
|
||||
{ area: "Web3 und Systeme", items: ["Solidity", "Rust"] },
|
||||
{
|
||||
area: "Auslieferung",
|
||||
items: ["Automatisierte Tests", "Docker", "CI/CD"],
|
||||
},
|
||||
],
|
||||
pipelineTitle: "Was passiert, bevor eine Änderung live geht.",
|
||||
pipelineIntro:
|
||||
"Software hängt zusammen: Eine Änderung an einer Stelle kann eine andere kaputt machen. Ich sichere mit automatisierten Tests ab, was ich baue, und jede Änderung muss sie bestehen, bevor sie veröffentlicht wird. So sieht ein solcher Durchlauf aus.",
|
||||
pipeline: {
|
||||
simulate: "Änderung simulieren in",
|
||||
scenarios: {
|
||||
orders: {
|
||||
label: "Bestellungen",
|
||||
testName: "Rechnungssumme passt zur rabattierten Bestellung",
|
||||
expected: "119.00",
|
||||
received: "125.00",
|
||||
},
|
||||
login: {
|
||||
label: "Login",
|
||||
testName: "Bestätigung geht an die E-Mail-Adresse des Kontos",
|
||||
expected: '"anna@example.com"',
|
||||
received: "undefined",
|
||||
},
|
||||
invoices: {
|
||||
label: "Rechnungen",
|
||||
testName: "Rechnungs-E-Mail hat das PDF im Anhang",
|
||||
expected: "1 Anhang",
|
||||
received: "0 Anhänge",
|
||||
},
|
||||
emails: {
|
||||
label: "E-Mails",
|
||||
testName:
|
||||
"Bestellung ist bestätigt, sobald die E-Mail eingereiht ist",
|
||||
expected: '"bestätigt"',
|
||||
received: '"offen"',
|
||||
},
|
||||
},
|
||||
stages: ["Lint", "Typen", "Tests", "Deploy"],
|
||||
explanations: [
|
||||
{
|
||||
title: "Eine Änderung kommt herein",
|
||||
text: "Im Bereich {area} wird etwas geändert. Bevor es live gehen kann, wird das ganze Projekt automatisch geprüft.",
|
||||
},
|
||||
{
|
||||
title: "Ein Test schlägt fehl, die Freigabe ist gesperrt",
|
||||
text: "Die Änderung hat an einer anderen Stelle etwas kaputt gemacht. Das fällt hier auf, bei mir, und nichts davon erreicht Ihre Nutzer.",
|
||||
},
|
||||
{
|
||||
title: "Behoben, erneut geprüft, veröffentlicht",
|
||||
text: "Ich repariere, was kaputtgegangen ist, der Durchlauf besteht, und erst dann geht die Änderung live.",
|
||||
},
|
||||
],
|
||||
runAgain: "Erneut starten",
|
||||
run: "Lauf",
|
||||
fixPushed: "Korrektur eingespielt",
|
||||
lint: "$ lint: prüft den Code-Stil…",
|
||||
types: "$ types: prüft, ob die Teile zusammenpassen…",
|
||||
tests: "Tests",
|
||||
expected: "erwartet",
|
||||
received: "erhalten",
|
||||
runningFile: "läuft: {file}…",
|
||||
summaryLabel: "Tests",
|
||||
failed: "{n} fehlgeschlagen",
|
||||
passed: "{n} bestanden",
|
||||
of: "von {total}",
|
||||
pending: "Freigabe offen",
|
||||
blocked: "Freigabe gesperrt",
|
||||
released: "veröffentlicht",
|
||||
footnote:
|
||||
"Eine Illustration mit erfundenen Dateinamen und Zahlen, einem echten Testlauf nachempfunden. Tests finden nicht jeden Fehler, aber diese Art schon.",
|
||||
},
|
||||
},
|
||||
working: {
|
||||
label: "Zusammenarbeit",
|
||||
title: { lead: "So arbeite ich.", rest: "Nichts Ungewöhnliches." },
|
||||
principles: [
|
||||
{
|
||||
title: "Ein Ansprechpartner",
|
||||
text: "Sie sprechen mit mir, und ich bin derjenige, der die Arbeit macht.",
|
||||
},
|
||||
{
|
||||
title: "Stunden, die Sie nachprüfen können",
|
||||
text: "Ich erfasse meine Zeit. Die Monatsrechnung zeigt, wofür die Stunden angefallen sind.",
|
||||
},
|
||||
{
|
||||
title: "Sorgfältige Arbeit",
|
||||
text: "Ich teste, was ich baue, bevor ich es übergebe, und sage es, wenn etwas noch nicht fertig ist oder ich mir nicht sicher bin.",
|
||||
},
|
||||
{
|
||||
title: "Was ich baue, gehört Ihnen",
|
||||
text: "Code und Design gehören Ihnen. Sie können beides an einen anderen Entwickler übergeben.",
|
||||
},
|
||||
],
|
||||
diagram: {
|
||||
description:
|
||||
"Oft geht eine Anfrage durch mehrere Hände, bevor sie beim Entwickler ankommt. Bei mir gibt es eine direkte Leitung zwischen Ihnen und dem, der die Arbeit macht.",
|
||||
chain: ["Sie", "Vertrieb", "Manager", "Designer", "Entwickler"],
|
||||
you: "Sie",
|
||||
me: "Ich",
|
||||
often: "oft",
|
||||
withMe: "bei mir",
|
||||
oftenLong: "oft: eine Anfrage wird weitergereicht",
|
||||
withMeLong: "bei mir: eine direkte Leitung",
|
||||
},
|
||||
},
|
||||
work: {
|
||||
label: "Meine Arbeit",
|
||||
title: {
|
||||
lead: "Zwei Websites, die ich gebaut habe.",
|
||||
rest: "Beide sind live.",
|
||||
},
|
||||
items: {
|
||||
"klz-cables.com": {
|
||||
client: "Kabellieferant",
|
||||
text: "Die Unternehmenswebsite eines Lieferanten für Energiekabel, auf Deutsch und Englisch.",
|
||||
did: [
|
||||
"Produktkatalog mit Kategorien und Detailseiten",
|
||||
"Datenblätter, erzeugt aus den Produktdaten",
|
||||
"Inhalte, die der Kunde in einem CMS pflegen kann",
|
||||
],
|
||||
tech: ["Next.js", "TypeScript", "Headless CMS", "Zwei Sprachen"],
|
||||
},
|
||||
"e-tib.com": {
|
||||
client: "Kabelnetzbau",
|
||||
text: "Die Unternehmenswebsite einer Gruppe im Kabelnetzbau, auf Deutsch und Englisch.",
|
||||
did: [
|
||||
"Seiten für Leistungen, Referenzen, Karriere und Veranstaltungen",
|
||||
"Dieselben Inhalte in zwei Sprachen",
|
||||
"Funktioniert auf Handy, Tablet und Desktop",
|
||||
],
|
||||
tech: ["Next.js", "TypeScript", "Headless CMS", "Zwei Sprachen"],
|
||||
},
|
||||
},
|
||||
openLive: "Live-Website öffnen",
|
||||
desktopAlt: "{name} auf einem Desktop-Bildschirm",
|
||||
phoneAlt: "{name} auf einem Handy",
|
||||
note: "Neben der Arbeit für Kunden baue ich eigene Software, unter anderem ein System in Rust und Solidity, das auf sieben Blockchains läuft.",
|
||||
},
|
||||
faq: {
|
||||
label: "Fragen",
|
||||
title: { lead: "Gut zu wissen,", rest: "bevor wir sprechen." },
|
||||
items: [
|
||||
{
|
||||
question: "Ich bin kein Technikmensch. Ist das ein Problem?",
|
||||
answer:
|
||||
"Nein. Ich erkläre in verständlicher Sprache, was ich tue, und Sie können jederzeit nachfragen. Code müssen Sie nicht lesen.",
|
||||
},
|
||||
{
|
||||
question: "Wie lange bin ich gebunden?",
|
||||
answer:
|
||||
"Projektarbeit wird nach Aufwand abgerechnet. Beide Seiten können mit zwei Wochen Frist zum Monatsende kündigen.",
|
||||
},
|
||||
{
|
||||
question: "Wem gehört, was Sie bauen?",
|
||||
answer:
|
||||
"Ihnen. Code und Design gehören Ihnen, und Sie können beides an einen anderen Entwickler übergeben.",
|
||||
},
|
||||
{
|
||||
question: "Können Sie mit unserem bestehenden Team und Code arbeiten?",
|
||||
answer:
|
||||
"Ja. Ich habe jahrelang in Agenturen und Produktteams gearbeitet, der Einstieg in ein bestehendes Projekt ist mir vertraut.",
|
||||
},
|
||||
{
|
||||
question: "Wie erreiche ich Sie?",
|
||||
answer:
|
||||
"Per E-Mail oder Videocall. Ich arbeite remote und bin zu europäischen Geschäftszeiten erreichbar.",
|
||||
},
|
||||
{
|
||||
question: "Was machen Sie nicht?",
|
||||
answer:
|
||||
"Mobile Apps für die App-Stores, Windows-Software und den Betrieb Ihrer Server. Wenn Ihr Projekt das braucht, sage ich das im ersten Gespräch.",
|
||||
},
|
||||
],
|
||||
},
|
||||
contact: {
|
||||
headline: [
|
||||
{ text: "REDEN", tone: "bits" },
|
||||
{ text: "WIR.", tone: "solid" },
|
||||
],
|
||||
lead: "Wählen Sie aus, was Sie brauchen, und die E-Mail schreibt sich von selbst. Oder schreiben Sie mir direkt:",
|
||||
newMessage: "Neue Nachricht",
|
||||
draft: "Entwurf",
|
||||
need: "Ich brauche",
|
||||
to: "an",
|
||||
subject: "Betreff",
|
||||
open: "Im E-Mail-Programm öffnen",
|
||||
copy: "Adresse kopieren",
|
||||
copied: "Kopiert",
|
||||
// The square brackets are blanks for the visitor to fill in.
|
||||
topics: [
|
||||
{
|
||||
id: "developer",
|
||||
label: "Einen Entwickler",
|
||||
draft: {
|
||||
subject: "Ein Entwickler für unser Produkt",
|
||||
body: `${GREETING}\n\nwir arbeiten an [was Sie bauen] und brauchen Unterstützung bei [was als Nächstes ansteht].\n\nEin erstes Gespräch würde uns um den [Datum oder Woche] passen.\n\n${SIGN_OFF}`,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "rescue",
|
||||
label: "Eine zuverlässige App",
|
||||
draft: {
|
||||
subject: "Unsere App soll zuverlässig werden",
|
||||
body: `${GREETING}\n\nunsere App [Name oder kurze Beschreibung] läuft, aber [was immer wieder schiefgeht].\n\nKönnten Sie sich das ansehen?\n\n${SIGN_OFF}`,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "automation",
|
||||
label: "Automatisierung",
|
||||
draft: {
|
||||
subject: "Wir möchten einen Ablauf automatisieren",
|
||||
body: `${GREETING}\n\nwir möchten [den Ablauf] automatisieren. Heute wird er von Hand in [beteiligte Programme] erledigt.\n\nKönnen wir darüber sprechen?\n\n${SIGN_OFF}`,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "website",
|
||||
label: "Eine Website",
|
||||
draft: {
|
||||
subject: "Eine Website für mein Unternehmen",
|
||||
body: `${GREETING}\n\nich brauche eine Website für [Ihr Unternehmen]. Sie soll [wofür sie da ist].\n\nWann könnten wir sprechen?\n\n${SIGN_OFF}`,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "other",
|
||||
label: "Etwas anderes",
|
||||
draft: {
|
||||
subject: "Eine Frage",
|
||||
body: `${GREETING}\n\n[was Sie vorhaben]\n\n${SIGN_OFF}`,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
notFound: {
|
||||
kicker: "Fehler 404",
|
||||
title: "Seite nicht gefunden.",
|
||||
text: "Diese Adresse gibt es nicht. Die Seite wurde vielleicht verschoben oder entfernt.",
|
||||
home: "Zur Startseite",
|
||||
},
|
||||
error: {
|
||||
kicker: "Fehler 500",
|
||||
title: "Etwas ist schiefgelaufen.",
|
||||
text: "Ein unerwartetes Problem ist aufgetreten. Es wurde protokolliert.",
|
||||
retry: "Erneut versuchen",
|
||||
home: "Zur Startseite",
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,708 @@
|
||||
import type { Content } from "./types";
|
||||
|
||||
const SIGN_OFF = "Best regards";
|
||||
const FIRST_CALL = "A first call, about 20 minutes";
|
||||
const GET_IN_TOUCH = "Get in touch";
|
||||
|
||||
/** The English texts of the site. de.ts holds the same in German. */
|
||||
export const en: Content = {
|
||||
site: {
|
||||
title: "Freelance web developer for web apps & websites | Marc Mintel",
|
||||
description:
|
||||
"Freelance web developer with 15 years of experience. I build web apps, interfaces and websites with React, Next.js and TypeScript, and automate processes.",
|
||||
jobTitle: "Freelance web developer",
|
||||
ogLocale: "en_US",
|
||||
shareImageAlt:
|
||||
"Web apps and websites. Marc Mintel, freelance web developer with 15 years of experience.",
|
||||
knowsAbout: [
|
||||
"Web applications",
|
||||
"User interfaces",
|
||||
"Websites",
|
||||
"Process automation",
|
||||
"Web3 frontends",
|
||||
],
|
||||
catalogName: "Services",
|
||||
},
|
||||
nav: {
|
||||
contact: GET_IN_TOUCH,
|
||||
contactLong: GET_IN_TOUCH,
|
||||
logoLabel: "Marc Mintel, back to top",
|
||||
imprint: "Imprint",
|
||||
privacy: "Privacy",
|
||||
backToTop: "Back to top",
|
||||
language: "Language",
|
||||
languageNames: { en: "English", de: "Deutsch" },
|
||||
},
|
||||
hero: {
|
||||
headline: [
|
||||
{ text: "WEB APPS", tone: "solid" },
|
||||
{ text: "& WEBSITES.", tone: "bits" },
|
||||
],
|
||||
lead: "Freelance web developer with 15 years of experience.",
|
||||
leadStrong: "I build web apps, interfaces and websites.",
|
||||
primaryCta: GET_IN_TOUCH,
|
||||
secondaryCta: "What I do",
|
||||
trust: ["15 years in web", "Remote, EU core hours"],
|
||||
builtAndRunning: "Built and running:",
|
||||
},
|
||||
hello: {
|
||||
label: "Hello",
|
||||
title: "I am Marc.",
|
||||
lead: "I have been building for the web for 15 years: first in agencies, later as a senior developer, and now on my own.",
|
||||
paragraphs: [
|
||||
"My strength is the part people see and use: design and interface. I am also at home with what lies behind it, so I can build a complete application and not just its surface.",
|
||||
"You work with me directly. Whether you run a product team or are commissioning something like this for the first time.",
|
||||
],
|
||||
portraitAlt: "Marc Mintel, freelance web developer",
|
||||
careerLabel: "The path so far",
|
||||
career: [
|
||||
{ years: "2011", role: "Started in the web", place: "Apprenticeship" },
|
||||
{ years: "2014 – 2020", role: "Agencies", place: "Frontend developer" },
|
||||
{
|
||||
years: "2020 – 2023",
|
||||
role: "Product companies",
|
||||
place: "Senior software developer",
|
||||
},
|
||||
{
|
||||
years: "Today",
|
||||
role: "Independent",
|
||||
place: "Working directly with clients",
|
||||
},
|
||||
],
|
||||
},
|
||||
services: {
|
||||
label: "Services",
|
||||
title: { lead: "What do you need?", rest: "Four typical cases." },
|
||||
tablistLabel: "What do you need?",
|
||||
youGet: "What you get",
|
||||
howItRuns: "How it runs",
|
||||
whatItCosts: "What it costs",
|
||||
paths: [
|
||||
{
|
||||
id: "developer",
|
||||
service: "Web app and frontend development",
|
||||
need: "We need a developer for our product",
|
||||
forWhom: "Startups, product teams, agencies, Web3 teams",
|
||||
headline: "An experienced developer who joins your project.",
|
||||
intro:
|
||||
"You have a product and more work than your team can handle. I work on agreed days of the week on what is next on your list: new features, the interface, or the parts nobody gets around to.",
|
||||
youGet: [
|
||||
"Web apps, dashboards, customer portals and internal tools",
|
||||
"Interfaces with attention to design and usability",
|
||||
"Web3 frontends: wallet connection, live data, dashboards",
|
||||
"Small tools and automation for recurring work",
|
||||
],
|
||||
steps: [
|
||||
{
|
||||
title: FIRST_CALL,
|
||||
text: "You tell me what you are building and where you need help. If I am not the right person for it, I say so.",
|
||||
},
|
||||
{
|
||||
title: "We agree on the days",
|
||||
text: "How many days a week I work for you, and what should come first.",
|
||||
},
|
||||
{
|
||||
title: "I work through the list",
|
||||
text: "Finished parts go to you for review as they are done. I track my hours and you can see what they were spent on.",
|
||||
},
|
||||
{
|
||||
title: "Two weeks notice",
|
||||
text: "The agreement can be ended with two weeks notice to the end of a month.",
|
||||
},
|
||||
],
|
||||
pricingModel: "By effort, for as long as your project runs",
|
||||
pricingText:
|
||||
"I work on your project on agreed days of the week and bill the hours I actually worked. There is no package to buy and no minimum term.",
|
||||
prices: [
|
||||
{ label: "Per hour", amount: "100 EUR", note: "net" },
|
||||
{ label: "Per day", amount: "800 EUR", note: "8 hours, net" },
|
||||
],
|
||||
smallPrint:
|
||||
"One invoice per month, with the tracked hours. Either side can end it with two weeks notice to the end of a month.",
|
||||
cta: GET_IN_TOUCH,
|
||||
},
|
||||
{
|
||||
id: "rescue",
|
||||
service: "Code review and stabilisation of web apps",
|
||||
need: "Our app needs to become reliable",
|
||||
forWhom: "Teams whose app was built quickly and is now hard to change",
|
||||
headline: "A week to look at your app and fix what is most urgent.",
|
||||
intro:
|
||||
"The app works, but bugs come back and small changes take longer than they should. I take a week to go through the code, fix what I can in that time and write down what I would do next.",
|
||||
youGet: [
|
||||
"An honest assessment: what is solid and what is fragile",
|
||||
"Automated tests for the most important parts",
|
||||
"The most urgent problems fixed, as far as a week allows",
|
||||
"A written list of next steps, in plain language",
|
||||
],
|
||||
steps: [
|
||||
{
|
||||
title: FIRST_CALL,
|
||||
text: "You show me the app and tell me where it causes trouble.",
|
||||
},
|
||||
{
|
||||
title: "One week of work",
|
||||
text: "I read the code, add tests where they matter most and fix the most urgent problems.",
|
||||
},
|
||||
{
|
||||
title: "A written summary",
|
||||
text: "What I found, what I changed and what I recommend. Written so that you can follow it without being a developer.",
|
||||
},
|
||||
{
|
||||
title: "You decide how to continue",
|
||||
text: "With me, with your own team, or not at all.",
|
||||
},
|
||||
],
|
||||
pricingModel: "A fixed week at a fixed price",
|
||||
pricingText:
|
||||
"The scope is one week of my time. You know the cost before I start.",
|
||||
prices: [
|
||||
{ label: "One week, 35 hours", amount: "3,500 EUR", note: "net" },
|
||||
],
|
||||
smallPrint:
|
||||
"A week does not fix everything. If I see in the first call that it will not be enough to be useful, I tell you.",
|
||||
cta: GET_IN_TOUCH,
|
||||
},
|
||||
{
|
||||
id: "automation",
|
||||
service: "Process automation and system integration",
|
||||
need: "We want to automate a process",
|
||||
forWhom:
|
||||
"Companies where people copy data between tools or repeat the same steps",
|
||||
headline:
|
||||
"Systems that pass work on to each other, so people do not have to.",
|
||||
intro:
|
||||
"Someone exports a list here, types it in there and sends an email about it. I connect the tools you already use, so that recurring steps run on their own and your people can do the work that needs them.",
|
||||
youGet: [
|
||||
"Connections between your systems through their interfaces (APIs)",
|
||||
"Recurring workflows that run without manual steps",
|
||||
"Where it helps, steps handled with AI: sorting, summarising, reading documents",
|
||||
"A short description of how it works, so it can be maintained",
|
||||
],
|
||||
steps: [
|
||||
{
|
||||
title: FIRST_CALL,
|
||||
text: "You describe the process and which tools are involved.",
|
||||
},
|
||||
{
|
||||
title: "I look at how it is done today",
|
||||
text: "Step by step, with the people who do it. Often a part can be automated, not everything.",
|
||||
},
|
||||
{
|
||||
title: "I connect the systems",
|
||||
text: "One step at a time. You test it with real cases before it replaces the manual work.",
|
||||
},
|
||||
{
|
||||
title: "It runs on its own",
|
||||
text: "You get a note on what it does and what to check when one of the tools changes.",
|
||||
},
|
||||
],
|
||||
pricingModel: "An estimate first, then a budget",
|
||||
pricingText:
|
||||
"Once I have seen the process I estimate the hours. That estimate becomes the budget. I bill the hours I actually needed and tell you early if it will not be enough.",
|
||||
prices: [{ label: "Per hour", amount: "100 EUR", note: "net" }],
|
||||
smallPrint:
|
||||
"How long it takes depends on the process, so there is no price before I have seen it. Fees for the tools themselves are paid by you.",
|
||||
cta: GET_IN_TOUCH,
|
||||
},
|
||||
{
|
||||
id: "website",
|
||||
service: "Website design and development",
|
||||
need: "I just need a website",
|
||||
forWhom: "Companies and self-employed people who want it taken care of",
|
||||
headline: "A website, built and looked after for you.",
|
||||
intro:
|
||||
"You do not want to learn a website builder or deal with hosting. You tell me what your business does and what the site should achieve. I design it, build it and put it online.",
|
||||
youGet: [
|
||||
"A design made for your business, not a template",
|
||||
"Works on phone, tablet and desktop",
|
||||
"Hosting taken care of, no server for you to manage",
|
||||
"Changes by email: you write what you need, I make the change",
|
||||
],
|
||||
steps: [
|
||||
{
|
||||
title: FIRST_CALL,
|
||||
text: "You tell me about your business and what the website is for.",
|
||||
},
|
||||
{
|
||||
title: "A design to look at",
|
||||
text: "You get a first version to click through. We adjust it until it fits.",
|
||||
},
|
||||
{
|
||||
title: "I build it and put it online",
|
||||
text: "With your texts and images and a contact form. You approve it before it goes live.",
|
||||
},
|
||||
{
|
||||
title: "Changes afterwards",
|
||||
text: "A new photo or a changed price: you send me an email.",
|
||||
},
|
||||
],
|
||||
pricingModel: "An estimate first, then a budget",
|
||||
pricingText:
|
||||
"After the first call you get an estimate. It becomes your budget: I bill the hours actually used. The estimate usually holds, and if it will not, I tell you before the budget is used up.",
|
||||
prices: [
|
||||
{
|
||||
label: "A typical company website",
|
||||
amount: "from 3,800 EUR",
|
||||
note: "one-off, net",
|
||||
},
|
||||
{ label: "Anything extra, per hour", amount: "100 EUR", note: "net" },
|
||||
],
|
||||
smallPrint:
|
||||
"Hosting and small changes afterwards cost a monthly fee that we agree on before we start. I do not manage email inboxes; they stay with your provider.",
|
||||
cta: GET_IN_TOUCH,
|
||||
},
|
||||
],
|
||||
scenes: {
|
||||
you: "You",
|
||||
me: "Me",
|
||||
talkLength: "about 20 minutes",
|
||||
days: ["Mon", "Tue", "Wed", "Thu", "Fri"],
|
||||
daysNote: "for example three days a week",
|
||||
review: "finished parts go to you for review",
|
||||
week: "week {n}",
|
||||
monthEnd: "end of month",
|
||||
notice: "two weeks notice",
|
||||
repair: "fragile parts get tests and fixes",
|
||||
summaryParts: ["found", "changed", "next"],
|
||||
summaryNote: "written so you can follow it",
|
||||
options: ["continue with me", "your own team", "stop here"],
|
||||
online: "online",
|
||||
exampleDomain: "yourcompany.com",
|
||||
firstVersion: "a first version to click through",
|
||||
approve: "you approve it before it goes live",
|
||||
changeByEmail: "you send an email, I make the change",
|
||||
tools: ["Shop", "Accounting", "Email", "CRM"],
|
||||
byHand: "today: copied by hand",
|
||||
wiring: "connected one step at a time",
|
||||
running: "the steps run without anyone copying",
|
||||
},
|
||||
},
|
||||
automation: {
|
||||
label: "Process automation",
|
||||
title: { lead: "Let the systems", rest: "do the copying." },
|
||||
intro:
|
||||
"In most companies, people move information from one tool to another: read it here, type it in there, send an email about it. Automation connects those tools so that the work passes from one to the next on its own. Three typical examples:",
|
||||
examplesLabel: "Example workflows",
|
||||
running: "running",
|
||||
runComplete: "run complete",
|
||||
log: "Log",
|
||||
byHand: "By hand",
|
||||
automated: "Automated",
|
||||
kinds: {
|
||||
trigger: "Trigger: something happens",
|
||||
rule: "Rule: a fixed check",
|
||||
ai: "AI step: reads or sorts",
|
||||
action: "Action: a system does something",
|
||||
human: "Person: decides the unclear cases",
|
||||
},
|
||||
workflows: {
|
||||
order: {
|
||||
name: "Order to invoice",
|
||||
summary:
|
||||
"A new order arrives in the shop. The invoice, the customer record and the confirmation follow without anyone typing.",
|
||||
byHand:
|
||||
"Someone opens the order, writes the invoice, updates the customer record and sends an email.",
|
||||
automated:
|
||||
"The order triggers everything. People only see the finished result.",
|
||||
nodes: {
|
||||
shop: { system: "Shop", label: "New order", log: "order received" },
|
||||
stock: {
|
||||
system: "Rule",
|
||||
label: "In stock?",
|
||||
log: "stock checked: available",
|
||||
},
|
||||
invoice: {
|
||||
system: "Accounting",
|
||||
label: "Create invoice",
|
||||
log: "invoice created",
|
||||
},
|
||||
crm: {
|
||||
system: "CRM",
|
||||
label: "Update customer",
|
||||
log: "customer record updated",
|
||||
},
|
||||
mail: {
|
||||
system: "Email",
|
||||
label: "Send confirmation",
|
||||
log: "confirmation sent",
|
||||
},
|
||||
},
|
||||
},
|
||||
documents: {
|
||||
name: "Incoming documents",
|
||||
summary:
|
||||
"Invoices arrive as PDF by email. An AI step reads them, a rule checks the result, and a person only sees the unclear ones.",
|
||||
byHand:
|
||||
"Someone opens every PDF, reads supplier, amount and date, and types them into the accounting system.",
|
||||
automated:
|
||||
"Clear cases are booked directly. Unclear ones go to a person with the document attached.",
|
||||
nodes: {
|
||||
inbox: {
|
||||
system: "Inbox",
|
||||
label: "Email with PDF",
|
||||
log: "document received",
|
||||
},
|
||||
read: {
|
||||
system: "AI step",
|
||||
label: "Read the document",
|
||||
log: "supplier, amount and date extracted",
|
||||
},
|
||||
check: { system: "Rule", label: "Plausible?", log: "values checked" },
|
||||
book: {
|
||||
system: "Accounting",
|
||||
label: "Book it",
|
||||
log: "booked automatically",
|
||||
},
|
||||
ask: {
|
||||
system: "A person",
|
||||
label: "Review and decide",
|
||||
log: "unclear: sent to a person for review",
|
||||
},
|
||||
},
|
||||
},
|
||||
lead: {
|
||||
name: "Enquiry to follow-up",
|
||||
summary:
|
||||
"Someone fills in the contact form. The contact is created, the right person is told, and a reminder makes sure nobody forgets.",
|
||||
byHand:
|
||||
"Someone copies the enquiry into the CRM, forwards it by email and hopes the follow-up is not forgotten.",
|
||||
automated:
|
||||
"The enquiry is filed, assigned and followed up on schedule.",
|
||||
nodes: {
|
||||
form: {
|
||||
system: "Website",
|
||||
label: "Contact form",
|
||||
log: "enquiry received",
|
||||
},
|
||||
contact: {
|
||||
system: "CRM",
|
||||
label: "Create contact",
|
||||
log: "contact created",
|
||||
},
|
||||
notify: {
|
||||
system: "Team chat",
|
||||
label: "Notify sales",
|
||||
log: "sales notified",
|
||||
},
|
||||
wait: {
|
||||
system: "Rule",
|
||||
label: "No reply in 3 days?",
|
||||
log: "follow-up scheduled",
|
||||
},
|
||||
remind: {
|
||||
system: "Tasks",
|
||||
label: "Create reminder",
|
||||
log: "reminder created",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
notes: [
|
||||
{
|
||||
title: "Worth automating when",
|
||||
items: [
|
||||
"The same steps repeat every day or every week",
|
||||
"Data is typed from one tool into another",
|
||||
"Mistakes happen because the work is monotonous",
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "What stays with people",
|
||||
items: [
|
||||
"Decisions and exceptions",
|
||||
"Anything a customer should hear from a person",
|
||||
"The final say when the automation is unsure",
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "What I need from you",
|
||||
items: [
|
||||
"Access to the tools and their interfaces (APIs)",
|
||||
"Someone who knows the process as it is done today",
|
||||
"Real examples to test with",
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
technology: {
|
||||
label: "Technology",
|
||||
title: { lead: "Which projects", rest: "I am right for." },
|
||||
fit: [
|
||||
{
|
||||
title: "A good fit",
|
||||
items: [
|
||||
"Web apps: dashboards, portals, internal tools",
|
||||
"Interfaces where design and usability matter",
|
||||
"Company websites, also in several languages",
|
||||
"Web3 frontends: wallets, live data, dashboards",
|
||||
"Automation: connecting systems and their APIs",
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Also possible",
|
||||
items: [
|
||||
"The backend an app needs: APIs, login, integrations",
|
||||
"Command-line tools",
|
||||
"Taking over and tidying up existing code",
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Not my field",
|
||||
items: [
|
||||
"Mobile apps for the app stores",
|
||||
"Windows software",
|
||||
"Running servers, specialised database work",
|
||||
],
|
||||
},
|
||||
],
|
||||
techLabel: "What I work with",
|
||||
tech: [
|
||||
{
|
||||
area: "Interface",
|
||||
items: ["TypeScript", "React", "Next.js", "Vue.js", "Tailwind CSS"],
|
||||
},
|
||||
{
|
||||
area: "Behind it",
|
||||
items: ["Node.js", "REST APIs", "PostgreSQL", "Headless CMS"],
|
||||
},
|
||||
{ area: "Web3 and systems", items: ["Solidity", "Rust"] },
|
||||
{ area: "Delivery", items: ["Automated tests", "Docker", "CI/CD"] },
|
||||
],
|
||||
pipelineTitle: "What happens before a change goes live.",
|
||||
pipelineIntro:
|
||||
"Software is connected: a change in one place can break another. I secure what I build with automated tests, and every change has to pass them before it is released. This is what such a run looks like.",
|
||||
pipeline: {
|
||||
simulate: "Simulate a change in",
|
||||
scenarios: {
|
||||
orders: {
|
||||
label: "Orders",
|
||||
testName: "invoice total matches the discounted order",
|
||||
expected: "119.00",
|
||||
received: "125.00",
|
||||
},
|
||||
login: {
|
||||
label: "Login",
|
||||
testName: "confirmation goes to the account's email address",
|
||||
expected: '"anna@example.com"',
|
||||
received: "undefined",
|
||||
},
|
||||
invoices: {
|
||||
label: "Invoices",
|
||||
testName: "invoice email attaches the PDF",
|
||||
expected: "1 attachment",
|
||||
received: "0 attachments",
|
||||
},
|
||||
emails: {
|
||||
label: "Emails",
|
||||
testName: "order is confirmed once the email is queued",
|
||||
expected: '"confirmed"',
|
||||
received: '"pending"',
|
||||
},
|
||||
},
|
||||
stages: ["Lint", "Types", "Tests", "Deploy"],
|
||||
explanations: [
|
||||
{
|
||||
title: "A change comes in",
|
||||
text: "Something is changed in {area}. Before it can go live, the whole project is checked automatically.",
|
||||
},
|
||||
{
|
||||
title: "A test fails, the release is blocked",
|
||||
text: "The change broke something in a different place. It is caught here, on my side, and nothing reaches your users.",
|
||||
},
|
||||
{
|
||||
title: "Fixed, checked again, released",
|
||||
text: "I repair what broke, the run passes, and only then does the change go live.",
|
||||
},
|
||||
],
|
||||
runAgain: "Run again",
|
||||
run: "run",
|
||||
fixPushed: "fix pushed",
|
||||
lint: "$ lint: checking the code style…",
|
||||
types: "$ types: checking that the parts fit together…",
|
||||
tests: "tests",
|
||||
expected: "expected",
|
||||
received: "received",
|
||||
runningFile: "running {file}…",
|
||||
summaryLabel: "Tests",
|
||||
failed: "{n} failed",
|
||||
passed: "{n} passed",
|
||||
of: "of {total}",
|
||||
pending: "release pending",
|
||||
blocked: "release blocked",
|
||||
released: "released",
|
||||
footnote:
|
||||
"An illustration with made-up file names and numbers, modelled on a real test run. Tests do not catch every mistake, but they do catch this kind.",
|
||||
},
|
||||
},
|
||||
working: {
|
||||
label: "Working with me",
|
||||
title: { lead: "How I work.", rest: "Nothing unusual." },
|
||||
principles: [
|
||||
{
|
||||
title: "One contact",
|
||||
text: "You talk to me, and I am the one who does the work.",
|
||||
},
|
||||
{
|
||||
title: "Hours you can check",
|
||||
text: "I track my time. The monthly invoice shows what the hours were spent on.",
|
||||
},
|
||||
{
|
||||
title: "Careful work",
|
||||
text: "I test what I build before I hand it over, and I say so when something is not finished or I am not sure.",
|
||||
},
|
||||
{
|
||||
title: "What I build is yours",
|
||||
text: "Code and design belong to you. You can hand them to another developer.",
|
||||
},
|
||||
],
|
||||
diagram: {
|
||||
description:
|
||||
"Often a request passes through several people before it reaches the developer. With me there is one direct line between you and the person who does the work.",
|
||||
chain: ["You", "Sales", "Manager", "Designer", "Developer"],
|
||||
you: "You",
|
||||
me: "Me",
|
||||
often: "often",
|
||||
withMe: "with me",
|
||||
oftenLong: "often: a request is passed along",
|
||||
withMeLong: "with me: one direct line",
|
||||
},
|
||||
},
|
||||
work: {
|
||||
label: "My work",
|
||||
title: { lead: "Two sites I built.", rest: "Both are live." },
|
||||
items: {
|
||||
"klz-cables.com": {
|
||||
client: "Cable supplier",
|
||||
text: "The company website of a supplier of energy cables, in German and English.",
|
||||
did: [
|
||||
"Product catalogue with categories and detail pages",
|
||||
"Datasheets generated from the product data",
|
||||
"Content the client can edit in a CMS",
|
||||
],
|
||||
tech: ["Next.js", "TypeScript", "Headless CMS", "Two languages"],
|
||||
},
|
||||
"e-tib.com": {
|
||||
client: "Cable network construction",
|
||||
text: "The company website of a group in cable network construction, in German and English.",
|
||||
did: [
|
||||
"Pages for services, references, careers and events",
|
||||
"The same content in two languages",
|
||||
"Works on phone, tablet and desktop",
|
||||
],
|
||||
tech: ["Next.js", "TypeScript", "Headless CMS", "Two languages"],
|
||||
},
|
||||
},
|
||||
openLive: "Open the live site",
|
||||
desktopAlt: "{name} on a desktop screen",
|
||||
phoneAlt: "{name} on a phone",
|
||||
note: "Besides client work I build my own software, among other things a system in Rust and Solidity that runs on seven blockchains.",
|
||||
},
|
||||
faq: {
|
||||
label: "Questions",
|
||||
title: { lead: "Good to know", rest: "before we talk." },
|
||||
items: [
|
||||
{
|
||||
question: "I am not technical. Is that a problem?",
|
||||
answer:
|
||||
"No. I explain what I am doing in plain language, and you can ask at any time. You do not need to read code.",
|
||||
},
|
||||
{
|
||||
question: "How long am I committed?",
|
||||
answer:
|
||||
"Project work is billed by effort. Either side can end it with two weeks notice to the end of a month.",
|
||||
},
|
||||
{
|
||||
question: "Who owns what you build?",
|
||||
answer:
|
||||
"You do. Code and design belong to you, and you can hand them to another developer.",
|
||||
},
|
||||
{
|
||||
question: "Can you work with our existing team and code?",
|
||||
answer:
|
||||
"Yes. I worked in agencies and product teams for years, so joining an existing project is familiar to me.",
|
||||
},
|
||||
{
|
||||
question: "How do I reach you?",
|
||||
answer:
|
||||
"By email or video call. I work remotely and am available during European business hours.",
|
||||
},
|
||||
{
|
||||
question: "What do you not do?",
|
||||
answer:
|
||||
"Mobile apps for the app stores, Windows software and running your servers. If your project needs those, I say so in the first call.",
|
||||
},
|
||||
],
|
||||
},
|
||||
contact: {
|
||||
headline: [
|
||||
{ text: "LET'S", tone: "bits" },
|
||||
{ text: "TALK.", tone: "solid" },
|
||||
],
|
||||
lead: "Pick what you need and the email writes itself. Or write to me directly:",
|
||||
newMessage: "New message",
|
||||
draft: "draft",
|
||||
need: "I need",
|
||||
to: "to",
|
||||
subject: "subject",
|
||||
open: "Open in my mail app",
|
||||
copy: "Copy address",
|
||||
copied: "Copied",
|
||||
// The square brackets are blanks for the visitor to fill in.
|
||||
topics: [
|
||||
{
|
||||
id: "developer",
|
||||
label: "A developer",
|
||||
draft: {
|
||||
subject: "A developer for our product",
|
||||
body: `Hi Marc,\n\nwe are working on [what you are building] and need help with [what is next on your list].\n\nA first call would suit us around [date or week].\n\n${SIGN_OFF}`,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "rescue",
|
||||
label: "A reliable app",
|
||||
draft: {
|
||||
subject: "Our app needs to become reliable",
|
||||
body: `Hi Marc,\n\nour app [name or short description] works, but [what keeps going wrong].\n\nCould you take a look?\n\n${SIGN_OFF}`,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "automation",
|
||||
label: "Automation",
|
||||
draft: {
|
||||
subject: "We want to automate a process",
|
||||
body: `Hi Marc,\n\nwe would like to automate [the process]. Today it is done by hand in [the tools involved].\n\nCan we talk about it?\n\n${SIGN_OFF}`,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "website",
|
||||
label: "A website",
|
||||
draft: {
|
||||
subject: "A website for my business",
|
||||
body: `Hi Marc,\n\nI need a website for [your business]. It should [what it is for].\n\nWhen could we talk?\n\n${SIGN_OFF}`,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "other",
|
||||
label: "Something else",
|
||||
draft: {
|
||||
subject: "A question",
|
||||
body: `Hi Marc,\n\n[what you have in mind]\n\n${SIGN_OFF}`,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
notFound: {
|
||||
kicker: "Error 404",
|
||||
title: "Page not found.",
|
||||
text: "This address does not exist. The page may have been moved or removed.",
|
||||
home: "Back to the homepage",
|
||||
},
|
||||
error: {
|
||||
kicker: "Error 500",
|
||||
title: "Something went wrong.",
|
||||
text: "An unexpected problem occurred. It has been logged.",
|
||||
retry: "Try again",
|
||||
home: "Back to the homepage",
|
||||
},
|
||||
};
|
||||
@@ -1,38 +0,0 @@
|
||||
export type FaqItem = {
|
||||
readonly question: string;
|
||||
readonly answer: string;
|
||||
};
|
||||
|
||||
/** Shown in the questions section and passed to search engines as structured data. */
|
||||
export const FAQ: readonly FaqItem[] = [
|
||||
{
|
||||
question: "I am not technical. Is that a problem?",
|
||||
answer:
|
||||
"No. I explain what I am doing in plain language, and you can ask at any time. You do not need to read code.",
|
||||
},
|
||||
{
|
||||
question: "How long am I committed?",
|
||||
answer:
|
||||
"Project work is billed by effort. Either side can end it with two weeks notice to the end of a month.",
|
||||
},
|
||||
{
|
||||
question: "Who owns what you build?",
|
||||
answer:
|
||||
"You do. Code and design belong to you, and you can hand them to another developer.",
|
||||
},
|
||||
{
|
||||
question: "Can you work with our existing team and code?",
|
||||
answer:
|
||||
"Yes. I worked in agencies and product teams for years, so joining an existing project is familiar to me.",
|
||||
},
|
||||
{
|
||||
question: "How do I reach you?",
|
||||
answer:
|
||||
"By email or video call. I work remotely and am available during European business hours.",
|
||||
},
|
||||
{
|
||||
question: "What do you not do?",
|
||||
answer:
|
||||
"Mobile apps for the app stores, Windows software and running your servers. If your project needs those, I say so in the first call.",
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,9 @@
|
||||
import type { Locale } from "@/src/domain/locale";
|
||||
import { de } from "./de";
|
||||
import { en } from "./en";
|
||||
import type { Content } from "./types";
|
||||
|
||||
const CONTENT: Readonly<Record<Locale, Content>> = { en, de };
|
||||
|
||||
/** The texts of the site in one language. */
|
||||
export const contentFor = (locale: Locale): Content => CONTENT[locale];
|
||||
@@ -0,0 +1,93 @@
|
||||
import { EmailLink } from "@/src/components/EmailLink";
|
||||
import { LegalSection } from "@/src/components/LegalPage";
|
||||
import { OWNER } from "@/src/content/site";
|
||||
import type { Locale } from "@/src/domain/locale";
|
||||
import type { LegalText } from "./types";
|
||||
|
||||
const LINK = "font-bold text-slate-900 underline-offset-4 hover:underline";
|
||||
|
||||
const Address: React.FC<{ country: string }> = ({ country }) => (
|
||||
<p>
|
||||
{OWNER.name}
|
||||
<br />
|
||||
{OWNER.street}
|
||||
<br />
|
||||
{OWNER.postalCode} {OWNER.locality}
|
||||
<br />
|
||||
{country}
|
||||
</p>
|
||||
);
|
||||
|
||||
/** The imprint (section 5 DDG) in both languages. */
|
||||
export const IMPRINT: Readonly<Record<Locale, LegalText>> = {
|
||||
de: {
|
||||
title: "Impressum",
|
||||
description: "Impressum von mintel.me.",
|
||||
Body: () => (
|
||||
<>
|
||||
<LegalSection heading="Angaben gemäß § 5 DDG">
|
||||
<Address country="Deutschland" />
|
||||
</LegalSection>
|
||||
|
||||
<LegalSection heading="Kontakt">
|
||||
<p>
|
||||
E-Mail: <EmailLink className={LINK} />
|
||||
</p>
|
||||
</LegalSection>
|
||||
|
||||
<LegalSection heading="Umsatzsteuer-Identifikationsnummer">
|
||||
<p>
|
||||
Umsatzsteuer-Identifikationsnummer gemäß § 27a Umsatzsteuergesetz:{" "}
|
||||
{OWNER.vatId}
|
||||
</p>
|
||||
</LegalSection>
|
||||
|
||||
<LegalSection heading="Verantwortlich für den Inhalt">
|
||||
<p>{OWNER.name}, Anschrift wie oben (§ 18 Abs. 2 MStV).</p>
|
||||
</LegalSection>
|
||||
|
||||
<LegalSection heading="Verbraucherstreitbeilegung">
|
||||
<p>
|
||||
Ich bin weder bereit noch verpflichtet, an Streitbeilegungsverfahren
|
||||
vor einer Verbraucherschlichtungsstelle teilzunehmen.
|
||||
</p>
|
||||
</LegalSection>
|
||||
</>
|
||||
),
|
||||
},
|
||||
en: {
|
||||
title: "Imprint",
|
||||
description: "Legal notice (Impressum) for mintel.me.",
|
||||
Body: () => (
|
||||
<>
|
||||
<LegalSection heading="Information according to section 5 DDG">
|
||||
<Address country="Germany" />
|
||||
</LegalSection>
|
||||
|
||||
<LegalSection heading="Contact">
|
||||
<p>
|
||||
Email: <EmailLink className={LINK} />
|
||||
</p>
|
||||
</LegalSection>
|
||||
|
||||
<LegalSection heading="VAT identification number">
|
||||
<p>
|
||||
VAT identification number according to section 27a of the German VAT
|
||||
Act (Umsatzsteuergesetz): {OWNER.vatId}
|
||||
</p>
|
||||
</LegalSection>
|
||||
|
||||
<LegalSection heading="Responsible for the content">
|
||||
<p>{OWNER.name}, address as above (section 18 paragraph 2 MStV).</p>
|
||||
</LegalSection>
|
||||
|
||||
<LegalSection heading="Consumer dispute resolution">
|
||||
<p>
|
||||
I am neither willing nor obliged to take part in dispute resolution
|
||||
proceedings before a consumer arbitration board.
|
||||
</p>
|
||||
</LegalSection>
|
||||
</>
|
||||
),
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,235 @@
|
||||
import { EmailLink } from "@/src/components/EmailLink";
|
||||
import { LegalSection } from "@/src/components/LegalPage";
|
||||
import { OWNER } from "@/src/content/site";
|
||||
import { LOCALE_COOKIE, type Locale } from "@/src/domain/locale";
|
||||
import type { LegalText } from "./types";
|
||||
|
||||
const LINK = "font-bold text-slate-900 underline-offset-4 hover:underline";
|
||||
|
||||
const address = (country: string): string =>
|
||||
`${OWNER.name}, ${OWNER.street}, ${OWNER.postalCode} ${OWNER.locality}, ${country}.`;
|
||||
|
||||
/**
|
||||
* The privacy notice in both languages. It describes what the code actually
|
||||
* processes; keep it in step with the code (the language cookie is set by
|
||||
* LanguageSwitch, the detection happens in proxy.ts).
|
||||
*/
|
||||
export const PRIVACY: Readonly<Record<Locale, LegalText>> = {
|
||||
de: {
|
||||
title: "Datenschutz",
|
||||
description: "Wie mintel.me mit personenbezogenen Daten umgeht.",
|
||||
Body: () => (
|
||||
<>
|
||||
<LegalSection heading="Verantwortlicher">
|
||||
<p>
|
||||
{address("Deutschland")}
|
||||
<br />
|
||||
E-Mail: <EmailLink className={LINK} />
|
||||
</p>
|
||||
</LegalSection>
|
||||
|
||||
<LegalSection heading="Kurz gesagt">
|
||||
<p>
|
||||
Diese Website zeigt keine Werbung und bindet keine Inhalte von
|
||||
Dritten ein. Schriften und Bilder werden von dieser Website selbst
|
||||
ausgeliefert. Ein Cookie wird nur gesetzt, wenn Sie die Sprache
|
||||
umschalten (siehe „Sprache“).
|
||||
</p>
|
||||
</LegalSection>
|
||||
|
||||
<LegalSection heading="Server-Protokolle">
|
||||
<p>
|
||||
Wenn Sie eine Seite öffnen, verarbeitet der Server technische Daten,
|
||||
die Ihr Browser sendet: Ihre IP-Adresse, den Zeitpunkt, die
|
||||
angeforderte Seite und den Browsertyp. Das ist nötig, um die Website
|
||||
auszuliefern und sicher zu betreiben. Rechtsgrundlage ist mein
|
||||
berechtigtes Interesse am Betrieb der Website (Art. 6 Abs. 1 lit. f
|
||||
DSGVO).
|
||||
</p>
|
||||
</LegalSection>
|
||||
|
||||
<LegalSection heading="Sprache">
|
||||
<p>
|
||||
Die Website gibt es auf Deutsch und Englisch. Welche Sprache Sie
|
||||
zuerst sehen, richtet sich nach der Spracheinstellung, die Ihr
|
||||
Browser bei jedem Seitenaufruf mitsendet. Sie wird dafür nicht
|
||||
gespeichert.
|
||||
</p>
|
||||
<p>
|
||||
Wenn Sie die Sprache selbst umschalten, speichert die Website Ihre
|
||||
Wahl in einem Cookie namens „{LOCALE_COOKIE}“, damit Sie nicht
|
||||
wieder zurückgeleitet werden. Es enthält nur das Sprachkürzel („de“
|
||||
oder „en“), gilt ein Jahr und wird für nichts anderes verwendet. Es
|
||||
ist für die von Ihnen gewünschte Funktion erforderlich (§ 25 Abs. 2
|
||||
Nr. 2 TDDDG).
|
||||
</p>
|
||||
</LegalSection>
|
||||
|
||||
<LegalSection heading="Besucherstatistik">
|
||||
<p>
|
||||
Ich zähle Seitenaufrufe mit Umami, das ich auf meinem eigenen Server
|
||||
betreibe. Es arbeitet ohne Cookies und erstellt keine Profile
|
||||
einzelner Besucher. Die Daten werden nicht an Dritte weitergegeben.
|
||||
Rechtsgrundlage ist mein berechtigtes Interesse zu verstehen, wie
|
||||
die Website genutzt wird (Art. 6 Abs. 1 lit. f DSGVO).
|
||||
</p>
|
||||
</LegalSection>
|
||||
|
||||
<LegalSection heading="Fehlerberichte">
|
||||
<p>
|
||||
Wenn auf dieser Website technisch etwas fehlschlägt, wird ein
|
||||
Fehlerbericht an einen Dienst zur Fehlererfassung gesendet, den ich
|
||||
auf meinem eigenen Server betreibe. Er enthält technische Angaben
|
||||
wie die Seite, den Browser und die Fehlermeldung. Sitzungen werden
|
||||
nicht aufgezeichnet. Rechtsgrundlage ist mein berechtigtes Interesse
|
||||
an einer funktionierenden Website (Art. 6 Abs. 1 lit. f DSGVO).
|
||||
</p>
|
||||
</LegalSection>
|
||||
|
||||
<LegalSection heading="Speicherung in Ihrem Browser">
|
||||
<p>
|
||||
Wenn das Laden einer Seite fehlschlägt, speichert die Website einen
|
||||
Zeitstempel im Session Storage Ihres Browsers, damit sie nur einmal
|
||||
neu lädt. Er wird gelöscht, wenn Sie den Tab schließen, und
|
||||
nirgendwohin übertragen.
|
||||
</p>
|
||||
</LegalSection>
|
||||
|
||||
<LegalSection heading="Wenn Sie mir schreiben">
|
||||
<p>
|
||||
Wenn Sie mir eine E-Mail senden, verarbeite ich Ihre Nachricht und
|
||||
Ihre Adresse, um Ihnen zu antworten und gegebenenfalls einen Vertrag
|
||||
vorzubereiten (Art. 6 Abs. 1 lit. b DSGVO). Ich bewahre die
|
||||
Korrespondenz so lange auf, wie es dafür nötig ist oder das Gesetz
|
||||
es verlangt.
|
||||
</p>
|
||||
</LegalSection>
|
||||
|
||||
<LegalSection heading="Ihre Rechte">
|
||||
<p>Sie haben das Recht,</p>
|
||||
<ul>
|
||||
<li>
|
||||
Auskunft darüber zu verlangen, welche Daten ich über Sie habe
|
||||
</li>
|
||||
<li>unrichtige Daten berichtigen zu lassen</li>
|
||||
<li>
|
||||
Ihre Daten löschen oder deren Verarbeitung einschränken zu lassen
|
||||
</li>
|
||||
<li>
|
||||
einer Verarbeitung zu widersprechen, die auf berechtigtem
|
||||
Interesse beruht
|
||||
</li>
|
||||
<li>Ihre Daten in einem übertragbaren Format zu erhalten</li>
|
||||
<li>sich bei einer Datenschutz-Aufsichtsbehörde zu beschweren</li>
|
||||
</ul>
|
||||
<p>
|
||||
Um eines dieser Rechte auszuüben, schreiben Sie an{" "}
|
||||
<EmailLink className={LINK} />.
|
||||
</p>
|
||||
</LegalSection>
|
||||
</>
|
||||
),
|
||||
},
|
||||
en: {
|
||||
title: "Privacy",
|
||||
description: "How mintel.me handles personal data.",
|
||||
Body: () => (
|
||||
<>
|
||||
<LegalSection heading="Who is responsible">
|
||||
<p>
|
||||
{address("Germany")}
|
||||
<br />
|
||||
Email: <EmailLink className={LINK} />
|
||||
</p>
|
||||
</LegalSection>
|
||||
|
||||
<LegalSection heading="In short">
|
||||
<p>
|
||||
This website shows no advertising and includes no content from third
|
||||
parties. Fonts and images are served from this website itself. A
|
||||
cookie is only set when you switch the language (see
|
||||
"Language").
|
||||
</p>
|
||||
</LegalSection>
|
||||
|
||||
<LegalSection heading="Server logs">
|
||||
<p>
|
||||
When you open a page, the server processes technical data that your
|
||||
browser sends: your IP address, the time, the page requested and the
|
||||
browser type. This is necessary to deliver the website and to keep
|
||||
it secure. The legal basis is my legitimate interest in running the
|
||||
website (Article 6(1)(f) GDPR).
|
||||
</p>
|
||||
</LegalSection>
|
||||
|
||||
<LegalSection heading="Language">
|
||||
<p>
|
||||
The website exists in German and English. Which language you see
|
||||
first depends on the language setting your browser sends with every
|
||||
page request. It is not stored for that.
|
||||
</p>
|
||||
<p>
|
||||
If you switch the language yourself, the website stores your choice
|
||||
in a cookie named "{LOCALE_COOKIE}" so that you are not
|
||||
sent back. It contains only the language code ("de" or
|
||||
"en"), lasts one year and is used for nothing else. It is
|
||||
necessary for the function you asked for (section 25(2) no. 2
|
||||
TDDDG).
|
||||
</p>
|
||||
</LegalSection>
|
||||
|
||||
<LegalSection heading="Visitor statistics">
|
||||
<p>
|
||||
I count page views with Umami, which I run on my own server. It
|
||||
works without cookies and does not create profiles of individual
|
||||
visitors. The data is not passed on to third parties. The legal
|
||||
basis is my legitimate interest in understanding how the website is
|
||||
used (Article 6(1)(f) GDPR).
|
||||
</p>
|
||||
</LegalSection>
|
||||
|
||||
<LegalSection heading="Error reports">
|
||||
<p>
|
||||
If something on this website fails technically, an error report is
|
||||
sent to an error tracking service that I run on my own server. It
|
||||
contains technical details such as the page, the browser and the
|
||||
error message. Sessions are not recorded. The legal basis is my
|
||||
legitimate interest in a working website (Article 6(1)(f) GDPR).
|
||||
</p>
|
||||
</LegalSection>
|
||||
|
||||
<LegalSection heading="Storage in your browser">
|
||||
<p>
|
||||
If loading a page fails, the website stores a timestamp in your
|
||||
browser's session storage so that it reloads only once. It is
|
||||
deleted when you close the tab and is not sent anywhere.
|
||||
</p>
|
||||
</LegalSection>
|
||||
|
||||
<LegalSection heading="When you write to me">
|
||||
<p>
|
||||
If you send me an email, I process what you write and your address
|
||||
in order to answer you and, if it comes to that, to prepare a
|
||||
contract (Article 6(1)(b) GDPR). I keep the correspondence for as
|
||||
long as it is needed for that purpose or as the law requires.
|
||||
</p>
|
||||
</LegalSection>
|
||||
|
||||
<LegalSection heading="Your rights">
|
||||
<p>You have the right to:</p>
|
||||
<ul>
|
||||
<li>ask what data I hold about you</li>
|
||||
<li>have incorrect data corrected</li>
|
||||
<li>have your data deleted or its processing restricted</li>
|
||||
<li>object to processing that is based on legitimate interest</li>
|
||||
<li>receive your data in a portable format</li>
|
||||
<li>complain to a data protection supervisory authority</li>
|
||||
</ul>
|
||||
<p>
|
||||
To use any of these rights, write to <EmailLink className={LINK} />.
|
||||
</p>
|
||||
</LegalSection>
|
||||
</>
|
||||
),
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
/** A legal page in one language: its title, its description and its text. */
|
||||
export type LegalText = {
|
||||
readonly title: string;
|
||||
readonly description: string;
|
||||
readonly Body: React.FC;
|
||||
};
|
||||
@@ -1,209 +0,0 @@
|
||||
/**
|
||||
* The three ways a visitor can work with me, written for the visitor, not for engineers.
|
||||
* Each path answers the same four questions: what you get, how it runs, what it costs, what to do next.
|
||||
*/
|
||||
|
||||
export type PathStep = {
|
||||
readonly title: string;
|
||||
readonly text: string;
|
||||
};
|
||||
|
||||
export type PathPrice = {
|
||||
readonly label: string;
|
||||
readonly amount: string;
|
||||
readonly note: string;
|
||||
};
|
||||
|
||||
export type ServicePath = {
|
||||
readonly id: string;
|
||||
/** The service in neutral words, as search engines list it. */
|
||||
readonly service: string;
|
||||
/** How the visitor would say it. */
|
||||
readonly need: string;
|
||||
readonly forWhom: string;
|
||||
readonly headline: string;
|
||||
readonly intro: string;
|
||||
readonly youGet: readonly string[];
|
||||
readonly steps: readonly PathStep[];
|
||||
/** How this kind of work is priced, in one line, and why. */
|
||||
readonly pricingModel: string;
|
||||
readonly pricingText: string;
|
||||
readonly prices: readonly PathPrice[];
|
||||
readonly smallPrint: string;
|
||||
readonly cta: string;
|
||||
};
|
||||
|
||||
export const SERVICE_PATHS: readonly ServicePath[] = [
|
||||
{
|
||||
id: "developer",
|
||||
service: "Web app and frontend development",
|
||||
need: "We need a developer for our product",
|
||||
forWhom: "Startups, product teams, agencies, Web3 teams",
|
||||
headline: "An experienced developer who joins your project.",
|
||||
intro:
|
||||
"You have a product and more work than your team can handle. I work on agreed days of the week on what is next on your list: new features, the interface, or the parts nobody gets around to.",
|
||||
youGet: [
|
||||
"Web apps, dashboards, customer portals and internal tools",
|
||||
"Interfaces with attention to design and usability",
|
||||
"Web3 frontends: wallet connection, live data, dashboards",
|
||||
"Small tools and automation for recurring work",
|
||||
],
|
||||
steps: [
|
||||
{
|
||||
title: "A first call, about 20 minutes",
|
||||
text: "You tell me what you are building and where you need help. If I am not the right person for it, I say so.",
|
||||
},
|
||||
{
|
||||
title: "We agree on the days",
|
||||
text: "How many days a week I work for you, and what should come first.",
|
||||
},
|
||||
{
|
||||
title: "I work through the list",
|
||||
text: "Finished parts go to you for review as they are done. I track my hours and you can see what they were spent on.",
|
||||
},
|
||||
{
|
||||
title: "Two weeks notice",
|
||||
text: "The agreement can be ended with two weeks notice to the end of a month.",
|
||||
},
|
||||
],
|
||||
pricingModel: "By effort, for as long as your project runs",
|
||||
pricingText:
|
||||
"I work on your project on agreed days of the week and bill the hours I actually worked. There is no package to buy and no minimum term.",
|
||||
prices: [
|
||||
{ label: "Per hour", amount: "100 EUR", note: "net" },
|
||||
{ label: "Per day", amount: "800 EUR", note: "8 hours, net" },
|
||||
],
|
||||
smallPrint:
|
||||
"One invoice per month, with the tracked hours. Either side can end it with two weeks notice to the end of a month.",
|
||||
cta: "Get in touch",
|
||||
},
|
||||
{
|
||||
id: "rescue",
|
||||
service: "Code review and stabilisation of web apps",
|
||||
need: "Our app needs to become reliable",
|
||||
forWhom: "Teams whose app was built quickly and is now hard to change",
|
||||
headline: "A week to look at your app and fix what is most urgent.",
|
||||
intro:
|
||||
"The app works, but bugs come back and small changes take longer than they should. I take a week to go through the code, fix what I can in that time and write down what I would do next.",
|
||||
youGet: [
|
||||
"An honest assessment: what is solid and what is fragile",
|
||||
"Automated tests for the most important parts",
|
||||
"The most urgent problems fixed, as far as a week allows",
|
||||
"A written list of next steps, in plain language",
|
||||
],
|
||||
steps: [
|
||||
{
|
||||
title: "A first call, about 20 minutes",
|
||||
text: "You show me the app and tell me where it causes trouble.",
|
||||
},
|
||||
{
|
||||
title: "One week of work",
|
||||
text: "I read the code, add tests where they matter most and fix the most urgent problems.",
|
||||
},
|
||||
{
|
||||
title: "A written summary",
|
||||
text: "What I found, what I changed and what I recommend. Written so that you can follow it without being a developer.",
|
||||
},
|
||||
{
|
||||
title: "You decide how to continue",
|
||||
text: "With me, with your own team, or not at all.",
|
||||
},
|
||||
],
|
||||
pricingModel: "A fixed week at a fixed price",
|
||||
pricingText:
|
||||
"The scope is one week of my time. You know the cost before I start.",
|
||||
prices: [{ label: "One week, 35 hours", amount: "3,500 EUR", note: "net" }],
|
||||
smallPrint:
|
||||
"A week does not fix everything. If I see in the first call that it will not be enough to be useful, I tell you.",
|
||||
cta: "Get in touch",
|
||||
},
|
||||
{
|
||||
id: "automation",
|
||||
service: "Process automation and system integration",
|
||||
need: "We want to automate a process",
|
||||
forWhom:
|
||||
"Companies where people copy data between tools or repeat the same steps",
|
||||
headline:
|
||||
"Systems that pass work on to each other, so people do not have to.",
|
||||
intro:
|
||||
"Someone exports a list here, types it in there and sends an email about it. I connect the tools you already use, so that recurring steps run on their own and your people can do the work that needs them.",
|
||||
youGet: [
|
||||
"Connections between your systems through their interfaces (APIs)",
|
||||
"Recurring workflows that run without manual steps",
|
||||
"Where it helps, steps handled with AI: sorting, summarising, reading documents",
|
||||
"A short description of how it works, so it can be maintained",
|
||||
],
|
||||
steps: [
|
||||
{
|
||||
title: "A first call, about 20 minutes",
|
||||
text: "You describe the process and which tools are involved.",
|
||||
},
|
||||
{
|
||||
title: "I look at how it is done today",
|
||||
text: "Step by step, with the people who do it. Often a part can be automated, not everything.",
|
||||
},
|
||||
{
|
||||
title: "I connect the systems",
|
||||
text: "One step at a time. You test it with real cases before it replaces the manual work.",
|
||||
},
|
||||
{
|
||||
title: "It runs on its own",
|
||||
text: "You get a note on what it does and what to check when one of the tools changes.",
|
||||
},
|
||||
],
|
||||
pricingModel: "An estimate first, then a budget",
|
||||
pricingText:
|
||||
"Once I have seen the process I estimate the hours. That estimate becomes the budget. I bill the hours I actually needed and tell you early if it will not be enough.",
|
||||
prices: [{ label: "Per hour", amount: "100 EUR", note: "net" }],
|
||||
smallPrint:
|
||||
"How long it takes depends on the process, so there is no price before I have seen it. Fees for the tools themselves are paid by you.",
|
||||
cta: "Get in touch",
|
||||
},
|
||||
{
|
||||
id: "website",
|
||||
service: "Website design and development",
|
||||
need: "I just need a website",
|
||||
forWhom: "Companies and self-employed people who want it taken care of",
|
||||
headline: "A website, built and looked after for you.",
|
||||
intro:
|
||||
"You do not want to learn a website builder or deal with hosting. You tell me what your business does and what the site should achieve. I design it, build it and put it online.",
|
||||
youGet: [
|
||||
"A design made for your business, not a template",
|
||||
"Works on phone, tablet and desktop",
|
||||
"Hosting taken care of, no server for you to manage",
|
||||
"Changes by email: you write what you need, I make the change",
|
||||
],
|
||||
steps: [
|
||||
{
|
||||
title: "A first call, about 20 minutes",
|
||||
text: "You tell me about your business and what the website is for.",
|
||||
},
|
||||
{
|
||||
title: "A design to look at",
|
||||
text: "You get a first version to click through. We adjust it until it fits.",
|
||||
},
|
||||
{
|
||||
title: "I build it and put it online",
|
||||
text: "With your texts and images and a contact form. You approve it before it goes live.",
|
||||
},
|
||||
{
|
||||
title: "Changes afterwards",
|
||||
text: "A new photo or a changed price: you send me an email.",
|
||||
},
|
||||
],
|
||||
pricingModel: "An estimate first, then a budget",
|
||||
pricingText:
|
||||
"After the first call you get an estimate. It becomes your budget: I bill the hours actually used. The estimate usually holds, and if it will not, I tell you before the budget is used up.",
|
||||
prices: [
|
||||
{
|
||||
label: "A typical company website",
|
||||
amount: "from 3,800 EUR",
|
||||
note: "one-off, net",
|
||||
},
|
||||
{ label: "Anything extra, per hour", amount: "100 EUR", note: "net" },
|
||||
],
|
||||
smallPrint:
|
||||
"Hosting and small changes afterwards cost a monthly fee that we agree on before we start. I do not manage email inboxes; they stay with your provider.",
|
||||
cta: "Get in touch",
|
||||
},
|
||||
];
|
||||
@@ -1,17 +1,12 @@
|
||||
import { parseEmailParts, type EmailParts } from "@/src/domain/email";
|
||||
import { indexingFor } from "@/src/domain/indexing";
|
||||
|
||||
// What is the same in every language. Titles, descriptions and all other texts
|
||||
// live in en.ts and de.ts.
|
||||
|
||||
export const SITE_NAME = "Marc Mintel";
|
||||
export const SITE_URL = "https://mintel.me";
|
||||
|
||||
/** One wording for the browser title, search results and link previews. */
|
||||
export const SITE_TITLE =
|
||||
"Freelance web developer for web apps & websites | Marc Mintel";
|
||||
export const SITE_DESCRIPTION =
|
||||
"Freelance web developer with 15 years of experience. I build web apps, interfaces and websites with React, Next.js and TypeScript, and automate processes.";
|
||||
export const SITE_LANGUAGE = "en";
|
||||
export const JOB_TITLE = "Freelance web developer";
|
||||
|
||||
/**
|
||||
* Public profiles of the owner (LinkedIn, GitHub, ...). Search engines use them to
|
||||
* connect this site with the same person elsewhere. Only add profiles the owner confirmed.
|
||||
@@ -39,7 +34,8 @@ export const CONTACT_EMAIL_PARTS: EmailParts = parsedEmail.value;
|
||||
export const OWNER = {
|
||||
name: "Marc Mintel",
|
||||
street: "Georg-Meistermann-Straße 7",
|
||||
city: "54586 Schüller",
|
||||
country: "Germany",
|
||||
postalCode: "54586",
|
||||
locality: "Schüller",
|
||||
countryCode: "DE",
|
||||
vatId: "DE367588065",
|
||||
} as const;
|
||||
|
||||
@@ -0,0 +1,302 @@
|
||||
import type { MailDraft } from "@/src/domain/email";
|
||||
import type { Locale } from "@/src/domain/locale";
|
||||
|
||||
/**
|
||||
* Everything a visitor can read, as plain data. There is one `Content` per
|
||||
* language (see en.ts and de.ts); components take their texts from it and hold
|
||||
* none of their own. Blanks such as "{name}" are filled with `fill` from
|
||||
* src/domain/template.ts.
|
||||
*/
|
||||
|
||||
/** A headline in two tones: the lead in full ink, the rest muted. */
|
||||
export type SplitTitle = {
|
||||
readonly lead: string;
|
||||
readonly rest: string;
|
||||
};
|
||||
|
||||
export type HeadlineLine = {
|
||||
readonly text: string;
|
||||
readonly tone: "solid" | "bits";
|
||||
};
|
||||
|
||||
export type TitledList = {
|
||||
readonly title: string;
|
||||
readonly items: readonly string[];
|
||||
};
|
||||
|
||||
export type TitledText = {
|
||||
readonly title: string;
|
||||
readonly text: string;
|
||||
};
|
||||
|
||||
export type CareerStation = {
|
||||
readonly years: string;
|
||||
readonly role: string;
|
||||
readonly place: string;
|
||||
};
|
||||
|
||||
export type PathPrice = {
|
||||
readonly label: string;
|
||||
readonly amount: string;
|
||||
readonly note: string;
|
||||
};
|
||||
|
||||
export type ServicePathId = "developer" | "rescue" | "automation" | "website";
|
||||
|
||||
export type ServicePath = {
|
||||
readonly id: ServicePathId;
|
||||
/** The service in neutral words, as search engines list it. */
|
||||
readonly service: string;
|
||||
/** How the visitor would say it. */
|
||||
readonly need: string;
|
||||
readonly forWhom: string;
|
||||
readonly headline: string;
|
||||
readonly intro: string;
|
||||
readonly youGet: readonly string[];
|
||||
readonly steps: readonly TitledText[];
|
||||
/** How this kind of work is priced, in one line, and why. */
|
||||
readonly pricingModel: string;
|
||||
readonly pricingText: string;
|
||||
readonly prices: readonly PathPrice[];
|
||||
readonly smallPrint: string;
|
||||
readonly cta: string;
|
||||
};
|
||||
|
||||
/** The short labels inside the drawings that explain the steps of a path. */
|
||||
export type SceneLabels = {
|
||||
readonly you: string;
|
||||
readonly me: string;
|
||||
readonly talkLength: string;
|
||||
readonly days: readonly string[];
|
||||
readonly daysNote: string;
|
||||
readonly review: string;
|
||||
/** Has the blank {n}. */
|
||||
readonly week: string;
|
||||
readonly monthEnd: string;
|
||||
readonly notice: string;
|
||||
readonly repair: string;
|
||||
readonly summaryParts: readonly string[];
|
||||
readonly summaryNote: string;
|
||||
readonly options: readonly string[];
|
||||
readonly online: string;
|
||||
readonly exampleDomain: string;
|
||||
readonly firstVersion: string;
|
||||
readonly approve: string;
|
||||
readonly changeByEmail: string;
|
||||
readonly tools: readonly string[];
|
||||
readonly byHand: string;
|
||||
readonly wiring: string;
|
||||
readonly running: string;
|
||||
};
|
||||
|
||||
export type NodeKind = "trigger" | "rule" | "ai" | "action" | "human";
|
||||
|
||||
export type FlowNodeCopy = {
|
||||
readonly system: string;
|
||||
readonly label: string;
|
||||
/** What appears in the log when a run reaches this node. */
|
||||
readonly log: string;
|
||||
};
|
||||
|
||||
export type WorkflowCopy = {
|
||||
readonly name: string;
|
||||
readonly summary: string;
|
||||
readonly byHand: string;
|
||||
readonly automated: string;
|
||||
/** Texts of the nodes, by node id (see src/content/automation.ts). */
|
||||
readonly nodes: Readonly<Record<string, FlowNodeCopy>>;
|
||||
};
|
||||
|
||||
export type PipelineScenarioCopy = {
|
||||
readonly label: string;
|
||||
readonly testName: string;
|
||||
readonly expected: string;
|
||||
readonly received: string;
|
||||
};
|
||||
|
||||
export type WorkCopy = {
|
||||
readonly client: string;
|
||||
readonly text: string;
|
||||
readonly did: readonly string[];
|
||||
readonly tech: readonly string[];
|
||||
};
|
||||
|
||||
export type FaqItem = {
|
||||
readonly question: string;
|
||||
readonly answer: string;
|
||||
};
|
||||
|
||||
export type ContactTopic = {
|
||||
readonly id: string;
|
||||
readonly label: string;
|
||||
readonly draft: MailDraft;
|
||||
};
|
||||
|
||||
export type Content = {
|
||||
readonly site: {
|
||||
/** One wording for the browser title, search results and link previews. */
|
||||
readonly title: string;
|
||||
readonly description: string;
|
||||
readonly jobTitle: string;
|
||||
/** Open Graph form of the language, such as en_US. */
|
||||
readonly ogLocale: string;
|
||||
readonly shareImageAlt: string;
|
||||
readonly knowsAbout: readonly string[];
|
||||
readonly catalogName: string;
|
||||
};
|
||||
readonly nav: {
|
||||
readonly contact: string;
|
||||
readonly contactLong: string;
|
||||
readonly logoLabel: string;
|
||||
readonly imprint: string;
|
||||
readonly privacy: string;
|
||||
readonly backToTop: string;
|
||||
readonly language: string;
|
||||
/** Each language in its own words, whatever the page language is. */
|
||||
readonly languageNames: Readonly<Record<Locale, string>>;
|
||||
};
|
||||
readonly hero: {
|
||||
readonly headline: readonly HeadlineLine[];
|
||||
readonly lead: string;
|
||||
readonly leadStrong: string;
|
||||
readonly primaryCta: string;
|
||||
readonly secondaryCta: string;
|
||||
readonly trust: readonly string[];
|
||||
readonly builtAndRunning: string;
|
||||
};
|
||||
readonly hello: {
|
||||
readonly label: string;
|
||||
readonly title: string;
|
||||
readonly lead: string;
|
||||
readonly paragraphs: readonly string[];
|
||||
readonly portraitAlt: string;
|
||||
readonly careerLabel: string;
|
||||
readonly career: readonly CareerStation[];
|
||||
};
|
||||
readonly services: {
|
||||
readonly label: string;
|
||||
readonly title: SplitTitle;
|
||||
readonly tablistLabel: string;
|
||||
readonly youGet: string;
|
||||
readonly howItRuns: string;
|
||||
readonly whatItCosts: string;
|
||||
readonly paths: readonly ServicePath[];
|
||||
readonly scenes: SceneLabels;
|
||||
};
|
||||
readonly automation: {
|
||||
readonly label: string;
|
||||
readonly title: SplitTitle;
|
||||
readonly intro: string;
|
||||
readonly examplesLabel: string;
|
||||
readonly running: string;
|
||||
readonly runComplete: string;
|
||||
readonly log: string;
|
||||
readonly byHand: string;
|
||||
readonly automated: string;
|
||||
readonly kinds: Readonly<Record<NodeKind, string>>;
|
||||
/** Texts of the example workflows, by workflow id. */
|
||||
readonly workflows: Readonly<Record<string, WorkflowCopy>>;
|
||||
readonly notes: readonly TitledList[];
|
||||
};
|
||||
readonly technology: {
|
||||
readonly label: string;
|
||||
readonly title: SplitTitle;
|
||||
/** A good fit, also possible, not my field: in this order. */
|
||||
readonly fit: readonly TitledList[];
|
||||
readonly techLabel: string;
|
||||
readonly tech: readonly {
|
||||
readonly area: string;
|
||||
readonly items: readonly string[];
|
||||
}[];
|
||||
readonly pipelineTitle: string;
|
||||
readonly pipelineIntro: string;
|
||||
readonly pipeline: {
|
||||
readonly simulate: string;
|
||||
/** Texts of the scenarios, by scenario id (see PipelineRun). */
|
||||
readonly scenarios: Readonly<Record<string, PipelineScenarioCopy>>;
|
||||
readonly stages: readonly string[];
|
||||
/** Three explanations; the first has the blank {area}. */
|
||||
readonly explanations: readonly TitledText[];
|
||||
readonly runAgain: string;
|
||||
readonly run: string;
|
||||
readonly fixPushed: string;
|
||||
readonly lint: string;
|
||||
readonly types: string;
|
||||
readonly tests: string;
|
||||
readonly expected: string;
|
||||
readonly received: string;
|
||||
/** Has the blank {file}. */
|
||||
readonly runningFile: string;
|
||||
readonly summaryLabel: string;
|
||||
/** Has the blank {n}. */
|
||||
readonly failed: string;
|
||||
/** Has the blank {n}. */
|
||||
readonly passed: string;
|
||||
/** Has the blank {total}. */
|
||||
readonly of: string;
|
||||
readonly pending: string;
|
||||
readonly blocked: string;
|
||||
readonly released: string;
|
||||
readonly footnote: string;
|
||||
};
|
||||
};
|
||||
readonly working: {
|
||||
readonly label: string;
|
||||
readonly title: SplitTitle;
|
||||
readonly principles: readonly TitledText[];
|
||||
readonly diagram: {
|
||||
readonly description: string;
|
||||
/** The hands a request passes through, starting with the client. */
|
||||
readonly chain: readonly string[];
|
||||
readonly you: string;
|
||||
readonly me: string;
|
||||
readonly often: string;
|
||||
readonly withMe: string;
|
||||
readonly oftenLong: string;
|
||||
readonly withMeLong: string;
|
||||
};
|
||||
};
|
||||
readonly work: {
|
||||
readonly label: string;
|
||||
readonly title: SplitTitle;
|
||||
/** Texts of the shown sites, by their name (see src/content/work.ts). */
|
||||
readonly items: Readonly<Record<string, WorkCopy>>;
|
||||
readonly openLive: string;
|
||||
/** Has the blank {name}. */
|
||||
readonly desktopAlt: string;
|
||||
/** Has the blank {name}. */
|
||||
readonly phoneAlt: string;
|
||||
readonly note: string;
|
||||
};
|
||||
readonly faq: {
|
||||
readonly label: string;
|
||||
readonly title: SplitTitle;
|
||||
readonly items: readonly FaqItem[];
|
||||
};
|
||||
readonly contact: {
|
||||
readonly headline: readonly HeadlineLine[];
|
||||
readonly lead: string;
|
||||
readonly newMessage: string;
|
||||
readonly draft: string;
|
||||
readonly need: string;
|
||||
readonly to: string;
|
||||
readonly subject: string;
|
||||
readonly open: string;
|
||||
readonly copy: string;
|
||||
readonly copied: string;
|
||||
readonly topics: readonly ContactTopic[];
|
||||
};
|
||||
readonly notFound: {
|
||||
readonly kicker: string;
|
||||
readonly title: string;
|
||||
readonly text: string;
|
||||
readonly home: string;
|
||||
};
|
||||
readonly error: {
|
||||
readonly kicker: string;
|
||||
readonly title: string;
|
||||
readonly text: string;
|
||||
readonly retry: string;
|
||||
readonly home: string;
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
/** The sites shown in the work section. Their texts are in en.ts and de.ts, by name. */
|
||||
export type WorkSite = {
|
||||
readonly name: string;
|
||||
readonly href: string;
|
||||
readonly desktop: {
|
||||
readonly src: string;
|
||||
readonly width: number;
|
||||
readonly height: number;
|
||||
};
|
||||
readonly mobile: {
|
||||
readonly src: string;
|
||||
readonly width: number;
|
||||
readonly height: number;
|
||||
};
|
||||
};
|
||||
|
||||
export const WORK_SITES: readonly WorkSite[] = [
|
||||
{
|
||||
name: "klz-cables.com",
|
||||
href: "https://klz-cables.com",
|
||||
desktop: { src: "/work/klz-long.webp", width: 1200, height: 1680 },
|
||||
mobile: { src: "/work/klz-mobile.webp", width: 390, height: 3600 },
|
||||
},
|
||||
{
|
||||
name: "e-tib.com",
|
||||
href: "https://e-tib.com",
|
||||
desktop: { src: "/work/etib-long.webp", width: 1200, height: 4333 },
|
||||
mobile: { src: "/work/etib-mobile.webp", width: 390, height: 3600 },
|
||||
},
|
||||
];
|
||||
Reference in New Issue
Block a user