Files
dezky/apps/website/pages/partners.vue
T
Ronni Baslund 7bee161ac1 feat(website): bilingual i18n (English default, Danish at /da) + SEO
Add @nuxtjs/i18n: English is the default locale (no prefix), Danish lives
under /da (prefix_except_default). Both server-rendered and indexed with
hreflang alternates + per-locale canonical (useLocaleHead in app.vue).
First-visit browser language is auto-detected and remembered in the
i18n_redirected cookie (redirectOn root).

- Keep the hand-authored COPY object; useLang/useCopy now read the i18n
  locale; useLocalizeHref/useLangToggle added. Every internal link is
  localized so navigation stays in-locale.
- Clear segmented EN|DA language switcher (active segment filled) replacing
  the ambiguous "en · da" pill.
- SEO: useSeoMeta defaults are locale-aware; og:image switches per locale
  (English / Danish share card); favicon links; robots.txt + sitemap.xml;
  env-aware siteUrl/baseUrl (localhost in dev, dezky.eu in prod).
- Update the cookie policy (dezky-lang -> i18n_redirected).
- Gate the Traefik wss:443 HMR behind DEZKY_TRAEFIK so localhost dev/DevTools
  connect over plain ws (fixes the DevTools disconnect loop).
2026-06-06 20:46:26 +02:00

311 lines
16 KiB
Vue
Raw 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">
import { ref } from 'vue'
import { useTheme, useCopy } from '~/composables/useLanding'
definePageMeta({ layout: 'page' })
const t = useTheme()
const copy = useCopy()
const localePath = useLocalePath()
const c = computed(() => copy.value.pages.partners)
const openFaq = ref<number | null>(0)
function toggleFaq(i: number) {
openFaq.value = openFaq.value === i ? null : i
}
// Partner application form — interim: composes a prefilled email to
// info@dezky.eu (no backend). Swap for a real intake/CRM later.
const pName = ref('')
const pCompany = ref('')
const pEmail = ref('')
const pWebsite = ref('')
const pType = ref('')
const pMessage = ref('')
function submitPartner() {
const f = c.value.form
const subject = `Partner application — ${(pCompany.value || pName.value).trim()}`
const body = [
`${f.nameLabel}: ${pName.value}`,
`${f.companyLabel}: ${pCompany.value}`,
`${f.emailLabel}: ${pEmail.value}`,
`${f.websiteLabel}: ${pWebsite.value}`,
`${f.typeLabel}: ${pType.value}`,
'',
pMessage.value,
].join('\n')
window.location.href = `mailto:info@dezky.eu?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`
}
const formEyebrow = computed(() => ({
fontFamily: '\'JetBrains Mono\', monospace', fontSize: '11px', color: t.value.fgDim,
letterSpacing: '0.12em', textTransform: 'uppercase' as const,
}))
const formH2 = computed(() => ({
fontFamily: '\'Inter Tight\', sans-serif', fontWeight: 600, fontSize: 'clamp(28px, 3.6vw, 44px)',
letterSpacing: '-0.03em', lineHeight: 1.05, margin: '14px 0 0', color: t.value.fg, textWrap: 'balance' as const,
}))
const fieldLabel = computed(() => ({
display: 'block', marginBottom: '8px', fontFamily: '\'JetBrains Mono\', monospace',
fontSize: '11px', color: t.value.fgMuted, letterSpacing: '0.06em', textTransform: 'uppercase' as const,
}))
const inputStyle = computed(() => ({
width: '100%', padding: '12px 14px', border: `1px solid ${t.value.border}`, borderRadius: '4px',
background: t.value.bg, color: t.value.fg, fontFamily: '\'Inter\', sans-serif', fontSize: '15px',
boxSizing: 'border-box' as const,
}))
useHead({ title: () => `${copy.value.pages.partners.label} · dezky` })
</script>
<template>
<LandingPageHeader :label="c.label" :title="c.title" :intro="c.intro" />
<!-- What you get -->
<LandingContainer pad="clamp(40px, 6vw, 56px) clamp(20px, 5vw, 64px) 0">
<div :style="{ fontFamily: '\'JetBrains Mono\', monospace', fontSize: '11px', color: t.fgDim, letterSpacing: '0.08em', textTransform: 'uppercase', marginBottom: '20px' }">{{ c.benefitsLabel }}</div>
<div :style="{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(min(100%, 260px), 1fr))', gap: '0', border: `1px solid ${t.border}`, borderRadius: '4px', overflow: 'hidden' }">
<div
v-for="(b, i) in c.benefits" :key="i"
:style="{
padding: '28px', background: t.surface,
borderTop: i > 1 ? `1px solid ${t.border}` : 'none',
borderLeft: i % 2 === 1 ? `1px solid ${t.border}` : 'none',
}"
>
<div :style="{ fontFamily: '\'Inter Tight\', sans-serif', fontWeight: 600, fontSize: '20px', color: t.fg, letterSpacing: '-0.015em' }">{{ b[0] }}</div>
<p :style="{ marginTop: '10px', fontFamily: '\'Inter\', sans-serif', fontSize: '15px', lineHeight: 1.6, color: t.fgMuted, margin: '10px 0 0', textWrap: 'pretty' }">{{ b[1] }}</p>
</div>
</div>
</LandingContainer>
<!-- Margin calculator -->
<LandingContainer pad="clamp(48px, 7vw, 72px) clamp(20px, 5vw, 64px) 0">
<div class="partners-section-header">
<h2 :style="{ fontFamily: '\'Inter Tight\', sans-serif', fontWeight: 600, fontSize: 'clamp(30px, 3.6vw, 48px)', letterSpacing: '-0.03em', lineHeight: 1.0, margin: 0, color: t.fg }">{{ c.calc.heading }}</h2>
<div :style="{ fontFamily: '\'JetBrains Mono\', monospace', fontSize: '11px', color: t.fgDim, letterSpacing: '0.08em', textTransform: 'uppercase' }">{{ c.calc.label }}</div>
</div>
<LandingPartnerCalculator />
<p :style="{ marginTop: '16px', fontFamily: '\'Inter\', sans-serif', fontSize: '13px', color: t.fgDim }">{{ c.calc.note }}</p>
</LandingContainer>
<!-- CSP vs Dezky comparison -->
<LandingContainer pad="clamp(48px, 7vw, 72px) clamp(20px, 5vw, 64px) 0">
<div class="partners-section-header">
<h2 :style="{ fontFamily: '\'Inter Tight\', sans-serif', fontWeight: 600, fontSize: 'clamp(30px, 3.6vw, 48px)', letterSpacing: '-0.03em', lineHeight: 1.0, margin: 0, color: t.fg }">{{ c.compare.heading }}</h2>
<div :style="{ fontFamily: '\'JetBrains Mono\', monospace', fontSize: '11px', color: t.fgDim, letterSpacing: '0.08em', textTransform: 'uppercase' }">{{ c.compare.label }}</div>
</div>
<div :style="{ border: `1px solid ${t.border}`, borderRadius: '4px', overflow: 'hidden' }">
<div class="partners-compare-grid partners-compare-header" :style="{ background: t.surface, borderBottom: `1px solid ${t.borderStrong}` }">
<div :style="{ padding: '18px 24px' }" />
<div :style="{ padding: '18px 24px', fontFamily: '\'JetBrains Mono\', monospace', fontSize: '13px', color: t.fgMuted }">{{ c.compare.cols[0] }}</div>
<div :style="{ padding: '18px 24px', fontFamily: '\'JetBrains Mono\', monospace', fontSize: '13px', fontWeight: 600, color: t.fg, background: `${t.signal}22` }">{{ c.compare.cols[1] }}</div>
</div>
<div
v-for="(row, i) in c.compare.rows" :key="i"
class="partners-compare-grid"
:style="{ borderTop: i === 0 ? 'none' : `1px solid ${t.border}`, fontFamily: '\'Inter\', sans-serif', fontSize: '15px' }"
>
<div class="pc-cat" :style="{ padding: '18px 24px', color: t.fgMuted, fontFamily: '\'JetBrains Mono\', monospace', fontSize: '12px', letterSpacing: '0.04em', textTransform: 'uppercase', alignSelf: 'center' }">{{ row[0] }}</div>
<div class="pc-val" :style="{ padding: '18px 24px', color: t.fgMuted }">
<span class="pc-key">{{ c.compare.cols[0] }}</span>{{ row[1] }}
</div>
<div class="pc-val" :style="{ padding: '18px 24px', color: t.fg, fontWeight: 600, background: `${t.signal}11` }">
<span class="pc-key">{{ c.compare.cols[1] }}</span>{{ row[2] }}
</div>
</div>
</div>
</LandingContainer>
<!-- Partner tiers -->
<LandingContainer pad="clamp(48px, 7vw, 72px) clamp(20px, 5vw, 64px) 0">
<div class="partners-section-header">
<h2 :style="{ fontFamily: '\'Inter Tight\', sans-serif', fontWeight: 600, fontSize: 'clamp(30px, 3.6vw, 48px)', letterSpacing: '-0.03em', lineHeight: 1.0, margin: 0, color: t.fg }">{{ c.tiers.heading }}</h2>
<div :style="{ fontFamily: '\'JetBrains Mono\', monospace', fontSize: '11px', color: t.fgDim, letterSpacing: '0.08em', textTransform: 'uppercase' }">{{ c.tiers.label }}</div>
</div>
<div :style="{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(min(100%, 240px), 1fr))', gap: '0', border: `1px solid ${t.border}`, borderRadius: '4px', overflow: 'hidden' }">
<div
v-for="(tier, i) in c.tiers.items" :key="i"
:style="{ padding: '32px 28px', background: t.surface, borderLeft: i === 0 ? 'none' : `1px solid ${t.border}`, display: 'flex', flexDirection: 'column', gap: '20px' }"
>
<div>
<div :style="{ fontFamily: '\'Inter Tight\', sans-serif', fontWeight: 600, fontSize: '20px', color: t.fg, letterSpacing: '-0.015em' }">{{ tier[0] }}</div>
<div :style="{ marginTop: '4px', fontFamily: '\'JetBrains Mono\', monospace', fontSize: '11px', color: t.fgDim, letterSpacing: '0.04em' }">{{ tier[1] }}</div>
</div>
<div :style="{ display: 'flex', alignItems: 'baseline', gap: '8px' }">
<span :style="{ fontFamily: '\'Inter Tight\', sans-serif', fontWeight: 600, fontSize: '40px', letterSpacing: '-0.03em', color: t.fg }">{{ tier[2] }}</span>
<span :style="{ fontFamily: '\'JetBrains Mono\', monospace', fontSize: '11px', color: t.fgMuted }">margin</span>
</div>
<div :style="{ display: 'flex', flexDirection: 'column', gap: '10px' }">
<div
v-for="(perk, j) in tier[3]" :key="j"
:style="{ display: 'flex', gap: '10px', fontFamily: '\'Inter\', sans-serif', fontSize: '14px', lineHeight: 1.45, color: t.fg }"
>
<span :style="{ color: t.signal, flexShrink: 0, fontWeight: 700 }"></span>
<span>{{ perk }}</span>
</div>
</div>
</div>
</div>
<p :style="{ marginTop: '16px', fontFamily: '\'Inter\', sans-serif', fontSize: '13px', color: t.fgDim }">{{ c.tiers.note }}</p>
</LandingContainer>
<!-- How to get started -->
<LandingContainer pad="clamp(48px, 7vw, 72px) clamp(20px, 5vw, 64px) 0">
<div :style="{ fontFamily: '\'JetBrains Mono\', monospace', fontSize: '11px', color: t.fgDim, letterSpacing: '0.08em', textTransform: 'uppercase', marginBottom: '32px' }">{{ c.stepsLabel }}</div>
<div :style="{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(min(100%, 220px), 1fr))', gap: '40px' }">
<div v-for="(step, i) in c.steps" :key="i" :style="{ paddingTop: '20px', borderTop: `1px solid ${t.borderStrong}` }">
<div :style="{ fontFamily: '\'JetBrains Mono\', monospace', fontSize: '12px', color: t.fgDim, letterSpacing: '0.06em' }">step {{ step[0] }}</div>
<div :style="{ marginTop: '20px', fontFamily: '\'Inter Tight\', sans-serif', fontWeight: 600, fontSize: '24px', color: t.fg, letterSpacing: '-0.02em' }">{{ step[1] }}</div>
<p :style="{ marginTop: '12px', fontFamily: '\'Inter\', sans-serif', fontSize: '15px', lineHeight: 1.6, color: t.fgMuted, textWrap: 'pretty' }">{{ step[2] }}</p>
</div>
</div>
</LandingContainer>
<!-- Partner FAQ -->
<LandingContainer pad="clamp(48px, 7vw, 72px) clamp(20px, 5vw, 64px) 0">
<div class="partners-section-header">
<h2 :style="{ fontFamily: '\'Inter Tight\', sans-serif', fontWeight: 600, fontSize: 'clamp(30px, 3.6vw, 48px)', letterSpacing: '-0.03em', lineHeight: 1.0, margin: 0, color: t.fg }">{{ c.faq.heading }}</h2>
<div :style="{ fontFamily: '\'JetBrains Mono\', monospace', fontSize: '11px', color: t.fgDim, letterSpacing: '0.08em', textTransform: 'uppercase' }">{{ c.faq.label }}</div>
</div>
<div>
<div
v-for="(item, i) in c.faq.items" :key="i"
:style="{ borderTop: `1px solid ${t.border}`, borderBottom: i === c.faq.items.length - 1 ? `1px solid ${t.border}` : 'none' }"
>
<button
:style="{ width: '100%', background: 'transparent', border: 'none', padding: '24px 0', display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: '24px', cursor: 'pointer', textAlign: 'left' }"
@click="toggleFaq(i)"
>
<span :style="{ fontFamily: '\'Inter Tight\', sans-serif', fontWeight: 600, fontSize: '18px', color: t.fg, letterSpacing: '-0.015em' }">{{ item[0] }}</span>
<span :style="{ fontFamily: '\'JetBrains Mono\', monospace', fontSize: '20px', color: t.fgMuted, flexShrink: 0 }">{{ openFaq === i ? '' : '+' }}</span>
</button>
<p v-if="openFaq === i" :style="{ margin: '0', padding: '0 0 24px', maxWidth: '720px', fontFamily: '\'Inter\', sans-serif', fontSize: '15px', lineHeight: 1.6, color: t.fgMuted, textWrap: 'pretty' }">{{ item[1] }}</p>
</div>
</div>
</LandingContainer>
<!-- Become a partner -->
<LandingContainer pad="clamp(48px, 7vw, 72px) clamp(20px, 5vw, 64px) clamp(80px, 12vw, 160px)">
<div :style="{ borderTop: `1px solid ${t.borderStrong}`, paddingTop: 'clamp(40px, 5vw, 56px)' }">
<div class="partner-form-grid" :style="{ display: 'grid', gap: '48px', alignItems: 'start' }">
<!-- Pitch -->
<div>
<div :style="formEyebrow">{{ c.form.label }}</div>
<h2 :style="formH2">{{ c.form.heading }}</h2>
<p :style="{ marginTop: '20px', fontFamily: '\'Inter\', sans-serif', fontSize: '18px', lineHeight: 1.6, color: t.fgMuted, maxWidth: '420px', textWrap: 'pretty' }">{{ c.form.lede }}</p>
<button
:style="{ marginTop: '28px', background: 'transparent', border: 'none', padding: 0, cursor: 'pointer', fontFamily: '\'Inter\', sans-serif', fontSize: '15px', color: t.fgMuted, textDecoration: 'underline', textUnderlineOffset: '3px' }"
@click="navigateTo(localePath('/demo'))"
>{{ c.cta }} </button>
</div>
<!-- Application form -->
<form
@submit.prevent="submitPartner"
:style="{ padding: 'clamp(24px, 4vw, 36px)', border: `1px solid ${t.border}`, borderRadius: '4px', background: t.surface, display: 'flex', flexDirection: 'column', gap: '18px' }"
>
<div>
<label :style="fieldLabel">{{ c.form.nameLabel }}</label>
<input v-model="pName" type="text" required :placeholder="c.form.namePh" :style="inputStyle">
</div>
<div>
<label :style="fieldLabel">{{ c.form.companyLabel }}</label>
<input v-model="pCompany" type="text" required :placeholder="c.form.companyPh" :style="inputStyle">
</div>
<div>
<label :style="fieldLabel">{{ c.form.emailLabel }}</label>
<input v-model="pEmail" type="email" required :placeholder="c.form.emailPh" :style="inputStyle">
</div>
<div>
<label :style="fieldLabel">{{ c.form.websiteLabel }}</label>
<input v-model="pWebsite" type="text" :placeholder="c.form.websitePh" :style="inputStyle">
</div>
<div>
<label :style="fieldLabel">{{ c.form.typeLabel }}</label>
<select v-model="pType" :style="{ ...inputStyle, appearance: 'none', cursor: 'pointer' }">
<option value="" disabled>{{ c.form.typePh }}</option>
<option v-for="ty in c.form.types" :key="ty" :value="ty">{{ ty }}</option>
</select>
</div>
<div>
<label :style="fieldLabel">{{ c.form.messageLabel }}</label>
<textarea v-model="pMessage" rows="3" :placeholder="c.form.messagePh" :style="{ ...inputStyle, resize: 'vertical' }" />
</div>
<button
type="submit"
:style="{ marginTop: '4px', background: t.signal, color: t.fg, border: 'none', padding: '16px 24px', fontFamily: '\'Inter\', sans-serif', fontSize: '15px', fontWeight: 600, borderRadius: '4px', cursor: 'pointer', letterSpacing: '-0.005em' }"
>{{ c.form.submit }} </button>
<p :style="{ margin: 0, fontFamily: '\'Inter\', sans-serif', fontSize: '13px', lineHeight: 1.5, color: t.fgDim }">{{ c.form.note }}</p>
</form>
</div>
</div>
</LandingContainer>
</template>
<style scoped>
/* Asymmetric 2-col section headers: heading + label side-by-side on desktop, stacked on mobile */
.partners-section-header {
display: grid;
grid-template-columns: 1.1fr 1fr;
gap: 40px;
align-items: end;
margin-bottom: 32px;
}
/* Asymmetric 3-col compare table: label + csp-col + dezky-col */
.partners-compare-grid {
display: grid;
grid-template-columns: 1.3fr 1fr 1fr;
}
/* Become-a-partner: pitch + form side-by-side, stacked on mobile */
.partner-form-grid {
grid-template-columns: 0.85fr 1.15fr;
}
/* Per-cell column label only appears once the table stacks on mobile. */
.pc-key {
display: none;
}
@media (max-width: 768px) {
.partners-section-header {
grid-template-columns: 1fr;
}
.partners-compare-grid {
grid-template-columns: 1fr;
}
.partner-form-grid {
grid-template-columns: 1fr;
}
/* Header row redundant once each value is labelled inline. */
.partners-compare-header {
display: none;
}
.pc-cat {
padding-bottom: 4px !important;
}
.pc-val {
padding-top: 10px !important;
padding-bottom: 12px !important;
}
.pc-key {
display: block;
font-family: 'JetBrains Mono', monospace;
font-size: 10px;
letter-spacing: 0.1em;
text-transform: uppercase;
color: rgba(10, 10, 10, 0.5);
margin-bottom: 4px;
font-weight: 500;
}
}
</style>