Anmeldung und Kalendar

This commit is contained in:
Ismail Ali
2025-07-15 20:38:04 +02:00
parent 2a34091404
commit 1cc8951c12
51 changed files with 19961 additions and 177 deletions

240
app/(tabs)/calendar.tsx Normal file
View File

@@ -0,0 +1,240 @@
import * as Calendar from "expo-calendar";
import * as SQLite from "expo-sqlite";
import React, { useEffect, useState } from "react";
import {
FlatList,
SafeAreaView,
StyleSheet,
Text,
TouchableOpacity,
View,
} from "react-native";
export interface EventItem {
id: string;
title: string;
startDate: string;
endDate: string;
}
const db = SQLite.openDatabaseSync("events.db");
export default function CalendarTab() {
const [source, setSource] = useState<"sqlite" | "iphone">("sqlite");
const [events, setEvents] = useState<EventItem[]>([]);
const [loading, setLoading] = useState(false);
const [selectedYear, setSelectedYear] = useState<number>(
new Date().getFullYear()
);
const [permissionError, setPermissionError] = useState<string | null>(null);
const [calendarType, setCalendarType] = useState<"calendar" | "reminder">(
"calendar"
);
useEffect(() => {
if (source === "sqlite") {
loadEventsFromDB();
} else {
loadEventsFromDevice();
}
}, [source, selectedYear, calendarType]);
const loadEventsFromDB = async () => {
setLoading(true);
try {
const stmt = await db.prepareAsync("SELECT * FROM events;");
const loadedEvents: EventItem[] = [];
const result = await stmt.executeAsync([]);
for await (const row of result) {
loadedEvents.push(row as EventItem);
}
setEvents(loadedEvents);
await stmt.finalizeAsync();
} catch (e) {
setEvents([]);
} finally {
setLoading(false);
}
};
const loadEventsFromDevice = async () => {
setLoading(true);
setPermissionError(null);
try {
let perm;
if (calendarType === "reminder") {
perm = await Calendar.requestRemindersPermissionsAsync();
} else {
perm = await Calendar.requestCalendarPermissionsAsync();
}
if (!perm.granted) {
setEvents([]);
setPermissionError(
`Keine Berechtigung für ${
calendarType === "reminder" ? "Erinnerungen" : "Kalender"
}-Zugriff. Bitte in den iOS-Einstellungen aktivieren.`
);
return;
}
let calendars = await Calendar.getCalendarsAsync();
console.log(
"Gefundene Kalender:",
calendars.map((c) => ({ id: c.id, title: c.title, type: c.type }))
);
if (calendarType === "calendar") {
calendars = calendars.filter((c) => String(c.type) === "calendar");
} else {
calendars = calendars.filter((c) => String(c.type) === "reminder");
}
const start = new Date(selectedYear, 0, 1);
const end = new Date(selectedYear, 11, 31, 23, 59, 59);
let allEvents: EventItem[] = [];
for (const cal of calendars) {
const calEvents = await Calendar.getEventsAsync([cal.id], start, end);
console.log(
`Events für Kalender ${cal.title}:`,
calEvents.map((e) => ({
id: e.id,
title: e.title,
start: e.startDate,
end: e.endDate,
}))
);
allEvents = allEvents.concat(
calEvents.map((e) => ({
id: e.id,
title: e.title || "(Kein Titel)",
startDate:
typeof e.startDate === "string"
? e.startDate
: new Date(e.startDate).toISOString(),
endDate:
typeof e.endDate === "string"
? e.endDate
: new Date(e.endDate).toISOString(),
}))
);
}
setEvents(allEvents);
} catch (e: any) {
setEvents([]);
setPermissionError(
"Fehler beim Zugriff auf " +
(calendarType === "reminder" ? "Erinnerungen" : "Kalender") +
": " +
(e?.message || JSON.stringify(e))
);
} finally {
setLoading(false);
}
};
const renderItem = ({ item }: { item: EventItem }) => (
<View style={styles.item}>
<Text style={styles.title}>{item.title}</Text>
<Text style={styles.time}>
{new Date(item.startDate).toLocaleString()} -{" "}
{new Date(item.endDate).toLocaleString()}
</Text>
</View>
);
return (
<SafeAreaView style={styles.container}>
<View style={styles.switchRow}>
<TouchableOpacity
style={[
styles.switchButton,
source === "sqlite" && styles.activeButton,
]}
onPress={() => setSource("sqlite")}
>
<Text style={styles.switchText}>SQLite</Text>
</TouchableOpacity>
<TouchableOpacity
style={[
styles.switchButton,
source === "iphone" && styles.activeButton,
]}
onPress={() => setSource("iphone")}
>
<Text style={styles.switchText}>
iPhone ({calendarType === "calendar" ? "Kalender" : "Erinnerungen"})
</Text>
</TouchableOpacity>
</View>
{source === "iphone" && (
<View style={styles.switchRow}>
<TouchableOpacity
style={[
styles.switchButton,
calendarType === "calendar" && styles.activeButton,
]}
onPress={() => setCalendarType("calendar")}
>
<Text style={styles.switchText}>Kalender</Text>
</TouchableOpacity>
<TouchableOpacity
style={[
styles.switchButton,
calendarType === "reminder" && styles.activeButton,
]}
onPress={() => setCalendarType("reminder")}
>
<Text style={styles.switchText}>Erinnerungen</Text>
</TouchableOpacity>
</View>
)}
<Text style={styles.header}>Kalender Events ({selectedYear})</Text>
<FlatList
data={events}
keyExtractor={(item) => item.id}
renderItem={renderItem}
ListEmptyComponent={
permissionError ? (
<Text style={{ textAlign: "center", color: "red" }}>
{permissionError}
</Text>
) : (
<Text style={{ textAlign: "center" }}>Keine Events gefunden.</Text>
)
}
refreshing={loading}
onRefresh={
source === "sqlite" ? loadEventsFromDB : loadEventsFromDevice
}
/>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: "#fff", padding: 10 },
header: {
fontSize: 20,
fontWeight: "bold",
marginBottom: 16,
textAlign: "center",
},
item: {
marginBottom: 12,
padding: 12,
borderRadius: 8,
backgroundColor: "#f2f2f2",
},
title: { fontSize: 16, fontWeight: "600" },
time: { fontSize: 14, color: "#555" },
switchRow: {
flexDirection: "row",
justifyContent: "center",
marginBottom: 16,
},
switchButton: {
padding: 10,
borderRadius: 8,
backgroundColor: "#eee",
marginHorizontal: 8,
},
activeButton: { backgroundColor: "#007AFF" },
switchText: { color: "#333", fontWeight: "bold" },
});

