diff --git a/.gitea/workflows/build-apk.yml b/.gitea/workflows/build-apk.yml index 96e51e8..6aa972b 100644 --- a/.gitea/workflows/build-apk.yml +++ b/.gitea/workflows/build-apk.yml @@ -44,20 +44,8 @@ jobs: working-directory: ./apk/android run: | ./gradlew assembleRelease - - name: 📦 Zip APK - working-directory: . - run: | - mkdir -p dist - cp apk/android/app/build/outputs/apk/release/app-release.apk dist/ - zip -j dist/build.zip dist/app-release.apk - - - name: Create Release - uses: https://gitea.com/actions/gitea-release-action@v1 - working-directory: dist + - name: 📤 Upload APK Artifact + uses: actions/upload-artifact@v3 with: - tag_name: latest - name: Latest Build - files: | - - build.zip - env: - GITEA_TOKEN: ${{ secrets.GITEA }} \ No newline at end of file + name: app-release-apk + path: apk/android/app/build/outputs/apk/release/app-release.apk \ No newline at end of file diff --git a/apk/app/(auth)/login.tsx b/apk/app/(auth)/login.tsx index 2b44310..589c9b0 100644 --- a/apk/app/(auth)/login.tsx +++ b/apk/app/(auth)/login.tsx @@ -1,6 +1,7 @@ import React, { useState } from "react"; import { View, Text, TextInput, TouchableOpacity, StyleSheet } from "react-native"; import { useAuth } from "@/context/auth-context"; +import { router } from "expo-router"; export default function AuthScreen() { const [isLogin, setIsLogin] = useState(true); @@ -17,9 +18,12 @@ function LoginScreen({ onSwitch }) { const [password, setPassword] = useState(""); const { login } = useAuth(); - const handleLogin = () => { - console.log("Login ->", { email, password }); - login(email, password); + const handleLogin = async () => { + const loginResult = await login(email, password); + if (loginResult) { + console.log("Login successful"); + router.replace("/(tabs)"); + } }; return ( diff --git a/apk/app/(tabs)/_layout.tsx b/apk/app/(tabs)/_layout.tsx index 3d54133..f0bd0af 100644 --- a/apk/app/(tabs)/_layout.tsx +++ b/apk/app/(tabs)/_layout.tsx @@ -1,5 +1,25 @@ import { Tabs } from "expo-router"; +import { Ionicons } from "@expo/vector-icons"; export default function TabsLayout() { - return ; -} + return ( + + ( + + ), + }} /> + ( + + ), + }} /> + + ); +} \ No newline at end of file diff --git a/apk/app/(tabs)/index.tsx b/apk/app/(tabs)/index.tsx index 417f39e..5819aeb 100644 --- a/apk/app/(tabs)/index.tsx +++ b/apk/app/(tabs)/index.tsx @@ -14,7 +14,11 @@ export default function HomeScreen() { Upcoming Birthdays router.push("/add")} + onPress={() => { + router.push("/add"); + } + } + style={styles.addButton} > + diff --git a/apk/app/(tabs)/settings.tsx b/apk/app/(tabs)/settings.tsx new file mode 100644 index 0000000..12f06be --- /dev/null +++ b/apk/app/(tabs)/settings.tsx @@ -0,0 +1,148 @@ +import React, { useState } from "react"; +import { StyleSheet, Text, View, TouchableOpacity, Alert } from "react-native"; +import { SafeAreaView } from "react-native-safe-area-context"; + +import { useRouter } from "expo-router"; +import { useAuth } from "@/context/auth-context"; + +export default function SettingsScreen() { + const router = useRouter() + const { logout } = useAuth(); + const [theme, setTheme] = useState("light"); // light | dark | system + + const handleLogout = () => { + Alert.alert("Logout", "Are you sure you want to logout?", [ + { text: "Cancel", style: "cancel" }, + { + text: "Logout", + style: "destructive", + onPress: () => { + // TODO: clear auth state here + router.replace("/login"); + logout(); + }, + }, + ]); + }; + + const toggleTheme = (value) => { + setTheme(value); + // TODO: persist theme (AsyncStorage / context / zustand) + }; + + return ( + + + + Settings + + {/* Theme Selector */} + + Theme + + + toggleTheme("light")} + /> + toggleTheme("dark")} + /> + toggleTheme("system")} + /> + + + + {/* Logout */} + + Account + + + Log Out + + + + + + ); +} + +function ThemeButton({ label, active, onPress }) { + return ( + + + {label} + + + ); +} + +const styles = StyleSheet.create({ + screen: { + flex: 1, + backgroundColor: "#fff", + }, + content: { + flex: 1, + paddingHorizontal: 16, + paddingTop: 12, + }, + title: { + fontSize: 24, + fontWeight: "bold", + marginBottom: 20, + }, + + section: { + marginBottom: 24, + }, + sectionTitle: { + fontSize: 16, + fontWeight: "600", + marginBottom: 10, + }, + + row: { + flexDirection: "row", + gap: 10, + }, + + themeButton: { + paddingVertical: 8, + paddingHorizontal: 12, + borderRadius: 8, + borderWidth: 1, + borderColor: "#ccc", + }, + themeButtonActive: { + backgroundColor: "#111", + borderColor: "#111", + }, + themeText: { + color: "#333", + paddingHorizontal: 5, + }, + themeTextActive: { + color: "#fff", + }, + + logoutButton: { + padding: 14, + borderRadius: 10, + backgroundColor: "#ff3b30", + alignItems: "center", + }, + logoutText: { + color: "#fff", + fontWeight: "600", + }, +}); \ No newline at end of file diff --git a/apk/app/(tabs)/add.tsx b/apk/app/add.tsx similarity index 100% rename from apk/app/(tabs)/add.tsx rename to apk/app/add.tsx diff --git a/apk/components/views/loading-screen.tsx b/apk/components/views/loading-screen.tsx new file mode 100644 index 0000000..3a134f9 --- /dev/null +++ b/apk/components/views/loading-screen.tsx @@ -0,0 +1,23 @@ +import React from "react"; +import { View, Text, ActivityIndicator, StyleSheet } from "react-native"; + +export default function LoadingScreen({ message = "Loading..." }) { + return ( + + + {message} + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + alignItems: "center", + justifyContent: "center", + }, + text: { + marginTop: 10, + fontSize: 16, + }, +}); \ No newline at end of file diff --git a/apk/components/views/root-navigation.tsx b/apk/components/views/root-navigation.tsx index 3ccd1a1..5951315 100644 --- a/apk/components/views/root-navigation.tsx +++ b/apk/components/views/root-navigation.tsx @@ -1,29 +1,32 @@ import { BirthdaysProvider } from "@/context/birthdays-context"; import { useAuth } from "@/context/auth-context"; import { Stack, router, useRootNavigationState, useSegments } from "expo-router"; -import { useEffect } from "react"; +import React, { useEffect } from "react"; +import LoadingScreen from "./loading-screen"; export default function RootLayout() { const { user, isHydrated } = useAuth(); const navigationState = useRootNavigationState(); const segments = useSegments(); + const [loaded, setLoaded] = React.useState(false); useEffect(() => { - if (!isHydrated || !navigationState?.key) { - return; - } + if (!isHydrated || !navigationState?.key) return; - const inAuthGroup = segments[0] === "(auth)"; + const inAuthGroup = segments.length === 0; - if (!user && !inAuthGroup) { - router.replace("/(auth)/login"); - return; - } + if (!user) { + router.replace("/(auth)/login"); + } else if (inAuthGroup) { + router.replace("/(tabs)"); + } - if (user && inAuthGroup) { - router.replace("/(tabs)"); - } - }, [isHydrated, navigationState?.key, segments, user]); + setLoaded(true); // 👈 ALWAYS run this +}, [isHydrated, navigationState?.key, segments, user]); + + if(!loaded) { + return (); // or a loading spinner + } return ( diff --git a/apk/components/views/tabs.tsx b/apk/components/views/tabs.tsx deleted file mode 100644 index efab7b9..0000000 --- a/apk/components/views/tabs.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { - DarkTheme, - DefaultTheme, - ThemeProvider, -} from "@react-navigation/native"; -import { Stack } from "expo-router"; -import { StatusBar } from "expo-status-bar"; -import "react-native-reanimated"; - -import { BirthdaysProvider } from "@/context/birthdays-context"; -import { useColorScheme } from "@/hooks/use-color-scheme"; - -export const unstable_settings = { - anchor: "(tabs)", -}; - -export default function RootLayout() { - const colorScheme = useColorScheme(); - - return ( - - - - - - - - - - ); -} diff --git a/apk/context/auth-context.tsx b/apk/context/auth-context.tsx index 565ed0c..a86e0bc 100644 --- a/apk/context/auth-context.tsx +++ b/apk/context/auth-context.tsx @@ -8,7 +8,7 @@ interface User { interface AuthContextValue { user: User | null; isHydrated: boolean; - login: (email: string, password: string) => Promise; + login: (email: string, password: string) => Promise; logout: () => Promise; } @@ -41,6 +41,9 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(fakeUser)); setUser(fakeUser); + console.log(fakeUser) + await new Promise((resolve) => setTimeout(resolve, 1000)); + return true; } const logout = async () => {