mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-20 09:23:37 +02:00
feat: major update with network graphs, themes, todo extensions, and more
## New Features ### Network Graph Visualization (Contacts, Calendar, Todo) - D3.js force simulation for physics-based layout - Zoom & pan with mouse/touchpad - Keyboard shortcuts: +/- zoom, 0 reset, Esc deselect, / search, F focus - Filtering by tags, company/location/project, connection strength - Shared components in @manacore/shared-ui ### Central Tags API (mana-core-auth) - CRUD endpoints for tags - Schema: tags table with userId, name, color, app - Shared tag components in @manacore/shared-ui ### Custom Themes System - Theme editor with live preview and color picker - Community theme gallery - Theme sharing (public, unlisted, private) - Backend API in mana-core-auth ### Todo App Extensions - Glass-pill design for task input and items - Settings page with 20+ preferences - Task edit modal with inline editing - Statistics page with visualizations - PWA support with offline capabilities - Multiple kanban boards ### Contacts App Features - Duplicate detection - Photo upload - Batch operations - Enhanced favorites page with multiple view modes - Alphabet view improvements - Search modal ### Help System - @manacore/shared-help-content - @manacore/shared-help-ui - @manacore/shared-help-types ### Other Features - Themes page for all apps - Referral system frontend - CommandBar (global search) - Skeleton loaders - Settings page improvements ## Bug Fixes - Network graph simulation initialization - Database schema TEXT for user_id columns (Better Auth compatibility) - Various styling fixes ## Documentation - Daily report for 2025-12-10 - CI/CD deployment guide 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
e84371aa94
commit
ee42b6cc76
381 changed files with 39284 additions and 6275 deletions
46
packages/shared-help-mobile/src/components/CategoryTabs.tsx
Normal file
46
packages/shared-help-mobile/src/components/CategoryTabs.tsx
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
/**
|
||||
* Category Tabs component for mobile Help screen
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Text, TouchableOpacity, ScrollView } from 'react-native';
|
||||
import type { HelpSection } from '../types';
|
||||
|
||||
interface CategoryTabsProps {
|
||||
sections: Array<{ id: HelpSection; label: string; show: boolean }>;
|
||||
activeSection: HelpSection;
|
||||
onSectionChange: (section: HelpSection) => void;
|
||||
}
|
||||
|
||||
export function CategoryTabs({ sections, activeSection, onSectionChange }: CategoryTabsProps) {
|
||||
const visibleSections = sections.filter((s) => s.show);
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
className="mb-4"
|
||||
contentContainerStyle={{ paddingHorizontal: 4 }}
|
||||
>
|
||||
{visibleSections.map((section) => (
|
||||
<TouchableOpacity
|
||||
key={section.id}
|
||||
onPress={() => onSectionChange(section.id)}
|
||||
className={`px-4 py-2 mr-2 rounded-full ${
|
||||
activeSection === section.id
|
||||
? 'bg-blue-500 dark:bg-blue-600'
|
||||
: 'bg-gray-100 dark:bg-gray-800'
|
||||
}`}
|
||||
>
|
||||
<Text
|
||||
className={`text-sm font-medium ${
|
||||
activeSection === section.id ? 'text-white' : 'text-gray-600 dark:text-gray-300'
|
||||
}`}
|
||||
>
|
||||
{section.label}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
71
packages/shared-help-mobile/src/components/ContactCard.tsx
Normal file
71
packages/shared-help-mobile/src/components/ContactCard.tsx
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
/**
|
||||
* Contact Card component for mobile
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { View, Text, TouchableOpacity, Linking } from 'react-native';
|
||||
import type { ContactInfo } from '@manacore/shared-help-types';
|
||||
import type { HelpTranslations } from '../types';
|
||||
|
||||
interface ContactCardProps {
|
||||
contact: ContactInfo | null;
|
||||
translations: Pick<HelpTranslations, 'contact'>;
|
||||
}
|
||||
|
||||
export function ContactCard({ contact, translations }: ContactCardProps) {
|
||||
if (!contact) {
|
||||
return (
|
||||
<View className="py-8 items-center">
|
||||
<Text className="text-gray-500 dark:text-gray-400">{translations.contact.noInfo}</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
function handleEmailPress() {
|
||||
if (contact.supportEmail) {
|
||||
Linking.openURL(`mailto:${contact.supportEmail}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Strip HTML tags for mobile display
|
||||
const plainContent = contact.content.replace(/<[^>]*>/g, '').trim();
|
||||
|
||||
return (
|
||||
<View>
|
||||
{plainContent && (
|
||||
<Text className="text-gray-600 dark:text-gray-300 text-sm leading-relaxed mb-4">
|
||||
{plainContent}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{contact.supportEmail && (
|
||||
<TouchableOpacity
|
||||
onPress={handleEmailPress}
|
||||
className="flex-row items-center bg-white dark:bg-gray-800 rounded-xl p-4 mb-3 border border-gray-200 dark:border-gray-700"
|
||||
>
|
||||
<View className="w-10 h-10 rounded-full bg-blue-100 dark:bg-blue-900/30 items-center justify-center mr-3">
|
||||
<Text>✉️</Text>
|
||||
</View>
|
||||
<View className="flex-1">
|
||||
<Text className="font-medium text-gray-900 dark:text-gray-100">
|
||||
{translations.contact.email}
|
||||
</Text>
|
||||
<Text className="text-sm text-gray-600 dark:text-gray-400">{contact.supportEmail}</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
|
||||
{contact.responseTime && (
|
||||
<View className="flex-row items-center bg-white dark:bg-gray-800 rounded-xl p-4 border border-gray-200 dark:border-gray-700">
|
||||
<View className="w-10 h-10 rounded-full bg-green-100 dark:bg-green-900/30 items-center justify-center mr-3">
|
||||
<Text>⏱️</Text>
|
||||
</View>
|
||||
<View className="flex-1">
|
||||
<Text className="font-medium text-gray-900 dark:text-gray-100">Response Time</Text>
|
||||
<Text className="text-sm text-gray-600 dark:text-gray-400">{contact.responseTime}</Text>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
57
packages/shared-help-mobile/src/components/FAQItem.tsx
Normal file
57
packages/shared-help-mobile/src/components/FAQItem.tsx
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
/**
|
||||
* Expandable FAQ Item component for mobile
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { View, Text, TouchableOpacity, LayoutAnimation, Platform, UIManager } from 'react-native';
|
||||
import type { FAQItem as FAQItemType } from '@manacore/shared-help-types';
|
||||
|
||||
// Enable LayoutAnimation on Android
|
||||
if (Platform.OS === 'android' && UIManager.setLayoutAnimationEnabledExperimental) {
|
||||
UIManager.setLayoutAnimationEnabledExperimental(true);
|
||||
}
|
||||
|
||||
interface FAQItemProps {
|
||||
item: FAQItemType;
|
||||
expanded?: boolean;
|
||||
onToggle?: () => void;
|
||||
}
|
||||
|
||||
export function FAQItem({ item, expanded = false, onToggle }: FAQItemProps) {
|
||||
function handlePress() {
|
||||
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
|
||||
onToggle?.();
|
||||
}
|
||||
|
||||
// Strip HTML tags for mobile display
|
||||
const plainAnswer = item.answer.replace(/<[^>]*>/g, '').trim();
|
||||
|
||||
return (
|
||||
<View className="border-b border-gray-200 dark:border-gray-700">
|
||||
<TouchableOpacity
|
||||
onPress={handlePress}
|
||||
className="py-4 flex-row items-center justify-between"
|
||||
accessibilityRole="button"
|
||||
accessibilityState={{ expanded }}
|
||||
>
|
||||
<Text className="flex-1 pr-4 font-medium text-gray-900 dark:text-gray-100 text-base">
|
||||
{item.question}
|
||||
</Text>
|
||||
<Text
|
||||
className="text-gray-500 dark:text-gray-400"
|
||||
style={{ transform: [{ rotate: expanded ? '180deg' : '0deg' }] }}
|
||||
>
|
||||
▼
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
{expanded && (
|
||||
<View className="pb-4">
|
||||
<Text className="text-gray-600 dark:text-gray-300 text-sm leading-relaxed">
|
||||
{plainAnswer}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
39
packages/shared-help-mobile/src/components/FAQList.tsx
Normal file
39
packages/shared-help-mobile/src/components/FAQList.tsx
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
/**
|
||||
* FAQ List component for mobile
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { View, Text } from 'react-native';
|
||||
import { FAQItem } from './FAQItem';
|
||||
import type { FAQListProps } from '../types';
|
||||
|
||||
export function FAQList({ items, translations }: FAQListProps) {
|
||||
const [expandedId, setExpandedId] = useState<string | null>(
|
||||
items.length > 0 ? items[0].id : null
|
||||
);
|
||||
|
||||
function toggleItem(id: string) {
|
||||
setExpandedId(expandedId === id ? null : id);
|
||||
}
|
||||
|
||||
if (items.length === 0) {
|
||||
return (
|
||||
<View className="py-8 items-center">
|
||||
<Text className="text-gray-500 dark:text-gray-400">{translations.faq.noItems}</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View>
|
||||
{items.map((item) => (
|
||||
<FAQItem
|
||||
key={item.id}
|
||||
item={item}
|
||||
expanded={expandedId === item.id}
|
||||
onToggle={() => toggleItem(item.id)}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
49
packages/shared-help-mobile/src/components/FeatureCard.tsx
Normal file
49
packages/shared-help-mobile/src/components/FeatureCard.tsx
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
/**
|
||||
* Feature Card component for mobile
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { View, Text } from 'react-native';
|
||||
import type { FeatureItem } from '@manacore/shared-help-types';
|
||||
|
||||
interface FeatureCardProps {
|
||||
item: FeatureItem;
|
||||
comingSoonLabel?: string;
|
||||
}
|
||||
|
||||
export function FeatureCard({ item, comingSoonLabel = 'Coming soon' }: FeatureCardProps) {
|
||||
return (
|
||||
<View className="bg-white dark:bg-gray-800 rounded-xl p-4 mb-3 border border-gray-200 dark:border-gray-700">
|
||||
<View className="flex-row items-center mb-2">
|
||||
{item.icon && <Text className="text-2xl mr-3">{item.icon}</Text>}
|
||||
<View className="flex-1">
|
||||
<View className="flex-row items-center">
|
||||
<Text className="font-semibold text-gray-900 dark:text-gray-100 text-base">
|
||||
{item.title}
|
||||
</Text>
|
||||
{item.comingSoon && (
|
||||
<View className="ml-2 bg-amber-100 dark:bg-amber-900/30 px-2 py-0.5 rounded-full">
|
||||
<Text className="text-xs text-amber-700 dark:text-amber-400">
|
||||
{comingSoonLabel}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Text className="text-gray-600 dark:text-gray-400 text-sm mb-2">{item.description}</Text>
|
||||
|
||||
{item.highlights && item.highlights.length > 0 && (
|
||||
<View className="mt-2">
|
||||
{item.highlights.map((highlight, index) => (
|
||||
<View key={index} className="flex-row items-start mb-1">
|
||||
<Text className="text-green-500 mr-2">✓</Text>
|
||||
<Text className="text-gray-600 dark:text-gray-400 text-sm flex-1">{highlight}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
26
packages/shared-help-mobile/src/components/FeaturesList.tsx
Normal file
26
packages/shared-help-mobile/src/components/FeaturesList.tsx
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
/**
|
||||
* Features List component for mobile
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { View, Text } from 'react-native';
|
||||
import { FeatureCard } from './FeatureCard';
|
||||
import type { FeaturesListProps } from '../types';
|
||||
|
||||
export function FeaturesList({ items, translations }: FeaturesListProps) {
|
||||
if (items.length === 0) {
|
||||
return (
|
||||
<View className="py-8 items-center">
|
||||
<Text className="text-gray-500 dark:text-gray-400">{translations.features.noItems}</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View>
|
||||
{items.map((item) => (
|
||||
<FeatureCard key={item.id} item={item} comingSoonLabel={translations.features.comingSoon} />
|
||||
))}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
42
packages/shared-help-mobile/src/components/HelpSearchBar.tsx
Normal file
42
packages/shared-help-mobile/src/components/HelpSearchBar.tsx
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
/**
|
||||
* Search Bar component for mobile Help screen
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { View, TextInput, TouchableOpacity, Text } from 'react-native';
|
||||
import type { HelpSearchBarProps } from '../types';
|
||||
|
||||
export function HelpSearchBar({ placeholder, onSearch, onClear }: HelpSearchBarProps) {
|
||||
const [query, setQuery] = useState('');
|
||||
|
||||
function handleChangeText(text: string) {
|
||||
setQuery(text);
|
||||
onSearch(text);
|
||||
}
|
||||
|
||||
function handleClear() {
|
||||
setQuery('');
|
||||
onClear();
|
||||
}
|
||||
|
||||
return (
|
||||
<View className="flex-row items-center bg-gray-100 dark:bg-gray-800 rounded-xl px-4 py-3 mb-4">
|
||||
<Text className="text-gray-400 mr-2">🔍</Text>
|
||||
<TextInput
|
||||
className="flex-1 text-gray-900 dark:text-gray-100 text-base"
|
||||
placeholder={placeholder}
|
||||
placeholderTextColor="#9CA3AF"
|
||||
value={query}
|
||||
onChangeText={handleChangeText}
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
clearButtonMode="while-editing"
|
||||
/>
|
||||
{query.length > 0 && (
|
||||
<TouchableOpacity onPress={handleClear} className="ml-2">
|
||||
<Text className="text-gray-400">✕</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
51
packages/shared-help-mobile/src/hooks/useHelpContent.ts
Normal file
51
packages/shared-help-mobile/src/hooks/useHelpContent.ts
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
/**
|
||||
* Hook for loading and managing help content in mobile apps
|
||||
*/
|
||||
|
||||
import { useState, useMemo } from 'react';
|
||||
import type { HelpContent } from '@manacore/shared-help-types';
|
||||
import { mergeContent, createEmptyContent, createSearcher } from '@manacore/shared-help-content';
|
||||
import type { UseHelpContentOptions, UseHelpContentResult } from '../types';
|
||||
|
||||
export function useHelpContent(options: UseHelpContentOptions): UseHelpContentResult {
|
||||
const { appId, locale, centralContent, appContent } = options;
|
||||
const [loading] = useState(false);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
// Merge central and app-specific content
|
||||
const content = useMemo(() => {
|
||||
try {
|
||||
const base = centralContent ?? createEmptyContent();
|
||||
if (appContent) {
|
||||
return mergeContent(base, appContent, {
|
||||
appId,
|
||||
locale,
|
||||
});
|
||||
}
|
||||
return base;
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err : new Error('Failed to merge content'));
|
||||
return createEmptyContent();
|
||||
}
|
||||
}, [centralContent, appContent, appId, locale]);
|
||||
|
||||
return {
|
||||
content,
|
||||
loading,
|
||||
error,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for searching help content
|
||||
*/
|
||||
export function useHelpSearch(content: HelpContent) {
|
||||
const searcher = useMemo(() => createSearcher(content), [content]);
|
||||
|
||||
return {
|
||||
search: (query: string, limit?: number) => {
|
||||
if (!query.trim()) return [];
|
||||
return searcher(query, { limit: limit ?? 10 });
|
||||
},
|
||||
};
|
||||
}
|
||||
32
packages/shared-help-mobile/src/index.ts
Normal file
32
packages/shared-help-mobile/src/index.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
/**
|
||||
* @manacore/shared-help-mobile
|
||||
* React Native components for the Help system
|
||||
*/
|
||||
|
||||
// Main screen
|
||||
export { HelpScreen } from './screens/HelpScreen';
|
||||
|
||||
// Components
|
||||
export { FAQList } from './components/FAQList';
|
||||
export { FAQItem } from './components/FAQItem';
|
||||
export { FeaturesList } from './components/FeaturesList';
|
||||
export { FeatureCard } from './components/FeatureCard';
|
||||
export { HelpSearchBar } from './components/HelpSearchBar';
|
||||
export { CategoryTabs } from './components/CategoryTabs';
|
||||
export { ContactCard } from './components/ContactCard';
|
||||
|
||||
// Hooks
|
||||
export { useHelpContent, useHelpSearch } from './hooks/useHelpContent';
|
||||
|
||||
// Types
|
||||
export type {
|
||||
HelpScreenProps,
|
||||
HelpTranslations,
|
||||
HelpSection,
|
||||
UseHelpContentOptions,
|
||||
UseHelpContentResult,
|
||||
FAQListProps,
|
||||
FeaturesListProps,
|
||||
HelpSearchBarProps,
|
||||
HelpSearchResultsProps,
|
||||
} from './types';
|
||||
206
packages/shared-help-mobile/src/screens/HelpScreen.tsx
Normal file
206
packages/shared-help-mobile/src/screens/HelpScreen.tsx
Normal file
|
|
@ -0,0 +1,206 @@
|
|||
/**
|
||||
* Main Help Screen component for mobile apps
|
||||
*/
|
||||
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import { View, Text, ScrollView, SafeAreaView } from 'react-native';
|
||||
import type { HelpScreenProps, HelpSection } from '../types';
|
||||
import { HelpSearchBar } from '../components/HelpSearchBar';
|
||||
import { CategoryTabs } from '../components/CategoryTabs';
|
||||
import { FAQList } from '../components/FAQList';
|
||||
import { FeaturesList } from '../components/FeaturesList';
|
||||
import { ContactCard } from '../components/ContactCard';
|
||||
import { useHelpSearch } from '../hooks/useHelpContent';
|
||||
import type { SearchResult } from '@manacore/shared-help-types';
|
||||
|
||||
export function HelpScreen({
|
||||
content,
|
||||
appName,
|
||||
appId: _appId,
|
||||
translations,
|
||||
onBack: _onBack,
|
||||
defaultSection = 'faq',
|
||||
}: HelpScreenProps) {
|
||||
const [activeSection, setActiveSection] = useState<HelpSection>(defaultSection);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [searchResults, setSearchResults] = useState<SearchResult[]>([]);
|
||||
|
||||
const { search } = useHelpSearch(content);
|
||||
|
||||
// Define available sections
|
||||
const sections = useMemo(
|
||||
() => [
|
||||
{ id: 'faq' as HelpSection, label: translations.sections.faq, show: content.faq.length > 0 },
|
||||
{
|
||||
id: 'features' as HelpSection,
|
||||
label: translations.sections.features,
|
||||
show: content.features.length > 0,
|
||||
},
|
||||
{
|
||||
id: 'shortcuts' as HelpSection,
|
||||
label: translations.sections.shortcuts,
|
||||
show: content.shortcuts.length > 0,
|
||||
},
|
||||
{
|
||||
id: 'getting-started' as HelpSection,
|
||||
label: translations.sections.gettingStarted,
|
||||
show: content.gettingStarted.length > 0,
|
||||
},
|
||||
{
|
||||
id: 'changelog' as HelpSection,
|
||||
label: translations.sections.changelog,
|
||||
show: content.changelog.length > 0,
|
||||
},
|
||||
{
|
||||
id: 'contact' as HelpSection,
|
||||
label: translations.sections.contact,
|
||||
show: !!content.contact,
|
||||
},
|
||||
],
|
||||
[content, translations]
|
||||
);
|
||||
|
||||
function handleSearch(query: string) {
|
||||
setSearchQuery(query);
|
||||
if (query.trim().length >= 2) {
|
||||
const results = search(query, 10);
|
||||
setSearchResults(results);
|
||||
} else {
|
||||
setSearchResults([]);
|
||||
}
|
||||
}
|
||||
|
||||
function handleClearSearch() {
|
||||
setSearchQuery('');
|
||||
setSearchResults([]);
|
||||
}
|
||||
|
||||
function handleResultPress(result: SearchResult) {
|
||||
// Navigate to appropriate section
|
||||
switch (result.type) {
|
||||
case 'faq':
|
||||
setActiveSection('faq');
|
||||
break;
|
||||
case 'feature':
|
||||
setActiveSection('features');
|
||||
break;
|
||||
case 'guide':
|
||||
setActiveSection('getting-started');
|
||||
break;
|
||||
case 'changelog':
|
||||
setActiveSection('changelog');
|
||||
break;
|
||||
}
|
||||
handleClearSearch();
|
||||
}
|
||||
|
||||
// Use handleResultPress in search results (currently just viewing results)
|
||||
void handleResultPress;
|
||||
|
||||
function renderContent() {
|
||||
// Show search results if searching
|
||||
if (searchQuery.length >= 2) {
|
||||
if (searchResults.length === 0) {
|
||||
return (
|
||||
<View className="py-8 items-center">
|
||||
<Text className="text-gray-500 dark:text-gray-400">
|
||||
{translations.search.noResults.replace('{query}', searchQuery)}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View>
|
||||
<Text className="text-sm text-gray-500 dark:text-gray-400 mb-4">
|
||||
{translations.search.resultsCount.replace('{count}', String(searchResults.length))}
|
||||
</Text>
|
||||
{searchResults.map((result) => (
|
||||
<View
|
||||
key={result.id}
|
||||
className="bg-white dark:bg-gray-800 rounded-xl p-4 mb-2 border border-gray-200 dark:border-gray-700"
|
||||
>
|
||||
<Text className="font-medium text-gray-900 dark:text-gray-100">{result.title}</Text>
|
||||
<Text className="text-sm text-gray-600 dark:text-gray-400 mt-1">
|
||||
{result.excerpt}
|
||||
</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
// Show section content
|
||||
switch (activeSection) {
|
||||
case 'faq':
|
||||
return <FAQList items={content.faq} translations={translations} />;
|
||||
case 'features':
|
||||
return <FeaturesList items={content.features} translations={translations} />;
|
||||
case 'contact':
|
||||
return <ContactCard contact={content.contact} translations={translations} />;
|
||||
case 'shortcuts':
|
||||
return (
|
||||
<View className="py-8 items-center">
|
||||
<Text className="text-gray-500 dark:text-gray-400">
|
||||
{translations.shortcuts.noItems}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
case 'getting-started':
|
||||
return (
|
||||
<View className="py-8 items-center">
|
||||
<Text className="text-gray-500 dark:text-gray-400">
|
||||
{translations.gettingStarted.noItems}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
case 'changelog':
|
||||
return (
|
||||
<View className="py-8 items-center">
|
||||
<Text className="text-gray-500 dark:text-gray-400">
|
||||
{translations.changelog.noItems}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<SafeAreaView className="flex-1 bg-gray-50 dark:bg-gray-900">
|
||||
<ScrollView className="flex-1 px-4 pt-4">
|
||||
{/* Header */}
|
||||
<View className="mb-6">
|
||||
<Text className="text-2xl font-bold text-gray-900 dark:text-gray-100">
|
||||
{translations.title}
|
||||
</Text>
|
||||
{translations.subtitle && (
|
||||
<Text className="text-gray-600 dark:text-gray-400 mt-1">
|
||||
{translations.subtitle} - {appName}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* Search */}
|
||||
<HelpSearchBar
|
||||
placeholder={translations.searchPlaceholder}
|
||||
onSearch={handleSearch}
|
||||
onClear={handleClearSearch}
|
||||
/>
|
||||
|
||||
{/* Category Tabs */}
|
||||
{searchQuery.length < 2 && (
|
||||
<CategoryTabs
|
||||
sections={sections}
|
||||
activeSection={activeSection}
|
||||
onSectionChange={setActiveSection}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Content */}
|
||||
<View className="pb-8">{renderContent()}</View>
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
95
packages/shared-help-mobile/src/types.ts
Normal file
95
packages/shared-help-mobile/src/types.ts
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
/**
|
||||
* Mobile-specific types for Help components
|
||||
*/
|
||||
|
||||
import type { HelpContent, SearchResult, SupportedLanguage } from '@manacore/shared-help-types';
|
||||
|
||||
export type HelpSection =
|
||||
| 'faq'
|
||||
| 'features'
|
||||
| 'shortcuts'
|
||||
| 'getting-started'
|
||||
| 'changelog'
|
||||
| 'contact';
|
||||
|
||||
export interface HelpScreenProps {
|
||||
content: HelpContent;
|
||||
appName: string;
|
||||
appId: string;
|
||||
translations: HelpTranslations;
|
||||
onBack?: () => void;
|
||||
defaultSection?: HelpSection;
|
||||
}
|
||||
|
||||
export interface HelpTranslations {
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
searchPlaceholder: string;
|
||||
sections: {
|
||||
faq: string;
|
||||
features: string;
|
||||
shortcuts: string;
|
||||
gettingStarted: string;
|
||||
changelog: string;
|
||||
contact: string;
|
||||
};
|
||||
search: {
|
||||
noResults: string;
|
||||
resultsCount: string;
|
||||
};
|
||||
faq: {
|
||||
noItems: string;
|
||||
};
|
||||
features: {
|
||||
noItems: string;
|
||||
comingSoon: string;
|
||||
};
|
||||
shortcuts: {
|
||||
noItems: string;
|
||||
};
|
||||
gettingStarted: {
|
||||
noItems: string;
|
||||
};
|
||||
changelog: {
|
||||
noItems: string;
|
||||
};
|
||||
contact: {
|
||||
noInfo: string;
|
||||
email: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface UseHelpContentOptions {
|
||||
appId: string;
|
||||
locale: SupportedLanguage;
|
||||
centralContent?: HelpContent;
|
||||
appContent?: Partial<HelpContent>;
|
||||
}
|
||||
|
||||
export interface UseHelpContentResult {
|
||||
content: HelpContent;
|
||||
loading: boolean;
|
||||
error: Error | null;
|
||||
}
|
||||
|
||||
export interface FAQListProps {
|
||||
items: HelpContent['faq'];
|
||||
translations: Pick<HelpTranslations, 'faq'>;
|
||||
}
|
||||
|
||||
export interface FeaturesListProps {
|
||||
items: HelpContent['features'];
|
||||
translations: Pick<HelpTranslations, 'features'>;
|
||||
}
|
||||
|
||||
export interface HelpSearchBarProps {
|
||||
placeholder?: string;
|
||||
onSearch: (query: string) => void;
|
||||
onClear: () => void;
|
||||
}
|
||||
|
||||
export interface HelpSearchResultsProps {
|
||||
results: SearchResult[];
|
||||
onResultPress: (result: SearchResult) => void;
|
||||
translations: Pick<HelpTranslations, 'search'>;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue