fix(shared-help): harden help system with XSS protection, i18n, type safety, and reference implementation

- Add HTML sanitization via isomorphic-dompurify in parser layer to prevent XSS
- Replace all hardcoded English strings with translations (FAQSection, KeyboardShortcuts, ChangelogEntry/Section)
- Remove unsafe `as` type casting in loader.ts, use Zod-inferred generics instead
- Add error logging in content loader (replaces silent catch blocks)
- Fix HelpSearch blur handling (mousedown+preventDefault instead of setTimeout hack)
- Add ARIA attributes to HelpSearch for accessibility
- Derive FAQ categories from items instead of hardcoding all 6
- Fix null-safety in GettingStartedGuide.svelte
- Fix unused appId variable in HelpPage.svelte, add scroll-reset on tab switch
- Rebuild Contacts help page as reference implementation using shared HelpPage component
- Add README with quick-start guide, props docs, and translations template

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-03-24 22:38:46 +01:00
parent 8356ac634a
commit 42dd7d2a7a
18 changed files with 845 additions and 801 deletions

View file

@ -1,173 +1,172 @@
/**
* Central help content loader for Contacts app
* This file loads and merges the central help content from @manacore/shared-help-content
* Help content for Contacts app
* Uses @manacore/shared-help-types for type-safe content definition
*/
import type { HelpContent } from '@manacore/shared-help-types';
import { createEmptyContent } from '@manacore/shared-help-content';
/**
* Central help content that applies to all Manacore apps
* In a production setup, this would be loaded from the shared-help-content package's
* Markdown files. For now, we provide the content inline for simplicity.
*/
export const centralHelpContent: HelpContent = {
faq: [
// Account FAQs
{
id: 'faq-account-001',
question: 'How do I create an account?',
answer: `<p>Creating an account is simple:</p>
<ol>
<li>Click the <strong>Sign Up</strong> button on the login page</li>
<li>Enter your email address and choose a secure password</li>
<li>Verify your email address by clicking the link we send you</li>
<li>Complete your profile setup</li>
</ol>
<p>You can also sign up using your Google or Apple account for faster registration.</p>`,
category: 'account',
export function getContactsHelpContent(locale: string): HelpContent {
const isDE = locale === 'de';
return {
faq: [
{
id: 'faq-import',
question: isDE
? 'Wie importiere ich Kontakte aus Google?'
: 'How do I import contacts from Google?',
answer: isDE
? '<p>Um Kontakte aus Google zu importieren: Gehe zu <strong>Daten > Import</strong>, wähle Google Kontakte, melde dich bei deinem Google-Konto an, wähle die Kontakte aus und klicke auf <strong>Importieren</strong>.</p>'
: '<p>To import contacts from Google: Go to <strong>Data > Import</strong>, select Google Contacts, sign in to your Google account, select the contacts you want to import, and click <strong>Import</strong>.</p>',
category: 'features',
order: 1,
language: isDE ? 'de' : 'en',
tags: isDE ? ['import', 'google', 'kontakte'] : ['import', 'google', 'contacts'],
},
{
id: 'faq-export',
question: isDE ? 'Wie exportiere ich meine Kontakte?' : 'How do I export my contacts?',
answer: isDE
? '<p>Du kannst deine Kontakte in verschiedenen Formaten exportieren: Gehe zu <strong>Daten > Export</strong>, wähle das gewünschte Format (vCard, CSV, JSON) und klicke auf <strong>Exportieren</strong>.</p>'
: '<p>You can export your contacts in various formats: Go to <strong>Data > Export</strong>, select the desired format (vCard, CSV, JSON), and click <strong>Export</strong>.</p>',
category: 'features',
order: 2,
language: isDE ? 'de' : 'en',
tags: isDE ? ['export', 'vcard', 'csv'] : ['export', 'vcard', 'csv'],
},
{
id: 'faq-duplicates',
question: isDE ? 'Wie finde ich doppelte Kontakte?' : 'How do I find duplicate contacts?',
answer: isDE
? '<p>Wir erkennen automatisch potenzielle Duplikate. Gehe zu <strong>Duplikate</strong> in der Seitenleiste, überprüfe die Vorschläge und wähle <strong>Zusammenführen</strong> oder <strong>Ignorieren</strong>.</p>'
: '<p>We automatically detect potential duplicates. Go to <strong>Duplicates</strong> in the sidebar, review the suggestions, and choose to <strong>Merge</strong> or <strong>Ignore</strong>.</p>',
category: 'features',
order: 3,
language: isDE ? 'de' : 'en',
tags: isDE ? ['duplikate', 'zusammenführen'] : ['duplicates', 'merge'],
},
{
id: 'faq-subscription',
question: isDE ? 'Wie kann ich mein Abo kündigen?' : 'How do I cancel my subscription?',
answer: isDE
? '<p>Du kannst dein Abo jederzeit kündigen: Gehe zu <strong>Einstellungen > Abonnement > Abo verwalten > Abo kündigen</strong>. Dein Abo bleibt bis zum Ende des Abrechnungszeitraums aktiv.</p>'
: '<p>You can cancel your subscription at any time: Go to <strong>Settings > Subscription > Manage Subscription > Cancel Subscription</strong>. Your subscription will remain active until the end of the billing period.</p>',
category: 'billing',
order: 4,
language: isDE ? 'de' : 'en',
tags: isDE ? ['abo', 'kündigung', 'abrechnung'] : ['subscription', 'cancel', 'billing'],
},
{
id: 'faq-privacy',
question: isDE ? 'Wie werden meine Daten geschützt?' : 'How is my data protected?',
answer: isDE
? '<p>Wir nehmen deinen Datenschutz ernst:</p><ul><li><strong>Verschlüsselung</strong>: Alle Daten werden bei der Übertragung (TLS) und im Ruhezustand verschlüsselt</li><li><strong>DSGVO-konform</strong>: Wir halten uns an die EU-Datenschutzverordnung</li><li><strong>Kein Datenverkauf</strong>: Wir verkaufen niemals deine persönlichen Daten</li><li><strong>Datenexport</strong>: Du kannst jederzeit alle deine Daten exportieren</li><li><strong>Kontolöschung</strong>: Du kannst dein Konto und alle Daten dauerhaft löschen</li></ul>'
: '<p>We take your privacy seriously:</p><ul><li><strong>Encryption</strong>: All data is encrypted in transit (TLS) and at rest</li><li><strong>GDPR Compliant</strong>: We follow EU data protection regulations</li><li><strong>No Data Selling</strong>: We never sell your personal data</li><li><strong>Data Export</strong>: You can export all your data at any time</li><li><strong>Account Deletion</strong>: You can permanently delete your account and all data</li></ul>',
category: 'privacy',
order: 5,
language: isDE ? 'de' : 'en',
featured: true,
tags: isDE ? ['datenschutz', 'dsgvo', 'sicherheit'] : ['privacy', 'gdpr', 'security'],
},
],
features: [
{
id: 'feature-management',
title: isDE ? 'Kontaktverwaltung' : 'Contact Management',
description: isDE
? 'Verwalte alle deine Kontakte an einem Ort'
: 'Manage all your contacts in one place',
icon: '👥',
category: 'core',
highlights: isDE
? ['Unbegrenzte Kontakte', 'Benutzerdefinierte Felder', 'Tags und Kategorien']
: ['Unlimited contacts', 'Custom fields', 'Tags and categories'],
content: '',
order: 1,
language: isDE ? 'de' : 'en',
},
{
id: 'feature-import-export',
title: 'Import & Export',
description: isDE
? 'Importiere und exportiere Kontakte in verschiedenen Formaten'
: 'Import and export contacts in various formats',
icon: '📤',
category: 'core',
highlights: isDE
? ['Google Kontakte Sync', 'vCard Import/Export', 'CSV Import/Export']
: ['Google Contacts sync', 'vCard import/export', 'CSV import/export'],
content: '',
order: 2,
language: isDE ? 'de' : 'en',
},
{
id: 'feature-duplicates',
title: isDE ? 'Duplikat-Erkennung' : 'Duplicate Detection',
description: isDE
? 'Automatische Erkennung und Zusammenführung von Duplikaten'
: 'Automatic detection and merging of duplicates',
icon: '🔍',
category: 'advanced',
highlights: isDE
? ['Intelligente Erkennung', 'Ein-Klick Zusammenführung', 'Überprüfungsmodus']
: ['Smart detection', 'One-click merge', 'Review mode'],
content: '',
order: 3,
language: isDE ? 'de' : 'en',
},
{
id: 'feature-favorites',
title: isDE ? 'Favoriten' : 'Favorites',
description: isDE
? 'Markiere wichtige Kontakte als Favoriten'
: 'Mark important contacts as favorites',
icon: '⭐',
category: 'core',
highlights: isDE
? ['Schnellzugriff', 'Verschiedene Ansichten', 'Sortierung']
: ['Quick access', 'Multiple views', 'Sorting'],
content: '',
order: 4,
language: isDE ? 'de' : 'en',
},
],
shortcuts: [
{
id: 'shortcuts-general',
category: 'general',
title: isDE ? 'Allgemein' : 'General',
language: isDE ? 'de' : 'en',
order: 1,
shortcuts: [
{
shortcut: 'Cmd/Ctrl + K',
action: isDE ? 'Suche öffnen' : 'Open search',
},
{
shortcut: 'Cmd/Ctrl + N',
action: isDE ? 'Neuer Kontakt' : 'New contact',
},
{
shortcut: 'Cmd/Ctrl + 1-6',
action: isDE ? 'Navigation' : 'Navigation',
},
],
},
],
gettingStarted: [],
changelog: [],
contact: {
id: 'contact-support',
title: isDE ? 'Support kontaktieren' : 'Contact Support',
content: isDE
? '<p>Unser Support-Team hilft dir bei allen Fragen.</p>'
: '<p>Our support team is here to help you with any questions or issues.</p>',
language: isDE ? 'de' : 'en',
order: 1,
language: 'en',
featured: true,
tags: ['account', 'registration', 'signup'],
supportEmail: 'support@mana.how',
documentationUrl: 'https://mana.how/docs',
responseTime: isDE ? 'Normalerweise innerhalb von 24 Stunden' : 'Usually within 24 hours',
},
{
id: 'faq-account-001',
question: 'Wie erstelle ich ein Konto?',
answer: `<p>Die Kontoerstellung ist einfach:</p>
<ol>
<li>Klicke auf <strong>Registrieren</strong> auf der Anmeldeseite</li>
<li>Gib deine E-Mail-Adresse ein und wähle ein sicheres Passwort</li>
<li>Bestätige deine E-Mail-Adresse durch Klick auf den Link, den wir dir senden</li>
<li>Vervollständige dein Profil</li>
</ol>
<p>Du kannst dich auch mit deinem Google- oder Apple-Konto registrieren, um schneller loszulegen.</p>`,
category: 'account',
order: 1,
language: 'de',
featured: true,
tags: ['konto', 'registrierung', 'anmeldung'],
},
// Billing FAQs
{
id: 'faq-billing-001',
question: 'How do I cancel my subscription?',
answer: `<p>You can cancel your subscription at any time:</p>
<ol>
<li>Go to <strong>Settings</strong> > <strong>Subscription</strong></li>
<li>Click <strong>Manage Subscription</strong></li>
<li>Select <strong>Cancel Subscription</strong></li>
<li>Confirm your cancellation</li>
</ol>
<p>Your subscription will remain active until the end of the current billing period. You won't be charged again after cancellation.</p>`,
category: 'billing',
order: 1,
language: 'en',
featured: true,
tags: ['subscription', 'cancel', 'billing'],
},
{
id: 'faq-billing-001',
question: 'Wie kann ich mein Abo kündigen?',
answer: `<p>Du kannst dein Abo jederzeit kündigen:</p>
<ol>
<li>Gehe zu <strong>Einstellungen</strong> > <strong>Abonnement</strong></li>
<li>Klicke auf <strong>Abo verwalten</strong></li>
<li>Wähle <strong>Abo kündigen</strong></li>
<li>Bestätige die Kündigung</li>
</ol>
<p>Dein Abo bleibt bis zum Ende des aktuellen Abrechnungszeitraums aktiv. Nach der Kündigung erfolgen keine weiteren Abbuchungen.</p>`,
category: 'billing',
order: 1,
language: 'de',
featured: true,
tags: ['abo', 'kündigung', 'abrechnung'],
},
// Privacy FAQs
{
id: 'faq-privacy-001',
question: 'How is my data protected?',
answer: `<p>We take your privacy seriously:</p>
<ul>
<li><strong>Encryption</strong>: All data is encrypted in transit (TLS) and at rest</li>
<li><strong>GDPR Compliant</strong>: We follow EU data protection regulations</li>
<li><strong>No Data Selling</strong>: We never sell your personal data to third parties</li>
<li><strong>Data Export</strong>: You can export all your data at any time</li>
<li><strong>Account Deletion</strong>: You can permanently delete your account and all associated data</li>
</ul>`,
category: 'privacy',
order: 1,
language: 'en',
featured: true,
tags: ['privacy', 'data', 'security', 'gdpr'],
},
{
id: 'faq-privacy-001',
question: 'Wie werden meine Daten geschützt?',
answer: `<p>Wir nehmen deinen Datenschutz ernst:</p>
<ul>
<li><strong>Verschlüsselung</strong>: Alle Daten werden bei der Übertragung (TLS) und im Ruhezustand verschlüsselt</li>
<li><strong>DSGVO-konform</strong>: Wir halten uns an die EU-Datenschutzverordnung</li>
<li><strong>Kein Datenverkauf</strong>: Wir verkaufen niemals deine persönlichen Daten an Dritte</li>
<li><strong>Datenexport</strong>: Du kannst jederzeit alle deine Daten exportieren</li>
<li><strong>Kontolöschung</strong>: Du kannst dein Konto und alle zugehörigen Daten dauerhaft löschen</li>
</ul>`,
category: 'privacy',
order: 1,
language: 'de',
featured: true,
tags: ['datenschutz', 'daten', 'sicherheit', 'dsgvo'],
},
],
features: [],
shortcuts: [],
gettingStarted: [
{
id: 'guide-welcome',
title: 'Getting Started',
description: 'Learn the basics and get up and running quickly',
content: `<h2>Create Your Account</h2>
<p>Start by creating your free account. You can sign up with your email address or use Google/Apple sign-in for a faster setup.</p>
<h2>Explore the Dashboard</h2>
<p>After logging in, you'll see your dashboard. This is your home base where you can access all features and see important information at a glance.</p>
<h2>Customize Your Settings</h2>
<p>Visit the Settings page to personalize your experience.</p>`,
difficulty: 'beginner',
estimatedTime: '5 minutes',
order: 1,
language: 'en',
},
{
id: 'guide-welcome',
title: 'Erste Schritte',
description: 'Lerne die Grundlagen und starte schnell durch',
content: `<h2>Konto erstellen</h2>
<p>Beginne mit der Erstellung deines kostenlosen Kontos. Du kannst dich mit deiner E-Mail-Adresse registrieren oder Google/Apple für eine schnellere Anmeldung nutzen.</p>
<h2>Dashboard erkunden</h2>
<p>Nach dem Einloggen siehst du dein Dashboard. Dies ist deine Zentrale, von der aus du auf alle Funktionen zugreifen kannst.</p>
<h2>Einstellungen anpassen</h2>
<p>Besuche die Einstellungen, um dein Erlebnis zu personalisieren.</p>`,
difficulty: 'beginner',
estimatedTime: '5 Minuten',
order: 1,
language: 'de',
},
],
changelog: [],
contact: {
id: 'contact-support',
title: 'Contact Support',
content: `<h2>Need Help?</h2>
<p>Our support team is here to help you with any questions or issues.</p>
<h3>Before Contacting Us</h3>
<ul>
<li>Check the <strong>FAQ</strong> section for quick answers</li>
<li>Browse our <strong>Getting Started</strong> guides</li>
<li>Search the help center using the search bar</li>
</ul>`,
language: 'en',
order: 1,
supportEmail: 'support@mana.how',
responseTime: 'Usually within 24 hours',
},
};
};
}

View file

@ -1,508 +1,187 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { locale } from 'svelte-i18n';
import {
Question,
Star,
Command,
Envelope,
MagnifyingGlass,
CaretDown,
Check,
Info,
ChatCircle,
Clock,
ArrowRight,
ArrowLeft,
ArrowSquareOut,
BookOpen,
} from '@manacore/shared-icons';
import { HelpPage } from '@manacore/shared-help-ui';
import type { HelpPageTranslations } from '@manacore/shared-help-ui';
import { getContactsHelpContent } from '$lib/content/help/index.js';
type HelpSection = 'faq' | 'features' | 'shortcuts' | 'contact';
const content = $derived(getContactsHelpContent($locale ?? 'en'));
let activeSection = $state<HelpSection>('faq');
let searchQuery = $state('');
let expandedFaqId = $state<string | null>('faq-1');
// FAQ data
const faqs = $derived(
const translations: HelpPageTranslations = $derived(
$locale === 'de'
? [
{
id: 'faq-1',
question: 'Wie importiere ich Kontakte aus Google?',
answer:
'Um Kontakte aus Google zu importieren: Gehe zu Daten > Import, wähle Google Kontakte, melde dich bei deinem Google-Konto an, wähle die Kontakte aus und klicke auf Importieren.',
category: 'features',
? {
title: 'Hilfe & Support',
subtitle: 'Finde Antworten und lerne die App kennen',
searchPlaceholder: 'Hilfe durchsuchen...',
sections: {
faq: 'FAQ',
features: 'Features',
shortcuts: 'Tastenkürzel',
gettingStarted: 'Erste Schritte',
changelog: 'Änderungen',
contact: 'Kontakt',
},
{
id: 'faq-2',
question: 'Wie exportiere ich meine Kontakte?',
answer:
'Du kannst deine Kontakte in verschiedenen Formaten exportieren: Gehe zu Daten > Export, wähle das gewünschte Format (vCard, CSV, JSON) und klicke auf Exportieren.',
category: 'features',
search: {
noResults: 'Keine Ergebnisse für "{query}"',
resultsCount: '{count} Ergebnisse',
searching: 'Suche...',
},
{
id: 'faq-3',
question: 'Wie finde ich doppelte Kontakte?',
answer:
'Wir erkennen automatisch potenzielle Duplikate. Gehe zu Duplikate in der Seitenleiste, überprüfe die Vorschläge und wähle Zusammenführen oder Ignorieren.',
category: 'features',
faq: {
noItems: 'Keine häufigen Fragen verfügbar.',
allCategories: 'Alle',
categories: {
general: 'Allgemein',
account: 'Konto',
billing: 'Abrechnung',
features: 'Funktionen',
technical: 'Technisch',
privacy: 'Datenschutz',
},
},
{
id: 'faq-4',
question: 'Wie kann ich mein Abo kündigen?',
answer:
'Du kannst dein Abo jederzeit kündigen: Gehe zu Einstellungen > Abonnement > Abo verwalten > Abo kündigen. Dein Abo bleibt bis zum Ende des Abrechnungszeitraums aktiv.',
category: 'billing',
features: {
noItems: 'Keine Features verfügbar.',
comingSoon: 'Demnächst',
learnMore: 'Mehr erfahren',
},
{
id: 'faq-5',
question: 'Wie werden meine Daten geschützt?',
answer:
'Wir nehmen deinen Datenschutz ernst: Alle Daten werden verschlüsselt (TLS), wir sind DSGVO-konform, verkaufen keine Daten und du kannst jederzeit alle Daten exportieren oder dein Konto löschen.',
category: 'privacy',
shortcuts: {
noItems: 'Keine Tastenkürzel verfügbar.',
columns: {
shortcut: 'Kürzel',
action: 'Aktion',
description: 'Beschreibung',
},
},
]
: [
{
id: 'faq-1',
question: 'How do I import contacts from Google?',
answer:
'To import contacts from Google: Go to Data > Import, select Google Contacts, sign in to your Google account, select the contacts you want to import, and click Import.',
category: 'features',
gettingStarted: {
noItems: 'Keine Anleitungen verfügbar.',
estimatedTime: 'Geschätzte Zeit',
difficulty: {
beginner: 'Einsteiger',
intermediate: 'Fortgeschritten',
advanced: 'Experte',
},
},
{
id: 'faq-2',
question: 'How do I export my contacts?',
answer:
'You can export your contacts in various formats: Go to Data > Export, select the desired format (vCard, CSV, JSON), and click Export.',
category: 'features',
changelog: {
noItems: 'Keine Änderungen verfügbar.',
showAll: 'Alle Versionen anzeigen',
types: {
major: 'Hauptversion',
minor: 'Nebenversion',
patch: 'Patch',
beta: 'Beta',
},
labels: {
features: 'Neue Funktionen',
improvements: 'Verbesserungen',
bugFixes: 'Fehlerbehebungen',
},
},
{
id: 'faq-3',
question: 'How do I find duplicate contacts?',
answer:
'We automatically detect potential duplicates. Go to Duplicates in the sidebar, review the suggestions, and choose to Merge or Ignore.',
category: 'features',
contact: {
noInfo: 'Keine Kontaktinformationen verfügbar.',
email: 'E-Mail senden',
responseTime: 'Antwortzeit',
},
{
id: 'faq-4',
question: 'How do I cancel my subscription?',
answer:
'You can cancel your subscription at any time: Go to Settings > Subscription > Manage Subscription > Cancel Subscription. Your subscription will remain active until the end of the billing period.',
category: 'billing',
common: {
back: 'Zurück',
showMore: 'Mehr anzeigen',
showLess: 'Weniger anzeigen',
},
{
id: 'faq-5',
question: 'How is my data protected?',
answer:
'We take your privacy seriously: All data is encrypted (TLS), we are GDPR compliant, we never sell data, and you can export all your data or delete your account at any time.',
category: 'privacy',
}
: {
title: 'Help & Support',
subtitle: 'Find answers and learn how to use the app',
searchPlaceholder: 'Search help...',
sections: {
faq: 'FAQ',
features: 'Features',
shortcuts: 'Shortcuts',
gettingStarted: 'Getting Started',
changelog: 'Changelog',
contact: 'Contact',
},
]
search: {
noResults: 'No results for "{query}"',
resultsCount: '{count} results',
searching: 'Searching...',
},
faq: {
noItems: 'No frequently asked questions available.',
allCategories: 'All',
categories: {
general: 'General',
account: 'Account',
billing: 'Billing',
features: 'Features',
technical: 'Technical',
privacy: 'Privacy',
},
},
features: {
noItems: 'No features available.',
comingSoon: 'Coming Soon',
learnMore: 'Learn More',
},
shortcuts: {
noItems: 'No keyboard shortcuts available.',
columns: {
shortcut: 'Shortcut',
action: 'Action',
description: 'Description',
},
},
gettingStarted: {
noItems: 'No guides available.',
estimatedTime: 'Estimated time',
difficulty: {
beginner: 'Beginner',
intermediate: 'Intermediate',
advanced: 'Advanced',
},
},
changelog: {
noItems: 'No changelog available.',
showAll: 'Show all releases',
types: {
major: 'Major',
minor: 'Minor',
patch: 'Patch',
beta: 'Beta',
},
labels: {
features: 'New Features',
improvements: 'Improvements',
bugFixes: 'Bug Fixes',
},
},
contact: {
noInfo: 'No contact information available.',
email: 'Send email',
responseTime: 'Response time',
},
common: {
back: 'Back',
showMore: 'Show more',
showLess: 'Show less',
},
}
);
// Features data
const features = $derived(
$locale === 'de'
? [
{
icon: '👥',
title: 'Kontaktverwaltung',
description: 'Verwalte alle deine Kontakte an einem Ort',
highlights: [
'Unbegrenzte Kontakte',
'Benutzerdefinierte Felder',
'Tags und Kategorien',
],
},
{
icon: '📤',
title: 'Import & Export',
description: 'Importiere und exportiere Kontakte in verschiedenen Formaten',
highlights: ['Google Kontakte Sync', 'vCard Import/Export', 'CSV Import/Export'],
},
{
icon: '🔍',
title: 'Duplikat-Erkennung',
description: 'Automatische Erkennung und Zusammenführung von Duplikaten',
highlights: [
'Intelligente Erkennung',
'Ein-Klick Zusammenführung',
'Überprüfungsmodus',
],
},
{
icon: '⭐',
title: 'Favoriten',
description: 'Markiere wichtige Kontakte als Favoriten',
highlights: ['Schnellzugriff', 'Verschiedene Ansichten', 'Sortierung'],
},
]
: [
{
icon: '👥',
title: 'Contact Management',
description: 'Manage all your contacts in one place',
highlights: ['Unlimited contacts', 'Custom fields', 'Tags and categories'],
},
{
icon: '📤',
title: 'Import & Export',
description: 'Import and export contacts in various formats',
highlights: ['Google Contacts sync', 'vCard import/export', 'CSV import/export'],
},
{
icon: '🔍',
title: 'Duplicate Detection',
description: 'Automatic detection and merging of duplicates',
highlights: ['Smart detection', 'One-click merge', 'Review mode'],
},
{
icon: '⭐',
title: 'Favorites',
description: 'Mark important contacts as favorites',
highlights: ['Quick access', 'Multiple views', 'Sorting'],
},
]
);
// Keyboard shortcuts
const shortcuts = [
{ shortcut: 'Cmd/Ctrl + K', action: $locale === 'de' ? 'Suche öffnen' : 'Open search' },
{ shortcut: 'Cmd/Ctrl + N', action: $locale === 'de' ? 'Neuer Kontakt' : 'New contact' },
{ shortcut: 'Cmd/Ctrl + 1-6', action: $locale === 'de' ? 'Navigation' : 'Navigation' },
];
// Translations
const t = $derived({
title: $locale === 'de' ? 'Hilfe & Support' : 'Help & Support',
subtitle:
$locale === 'de'
? 'Finde Antworten und lerne die App kennen'
: 'Find answers and learn how to use the app',
searchPlaceholder: $locale === 'de' ? 'Hilfe durchsuchen...' : 'Search help...',
sections: {
faq: 'FAQ',
features: 'Features',
shortcuts: $locale === 'de' ? 'Tastenkürzel' : 'Shortcuts',
contact: $locale === 'de' ? 'Kontakt' : 'Contact',
},
back: $locale === 'de' ? 'Zurück' : 'Back',
contactTitle: $locale === 'de' ? 'Support kontaktieren' : 'Contact Support',
contactDescription:
$locale === 'de'
? 'Unser Support-Team hilft dir bei allen Fragen.'
: 'Our support team is here to help you.',
email: $locale === 'de' ? 'E-Mail senden' : 'Send email',
responseTime: $locale === 'de' ? 'Antwortzeit: 24 Stunden' : 'Response time: 24 hours',
});
// Filter FAQs based on search
const filteredFaqs = $derived(
searchQuery.trim().length > 0
? faqs.filter(
(faq) =>
faq.question.toLowerCase().includes(searchQuery.toLowerCase()) ||
faq.answer.toLowerCase().includes(searchQuery.toLowerCase())
)
: faqs
);
function toggleFaq(id: string) {
expandedFaqId = expandedFaqId === id ? null : id;
}
function handleBack() {
goto('/');
}
const sections: { id: HelpSection; label: string }[] = $derived([
{ id: 'faq', label: t.sections.faq },
{ id: 'features', label: t.sections.features },
{ id: 'shortcuts', label: t.sections.shortcuts },
{ id: 'contact', label: t.sections.contact },
]);
</script>
<svelte:head>
<title>{t.title} | Contacts</title>
<title>{translations.title} | Contacts</title>
</svelte:head>
<div class="mx-auto max-w-4xl px-4 py-8">
<!-- Header -->
<div class="mb-8">
<button
type="button"
class="mb-4 inline-flex items-center gap-1.5 rounded-full bg-white/80 px-3 py-1.5 text-sm font-medium text-gray-600 shadow-sm ring-1 ring-gray-200 backdrop-blur-sm transition-all hover:-translate-y-0.5 hover:bg-white hover:text-gray-900 hover:shadow-md dark:bg-white/10 dark:text-gray-400 dark:ring-white/10 dark:hover:bg-white/20 dark:hover:text-gray-100"
onclick={handleBack}
>
<ArrowLeft size={14} />
{t.back}
</button>
<h1 class="text-2xl font-bold text-gray-900 dark:text-gray-100">
{t.title}
</h1>
<p class="mt-1 text-gray-600 dark:text-gray-400">
{t.subtitle}
</p>
</div>
<!-- Search - Glass Style -->
<div class="relative mb-8">
<input
type="text"
bind:value={searchQuery}
placeholder={t.searchPlaceholder}
class="w-full rounded-full border border-gray-200 bg-white/80 py-3 pl-11 pr-4 text-sm text-gray-900 shadow-sm backdrop-blur-sm placeholder:text-gray-500 focus:border-blue-400 focus:bg-white focus:outline-none focus:ring-2 focus:ring-blue-400/20 dark:border-white/10 dark:bg-white/10 dark:text-gray-100 dark:placeholder:text-gray-400 dark:focus:border-blue-500/50 dark:focus:bg-white/15 dark:focus:ring-blue-500/20"
/>
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-4">
<MagnifyingGlass size={18} class="text-gray-400" />
</div>
</div>
<!-- Navigation Pills -->
<div class="mb-8 flex flex-wrap gap-2">
{#each sections as section (section.id)}
<button
type="button"
class="inline-flex items-center gap-2 rounded-full px-4 py-2 text-sm font-medium shadow-sm backdrop-blur-sm transition-all hover:-translate-y-0.5 hover:shadow-md {activeSection ===
section.id
? 'bg-blue-500 text-white ring-1 ring-blue-400 dark:bg-blue-600 dark:ring-blue-500'
: 'bg-white/80 text-gray-700 ring-1 ring-gray-200 hover:bg-white dark:bg-white/10 dark:text-gray-300 dark:ring-white/10 dark:hover:bg-white/20'}"
onclick={() => (activeSection = section.id)}
>
{#if section.id === 'faq'}
<Question size={16} />
{:else if section.id === 'features'}
<Star size={16} />
{:else if section.id === 'shortcuts'}
<Command size={16} />
{:else if section.id === 'contact'}
<Envelope size={16} />
{/if}
{section.label}
</button>
{/each}
</div>
<!-- Content -->
<div class="min-h-[400px]">
<!-- FAQ Section -->
{#if activeSection === 'faq'}
<div class="space-y-3">
{#each filteredFaqs as faq (faq.id)}
<div
class="overflow-hidden rounded-2xl bg-white/80 shadow-sm ring-1 ring-gray-200 backdrop-blur-sm transition-all dark:bg-white/5 dark:ring-white/10 {expandedFaqId ===
faq.id
? 'ring-blue-300 dark:ring-blue-500/30'
: ''}"
>
<button
type="button"
class="flex w-full items-center justify-between p-4 text-left transition-colors hover:bg-gray-50/50 dark:hover:bg-white/5"
onclick={() => toggleFaq(faq.id)}
>
<span class="pr-4 font-medium text-gray-900 dark:text-gray-100">
{faq.question}
</span>
<span
class="flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full bg-gray-100 text-gray-500 transition-all duration-200 dark:bg-white/10 dark:text-gray-400 {expandedFaqId ===
faq.id
? 'rotate-180 bg-blue-100 text-blue-600 dark:bg-blue-500/20 dark:text-blue-400'
: ''}"
>
<CaretDown size={16} />
</span>
</button>
{#if expandedFaqId === faq.id}
<div
class="border-t border-gray-100 bg-gray-50/50 px-4 py-4 text-gray-600 dark:border-white/5 dark:bg-white/5 dark:text-gray-300"
>
{faq.answer}
</div>
{/if}
</div>
{/each}
{#if filteredFaqs.length === 0}
<div
class="flex flex-col items-center justify-center rounded-2xl bg-white/80 py-12 shadow-sm ring-1 ring-gray-200 backdrop-blur-sm dark:bg-white/5 dark:ring-white/10"
>
<MagnifyingGlass size={32} class="mb-3 text-gray-400" />
<p class="text-gray-500 dark:text-gray-400">
{$locale === 'de' ? 'Keine Ergebnisse gefunden' : 'No results found'}
</p>
</div>
{/if}
</div>
{/if}
<!-- Features Section -->
{#if activeSection === 'features'}
<div class="grid gap-4 sm:grid-cols-2">
{#each features as feature}
<div
class="group rounded-2xl bg-white/80 p-5 shadow-sm ring-1 ring-gray-200 backdrop-blur-sm transition-all hover:-translate-y-1 hover:shadow-lg dark:bg-white/5 dark:ring-white/10"
>
<div class="mb-3 flex items-center gap-3">
<span
class="flex h-10 w-10 items-center justify-center rounded-xl bg-blue-100 text-xl transition-transform group-hover:scale-110 dark:bg-blue-500/20"
>
{feature.icon}
</span>
<h3 class="font-semibold text-gray-900 dark:text-gray-100">
{feature.title}
</h3>
</div>
<p class="mb-4 text-sm text-gray-600 dark:text-gray-400">
{feature.description}
</p>
<ul class="space-y-2">
{#each feature.highlights as highlight}
<li class="flex items-center gap-2 text-sm text-gray-600 dark:text-gray-400">
<span
class="flex h-5 w-5 flex-shrink-0 items-center justify-center rounded-full bg-green-100 dark:bg-green-500/20"
>
<Check size={12} class="text-green-600 dark:text-green-400" />
</span>
{highlight}
</li>
{/each}
</ul>
</div>
{/each}
</div>
{/if}
<!-- Shortcuts Section -->
{#if activeSection === 'shortcuts'}
<div
class="overflow-hidden rounded-2xl bg-white/80 shadow-sm ring-1 ring-gray-200 backdrop-blur-sm dark:bg-white/5 dark:ring-white/10"
>
<div class="divide-y divide-gray-100 dark:divide-white/5">
{#each shortcuts as shortcut, i}
<div
class="flex items-center justify-between p-4 transition-colors hover:bg-gray-50/50 dark:hover:bg-white/5"
>
<span class="text-gray-700 dark:text-gray-300">{shortcut.action}</span>
<kbd
class="ml-4 flex-shrink-0 rounded-lg bg-gray-100 px-3 py-1.5 font-mono text-xs font-medium text-gray-700 ring-1 ring-gray-200 dark:bg-white/10 dark:text-gray-300 dark:ring-white/10"
>
{shortcut.shortcut}
</kbd>
</div>
{/each}
</div>
</div>
<!-- Tip -->
<div
class="mt-4 flex items-start gap-3 rounded-2xl bg-blue-50/80 p-4 ring-1 ring-blue-100 backdrop-blur-sm dark:bg-blue-500/10 dark:ring-blue-500/20"
>
<Info size={20} class="mt-0.5 flex-shrink-0 text-blue-500" />
<p class="text-sm text-blue-700 dark:text-blue-300">
{$locale === 'de'
? 'Tipp: Drücke Cmd/Ctrl + K, um jederzeit schnell zur Suche zu gelangen.'
: 'Tip: Press Cmd/Ctrl + K to quickly access search anytime.'}
</p>
</div>
{/if}
<!-- Contact Section -->
{#if activeSection === 'contact'}
<div class="space-y-6">
<!-- Header Card -->
<div
class="rounded-2xl bg-gradient-to-br from-blue-500 to-blue-600 p-6 text-white shadow-lg dark:from-blue-600 dark:to-blue-700"
>
<div class="flex items-center gap-3">
<div class="flex h-12 w-12 items-center justify-center rounded-xl bg-white/20">
<ChatCircle size={24} />
</div>
<div>
<h2 class="text-lg font-semibold">{t.contactTitle}</h2>
<p class="text-blue-100">{t.contactDescription}</p>
</div>
</div>
</div>
<!-- Contact Options -->
<div class="grid gap-4 sm:grid-cols-2">
<a
href="mailto:support@mana.how"
class="group flex items-center gap-4 rounded-2xl bg-white/80 p-5 shadow-sm ring-1 ring-gray-200 backdrop-blur-sm transition-all hover:-translate-y-1 hover:shadow-lg dark:bg-white/5 dark:ring-white/10"
>
<div
class="flex h-12 w-12 items-center justify-center rounded-xl bg-blue-100 text-blue-600 transition-transform group-hover:scale-110 dark:bg-blue-500/20 dark:text-blue-400"
>
<Envelope size={22} />
</div>
<div>
<p class="font-medium text-gray-900 dark:text-gray-100">
{t.email}
</p>
<p class="text-sm text-gray-600 dark:text-gray-400">support@mana.how</p>
</div>
<ArrowRight
size={18}
class="ml-auto text-gray-400 transition-transform group-hover:translate-x-1"
/>
</a>
<div
class="flex items-center gap-4 rounded-2xl bg-white/80 p-5 shadow-sm ring-1 ring-gray-200 backdrop-blur-sm dark:bg-white/5 dark:ring-white/10"
>
<div
class="flex h-12 w-12 items-center justify-center rounded-xl bg-green-100 text-green-600 dark:bg-green-500/20 dark:text-green-400"
>
<Clock size={22} />
</div>
<div>
<p class="font-medium text-gray-900 dark:text-gray-100">
{$locale === 'de' ? 'Antwortzeit' : 'Response Time'}
</p>
<p class="text-sm text-gray-600 dark:text-gray-400">
{t.responseTime}
</p>
</div>
</div>
</div>
<!-- Additional Info -->
<div
class="rounded-2xl bg-gray-50/80 p-5 ring-1 ring-gray-200 backdrop-blur-sm dark:bg-white/5 dark:ring-white/10"
>
<h3 class="mb-3 font-medium text-gray-900 dark:text-gray-100">
{$locale === 'de' ? 'Weitere Ressourcen' : 'Additional Resources'}
</h3>
<div class="space-y-2">
<a
href="https://mana.how"
target="_blank"
rel="noopener noreferrer"
class="flex items-center gap-2 text-sm text-gray-600 transition-colors hover:text-blue-600 dark:text-gray-400 dark:hover:text-blue-400"
>
<ArrowSquareOut size={14} />
{$locale === 'de' ? 'Website besuchen' : 'Visit Website'}
</a>
<a
href="https://mana.how/docs"
target="_blank"
rel="noopener noreferrer"
class="flex items-center gap-2 text-sm text-gray-600 transition-colors hover:text-blue-600 dark:text-gray-400 dark:hover:text-blue-400"
>
<BookOpen size={14} />
{$locale === 'de' ? 'Dokumentation' : 'Documentation'}
</a>
</div>
</div>
</div>
{/if}
</div>
</div>
<HelpPage
{content}
appName="Contacts"
appId="contacts"
{translations}
showBackButton
onBack={handleBack}
showGettingStarted={false}
showChangelog={false}
defaultSection="faq"
/>