mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-19 15:57:42 +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>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue