implement frontend skeleton with tab navigation and service layer

- Add tab-based navigation (Chat, Calendar) using Expo-Router
- Create auth screens (login, register) as skeletons
- Add dynamic routes for event detail and note editing
- Implement service layer (ApiClient, AuthService, EventService, ChatService)
- Add Zustand stores (AuthStore, EventsStore) for state management
- Create EventCard and EventConfirmDialog components
- Update CLAUDE.md with new frontend architecture documentation
- Add Zustand and FlashList to technology stack
This commit is contained in:
2026-01-03 10:47:12 +01:00
parent 5cc1ce7f1c
commit 9cc6d17607
24 changed files with 537 additions and 75 deletions

View File

@@ -38,7 +38,8 @@
"react-native-safe-area-context": "5.6.0",
"react-native-screens": "~4.16.0",
"react-native-web": "~0.21.0",
"react-native-worklets": "0.5.1"
"react-native-worklets": "0.5.1",
"zustand": "^5.0.9"
},
"devDependencies": {
"@types/react": "~19.1.0",

View File

@@ -0,0 +1,29 @@
import { Ionicons } from "@expo/vector-icons";
import { Tabs } from "expo-router";
import theme from "../../Themes";
export default function TabLayout() {
return (
<Tabs screenOptions={{
headerShown: false,
tabBarActiveTintColor: theme.chatBot,
tabBarInactiveTintColor: theme.primeFg,
tabBarStyle: { backgroundColor: theme.primeBg },
}}>
<Tabs.Screen
name="chat"
options={{
title: 'Chat',
tabBarIcon: ({ color }) => <Ionicons size={28} name="chatbubble" color={color} />,
}}
/>
<Tabs.Screen
name="calendar"
options={{
title: 'Calendar',
tabBarIcon: ({ color }) => <Ionicons size={28} name="calendar" color={color} />,
}}
/>
</Tabs>
);
}

View File

@@ -1,9 +1,9 @@
import { Animated, Modal, Pressable, Text, View } from "react-native";
import { DAYS, MONTHS, Month } from "../Constants";
import Header from "../components/Header";
import { DAYS, MONTHS, Month } from "../../Constants";
import Header from "../../components/Header";
import React, { useEffect, useMemo, useRef, useState } from "react";
import currentTheme from "../Themes";
import BaseBackground from "../components/BaseBackground";
import currentTheme from "../../Themes";
import BaseBackground from "../../components/BaseBackground";
import { FlashList } from "@shopify/flash-list";
// TODO: month selection dropdown menu

View File

@@ -1,8 +1,8 @@
import { View, Text, TextInput } from "react-native";
import currentTheme from "../Themes";
import currentTheme from "../../Themes";
import { useState } from "react";
import Header from "../components/Header";
import BaseBackground from "../components/BaseBackground";
import Header from "../../components/Header";
import BaseBackground from "../../components/BaseBackground";
import { FlashList } from "@shopify/flash-list";
// TODO: better shadows for everything

View File

@@ -1,49 +0,0 @@
import React, { useState } from 'react';
import { Button, Text, View } from 'react-native';
// const styles = StyleSheet.create({
// container: {
// alignItems: 'center',
// },
// text: {
// fontSize: 40
// }
// })
type HelloWorldProps = {
text: string;
aNumber: number;
}
const HelloWorld = (props: HelloWorldProps) => {
return (
<View className='flex-1 items-center justify-center'>
<Text>{props.text} : {props.aNumber}</Text>
</View>
)
}
const Counter = () => {
const [count, setCount] = useState<number>(0);
return (
<View>
<Button
onPress={() => setCount(count + 1)}
title={`You tabbed me ${count} times`}/>
</View>
)
}
const ManyHelloes = () => {
return (
<View>
<HelloWorld text="first number" aNumber={1}/>
<HelloWorld text="second number" aNumber={2}/>
<HelloWorld text="third number" aNumber={3}/>
<Counter/>
</View>
)
}
export default ManyHelloes;

View File

@@ -2,5 +2,13 @@ import { Stack } from "expo-router";
import "../../global.css";
export default function RootLayout() {
return <Stack screenOptions={{ headerShown: false }} />;
return (
<Stack screenOptions={{ headerShown: false }}>
<Stack.Screen name="(tabs)" />
<Stack.Screen name="login" />
<Stack.Screen name="register" />
<Stack.Screen name="event/[id]" />
<Stack.Screen name="note/[id]" />
</Stack>
);
}

View File

@@ -0,0 +1,44 @@
import { View, Text, TextInput, Pressable } from 'react-native';
import { useLocalSearchParams } from 'expo-router';
import BaseBackground from '../../components/BaseBackground';
const EventDetailScreen = () => {
const { id } = useLocalSearchParams<{ id: string }>();
// TODO: Fetch event by id using EventService.getById()
// TODO: Display event details (title, description, start/end time)
// TODO: Edit mode toggle
// TODO: Save changes -> EventService.update()
// TODO: Delete button -> EventService.delete()
// TODO: Link to NoteScreen for this event
// TODO: Loading and error states
throw new Error('Not implemented');
return (
<BaseBackground>
<View className="flex-1 p-4">
<Text className="text-2xl mb-4">Event Detail</Text>
<Text className="text-gray-500 mb-4">ID: {id}</Text>
<TextInput
placeholder="Title"
className="w-full border rounded p-2 mb-4"
/>
<TextInput
placeholder="Description"
multiline
className="w-full border rounded p-2 mb-4 h-24"
/>
<View className="flex-row gap-2">
<Pressable className="bg-blue-500 p-3 rounded flex-1">
<Text className="text-white text-center">Save</Text>
</Pressable>
<Pressable className="bg-red-500 p-3 rounded flex-1">
<Text className="text-white text-center">Delete</Text>
</Pressable>
</View>
</View>
</BaseBackground>
);
};
export default EventDetailScreen;

View File

@@ -1,10 +1,5 @@
import React from "react";
import Chat from "./Chat";
import Calender from "./Calender";
import { Redirect } from "expo-router";
export default function Index() {
return (
// <Chat />
<Calender />
);
return <Redirect href="/(tabs)/chat" />;
}

View File

@@ -0,0 +1,34 @@
import { View, Text, TextInput, Pressable } from 'react-native';
import BaseBackground from '../components/BaseBackground';
const LoginScreen = () => {
// TODO: Email input field
// TODO: Password input field
// TODO: Login button -> AuthService.login()
// TODO: Link to RegisterScreen
// TODO: Error handling and display
// TODO: Navigate to Calendar on success
throw new Error('Not implemented');
return (
<BaseBackground>
<View className="flex-1 justify-center items-center p-4">
<Text className="text-2xl mb-8">Login</Text>
<TextInput
placeholder="Email"
className="w-full border rounded p-2 mb-4"
/>
<TextInput
placeholder="Password"
secureTextEntry
className="w-full border rounded p-2 mb-4"
/>
<Pressable className="bg-blue-500 p-3 rounded w-full">
<Text className="text-white text-center">Login</Text>
</Pressable>
</View>
</BaseBackground>
);
};
export default LoginScreen;

View File

@@ -0,0 +1,34 @@
import { View, Text, TextInput, Pressable } from 'react-native';
import { useLocalSearchParams } from 'expo-router';
import BaseBackground from '../../components/BaseBackground';
const NoteScreen = () => {
const { id } = useLocalSearchParams<{ id: string }>();
// TODO: Fetch event by id using EventService.getById()
// TODO: Display and edit the event's note field
// TODO: Auto-save or manual save button
// TODO: Save changes -> EventService.update({ note: ... })
// TODO: Loading and error states
throw new Error('Not implemented');
return (
<BaseBackground>
<View className="flex-1 p-4">
<Text className="text-2xl mb-4">Note</Text>
<Text className="text-gray-500 mb-4">Event ID: {id}</Text>
<TextInput
placeholder="Write your note here..."
multiline
className="w-full border rounded p-2 flex-1 mb-4"
textAlignVertical="top"
/>
<Pressable className="bg-blue-500 p-3 rounded">
<Text className="text-white text-center">Save Note</Text>
</Pressable>
</View>
</BaseBackground>
);
};
export default NoteScreen;

View File

@@ -0,0 +1,45 @@
import { View, Text, TextInput, Pressable } from 'react-native';
import BaseBackground from '../components/BaseBackground';
const RegisterScreen = () => {
// TODO: Email input field
// TODO: Display name input field
// TODO: Password input field
// TODO: Password confirmation field
// TODO: Register button -> AuthService.register()
// TODO: Link to LoginScreen
// TODO: Error handling and display
// TODO: Navigate to Calendar on success
throw new Error('Not implemented');
return (
<BaseBackground>
<View className="flex-1 justify-center items-center p-4">
<Text className="text-2xl mb-8">Register</Text>
<TextInput
placeholder="Email"
className="w-full border rounded p-2 mb-4"
/>
<TextInput
placeholder="Display Name"
className="w-full border rounded p-2 mb-4"
/>
<TextInput
placeholder="Password"
secureTextEntry
className="w-full border rounded p-2 mb-4"
/>
<TextInput
placeholder="Confirm Password"
secureTextEntry
className="w-full border rounded p-2 mb-4"
/>
<Pressable className="bg-blue-500 p-3 rounded w-full">
<Text className="text-white text-center">Register</Text>
</Pressable>
</View>
</BaseBackground>
);
};
export default RegisterScreen;

View File

@@ -0,0 +1,24 @@
import { View, Text, Pressable } from 'react-native';
import { CalendarEvent } from '@caldav/shared';
type EventCardProps = {
event: CalendarEvent;
onPress?: (event: CalendarEvent) => void;
};
const EventCard = ({ event: _event, onPress: _onPress }: EventCardProps) => {
// TODO: Display event title, time, and description preview
// TODO: Handle onPress to navigate to EventDetailScreen
// TODO: Style based on event type or time of day
throw new Error('Not implemented');
return (
<Pressable>
<View>
<Text>EventCard - Not Implemented</Text>
</View>
</Pressable>
);
};
export default EventCard;

View File

@@ -0,0 +1,36 @@
import { View, Text, Modal, Pressable } from 'react-native';
import { CreateEventDTO } from '@caldav/shared';
type EventConfirmDialogProps = {
visible: boolean;
proposedEvent: CreateEventDTO | null;
onConfirm: () => void;
onReject: () => void;
onClose: () => void;
};
const EventConfirmDialog = ({
visible: _visible,
proposedEvent: _proposedEvent,
onConfirm: _onConfirm,
onReject: _onReject,
onClose: _onClose,
}: EventConfirmDialogProps) => {
// TODO: Display proposed event details (title, time, description)
// TODO: Confirm button calls onConfirm and closes dialog
// TODO: Reject button calls onReject and closes dialog
// TODO: Close button or backdrop tap calls onClose
throw new Error('Not implemented');
return (
<Modal visible={false} transparent animationType="fade">
<View>
<Pressable>
<Text>EventConfirmDialog - Not Implemented</Text>
</Pressable>
</View>
</Modal>
);
};
export default EventConfirmDialog;

View File

@@ -0,0 +1,51 @@
const API_BASE_URL =
process.env.EXPO_PUBLIC_API_URL || "http://localhost:3000/api";
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
interface RequestOptions {
headers?: Record<string, string>;
body?: unknown;
}
async function request<T>(
_method: HttpMethod,
_endpoint: string,
_options?: RequestOptions,
): Promise<T> {
throw new Error("Not implemented");
}
export const ApiClient = {
get: async <T>(
endpoint: string,
options?: Omit<RequestOptions, "body">,
): Promise<T> => {
return request<T>("GET", endpoint, options);
},
post: async <T>(
endpoint: string,
body?: unknown,
options?: RequestOptions,
): Promise<T> => {
return request<T>("POST", endpoint, { ...options, body });
},
put: async <T>(
endpoint: string,
body?: unknown,
options?: RequestOptions,
): Promise<T> => {
return request<T>("PUT", endpoint, { ...options, body });
},
delete: async <T>(
endpoint: string,
options?: Omit<RequestOptions, "body">,
): Promise<T> => {
return request<T>("DELETE", endpoint, options);
},
};
export { API_BASE_URL };

View File

@@ -0,0 +1,19 @@
import { LoginDTO, CreateUserDTO, AuthResponse } from '@caldav/shared';
export const AuthService = {
login: async (_credentials: LoginDTO): Promise<AuthResponse> => {
throw new Error('Not implemented');
},
register: async (_data: CreateUserDTO): Promise<AuthResponse> => {
throw new Error('Not implemented');
},
logout: async (): Promise<void> => {
throw new Error('Not implemented');
},
refresh: async (): Promise<AuthResponse> => {
throw new Error('Not implemented');
},
};

View File

@@ -0,0 +1,39 @@
import {
SendMessageDTO,
ChatResponse,
ChatMessage,
ConversationSummary,
GetMessagesOptions,
CalendarEvent,
} from '@caldav/shared';
export const ChatService = {
sendMessage: async (_data: SendMessageDTO): Promise<ChatResponse> => {
throw new Error('Not implemented');
},
confirmEvent: async (
_conversationId: string,
_messageId: string
): Promise<CalendarEvent> => {
throw new Error('Not implemented');
},
rejectEvent: async (
_conversationId: string,
_messageId: string
): Promise<void> => {
throw new Error('Not implemented');
},
getConversations: async (): Promise<ConversationSummary[]> => {
throw new Error('Not implemented');
},
getConversation: async (
_id: string,
_options?: GetMessagesOptions
): Promise<ChatMessage[]> => {
throw new Error('Not implemented');
},
};

View File

@@ -0,0 +1,27 @@
import { CalendarEvent, CreateEventDTO, UpdateEventDTO } from '@caldav/shared';
export const EventService = {
getAll: async (): Promise<CalendarEvent[]> => {
throw new Error('Not implemented');
},
getById: async (_id: string): Promise<CalendarEvent> => {
throw new Error('Not implemented');
},
getByDateRange: async (_start: Date, _end: Date): Promise<CalendarEvent[]> => {
throw new Error('Not implemented');
},
create: async (_data: CreateEventDTO): Promise<CalendarEvent> => {
throw new Error('Not implemented');
},
update: async (_id: string, _data: UpdateEventDTO): Promise<CalendarEvent> => {
throw new Error('Not implemented');
},
delete: async (_id: string): Promise<void> => {
throw new Error('Not implemented');
},
};

View File

@@ -0,0 +1,4 @@
export { ApiClient, API_BASE_URL } from './ApiClient';
export { AuthService } from './AuthService';
export { EventService } from './EventService';
export { ChatService } from './ChatService';

View File

@@ -0,0 +1,26 @@
import { create } from 'zustand';
import { User } from '@caldav/shared';
interface AuthState {
user: User | null;
token: string | null;
isAuthenticated: boolean;
login: (user: User, token: string) => void;
logout: () => void;
setToken: (token: string) => void;
}
export const useAuthStore = create<AuthState>((set) => ({
user: null,
token: null,
isAuthenticated: false,
login: (_user: User, _token: string) => {
throw new Error('Not implemented');
},
logout: () => {
throw new Error('Not implemented');
},
setToken: (_token: string) => {
throw new Error('Not implemented');
},
}));

View File

@@ -0,0 +1,26 @@
import { create } from 'zustand';
import { CalendarEvent } from '@caldav/shared';
interface EventsState {
events: CalendarEvent[];
setEvents: (events: CalendarEvent[]) => void;
addEvent: (event: CalendarEvent) => void;
updateEvent: (id: string, event: Partial<CalendarEvent>) => void;
deleteEvent: (id: string) => void;
}
export const useEventsStore = create<EventsState>((set) => ({
events: [],
setEvents: (_events: CalendarEvent[]) => {
throw new Error('Not implemented');
},
addEvent: (_event: CalendarEvent) => {
throw new Error('Not implemented');
},
updateEvent: (_id: string, _event: Partial<CalendarEvent>) => {
throw new Error('Not implemented');
},
deleteEvent: (_id: string) => {
throw new Error('Not implemented');
},
}));

View File

@@ -0,0 +1,2 @@
export { useAuthStore } from './AuthStore';
export { useEventsStore } from './EventsStore';

View File

@@ -2,12 +2,14 @@
"extends": "../../tsconfig.json",
"compilerOptions": {
"target": "ES2020",
"module": "CommonJS",
"module": "ESNext",
"outDir": "dist",
"strict": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"skipLibCheck": true
"skipLibCheck": true,
"jsx": "react-native",
"moduleResolution": "bundler"
},
"include": [
"**/*.ts",