diff --git a/CLAUDE.md b/CLAUDE.md index 7d20d74..4b3af3b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -72,24 +72,26 @@ src/ │ ├── login.tsx # Login screen │ ├── register.tsx # Registration screen │ ├── (tabs)/ # Tab navigation group -│ │ ├── _layout.tsx # Tab bar configuration +│ │ ├── _layout.tsx # Tab bar configuration (themed) │ │ ├── chat.tsx # Chat screen (AI conversation) -│ │ └── calendar.tsx # Calendar overview +│ │ ├── calendar.tsx # Calendar overview +│ │ └── settings.tsx # Settings screen (theme switcher, logout) │ ├── event/ │ │ └── [id].tsx # Event detail screen (dynamic route) │ └── note/ │ └── [id].tsx # Note editor for event (dynamic route) ├── components/ -│ ├── BaseBackground.tsx # Common screen wrapper -│ ├── Header.tsx # Header component with logout button -│ ├── AuthButton.tsx # Reusable button for auth screens (with shadow) +│ ├── 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) │ ├── ChatBubble.tsx # Reusable chat bubble component (used by ChatMessage & TypingIndicator) │ ├── TypingIndicator.tsx # Animated typing indicator (. .. ...) shown while waiting for AI response │ ├── EventCardBase.tsx # Shared event card layout with icons (used by EventCard & ProposedEventCard) │ ├── EventCard.tsx # Calendar event card (uses EventCardBase + edit/delete buttons) -│ ├── EventConfirmDialog.tsx # AI-proposed event confirmation modal +│ ├── EventConfirmDialog.tsx # AI-proposed event confirmation modal (skeleton) │ └── ProposedEventCard.tsx # Chat event proposal (uses EventCardBase + confirm/reject buttons) -├── Themes.tsx # Centralized color/theme definitions +├── Themes.tsx # Theme definitions: THEMES object with defaultLight/defaultDark, Theme type ├── logging/ │ ├── index.ts # Re-exports │ └── logger.ts # react-native-logs config (apiLogger, storeLogger) @@ -104,10 +106,47 @@ src/ ├── 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() + ├── EventsStore.ts # events[], setEvents(), addEvent(), updateEvent(), deleteEvent() + └── ThemeStore.ts # theme, setTheme() - reactive theme switching with Zustand ``` -**Routing:** Tab-based navigation with Chat and Calendar as main screens. Auth screens (login, register) outside tabs. Dynamic routes for event detail and note editing. +**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. + +### Theme System + +The app supports multiple themes (light/dark) via a reactive Zustand store. + +**Theme Structure (`Themes.tsx`):** +```typescript +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; +``` + +**Usage in Components:** +```typescript +import { useThemeStore } from "../stores/ThemeStore"; + +const MyComponent = () => { + const { theme } = useThemeStore(); + return ; +}; +``` + +**Theme Switching:** +```typescript +const { setTheme } = useThemeStore(); +setTheme("defaultDark"); // or "defaultLight" +``` + +**Note:** `shadowColor` only works on iOS. Android uses `elevation` with system-defined shadow colors. ### Backend Architecture (apps/server) @@ -376,10 +415,16 @@ NODE_ENV=development # development = pretty logs, production = JSON - `ApiClient`: Automatically injects X-User-Id header for authenticated requests - Login screen: Supports email OR userName login - Register screen: Email validation, checks for existing email/userName - - `AuthButton`: Reusable button component with shadow effect - - `Header`: Contains logout button on all screens + - `AuthButton`: Reusable button component with themed shadow + - `Header`: Themed header component (logout moved to Settings) - `index.tsx`: Auth redirect - checks stored user on app start -- Tab navigation (Chat, Calendar) implemented with basic UI +- **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) @@ -404,9 +449,10 @@ NODE_ENV=development # development = pretty logs, production = JSON - `EventCardBase`: Shared base component with event layout (header, date/time/recurring icons, description) - used by both EventCard and ProposedEventCard - `EventCard`: Uses EventCardBase + edit/delete buttons for calendar display - `ProposedEventCard`: Uses EventCardBase + confirm/reject buttons for chat proposals (supports create/update/delete actions) -- `Themes.tsx`: Centralized color definitions including textPrimary, borderPrimary, eventIndicator, secondaryBg +- `Themes.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 switches +- `ThemeStore`: Zustand store with theme/setTheme() for reactive theme switching across all components - `ChatBubble`: Reusable chat bubble component with Tailwind styling, used by ChatMessage and TypingIndicator - `TypingIndicator`: Animated typing indicator component showing `. → .. → ...` loop while waiting for AI response - Event Detail and Note screens exist as skeletons diff --git a/apps/client/app.json b/apps/client/app.json index ca1371b..f3e6a14 100644 --- a/apps/client/app.json +++ b/apps/client/app.json @@ -2,7 +2,7 @@ "expo": { "jsEngine": "hermes", "name": "CalChat", - "slug": "calchat", + "slug": "caldav", "version": "1.0.0", "orientation": "portrait", "scheme": "calchat", @@ -32,6 +32,7 @@ "eas": { "projectId": "b722dde6-7d89-48ff-9095-e007e7c7da87" } - } + }, + "owner": "gilmour109" } } diff --git a/apps/client/src/Themes.tsx b/apps/client/src/Themes.tsx index 1d8c69e..78a5493 100644 --- a/apps/client/src/Themes.tsx +++ b/apps/client/src/Themes.tsx @@ -1,4 +1,4 @@ -type Theme = { +export type Theme = { chatBot: string; primeFg: string; primeBg: string; @@ -15,26 +15,46 @@ type Theme = { textMuted: string; eventIndicator: string; borderPrimary: string; + shadowColor: string; }; -const defaultLight: Theme = { - chatBot: "#DE6C20", - primeFg: "#3B3329", - primeBg: "#FFEEDE", - secondaryBg: "#FFFFFF", - messageBorderBg: "#FFFFFF", - placeholderBg: "#D9D9D9", - calenderBg: "#FBD5B2", - confirmButton: "#22c55e", - rejectButton: "#ef4444", - disabledButton: "#ccc", - buttonText: "#000000", - textPrimary: "#000000", - textSecondary: "#666", - textMuted: "#888", - eventIndicator: "#DE6C20", - borderPrimary: "#000000", -}; - -let currentTheme: Theme = defaultLight; -export default currentTheme; +export const THEMES = { + defaultLight: { + chatBot: "#DE6C20", + primeFg: "#3B3329", + primeBg: "#FFEEDE", + secondaryBg: "#FFFFFF", + messageBorderBg: "#FFFFFF", + placeholderBg: "#D9D9D9", + calenderBg: "#FBD5B2", + confirmButton: "#22c55e", + rejectButton: "#ef4444", + disabledButton: "#ccc", + buttonText: "#000000", + textPrimary: "#000000", + textSecondary: "#666", + textMuted: "#888", + eventIndicator: "#DE6C20", + borderPrimary: "#000000", + shadowColor: "#000000", + }, + defaultDark: { + chatBot: "#DE6C20", + primeFg: "#F5E6D3", + primeBg: "#1A1512", + secondaryBg: "#2A2420", + messageBorderBg: "#3A3430", + placeholderBg: "#4A4440", + calenderBg: "#3D2A1A", + confirmButton: "#22c55e", + rejectButton: "#ef4444", + disabledButton: "#555", + buttonText: "#FFFFFF", + textPrimary: "#FFFFFF", + textSecondary: "#AAA", + textMuted: "#777", + eventIndicator: "#DE6C20", + borderPrimary: "#FFFFFF", + shadowColor: "#FFFFFF", + } +} as const satisfies Record; diff --git a/apps/client/src/app/(tabs)/_layout.tsx b/apps/client/src/app/(tabs)/_layout.tsx index d74930f..a85f6a3 100644 --- a/apps/client/src/app/(tabs)/_layout.tsx +++ b/apps/client/src/app/(tabs)/_layout.tsx @@ -1,8 +1,9 @@ import { Ionicons } from "@expo/vector-icons"; import { Tabs } from "expo-router"; -import theme from "../../Themes"; +import { useThemeStore } from "../../stores/ThemeStore"; export default function TabLayout() { + const { theme } = useThemeStore(); return ( + ( + + ), + }} + /> ); } diff --git a/apps/client/src/app/(tabs)/calendar.tsx b/apps/client/src/app/(tabs)/calendar.tsx index 54cba68..9538ad4 100644 --- a/apps/client/src/app/(tabs)/calendar.tsx +++ b/apps/client/src/app/(tabs)/calendar.tsx @@ -19,7 +19,7 @@ import React, { } from "react"; import { useFocusEffect } from "expo-router"; import { Ionicons } from "@expo/vector-icons"; -import currentTheme from "../../Themes"; +import { useThemeStore } from "../../stores/ThemeStore"; import BaseBackground from "../../components/BaseBackground"; import { FlashList } from "@shopify/flash-list"; import { EventService } from "../../services"; @@ -226,6 +226,7 @@ const EventOverlay = ({ onEditEvent, onDeleteEvent, }: EventOverlayProps) => { + const { theme } = useThemeStore(); if (!date) return null; const dateString = date.toLocaleDateString("de-DE", { @@ -250,9 +251,9 @@ const EventOverlay = ({ e.stopPropagation()} > @@ -260,13 +261,13 @@ const EventOverlay = ({ - {dateString} - + {dateString} + {events.length} {events.length === 1 ? "Termin" : "Termine"} @@ -289,10 +290,10 @@ const EventOverlay = ({ className="py-3 items-center" style={{ borderTopWidth: 1, - borderTopColor: currentTheme.placeholderBg, + borderTopColor: theme.placeholderBg, }} > - + Schließen @@ -319,6 +320,7 @@ const MonthSelector = ({ currentMonthIndex, onSelectMonth, }: MonthSelectorProps) => { + const { theme } = useThemeStore(); const heightAnim = useRef(new Animated.Value(0)).current; const listRef = useRef>>(null); const INITIAL_RANGE = 12; // 12 months before and after current @@ -397,11 +399,11 @@ const MonthSelector = ({ style={{ backgroundColor: item.monthIndex % 2 === 0 - ? currentTheme.primeBg - : currentTheme.secondaryBg, + ? theme.primeBg + : theme.secondaryBg, }} > - + {item.label} @@ -423,9 +425,9 @@ const MonthSelector = ({ left: position.left, width: position.width, height: heightAnim, - backgroundColor: currentTheme.primeBg, + backgroundColor: theme.primeBg, borderWidth: 2, - borderColor: currentTheme.borderPrimary, + borderColor: theme.borderPrimary, borderRadius: 8, }} > @@ -457,6 +459,7 @@ type CalendarHeaderProps = { }; const CalendarHeader = (props: CalendarHeaderProps) => { + const { theme } = useThemeStore(); const [modalVisible, setModalVisible] = useState(false); const [dropdownPosition, setDropdownPosition] = useState({ top: 0, @@ -482,16 +485,16 @@ const CalendarHeader = (props: CalendarHeaderProps) => { ref={containerRef} className="relative flex flex-row items-center justify-around" > - + {MONTHS[props.monthIndex]} {props.currentYear} { @@ -528,42 +531,48 @@ type ChangeMonthButtonProps = { icon: "chevron-back" | "chevron-forward"; }; -const ChangeMonthButton = (props: ChangeMonthButtonProps) => ( - - { + const { theme } = useThemeStore(); + return ( + - -); + > + + + ); +}; -const WeekDaysLine = () => ( - - {/* TODO: px and gap need fine tuning to perfectly align with the grid */} - {DAYS.map((day, i) => ( - {day.substring(0, 2).toUpperCase()} - ))} - -); +const WeekDaysLine = () => { + const { theme } = useThemeStore(); + return ( + + {/* TODO: px and gap need fine tuning to perfectly align with the grid */} + {DAYS.map((day, i) => ( + {day.substring(0, 2).toUpperCase()} + ))} + + ); +}; type CalendarGridProps = { month: Month; @@ -573,6 +582,7 @@ type CalendarGridProps = { }; const CalendarGrid = (props: CalendarGridProps) => { + const { theme } = useThemeStore(); const { baseDate, dateOffset } = useMemo(() => { const monthIndex = MONTHS.indexOf(props.month); const base = new Date(props.year, monthIndex, 1); @@ -595,7 +605,7 @@ const CalendarGrid = (props: CalendarGridProps) => { {Array.from({ length: 6 }).map((_, i) => ( @@ -631,6 +641,7 @@ type SingleDayProps = { }; const SingleDay = (props: SingleDayProps) => { + const { theme } = useThemeStore(); const isSameMonth = MONTHS[props.date.getMonth()] === props.month; return ( @@ -638,11 +649,12 @@ const SingleDay = (props: SingleDayProps) => { onPress={props.onPress} className="h-full flex-1 aspect-auto rounded-xl items-center justify-between py-1" style={{ - backgroundColor: currentTheme.primeBg, + backgroundColor: theme.primeBg, }} > {props.date.getDate()} @@ -651,7 +663,7 @@ const SingleDay = (props: SingleDayProps) => { {props.hasEvents && ( )} diff --git a/apps/client/src/app/(tabs)/chat.tsx b/apps/client/src/app/(tabs)/chat.tsx index 90e7ed6..7df963d 100644 --- a/apps/client/src/app/(tabs)/chat.tsx +++ b/apps/client/src/app/(tabs)/chat.tsx @@ -7,7 +7,7 @@ import { Platform, Keyboard, } from "react-native"; -import currentTheme from "../../Themes"; +import { useThemeStore } from "../../stores/ThemeStore"; import React, { useState, useRef, useEffect } from "react"; import Header from "../../components/Header"; import BaseBackground from "../../components/BaseBackground"; @@ -234,20 +234,21 @@ const Chat = () => { }; const ChatHeader = () => { + const { theme } = useThemeStore(); return (
- CalChat + CalChat { + const { theme } = useThemeStore(); const [text, setText] = useState(""); const handleSend = () => { @@ -280,7 +282,7 @@ const ChatInput = ({ onSend }: ChatInputProps) => { { onChangeText={setText} value={text} placeholder="Nachricht..." - placeholderTextColor="#999" + placeholderTextColor={theme.textMuted} multiline /> @@ -310,6 +312,7 @@ const ChatMessage = ({ onConfirm, onReject, }: ChatMessageProps) => { + const { theme } = useThemeStore(); const [currentIndex, setCurrentIndex] = useState(0); const hasProposals = proposedChanges && proposedChanges.length > 0; @@ -333,7 +336,7 @@ const ChatMessage = ({ minWidth: hasProposals ? "75%" : undefined, }} > - {content} + {content} {hasProposals && currentProposal && onConfirm && onReject && ( @@ -350,7 +353,7 @@ const ChatMessage = ({ )} @@ -375,7 +378,7 @@ const ChatMessage = ({ )} @@ -385,7 +388,7 @@ const ChatMessage = ({ {hasMultiple && ( Event {currentIndex + 1} von {proposedChanges.length} diff --git a/apps/client/src/app/(tabs)/settings.tsx b/apps/client/src/app/(tabs)/settings.tsx new file mode 100644 index 0000000..a43cd69 --- /dev/null +++ b/apps/client/src/app/(tabs)/settings.tsx @@ -0,0 +1,60 @@ +import { Text, View } from "react-native"; +import BaseBackground from "../../components/BaseBackground"; +import BaseButton from "../../components/BaseButton"; +import { useThemeStore } from "../../stores/ThemeStore"; +import { AuthService } from "../../services/AuthService"; +import { router } from "expo-router"; +import { Ionicons } from "@expo/vector-icons"; +import Header from "../../components/Header"; +import { THEMES } from "../../Themes"; + +const handleLogout = async () => { + await AuthService.logout(); + router.replace("/login"); +}; + +const Settings = () => { + const { theme, setTheme } = useThemeStore(); + + return ( + +
+ + Settings + +
+ + + {" "} + Logout + + + + Select Theme + + + { + setTheme("defaultLight"); + }} + > + Default Light + + { + setTheme("defaultDark"); + }} + > + Default Dark + + +
+ ); +}; + +export default Settings; diff --git a/apps/client/src/app/event/[id].tsx b/apps/client/src/app/event/[id].tsx index 742087f..f903a2c 100644 --- a/apps/client/src/app/event/[id].tsx +++ b/apps/client/src/app/event/[id].tsx @@ -1,9 +1,11 @@ import { View, Text, TextInput, Pressable } from "react-native"; import { useLocalSearchParams } from "expo-router"; import BaseBackground from "../../components/BaseBackground"; +import { useThemeStore } from "../../stores/ThemeStore"; const EventDetailScreen = () => { const { id } = useLocalSearchParams<{ id: string }>(); + const { theme } = useThemeStore(); // TODO: Fetch event by id using EventService.getById() // TODO: Display event details (title, description, start/end time) @@ -17,23 +19,27 @@ const EventDetailScreen = () => { return ( - Event Detail - ID: {id} + Event Detail + ID: {id} - - Save + + Save - - Delete + + Delete diff --git a/apps/client/src/app/index.tsx b/apps/client/src/app/index.tsx index 8c98e8d..3b720e3 100644 --- a/apps/client/src/app/index.tsx +++ b/apps/client/src/app/index.tsx @@ -2,10 +2,11 @@ import { useEffect } from "react"; import { View, ActivityIndicator } from "react-native"; import { Redirect } from "expo-router"; import { useAuthStore } from "../stores"; -import currentTheme from "../Themes"; +import { useThemeStore } from "../stores/ThemeStore"; export default function Index() { const { isAuthenticated, isLoading, loadStoredUser } = useAuthStore(); + const { theme } = useThemeStore(); useEffect(() => { loadStoredUser(); @@ -18,10 +19,10 @@ export default function Index() { flex: 1, justifyContent: "center", alignItems: "center", - backgroundColor: currentTheme.primeBg, + backgroundColor: theme.primeBg, }} > - +
); } diff --git a/apps/client/src/app/login.tsx b/apps/client/src/app/login.tsx index d48b613..fd8bb18 100644 --- a/apps/client/src/app/login.tsx +++ b/apps/client/src/app/login.tsx @@ -4,9 +4,10 @@ import { Link, router } from "expo-router"; import BaseBackground from "../components/BaseBackground"; import AuthButton from "../components/AuthButton"; import { AuthService } from "../services"; -import currentTheme from "../Themes"; +import { useThemeStore } from "../stores/ThemeStore"; const LoginScreen = () => { + const { theme } = useThemeStore(); const [identifier, setIdentifier] = useState(""); const [password, setPassword] = useState(""); const [error, setError] = useState(null); @@ -36,44 +37,44 @@ const LoginScreen = () => { Anmelden {error && ( - + {error} )} @@ -85,7 +86,7 @@ const LoginScreen = () => { - + Noch kein Konto? Registrieren diff --git a/apps/client/src/app/note/[id].tsx b/apps/client/src/app/note/[id].tsx index 5635d00..7a940ff 100644 --- a/apps/client/src/app/note/[id].tsx +++ b/apps/client/src/app/note/[id].tsx @@ -1,9 +1,11 @@ import { View, Text, TextInput, Pressable } from "react-native"; import { useLocalSearchParams } from "expo-router"; import BaseBackground from "../../components/BaseBackground"; +import { useThemeStore } from "../../stores/ThemeStore"; const NoteScreen = () => { const { id } = useLocalSearchParams<{ id: string }>(); + const { theme } = useThemeStore(); // TODO: Fetch event by id using EventService.getById() // TODO: Display and edit the event's note field @@ -15,16 +17,18 @@ const NoteScreen = () => { return ( - Note - Event ID: {id} + Note + Event ID: {id} - - Save Note + + Save Note diff --git a/apps/client/src/app/register.tsx b/apps/client/src/app/register.tsx index a707f0d..d8098b7 100644 --- a/apps/client/src/app/register.tsx +++ b/apps/client/src/app/register.tsx @@ -4,11 +4,12 @@ import { Link, router } from "expo-router"; import BaseBackground from "../components/BaseBackground"; import AuthButton from "../components/AuthButton"; import { AuthService } from "../services"; -import currentTheme from "../Themes"; +import { useThemeStore } from "../stores/ThemeStore"; const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; const RegisterScreen = () => { + const { theme } = useThemeStore(); const [email, setEmail] = useState(""); const [userName, setUserName] = useState(""); const [password, setPassword] = useState(""); @@ -44,60 +45,60 @@ const RegisterScreen = () => { Registrieren {error && ( - + {error} )} @@ -109,7 +110,7 @@ const RegisterScreen = () => { - + Bereits ein Konto? Anmelden diff --git a/apps/client/src/components/AuthButton.tsx b/apps/client/src/components/AuthButton.tsx index 3bec939..bb29a26 100644 --- a/apps/client/src/components/AuthButton.tsx +++ b/apps/client/src/components/AuthButton.tsx @@ -1,5 +1,5 @@ import { Pressable, Text, ActivityIndicator } from "react-native"; -import currentTheme from "../Themes"; +import { useThemeStore } from "../stores/ThemeStore"; interface AuthButtonProps { title: string; @@ -8,6 +8,7 @@ interface AuthButtonProps { } const AuthButton = ({ title, onPress, isLoading = false }: AuthButtonProps) => { + const { theme } = useThemeStore(); return ( { className="w-full rounded-lg p-4 mb-4 border-4" style={{ backgroundColor: isLoading - ? currentTheme.disabledButton - : currentTheme.chatBot, - shadowColor: "#000", + ? theme.disabledButton + : theme.chatBot, + shadowColor: theme.shadowColor, shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.25, shadowRadius: 3.84, @@ -25,11 +26,11 @@ const AuthButton = ({ title, onPress, isLoading = false }: AuthButtonProps) => { }} > {isLoading ? ( - + ) : ( {title} diff --git a/apps/client/src/components/BaseBackground.tsx b/apps/client/src/components/BaseBackground.tsx index 0bbcf86..2c4ddb3 100644 --- a/apps/client/src/components/BaseBackground.tsx +++ b/apps/client/src/components/BaseBackground.tsx @@ -1,5 +1,5 @@ import { View } from "react-native"; -import currentTheme from "../Themes"; +import { useThemeStore } from "../stores/ThemeStore"; import { ReactNode } from "react"; type BaseBackgroundProps = { @@ -8,11 +8,12 @@ type BaseBackgroundProps = { }; const BaseBackground = (props: BaseBackgroundProps) => { + const { theme } = useThemeStore(); return ( {props.children} diff --git a/apps/client/src/components/BaseButton.tsx b/apps/client/src/components/BaseButton.tsx new file mode 100644 index 0000000..8a82151 --- /dev/null +++ b/apps/client/src/components/BaseButton.tsx @@ -0,0 +1,39 @@ +import { Pressable, Text } from "react-native"; +import { useThemeStore } from "../stores/ThemeStore"; +import { ReactNode } from "react"; + +type BaseButtonProps = { + children?: ReactNode; + onPress: () => void; + solid?: boolean; +}; + +const BaseButton = ({children, onPress, solid = false}: BaseButtonProps) => { + const { theme } = useThemeStore(); + return ( + + + {children} + + + ); +}; + +export default BaseButton; diff --git a/apps/client/src/components/ChatBubble.tsx b/apps/client/src/components/ChatBubble.tsx index 9a9ffd7..12fd9c2 100644 --- a/apps/client/src/components/ChatBubble.tsx +++ b/apps/client/src/components/ChatBubble.tsx @@ -1,5 +1,5 @@ import { View, ViewStyle } from "react-native"; -import colors from "../Themes"; +import { useThemeStore } from "../stores/ThemeStore"; type BubbleSide = "left" | "right"; @@ -11,7 +11,8 @@ type ChatBubbleProps = { }; export function ChatBubble({ side, children, className = "", style }: ChatBubbleProps) { - const borderColor = side === "left" ? colors.chatBot : colors.primeFg; + const { theme } = useThemeStore(); + const borderColor = side === "left" ? theme.chatBot : theme.primeFg; const sideClass = side === "left" ? "self-start ml-2 rounded-bl-sm" @@ -19,8 +20,8 @@ export function ChatBubble({ side, children, className = "", style }: ChatBubble return ( {children} diff --git a/apps/client/src/components/EventCard.tsx b/apps/client/src/components/EventCard.tsx index 5eb6040..8e9c83a 100644 --- a/apps/client/src/components/EventCard.tsx +++ b/apps/client/src/components/EventCard.tsx @@ -1,7 +1,7 @@ import { View, Pressable } from "react-native"; import { ExpandedEvent } from "@calchat/shared"; import { Feather } from "@expo/vector-icons"; -import currentTheme from "../Themes"; +import { useThemeStore } from "../stores/ThemeStore"; import { EventCardBase } from "./EventCardBase"; type EventCardProps = { @@ -11,6 +11,7 @@ type EventCardProps = { }; export const EventCard = ({ event, onEdit, onDelete }: EventCardProps) => { + const { theme } = useThemeStore(); return ( { className="w-10 h-10 rounded-full items-center justify-center" style={{ borderWidth: 1, - borderColor: currentTheme.borderPrimary, + borderColor: theme.borderPrimary, }} > - + diff --git a/apps/client/src/components/EventCardBase.tsx b/apps/client/src/components/EventCardBase.tsx index 02ec251..afca945 100644 --- a/apps/client/src/components/EventCardBase.tsx +++ b/apps/client/src/components/EventCardBase.tsx @@ -1,7 +1,7 @@ import { View, Text } from "react-native"; import { Feather } from "@expo/vector-icons"; import { ReactNode } from "react"; -import currentTheme from "../Themes"; +import { useThemeStore } from "../stores/ThemeStore"; type EventCardBaseProps = { className?: string; @@ -60,34 +60,35 @@ export const EventCardBase = ({ isRecurring, children, }: EventCardBaseProps) => { + const { theme } = useThemeStore(); return ( {/* Header with title */} - {title} + {title} {/* Content */} - + {/* Date */} - + {formatDate(startTime)} @@ -97,10 +98,10 @@ export const EventCardBase = ({ - + {formatTime(startTime)} - {formatTime(endTime)} ( {formatDuration(startTime, endTime)}) @@ -112,10 +113,10 @@ export const EventCardBase = ({ - + Wiederkehrend @@ -124,7 +125,7 @@ export const EventCardBase = ({ {/* Description */} {description && ( {description} diff --git a/apps/client/src/components/EventConfirmDialog.tsx b/apps/client/src/components/EventConfirmDialog.tsx index 9e54da1..c6d5f59 100644 --- a/apps/client/src/components/EventConfirmDialog.tsx +++ b/apps/client/src/components/EventConfirmDialog.tsx @@ -1,5 +1,6 @@ import { View, Text, Modal, Pressable } from "react-native"; import { CreateEventDTO } from "@calchat/shared"; +import { useThemeStore } from "../stores/ThemeStore"; type EventConfirmDialogProps = { visible: boolean; @@ -16,6 +17,8 @@ const EventConfirmDialog = ({ onReject: _onReject, onClose: _onClose, }: EventConfirmDialogProps) => { + const { theme } = useThemeStore(); + // TODO: Display proposed event details (title, time, description) // TODO: Confirm button calls onConfirm and closes dialog // TODO: Reject button calls onReject and closes dialog @@ -26,7 +29,7 @@ const EventConfirmDialog = ({ - EventConfirmDialog - Not Implemented + EventConfirmDialog - Not Implemented diff --git a/apps/client/src/components/Header.tsx b/apps/client/src/components/Header.tsx index 8372847..eea7bf7 100644 --- a/apps/client/src/components/Header.tsx +++ b/apps/client/src/components/Header.tsx @@ -1,42 +1,28 @@ -import { View, Pressable } from "react-native"; -import { Ionicons } from "@expo/vector-icons"; -import { router } from "expo-router"; -import currentTheme from "../Themes"; +import { View } from "react-native"; +import { useThemeStore } from "../stores/ThemeStore"; import { ReactNode } from "react"; -import { AuthService } from "../services"; type HeaderProps = { children?: ReactNode; className?: string; }; -const handleLogout = async () => { - await AuthService.logout(); - router.replace("/login"); -}; - const Header = (props: HeaderProps) => { + const { theme } = useThemeStore(); return ( {props.children} - - - void; onReject: () => void; -}) => ( - - - - Annehmen - - - - - Ablehnen - - - -); +}) => { + const { theme } = useThemeStore(); + return ( + + + + Annehmen + + + + + Ablehnen + + + + ); +}; export const ProposedEventCard = ({ proposedChange, diff --git a/apps/client/src/components/TypingIndicator.tsx b/apps/client/src/components/TypingIndicator.tsx index 86ed90a..1edd34f 100644 --- a/apps/client/src/components/TypingIndicator.tsx +++ b/apps/client/src/components/TypingIndicator.tsx @@ -1,12 +1,13 @@ import { useEffect, useState } from "react"; import { Text } from "react-native"; -import colors from "../Themes"; +import { useThemeStore } from "../stores/ThemeStore"; import { ChatBubble } from "./ChatBubble"; const DOTS = [".", "..", "..."]; const INTERVAL_MS = 400; export default function TypingIndicator() { + const { theme } = useThemeStore(); const [dotIndex, setDotIndex] = useState(0); useEffect(() => { @@ -21,7 +22,7 @@ export default function TypingIndicator() { {DOTS[dotIndex]} diff --git a/apps/client/src/stores/ThemeStore.ts b/apps/client/src/stores/ThemeStore.ts new file mode 100644 index 0000000..b3317be --- /dev/null +++ b/apps/client/src/stores/ThemeStore.ts @@ -0,0 +1,12 @@ +import { create } from "zustand"; +import { Theme, THEMES } from "../Themes"; + +interface ThemeState { + theme: Theme; + setTheme: (themeName: keyof typeof THEMES) => void; +} + +export const useThemeStore = create((set) => ({ + theme: THEMES.defaultLight, + setTheme: (themeName) => set({theme: THEMES[themeName]}) +}))