diff --git a/lang/es.json b/lang/es.json index 7e3f2c96..baa216b3 100644 --- a/lang/es.json +++ b/lang/es.json @@ -620,6 +620,7 @@ "Email address": "Dirección de correo electrónico", "Email address, name, and password (encrypted)": "Dirección de correo electrónico, nombre y contraseña (encriptados)", "Email password reset link": "Enviar enlace para restablecer contraseña", + "Email support": "Enviar correo a soporte", "Email verification": "Verificación de correo electrónico", "Email:": "Correo:", "Email\\n address, name, and password (encrypted)": "Dirección de correo electrónico, nombre y contraseña (cifrada)", @@ -878,6 +879,8 @@ "Jessica P.": "Jessica P.", "Join Waitlist": "Unirse a la lista", "Join our Discord": "Únete a nuestro Discord", + "Join our community on Discord — the fastest way to get help. Prefer email? Write to us instead.": "Únete a nuestra comunidad en Discord, la forma más rápida de obtener ayuda. ¿Prefieres el correo? Escríbenos en su lugar.", + "Join the community": "Unirse a la comunidad", "Join the waiting list. We'll let you know when you're in.": "Únete a la lista de espera. Te avisaremos cuando sea tu turno.", "Join thousands of users who have taken control of their finances without compromising their privacy.": "Únete a miles de usuarios que han tomado el control de sus finanzas sin comprometer su privacidad.", "Just reply to this email and let me know what's happening. I personally read every response and will help you get started. This is my project, so I care about making sure it works for you.": "Simplemente responde a este correo y cuéntame qué está pasando. Leo personalmente cada respuesta y te ayudaré a empezar. Este es mi proyecto, así que me importa que funcione para ti.", @@ -1019,6 +1022,7 @@ "Navigate to...": "Navegar a...", "Need Help Getting Started?": "¿Necesitas Ayuda para Empezar?", "Need help importing your transactions, :name?": "¿Necesitas ayuda para importar tus transacciones, :name?", + "Need help?": "¿Necesitas ayuda?", "Need help? Join our Discord": "¿Necesitas ayuda? Únete a nuestro Discord", "Net": "Neto", "Net Cashflow": "Flujo Neto", @@ -1147,6 +1151,7 @@ "Pick whichever plan fits you, paste the matching code at checkout, and you are set.": "Elige el plan que prefieras, pega el código correspondiente al pagar y listo.", "Pink": "Rosa", "Platform": "Plataforma", + "Please describe the problem and what you were doing when it happened. Keep the details below — they help us debug.": "Describe el problema y qué estabas haciendo cuando ocurrió. Conserva los detalles de abajo: nos ayudan a depurar.", "Please enter a balance": "Por favor, ingresa un balance", "Please enter a bank name.": "Por favor, ingresa un nombre de banco.", "Please enter an account name.": "Por favor, ingresa un nombre de cuenta.", @@ -1433,6 +1438,8 @@ "Strong": "Fuerte", "Subscription fees are billed in advance on a recurring basis until cancelled": "Las tarifas de suscripción se facturan por adelantado de forma recurrente hasta que se cancele", "Subscription fees are billed in advance on a\\n recurring basis until cancelled": "Las tarifas de suscripción se facturan por adelantado de forma recurrente hasta que se cancelen", + "Support": "Soporte", + "Support request": "Solicitud de soporte", "Supported formats": "Formatos soportados", "Supports CSV, XLS, and XLSX files": "Soporta archivos CSV, XLS y XLSX", "Swiss Franc": "Franco suizo", diff --git a/lang/fr.json b/lang/fr.json index bef87f74..833dc375 100644 --- a/lang/fr.json +++ b/lang/fr.json @@ -613,6 +613,7 @@ "Email address": "Adresse email", "Email address, name, and password (encrypted)": "Adresse e-mail, nom et mot de passe (cryptés)", "Email password reset link": "Envoyer le lien de réinitialisation du mot de passe", + "Email support": "Envoyer un e-mail au support", "Email verification": "Vérification des e-mails", "Email:": "Mail:", "Email\\n address, name, and password (encrypted)": "Adresse e-mail, nom et mot de passe (cryptés)", @@ -871,6 +872,8 @@ "Jessica P.": "Jessica P.", "Join Waitlist": "Rejoignez la liste", "Join our Discord": "Rejoignez notre Discord", + "Join our community on Discord — the fastest way to get help. Prefer email? Write to us instead.": "Rejoignez notre communauté sur Discord, le moyen le plus rapide d'obtenir de l'aide. Vous préférez l'e-mail ? Écrivez-nous plutôt.", + "Join the community": "Rejoindre la communauté", "Join the waiting list. We'll let you know when you're in.": "Rejoignez la liste d'attente. Nous vous ferons savoir quand ce sera votre tour.", "Join thousands of users who have taken control of their finances without compromising their privacy.": "Rejoignez des milliers d'utilisateurs qui ont pris le contrôle de leurs finances sans compromettre leur vie privée.", "Just reply to this email and let me know what's happening. I personally read every response and will help you get started. This is my project, so I care about making sure it works for you.": "Répondez simplement à cet e-mail et dites-moi ce qui se passe. J'ai personnellement lu chaque réponse et je vous aiderai à démarrer. C'est mon projet, donc je tiens à ce qu'il fonctionne pour vous.", @@ -1011,6 +1014,7 @@ "Navigate to...": "Accédez à...", "Need Help Getting Started?": "Avez-vous besoin d'aide pour démarrer ?", "Need help importing your transactions, :name?": "Avez-vous besoin d'aide pour importer vos transactions, :name ?", + "Need help?": "Besoin d'aide ?", "Need help? Join our Discord": "Avez-vous besoin d'aide ? Rejoignez notre Discord", "Net": "Filet", "Net Cashflow": "Flux net", @@ -1139,6 +1143,7 @@ "Pick whichever plan fits you, paste the matching code at checkout, and you are set.": "Choisissez le forfait que vous préférez, collez le code correspondant lors du paiement et le tour est joué.", "Pink": "Rose", "Platform": "Plate-forme", + "Please describe the problem and what you were doing when it happened. Keep the details below — they help us debug.": "Décrivez le problème et ce que vous faisiez lorsqu'il s'est produit. Conservez les détails ci-dessous : ils nous aident à déboguer.", "Please enter a balance": "Veuillez saisir un solde", "Please enter a bank name.": "Veuillez saisir un nom de banque.", "Please enter an account name.": "Veuillez saisir un nom de compte.", @@ -1424,6 +1429,8 @@ "Strong": "Fort", "Subscription fees are billed in advance on a recurring basis until cancelled": "Les frais d'abonnement sont facturés à l'avance de manière récurrente jusqu'à annulation.", "Subscription fees are billed in advance on a\\n recurring basis until cancelled": "Les frais d'abonnement sont facturés à l'avance de manière récurrente jusqu'à annulation.", + "Support": "Support", + "Support request": "Demande de support", "Supported formats": "Formats pris en charge", "Supports CSV, XLS, and XLSX files": "Prend en charge les fichiers CSV, XLS et XLSX", "Swiss Franc": "franc suisse", diff --git a/resources/js/components/nav-user.tsx b/resources/js/components/nav-user.tsx index 11259fcd..a6e8538d 100644 --- a/resources/js/components/nav-user.tsx +++ b/resources/js/components/nav-user.tsx @@ -1,3 +1,4 @@ +import { SupportDialog } from '@/components/support-dialog'; import { DropdownMenu, DropdownMenuContent, @@ -15,11 +16,13 @@ import { useIsMobile } from '@/hooks/use-mobile'; import { type SharedData } from '@/types'; import { usePage } from '@inertiajs/react'; import { ChevronsUpDown } from 'lucide-react'; +import { useState } from 'react'; export function NavUser({ className }: { className?: string }) { const { auth } = usePage().props; const { state } = useSidebar(); const isMobile = useIsMobile(); + const [supportOpen, setSupportOpen] = useState(false); return ( @@ -46,9 +49,17 @@ export function NavUser({ className }: { className?: string }) { : 'bottom' } > - + setSupportOpen(true)} + /> + ); diff --git a/resources/js/components/support-dialog.tsx b/resources/js/components/support-dialog.tsx new file mode 100644 index 00000000..da13d4c9 --- /dev/null +++ b/resources/js/components/support-dialog.tsx @@ -0,0 +1,92 @@ +import DiscordIcon from '@/components/icons/DiscordIcon'; +import { Button } from '@/components/ui/button'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { type SharedData, type User } from '@/types'; +import { __ } from '@/utils/i18n'; +import { usePage } from '@inertiajs/react'; +import { Mail } from 'lucide-react'; + +const DISCORD_URL = 'https://discord.gg/m8hUhx6D9D'; +const SUPPORT_EMAIL = 'support@whisper.money'; + +function buildSupportMailto(user: User, version: string): string { + const userAgent = + typeof navigator !== 'undefined' ? navigator.userAgent : 'unknown'; + const page = + typeof window !== 'undefined' ? window.location.href : 'unknown'; + + const body = [ + __( + 'Please describe the problem and what you were doing when it happened. Keep the details below — they help us debug.', + ), + '', + '', + '--- Diagnostics (please keep) ---', + `User ID: ${user.id}`, + `App version: ${version}`, + `Locale: ${user.locale ?? 'en'}`, + `Page: ${page}`, + `Browser: ${userAgent}`, + ].join('\n'); + + const query = `subject=${encodeURIComponent(__('Support request'))}&body=${encodeURIComponent(body)}`; + + return `mailto:${SUPPORT_EMAIL}?${query}`; +} + +interface SupportDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + user: User; +} + +export function SupportDialog({ + open, + onOpenChange, + user, +}: SupportDialogProps) { + const { version } = usePage().props; + + return ( + + + + {__('Need help?')} + + {__( + 'Join our community on Discord — the fastest way to get help. Prefer email? Write to us instead.', + )} + + +
+ + +
+
+
+ ); +} diff --git a/resources/js/components/user-menu-content.test.tsx b/resources/js/components/user-menu-content.test.tsx index 4d216be6..aa9ca74d 100644 --- a/resources/js/components/user-menu-content.test.tsx +++ b/resources/js/components/user-menu-content.test.tsx @@ -1,5 +1,5 @@ import { type User } from '@/types'; -import { render, screen } from '@testing-library/react'; +import { fireEvent, render, screen } from '@testing-library/react'; import { type ReactNode } from 'react'; import { describe, expect, it, vi } from 'vitest'; import { UserMenuContent } from './user-menu-content'; @@ -8,9 +8,13 @@ vi.mock('@/components/ui/dropdown-menu', () => ({ DropdownMenuGroup: ({ children }: { children: ReactNode }) => (
{children}
), - DropdownMenuItem: ({ children }: { children: ReactNode }) => ( -
{children}
- ), + DropdownMenuItem: ({ + children, + onClick, + }: { + children: ReactNode; + onClick?: () => void; + }) =>
{children}
, DropdownMenuLabel: ({ children }: { children: ReactNode }) => (
{children}
), @@ -66,21 +70,21 @@ vi.mock('@inertiajs/react', () => ({ }), })); +const user: User = { + id: '0194d20b-2b25-7000-8000-000000000001', + name: 'Test User', + email: 'test@example.com', + currency_code: 'USD', + locale: 'en', + timezone: 'UTC', + email_verified_at: null, + created_at: '2026-01-01T00:00:00.000000Z', + updated_at: '2026-01-01T00:00:00.000000Z', +}; + describe('UserMenuContent', () => { it('shows community above feedback in the user dropdown', () => { - const user: User = { - id: '0194d20b-2b25-7000-8000-000000000001', - name: 'Test User', - email: 'test@example.com', - currency_code: 'USD', - locale: 'en', - timezone: 'UTC', - email_verified_at: null, - created_at: '2026-01-01T00:00:00.000000Z', - updated_at: '2026-01-01T00:00:00.000000Z', - }; - - render(); + render(); const community = screen.getByRole('link', { name: /community/i }); const feedback = screen.getByRole('link', { name: /feedback/i }); @@ -92,4 +96,14 @@ describe('UserMenuContent', () => { Node.DOCUMENT_POSITION_FOLLOWING, ); }); + + it('triggers the support callback when the support item is clicked', () => { + const onOpenSupport = vi.fn(); + + render(); + + fireEvent.click(screen.getByText('Support')); + + expect(onOpenSupport).toHaveBeenCalledOnce(); + }); }); diff --git a/resources/js/components/user-menu-content.tsx b/resources/js/components/user-menu-content.tsx index 4495a7e4..a757c8c7 100644 --- a/resources/js/components/user-menu-content.tsx +++ b/resources/js/components/user-menu-content.tsx @@ -18,6 +18,7 @@ import { Link, router, usePage } from '@inertiajs/react'; import { Eye, EyeOff, + LifeBuoy, LogOut, Map, MessageSquare, @@ -27,9 +28,10 @@ import { interface UserMenuContentProps { user: User; + onOpenSupport: () => void; } -export function UserMenuContent({ user }: UserMenuContentProps) { +export function UserMenuContent({ user, onOpenSupport }: UserMenuContentProps) { const cleanup = useMobileNavigation(); const { isPrivacyModeEnabled, togglePrivacyMode } = usePrivacyMode(); const { version } = usePage().props; @@ -129,6 +131,15 @@ export function UserMenuContent({ user }: UserMenuContentProps) { {__('Roadmap')} + { + onOpenSupport(); + cleanup(); + }} + > + + {__('Support')} +