View File

@@ -1,110 +0,0 @@
import { Image } from 'expo-image';
import { Platform, StyleSheet } from 'react-native';
import { Collapsible } from '@/components/Collapsible';
import { ExternalLink } from '@/components/ExternalLink';
import ParallaxScrollView from '@/components/ParallaxScrollView';
import { ThemedText } from '@/components/ThemedText';
import { ThemedView } from '@/components/ThemedView';
import { IconSymbol } from '@/components/ui/IconSymbol';
export default function TabTwoScreen() {
return (
<ParallaxScrollView
headerBackgroundColor={{ light: '#D0D0D0', dark: '#353636' }}
headerImage={
<IconSymbol
size={310}
color="#808080"
name="chevron.left.forwardslash.chevron.right"
style={styles.headerImage}
/>
}>
<ThemedView style={styles.titleContainer}>
<ThemedText type="title">Explore</ThemedText>
</ThemedView>
<ThemedText>This app includes example code to help you get started.</ThemedText>
<Collapsible title="File-based routing">
<ThemedText>
This app has two screens:{' '}
<ThemedText type="defaultSemiBold">app/(tabs)/index.tsx</ThemedText> and{' '}
<ThemedText type="defaultSemiBold">app/(tabs)/explore.tsx</ThemedText>
</ThemedText>
<ThemedText>
The layout file in <ThemedText type="defaultSemiBold">app/(tabs)/_layout.tsx</ThemedText>{' '}
sets up the tab navigator.
</ThemedText>
<ExternalLink href="https://docs.expo.dev/router/introduction">
<ThemedText type="link">Learn more</ThemedText>
</ExternalLink>
</Collapsible>
<Collapsible title="Android, iOS, and web support">
<ThemedText>
You can open this project on Android, iOS, and the web. To open the web version, press{' '}
<ThemedText type="defaultSemiBold">w</ThemedText> in the terminal running this project.
</ThemedText>
</Collapsible>
<Collapsible title="Images">
<ThemedText>
For static images, you can use the <ThemedText type="defaultSemiBold">@2x</ThemedText> and{' '}
<ThemedText type="defaultSemiBold">@3x</ThemedText> suffixes to provide files for
different screen densities
</ThemedText>
<Image source={require('@/assets/images/react-logo.png')} style={{ alignSelf: 'center' }} />
<ExternalLink href="https://reactnative.dev/docs/images">
<ThemedText type="link">Learn more</ThemedText>
</ExternalLink>
</Collapsible>
<Collapsible title="Custom fonts">
<ThemedText>
Open <ThemedText type="defaultSemiBold">app/_layout.tsx</ThemedText> to see how to load{' '}
<ThemedText style={{ fontFamily: 'SpaceMono' }}>
custom fonts such as this one.
</ThemedText>
</ThemedText>
<ExternalLink href="https://docs.expo.dev/versions/latest/sdk/font">
<ThemedText type="link">Learn more</ThemedText>
</ExternalLink>
</Collapsible>
<Collapsible title="Light and dark mode components">
<ThemedText>
This template has light and dark mode support. The{' '}
<ThemedText type="defaultSemiBold">useColorScheme()</ThemedText> hook lets you inspect
what the user&apos;s current color scheme is, and so you can adjust UI colors accordingly.
</ThemedText>
<ExternalLink href="https://docs.expo.dev/develop/user-interface/color-themes/">
<ThemedText type="link">Learn more</ThemedText>
</ExternalLink>
</Collapsible>
<Collapsible title="Animations">
<ThemedText>
This template includes an example of an animated component. The{' '}
<ThemedText type="defaultSemiBold">components/HelloWave.tsx</ThemedText> component uses
the powerful <ThemedText type="defaultSemiBold">react-native-reanimated</ThemedText>{' '}
library to create a waving hand animation.
</ThemedText>
{Platform.select({
ios: (
<ThemedText>
The <ThemedText type="defaultSemiBold">components/ParallaxScrollView.tsx</ThemedText>{' '}
component provides a parallax effect for the header image.
</ThemedText>
),
})}
</Collapsible>
</ParallaxScrollView>
);
}
const styles = StyleSheet.create({
headerImage: {
color: '#808080',
bottom: -90,
left: -35,
position: 'absolute',
},
titleContainer: {
flexDirection: 'row',
gap: 8,
},
});

