feat: estimates on the homepage, monthly booking on the freelance page
The homepage showed a price list per case, including a day rate. Buyers of a result need a way to a price, not rates: each case now shows three steps to an estimate, one line for orientation and a button that opens the contact message with that topic chosen. The developer case is booked by the month, not by the day. The day rate is gone; the freelance page has the booking terms: 100 EUR per hour, fixed days of the week agreed for one month at a time, an example month, monthly invoice, two weeks notice to the end of a month. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,7 @@ import type { Metadata } from "next";
|
|||||||
import { DecodeText } from "@/src/components/bits/DecodeText";
|
import { DecodeText } from "@/src/components/bits/DecodeText";
|
||||||
import { Block } from "@/src/components/home/Block";
|
import { Block } from "@/src/components/home/Block";
|
||||||
import { ClosingCta } from "@/src/components/home/ClosingCta";
|
import { ClosingCta } from "@/src/components/home/ClosingCta";
|
||||||
|
import { TopicButton } from "@/src/components/home/TopicButton";
|
||||||
import { PipelineRun } from "@/src/components/illustrations/PipelineRun";
|
import { PipelineRun } from "@/src/components/illustrations/PipelineRun";
|
||||||
import { Reveal } from "@/src/components/Reveal";
|
import { Reveal } from "@/src/components/Reveal";
|
||||||
import { SwipeRow } from "@/src/components/SwipeRow";
|
import { SwipeRow } from "@/src/components/SwipeRow";
|
||||||
@@ -36,6 +37,7 @@ export default async function FreelancePage({ params }: Props) {
|
|||||||
const locale = parseLocale((await params).locale) ?? DEFAULT_LOCALE;
|
const locale = parseLocale((await params).locale) ?? DEFAULT_LOCALE;
|
||||||
const content = contentFor(locale);
|
const content = contentFor(locale);
|
||||||
const { freelance, technology } = content;
|
const { freelance, technology } = content;
|
||||||
|
const { booking } = freelance;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative flex flex-col bg-white">
|
<div className="relative flex flex-col bg-white">
|
||||||
@@ -135,6 +137,57 @@ export default async function FreelancePage({ params }: Props) {
|
|||||||
</Reveal>
|
</Reveal>
|
||||||
</Block>
|
</Block>
|
||||||
|
|
||||||
|
<Block
|
||||||
|
index="03"
|
||||||
|
label={booking.label}
|
||||||
|
title={
|
||||||
|
<>
|
||||||
|
{booking.title.lead}{" "}
|
||||||
|
<span className={MUTED}>{booking.title.rest}</span>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Reveal>
|
||||||
|
<div className="grid grid-cols-1 gap-12 md:grid-cols-12 md:gap-12">
|
||||||
|
<div className="md:col-span-6">
|
||||||
|
<p className={BODY}>{booking.text}</p>
|
||||||
|
<ul className="m-0 mt-8 list-none p-0">
|
||||||
|
{booking.terms.map((term) => (
|
||||||
|
<li
|
||||||
|
key={term}
|
||||||
|
className="border-t border-slate-200 py-4 font-sans text-lg text-slate-900 md:text-xl"
|
||||||
|
>
|
||||||
|
{term}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div className="md:col-span-6">
|
||||||
|
<div className="rounded-3xl bg-slate-50 p-6 md:p-10">
|
||||||
|
<div className="grid grid-cols-1 gap-8 sm:grid-cols-2">
|
||||||
|
{[booking.rate, booking.example].map((price) => (
|
||||||
|
<div key={price.label}>
|
||||||
|
<p className="m-0 font-sans text-base text-slate-500 md:text-lg">
|
||||||
|
{price.label}
|
||||||
|
</p>
|
||||||
|
<p className="m-0 mt-2 font-sans text-3xl font-black tracking-tighter text-slate-900 md:text-5xl">
|
||||||
|
{price.amount}
|
||||||
|
</p>
|
||||||
|
<p className="m-0 mt-2 font-mono text-[11px] uppercase tracking-[0.2em] text-slate-500">
|
||||||
|
{price.note}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="mt-10 border-t border-slate-200 pt-8">
|
||||||
|
<TopicButton topic="developer">{booking.cta}</TopicButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Reveal>
|
||||||
|
</Block>
|
||||||
|
|
||||||
<ClosingCta contact={content.contact} />
|
<ClosingCta contact={content.contact} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { onContactTopic } from "./contactTopic";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { useContent } from "@/src/components/ContentProvider";
|
import { useContent } from "@/src/components/ContentProvider";
|
||||||
import { CONTACT_EMAIL_PARTS } from "@/src/content/site";
|
import { CONTACT_EMAIL_PARTS } from "@/src/content/site";
|
||||||
@@ -61,6 +62,15 @@ export const ContactComposer: React.FC = () => {
|
|||||||
setAddress(assembleEmail(CONTACT_EMAIL_PARTS));
|
setAddress(assembleEmail(CONTACT_EMAIL_PARTS));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// A button elsewhere on the page can open the message with its topic chosen.
|
||||||
|
React.useEffect(
|
||||||
|
() =>
|
||||||
|
onContactTopic((id) => {
|
||||||
|
if (topics.some((item) => item.id === id)) setTopicId(id);
|
||||||
|
}),
|
||||||
|
[topics],
|
||||||
|
);
|
||||||
|
|
||||||
const topic = topics.find((item) => item.id === topicId) ?? topics[0];
|
const topic = topics.find((item) => item.id === topicId) ?? topics[0];
|
||||||
const typed = useTyped(topic?.draft.body ?? "");
|
const typed = useTyped(topic?.draft.body ?? "");
|
||||||
|
|
||||||
|
|||||||
@@ -2,11 +2,13 @@
|
|||||||
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
import { Button } from "@/src/components/Button";
|
import Link from "next/link";
|
||||||
import { BitAura } from "@/src/components/bits/BitAura";
|
import { BitAura } from "@/src/components/bits/BitAura";
|
||||||
import { DecodeText } from "@/src/components/bits/DecodeText";
|
import { DecodeText } from "@/src/components/bits/DecodeText";
|
||||||
import { PathScene } from "@/src/components/illustrations/PathScenes";
|
import { PathScene } from "@/src/components/illustrations/PathScenes";
|
||||||
import { useContent } from "@/src/components/ContentProvider";
|
import { useContent, useLocale } from "@/src/components/ContentProvider";
|
||||||
|
import { pathFor } from "@/src/domain/routes";
|
||||||
|
import { TopicButton } from "./TopicButton";
|
||||||
import type { ServicePath } from "@/src/content/types";
|
import type { ServicePath } from "@/src/content/types";
|
||||||
|
|
||||||
const MONO = "font-mono text-[11px] uppercase tracking-[0.2em] text-slate-500";
|
const MONO = "font-mono text-[11px] uppercase tracking-[0.2em] text-slate-500";
|
||||||
@@ -148,6 +150,8 @@ const PathStory: React.FC<{ path: ServicePath; active: boolean }> = ({
|
|||||||
*/
|
*/
|
||||||
export const PathPicker: React.FC = () => {
|
export const PathPicker: React.FC = () => {
|
||||||
const { services } = useContent();
|
const { services } = useContent();
|
||||||
|
const { estimate } = services;
|
||||||
|
const locale = useLocale();
|
||||||
const paths = services.paths;
|
const paths = services.paths;
|
||||||
const [activeId, setActiveId] = React.useState(paths[0]?.id ?? "");
|
const [activeId, setActiveId] = React.useState(paths[0]?.id ?? "");
|
||||||
|
|
||||||
@@ -244,37 +248,53 @@ export const PathPicker: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-12 rounded-3xl bg-slate-50 p-6 md:mt-16 md:p-10">
|
<div className="mt-12 rounded-3xl bg-slate-50 p-6 md:mt-16 md:p-10">
|
||||||
<p className={`${MONO} m-0`}>{services.whatItCosts}</p>
|
<p className={`${MONO} m-0`}>{estimate.label}</p>
|
||||||
<div className="mt-6 grid grid-cols-1 gap-10 md:grid-cols-12 md:gap-12">
|
<div className="mt-6 grid grid-cols-1 gap-10 md:grid-cols-12 md:gap-12">
|
||||||
<div className="md:col-span-6">
|
<div className="md:col-span-7">
|
||||||
<p className="m-0 font-sans text-2xl font-bold leading-tight tracking-tight text-slate-900 md:text-4xl">
|
<p className="m-0 font-sans text-2xl font-bold leading-tight tracking-tight text-slate-900 md:text-4xl">
|
||||||
{path.pricingModel}
|
{estimate.title}
|
||||||
</p>
|
</p>
|
||||||
<p className="m-0 mt-4 font-sans text-base leading-relaxed text-slate-500 md:text-lg">
|
<ol className="m-0 mt-6 list-none p-0">
|
||||||
{path.pricingText}
|
{estimate.steps.map((item, index) => (
|
||||||
|
<li
|
||||||
|
key={item.title}
|
||||||
|
className="flex gap-5 border-t border-slate-200 py-4"
|
||||||
|
>
|
||||||
|
<span className="w-6 shrink-0 font-sans text-2xl font-black leading-none tracking-tighter text-slate-300">
|
||||||
|
{index + 1}
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
<span className="block font-sans text-lg font-bold tracking-tight text-slate-900">
|
||||||
|
{item.title}
|
||||||
|
</span>
|
||||||
|
<span className="mt-1 block font-sans text-base leading-relaxed text-slate-500">
|
||||||
|
{item.text}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col md:col-span-5">
|
||||||
|
<p className={`${MONO} m-0`}>{estimate.orientationLabel}</p>
|
||||||
|
<p className="m-0 mt-3 font-sans text-2xl font-black leading-tight tracking-tighter text-slate-900 md:text-4xl">
|
||||||
|
{path.orientation}
|
||||||
</p>
|
</p>
|
||||||
|
<p className="m-0 mt-4 font-sans text-base leading-relaxed text-slate-500">
|
||||||
|
{path.orientationNote} {estimate.net}
|
||||||
|
</p>
|
||||||
|
<div className="mt-8 flex flex-col items-start gap-5 md:mt-auto md:pt-8">
|
||||||
|
<TopicButton topic={path.id}>{estimate.cta}</TopicButton>
|
||||||
|
{path.id === "developer" && (
|
||||||
|
<Link
|
||||||
|
href={pathFor("freelance", locale)}
|
||||||
|
className="border-b border-slate-900 pb-0.5 font-sans text-base font-bold text-slate-900 no-underline transition-opacity hover:opacity-60"
|
||||||
|
>
|
||||||
|
{estimate.freelance} →
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-2 gap-6 md:col-span-6 md:gap-8">
|
|
||||||
{path.prices.map((price) => (
|
|
||||||
<div key={price.label}>
|
|
||||||
<p className="m-0 font-sans text-base text-slate-500 md:text-lg">
|
|
||||||
{price.label}
|
|
||||||
</p>
|
|
||||||
<p className="m-0 mt-2 font-sans text-3xl font-black tracking-tighter text-slate-900 md:text-5xl">
|
|
||||||
{price.amount}
|
|
||||||
</p>
|
|
||||||
<p className={`${MONO} m-0 mt-2`}>{price.note}</p>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="mt-10 flex flex-col gap-6 border-t border-slate-200 pt-8 md:flex-row md:items-center md:justify-between">
|
|
||||||
<p className="m-0 max-w-2xl font-sans text-base leading-relaxed text-slate-500">
|
|
||||||
{path.smallPrint}
|
|
||||||
</p>
|
|
||||||
<Button href="#contact" size="large">
|
|
||||||
{path.cta}
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import { Button } from "@/src/components/Button";
|
||||||
|
import { chooseContactTopic } from "./contactTopic";
|
||||||
|
|
||||||
|
/** A call to action that jumps to the contact section with a topic already chosen. */
|
||||||
|
export const TopicButton: React.FC<{
|
||||||
|
topic: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}> = ({ topic, children }) => (
|
||||||
|
<Button
|
||||||
|
href="#contact"
|
||||||
|
size="large"
|
||||||
|
onClick={() => chooseContactTopic(topic)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
/** The contact section listens for this to open with a given topic chosen. */
|
||||||
|
const EVENT = "mintel:contact-topic";
|
||||||
|
|
||||||
|
/** Ask the contact section to choose a topic (see ContactComposer). */
|
||||||
|
export const chooseContactTopic = (topicId: string): void => {
|
||||||
|
window.dispatchEvent(new CustomEvent<string>(EVENT, { detail: topicId }));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const onContactTopic = (
|
||||||
|
listener: (topicId: string) => void,
|
||||||
|
): (() => void) => {
|
||||||
|
const handle = (event: Event) => {
|
||||||
|
if (event instanceof CustomEvent && typeof event.detail === "string") {
|
||||||
|
listener(event.detail);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener(EVENT, handle);
|
||||||
|
return () => window.removeEventListener(EVENT, handle);
|
||||||
|
};
|
||||||
+51
-48
@@ -78,7 +78,28 @@ export const de: Content = {
|
|||||||
tablistLabel: "Was brauchen Sie?",
|
tablistLabel: "Was brauchen Sie?",
|
||||||
youGet: "Das bekommen Sie",
|
youGet: "Das bekommen Sie",
|
||||||
howItRuns: "So läuft es ab",
|
howItRuns: "So läuft es ab",
|
||||||
whatItCosts: "Das kostet es",
|
estimate: {
|
||||||
|
label: "Was es kostet",
|
||||||
|
title: "So kommen Sie zu einem Preis",
|
||||||
|
steps: [
|
||||||
|
{
|
||||||
|
title: "Ein Gespräch, etwa 20 Minuten",
|
||||||
|
text: "Sie erzählen mir, was Sie brauchen.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Eine schriftliche Schätzung",
|
||||||
|
text: "Sie sehen die geschätzten Stunden und was darin enthalten ist.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Sie geben ein Budget frei",
|
||||||
|
text: "Ich rechne die tatsächlichen Stunden ab und sage früh Bescheid, wenn es nicht reicht.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
orientationLabel: "Zur Orientierung",
|
||||||
|
net: "Alle Preise netto.",
|
||||||
|
cta: "Schätzung anfragen",
|
||||||
|
freelance: "Buchung und Preise für Teams",
|
||||||
|
},
|
||||||
paths: [
|
paths: [
|
||||||
{
|
{
|
||||||
id: "developer",
|
id: "developer",
|
||||||
@@ -112,16 +133,9 @@ export const de: Content = {
|
|||||||
text: "Die Vereinbarung kann mit zwei Wochen Frist zum Monatsende beendet werden.",
|
text: "Die Vereinbarung kann mit zwei Wochen Frist zum Monatsende beendet werden.",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
pricingModel: "Nach Aufwand, solange Ihr Projekt läuft",
|
orientation: "100 EUR pro Stunde, monatsweise gebucht",
|
||||||
pricingText:
|
orientationNote:
|
||||||
"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.",
|
"Sie buchen feste Wochentage für jeweils einen Monat. Abgerechnet werden die Stunden, die ich tatsächlich gearbeitet habe.",
|
||||||
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",
|
id: "rescue",
|
||||||
@@ -157,19 +171,9 @@ export const de: Content = {
|
|||||||
text: "Mit mir, mit Ihrem eigenen Team oder gar nicht.",
|
text: "Mit mir, mit Ihrem eigenen Team oder gar nicht.",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
pricingModel: "Eine feste Woche zum festen Preis",
|
orientation: "3.500 EUR für eine feste Woche",
|
||||||
pricingText:
|
orientationNote:
|
||||||
"Der Umfang ist eine Woche meiner Zeit. Sie kennen die Kosten, bevor ich anfange.",
|
"Der Umfang ist eine Woche meiner Zeit, 35 Stunden. 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.",
|
||||||
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",
|
id: "automation",
|
||||||
@@ -205,13 +209,9 @@ export const de: Content = {
|
|||||||
text: "Sie bekommen eine Notiz dazu, was es tut und was zu prüfen ist, wenn sich eines der Programme ändert.",
|
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",
|
orientation: "100 EUR pro Stunde, nach Schätzung",
|
||||||
pricingText:
|
orientationNote:
|
||||||
"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.",
|
"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",
|
id: "website",
|
||||||
@@ -246,24 +246,9 @@ export const de: Content = {
|
|||||||
text: "Ein neues Foto oder ein geänderter Preis: Sie schreiben mir eine E-Mail.",
|
text: "Ein neues Foto oder ein geänderter Preis: Sie schreiben mir eine E-Mail.",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
pricingModel: "Erst eine Schätzung, dann ein Budget",
|
orientation: "Ab 3.800 EUR, einmalig",
|
||||||
pricingText:
|
orientationNote:
|
||||||
"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.",
|
"Das gilt für eine typische Unternehmenswebsite. Hosting und kleine Änderungen danach kosten einen monatlichen Betrag, den wir vor dem Start vereinbaren.",
|
||||||
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: {
|
scenes: {
|
||||||
@@ -518,6 +503,24 @@ export const de: Content = {
|
|||||||
intro:
|
intro:
|
||||||
"Diese Seite ist für alle, die selbst entwickeln oder ein Team leiten: für welche Projekte ich passe, womit ich arbeite und wie ich Änderungen absichere, bevor sie live gehen.",
|
"Diese Seite ist für alle, die selbst entwickeln oder ein Team leiten: für welche Projekte ich passe, womit ich arbeite und wie ich Änderungen absichere, bevor sie live gehen.",
|
||||||
pipelineLabel: "Absicherung",
|
pipelineLabel: "Absicherung",
|
||||||
|
booking: {
|
||||||
|
label: "Buchung",
|
||||||
|
title: { lead: "Gebucht wird monatsweise.", rest: "Nicht tageweise." },
|
||||||
|
text: "Ich arbeite an festen Wochentagen an Ihrem Projekt, vereinbart für jeweils einen Monat. Es gibt kein Paket zu kaufen: Sie zahlen die Stunden, die ich tatsächlich gearbeitet habe.",
|
||||||
|
rate: { label: "Pro Stunde", amount: "100 EUR", note: "netto" },
|
||||||
|
example: {
|
||||||
|
label: "Beispiel: 2 Tage pro Woche",
|
||||||
|
amount: "ca. 6.400 EUR",
|
||||||
|
note: "64 Stunden in vier Wochen, netto",
|
||||||
|
},
|
||||||
|
terms: [
|
||||||
|
"Feste Wochentage, vereinbart für jeweils einen Monat",
|
||||||
|
"Eine Rechnung pro Monat, mit den erfassten Stunden",
|
||||||
|
"Beide Seiten können mit zwei Wochen Frist zum Monatsende kündigen",
|
||||||
|
"Einzelne Tage buche ich nicht",
|
||||||
|
],
|
||||||
|
cta: "Verfügbarkeit anfragen",
|
||||||
|
},
|
||||||
teaser: {
|
teaser: {
|
||||||
title: "Sie entwickeln selbst oder leiten ein Team?",
|
title: "Sie entwickeln selbst oder leiten ein Team?",
|
||||||
text: "Für welche Projekte ich passe, womit ich arbeite und wie ich Änderungen absichere, steht auf einer eigenen Seite.",
|
text: "Für welche Projekte ich passe, womit ich arbeite und wie ich Änderungen absichere, steht auf einer eigenen Seite.",
|
||||||
|
|||||||
+51
-40
@@ -77,7 +77,28 @@ export const en: Content = {
|
|||||||
tablistLabel: "What do you need?",
|
tablistLabel: "What do you need?",
|
||||||
youGet: "What you get",
|
youGet: "What you get",
|
||||||
howItRuns: "How it runs",
|
howItRuns: "How it runs",
|
||||||
whatItCosts: "What it costs",
|
estimate: {
|
||||||
|
label: "What it costs",
|
||||||
|
title: "How you get to a price",
|
||||||
|
steps: [
|
||||||
|
{
|
||||||
|
title: "A call, about 20 minutes",
|
||||||
|
text: "You tell me what you need.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "A written estimate",
|
||||||
|
text: "You see the estimated hours and what they include.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "You approve a budget",
|
||||||
|
text: "I bill the hours actually used and tell you early if it will not be enough.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
orientationLabel: "For orientation",
|
||||||
|
net: "All prices net.",
|
||||||
|
cta: "Ask for an estimate",
|
||||||
|
freelance: "Booking and rates for teams",
|
||||||
|
},
|
||||||
paths: [
|
paths: [
|
||||||
{
|
{
|
||||||
id: "developer",
|
id: "developer",
|
||||||
@@ -111,16 +132,9 @@ export const en: Content = {
|
|||||||
text: "The agreement can be ended with two weeks notice to the end of a month.",
|
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",
|
orientation: "100 EUR per hour, booked by the month",
|
||||||
pricingText:
|
orientationNote:
|
||||||
"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.",
|
"You book fixed days of the week for one month at a time. I bill the hours I actually worked.",
|
||||||
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",
|
id: "rescue",
|
||||||
@@ -154,15 +168,9 @@ export const en: Content = {
|
|||||||
text: "With me, with your own team, or not at all.",
|
text: "With me, with your own team, or not at all.",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
pricingModel: "A fixed week at a fixed price",
|
orientation: "3,500 EUR for one fixed week",
|
||||||
pricingText:
|
orientationNote:
|
||||||
"The scope is one week of my time. You know the cost before I start.",
|
"The scope is one week of my time, 35 hours. 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.",
|
||||||
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",
|
id: "automation",
|
||||||
@@ -198,13 +206,9 @@ export const en: Content = {
|
|||||||
text: "You get a note on what it does and what to check when one of the tools changes.",
|
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",
|
orientation: "100 EUR per hour, after an estimate",
|
||||||
pricingText:
|
orientationNote:
|
||||||
"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.",
|
"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",
|
id: "website",
|
||||||
@@ -238,20 +242,9 @@ export const en: Content = {
|
|||||||
text: "A new photo or a changed price: you send me an email.",
|
text: "A new photo or a changed price: you send me an email.",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
pricingModel: "An estimate first, then a budget",
|
orientation: "From 3,800 EUR, one-off",
|
||||||
pricingText:
|
orientationNote:
|
||||||
"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.",
|
"This is for a typical company website. Hosting and small changes afterwards cost a monthly fee that we agree on before we start.",
|
||||||
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: {
|
scenes: {
|
||||||
@@ -498,6 +491,24 @@ export const en: Content = {
|
|||||||
intro:
|
intro:
|
||||||
"This page is for people who develop themselves or lead a team: which projects I am right for, what I work with, and how I secure changes before they go live.",
|
"This page is for people who develop themselves or lead a team: which projects I am right for, what I work with, and how I secure changes before they go live.",
|
||||||
pipelineLabel: "Safeguards",
|
pipelineLabel: "Safeguards",
|
||||||
|
booking: {
|
||||||
|
label: "Booking",
|
||||||
|
title: { lead: "Booked by the month.", rest: "Not by the day." },
|
||||||
|
text: "I work on your project on fixed days of the week, agreed for one month at a time. There is no package to buy: you pay the hours I actually worked.",
|
||||||
|
rate: { label: "Per hour", amount: "100 EUR", note: "net" },
|
||||||
|
example: {
|
||||||
|
label: "Example: 2 days a week",
|
||||||
|
amount: "about 6,400 EUR",
|
||||||
|
note: "64 hours in four weeks, net",
|
||||||
|
},
|
||||||
|
terms: [
|
||||||
|
"Fixed days of the week, agreed for one month at a time",
|
||||||
|
"One invoice per month, with the tracked hours",
|
||||||
|
"Either side can end it with two weeks notice to the end of a month",
|
||||||
|
"I do not book single days",
|
||||||
|
],
|
||||||
|
cta: "Ask about availability",
|
||||||
|
},
|
||||||
teaser: {
|
teaser: {
|
||||||
title: "You develop yourself or lead a team?",
|
title: "You develop yourself or lead a team?",
|
||||||
text: "Which projects I am right for, what I work with and how I secure changes is on a page of its own.",
|
text: "Which projects I am right for, what I work with and how I secure changes is on a page of its own.",
|
||||||
|
|||||||
@@ -55,11 +55,10 @@ export type ServicePath = {
|
|||||||
readonly youGet: readonly string[];
|
readonly youGet: readonly string[];
|
||||||
readonly steps: readonly TitledText[];
|
readonly steps: readonly TitledText[];
|
||||||
/** How this kind of work is priced, in one line, and why. */
|
/** How this kind of work is priced, in one line, and why. */
|
||||||
readonly pricingModel: string;
|
/** The order of magnitude in one line, such as "from 3,800 EUR, one-off". */
|
||||||
readonly pricingText: string;
|
readonly orientation: string;
|
||||||
readonly prices: readonly PathPrice[];
|
/** What that figure depends on or leaves out. */
|
||||||
readonly smallPrint: string;
|
readonly orientationNote: string;
|
||||||
readonly cta: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/** The short labels inside the drawings that explain the steps of a path. */
|
/** The short labels inside the drawings that explain the steps of a path. */
|
||||||
@@ -180,7 +179,17 @@ export type Content = {
|
|||||||
readonly tablistLabel: string;
|
readonly tablistLabel: string;
|
||||||
readonly youGet: string;
|
readonly youGet: string;
|
||||||
readonly howItRuns: string;
|
readonly howItRuns: string;
|
||||||
readonly whatItCosts: string;
|
/** How a visitor gets to a price: the homepage shows this instead of a price list. */
|
||||||
|
readonly estimate: {
|
||||||
|
readonly label: string;
|
||||||
|
readonly title: string;
|
||||||
|
readonly steps: readonly TitledText[];
|
||||||
|
readonly orientationLabel: string;
|
||||||
|
readonly net: string;
|
||||||
|
readonly cta: string;
|
||||||
|
/** The pointer to the booking terms on the freelance page. */
|
||||||
|
readonly freelance: string;
|
||||||
|
};
|
||||||
readonly paths: readonly ServicePath[];
|
readonly paths: readonly ServicePath[];
|
||||||
readonly scenes: SceneLabels;
|
readonly scenes: SceneLabels;
|
||||||
};
|
};
|
||||||
@@ -241,6 +250,16 @@ export type Content = {
|
|||||||
readonly title: SplitTitle;
|
readonly title: SplitTitle;
|
||||||
readonly intro: string;
|
readonly intro: string;
|
||||||
readonly pipelineLabel: string;
|
readonly pipelineLabel: string;
|
||||||
|
/** How the owner is booked as a developer: by the month, not by the day. */
|
||||||
|
readonly booking: {
|
||||||
|
readonly label: string;
|
||||||
|
readonly title: SplitTitle;
|
||||||
|
readonly text: string;
|
||||||
|
readonly rate: PathPrice;
|
||||||
|
readonly example: PathPrice;
|
||||||
|
readonly terms: readonly string[];
|
||||||
|
readonly cta: string;
|
||||||
|
};
|
||||||
/** The pointer to this page on the homepage. */
|
/** The pointer to this page on the homepage. */
|
||||||
readonly teaser: TitledText & { readonly cta: string };
|
readonly teaser: TitledText & { readonly cta: string };
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user