Add a unified event editor that works in two modes: - Calendar mode: Create/edit events directly via EventService API - Chat mode: Edit AI-proposed events before confirming them The chat mode allows users to modify proposed events (title, time, recurrence) and persists changes both locally and to the server. New components: DateTimePicker, ScrollableDropdown, useDropdownPosition New API: PUT /api/chat/messages/:messageId/proposal
31 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Project Overview
CalChat is a calendar mobile app with AI support. The core concept is managing calendar events through a chat interface with an AI chatbot. Users can add, edit, and delete events via natural language conversation.
This is a fullstack TypeScript monorepo with npm workspaces.
Commands
Root (monorepo)
npm install # Install all dependencies for all workspaces
npm run format # Format all TypeScript files with Prettier
Client (apps/client) - Expo React Native app
npm run start -w @calchat/client # Start Expo dev server
npm run android -w @calchat/client # Start on Android
npm run ios -w @calchat/client # Start on iOS
npm run web -w @calchat/client # Start web version
npm run lint -w @calchat/client # Run ESLint
npm run build:apk -w @calchat/client # Build APK locally with EAS
Server (apps/server) - Express.js backend
npm run dev -w @calchat/server # Start dev server with hot reload (tsx watch)
npm run build -w @calchat/server # Compile TypeScript
npm run start -w @calchat/server # Run compiled server (port 3000)
Technology Stack
| Area | Technology | Purpose |
|---|---|---|
| Frontend | React Native | Mobile UI Framework |
| Expo | Development platform | |
| Expo-Router | File-based routing | |
| NativeWind | Tailwind CSS for React Native | |
| Zustand | State management | |
| FlashList | High-performance lists | |
| EAS Build | Local APK/IPA builds | |
| Backend | Express.js | Web framework |
| MongoDB | Database | |
| Mongoose | ODM | |
| GPT (OpenAI) | AI/LLM for chat | |
| X-User-Id Header | Authentication (simple, no JWT yet) | |
| pino / pino-http | Structured logging | |
| react-native-logs | Client-side logging | |
| Planned | iCalendar | Event export/import |
Architecture
Workspace Structure
apps/client - @calchat/client - Expo React Native app
apps/server - @calchat/server - Express.js backend
packages/shared - @calchat/shared - Shared TypeScript types and models
Frontend Architecture (apps/client)
src/
├── app/ # Expo-Router file-based routing
│ ├── _layout.tsx # Root Stack layout
│ ├── index.tsx # Entry redirect
│ ├── login.tsx # Login screen
│ ├── register.tsx # Registration screen
│ ├── (tabs)/ # Tab navigation group
│ │ ├── _layout.tsx # Tab bar configuration (themed)
│ │ ├── chat.tsx # Chat screen (AI conversation)
│ │ ├── calendar.tsx # Calendar overview
│ │ └── settings.tsx # Settings screen (theme switcher, logout)
│ ├── editEvent.tsx # Event edit screen (dual-mode: calendar/chat)
│ ├── event/
│ │ └── [id].tsx # Event detail screen (dynamic route)
│ └── note/
│ └── [id].tsx # Note editor for event (dynamic route)
├── components/
│ ├── AuthGuard.tsx # Auth wrapper: loads user, shows loading, redirects if unauthenticated
│ ├── BaseBackground.tsx # Common screen wrapper (themed)
│ ├── BaseButton.tsx # Reusable button component (themed, supports children)
│ ├── Header.tsx # Header component (themed)
│ ├── AuthButton.tsx # Reusable button for auth screens (themed, with shadow)
│ ├── CardBase.tsx # Reusable card component (header + content + optional footer)
│ ├── ModalBase.tsx # Reusable modal with backdrop (uses CardBase, click-outside-to-close)
│ ├── ChatBubble.tsx # Reusable chat bubble component (used by ChatMessage & TypingIndicator)
│ ├── TypingIndicator.tsx # Animated typing indicator (. .. ...) shown while waiting for AI response
│ ├── EventCardBase.tsx # Event card layout with icons (uses CardBase)
│ ├── EventCard.tsx # Calendar event card (uses EventCardBase + edit/delete buttons)
│ ├── EventConfirmDialog.tsx # AI-proposed event confirmation modal (skeleton)
│ ├── ProposedEventCard.tsx # Chat event proposal (uses EventCardBase + confirm/reject/edit buttons)
│ ├── DeleteEventModal.tsx # Delete confirmation modal (uses ModalBase)
│ ├── DateTimePicker.tsx # Date and time picker components
│ └── ScrollableDropdown.tsx # Scrollable dropdown component
├── Themes.tsx # Theme definitions: THEMES object with defaultLight/defaultDark, Theme type
├── logging/
│ ├── index.ts # Re-exports
│ └── logger.ts # react-native-logs config (apiLogger, storeLogger)
├── services/
│ ├── index.ts # Re-exports all services
│ ├── ApiClient.ts # HTTP client with X-User-Id header injection, request logging, handles empty responses (204)
│ ├── AuthService.ts # login(), register(), logout() - calls API and updates AuthStore
│ ├── EventService.ts # getAll(), getById(), getByDateRange(), create(), update(), delete(mode, occurrenceDate)
│ └── ChatService.ts # sendMessage(), confirmEvent(deleteMode, occurrenceDate), rejectEvent(), getConversations(), getConversation(), updateProposalEvent()
├── stores/ # Zustand state management
│ ├── index.ts # Re-exports all stores
│ ├── AuthStore.ts # user, isAuthenticated, isLoading, login(), logout(), loadStoredUser()
│ │ # Uses expo-secure-store (native) / localStorage (web)
│ ├── ChatStore.ts # messages[], isWaitingForResponse, addMessage(), addMessages(), updateMessage(), clearMessages(), setWaitingForResponse(), chatMessageToMessageData()
│ ├── EventsStore.ts # events[], setEvents(), addEvent(), updateEvent(), deleteEvent()
│ └── ThemeStore.ts # theme, setTheme() - reactive theme switching with Zustand
└── hooks/
└── useDropdownPosition.ts # Hook for positioning dropdowns relative to trigger element
Routing: Tab-based navigation with Chat, Calendar, and Settings as main screens. Auth screens (login, register) outside tabs. Dynamic routes for event detail and note editing.
Authentication Flow:
AuthGuardcomponent wraps the tab layout in(tabs)/_layout.tsx- On app start,
AuthGuardcallsloadStoredUser()and shows loading indicator - If not authenticated, redirects to
/login index.tsxsimply redirects to/(tabs)/chat- AuthGuard handles the rest- This pattern handles Expo Router's navigation state caching (avoids race conditions)
Theme System
The app supports multiple themes (light/dark) via a reactive Zustand store.
Theme Structure (Themes.tsx):
export type Theme = {
chatBot, primeFg, primeBg, secondaryBg, messageBorderBg, placeholderBg,
calenderBg, confirmButton, rejectButton, disabledButton, buttonText,
textPrimary, textSecondary, textMuted, eventIndicator, borderPrimary, shadowColor
};
export const THEMES = {
defaultLight: { ... },
defaultDark: { ... }
} as const satisfies Record<string, Theme>;
Usage in Components:
import { useThemeStore } from "../stores/ThemeStore";
const MyComponent = () => {
const { theme } = useThemeStore();
return <View style={{ backgroundColor: theme.primeBg }} />;
};
Theme Switching:
const { setTheme } = useThemeStore();
setTheme("defaultDark"); // or "defaultLight"
Note: shadowColor only works on iOS. Android uses elevation with system-defined shadow colors.
Base Components (CardBase & ModalBase)
Reusable base components for cards and modals with consistent styling.
CardBase - Card structure with header, content, and optional footer:
<CardBase
title="Title"
subtitle="Optional subtitle"
footer={{ label: "Button", onPress: () => {} }}
// Styling props (all optional):
headerPadding={4} // p-{n}, default: px-3 py-2
contentPadding={4} // p-{n}, default: px-3 py-2
headerTextSize="text-lg" // "text-sm" | "text-base" | "text-lg" | "text-xl"
borderWidth={2} // outer border, default: 2
headerBorderWidth={3} // header bottom border, default: borderWidth
contentBg={theme.primeBg} // content background color, default: theme.secondaryBg
scrollable={true} // wrap content in ScrollView
maxContentHeight={400} // for scrollable content
>
{children}
</CardBase>
ModalBase - Modal with backdrop using CardBase internally:
<ModalBase
visible={isVisible}
onClose={() => setVisible(false)}
title="Modal Title"
subtitle="Optional"
footer={{ label: "Close", onPress: onClose }}
scrollable={true}
maxContentHeight={400}
>
{children}
</ModalBase>
ModalBase provides: transparent Modal + backdrop (click-outside-to-close) + Android back button support.
ModalBase Architecture Note: Uses absolute-positioned backdrop behind the card content (not nested Pressables). This approach:
- Fixes modal stacking issues on web (React Native Web renders modals as DOM portals)
- Allows proper scrolling on Android (no touch event conflicts)
- Card naturally blocks touches from reaching backdrop due to z-order
Component Hierarchy:
CardBase
├── ModalBase (uses CardBase)
│ ├── DeleteEventModal
│ └── EventOverlay (in calendar.tsx)
└── EventCardBase (uses CardBase)
├── EventCard
└── ProposedEventCard
Backend Architecture (apps/server)
src/
├── app.ts # Entry point, DI setup, Express config
├── controllers/ # Request handlers + middleware (per architecture diagram)
│ ├── AuthController.ts # login(), register(), refresh(), logout()
│ ├── ChatController.ts # sendMessage(), confirmEvent(), rejectEvent(), getConversations(), getConversation(), updateProposalEvent()
│ ├── EventController.ts # create(), getById(), getAll(), getByDateRange(), update(), delete()
│ ├── AuthMiddleware.ts # authenticate() - X-User-Id header validation
│ └── LoggingMiddleware.ts # httpLogger - pino-http request logging
├── logging/
│ ├── index.ts # Re-exports
│ ├── logger.ts # pino config with redact for sensitive data
│ └── Logged.ts # @Logged() class decorator for automatic method logging
├── routes/ # API endpoint definitions
│ ├── index.ts # Combines all routes under /api
│ ├── auth.routes.ts # /api/auth/*
│ ├── chat.routes.ts # /api/chat/* (protected)
│ └── event.routes.ts # /api/events/* (protected)
├── services/ # Business logic
│ ├── interfaces/ # DB-agnostic interfaces (for dependency injection)
│ │ ├── AIProvider.ts # processMessage()
│ │ ├── UserRepository.ts # findById, findByEmail, findByUserName, create + CreateUserData
│ │ ├── EventRepository.ts
│ │ └── ChatRepository.ts
│ ├── AuthService.ts
│ ├── ChatService.ts
│ └── EventService.ts
├── repositories/ # Data access (DB-specific implementations)
│ ├── index.ts # Re-exports from ./mongo
│ └── mongo/ # MongoDB implementation
│ ├── models/ # Mongoose schemas
│ │ ├── types.ts # Shared types (IdVirtual interface)
│ │ ├── UserModel.ts
│ │ ├── EventModel.ts
│ │ └── ChatModel.ts
│ ├── MongoUserRepository.ts # findById, findByEmail, findByUserName, create
│ ├── MongoEventRepository.ts
│ └── MongoChatRepository.ts
├── ai/
│ ├── GPTAdapter.ts # Implements AIProvider using OpenAI GPT
│ ├── index.ts # Re-exports GPTAdapter
│ └── utils/ # Shared AI utilities (provider-agnostic)
│ ├── index.ts # Re-exports
│ ├── eventFormatter.ts # formatExistingEvents() for system prompt
│ ├── systemPrompt.ts # buildSystemPrompt() - German calendar assistant prompt
│ ├── toolDefinitions.ts # TOOL_DEFINITIONS - provider-agnostic tool specs
│ └── toolExecutor.ts # executeToolCall() - handles getDay, proposeCreate/Update/Delete, searchEvents
├── utils/
│ ├── jwt.ts # signToken(), verifyToken() - NOT USED YET (no JWT)
│ ├── password.ts # hash(), compare() using bcrypt
│ ├── eventFormatters.ts # getWeeksOverview(), getMonthOverview() - formatted event listings
│ └── recurrenceExpander.ts # expandRecurringEvents() - expand recurring events into occurrences
└── scripts/
└── hash-password.js # Utility to hash passwords for manual DB updates
API Endpoints:
POST /api/auth/login- User loginPOST /api/auth/register- User registrationPOST /api/auth/refresh- Refresh JWT tokenPOST /api/auth/logout- User logoutGET /api/events- Get all events (protected)GET /api/events/range- Get events by date range (protected)GET /api/events/:id- Get single event (protected)POST /api/events- Create event (protected)PUT /api/events/:id- Update event (protected)DELETE /api/events/:id- Delete event (protected, query params: mode, occurrenceDate for recurring)POST /api/chat/message- Send message to AI (protected)POST /api/chat/confirm/:conversationId/:messageId- Confirm proposed event (protected)POST /api/chat/reject/:conversationId/:messageId- Reject proposed event (protected)GET /api/chat/conversations- Get all conversations (protected)GET /api/chat/conversations/:id- Get messages of a conversation with cursor-based pagination (protected)PUT /api/chat/messages/:messageId/proposal- Update proposal event data before confirming (protected)GET /health- Health checkPOST /api/ai/test- AI test endpoint (development only)
Shared Package (packages/shared)
src/
├── index.ts
├── models/
│ ├── index.ts
│ ├── User.ts # User, CreateUserDTO, LoginDTO, AuthResponse
│ ├── CalendarEvent.ts # CalendarEvent, CreateEventDTO, UpdateEventDTO, ExpandedEvent
│ ├── ChatMessage.ts # ChatMessage, Conversation, SendMessageDTO, CreateMessageDTO,
│ │ # GetMessagesOptions, ChatResponse, ConversationSummary,
│ │ # ProposedEventChange, EventAction, RespondedAction, UpdateMessageDTO
│ └── Constants.ts # DAYS, MONTHS, Day, Month, DAY_INDEX, DAY_INDEX_TO_DAY,
│ # DAY_TO_GERMAN, DAY_TO_GERMAN_SHORT, MONTH_TO_GERMAN
└── utils/
├── index.ts
├── dateHelpers.ts # getDay() - get date for specific weekday relative to today
├── formatters.ts # formatDate(), formatTime(), formatDateTime(), formatDateWithWeekday() - German locale
└── rruleHelpers.ts # parseRRule() - parse RRULE strings to extract freq, until, count, interval, byDay
Key Types:
User: id, email, userName, passwordHash?, createdAt?, updatedAt?CalendarEvent: id, userId, title, description?, startTime, endTime, note?, isRecurring?, recurrenceRule?, exceptionDates?ExpandedEvent: Extends CalendarEvent with occurrenceStart, occurrenceEnd (for recurring event instances)ChatMessage: id, conversationId, sender ('user' | 'assistant'), content, proposedChanges?ProposedEventChange: id, action ('create' | 'update' | 'delete'), eventId?, event?, updates?, respondedAction?, deleteMode?, occurrenceDate?- Each proposal has unique
id(e.g., "proposal-0") for individual confirm/reject respondedActiontracks user response per proposal (not per message)deleteMode('single' | 'future' | 'all') andoccurrenceDatefor recurring event deletion
- Each proposal has unique
RecurringDeleteMode: 'single' | 'future' | 'all' - delete modes for recurring eventsDeleteRecurringEventDTO: mode, occurrenceDate? - DTO for recurring event deletionConversation: id, userId, createdAt?, updatedAt? (messages loaded separately via lazy loading)CreateUserDTO: email, userName, password (for registration)LoginDTO: identifier (email OR userName), passwordCreateEventDTO: Used for creating events AND for AI-proposed events, includes optionalexceptionDatesfor proposalsGetMessagesOptions: Cursor-based pagination withbefore?: stringandlimit?: numberConversationSummary: id, lastMessage?, createdAt? (for conversation list)UpdateMessageDTO: proposalId?, respondedAction? (for marking individual proposals as confirmed/rejected)RespondedAction: 'confirm' | 'reject' (tracks user response to proposed events)Day: "Monday" | "Tuesday" | ... | "Sunday"Month: "January" | "February" | ... | "December"
Database Abstraction
The repository pattern allows swapping databases:
- Interfaces (
services/interfaces/) are DB-agnostic - Implementations (
repositories/mongo/) are DB-specific - To add MySQL: create
repositories/mysql/with TypeORM entities
Mongoose Model Pattern
All Mongoose models use a consistent pattern for TypeScript-safe id virtuals:
import { IdVirtual } from './types';
const Schema = new Schema<Doc, Model<Doc, {}, {}, IdVirtual>, {}, {}, IdVirtual>(
{ /* fields */ },
{
virtuals: {
id: {
get() { return this._id.toString(); }
}
},
toJSON: {
virtuals: true,
transform: (_, ret) => {
delete ret._id;
delete ret.__v;
return ret;
}
}
}
);
Repositories use doc.toJSON() as unknown as Type casting (required because Mongoose's TypeScript types don't reflect virtual fields in toJSON output).
Logging
Structured logging with pino (server) and react-native-logs (client).
Server Logging:
pinowithpino-prettyfor development, JSON in productionpino-httpmiddleware logs all HTTP requests (method, path, status, duration)@Logged()class decorator for automatic method logging on repositories and services- Sensitive data (password, token, etc.) automatically redacted via pino's
redactconfig
@Logged Decorator Pattern:
@Logged("MongoEventRepository")
export class MongoEventRepository implements EventRepository { ... }
@Logged("GPTAdapter")
export class GPTAdapter implements AIProvider { ... }
The decorator uses a Proxy to intercept method calls lazily, preserves sync/async nature, and logs start/completion/failure with duration.
Log Summarization:
The @Logged decorator automatically summarizes large arguments to keep logs readable:
conversationHistory→"[5 messages]"existingEvents→"[3 events]"proposedChanges→ logged in full (for debugging AI issues)- Long strings (>100 chars) → truncated
- Arrays →
"[Array(n)]"
Client Logging:
react-native-logswith namespaced loggers (apiLogger, storeLogger)- ApiClient logs all requests with method, endpoint, status, duration
- Log level: debug in DEV, warn in production
MVP Feature Scope
Must-Have
- Chat interface with AI assistant (text input) for event management
- Calendar overview
- Manual event CRUD (without AI)
- View completed events
- Simple reminders
- One note per event
- Recurring events
Nice-to-Have
- iCalendar import/export
- Multiple calendars
- CalDAV synchronization with external services
Development Environment
MongoDB (Docker)
cd apps/server/docker/mongo
docker compose up -d # Start MongoDB + Mongo Express
docker compose down # Stop services
- MongoDB:
localhost:27017(root/mongoose) - Mongo Express UI:
localhost:8083(admin/admin)
Environment Variables
Server requires .env file in apps/server/:
JWT_SECRET=your-secret-key
JWT_EXPIRES_IN=1h
MONGODB_URI=mongodb://root:mongoose@localhost:27017/calchat?authSource=admin
OPENAI_API_KEY=sk-proj-...
USE_TEST_RESPONSES=false # true = static test responses, false = real GPT AI
LOG_LEVEL=debug # debug | info | warn | error | fatal
NODE_ENV=development # development = pretty logs, production = JSON
Current Implementation Status
Backend:
- Implemented:
AuthController: login(), register() with error handlingAuthService: login() supports email OR userName, register() checks for existing email AND userNameAuthMiddleware: Validates X-User-Id header for protected routesMongoUserRepository: findById(), findByEmail(), findByUserName(), create()utils/password: hash(), compare() using bcryptscripts/hash-password.js: Utility for manual password resetsdotenvintegration for environment variablesChatController: sendMessage(), confirmEvent(), rejectEvent()ChatService: processMessage() with test responses (create, update, delete actions), confirmEvent() handles all CRUD actionsMongoEventRepository: Full CRUD implemented (findById, findByUserId, findByDateRange, create, update, delete, addExceptionDate)EventController: Full CRUD (create, getById, getAll, getByDateRange, update, delete)EventService: Full CRUD with recurring event expansion via recurrenceExpander, deleteRecurring() with three modes (single/future/all)utils/eventFormatters: getWeeksOverview(), getMonthOverview() with German localizationutils/recurrenceExpander: expandRecurringEvents() using rrule library for RRULE parsingChatController: getConversations(), getConversation() with cursor-based pagination supportChatService: getConversations(), getConversation(), processMessage() uses real AI or test responses (via USE_TEST_RESPONSES), confirmEvent()/rejectEvent() update respondedAction and persist response messagesMongoChatRepository: Full CRUD implemented (getConversationsByUser, createConversation, getMessages with cursor pagination, createMessage, updateMessage, updateProposalResponse, updateProposalEvent)ChatRepositoryinterface: updateMessage(), updateProposalResponse(), updateProposalEvent() for per-proposal trackingGPTAdapter: Full implementation with OpenAI GPT (gpt-4o-mini model), function calling for calendar operations, collects multiple proposals per responseai/utils/: Provider-agnostic shared utilities (systemPrompt, toolDefinitions, toolExecutor, eventFormatter)ai/utils/systemPrompt: Includes RRULE documentation - AI knows to create separate events when times differ by day, warns AI not to put RRULE in description fieldai/utils/toolDefinitions: proposeUpdateEvent supportsisRecurringandrecurrenceRuleparameters for adding UNTIL or modifying recurrenceutils/recurrenceExpander: Handles RRULE parsing, stripsRRULE:prefix if present (AI may include it), filters out exceptionDateslogging/: Structured logging with pino, pino-http middleware, @Logged decorator- All repositories and GPTAdapter decorated with @Logged for automatic method logging
- CORS configured to allow X-User-Id header
- Stubbed (TODO):
AuthController: refresh(), logout()AuthService: refreshToken()- JWT authentication (currently using simple X-User-Id header)
Shared:
- Types, DTOs, constants (Day, Month with German translations), ExpandedEvent type defined and exported
rruleHelpers.ts:parseRRule()parses RRULE strings using rrule library, returnsParsedRRulewith freq, until, count, interval, byDayformatters.ts: German date/time formatters (formatDate,formatTime,formatDateTime,formatDateWithWeekday) used by both client and server- rrule library added as dependency for RRULE parsing
Frontend:
- Authentication fully implemented:
AuthStore: Manages user state with expo-secure-store (native) / localStorage (web)AuthService: login(), register(), logout() - calls backend APIApiClient: Automatically injects X-User-Id header for authenticated requests, handles empty responses (204)AuthGuard: Reusable component that wraps protected routes - loads user, shows loading, redirects if unauthenticated- Login screen: Supports email OR userName login
- Register screen: Email validation, checks for existing email/userName
AuthButton: Reusable button component with themed shadowHeader: Themed header component (logout moved to Settings)(tabs)/_layout.tsx: Wraps tabs with AuthGuard for protected accessindex.tsx: Simple redirect to chat (AuthGuard handles auth)
- Theme system fully implemented:
ThemeStore: Zustand store with theme state and setTheme()Themes.tsx: THEMES object with defaultLight/defaultDark variants- All components use
useThemeStore()for reactive theme colors - Settings screen with theme switcher (light/dark)
BaseButton: Reusable themed button component
- Tab navigation (Chat, Calendar, Settings) implemented with themed UI
- Calendar screen fully functional:
- Month navigation with grid display and Ionicons (chevron-back/forward)
- MonthSelector dropdown with infinite scroll (dynamically loads months, lazy-loaded when modal opens, cleared on close for memory efficiency)
- Events loaded from API via EventService.getByDateRange()
- Orange dot indicator for days with events
- Tap-to-open modal overlay showing EventCards for selected day
- Supports events from adjacent months visible in grid
- Uses
useFocusEffectfor automatic reload on tab focus - DeleteEventModal integration for recurring event deletion with three modes
- EventOverlay hides when DeleteEventModal is open (fixes modal stacking on web)
- Chat screen fully functional with FlashList, message sending, and event confirm/reject
- Multiple event proposals: AI can propose multiple events in one response
- Arrow navigation between proposals with "Event X von Y" counter
- Each proposal individually confirmable/rejectable
- Typing indicator: Animated dots (. .. ...) shown after 500ms delay while waiting for AI response
- Messages persisted to database via ChatService, loaded via
useFocusEffectwhen screen gains focus - Tracks conversationId for message continuity across sessions
- ChatStore with addMessages() for bulk loading, chatMessageToMessageData() helper
- KeyboardAvoidingView for proper keyboard handling (iOS padding, Android height)
- Auto-scroll to end on new messages and keyboard show
- keyboardDismissMode="interactive" and keyboardShouldPersistTaps="handled"
EventService: getAll(), getById(), getByDateRange(), create(), update(), delete(mode, occurrenceDate) - fully implemented with recurring delete modesChatService: sendMessage(), confirmEvent(deleteMode, occurrenceDate), rejectEvent(), getConversations(), getConversation(), updateProposalEvent() - fully implemented with cursor pagination, recurring delete support, and proposal editingCardBase: Reusable card component with header (title/subtitle), content area, and optional footer button - configurable padding, border, text size via props, ScrollView usesnestedScrollEnabledfor AndroidModalBase: Reusable modal wrapper with backdrop (absolute-positioned behind card), uses CardBase internally - provides click-outside-to-close, Android back button support, and proper scrolling on AndroidEventCardBase: Event card with date/time/recurring icons - uses CardBase for structureEventCard: Uses EventCardBase + edit/delete buttons (TouchableOpacity with delayPressIn for scroll-friendly touch handling)ProposedEventCard: Uses EventCardBase + confirm/reject/edit buttons for chat proposals, displays green highlighted text for new changes ("Neue Ausnahme: [date]" for single delete, "Neues Ende: [date]" for UNTIL updates). Edit button allows modifying proposals before confirming.DeleteEventModal: Delete confirmation modal using ModalBase - shows three options for recurring events (single/future/all), simple confirm for non-recurringEventOverlay(in calendar.tsx): Day events overlay using ModalBase - shows EventCards for selected dayThemes.tsx: Theme definitions with THEMES object (defaultLight, defaultDark) including all color tokens (textPrimary, borderPrimary, eventIndicator, secondaryBg, shadowColor, etc.)EventsStore: Zustand store with setEvents(), addEvent(), updateEvent(), deleteEvent() - stores ExpandedEvent[]ChatStore: Zustand store with addMessage(), addMessages(), updateMessage(), clearMessages(), isWaitingForResponse/setWaitingForResponse() for typing indicator - loads from server on mount and persists across tab switchesThemeStore: Zustand store with theme/setTheme() for reactive theme switching across all componentsChatBubble: Reusable chat bubble component with Tailwind styling, used by ChatMessage and TypingIndicatorTypingIndicator: Animated typing indicator component showing. → .. → ...loop while waiting for AI response- Event Detail and Note screens exist as skeletons
editEvent.tsx: Dual-mode event editor screen- Calendar mode: Edit existing events, create new events - calls EventService API
- Chat mode: Edit AI-proposed events before confirming - updates ChatStore locally and persists to server via ChatService.updateProposalEvent()
- Route params:
mode('calendar' | 'chat'),id?,date?,eventData?(JSON),proposalContext?(JSON with messageId, proposalId, conversationId) - Supports recurring events with RRULE configuration (daily/weekly/monthly/yearly)
Building
Local APK Build with EAS
npm run build:apk -w @calchat/client
This uses the preview profile from eas.json which builds an APK with:
arm64-v8aarchitecture only (smaller APK size)- No credentials required (
withoutCredentials: true) - Internal distribution
Requirements: Android SDK and Java must be installed locally.
EAS Configuration: apps/client/eas.json contains build profiles:
development: Development client with internal distributionpreview: APK build for testing (used bybuild:apk)production: Production build with auto-increment versioning
App Identity:
- Package name:
com.gilmour109.calchat - EAS Project ID:
b722dde6-7d89-48ff-9095-e007e7c7da87
Documentation
Detailed architecture diagrams are in docs/:
api-routes.md- API endpoint overview (German)technisches_brainstorm.tex- Technical concept document (German)architecture-class-diagram.puml- Backend class diagramfrontend-class-diagram.puml- Frontend class diagramcomponent-diagram.puml- System component overview