- ChatBubble: useActionSheet replaces custom Modal (native iOS popup, Android bottom sheet) - DM mode (isDM prop): hides like-count, shows Insta-style heart badge under bubble when liked - Group chat unchanged - Cleanup: remove unused Modal/Platform imports, sheet styles, actionsOpen state - deploy.sh: auto-detect ANDROID_HOME + auto-create local.properties for local Gradle - NEXT_RELEASE.md: DM reactions release note - Includes other staged work across binder-mac, marketing, ops/mdm, ios/
287 lines
14 KiB
Vue
287 lines
14 KiB
Vue
<template>
|
||
<div class="min-h-screen bg-default font-sans pb-16 md:pb-0">
|
||
<!-- Header -->
|
||
<div v-motion :initial="{ opacity: 0, y: -30 }" :visible="{ opacity: 1, y: 0, transition: { duration: 600 } }"
|
||
class="pt-10 pb-12 text-center px-4">
|
||
<div
|
||
class="inline-flex items-center gap-2 bg-amber-950/60 border border-amber-700/40 rounded-full px-4 py-1.5 text-sm text-amber-300 mb-4 animate-pulse">
|
||
<UIcon name="i-heroicons-fire" class="text-amber-400" />
|
||
{{ $t('pricing.founding_banner') }}
|
||
</div>
|
||
|
||
<div
|
||
class="inline-flex items-center gap-2 bg-primary-950/60 border border-primary-800/40 rounded-full px-4 py-1.5 text-sm text-primary-300 mb-6">
|
||
<UIcon name="i-heroicons-sparkles" class="text-primary-400" />
|
||
{{ $t('pricing.title') }}
|
||
</div>
|
||
|
||
<h1 class="text-4xl md:text-5xl font-extrabold text-highlighted mb-4">
|
||
{{ $t('pricing.subtitle_start') }}<br />
|
||
<span
|
||
class="text-transparent bg-clip-text bg-linear-to-r from-primary-400 via-primary-300 to-green-400">
|
||
{{ $t('pricing.subtitle_end') }}
|
||
</span>
|
||
</h1>
|
||
|
||
<!-- Billing Cycle Picker -->
|
||
<div class="flex items-center justify-center gap-2 mt-8">
|
||
<button v-for="opt in billingOptions" :key="opt.value" @click="billing = opt.value"
|
||
class="relative px-4 py-2 rounded-full text-sm font-semibold transition-all"
|
||
:class="billing === opt.value ? 'bg-primary-700 text-white' : 'bg-muted text-muted hover:text-highlighted'">
|
||
{{ opt.label }}
|
||
<span v-if="opt.badge"
|
||
class="absolute -top-2 -right-2 bg-green-500 text-white text-[9px] font-bold px-1.5 py-0.5 rounded-full">{{
|
||
opt.badge }}</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Pricing Plans -->
|
||
<div v-motion :initial="{ opacity: 0, y: 40 }"
|
||
:visible="{ opacity: 1, y: 0, transition: { duration: 700, delay: 100 } }"
|
||
class="px-4 pb-16 max-w-5xl mx-auto">
|
||
<UPricingPlans :plans="plans" />
|
||
</div>
|
||
|
||
<!-- Feature Comparison -->
|
||
<div v-motion :initial="{ opacity: 0, y: 50 }" :visible="{ opacity: 1, y: 0, transition: { duration: 700 } }"
|
||
class="px-4 pb-12 max-w-3xl mx-auto">
|
||
<div class="bg-elevated border border-purple-800/30 rounded-2xl p-6 md:p-8">
|
||
<div class="flex items-start gap-4">
|
||
<div class="w-10 h-10 rounded-xl bg-purple-800/30 flex items-center justify-center shrink-0 mt-0.5">
|
||
<UIcon name="i-heroicons-shield-check" class="text-purple-400 text-xl" />
|
||
</div>
|
||
<div>
|
||
<h3 class="font-bold text-highlighted text-lg mb-2">{{ $t('pricing.pro_meaning_title') }}</h3>
|
||
<p class="text-muted text-sm leading-relaxed">
|
||
{{ $t('pricing.pro_meaning_desc') }}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Feature Comparison Table -->
|
||
<div v-motion :initial="{ opacity: 0, y: 50 }" :visible="{ opacity: 1, y: 0, transition: { duration: 700 } }"
|
||
class="px-4 pb-12 max-w-4xl mx-auto">
|
||
<h2 class="text-2xl font-extrabold text-highlighted text-center mb-2">
|
||
{{ $t('pricing.comparison_title') }}
|
||
</h2>
|
||
<p class="text-muted text-center text-sm mb-10">
|
||
{{ $t('pricing.comparison_subtitle') }}
|
||
</p>
|
||
<div class="bg-elevated border border-default rounded-2xl overflow-hidden">
|
||
<table class="w-full text-sm">
|
||
<thead>
|
||
<tr class="border-b border-default">
|
||
<th class="text-left p-4 text-muted font-semibold">{{ $t('pricing.feature') }}</th>
|
||
<th class="p-4 text-center font-semibold text-xs text-primary-300">Pro</th>
|
||
<th class="p-4 text-center text-purple-400 font-semibold text-xs">Legend</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="(row, i) in comparisonRows" :key="row.label"
|
||
:class="i % 2 === 0 ? 'bg-white/2' : ''">
|
||
<td class="p-4 text-default font-medium">{{ row.label }}</td>
|
||
<td class="p-4 text-center">
|
||
<UIcon v-if="row.pro === true" name="i-heroicons-check-circle"
|
||
class="text-primary-400 text-lg" />
|
||
<span v-else-if="typeof row.pro === 'string'"
|
||
class="text-primary-300 text-xs font-semibold">{{ row.pro }}</span>
|
||
<UIcon v-else name="i-heroicons-minus" class="text-dimmed" />
|
||
</td>
|
||
<td class="p-4 text-center">
|
||
<UIcon v-if="row.legend === true" name="i-heroicons-check-circle"
|
||
class="text-purple-400 text-lg" />
|
||
<span v-else-if="typeof row.legend === 'string'"
|
||
class="text-purple-300 text-xs font-semibold">{{ row.legend }}</span>
|
||
<UIcon v-else name="i-heroicons-minus" class="text-dimmed" />
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Quotes -->
|
||
<div v-motion :initial="{ opacity: 0, y: 40 }" :visible="{ opacity: 1, y: 0, transition: { duration: 700 } }"
|
||
class="px-4 pb-24 max-w-4xl mx-auto">
|
||
<h2 class="text-2xl font-extrabold text-highlighted text-center mb-2">
|
||
{{ $t('pricing.quotes_title') }}
|
||
</h2>
|
||
<p class="text-muted text-center text-sm mb-10">{{ $t('pricing.quotes_subtitle') }}</p>
|
||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||
<UPageCard v-for="(q, i) in quotes" :key="q.author" v-motion :initial="{ opacity: 0, y: 30 }"
|
||
:visible="{ opacity: 1, y: 0, transition: { duration: 500, delay: i * 120 } }">
|
||
<div class="flex items-center gap-3 mb-4">
|
||
<UAvatar :src="q.image" :text="q.initials" size="md" class="ring-2 ring-white/10 shrink-0" />
|
||
<div>
|
||
<div class="text-highlighted text-sm font-semibold leading-tight">{{ q.author }}</div>
|
||
<div class="text-muted text-xs">{{ q.role }}</div>
|
||
</div>
|
||
</div>
|
||
<p class="text-default text-sm leading-relaxed italic">“ {{ q.text }} ”</p>
|
||
</UPageCard>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- FAQ -->
|
||
<div v-motion :initial="{ opacity: 0, y: 40 }" :visible="{ opacity: 1, y: 0, transition: { duration: 700 } }"
|
||
class="px-4 pb-24 max-w-2xl mx-auto">
|
||
<h2 class="text-2xl font-extrabold text-highlighted text-center mb-8">{{ $t('pricing.faq_title') }}</h2>
|
||
<UAccordion :items="faqItems" />
|
||
</div>
|
||
|
||
<!-- CTA -->
|
||
<div v-motion :initial="{ opacity: 0, scale: 0.95 }"
|
||
:visible="{ opacity: 1, scale: 1, transition: { duration: 600 } }"
|
||
class="px-4 pb-32 max-w-xl mx-auto text-center">
|
||
<h2 class="text-3xl font-extrabold text-highlighted mb-3">{{ $t('pricing.cta_title') }}</h2>
|
||
<p class="text-muted mb-6">{{ $t('pricing.cta_desc') }}</p>
|
||
<a href="https://apps.apple.com/app/rebreak" target="_blank" rel="noopener">
|
||
<UButton size="xl" class="font-bold px-10 rounded-full">
|
||
{{ $t('pricing.cta_button') }}
|
||
<template #trailing>
|
||
<UIcon name="i-heroicons-arrow-right" />
|
||
</template>
|
||
</UButton>
|
||
</a>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import type { PricingPlanProps } from "@nuxt/ui";
|
||
|
||
definePageMeta({ layout: "default" });
|
||
|
||
const { t } = useI18n();
|
||
const billing = ref<'monthly' | 'yearly'>('monthly');
|
||
|
||
const billingOptions = computed(() => [
|
||
{ value: 'monthly', label: t('pricing.billing_monthly'), badge: null },
|
||
{ value: 'yearly', label: t('pricing.billing_yearly'), badge: t('pricing.billing_save_pct') },
|
||
] as const);
|
||
|
||
const proMonthly = 3.99;
|
||
const legendMonthly = 7.99;
|
||
|
||
const proPrice = computed(() => {
|
||
if (billing.value === 'yearly') return (29 / 12).toFixed(2);
|
||
return proMonthly.toFixed(2);
|
||
});
|
||
const legendPrice = computed(() => {
|
||
if (billing.value === 'yearly') return (59 / 12).toFixed(2);
|
||
return legendMonthly.toFixed(2);
|
||
});
|
||
const billingCycleLabel = computed(() => {
|
||
if (billing.value === 'yearly') return t('pricing.billing_per_year');
|
||
return t('pricing.billing_per_month');
|
||
});
|
||
|
||
// Marketing: alle Plan-Buttons zeigen auf App-Store
|
||
const appStoreUrl = "https://apps.apple.com/app/rebreak";
|
||
|
||
const plans = computed<PricingPlanProps[]>(() => [
|
||
{
|
||
title: t('pricing.plan_pro_title'),
|
||
description: t('pricing.plan_pro_desc'),
|
||
price: `${proPrice.value}€`,
|
||
billingCycle: billingCycleLabel.value,
|
||
scale: true,
|
||
badge: t('pricing.plan_recommended'),
|
||
features: [
|
||
t('pricing.feat_pro_devices'),
|
||
t('pricing.feat_blocklist'),
|
||
t('pricing.feat_pro_domains'),
|
||
t('pricing.feat_pro_mail'),
|
||
t('pricing.feat_coach_pro'),
|
||
t('pricing.feat_streak'),
|
||
t('pricing.feat_urge_stats'),
|
||
t('pricing.feat_community_post'),
|
||
],
|
||
button: {
|
||
label: t('pricing.plan_pro_btn'),
|
||
to: appStoreUrl,
|
||
target: "_blank",
|
||
},
|
||
},
|
||
{
|
||
title: t('pricing.plan_legend_title'),
|
||
description: t('pricing.plan_legend_desc'),
|
||
price: `${legendPrice.value}€`,
|
||
billingCycle: billingCycleLabel.value,
|
||
features: [
|
||
t('pricing.feat_all_pro'),
|
||
t('pricing.feat_legend_devices'),
|
||
t('pricing.feat_legend_domains'),
|
||
t('pricing.feat_legend_mail'),
|
||
t('pricing.feat_legend_binder'),
|
||
t('pricing.feat_legend_add'),
|
||
t('pricing.feat_legend_validate'),
|
||
t('pricing.feat_legend_groups'),
|
||
t('pricing.feat_coach_legend'),
|
||
],
|
||
button: {
|
||
label: t('pricing.plan_legend_btn'),
|
||
to: appStoreUrl,
|
||
target: "_blank",
|
||
color: "neutral" as const,
|
||
variant: "subtle" as const,
|
||
},
|
||
},
|
||
]);
|
||
|
||
const comparisonRows = computed(() => [
|
||
{ label: t('pricing.comp_devices'), pro: t('pricing.comp_pro_devices'), legend: t('pricing.comp_legend_devices') },
|
||
{ label: t('pricing.comp_domains'), pro: t('pricing.comp_pro_domains'), legend: t('pricing.comp_legend_domains') },
|
||
{ label: t('pricing.comp_mail'), pro: t('pricing.comp_pro_mail_val'), legend: t('pricing.comp_legend_mail_val') },
|
||
{ label: t('pricing.comp_coach'), pro: t('pricing.comp_pro_coach_val'), legend: t('pricing.comp_legend_coach_val') },
|
||
{ label: t('pricing.comp_blocklist'), pro: true, legend: true },
|
||
{ label: t('pricing.comp_streak'), pro: true, legend: true },
|
||
{ label: t('pricing.comp_urge'), pro: true, legend: true },
|
||
{ label: t('pricing.comp_sos'), pro: true, legend: true },
|
||
{ label: t('pricing.comp_community'), pro: true, legend: true },
|
||
{ label: t('pricing.comp_post'), pro: true, legend: true },
|
||
{ label: t('pricing.comp_buddy'), pro: true, legend: true },
|
||
{ label: t('pricing.comp_urge_stats'), pro: true, legend: true },
|
||
{ label: t('pricing.comp_binder'), pro: false, legend: true },
|
||
{ label: t('pricing.comp_add_domain'), pro: false, legend: true },
|
||
{ label: t('pricing.comp_validate'), pro: false, legend: true },
|
||
{ label: t('pricing.comp_groups'), pro: false, legend: true },
|
||
]);
|
||
|
||
const quotes = [
|
||
{
|
||
text: "Zwischen Reiz und Reaktion liegt ein Raum. In diesem Raum liegt unsere Macht, unsere Reaktion zu wählen.",
|
||
author: "Viktor Frankl",
|
||
role: "Psychiater & Logotherapeut",
|
||
image: "https://upload.wikimedia.org/wikipedia/commons/thumb/8/80/Viktor_Frankl2.jpg/200px-Viktor_Frankl2.jpg",
|
||
initials: "VF",
|
||
},
|
||
{
|
||
text: "Bis du das Unbewusste bewusst machst, wird es dein Leben lenken – und du wirst es Schicksal nennen.",
|
||
author: "Carl Gustav Jung",
|
||
role: "Psychiater & Begründer der analytischen Psychologie",
|
||
image: "https://upload.wikimedia.org/wikipedia/commons/thumb/0/00/CGJung.jpg/200px-CGJung.jpg",
|
||
initials: "CJ",
|
||
},
|
||
{
|
||
text: "Sucht ist keine Charakterschwäche. Sie ist eine Erkrankung des Gehirns – und sie ist behandelbar.",
|
||
author: "Nora Volkow",
|
||
role: "Neurowissenschaftlerin, Direktorin des NIDA",
|
||
image: "https://upload.wikimedia.org/wikipedia/commons/thumb/9/9b/Nora_Volkow2.jpg/200px-Nora_Volkow2.jpg",
|
||
initials: "NV",
|
||
},
|
||
];
|
||
|
||
const faqItems = computed(() => [
|
||
{ label: t('pricing.faq1_q'), content: t('pricing.faq1_a') },
|
||
{ label: t('pricing.faq2_q'), content: t('pricing.faq2_a') },
|
||
{ label: t('pricing.faq3_q'), content: t('pricing.faq3_a') },
|
||
{ label: t('pricing.faq4_q'), content: t('pricing.faq4_a') },
|
||
{ label: t('pricing.faq5_q'), content: t('pricing.faq5_a') },
|
||
{ label: t('pricing.faq6_q'), content: t('pricing.faq6_a') },
|
||
]);
|
||
</script>
|