Files
Ronni Baslund bf183fce07 feat(website): tier-driven progressive partner margin calculator
The /partners margin calculator now derives margin from the user count using
progressive brackets (like tax brackets): first 500 users at 15%, 501–1000 at
30%, 1001+ at 40%, off the 49 kr/user/mo list price. Replaces the manual margin
slider with a live per-bracket breakdown. Rates are read from the tier copy;
tier thresholds aligned to 501 / 1.001 so the cards and calculator agree.
2026-06-05 15:59:19 +02:00

86 lines
4.7 KiB
Vue
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
// Interactive reseller margin calculator. Margin is PROGRESSIVE (like tax
// brackets): the first 500 users earn 15%, users 5011000 earn 30%, and every
// user beyond 1000 earns 40% — all off the 49 kr/user/mo list price. The
// per-bracket rates come from the tier copy so they never drift.
import { ref, computed } from 'vue'
import { useTheme, useCopy, useLang } from '~/composables/useLanding'
const t = useTheme()
const copy = useCopy()
const lang = useLang()
const c = computed(() => copy.value.pages.partners.calc)
const LIST_PRICE = 49 // DKK per user per month
const seats = ref(600)
const nf = computed(() => new Intl.NumberFormat(lang.value === 'en' ? 'en-US' : 'da-DK'))
// Progressive brackets. `lo`/`hi` are the user-count bounds; pct is read from
// the matching tier so the calculator and tier cards stay in sync.
const brackets = computed(() => {
const items = copy.value.pages.partners.tiers.items
const f = nf.value
return [
{ lo: 0, hi: 500, pct: parseInt(String(items[0][2]), 10) || 0, label: `0${f.format(500)}` },
{ lo: 500, hi: 1000, pct: parseInt(String(items[1][2]), 10) || 0, label: `${f.format(501)}${f.format(1000)}` },
{ lo: 1000, hi: Infinity, pct: parseInt(String(items[2][2]), 10) || 0, label: `${f.format(1001)}+` },
]
})
function usersIn(b: { lo: number, hi: number }) {
return Math.max(0, Math.min(seats.value, b.hi) - b.lo)
}
const monthly = computed(() =>
Math.round(brackets.value.reduce((sum, b) => sum + usersIn(b) * LIST_PRICE * (b.pct / 100), 0)),
)
const annual = computed(() => monthly.value * 12)
const dkk = new Intl.NumberFormat('da-DK', { style: 'currency', currency: 'DKK', maximumFractionDigits: 0 })
const fmtMonthly = computed(() => dkk.format(monthly.value))
const fmtAnnual = computed(() => dkk.format(annual.value))
</script>
<template>
<div :style="{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '0', border: `1px solid ${t.border}`, borderRadius: '4px', overflow: 'hidden' }">
<!-- Controls -->
<div :style="{ padding: '36px', background: t.surface, display: 'flex', flexDirection: 'column', gap: '28px', justifyContent: 'center' }">
<div>
<div :style="{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', marginBottom: '12px' }">
<span :style="{ fontFamily: '\'JetBrains Mono\', monospace', fontSize: '11px', color: t.fgMuted, letterSpacing: '0.06em', textTransform: 'uppercase' }">{{ c.seatsLabel }}</span>
<span :style="{ fontFamily: '\'Inter Tight\', sans-serif', fontWeight: 600, fontSize: '20px', color: t.fg }">{{ nf.format(seats) }}</span>
</div>
<input v-model.number="seats" type="range" min="10" max="2000" step="10" :style="{ width: '100%', accentColor: t.signal, cursor: 'pointer' }" >
</div>
<!-- Progressive bracket breakdown -->
<div>
<div :style="{ fontFamily: '\'JetBrains Mono\', monospace', fontSize: '11px', color: t.fgMuted, letterSpacing: '0.06em', textTransform: 'uppercase', marginBottom: '14px' }">{{ c.marginLabel }}</div>
<div :style="{ display: 'flex', flexDirection: 'column', gap: '10px' }">
<div
v-for="(b, i) in brackets" :key="i"
:style="{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', opacity: usersIn(b) > 0 ? 1 : 0.4 }"
>
<span :style="{ display: 'flex', alignItems: 'center', gap: '9px', fontFamily: '\'JetBrains Mono\', monospace', fontSize: '12px', color: t.fg }">
<span :style="{ width: '5px', height: '5px', borderRadius: '999px', background: usersIn(b) > 0 ? t.signal : t.fgDim }" />
{{ b.label }}
</span>
<span :style="{ fontFamily: '\'Inter Tight\', sans-serif', fontWeight: 600, fontSize: '15px', color: t.fg }">{{ b.pct }} %</span>
</div>
</div>
</div>
</div>
<!-- Output -->
<div :style="{ padding: '36px', background: t.bgAlt, borderLeft: `1px solid ${t.border}`, display: 'flex', flexDirection: 'column', justifyContent: 'center' }">
<div :style="{ fontFamily: '\'JetBrains Mono\', monospace', fontSize: '11px', color: t.fgMuted, letterSpacing: '0.08em', textTransform: 'uppercase' }">{{ c.monthlyLabel }}</div>
<div :style="{ fontFamily: '\'Inter Tight\', sans-serif', fontWeight: 600, fontSize: 'clamp(40px, 5vw, 60px)', letterSpacing: '-0.03em', lineHeight: 1.0, color: t.fg, marginTop: '8px' }">{{ fmtMonthly }}</div>
<div :style="{ marginTop: '20px', paddingTop: '20px', borderTop: `1px solid ${t.border}`, fontFamily: '\'Inter\', sans-serif', fontSize: '15px', color: t.fgMuted }">
{{ c.annualLabel }} <span :style="{ color: t.fg, fontWeight: 600 }">{{ fmtAnnual }}</span>
</div>
</div>
</div>
</template>