diff --git a/app/.gitignore b/app/.gitignore
new file mode 100644
index 0000000..f610ec0
--- /dev/null
+++ b/app/.gitignore
@@ -0,0 +1,39 @@
+# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files
+
+# dependencies
+node_modules/
+
+# Expo
+.expo/
+dist/
+web-build/
+expo-env.d.ts
+
+# Native
+.kotlin/
+*.orig.*
+*.jks
+*.p8
+*.p12
+*.key
+*.mobileprovision
+
+# Metro
+.metro-health-check*
+
+# debug
+npm-debug.*
+yarn-debug.*
+yarn-error.*
+
+# macOS
+.DS_Store
+*.pem
+
+# local env files
+.env*.local
+
+# typescript
+*.tsbuildinfo
+
+app-example
diff --git a/app/.vscode/settings.json b/app/.vscode/settings.json
new file mode 100644
index 0000000..e2798e4
--- /dev/null
+++ b/app/.vscode/settings.json
@@ -0,0 +1,7 @@
+{
+ "editor.codeActionsOnSave": {
+ "source.fixAll": "explicit",
+ "source.organizeImports": "explicit",
+ "source.sortMembers": "explicit"
+ }
+}
diff --git a/app/README.md b/app/README.md
new file mode 100644
index 0000000..e940462
--- /dev/null
+++ b/app/README.md
@@ -0,0 +1,58 @@
+# Calendi Mobile App
+
+A React Native mobile app version of the Calendi application built with Expo.
+
+## Features
+
+- Calendar view with events
+- Event management (create, view, edit, delete)
+- User profile management
+- Responsive UI for iOS and Android
+
+## Setup Instructions
+
+### Prerequisites
+
+- Node.js (16.x or newer)
+- npm or yarn
+- Expo CLI (`npm install -g expo-cli`)
+- iOS Simulator (for Mac users) or Android Emulator
+
+### Installation
+
+1. Clone this repository
+2. Navigate to the app directory
+3. Install dependencies:
+
+```bash
+npm install
+# or
+yarn install
+```
+
+### Running the App
+
+```bash
+npm start
+# or
+yarn start
+```
+
+This will start the Expo development server. You can run the app on:
+- iOS Simulator (press `i`)
+- Android Emulator (press `a`)
+- Your physical device by scanning the QR code with the Expo Go app
+
+## Project Structure
+
+- `app/` - Expo Router app directory with screens
+- `components/` - Reusable UI components
+- `constants/` - App constants and theme
+- `lib/` - Domain logic and services
+ - `api/` - API service for backend communication
+ - `models/` - Data models
+ - `utils/` - Utility functions
+
+## Backend Connection
+
+The app is configured to connect to a backend API. Update the API URL in `lib/api/apiService.js` to point to your backend server.
diff --git a/app/app.json b/app/app.json
new file mode 100644
index 0000000..428a0b3
--- /dev/null
+++ b/app/app.json
@@ -0,0 +1,42 @@
+{
+ "expo": {
+ "name": "app",
+ "slug": "app",
+ "version": "1.0.0",
+ "orientation": "portrait",
+ "icon": "./assets/images/icon.png",
+ "scheme": "app",
+ "userInterfaceStyle": "automatic",
+ "newArchEnabled": true,
+ "ios": {
+ "supportsTablet": true
+ },
+ "android": {
+ "adaptiveIcon": {
+ "foregroundImage": "./assets/images/adaptive-icon.png",
+ "backgroundColor": "#ffffff"
+ },
+ "edgeToEdgeEnabled": true
+ },
+ "web": {
+ "bundler": "metro",
+ "output": "static",
+ "favicon": "./assets/images/favicon.png"
+ },
+ "plugins": [
+ "expo-router",
+ [
+ "expo-splash-screen",
+ {
+ "image": "./assets/images/splash-icon.png",
+ "imageWidth": 200,
+ "resizeMode": "contain",
+ "backgroundColor": "#ffffff"
+ }
+ ]
+ ],
+ "experiments": {
+ "typedRoutes": true
+ }
+ }
+}
diff --git a/app/app/(tabs)/_layout.js b/app/app/(tabs)/_layout.js
new file mode 100644
index 0000000..e691654
--- /dev/null
+++ b/app/app/(tabs)/_layout.js
@@ -0,0 +1,142 @@
+import React from 'react';
+import { Tabs } from 'expo-router';
+import { View, Text, StyleSheet } from 'react-native';
+import { FontAwesome } from '@expo/vector-icons';
+import Colors from '../../constants';
+
+/**
+ * Custom styled tab bar more similar to frontend styling
+ */
+function CustomTabBar({ state, descriptors, navigation }) {
+ return (
+
+ {state.routes.map((route, index) => {
+ const { options } = descriptors[route.key];
+ const label = options.title || route.name;
+ const isFocused = state.index === index;
+
+ const onPress = () => {
+ const event = navigation.emit({
+ type: 'tabPress',
+ target: route.key,
+ canPreventDefault: true,
+ });
+
+ if (!isFocused && !event.defaultPrevented) {
+ navigation.navigate(route.name);
+ }
+ };
+
+ let iconName;
+ if (route.name === 'index') {
+ iconName = 'home';
+ } else if (route.name === 'calendar') {
+ iconName = 'calendar';
+ } else if (route.name === 'profile') {
+ iconName = 'user';
+ }
+
+ return (
+
+
+
+ {label}
+
+
+ );
+ })}
+
+ );
+}
+
+/**
+ * Bottom tab navigation similar to the frontend
+ */
+export default function TabLayout() {
+ return (
+ }
+ >
+
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ tabContainer: {
+ flexDirection: 'row',
+ height: 60,
+ backgroundColor: Colors.card,
+ borderTopWidth: 1,
+ borderTopColor: Colors.divider,
+ paddingBottom: 5,
+ },
+ tabItem: {
+ flex: 1,
+ alignItems: 'center',
+ justifyContent: 'center',
+ paddingTop: 8,
+ },
+ tabItemActive: {
+ borderTopWidth: 2,
+ borderTopColor: Colors.primary,
+ marginTop: -2,
+ },
+ tabLabel: {
+ fontSize: 12,
+ marginTop: 4,
+ fontWeight: '500',
+ },
+});
\ No newline at end of file
diff --git a/app/app/(tabs)/calendar.js b/app/app/(tabs)/calendar.js
new file mode 100644
index 0000000..931b3de
--- /dev/null
+++ b/app/app/(tabs)/calendar.js
@@ -0,0 +1,214 @@
+import React, { useEffect, useState } from 'react';
+import { View, Text, StyleSheet, FlatList, TouchableOpacity, ActivityIndicator } from 'react-native';
+import { Stack, useRouter } from 'expo-router';
+import CalendarView from '../../components/ui/Calendar/CalendarView';
+import apiService from '../../lib/api/apiService';
+import Colors from '../../constants';
+
+export default function CalendarScreen() {
+ const [events, setEvents] = useState([]);
+ const [selectedDate, setSelectedDate] = useState(new Date());
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+ const router = useRouter();
+
+ useEffect(() => {
+ loadEvents();
+ }, []);
+
+ const loadEvents = async () => {
+ try {
+ setLoading(true);
+ setError(null);
+ const eventsList = await apiService.getEvents();
+ setEvents(eventsList);
+ } catch (err) {
+ setError('Failed to load events. Please try again.');
+ console.error(err);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleSelectDate = (date) => {
+ setSelectedDate(date);
+ };
+
+ const handleEventPress = (eventId) => {
+ router.push(`/event-details/${eventId}`);
+ };
+
+ const handleAddEvent = () => {
+ router.push('/edit-event');
+ };
+
+ const filteredEvents = events.filter(event => {
+ const eventDate = new Date(event.startDate);
+ return eventDate.getDate() === selectedDate.getDate() &&
+ eventDate.getMonth() === selectedDate.getMonth() &&
+ eventDate.getFullYear() === selectedDate.getFullYear();
+ });
+
+ const renderEventItem = ({ item }) => (
+ handleEventPress(item.id)}
+ >
+
+
+ {item.isFullDay()
+ ? 'All day'
+ : new Date(item.startDate).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
+ }
+
+
+
+ {item.title}
+ {item.location && (
+ {item.location}
+ )}
+
+
+ );
+
+ return (
+
+ (
+
+ +
+
+ ),
+ }}
+ />
+
+
+
+
+
+ {selectedDate.toLocaleDateString(undefined, { weekday: 'long', month: 'long', day: 'numeric' })}
+
+
+ {loading ? (
+
+ ) : error ? (
+ {error}
+ ) : filteredEvents.length > 0 ? (
+ item.id.toString()}
+ contentContainerStyle={styles.eventsList}
+ />
+ ) : (
+
+ No events for this day
+
+ Create Event
+
+
+ )}
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: Colors.background,
+ },
+ addButton: {
+ width: 32,
+ height: 32,
+ borderRadius: 16,
+ backgroundColor: Colors.primary,
+ justifyContent: 'center',
+ alignItems: 'center',
+ marginRight: 10,
+ },
+ addButtonText: {
+ color: 'white',
+ fontSize: 24,
+ fontWeight: 'bold',
+ lineHeight: 24,
+ },
+ eventsContainer: {
+ flex: 1,
+ padding: 16,
+ },
+ dateTitle: {
+ fontSize: 18,
+ fontWeight: 'bold',
+ marginBottom: 16,
+ },
+ eventsList: {
+ paddingBottom: 20,
+ },
+ eventItem: {
+ flexDirection: 'row',
+ backgroundColor: Colors.card,
+ borderRadius: 8,
+ padding: 12,
+ marginBottom: 8,
+ shadowColor: '#000',
+ shadowOffset: { width: 0, height: 1 },
+ shadowOpacity: 0.1,
+ shadowRadius: 1,
+ elevation: 2,
+ },
+ eventTime: {
+ marginRight: 12,
+ width: 60,
+ },
+ eventTimeText: {
+ fontSize: 14,
+ color: Colors.secondary,
+ },
+ eventContent: {
+ flex: 1,
+ },
+ eventTitle: {
+ fontSize: 16,
+ fontWeight: 'bold',
+ marginBottom: 4,
+ },
+ eventLocation: {
+ fontSize: 14,
+ color: 'gray',
+ },
+ noEventsContainer: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ noEventsText: {
+ fontSize: 16,
+ color: 'gray',
+ marginBottom: 16,
+ },
+ createEventButton: {
+ backgroundColor: Colors.primary,
+ paddingVertical: 10,
+ paddingHorizontal: 20,
+ borderRadius: 8,
+ },
+ createEventText: {
+ color: 'white',
+ fontWeight: 'bold',
+ },
+ errorText: {
+ color: Colors.error,
+ textAlign: 'center',
+ marginTop: 20,
+ },
+});
\ No newline at end of file
diff --git a/app/app/(tabs)/index.js b/app/app/(tabs)/index.js
new file mode 100644
index 0000000..8f83c92
--- /dev/null
+++ b/app/app/(tabs)/index.js
@@ -0,0 +1,285 @@
+import React, { useEffect, useState } from 'react';
+import { View, Text, StyleSheet, FlatList, TouchableOpacity, ActivityIndicator, SafeAreaView } from 'react-native';
+import { Stack, useRouter } from 'expo-router';
+import { FontAwesome } from '@expo/vector-icons';
+import Colors from '../../constants';
+import apiService from '../../lib/api/apiService';
+
+export default function HomeScreen() {
+ const [events, setEvents] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+ const router = useRouter();
+
+ useEffect(() => {
+ loadEvents();
+ }, []);
+
+ const loadEvents = async () => {
+ try {
+ setLoading(true);
+ setError(null);
+ const eventsList = await apiService.getEvents();
+ setEvents(eventsList);
+ } catch (err) {
+ setError('Failed to load events. Please try again.');
+ console.error(err);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleEventPress = (eventId) => {
+ router.push(`/event-details/${eventId}`);
+ };
+
+ const handleAddEvent = () => {
+ router.push('/edit-event');
+ };
+
+ const renderEventCard = ({ item }) => {
+ const eventDate = new Date(item.startDate);
+ const now = new Date();
+ const isUpcoming = eventDate > now;
+
+ return (
+ handleEventPress(item.id)}
+ activeOpacity={0.7}
+ >
+
+
+ {eventDate.toLocaleString('default', { month: 'short' }).toUpperCase()}
+
+ {eventDate.getDate()}
+
+
+ {item.title}
+
+
+
+ {item.isFullDay()
+ ? 'All day'
+ : eventDate.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
+ }
+
+
+ {item.location && (
+
+
+
+ {item.location}
+
+
+ )}
+
+
+ );
+ };
+
+ return (
+
+ (
+
+
+
+ ),
+ }}
+ />
+
+
+ Upcoming Events
+
+
+ {loading ? (
+
+
+
+ ) : error ? (
+
+ {error}
+
+ Retry
+
+
+ ) : events.length > 0 ? (
+ item.id.toString()}
+ contentContainerStyle={styles.eventsList}
+ showsVerticalScrollIndicator={false}
+ />
+ ) : (
+
+
+ No upcoming events
+
+ Create Event
+
+
+ )}
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: Colors.background,
+ },
+ headerContainer: {
+ paddingHorizontal: 16,
+ paddingTop: 12,
+ paddingBottom: 8,
+ backgroundColor: Colors.headerBg,
+ },
+ headerTitle: {
+ fontSize: 18,
+ fontWeight: '600',
+ color: Colors.text,
+ },
+ screenTitle: {
+ fontSize: 22,
+ fontWeight: '700',
+ color: Colors.text,
+ },
+ addButton: {
+ width: 32,
+ height: 32,
+ borderRadius: 16,
+ backgroundColor: Colors.primary,
+ justifyContent: 'center',
+ alignItems: 'center',
+ marginRight: 16,
+ shadowColor: Colors.cardShadow,
+ shadowOffset: { width: 0, height: 2 },
+ shadowOpacity: 0.3,
+ shadowRadius: 2,
+ elevation: 3,
+ },
+ centeredContainer: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ padding: 20,
+ },
+ eventsList: {
+ padding: 16,
+ },
+ eventCard: {
+ flexDirection: 'row',
+ backgroundColor: Colors.card,
+ borderRadius: 10,
+ marginBottom: 16,
+ shadowColor: Colors.cardShadow,
+ shadowOffset: { width: 0, height: 3 },
+ shadowOpacity: 0.1,
+ shadowRadius: 6,
+ elevation: 3,
+ overflow: 'hidden',
+ borderWidth: 1,
+ borderColor: Colors.border,
+ },
+ dateBox: {
+ width: 70,
+ justifyContent: 'center',
+ alignItems: 'center',
+ padding: 12,
+ },
+ upcomingDateBox: {
+ backgroundColor: Colors.primary,
+ },
+ pastDateBox: {
+ backgroundColor: Colors.inactive,
+ },
+ dateMonth: {
+ color: '#fff',
+ fontWeight: '600',
+ fontSize: 13,
+ letterSpacing: 1,
+ },
+ dateDay: {
+ color: '#fff',
+ fontWeight: 'bold',
+ fontSize: 24,
+ marginTop: 2,
+ },
+ eventDetails: {
+ flex: 1,
+ padding: 16,
+ justifyContent: 'center',
+ },
+ eventTitle: {
+ fontSize: 16,
+ fontWeight: '600',
+ marginBottom: 8,
+ color: Colors.text,
+ },
+ eventMeta: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ marginBottom: 6,
+ },
+ eventIcon: {
+ marginRight: 6,
+ },
+ eventTime: {
+ fontSize: 14,
+ color: Colors.secondary,
+ },
+ eventLocation: {
+ fontSize: 14,
+ color: Colors.secondary,
+ },
+ emptyIcon: {
+ marginBottom: 16,
+ },
+ noEventsText: {
+ fontSize: 16,
+ color: Colors.placeholder,
+ marginBottom: 20,
+ },
+ createEventButton: {
+ backgroundColor: Colors.primary,
+ paddingVertical: 12,
+ paddingHorizontal: 24,
+ borderRadius: 8,
+ shadowColor: Colors.cardShadow,
+ shadowOffset: { width: 0, height: 2 },
+ shadowOpacity: 0.2,
+ shadowRadius: 3,
+ elevation: 2,
+ },
+ createEventText: {
+ color: Colors.buttonText,
+ fontWeight: '600',
+ fontSize: 15,
+ },
+ errorText: {
+ color: Colors.error,
+ textAlign: 'center',
+ marginBottom: 20,
+ },
+ retryButton: {
+ backgroundColor: Colors.primary,
+ paddingVertical: 12,
+ paddingHorizontal: 24,
+ borderRadius: 8,
+ },
+ retryButtonText: {
+ color: Colors.buttonText,
+ fontWeight: '600',
+ },
+});
\ No newline at end of file
diff --git a/app/app/(tabs)/profile.js b/app/app/(tabs)/profile.js
new file mode 100644
index 0000000..d52300a
--- /dev/null
+++ b/app/app/(tabs)/profile.js
@@ -0,0 +1,322 @@
+import React, { useEffect, useState } from 'react';
+import { View, Text, StyleSheet, TouchableOpacity, ActivityIndicator, Image, TextInput, ScrollView, Alert } from 'react-native';
+import { Stack } from 'expo-router';
+import apiService from '../../lib/api/apiService';
+import Colors from '../../constants';
+
+export default function ProfileScreen() {
+ const [profile, setProfile] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+ const [isEditing, setIsEditing] = useState(false);
+ const [formData, setFormData] = useState({
+ name: '',
+ email: '',
+ bio: '',
+ });
+
+ useEffect(() => {
+ loadProfile();
+ }, []);
+
+ const loadProfile = async () => {
+ try {
+ setLoading(true);
+ setError(null);
+ const userData = await apiService.getUserProfile();
+ setProfile(userData);
+ setFormData({
+ name: userData.name || '',
+ email: userData.email || '',
+ bio: userData.bio || '',
+ });
+ } catch (err) {
+ setError('Failed to load profile data. Please try again.');
+ console.error(err);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleInputChange = (field, value) => {
+ setFormData({
+ ...formData,
+ [field]: value
+ });
+ };
+
+ const handleSave = async () => {
+ try {
+ setLoading(true);
+ await apiService.updateUserProfile(formData);
+ setProfile({
+ ...profile,
+ ...formData
+ });
+ setIsEditing(false);
+ Alert.alert('Success', 'Profile updated successfully!');
+ } catch (err) {
+ Alert.alert('Error', 'Failed to update profile. Please try again.');
+ console.error(err);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleCancel = () => {
+ setFormData({
+ name: profile.name || '',
+ email: profile.email || '',
+ bio: profile.bio || '',
+ });
+ setIsEditing(false);
+ };
+
+ if (loading && !profile) {
+ return (
+
+
+
+ );
+ }
+
+ if (error && !profile) {
+ return (
+
+ {error}
+
+ Retry
+
+
+ );
+ }
+
+ return (
+
+ (
+ setIsEditing(true)}
+ disabled={loading}
+ >
+
+ {isEditing ? 'Save' : 'Edit'}
+
+
+ ),
+ }}
+ />
+
+ {loading && (
+
+ )}
+
+
+
+
+ {isEditing ? (
+ handleInputChange('name', text)}
+ placeholder="Name"
+ />
+ ) : (
+ {profile?.name}
+ )}
+
+
+
+
+ Email
+ {isEditing ? (
+ handleInputChange('email', text)}
+ keyboardType="email-address"
+ />
+ ) : (
+ {profile?.email}
+ )}
+
+
+
+ Bio
+ {isEditing ? (
+ handleInputChange('bio', text)}
+ multiline
+ numberOfLines={4}
+ />
+ ) : (
+ {profile?.bio || 'No bio provided'}
+ )}
+
+
+ {isEditing && (
+
+ Cancel
+
+ )}
+
+
+ Your Stats
+
+
+ {profile?.stats?.eventsCreated || 0}
+ Events Created
+
+
+ {profile?.stats?.eventsAttended || 0}
+ Events Attended
+
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: Colors.background,
+ },
+ centeredContainer: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ padding: 20,
+ },
+ loadingIndicator: {
+ marginTop: 10,
+ },
+ headerButton: {
+ color: Colors.primary,
+ fontSize: 16,
+ fontWeight: 'bold',
+ marginRight: 15,
+ },
+ profileHeader: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ padding: 20,
+ backgroundColor: Colors.card,
+ borderBottomWidth: 1,
+ borderBottomColor: Colors.border,
+ },
+ avatar: {
+ width: 80,
+ height: 80,
+ borderRadius: 40,
+ marginRight: 20,
+ },
+ profileInfo: {
+ flex: 1,
+ },
+ name: {
+ fontSize: 22,
+ fontWeight: 'bold',
+ },
+ nameInput: {
+ fontSize: 22,
+ fontWeight: 'bold',
+ padding: 5,
+ borderBottomWidth: 1,
+ borderBottomColor: Colors.primary,
+ },
+ section: {
+ padding: 20,
+ backgroundColor: Colors.card,
+ marginBottom: 10,
+ },
+ sectionTitle: {
+ fontSize: 16,
+ color: 'gray',
+ marginBottom: 8,
+ },
+ sectionContent: {
+ fontSize: 16,
+ },
+ input: {
+ padding: 10,
+ borderWidth: 1,
+ borderColor: Colors.border,
+ borderRadius: 5,
+ fontSize: 16,
+ },
+ bioInput: {
+ height: 100,
+ textAlignVertical: 'top',
+ },
+ cancelButton: {
+ backgroundColor: Colors.background,
+ padding: 15,
+ alignItems: 'center',
+ borderWidth: 1,
+ borderColor: Colors.error,
+ marginHorizontal: 20,
+ borderRadius: 5,
+ marginBottom: 20,
+ },
+ cancelButtonText: {
+ color: Colors.error,
+ fontWeight: 'bold',
+ },
+ errorText: {
+ color: Colors.error,
+ textAlign: 'center',
+ marginBottom: 20,
+ },
+ retryButton: {
+ backgroundColor: Colors.primary,
+ paddingVertical: 10,
+ paddingHorizontal: 20,
+ borderRadius: 8,
+ },
+ retryButtonText: {
+ color: 'white',
+ fontWeight: 'bold',
+ },
+ statsSection: {
+ padding: 20,
+ backgroundColor: Colors.card,
+ marginBottom: 30,
+ },
+ statsContainer: {
+ flexDirection: 'row',
+ justifyContent: 'space-around',
+ marginTop: 10,
+ },
+ statBox: {
+ alignItems: 'center',
+ padding: 15,
+ backgroundColor: Colors.background,
+ borderRadius: 10,
+ width: '45%',
+ },
+ statNumber: {
+ fontSize: 24,
+ fontWeight: 'bold',
+ color: Colors.primary,
+ },
+ statLabel: {
+ marginTop: 5,
+ color: 'gray',
+ },
+});
\ No newline at end of file
diff --git a/app/app/_layout.js b/app/app/_layout.js
new file mode 100644
index 0000000..3d2ed5e
--- /dev/null
+++ b/app/app/_layout.js
@@ -0,0 +1,17 @@
+import { Slot, Stack } from 'expo-router';
+import { useColorScheme } from 'react-native';
+import { StatusBar } from 'expo-status-bar';
+
+export default function RootLayout() {
+ const colorScheme = useColorScheme();
+
+ return (
+ <>
+
+
+
+
+
+ >
+ );
+}
\ No newline at end of file
diff --git a/app/app/event-details/[id].js b/app/app/event-details/[id].js
new file mode 100644
index 0000000..7d9de55
--- /dev/null
+++ b/app/app/event-details/[id].js
@@ -0,0 +1,337 @@
+import React, { useEffect, useState } from 'react';
+import { View, Text, StyleSheet, ActivityIndicator, ScrollView, TouchableOpacity, Alert, SafeAreaView } from 'react-native';
+import { Stack, useLocalSearchParams, useRouter } from 'expo-router';
+import { FontAwesome } from '@expo/vector-icons';
+import apiService from '../../lib/api/apiService';
+import Colors from '../../constants';
+
+export default function EventDetailsScreen() {
+ const { id } = useLocalSearchParams();
+ const router = useRouter();
+ const [event, setEvent] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+
+ useEffect(() => {
+ loadEvent();
+ }, [id]);
+
+ const loadEvent = async () => {
+ try {
+ setLoading(true);
+ setError(null);
+ const eventData = await apiService.getEvent(id);
+ setEvent(eventData);
+ } catch (err) {
+ setError('Failed to load event details. Please try again.');
+ console.error(err);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleEditEvent = () => {
+ router.push(`/edit-event/${id}`);
+ };
+
+ const handleDeleteEvent = async () => {
+ Alert.alert(
+ 'Delete Event',
+ 'Are you sure you want to delete this event? This action cannot be undone.',
+ [
+ {
+ text: 'Cancel',
+ style: 'cancel',
+ },
+ {
+ text: 'Delete',
+ style: 'destructive',
+ onPress: async () => {
+ try {
+ setLoading(true);
+ await apiService.deleteEvent(id);
+ Alert.alert('Success', 'Event deleted successfully');
+ router.back();
+ } catch (err) {
+ Alert.alert('Error', 'Failed to delete event. Please try again.');
+ console.error(err);
+ setLoading(false);
+ }
+ },
+ },
+ ]
+ );
+ };
+
+ if (loading && !event) {
+ return (
+
+
+
+ );
+ }
+
+ if (error && !event) {
+ return (
+
+ {error}
+
+ Retry
+
+
+ );
+ }
+
+ // Format the date display
+ const formattedDate = event?.getFormattedDate();
+
+ // Determine if the event is in the future
+ const isUpcoming = event?.startDate > new Date();
+
+ return (
+
+ (
+ router.back()}
+ style={styles.backButton}
+ hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
+ >
+
+
+ ),
+ headerRight: () => (
+
+
+
+ ),
+ }}
+ />
+
+ {loading && (
+
+ )}
+
+
+
+
+ {isUpcoming ? 'Upcoming' : 'Past Event'}
+
+
+
+
+ {event?.title}
+
+
+
+
+
+ {formattedDate}
+
+
+
+ {event?.location && (
+
+ Location
+
+
+ {event.location}
+
+
+ )}
+
+ {event?.description && (
+
+ Description
+ {event.description}
+
+ )}
+
+
+ Organizer
+
+
+ {event?.createdBy || 'Unknown'}
+
+
+
+
+
+ Delete Event
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: Colors.background,
+ },
+ scrollContent: {
+ padding: 16,
+ },
+ centeredContainer: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ padding: 20,
+ },
+ loadingIndicator: {
+ marginTop: 10,
+ },
+ headerTitle: {
+ fontSize: 18,
+ fontWeight: '600',
+ color: Colors.text,
+ },
+ backButton: {
+ marginLeft: 16,
+ },
+ editButton: {
+ marginRight: 16,
+ },
+ header: {
+ backgroundColor: Colors.card,
+ borderRadius: 10,
+ padding: 16,
+ marginBottom: 16,
+ shadowColor: Colors.cardShadow,
+ shadowOffset: { width: 0, height: 2 },
+ shadowOpacity: 0.1,
+ shadowRadius: 4,
+ elevation: 2,
+ borderWidth: 1,
+ borderColor: Colors.border,
+ },
+ statusBadge: {
+ alignSelf: 'flex-start',
+ paddingVertical: 6,
+ paddingHorizontal: 12,
+ borderRadius: 16,
+ marginBottom: 16,
+ },
+ upcomingBadge: {
+ backgroundColor: Colors.success,
+ },
+ pastBadge: {
+ backgroundColor: Colors.inactive,
+ },
+ statusText: {
+ color: '#fff',
+ fontWeight: '600',
+ fontSize: 12,
+ },
+ title: {
+ fontSize: 24,
+ fontWeight: 'bold',
+ color: Colors.text,
+ marginBottom: 8,
+ },
+ metaInfoSection: {
+ backgroundColor: Colors.card,
+ borderRadius: 10,
+ padding: 16,
+ marginBottom: 16,
+ shadowColor: Colors.cardShadow,
+ shadowOffset: { width: 0, height: 2 },
+ shadowOpacity: 0.1,
+ shadowRadius: 4,
+ elevation: 2,
+ borderWidth: 1,
+ borderColor: Colors.border,
+ },
+ metaItem: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ marginVertical: 4,
+ },
+ metaIcon: {
+ marginRight: 12,
+ width: 20,
+ textAlign: 'center',
+ },
+ date: {
+ fontSize: 16,
+ color: Colors.secondary,
+ },
+ section: {
+ backgroundColor: Colors.card,
+ borderRadius: 10,
+ padding: 16,
+ marginBottom: 16,
+ shadowColor: Colors.cardShadow,
+ shadowOffset: { width: 0, height: 2 },
+ shadowOpacity: 0.1,
+ shadowRadius: 4,
+ elevation: 2,
+ borderWidth: 1,
+ borderColor: Colors.border,
+ },
+ sectionTitle: {
+ fontSize: 16,
+ color: Colors.text,
+ fontWeight: '600',
+ marginBottom: 12,
+ borderBottomWidth: 1,
+ borderBottomColor: Colors.divider,
+ paddingBottom: 8,
+ },
+ sectionContent: {
+ fontSize: 16,
+ color: Colors.text,
+ lineHeight: 22,
+ },
+ deleteButton: {
+ flexDirection: 'row',
+ backgroundColor: Colors.background,
+ padding: 16,
+ alignItems: 'center',
+ justifyContent: 'center',
+ borderWidth: 1,
+ borderColor: Colors.error,
+ borderRadius: 8,
+ marginTop: 8,
+ marginBottom: 24,
+ },
+ deleteIcon: {
+ marginRight: 10,
+ },
+ deleteButtonText: {
+ color: Colors.error,
+ fontWeight: '600',
+ fontSize: 16,
+ },
+ errorText: {
+ color: Colors.error,
+ textAlign: 'center',
+ marginBottom: 20,
+ },
+ retryButton: {
+ backgroundColor: Colors.primary,
+ paddingVertical: 12,
+ paddingHorizontal: 24,
+ borderRadius: 8,
+ },
+ retryButtonText: {
+ color: '#fff',
+ fontWeight: '600',
+ },
+});
\ No newline at end of file
diff --git a/app/app/index.js b/app/app/index.js
new file mode 100644
index 0000000..3f36bf8
--- /dev/null
+++ b/app/app/index.js
@@ -0,0 +1,5 @@
+import { Redirect } from 'expo-router';
+
+export default function Index() {
+ return ;
+}
\ No newline at end of file
diff --git a/app/assets/fonts/SpaceMono-Regular.ttf b/app/assets/fonts/SpaceMono-Regular.ttf
new file mode 100755
index 0000000..28d7ff7
Binary files /dev/null and b/app/assets/fonts/SpaceMono-Regular.ttf differ
diff --git a/app/assets/images/adaptive-icon.png b/app/assets/images/adaptive-icon.png
new file mode 100644
index 0000000..03d6f6b
Binary files /dev/null and b/app/assets/images/adaptive-icon.png differ
diff --git a/app/assets/images/favicon.png b/app/assets/images/favicon.png
new file mode 100644
index 0000000..e75f697
Binary files /dev/null and b/app/assets/images/favicon.png differ
diff --git a/app/assets/images/icon.png b/app/assets/images/icon.png
new file mode 100644
index 0000000..a0b1526
Binary files /dev/null and b/app/assets/images/icon.png differ
diff --git a/app/assets/images/partial-react-logo.png b/app/assets/images/partial-react-logo.png
new file mode 100644
index 0000000..66fd957
Binary files /dev/null and b/app/assets/images/partial-react-logo.png differ
diff --git a/app/assets/images/react-logo.png b/app/assets/images/react-logo.png
new file mode 100644
index 0000000..9d72a9f
Binary files /dev/null and b/app/assets/images/react-logo.png differ
diff --git a/app/assets/images/react-logo@2x.png b/app/assets/images/react-logo@2x.png
new file mode 100644
index 0000000..2229b13
Binary files /dev/null and b/app/assets/images/react-logo@2x.png differ
diff --git a/app/assets/images/react-logo@3x.png b/app/assets/images/react-logo@3x.png
new file mode 100644
index 0000000..a99b203
Binary files /dev/null and b/app/assets/images/react-logo@3x.png differ
diff --git a/app/assets/images/splash-icon.png b/app/assets/images/splash-icon.png
new file mode 100644
index 0000000..03d6f6b
Binary files /dev/null and b/app/assets/images/splash-icon.png differ
diff --git a/app/components/Collapsible.tsx b/app/components/Collapsible.tsx
new file mode 100644
index 0000000..55bff2f
--- /dev/null
+++ b/app/components/Collapsible.tsx
@@ -0,0 +1,45 @@
+import { PropsWithChildren, useState } from 'react';
+import { StyleSheet, TouchableOpacity } from 'react-native';
+
+import { ThemedText } from '@/components/ThemedText';
+import { ThemedView } from '@/components/ThemedView';
+import { IconSymbol } from '@/components/ui/IconSymbol';
+import { Colors } from '@/constants/Colors';
+import { useColorScheme } from '@/hooks/useColorScheme';
+
+export function Collapsible({ children, title }: PropsWithChildren & { title: string }) {
+ const [isOpen, setIsOpen] = useState(false);
+ const theme = useColorScheme() ?? 'light';
+
+ return (
+
+ setIsOpen((value) => !value)}
+ activeOpacity={0.8}>
+
+
+ {title}
+
+ {isOpen && {children}}
+
+ );
+}
+
+const styles = StyleSheet.create({
+ heading: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: 6,
+ },
+ content: {
+ marginTop: 6,
+ marginLeft: 24,
+ },
+});
diff --git a/app/components/ExternalLink.tsx b/app/components/ExternalLink.tsx
new file mode 100644
index 0000000..dfbd23e
--- /dev/null
+++ b/app/components/ExternalLink.tsx
@@ -0,0 +1,24 @@
+import { Href, Link } from 'expo-router';
+import { openBrowserAsync } from 'expo-web-browser';
+import { type ComponentProps } from 'react';
+import { Platform } from 'react-native';
+
+type Props = Omit, 'href'> & { href: Href & string };
+
+export function ExternalLink({ href, ...rest }: Props) {
+ return (
+ {
+ if (Platform.OS !== 'web') {
+ // Prevent the default behavior of linking to the default browser on native.
+ event.preventDefault();
+ // Open the link in an in-app browser.
+ await openBrowserAsync(href);
+ }
+ }}
+ />
+ );
+}
diff --git a/app/components/HapticTab.tsx b/app/components/HapticTab.tsx
new file mode 100644
index 0000000..7f3981c
--- /dev/null
+++ b/app/components/HapticTab.tsx
@@ -0,0 +1,18 @@
+import { BottomTabBarButtonProps } from '@react-navigation/bottom-tabs';
+import { PlatformPressable } from '@react-navigation/elements';
+import * as Haptics from 'expo-haptics';
+
+export function HapticTab(props: BottomTabBarButtonProps) {
+ return (
+ {
+ if (process.env.EXPO_OS === 'ios') {
+ // Add a soft haptic feedback when pressing down on the tabs.
+ Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
+ }
+ props.onPressIn?.(ev);
+ }}
+ />
+ );
+}
diff --git a/app/components/HelloWave.tsx b/app/components/HelloWave.tsx
new file mode 100644
index 0000000..eb6ea61
--- /dev/null
+++ b/app/components/HelloWave.tsx
@@ -0,0 +1,40 @@
+import { useEffect } from 'react';
+import { StyleSheet } from 'react-native';
+import Animated, {
+ useAnimatedStyle,
+ useSharedValue,
+ withRepeat,
+ withSequence,
+ withTiming,
+} from 'react-native-reanimated';
+
+import { ThemedText } from '@/components/ThemedText';
+
+export function HelloWave() {
+ const rotationAnimation = useSharedValue(0);
+
+ useEffect(() => {
+ rotationAnimation.value = withRepeat(
+ withSequence(withTiming(25, { duration: 150 }), withTiming(0, { duration: 150 })),
+ 4 // Run the animation 4 times
+ );
+ }, [rotationAnimation]);
+
+ const animatedStyle = useAnimatedStyle(() => ({
+ transform: [{ rotate: `${rotationAnimation.value}deg` }],
+ }));
+
+ return (
+
+ 👋
+
+ );
+}
+
+const styles = StyleSheet.create({
+ text: {
+ fontSize: 28,
+ lineHeight: 32,
+ marginTop: -6,
+ },
+});
diff --git a/app/components/ParallaxScrollView.tsx b/app/components/ParallaxScrollView.tsx
new file mode 100644
index 0000000..5df1d75
--- /dev/null
+++ b/app/components/ParallaxScrollView.tsx
@@ -0,0 +1,82 @@
+import type { PropsWithChildren, ReactElement } from 'react';
+import { StyleSheet } from 'react-native';
+import Animated, {
+ interpolate,
+ useAnimatedRef,
+ useAnimatedStyle,
+ useScrollViewOffset,
+} from 'react-native-reanimated';
+
+import { ThemedView } from '@/components/ThemedView';
+import { useBottomTabOverflow } from '@/components/ui/TabBarBackground';
+import { useColorScheme } from '@/hooks/useColorScheme';
+
+const HEADER_HEIGHT = 250;
+
+type Props = PropsWithChildren<{
+ headerImage: ReactElement;
+ headerBackgroundColor: { dark: string; light: string };
+}>;
+
+export default function ParallaxScrollView({
+ children,
+ headerImage,
+ headerBackgroundColor,
+}: Props) {
+ const colorScheme = useColorScheme() ?? 'light';
+ const scrollRef = useAnimatedRef();
+ const scrollOffset = useScrollViewOffset(scrollRef);
+ const bottom = useBottomTabOverflow();
+ const headerAnimatedStyle = useAnimatedStyle(() => {
+ return {
+ transform: [
+ {
+ translateY: interpolate(
+ scrollOffset.value,
+ [-HEADER_HEIGHT, 0, HEADER_HEIGHT],
+ [-HEADER_HEIGHT / 2, 0, HEADER_HEIGHT * 0.75]
+ ),
+ },
+ {
+ scale: interpolate(scrollOffset.value, [-HEADER_HEIGHT, 0, HEADER_HEIGHT], [2, 1, 1]),
+ },
+ ],
+ };
+ });
+
+ return (
+
+
+
+ {headerImage}
+
+ {children}
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ },
+ header: {
+ height: HEADER_HEIGHT,
+ overflow: 'hidden',
+ },
+ content: {
+ flex: 1,
+ padding: 32,
+ gap: 16,
+ overflow: 'hidden',
+ },
+});
diff --git a/app/components/ThemedText.tsx b/app/components/ThemedText.tsx
new file mode 100644
index 0000000..9d214a2
--- /dev/null
+++ b/app/components/ThemedText.tsx
@@ -0,0 +1,60 @@
+import { StyleSheet, Text, type TextProps } from 'react-native';
+
+import { useThemeColor } from '@/hooks/useThemeColor';
+
+export type ThemedTextProps = TextProps & {
+ lightColor?: string;
+ darkColor?: string;
+ type?: 'default' | 'title' | 'defaultSemiBold' | 'subtitle' | 'link';
+};
+
+export function ThemedText({
+ style,
+ lightColor,
+ darkColor,
+ type = 'default',
+ ...rest
+}: ThemedTextProps) {
+ const color = useThemeColor({ light: lightColor, dark: darkColor }, 'text');
+
+ return (
+
+ );
+}
+
+const styles = StyleSheet.create({
+ default: {
+ fontSize: 16,
+ lineHeight: 24,
+ },
+ defaultSemiBold: {
+ fontSize: 16,
+ lineHeight: 24,
+ fontWeight: '600',
+ },
+ title: {
+ fontSize: 32,
+ fontWeight: 'bold',
+ lineHeight: 32,
+ },
+ subtitle: {
+ fontSize: 20,
+ fontWeight: 'bold',
+ },
+ link: {
+ lineHeight: 30,
+ fontSize: 16,
+ color: '#0a7ea4',
+ },
+});
diff --git a/app/components/ThemedView.tsx b/app/components/ThemedView.tsx
new file mode 100644
index 0000000..4d2cb09
--- /dev/null
+++ b/app/components/ThemedView.tsx
@@ -0,0 +1,14 @@
+import { View, type ViewProps } from 'react-native';
+
+import { useThemeColor } from '@/hooks/useThemeColor';
+
+export type ThemedViewProps = ViewProps & {
+ lightColor?: string;
+ darkColor?: string;
+};
+
+export function ThemedView({ style, lightColor, darkColor, ...otherProps }: ThemedViewProps) {
+ const backgroundColor = useThemeColor({ light: lightColor, dark: darkColor }, 'background');
+
+ return ;
+}
diff --git a/app/components/ui/Calendar/CalendarView.js b/app/components/ui/Calendar/CalendarView.js
new file mode 100644
index 0000000..f4eb082
--- /dev/null
+++ b/app/components/ui/Calendar/CalendarView.js
@@ -0,0 +1,216 @@
+import React, { useState } from 'react';
+import { View, Text, StyleSheet, TouchableOpacity, ScrollView } from 'react-native';
+import { FontAwesome } from '@expo/vector-icons';
+import Colors from '../../../constants';
+
+const days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
+const months = [
+ 'January', 'February', 'March', 'April', 'May', 'June',
+ 'July', 'August', 'September', 'October', 'November', 'December'
+];
+
+const CalendarView = ({ onSelectDate, events = [] }) => {
+ const [currentMonth, setCurrentMonth] = useState(new Date());
+ const today = new Date();
+
+ const getDaysInMonth = (date) => {
+ const year = date.getFullYear();
+ const month = date.getMonth();
+ const daysInMonth = new Date(year, month + 1, 0).getDate();
+ const firstDay = new Date(year, month, 1).getDay();
+
+ const days = [];
+
+ // Add empty days for the start of the month
+ for (let i = 0; i < firstDay; i++) {
+ days.push({ day: '', date: null });
+ }
+
+ // Add days of the month
+ for (let i = 1; i <= daysInMonth; i++) {
+ const date = new Date(year, month, i);
+ const hasEvent = events.some(event => {
+ const eventDate = new Date(event.startDate);
+ return eventDate.getDate() === i &&
+ eventDate.getMonth() === month &&
+ eventDate.getFullYear() === year;
+ });
+
+ const isToday = today.getDate() === i &&
+ today.getMonth() === month &&
+ today.getFullYear() === year;
+
+ days.push({
+ day: i,
+ date,
+ hasEvent,
+ isToday
+ });
+ }
+
+ return days;
+ };
+
+ const handlePrevMonth = () => {
+ const prevMonth = new Date(currentMonth);
+ prevMonth.setMonth(prevMonth.getMonth() - 1);
+ setCurrentMonth(prevMonth);
+ };
+
+ const handleNextMonth = () => {
+ const nextMonth = new Date(currentMonth);
+ nextMonth.setMonth(nextMonth.getMonth() + 1);
+ setCurrentMonth(nextMonth);
+ };
+
+ const calendarDays = getDaysInMonth(currentMonth);
+
+ return (
+
+
+
+
+
+
+ {months[currentMonth.getMonth()]} {currentMonth.getFullYear()}
+
+
+
+
+
+
+
+ {days.map(day => (
+ {day}
+ ))}
+
+
+
+ {calendarDays.map((item, index) => (
+ item.date && onSelectDate(item.date)}
+ disabled={!item.date}
+ activeOpacity={item.date ? 0.7 : 1}
+ >
+
+ {item.day}
+
+ {item.hasEvent && }
+
+ ))}
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ backgroundColor: Colors.card,
+ borderRadius: 10,
+ padding: 14,
+ margin: 16,
+ shadowColor: Colors.cardShadow,
+ shadowOffset: { width: 0, height: 3 },
+ shadowOpacity: 0.1,
+ shadowRadius: 6,
+ elevation: 3,
+ borderWidth: 1,
+ borderColor: Colors.border,
+ },
+ header: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ marginBottom: 16,
+ paddingHorizontal: 8,
+ },
+ navButton: {
+ width: 28,
+ height: 28,
+ borderRadius: 14,
+ justifyContent: 'center',
+ alignItems: 'center',
+ backgroundColor: Colors.background,
+ borderWidth: 1,
+ borderColor: Colors.border,
+ },
+ monthYear: {
+ fontSize: 16,
+ fontWeight: '600',
+ color: Colors.text,
+ },
+ daysHeader: {
+ flexDirection: 'row',
+ justifyContent: 'space-around',
+ marginBottom: 10,
+ borderBottomWidth: 1,
+ borderBottomColor: Colors.divider,
+ paddingBottom: 8,
+ },
+ dayHeader: {
+ width: 36,
+ textAlign: 'center',
+ fontWeight: '600',
+ fontSize: 12,
+ color: Colors.secondary,
+ },
+ calendarGrid: {
+ flexDirection: 'row',
+ flexWrap: 'wrap',
+ },
+ day: {
+ width: '14.28%',
+ height: 44,
+ justifyContent: 'center',
+ alignItems: 'center',
+ marginVertical: 2,
+ position: 'relative',
+ },
+ emptyDay: {
+ backgroundColor: 'transparent',
+ },
+ todayDay: {
+ backgroundColor: Colors.primary,
+ borderRadius: 22,
+ },
+ todayText: {
+ color: '#fff',
+ fontWeight: '600',
+ },
+ eventDay: {
+ position: 'relative',
+ },
+ dayText: {
+ textAlign: 'center',
+ fontSize: 14,
+ color: Colors.text,
+ },
+ eventDot: {
+ position: 'absolute',
+ bottom: 6,
+ width: 6,
+ height: 6,
+ borderRadius: 3,
+ backgroundColor: Colors.accent,
+ },
+});
+
+export default CalendarView;
\ No newline at end of file
diff --git a/app/components/ui/IconSymbol.ios.tsx b/app/components/ui/IconSymbol.ios.tsx
new file mode 100644
index 0000000..9177f4d
--- /dev/null
+++ b/app/components/ui/IconSymbol.ios.tsx
@@ -0,0 +1,32 @@
+import { SymbolView, SymbolViewProps, SymbolWeight } from 'expo-symbols';
+import { StyleProp, ViewStyle } from 'react-native';
+
+export function IconSymbol({
+ name,
+ size = 24,
+ color,
+ style,
+ weight = 'regular',
+}: {
+ name: SymbolViewProps['name'];
+ size?: number;
+ color: string;
+ style?: StyleProp;
+ weight?: SymbolWeight;
+}) {
+ return (
+
+ );
+}
diff --git a/app/components/ui/IconSymbol.tsx b/app/components/ui/IconSymbol.tsx
new file mode 100644
index 0000000..b7ece6b
--- /dev/null
+++ b/app/components/ui/IconSymbol.tsx
@@ -0,0 +1,41 @@
+// Fallback for using MaterialIcons on Android and web.
+
+import MaterialIcons from '@expo/vector-icons/MaterialIcons';
+import { SymbolWeight, SymbolViewProps } from 'expo-symbols';
+import { ComponentProps } from 'react';
+import { OpaqueColorValue, type StyleProp, type TextStyle } from 'react-native';
+
+type IconMapping = Record['name']>;
+type IconSymbolName = keyof typeof MAPPING;
+
+/**
+ * Add your SF Symbols to Material Icons mappings here.
+ * - see Material Icons in the [Icons Directory](https://icons.expo.fyi).
+ * - see SF Symbols in the [SF Symbols](https://developer.apple.com/sf-symbols/) app.
+ */
+const MAPPING = {
+ 'house.fill': 'home',
+ 'paperplane.fill': 'send',
+ 'chevron.left.forwardslash.chevron.right': 'code',
+ 'chevron.right': 'chevron-right',
+} as IconMapping;
+
+/**
+ * An icon component that uses native SF Symbols on iOS, and Material Icons on Android and web.
+ * This ensures a consistent look across platforms, and optimal resource usage.
+ * Icon `name`s are based on SF Symbols and require manual mapping to Material Icons.
+ */
+export function IconSymbol({
+ name,
+ size = 24,
+ color,
+ style,
+}: {
+ name: IconSymbolName;
+ size?: number;
+ color: string | OpaqueColorValue;
+ style?: StyleProp;
+ weight?: SymbolWeight;
+}) {
+ return ;
+}
diff --git a/app/components/ui/TabBarBackground.ios.tsx b/app/components/ui/TabBarBackground.ios.tsx
new file mode 100644
index 0000000..6668e78
--- /dev/null
+++ b/app/components/ui/TabBarBackground.ios.tsx
@@ -0,0 +1,22 @@
+import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs';
+import { BlurView } from 'expo-blur';
+import { StyleSheet } from 'react-native';
+import { useSafeAreaInsets } from 'react-native-safe-area-context';
+
+export default function BlurTabBarBackground() {
+ return (
+
+ );
+}
+
+export function useBottomTabOverflow() {
+ const tabHeight = useBottomTabBarHeight();
+ const { bottom } = useSafeAreaInsets();
+ return tabHeight - bottom;
+}
diff --git a/app/components/ui/TabBarBackground.tsx b/app/components/ui/TabBarBackground.tsx
new file mode 100644
index 0000000..70d1c3c
--- /dev/null
+++ b/app/components/ui/TabBarBackground.tsx
@@ -0,0 +1,6 @@
+// This is a shim for web and Android where the tab bar is generally opaque.
+export default undefined;
+
+export function useBottomTabOverflow() {
+ return 0;
+}
diff --git a/app/constants/Colors.js b/app/constants/Colors.js
new file mode 100644
index 0000000..73cc33e
--- /dev/null
+++ b/app/constants/Colors.js
@@ -0,0 +1,15 @@
+const Colors = {
+ primary: '#007AFF',
+ secondary: '#5856D6',
+ background: '#F2F2F7',
+ card: '#FFFFFF',
+ text: '#000000',
+ border: '#C7C7CC',
+ notification: '#FF3B30',
+ success: '#34C759',
+ warning: '#FF9500',
+ error: '#FF3B30',
+ inactive: '#C7C7CC',
+};
+
+export default Colors;
\ No newline at end of file
diff --git a/app/constants/Colors.ts b/app/constants/Colors.ts
new file mode 100644
index 0000000..14e6784
--- /dev/null
+++ b/app/constants/Colors.ts
@@ -0,0 +1,26 @@
+/**
+ * Below are the colors that are used in the app. The colors are defined in the light and dark mode.
+ * There are many other ways to style your app. For example, [Nativewind](https://www.nativewind.dev/), [Tamagui](https://tamagui.dev/), [unistyles](https://reactnativeunistyles.vercel.app), etc.
+ */
+
+const tintColorLight = '#0a7ea4';
+const tintColorDark = '#fff';
+
+export const Colors = {
+ light: {
+ text: '#11181C',
+ background: '#fff',
+ tint: tintColorLight,
+ icon: '#687076',
+ tabIconDefault: '#687076',
+ tabIconSelected: tintColorLight,
+ },
+ dark: {
+ text: '#ECEDEE',
+ background: '#151718',
+ tint: tintColorDark,
+ icon: '#9BA1A6',
+ tabIconDefault: '#9BA1A6',
+ tabIconSelected: tintColorDark,
+ },
+};
diff --git a/app/constants/index.js b/app/constants/index.js
new file mode 100644
index 0000000..8a58a0e
--- /dev/null
+++ b/app/constants/index.js
@@ -0,0 +1,27 @@
+// Import the themed Colors from the Expo generated file
+import { Colors as ThemedColors } from './Colors.ts';
+
+// Define our app's default colors to match the frontend
+const Colors = {
+ primary: '#3854a6', // Deeper blue like the frontend
+ secondary: '#667eea',
+ background: '#f8fafc',
+ card: '#ffffff',
+ text: '#2d3748',
+ border: '#e2e8f0',
+ notification: '#ef4444',
+ success: '#10b981',
+ warning: '#f59e0b',
+ error: '#ef4444',
+ inactive: '#94a3b8',
+ cardShadow: 'rgba(0, 0, 0, 0.1)',
+ divider: '#e2e8f0',
+ headerBg: '#ffffff',
+ buttonText: '#ffffff',
+ accent: '#7f9cf5',
+ placeholder: '#a0aec0',
+};
+
+// Export both color sets
+export { ThemedColors };
+export default Colors;
\ No newline at end of file
diff --git a/app/eslint.config.js b/app/eslint.config.js
new file mode 100644
index 0000000..5025da6
--- /dev/null
+++ b/app/eslint.config.js
@@ -0,0 +1,10 @@
+// https://docs.expo.dev/guides/using-eslint/
+const { defineConfig } = require('eslint/config');
+const expoConfig = require('eslint-config-expo/flat');
+
+module.exports = defineConfig([
+ expoConfig,
+ {
+ ignores: ['dist/*'],
+ },
+]);
diff --git a/app/hooks/useColorScheme.ts b/app/hooks/useColorScheme.ts
new file mode 100644
index 0000000..17e3c63
--- /dev/null
+++ b/app/hooks/useColorScheme.ts
@@ -0,0 +1 @@
+export { useColorScheme } from 'react-native';
diff --git a/app/hooks/useColorScheme.web.ts b/app/hooks/useColorScheme.web.ts
new file mode 100644
index 0000000..7eb1c1b
--- /dev/null
+++ b/app/hooks/useColorScheme.web.ts
@@ -0,0 +1,21 @@
+import { useEffect, useState } from 'react';
+import { useColorScheme as useRNColorScheme } from 'react-native';
+
+/**
+ * To support static rendering, this value needs to be re-calculated on the client side for web
+ */
+export function useColorScheme() {
+ const [hasHydrated, setHasHydrated] = useState(false);
+
+ useEffect(() => {
+ setHasHydrated(true);
+ }, []);
+
+ const colorScheme = useRNColorScheme();
+
+ if (hasHydrated) {
+ return colorScheme;
+ }
+
+ return 'light';
+}
diff --git a/app/hooks/useThemeColor.ts b/app/hooks/useThemeColor.ts
new file mode 100644
index 0000000..0608e73
--- /dev/null
+++ b/app/hooks/useThemeColor.ts
@@ -0,0 +1,21 @@
+/**
+ * Learn more about light and dark modes:
+ * https://docs.expo.dev/guides/color-schemes/
+ */
+
+import { Colors } from '@/constants/Colors';
+import { useColorScheme } from '@/hooks/useColorScheme';
+
+export function useThemeColor(
+ props: { light?: string; dark?: string },
+ colorName: keyof typeof Colors.light & keyof typeof Colors.dark
+) {
+ const theme = useColorScheme() ?? 'light';
+ const colorFromProps = props[theme];
+
+ if (colorFromProps) {
+ return colorFromProps;
+ } else {
+ return Colors[theme][colorName];
+ }
+}
diff --git a/app/lib/api/apiService.js b/app/lib/api/apiService.js
new file mode 100644
index 0000000..9f9f2d2
--- /dev/null
+++ b/app/lib/api/apiService.js
@@ -0,0 +1,120 @@
+import { Event } from '../models/Event';
+
+const API_URL = 'http://calendi.test:88/api'; // Change this to your backend URL
+
+/**
+ * Base API service for handling HTTP requests
+ */
+class ApiService {
+ /**
+ * Execute a fetch request with proper error handling
+ * @param {string} endpoint - API endpoint
+ * @param {Object} options - Fetch options
+ * @returns {Promise} Response data
+ */
+ async fetch(endpoint, options = {}) {
+ try {
+ const url = `${API_URL}${endpoint}`;
+
+ const defaultOptions = {
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Accept': 'application/json',
+ }
+ };
+
+ const response = await fetch(url, { ...defaultOptions, ...options });
+
+ if (!response.ok) {
+ throw new Error(`API request failed: ${response.status} ${response.statusText}`);
+ }
+
+ const data = await response.json();
+ return data;
+ } catch (error) {
+ console.error('API request error:', error);
+ throw error;
+ }
+ }
+
+ /**
+ * Get all events
+ * @returns {Promise} List of events
+ */
+ async getEvents() {
+ const data = await this.fetch('/events');
+ return data.map(eventData => new Event(eventData));
+ }
+
+ /**
+ * Get a single event by ID
+ * @param {string} id - Event ID
+ * @returns {Promise} Event object
+ */
+ async getEvent(id) {
+ const data = await this.fetch(`/events/${id}`);
+ return new Event(data);
+ }
+
+ /**
+ * Create a new event
+ * @param {Event} event - Event data
+ * @returns {Promise} Created event
+ */
+ async createEvent(event) {
+ const data = await this.fetch('/events', {
+ method: 'POST',
+ body: JSON.stringify(event)
+ });
+ return new Event(data);
+ }
+
+ /**
+ * Update an existing event
+ * @param {string} id - Event ID
+ * @param {Event} event - Updated event data
+ * @returns {Promise} Updated event
+ */
+ async updateEvent(id, event) {
+ const data = await this.fetch(`/events/${id}`, {
+ method: 'PUT',
+ body: JSON.stringify(event)
+ });
+ return new Event(data);
+ }
+
+ /**
+ * Delete an event
+ * @param {string} id - Event ID
+ * @returns {Promise}
+ */
+ async deleteEvent(id) {
+ await this.fetch(`/events/${id}`, {
+ method: 'DELETE'
+ });
+ }
+
+ /**
+ * Get user profile information
+ * @returns {Promise