View File

@@ -1,75 +1,501 @@
import { Image } from 'expo-image';
import { Platform, StyleSheet } from 'react-native';
import AsyncStorage from "@react-native-async-storage/async-storage";
import * as LocalAuthentication from "expo-local-authentication";
import * as SQLite from "expo-sqlite";
import React, { useEffect, useState } from "react";
import {
ActivityIndicator,
Alert,
KeyboardAvoidingView,
Platform,
SafeAreaView,
StyleSheet,
Text,
TextInput,
TouchableOpacity,
View,
} from "react-native";
import CalendarSync from "../../components/CalendarSync";
import { HelloWave } from '@/components/HelloWave';
import ParallaxScrollView from '@/components/ParallaxScrollView';
import { ThemedText } from '@/components/ThemedText';
import { ThemedView } from '@/components/ThemedView';
// Types
interface User {
id: number;
email: string;
firstName: string;
lastName: string;
createdAt: string;
}
// Simple Auth Component
export default function AuthScreen() {
const [isLoggedIn, setIsLoggedIn] = useState(false);
const [loading, setLoading] = useState(true);
const [currentUser, setCurrentUser] = useState<User | null>(null);
const [authMode, setAuthMode] = useState<"login" | "register">("login");
// Form states
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [firstName, setFirstName] = useState("");
const [lastName, setLastName] = useState("");
const [biometricSupported, setBiometricSupported] = useState(false);
const [showCalendar, setShowCalendar] = useState(false);
// Database
const [db, setDb] = useState<SQLite.SQLiteDatabase | null>(null);
useEffect(() => {
initApp();
}, []);
const initApp = async () => {
try {
// Initialize database
const database = await SQLite.openDatabaseAsync("AuthApp.db");
setDb(database);
await database.execAsync(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT UNIQUE NOT NULL,
firstName TEXT NOT NULL,
lastName TEXT NOT NULL,
password TEXT NOT NULL,
createdAt TEXT NOT NULL
)
`);
// Check biometric support
const compatible = await LocalAuthentication.hasHardwareAsync();
const enrolled = await LocalAuthentication.isEnrolledAsync();
setBiometricSupported(compatible && enrolled);
// Check if user is logged in
const session = await AsyncStorage.getItem("@user_session");
if (session) {
const userData = JSON.parse(session);
setCurrentUser(userData.user);
setIsLoggedIn(true);
}
} catch (error) {
console.error("App initialization failed:", error);
} finally {
setLoading(false);
}
};
const handleRegister = async () => {
if (!email || !password || !firstName || !lastName) {
Alert.alert("Fehler", "Bitte füllen Sie alle Felder aus.");
return;
}
if (password.length < 6) {
Alert.alert(
"Fehler",
"Das Passwort muss mindestens 6 Zeichen lang sein."
);
return;
}
if (!db) return;
try {
setLoading(true);
const createdAt = new Date().toISOString();
const result = await db.runAsync(
"INSERT INTO users (email, firstName, lastName, password, createdAt) VALUES (?, ?, ?, ?, ?)",
[email, firstName, lastName, password, createdAt]
);
const newUser: User = {
id: result.lastInsertRowId,
email,
firstName,
lastName,
createdAt,
};
// Save session
await AsyncStorage.setItem(
"@user_session",
JSON.stringify({ user: newUser })
);
setCurrentUser(newUser);
setIsLoggedIn(true);
Alert.alert("Erfolg", "Registrierung erfolgreich!");
} catch (error: any) {
if (error.message?.includes("UNIQUE constraint failed")) {
Alert.alert(
"Fehler",
"Ein Benutzer mit dieser E-Mail existiert bereits."
);
} else {
Alert.alert("Fehler", "Registrierung fehlgeschlagen.");
}
} finally {
setLoading(false);
}
};
const handleLogin = async () => {
if (!email || !password) {
Alert.alert("Fehler", "Bitte füllen Sie alle Felder aus.");
return;
}
if (!db) return;
try {
setLoading(true);
const result = (await db.getFirstAsync(
"SELECT * FROM users WHERE email = ? AND password = ?",
[email, password]
)) as any;
if (result) {
const user: User = {
id: result.id,
email: result.email,
firstName: result.firstName,
lastName: result.lastName,
createdAt: result.createdAt,
};
// Save session
await AsyncStorage.setItem("@user_session", JSON.stringify({ user }));
setCurrentUser(user);
setIsLoggedIn(true);
Alert.alert("Erfolg", "Anmeldung erfolgreich!");
} else {
Alert.alert("Fehler", "Ungültige E-Mail oder Passwort.");
}
} catch (error) {
Alert.alert("Fehler", "Anmeldung fehlgeschlagen.");
} finally {
setLoading(false);
}
};
const handleBiometricAuth = async () => {
try {
const result = await LocalAuthentication.authenticateAsync({
promptMessage: "Biometrische Authentifizierung",
cancelLabel: "Abbrechen",
fallbackLabel: "Passcode verwenden",
});
if (result.success) {
// For demo, auto-login first user
if (!db) return;
const user = (await db.getFirstAsync(
"SELECT * FROM users LIMIT 1"
)) as any;
if (user) {
const userData: User = {
id: user.id,
email: user.email,
firstName: user.firstName,
lastName: user.lastName,
createdAt: user.createdAt,
};
await AsyncStorage.setItem(
"@user_session",
JSON.stringify({ user: userData })
);
setCurrentUser(userData);
setIsLoggedIn(true);
}
}
} catch (error) {
Alert.alert("Fehler", "Biometrische Authentifizierung fehlgeschlagen.");
}
};
const handleLogout = async () => {
try {
await AsyncStorage.removeItem("@user_session");
setCurrentUser(null);
setIsLoggedIn(false);
setEmail("");
setPassword("");
setFirstName("");
setLastName("");
} catch (error) {
Alert.alert("Fehler", "Abmeldung fehlgeschlagen.");
}
};
if (loading) {
return (
<SafeAreaView style={styles.container}>
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color="#007AFF" />
<Text style={styles.loadingText}>Lade App...</Text>
</View>
</SafeAreaView>
);
}
if (isLoggedIn && currentUser) {
return (
<SafeAreaView style={styles.container}>
<View style={styles.welcomeContainer}>
<Text style={styles.welcomeTitle}>Willkommen zurück!</Text>
<Text style={styles.userName}>
{currentUser.firstName} {currentUser.lastName}
</Text>
<View style={styles.userInfo}>
<Text style={styles.infoLabel}>E-Mail:</Text>
<Text style={styles.infoValue}>{currentUser.email}</Text>
<Text style={styles.infoLabel}>Mitglied seit:</Text>
<Text style={styles.infoValue}>
{new Date(currentUser.createdAt).toLocaleDateString("de-DE")}
</Text>
</View>
<TouchableOpacity style={styles.logoutButton} onPress={handleLogout}>
<Text style={styles.logoutButtonText}>Abmelden</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.submitButton, { marginTop: 20 }]}
onPress={() => setShowCalendar((prev) => !prev)}
>
<Text style={styles.submitButtonText}>
{showCalendar ? "Kalender ausblenden" : "Kalender anzeigen"}
</Text>
</TouchableOpacity>
{showCalendar && <CalendarSync />}
</View>
</SafeAreaView>
);
}
export default function HomeScreen() {
return (
<ParallaxScrollView
headerBackgroundColor={{ light: '#A1CEDC', dark: '#1D3D47' }}
headerImage={
<Image
source={require('@/assets/images/partial-react-logo.png')}
style={styles.reactLogo}
/>
}>
<ThemedView style={styles.titleContainer}>
<ThemedText type="title">Welcome!</ThemedText>
<HelloWave />
</ThemedView>
<ThemedView style={styles.stepContainer}>
<ThemedText type="subtitle">Step 1: Try it</ThemedText>
<ThemedText>
Edit <ThemedText type="defaultSemiBold">app/(tabs)/index.tsx</ThemedText> to see changes.
Press{' '}
<ThemedText type="defaultSemiBold">
{Platform.select({
ios: 'cmd + d',
android: 'cmd + m',
web: 'F12',
})}
</ThemedText>{' '}
to open developer tools.
</ThemedText>
</ThemedView>
<ThemedView style={styles.stepContainer}>
<ThemedText type="subtitle">Step 2: Explore</ThemedText>
<ThemedText>
{`Tap the Explore tab to learn more about what's included in this starter app.`}
</ThemedText>
</ThemedView>
<ThemedView style={styles.stepContainer}>
<ThemedText type="subtitle">Step 3: Get a fresh start</ThemedText>
<ThemedText>
{`When you're ready, run `}
<ThemedText type="defaultSemiBold">npm run reset-project</ThemedText> to get a fresh{' '}
<ThemedText type="defaultSemiBold">app</ThemedText> directory. This will move the current{' '}
<ThemedText type="defaultSemiBold">app</ThemedText> to{' '}
<ThemedText type="defaultSemiBold">app-example</ThemedText>.
</ThemedText>
</ThemedView>
</ParallaxScrollView>
<SafeAreaView style={styles.container}>
<KeyboardAvoidingView
style={styles.keyboardView}
behavior={Platform.OS === "ios" ? "padding" : "height"}
>
<View style={styles.authContainer}>
<Text style={styles.title}>
{authMode === "login" ? "Anmelden" : "Registrieren"}
</Text>
{authMode === "register" && (
<>
<TextInput
style={styles.input}
placeholder="Vorname"
value={firstName}
onChangeText={setFirstName}
autoCapitalize="words"
/>
<TextInput
style={styles.input}
placeholder="Nachname"
value={lastName}
onChangeText={setLastName}
autoCapitalize="words"
/>
</>
)}
<TextInput
style={styles.input}
placeholder="E-Mail"
value={email}
onChangeText={setEmail}
keyboardType="email-address"
autoCapitalize="none"
/>
<TextInput
style={styles.input}
placeholder="Passwort"
value={password}
onChangeText={setPassword}
secureTextEntry
autoCapitalize="none"
/>
<TouchableOpacity
style={styles.submitButton}
onPress={authMode === "login" ? handleLogin : handleRegister}
disabled={loading}
>
{loading ? (
<ActivityIndicator color="white" />
) : (
<Text style={styles.submitButtonText}>
{authMode === "login" ? "Anmelden" : "Registrieren"}
</Text>
)}
</TouchableOpacity>
{biometricSupported && authMode === "login" && (
<TouchableOpacity
style={styles.biometricButton}
onPress={handleBiometricAuth}
>
<Text style={styles.biometricButtonText}>
Mit Face ID / Touch ID anmelden
</Text>
</TouchableOpacity>
)}
<TouchableOpacity
style={styles.switchButton}
onPress={() =>
setAuthMode(authMode === "login" ? "register" : "login")
}
>
<Text style={styles.switchButtonText}>
{authMode === "login"
? "Noch kein Konto? Registrieren"
: "Bereits ein Konto? Anmelden"}
</Text>
</TouchableOpacity>
</View>
</KeyboardAvoidingView>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
titleContainer: {
flexDirection: 'row',
alignItems: 'center',
gap: 8,
container: {
flex: 1,
backgroundColor: "#f8f9fa",
},
stepContainer: {
gap: 8,
marginBottom: 8,
keyboardView: {
flex: 1,
},
reactLogo: {
height: 178,
width: 290,
bottom: 0,
left: 0,
position: 'absolute',
authContainer: {
flex: 1,
justifyContent: "center",
paddingHorizontal: 30,
},
loadingContainer: {
flex: 1,
justifyContent: "center",
alignItems: "center",
},
loadingText: {
marginTop: 10,
fontSize: 16,
color: "#666",
},
welcomeContainer: {
flex: 1,
justifyContent: "center",
paddingHorizontal: 30,
},
welcomeTitle: {
fontSize: 28,
fontWeight: "bold",
color: "#333",
textAlign: "center",
marginBottom: 10,
},
userName: {
fontSize: 20,
color: "#666",
textAlign: "center",
marginBottom: 40,
},
userInfo: {
backgroundColor: "white",
padding: 20,
borderRadius: 10,
marginBottom: 30,
elevation: 2,
shadowColor: "#000",
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
},
infoLabel: {
fontSize: 14,
color: "#666",
marginTop: 10,
},
infoValue: {
fontSize: 16,
color: "#333",
fontWeight: "500",
marginBottom: 5,
},
title: {
fontSize: 32,
fontWeight: "bold",
color: "#333",
textAlign: "center",
marginBottom: 40,
},
input: {
backgroundColor: "white",
paddingHorizontal: 15,
paddingVertical: 15,
borderRadius: 10,
fontSize: 16,
marginBottom: 15,
elevation: 2,
shadowColor: "#000",
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
},
submitButton: {
backgroundColor: "#007AFF",
paddingVertical: 15,
borderRadius: 10,
alignItems: "center",
marginTop: 10,
},
submitButtonText: {
color: "white",
fontSize: 18,
fontWeight: "600",
},
biometricButton: {
backgroundColor: "#34C759",
paddingVertical: 15,
borderRadius: 10,
marginTop: 15,
alignItems: "center",
},
biometricButtonText: {
color: "white",
fontSize: 16,
fontWeight: "600",
},
switchButton: {
marginTop: 20,
alignItems: "center",
},
switchButtonText: {
color: "#007AFF",
fontSize: 16,
},
logoutButton: {
backgroundColor: "#FF3B30",
paddingVertical: 15,
borderRadius: 10,
alignItems: "center",
},
logoutButtonText: {
color: "white",
fontSize: 18,
fontWeight: "600",
},
});