Add animated verify-campaign tutorial after publishing statement

Once an organization publishes its verifier statement on /organizations,
show an interactive walkthrough demonstrating how to verify a campaign:
a looping three-step animation on a mock campaign card where a cursor opens
the three-dots menu and selects 'Verify this campaign'. Step list is
clickable to scrub; motion is gated behind prefers-reduced-motion.
This commit is contained in:
Alex Gleason
2026-06-12 16:00:35 -05:00
parent 818afe9bbf
commit e196227a23
19 changed files with 671 additions and 19 deletions
@@ -0,0 +1,337 @@
import { useEffect, useReducer, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import {
BadgeCheck,
MoreHorizontal,
MousePointer2,
} from 'lucide-react';
import { cn } from '@/lib/utils';
/**
* An animated, interactive tutorial shown on /organizations once an
* organization has published its verifier statement. It demonstrates the
* exact gesture a verifier uses to vouch for a campaign: tapping the
* three-dots (kebab) button on a campaign card and choosing
* "Verify this campaign".
*
* The component renders a faithful mock campaign card and drives a small
* three-step state machine that mimics a cursor opening the kebab menu and
* clicking the verify item. It auto-advances on a timer, loops, and exposes
* clickable step dots so users can scrub. Motion is fully gated behind
* `motion-safe:` / a `prefers-reduced-motion` check — with reduced motion the
* cursor and looping are disabled and the final state is shown statically.
*/
type Phase = 'idle' | 'menuOpen' | 'verified';
const PHASE_ORDER: Phase[] = ['idle', 'menuOpen', 'verified'];
// How long each phase is held before auto-advancing (ms).
const PHASE_DURATION: Record<Phase, number> = {
idle: 2200,
menuOpen: 2600,
verified: 3000,
};
interface State {
phase: Phase;
/** Bumps on every manual interaction to pause autoplay briefly. */
paused: boolean;
}
type Action =
| { type: 'advance' }
| { type: 'goto'; phase: Phase };
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'advance': {
const idx = PHASE_ORDER.indexOf(state.phase);
const next = PHASE_ORDER[(idx + 1) % PHASE_ORDER.length];
return { phase: next, paused: false };
}
case 'goto':
return { phase: action.phase, paused: true };
default:
return state;
}
}
function usePrefersReducedMotion(): boolean {
const ref = useRef(false);
if (typeof window !== 'undefined' && typeof window.matchMedia === 'function') {
ref.current = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
}
return ref.current;
}
export function VerifyTutorial({ className }: { className?: string }) {
const { t } = useTranslation();
const reducedMotion = usePrefersReducedMotion();
const [state, dispatch] = useReducer(reducer, {
phase: (reducedMotion ? 'verified' : 'idle') as Phase,
paused: false,
});
// Autoplay timer. Disabled under reduced motion, or while paused after a
// manual interaction (resumes on the next phase change).
useEffect(() => {
if (reducedMotion || state.paused) return;
const id = window.setTimeout(
() => dispatch({ type: 'advance' }),
PHASE_DURATION[state.phase],
);
return () => window.clearTimeout(id);
}, [state.phase, state.paused, reducedMotion]);
// When a user scrubs (paused), resume autoplay after a grace period.
useEffect(() => {
if (!state.paused || reducedMotion) return;
const id = window.setTimeout(
() => dispatch({ type: 'advance' }),
PHASE_DURATION[state.phase] + 1500,
);
return () => window.clearTimeout(id);
}, [state.paused, state.phase, reducedMotion]);
const phaseIndex = PHASE_ORDER.indexOf(state.phase);
const menuVisible = state.phase === 'menuOpen' || state.phase === 'verified';
const verified = state.phase === 'verified';
const stepCopy = [
{
title: t('organizations.tutorial.steps.open.title'),
body: t('organizations.tutorial.steps.open.body'),
},
{
title: t('organizations.tutorial.steps.verify.title'),
body: t('organizations.tutorial.steps.verify.body'),
},
{
title: t('organizations.tutorial.steps.confirm.title'),
body: t('organizations.tutorial.steps.confirm.body'),
},
];
return (
<section
className={cn(
'rounded-2xl border border-primary/20 bg-gradient-to-br from-primary/[0.07] via-background to-background p-6 sm:p-8 shadow-sm',
className,
)}
aria-labelledby="verify-tutorial-title"
>
{/* Header */}
<div className="flex flex-wrap items-start justify-between gap-4 mb-8">
<div className="max-w-md">
<p className="inline-flex items-center gap-1.5 text-xs font-semibold tracking-widest uppercase text-primary mb-2">
<BadgeCheck className="size-4" />
{t('organizations.tutorial.eyebrow')}
</p>
<h3
id="verify-tutorial-title"
className="text-xl sm:text-2xl font-bold tracking-tight mb-2"
>
{t('organizations.tutorial.title')}
</h3>
<p className="text-sm text-muted-foreground leading-relaxed">
{t('organizations.tutorial.lede')}
</p>
</div>
</div>
<div className="grid gap-8 lg:grid-cols-2 lg:items-center">
{/* ── Left: animated mock campaign card ───────────────────────── */}
<DemoStage
phaseIndex={phaseIndex}
menuVisible={menuVisible}
verified={verified}
reducedMotion={reducedMotion}
/>
{/* ── Right: step list, synced to the animation ───────────────── */}
<ol className="space-y-3">
{stepCopy.map((step, i) => {
const active = i === phaseIndex;
const done = i < phaseIndex;
return (
<li key={step.title}>
<button
type="button"
onClick={() => dispatch({ type: 'goto', phase: PHASE_ORDER[i] })}
aria-current={active ? 'step' : undefined}
className={cn(
'group flex w-full items-start gap-4 rounded-xl border p-4 text-left transition-all',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/60',
active
? 'border-primary/40 bg-primary/5 shadow-sm'
: 'border-border/60 bg-background hover:border-primary/30 hover:bg-muted/40',
)}
>
<span
className={cn(
'flex size-8 shrink-0 items-center justify-center rounded-full text-sm font-bold transition-colors',
done
? 'bg-primary text-primary-foreground'
: active
? 'bg-primary/15 text-primary ring-2 ring-primary/40'
: 'bg-muted text-muted-foreground',
)}
>
{done ? <BadgeCheck className="size-4" /> : i + 1}
</span>
<span className="space-y-1">
<span
className={cn(
'block text-sm font-semibold leading-snug',
active ? 'text-foreground' : 'text-foreground/90',
)}
>
{step.title}
</span>
<span className="block text-sm text-muted-foreground leading-relaxed">
{step.body}
</span>
</span>
</button>
</li>
);
})}
</ol>
</div>
</section>
);
}
// ── The animated mock card ───────────────────────────────────────────────
interface DemoStageProps {
phaseIndex: number;
menuVisible: boolean;
verified: boolean;
reducedMotion: boolean;
}
function DemoStage({
phaseIndex,
menuVisible,
verified,
reducedMotion,
}: DemoStageProps) {
const { t } = useTranslation();
return (
<div className="relative mx-auto w-full max-w-sm select-none" aria-hidden="true">
{/* Mock campaign card */}
<div className="overflow-hidden rounded-2xl border border-border/60 bg-card shadow-md">
{/* Banner */}
<div className="relative h-40 bg-gradient-to-br from-sky-500/80 via-cyan-500/70 to-emerald-500/80">
<div
aria-hidden
className="absolute inset-0 opacity-30 mix-blend-overlay"
style={{
backgroundImage:
'radial-gradient(circle at 30% 30%, rgba(255,255,255,0.5), transparent 45%)',
}}
/>
{/* Verified badge (top-left) — appears in the final phase */}
<div
className={cn(
'absolute left-3 top-3 flex items-center gap-1.5 rounded-full bg-background/90 px-2.5 py-1 text-xs font-semibold text-foreground shadow-sm backdrop-blur transition-all duration-500',
verified
? 'opacity-100 translate-y-0'
: 'opacity-0 -translate-y-1 pointer-events-none',
)}
>
<BadgeCheck className="size-4 text-primary" />
{t('organizations.tutorial.demo.verifiedBadge')}
</div>
{/* Three-dots button (top-right) */}
<div className="absolute right-3 top-3">
<div
className={cn(
'flex size-8 items-center justify-center rounded-md bg-background/80 text-muted-foreground backdrop-blur transition-all duration-300',
phaseIndex === 0 &&
!reducedMotion &&
'motion-safe:animate-pulse ring-2 ring-primary/60',
menuVisible && 'bg-background text-foreground ring-2 ring-primary/50',
)}
>
<MoreHorizontal className="size-4" />
</div>
{/* Dropdown menu */}
<div
className={cn(
'absolute right-0 top-10 z-20 w-52 origin-top-right rounded-md border bg-popover p-1 text-popover-foreground shadow-lg transition-all duration-200',
menuVisible
? 'scale-100 opacity-100'
: 'pointer-events-none scale-95 opacity-0',
)}
>
<div
className={cn(
'flex items-center gap-2 rounded-sm px-2 py-2 text-sm font-medium transition-colors',
phaseIndex >= 1
? 'bg-primary/10 text-primary'
: 'text-foreground',
)}
>
<BadgeCheck className="size-4 shrink-0" />
{t('organizations.tutorial.demo.menuVerify')}
</div>
</div>
</div>
</div>
{/* Card body */}
<div className="space-y-3 p-4">
<div>
<p className="font-semibold leading-snug">
{t('organizations.tutorial.demo.campaignTitle')}
</p>
<p className="text-xs text-muted-foreground">
{t('organizations.tutorial.demo.campaignOrganizer')}
</p>
</div>
{/* Fake progress bar */}
<div className="space-y-1.5">
<div className="h-2 w-full overflow-hidden rounded-full bg-muted">
<div className="h-full w-2/3 rounded-full bg-primary" />
</div>
<div className="flex justify-between text-xs text-muted-foreground">
<span>0.45 BTC</span>
<span>67%</span>
</div>
</div>
</div>
</div>
{/* Animated cursor — hidden under reduced motion */}
{!reducedMotion && (
<div
className={cn(
'pointer-events-none absolute z-30 transition-all duration-700 ease-out',
// idle → hover the kebab (top-right); menuOpen/verified → hover the verify item
phaseIndex === 0
? 'right-4 top-5'
: 'right-8 top-[4.5rem]',
)}
>
<MousePointer2
className={cn(
'size-6 fill-foreground text-background drop-shadow-md transition-transform',
'motion-safe:animate-tutorial-tap',
)}
/>
</div>
)}
</div>
);
}
export default VerifyTutorial;
+17 -1
View File
@@ -1076,7 +1076,23 @@
"lede": "سجّل الدخول بملف منظّمتك الشخصي لنشر بيان التحقق الخاص بك أو تحديثه أو سحبه."
},
"loginGateTitle": "سجّل الدخول بملف منظّمتك الشخصي",
"loginGateBody": "سجّل الدخول بملف منظّمتك الشخصي على Nostr، أو أنشئ واحداً، للبدء. بمجرد تسجيل دخولك، يمكنك نشر بيان التحقق الخاص بك هنا."
"loginGateBody": "سجّل الدخول بملف منظّمتك الشخصي على Nostr، أو أنشئ واحداً، للبدء. بمجرد تسجيل دخولك، يمكنك نشر بيان التحقق الخاص بك هنا.",
"tutorial": {
"eyebrow": "أنت الآن مُوثِّق",
"title": "كيفية توثيق حملة",
"lede": "بيانك منشور الآن. إليك كيف توثّق حملة تثق بها — يكفي نقرتان من أي بطاقة حملة.",
"steps": {
"open": { "title": "افتح القائمة", "body": "في أي بطاقة حملة، انقر على زر النقاط الثلاث في الزاوية العلوية اليمنى من اللافتة." },
"verify": { "title": "اختر \"وثّق هذه الحملة\"", "body": "تكشف القائمة عن إجراء التوثيق — وهو مرئي فقط للمشرفين والمُوثِّقين مثلك." },
"confirm": { "title": "أكِّد وانتهيت", "body": "أقرّ بأن الحملة أصلية. تنضم شارتك إلى البطاقة ليعلم المتبرعون أنك تقف خلفها." }
},
"demo": {
"campaignTitle": "مياه نظيفة لموانزا",
"campaignOrganizer": "بواسطة مؤسسة Mradi",
"menuVerify": "وثّق هذه الحملة",
"verifiedBadge": "موثّقة بواسطتك"
}
}
},
"organizers": {
"appoint": "تعيين منظّم",
+26 -1
View File
@@ -1539,7 +1539,32 @@
"lede": "Sign in with your organization's profile to publish, update, or withdraw your verification statement."
},
"loginGateTitle": "Sign in with your organization's profile",
"loginGateBody": "Log in with your organization's Nostr profile, or create one, to get started. Once you're signed in, you can publish your verification statement here."
"loginGateBody": "Log in with your organization's Nostr profile, or create one, to get started. Once you're signed in, you can publish your verification statement here.",
"tutorial": {
"eyebrow": "You're a verifier now",
"title": "How to verify a campaign",
"lede": "Your statement is live. Here's how to vouch for a campaign you trust — it takes two taps from any campaign card.",
"steps": {
"open": {
"title": "Open the menu",
"body": "On any campaign card, tap the three-dots button in the top-right corner of the banner."
},
"verify": {
"title": "Choose \"Verify this campaign\"",
"body": "The menu reveals a verify action — visible only to moderators and verifiers like you."
},
"confirm": {
"title": "Confirm & you're done",
"body": "Attest the campaign is authentic. Your badge joins the card so donors know you stand behind it."
}
},
"demo": {
"campaignTitle": "Clean Water for Mwanza",
"campaignOrganizer": "by Mradi Foundation",
"menuVerify": "Verify this campaign",
"verifiedBadge": "Verified by you"
}
}
},
"organizers": {
"appoint": "Appoint Organizer",
+26 -1
View File
@@ -1086,7 +1086,32 @@
"lede": "Inicia sesión con el perfil de tu organización para publicar, actualizar o retirar tu declaración de verificación."
},
"loginGateTitle": "Inicia sesión con el perfil de tu organización",
"loginGateBody": "Inicia sesión con el perfil de Nostr de tu organización, o crea uno, para empezar. Una vez que hayas iniciado sesión, podrás publicar aquí tu declaración de verificación."
"loginGateBody": "Inicia sesión con el perfil de Nostr de tu organización, o crea uno, para empezar. Una vez que hayas iniciado sesión, podrás publicar aquí tu declaración de verificación.",
"tutorial": {
"eyebrow": "Ahora eres verificador",
"title": "Cómo verificar una campaña",
"lede": "Tu declaración ya está publicada. Así puedes respaldar una campaña en la que confías: solo dos toques desde cualquier tarjeta de campaña.",
"steps": {
"open": {
"title": "Abre el menú",
"body": "En cualquier tarjeta de campaña, toca el botón de tres puntos en la esquina superior derecha del banner."
},
"verify": {
"title": "Elige \"Verificar esta campaña\"",
"body": "El menú muestra una acción de verificación, visible solo para moderadores y verificadores como tú."
},
"confirm": {
"title": "Confirma y listo",
"body": "Da fe de que la campaña es auténtica. Tu insignia se une a la tarjeta para que los donantes sepan que la respaldas."
}
},
"demo": {
"campaignTitle": "Agua limpia para Mwanza",
"campaignOrganizer": "de la Fundación Mradi",
"menuVerify": "Verificar esta campaña",
"verifiedBadge": "Verificada por ti"
}
}
},
"organizers": {
"appoint": "Designar Organizador",
+17 -1
View File
@@ -1086,7 +1086,23 @@
"lede": "با نمایهٔ سازمان خود وارد شوید تا بیانیهٔ تأیید خود را منتشر، به‌روزرسانی یا حذف کنید."
},
"loginGateTitle": "با نمایهٔ سازمان خود وارد شوید",
"loginGateBody": "برای شروع، با نمایهٔ Nostr سازمان خود وارد شوید یا یکی بسازید. پس از ورود، می‌توانید بیانیهٔ تأیید خود را اینجا منتشر کنید."
"loginGateBody": "برای شروع، با نمایهٔ Nostr سازمان خود وارد شوید یا یکی بسازید. پس از ورود، می‌توانید بیانیهٔ تأیید خود را اینجا منتشر کنید.",
"tutorial": {
"eyebrow": "اکنون شما یک تأییدکننده هستید",
"title": "چگونه یک کمپین را تأیید کنیم",
"lede": "بیانیهٔ شما فعال است. در اینجا می‌بینید که چگونه کمپینی را که به آن اعتماد دارید تأیید کنید — تنها با دو ضربه از روی هر کارت کمپین انجام می‌شود.",
"steps": {
"open": { "title": "منو را باز کنید", "body": "روی هر کارت کمپین، دکمهٔ سه‌نقطه را در گوشهٔ بالا سمت راست بنر لمس کنید." },
"verify": { "title": "گزینهٔ \"این کمپین را تأیید کنید\" را انتخاب کنید", "body": "منو یک کنش تأیید را نمایش می‌دهد — که فقط برای ناظران و تأییدکنندگانی مانند شما قابل مشاهده است." },
"confirm": { "title": "تأیید کنید و کار تمام است", "body": "گواهی دهید که کمپین معتبر است. نشان شما به کارت افزوده می‌شود تا اهداکنندگان بدانند که شما پشتیبان آن هستید." }
},
"demo": {
"campaignTitle": "آب پاکیزه برای Mwanza",
"campaignOrganizer": "توسط بنیاد Mradi",
"menuVerify": "این کمپین را تأیید کنید",
"verifiedBadge": "تأییدشده توسط شما"
}
}
},
"organizers": {
"appoint": "تعیین سازمان‌دهنده",
+26 -1
View File
@@ -1525,7 +1525,32 @@
"lede": "Connectez-vous avec le profil de votre organisation pour publier, mettre à jour ou retirer votre déclaration de vérification."
},
"loginGateTitle": "Connectez-vous avec le profil de votre organisation",
"loginGateBody": "Connectez-vous avec le profil Nostr de votre organisation, ou créez-en un, pour commencer. Une fois connecté, vous pourrez publier votre déclaration de vérification ici."
"loginGateBody": "Connectez-vous avec le profil Nostr de votre organisation, ou créez-en un, pour commencer. Une fois connecté, vous pourrez publier votre déclaration de vérification ici.",
"tutorial": {
"eyebrow": "Vous êtes désormais vérificateur",
"title": "Comment vérifier une campagne",
"lede": "Votre déclaration est en ligne. Voici comment vous porter garant d'une campagne en laquelle vous avez confiance — deux clics suffisent depuis n'importe quelle carte de campagne.",
"steps": {
"open": {
"title": "Ouvrez le menu",
"body": "Sur n'importe quelle carte de campagne, appuyez sur le bouton à trois points dans le coin supérieur droit de la bannière."
},
"verify": {
"title": "Choisissez \"Vérifier cette campagne\"",
"body": "Le menu révèle une action de vérification — visible uniquement par les modérateurs et les vérificateurs comme vous."
},
"confirm": {
"title": "Confirmez, et c'est terminé",
"body": "Attestez que la campagne est authentique. Votre badge rejoint la carte afin que les donateurs sachent que vous la soutenez."
}
},
"demo": {
"campaignTitle": "De l'eau potable pour Mwanza",
"campaignOrganizer": "par la Fondation Mradi",
"menuVerify": "Vérifier cette campagne",
"verifiedBadge": "Vérifié par vous"
}
}
},
"organizers": {
"appoint": "Désigner un organisateur",
+17 -1
View File
@@ -1528,7 +1528,23 @@
"lede": "अपने सत्यापन वक्तव्य को प्रकाशित करने, अपडेट करने या वापस लेने के लिए अपने संगठन की प्रोफ़ाइल से साइन इन करें।"
},
"loginGateTitle": "अपने संगठन की प्रोफ़ाइल से साइन इन करें",
"loginGateBody": "शुरू करने के लिए अपने संगठन की Nostr प्रोफ़ाइल से लॉग इन करें, या एक बनाएँ। साइन इन करने के बाद, आप यहाँ अपना सत्यापन वक्तव्य प्रकाशित कर सकते हैं।"
"loginGateBody": "शुरू करने के लिए अपने संगठन की Nostr प्रोफ़ाइल से लॉग इन करें, या एक बनाएँ। साइन इन करने के बाद, आप यहाँ अपना सत्यापन वक्तव्य प्रकाशित कर सकते हैं।",
"tutorial": {
"eyebrow": "अब आप एक सत्यापनकर्ता हैं",
"title": "किसी अभियान को कैसे सत्यापित करें",
"lede": "आपका वक्तव्य लाइव है। जिस अभियान पर आप भरोसा करते हैं उसकी पुष्टि करने का तरीका यहाँ है — किसी भी अभियान कार्ड से बस दो टैप में हो जाता है।",
"steps": {
"open": { "title": "मेनू खोलें", "body": "किसी भी अभियान कार्ड पर, बैनर के ऊपरी-दाएँ कोने में तीन-बिंदु वाले बटन पर टैप करें।" },
"verify": { "title": "\"इस अभियान को सत्यापित करें\" चुनें", "body": "मेनू एक सत्यापन क्रिया दिखाता है — जो केवल आपके जैसे मॉडरेटर और सत्यापनकर्ताओं को ही दिखती है।" },
"confirm": { "title": "पुष्टि करें और हो गया", "body": "प्रमाणित करें कि अभियान प्रामाणिक है। आपका बैज कार्ड में जुड़ जाता है ताकि दानदाताओं को पता चले कि आप इसके पीछे खड़े हैं।" }
},
"demo": {
"campaignTitle": "Mwanza के लिए स्वच्छ जल",
"campaignOrganizer": "Mradi Foundation द्वारा",
"menuVerify": "इस अभियान को सत्यापित करें",
"verifiedBadge": "आपके द्वारा सत्यापित"
}
}
},
"organizers": {
"appoint": "ऑर्गनाइज़र नियुक्त करें",
+17 -1
View File
@@ -1528,7 +1528,23 @@
"lede": "Masuk dengan profil organisasi Anda untuk menerbitkan, memperbarui, atau menarik kembali pernyataan verifikasi Anda."
},
"loginGateTitle": "Masuk dengan profil organisasi Anda",
"loginGateBody": "Masuk dengan profil Nostr organisasi Anda, atau buat satu, untuk memulai. Setelah masuk, Anda dapat menerbitkan pernyataan verifikasi Anda di sini."
"loginGateBody": "Masuk dengan profil Nostr organisasi Anda, atau buat satu, untuk memulai. Setelah masuk, Anda dapat menerbitkan pernyataan verifikasi Anda di sini.",
"tutorial": {
"eyebrow": "Anda kini seorang verifikator",
"title": "Cara memverifikasi kampanye",
"lede": "Pernyataan Anda sudah aktif. Berikut cara menjamin kampanye yang Anda percaya — cukup dua ketukan dari kartu kampanye mana pun.",
"steps": {
"open": { "title": "Buka menu", "body": "Pada kartu kampanye mana pun, ketuk tombol tiga titik di sudut kanan atas banner." },
"verify": { "title": "Pilih \"Verifikasi kampanye ini\"", "body": "Menu menampilkan aksi verifikasi — hanya terlihat oleh moderator dan verifikator seperti Anda." },
"confirm": { "title": "Konfirmasi & selesai", "body": "Tegaskan bahwa kampanye ini autentik. Lencana Anda akan muncul di kartu sehingga para donatur tahu Anda mendukungnya." }
},
"demo": {
"campaignTitle": "Air Bersih untuk Mwanza",
"campaignOrganizer": "oleh Mradi Foundation",
"menuVerify": "Verifikasi kampanye ini",
"verifiedBadge": "Diverifikasi oleh Anda"
}
}
},
"organizers": {
"appoint": "Tunjuk Organizer",
+17 -1
View File
@@ -1086,7 +1086,23 @@
"lede": "ចូលគណនីដោយប្រើប្រវត្តិរូបអង្គការរបស់អ្នក ដើម្បីផ្សព្វផ្សាយ ធ្វើបច្ចុប្បន្នភាព ឬដកសេចក្តីថ្លែងការណ៍ផ្ទៀងផ្ទាត់របស់អ្នកចេញ។"
},
"loginGateTitle": "ចូលគណនីដោយប្រើប្រវត្តិរូបអង្គការរបស់អ្នក",
"loginGateBody": "ចូលគណនីដោយប្រើប្រវត្តិរូប Nostr របស់អង្គការអ្នក ឬបង្កើតមួយ ដើម្បីចាប់ផ្តើម។ នៅពេលអ្នកបានចូលគណនីហើយ អ្នកអាចផ្សព្វផ្សាយសេចក្តីថ្លែងការណ៍ផ្ទៀងផ្ទាត់របស់អ្នកនៅទីនេះ។"
"loginGateBody": "ចូលគណនីដោយប្រើប្រវត្តិរូប Nostr របស់អង្គការអ្នក ឬបង្កើតមួយ ដើម្បីចាប់ផ្តើម។ នៅពេលអ្នកបានចូលគណនីហើយ អ្នកអាចផ្សព្វផ្សាយសេចក្តីថ្លែងការណ៍ផ្ទៀងផ្ទាត់របស់អ្នកនៅទីនេះ។",
"tutorial": {
"eyebrow": "ឥឡូវនេះអ្នកជាអ្នកផ្ទៀងផ្ទាត់ហើយ",
"title": "របៀបផ្ទៀងផ្ទាត់យុទ្ធនាការ",
"lede": "សេចក្តីថ្លែងការណ៍របស់អ្នកកំពុងដំណើរការ។ នេះជារបៀបធានាដល់យុទ្ធនាការដែលអ្នកទុកចិត្ត — វាត្រូវការតែការប៉ះពីរដងពីកាតយុទ្ធនាការណាមួយប៉ុណ្ណោះ។",
"steps": {
"open": { "title": "បើកម៉ឺនុយ", "body": "នៅលើកាតយុទ្ធនាការណាមួយ សូមប៉ះប៊ូតុងចំណុចបីនៅជ្រុងខាងស្តាំផ្នែកខាងលើនៃផ្ទាំងបដា។" },
"verify": { "title": "ជ្រើសរើស \"ផ្ទៀងផ្ទាត់យុទ្ធនាការនេះ\"", "body": "ម៉ឺនុយនឹងបង្ហាញសកម្មភាពផ្ទៀងផ្ទាត់ — មើលឃើញតែសម្រាប់អ្នកសម្របសម្រួល និងអ្នកផ្ទៀងផ្ទាត់ដូចជាអ្នកប៉ុណ្ណោះ។" },
"confirm": { "title": "បញ្ជាក់ ហើយអ្នកបានបញ្ចប់", "body": "បញ្ជាក់ថាយុទ្ធនាការនេះពិតប្រាកដ។ ផ្លាកសញ្ញារបស់អ្នកនឹងភ្ជាប់ទៅកាត ដើម្បីឱ្យអ្នកបរិច្ចាគដឹងថាអ្នកគាំទ្រវា។" }
},
"demo": {
"campaignTitle": "ទឹកស្អាតសម្រាប់ Mwanza",
"campaignOrganizer": "ដោយ Mradi Foundation",
"menuVerify": "ផ្ទៀងផ្ទាត់យុទ្ធនាការនេះ",
"verifiedBadge": "បានផ្ទៀងផ្ទាត់ដោយអ្នក"
}
}
},
"organizers": {
"appoint": "តែងតាំងអ្នករៀបចំ",
+17 -1
View File
@@ -1088,7 +1088,23 @@
"lede": "د خپل سازمان د پروفایل سره ننوځئ ترڅو خپل د تصدیق بیان خپور، تازه، یا وباسئ."
},
"loginGateTitle": "د خپل سازمان د پروفایل سره ننوځئ",
"loginGateBody": "د پیل لپاره د خپل سازمان د Nostr پروفایل سره ننوځئ، یا یو جوړ کړئ. کله چې ننوتلي اوسئ، تاسو کولی شئ خپل د تصدیق بیان دلته خپور کړئ."
"loginGateBody": "د پیل لپاره د خپل سازمان د Nostr پروفایل سره ننوځئ، یا یو جوړ کړئ. کله چې ننوتلي اوسئ، تاسو کولی شئ خپل د تصدیق بیان دلته خپور کړئ.",
"tutorial": {
"eyebrow": "اوس تاسو تصدیق کوونکی یاست",
"title": "د کمپاین تصدیق کولو څرنګوالی",
"lede": "ستاسو بیان فعال دی. دلته دا دی چې څنګه د هغه کمپاین ضمانت وکړئ چې تاسو پرې باور لرئ — دا د هر کمپاین له کارت څخه دوه ټک نیسي.",
"steps": {
"open": { "title": "مینو پرانیځئ", "body": "د هر کمپاین په کارت کې، د بینر په پورتنۍ ښۍ کونج کې د درې ټکو تڼۍ ټک کړئ." },
"verify": { "title": "\"دا کمپاین تصدیق کړئ\" غوره کړئ", "body": "مینو د تصدیق کړنه ښکاره کوي — یوازې تاسو په څېر اعتدال کوونکو او تصدیق کوونکو ته ښکاري." },
"confirm": { "title": "تایید کړئ او کار مو پای ته ورسېد", "body": "تصدیق کړئ چې کمپاین اصلي دی. ستاسو نښان د کارت سره یوځای کېږي ترڅو بسپنه ورکوونکي پوه شي چې تاسو یې ملاتړ کوئ." }
},
"demo": {
"campaignTitle": "د موانزا لپاره پاکې اوبه",
"campaignOrganizer": "د Mradi بنسټ لخوا",
"menuVerify": "دا کمپاین تصدیق کړئ",
"verifiedBadge": "ستاسو لخوا تصدیق شوی"
}
}
},
"organizers": {
"appoint": "تنظیموونکی ټاکل",
+26 -1
View File
@@ -1530,7 +1530,32 @@
"lede": "Entre com o perfil da sua organização para publicar, atualizar ou retirar sua declaração de verificação."
},
"loginGateTitle": "Entre com o perfil da sua organização",
"loginGateBody": "Entre com o perfil Nostr da sua organização, ou crie um, para começar. Depois de entrar, você pode publicar sua declaração de verificação aqui."
"loginGateBody": "Entre com o perfil Nostr da sua organização, ou crie um, para começar. Depois de entrar, você pode publicar sua declaração de verificação aqui.",
"tutorial": {
"eyebrow": "Agora você é um verificador",
"title": "Como verificar uma campanha",
"lede": "Sua declaração está no ar. Veja como avalizar uma campanha em que você confia — bastam dois toques a partir de qualquer cartão de campanha.",
"steps": {
"open": {
"title": "Abra o menu",
"body": "Em qualquer cartão de campanha, toque no botão de três pontos no canto superior direito do banner."
},
"verify": {
"title": "Escolha \"Verificar esta campanha\"",
"body": "O menu revela uma ação de verificação — visível apenas para moderadores e verificadores como você."
},
"confirm": {
"title": "Confirme e pronto",
"body": "Ateste que a campanha é autêntica. Seu selo se une ao cartão para que os doadores saibam que você a apoia."
}
},
"demo": {
"campaignTitle": "Água Limpa para Mwanza",
"campaignOrganizer": "por Mradi Foundation",
"menuVerify": "Verificar esta campanha",
"verifiedBadge": "Verificado por você"
}
}
},
"organizers": {
"appoint": "Nomear organizador",
+26 -1
View File
@@ -1530,7 +1530,32 @@
"lede": "Войдите с профилем вашей организации, чтобы опубликовать, обновить или отозвать своё заявление о верификации."
},
"loginGateTitle": "Войдите с профилем вашей организации",
"loginGateBody": "Войдите с профилем Nostr вашей организации или создайте его, чтобы начать. После входа вы сможете опубликовать здесь своё заявление о верификации."
"loginGateBody": "Войдите с профилем Nostr вашей организации или создайте его, чтобы начать. После входа вы сможете опубликовать здесь своё заявление о верификации.",
"tutorial": {
"eyebrow": "Теперь вы верификатор",
"title": "Как верифицировать кампанию",
"lede": "Ваше заявление опубликовано. Вот как поручиться за кампанию, которой вы доверяете, — это всего два касания на любой карточке кампании.",
"steps": {
"open": {
"title": "Откройте меню",
"body": "На любой карточке кампании нажмите кнопку с тремя точками в правом верхнем углу баннера."
},
"verify": {
"title": "Выберите «Проверить эту кампанию»",
"body": "В меню появится действие верификации — оно видно только модераторам и верификаторам, таким как вы."
},
"confirm": {
"title": "Подтвердите — и готово",
"body": "Подтвердите подлинность кампании. Ваш значок появится на карточке, чтобы доноры знали, что вы за неё ручаетесь."
}
},
"demo": {
"campaignTitle": "Чистая вода для Mwanza",
"campaignOrganizer": "от Mradi Foundation",
"menuVerify": "Проверить эту кампанию",
"verifiedBadge": "Проверено вами"
}
}
},
"organizers": {
"appoint": "Назначить организатора",
+17 -1
View File
@@ -1088,7 +1088,23 @@
"lede": "Pinda nepurofairi yesangano rako kuti uburitse, ugadziridze, kana kubvisa chirevo chako chekusimbisa."
},
"loginGateTitle": "Pinda nepurofairi yesangano rako",
"loginGateBody": "Pinda nepurofairi yeNostr yesangano rako, kana kuti gadzira imwe, kuti utange. Kana wapinda, unogona kuburitsa chirevo chako chekusimbisa pano."
"loginGateBody": "Pinda nepurofairi yeNostr yesangano rako, kana kuti gadzira imwe, kuti utange. Kana wapinda, unogona kuburitsa chirevo chako chekusimbisa pano.",
"tutorial": {
"eyebrow": "Iwe zvino uri muongorori",
"title": "Mashandisirwo ekusimbisa mushandirapamwe",
"lede": "Chirevo chako chava pachena. Heano matanho ekutsigira mushandirapamwe waunovimba nawo — zvinotora kubata kaviri chete kubva pakadhi rapi nerapi remushandirapamwe.",
"steps": {
"open": { "title": "Vhura menyu", "body": "Pakadhi ripi neripi remushandirapamwe, baya bhatani remadhoti matatu kona yekumusoro kurudyi kwebhanga." },
"verify": { "title": "Sarudza \"Simbisa mushandirapamwe uyu\"", "body": "Menyu inoratidza chiito chekusimbisa — chinoonekwa chete nevatariri nevaongorori sewe." },
"confirm": { "title": "Simbisa uye wapedza", "body": "Pupura kuti mushandirapamwe ndewechokwadi. Bheji rako rinobatana nekadhi kuti vanopa vazive kuti unowutsigira." }
},
"demo": {
"campaignTitle": "Mvura Yakachena yeMwanza",
"campaignOrganizer": "naMradi Foundation",
"menuVerify": "Simbisa mushandirapamwe uyu",
"verifiedBadge": "Yasimbiswa newe"
}
}
},
"organizers": {
"appoint": "Sarudza Mugadziri",
+17 -1
View File
@@ -1527,7 +1527,23 @@
"lede": "Ingia kwa wasifu wa shirika lako ili kuchapisha, kusasisha, au kuondoa taarifa yako ya uthibitishaji."
},
"loginGateTitle": "Ingia kwa wasifu wa shirika lako",
"loginGateBody": "Ingia kwa wasifu wa Nostr wa shirika lako, au tengeneza mmoja, ili kuanza. Mara tu unapoingia, unaweza kuchapisha taarifa yako ya uthibitishaji hapa."
"loginGateBody": "Ingia kwa wasifu wa Nostr wa shirika lako, au tengeneza mmoja, ili kuanza. Mara tu unapoingia, unaweza kuchapisha taarifa yako ya uthibitishaji hapa.",
"tutorial": {
"eyebrow": "Sasa wewe ni mthibitishaji",
"title": "Jinsi ya kuthibitisha kampeni",
"lede": "Taarifa yako iko hewani. Hivi ndivyo unavyoweza kuiunga mkono kampeni unayoiamini \u2014 inachukua mibofyo miwili kutoka kwa kadi yoyote ya kampeni.",
"steps": {
"open": { "title": "Fungua menyu", "body": "Kwenye kadi yoyote ya kampeni, gusa kitufe cha nukta tatu kwenye kona ya juu kulia ya bango." },
"verify": { "title": "Chagua \"Thibitisha kampeni hii\"", "body": "Menyu hufunua kitendo cha kuthibitisha \u2014 kinachoonekana kwa wasimamizi na wathibitishaji kama wewe pekee." },
"confirm": { "title": "Thibitisha na umemaliza", "body": "Shuhudia kuwa kampeni ni halisi. Beji yako huungana na kadi ili wafadhili wajue unaiunga mkono." }
},
"demo": {
"campaignTitle": "Maji Safi kwa Mwanza",
"campaignOrganizer": "na Mradi Foundation",
"menuVerify": "Thibitisha kampeni hii",
"verifiedBadge": "Imethibitishwa na wewe"
}
}
},
"organizers": {
"appoint": "Teua Mratibu",
+17 -1
View File
@@ -1529,7 +1529,23 @@
"lede": "Doğrulama beyanınızı yayımlamak, güncellemek veya geri çekmek için organizasyonunuzun profiliyle giriş yapın."
},
"loginGateTitle": "Organizasyonunuzun profiliyle giriş yapın",
"loginGateBody": "Başlamak için organizasyonunuzun Nostr profiliyle giriş yapın ya da bir tane oluşturun. Giriş yaptıktan sonra doğrulama beyanınızı buradan yayımlayabilirsiniz."
"loginGateBody": "Başlamak için organizasyonunuzun Nostr profiliyle giriş yapın ya da bir tane oluşturun. Giriş yaptıktan sonra doğrulama beyanınızı buradan yayımlayabilirsiniz.",
"tutorial": {
"eyebrow": "Artık bir doğrulayıcısınız",
"title": "Bir kampanya nasıl doğrulanır",
"lede": "Beyanınız yayında. Güvendiğiniz bir kampanyaya nasıl kefil olacağınız işte burada — herhangi bir kampanya kartından iki dokunuşla yapılır.",
"steps": {
"open": { "title": "Menüyü açın", "body": "Herhangi bir kampanya kartında, afişin sağ üst köşesindeki üç nokta düğmesine dokunun." },
"verify": { "title": "\"Bu kampanyayı doğrula\" seçeneğini seçin", "body": "Menüde bir doğrulama işlemi belirir — yalnızca sizin gibi moderatörler ve doğrulayıcılar tarafından görülebilir." },
"confirm": { "title": "Onaylayın ve işlem tamam", "body": "Kampanyanın gerçek olduğunu teyit edin. Rozetiniz kartta yerini alır, böylece bağışçılar arkasında durduğunuzu bilir." }
},
"demo": {
"campaignTitle": "Mwanza için Temiz Su",
"campaignOrganizer": "Mradi Vakfı tarafından",
"menuVerify": "Bu kampanyayı doğrula",
"verifiedBadge": "Sizin tarafınızdan doğrulandı"
}
}
},
"organizers": {
"appoint": "Organizatör Ata",
+17 -1
View File
@@ -1088,7 +1088,23 @@
"lede": "使用組織的個人資料登入,即可發布、更新或撤回你的驗證聲明。"
},
"loginGateTitle": "使用你組織的個人資料登入",
"loginGateBody": "使用你組織的 Nostr 個人資料登入,或建立一個,即可開始。登入後,你可以在這裡發布你的驗證聲明。"
"loginGateBody": "使用你組織的 Nostr 個人資料登入,或建立一個,即可開始。登入後,你可以在這裡發布你的驗證聲明。",
"tutorial": {
"eyebrow": "你現在是驗證者了",
"title": "如何驗證一個專案",
"lede": "你的聲明已上線。以下說明如何為你信任的專案背書——從任何專案卡片只需點兩下即可完成。",
"steps": {
"open": { "title": "開啟選單", "body": "在任何專案卡片上,點選橫幅右上角的三點按鈕。" },
"verify": { "title": "選擇「驗證此活動」", "body": "選單會顯示驗證操作——只有像你這樣的版主和驗證者才看得到。" },
"confirm": { "title": "確認後即完成", "body": "證明這個專案是真實的。你的徽章會加入卡片,讓捐款者知道你為它背書。" }
},
"demo": {
"campaignTitle": "為姆萬扎提供潔淨用水",
"campaignOrganizer": "由 Mradi 基金會發起",
"menuVerify": "驗證此活動",
"verifiedBadge": "已由你驗證"
}
}
},
"organizers": {
"appoint": "任命組織者",
+17 -1
View File
@@ -1088,7 +1088,23 @@
"lede": "使用组织的个人资料登录,即可发布、更新或撤回你的验证声明。"
},
"loginGateTitle": "使用你组织的个人资料登录",
"loginGateBody": "使用你组织的 Nostr 个人资料登录,或创建一个,即可开始。登录后,你可以在这里发布你的验证声明。"
"loginGateBody": "使用你组织的 Nostr 个人资料登录,或创建一个,即可开始。登录后,你可以在这里发布你的验证声明。",
"tutorial": {
"eyebrow": "你现在是验证者了",
"title": "如何验证一个活动",
"lede": "你的声明已生效。下面教你如何为信任的活动背书——从任意活动卡片只需两步即可完成。",
"steps": {
"open": { "title": "打开菜单", "body": "在任意活动卡片上,点击横幅右上角的三点按钮。" },
"verify": { "title": "选择\"验证此活动\"", "body": "菜单会显示一个验证操作——仅对你这样的版主和验证者可见。" },
"confirm": { "title": "确认即可完成", "body": "证明该活动真实可信。你的徽章会出现在卡片上,让捐赠者知道你为它背书。" }
},
"demo": {
"campaignTitle": "为 Mwanza 提供清洁饮水",
"campaignOrganizer": "由 Mradi 基金会发起",
"menuVerify": "验证此活动",
"verifiedBadge": "已由你验证"
}
}
},
"organizers": {
"appoint": "任命组织者",
+7
View File
@@ -12,6 +12,7 @@ import {
import { MilkdownEditor } from '@/components/markdown/MilkdownEditor';
import { LoginArea } from '@/components/auth/LoginArea';
import { VerifyTutorial } from '@/components/organizations/VerifyTutorial';
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import { useAppContext } from '@/hooks/useAppContext';
@@ -262,6 +263,7 @@ function VerifierEditor() {
};
return (
<div className="space-y-8">
<Card className="border-border/60 shadow-sm">
<CardContent className="p-6 sm:p-8 space-y-6">
{/* Prompt */}
@@ -321,6 +323,11 @@ function VerifierEditor() {
)}
</CardContent>
</Card>
{/* Once the org's statement is live, teach them the actual
verify gesture: the three-dots menu on any campaign card. */}
{isPublished && <VerifyTutorial />}
</div>
);
}
+8 -1
View File
@@ -138,6 +138,12 @@ export default {
// an organic audio indicator.
'0%, 100%': { transform: 'scaleY(0.35)' },
'50%': { transform: 'scaleY(1)' }
},
'tutorial-tap': {
// A soft "press" pulse for the demo cursor in the
// verifier tutorial — shrinks briefly to read as a tap.
'0%, 70%, 100%': { transform: 'scale(1)' },
'80%': { transform: 'scale(0.78)' }
}
},
animation: {
@@ -149,7 +155,8 @@ export default {
'collapsible-down': 'collapsible-down 0.2s ease-out',
'collapsible-up': 'collapsible-up 0.2s ease-out',
'accent-glow': 'accent-glow 2s ease-in-out infinite',
'equaliser-bar': 'equaliser-bar 0.9s ease-in-out infinite'
'equaliser-bar': 'equaliser-bar 0.9s ease-in-out infinite',
'tutorial-tap': 'tutorial-tap 2.4s ease-in-out infinite'
}
}
},