chahinebrini b31066a04c feat(chat): native action sheet + Insta-style heart for DM messages
- 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/
2026-05-30 09:14:32 +02:00

287 lines
14 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.

<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">&ldquo; {{ q.text }} &rdquo;</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>