diff --git a/app/CustomDrawerContent.tsx b/app/CustomDrawerContent.tsx new file mode 100644 index 0000000..d45b7cc --- /dev/null +++ b/app/CustomDrawerContent.tsx @@ -0,0 +1,28 @@ +// CustomDrawerContent.tsx +import { DrawerContentScrollView } from '@react-navigation/drawer'; +import React from 'react'; +import { Category, categories } from '../types/Category'; +import { SideMenu } from './SideMenu'; + +type CustomDrawerContentProps = { + selected: Category; + setSelected: (key: Category) => void; + handleLogout: () => void; +}; + +export function CustomDrawerContent({ + selected, + setSelected, + handleLogout, +}: CustomDrawerContentProps) { + return ( + + ({ key: c.key as Category, label: c.label }))} + selected={selected} + onSelect={key => setSelected(key)} + onLogout={handleLogout} + /> + + ); +} diff --git a/app/HomeScreen.tsx b/app/HomeScreen.tsx index b2936ae..5c58e4a 100644 --- a/app/HomeScreen.tsx +++ b/app/HomeScreen.tsx @@ -1,138 +1,40 @@ -import AsyncStorage from "@react-native-async-storage/async-storage"; -import * as Notifications from "expo-notifications"; -import React, { useEffect, useState } from "react"; -import { Linking, ScrollView, Text, TextInput, TouchableOpacity, View } from "react-native"; -import { SafeAreaView } from "react-native-safe-area-context"; -import { Category, categories, categoryKeys, categoryTitles } from "types/Category"; -import { Item } from "types/Item"; -import { usePushNotifications } from "../hooks/usePushNotifications"; -import { styles } from "../styles/HomeScreen.styles"; +import React from 'react'; +import { ScrollView, Text, View } from 'react-native'; +import { SafeAreaView } from 'react-native-safe-area-context'; +import { Category } from 'types/Category'; +import { Item } from 'types/Item'; +import { styles } from '../styles/HomeScreen.styles'; -const API_URL = "https://notifier.gansejunge.com"; -const STORAGE_KEY = "notifications"; -const API_KEY_STORAGE = "api_key"; +type HomeScreenProps = { + selectedCategory?: Category; + onSelectCategory?: (key: Category) => void; + data?: Item[]; + apiKey?: string | null; + setApiKey?: (key: string) => void; + tempKey?: string; + setTempKey?: (key: string) => void; +}; -export default function HomeScreen() { - const [data, setData] = useState([]); - const [selected, setSelected] = useState("home"); - const [menuOpen, setMenuOpen] = useState(false); - const [apiKey, setApiKey] = useState(null); - const [tempKey, setTempKey] = useState(""); - - const pushToken = usePushNotifications({ - apiKey, - backendUrl: API_URL, - appVersion: "1.0.0", - locale: "en-uk", - }); - - // Load API key on startup - useEffect(() => { - (async () => { - const storedKey = await AsyncStorage.getItem(API_KEY_STORAGE); - if (storedKey) setApiKey(storedKey); - })(); - }, []); - - // Load saved notifications - useEffect(() => { - (async () => { - try { - const stored = await AsyncStorage.getItem(STORAGE_KEY); - if (stored) setData(JSON.parse(stored)); - } catch (err) { - console.error("Failed to load stored notifications:", err); - } - })(); - }, []); - - - // Listen for incoming notifications - useEffect(() => { - const subscription = Notifications.addNotificationReceivedListener(notification => { - const rawCategory = notification.request.content.data?.category as Category | undefined; - const category: Category = rawCategory && categoryKeys.includes(rawCategory) ? rawCategory : "home"; - - const item: Item = { - timestamp: Date.now(), - category, - title: notification.request.content.title || "No title", - info: notification.request.content.body || "No description", - link: (notification.request.content.data?.link as string) || "#", - }; - - setData(prev => { - const updated = [...prev, item].sort((a, b) => b.timestamp - a.timestamp); - AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(updated)); - return updated; - }); - }); - return () => subscription.remove(); - }, []); - - const filteredData = selected === "home" ? data : data.filter(item => item.category === selected); - const menuItems = categories; - - if (!apiKey) { - return ( - - - Enter API Key: - - { - await AsyncStorage.setItem(API_KEY_STORAGE, tempKey); - setApiKey(tempKey); - }} - style={{ backgroundColor: "blue", padding: 10 }} - > - Save - - - - ); - } +export default function HomeScreen({ + selectedCategory = 'home', + data = [], +}: HomeScreenProps) { + const filteredData = selectedCategory === 'home' + ? data + : data.filter(item => item.category === selectedCategory); return ( - {/* Side Menu */} - - {menuItems.map(item => ( - { - setSelected(item.key); - setMenuOpen(false); - }} - > - {item.label} - - ))} - - - {/* Main Content */} - setMenuOpen(!menuOpen)}> - - - {categoryTitles[selected]} - + {selectedCategory} {filteredData.length === 0 ? ( - No items yet + No items yet ) : ( filteredData.map(item => ( {item.title} {item.info} - Linking.openURL(item.link)}> - Read more - )) )} @@ -141,4 +43,3 @@ export default function HomeScreen() { ); } - diff --git a/app/SideMenu.tsx b/app/SideMenu.tsx index d34cdcf..1f31681 100644 --- a/app/SideMenu.tsx +++ b/app/SideMenu.tsx @@ -1,38 +1,46 @@ +//SideMenu.tsx import React from 'react'; import { StyleSheet, Text, TouchableOpacity, View } from 'react-native'; +import { Category } from "../types/Category"; export type MenuItem = { label: string; - key: string; + key: Category; }; type SideMenuProps = { menuItems: MenuItem[]; - selected: string; - onSelect: (key: string) => void; + selected: Category; + onSelect: (key: Category) => void; + onLogout?: () => void; }; -export default function SideMenu({ menuItems, selected, onSelect }: SideMenuProps) { + +export function SideMenu({ menuItems, selected, onSelect, onLogout }: SideMenuProps) { return ( {menuItems.map(item => ( onSelect(item.key)}> {item.label} ))} + + {onLogout && ( + + Logout + + )} ); } const styles = StyleSheet.create({ sideMenu: { - width: 180, backgroundColor: '#333', paddingTop: 40, paddingHorizontal: 16, @@ -48,5 +56,15 @@ const styles = StyleSheet.create({ menuText: { color: '#fff', fontSize: 18, + textAlign: 'center', + }, + logoutButton: { + marginTop: 'auto', + backgroundColor: '#111', + paddingVertical: 16, + borderRadius: 4, + alignItems: 'center', + justifyContent: 'center', + flexDirection: 'column', }, }); diff --git a/app/index.tsx b/app/index.tsx index 4df6faf..d9012a0 100644 --- a/app/index.tsx +++ b/app/index.tsx @@ -1,11 +1,108 @@ -import React from 'react'; -import { SafeAreaProvider } from "react-native-safe-area-context"; +// index.tsx +import AsyncStorage from '@react-native-async-storage/async-storage'; +import { createDrawerNavigator } from '@react-navigation/drawer'; +import React, { useEffect, useState } from 'react'; +import { SafeAreaProvider } from 'react-native-safe-area-context'; + +import { useNotificationListener } from '../hooks/useNotificationListener'; +import { usePushNotifications } from '../hooks/usePushNotifications'; +import { Category } from '../types/Category'; +import { Item } from '../types/Item'; +import { CustomDrawerContent } from './CustomDrawerContent'; import HomeScreen from './HomeScreen'; +const STORAGE_KEY = 'notifications'; +const API_KEY_STORAGE = 'api_key'; +const API_URL = 'https://notifier.gansejunge.com'; + +const Drawer = createDrawerNavigator(); + + + + export default function App() { + const [data, setData] = useState([]); + const [selected, setSelected] = useState('home'); + const [apiKey, setApiKey] = useState(null); + const [tempKey, setTempKey] = useState(''); + + const pushToken = usePushNotifications({ + apiKey, + backendUrl: API_URL, + appVersion: '1.0.0', + locale: 'en-uk', + }); + + useEffect(() => { + (async () => { + const storedKey = await AsyncStorage.getItem(API_KEY_STORAGE); + if (storedKey) setApiKey(storedKey); + })(); + }, []); + + useEffect(() => { + (async () => { + try { + const stored = await AsyncStorage.getItem(STORAGE_KEY); + if (stored) setData(JSON.parse(stored)); + } catch (err) { + console.error('Failed to load stored notifications:', err); + } + })(); + }, []); + + useNotificationListener((items: Item[]) => { + setData(items); + }); + + const handleLogout = async () => { + await AsyncStorage.removeItem(API_KEY_STORAGE); + await AsyncStorage.removeItem(STORAGE_KEY); + setApiKey(null); + setData([]); + setSelected('home'); + }; + + if (!apiKey) { + // Show API key entry screen before Drawer + return ( + + + + ); + } + return ( - + ( + + )} + > + + {() => ( + + )} + + ); -} \ No newline at end of file +} diff --git a/eas.json b/eas.json index 1b001dc..59a97ab 100644 --- a/eas.json +++ b/eas.json @@ -8,7 +8,7 @@ "developmentClient": true, "distribution": "internal" }, - "preview": { + "release": { "android": {"buildType": "apk"} }, "debug":{ diff --git a/hooks/useNotificationListener.ts b/hooks/useNotificationListener.ts index eb6723c..8d2f272 100644 --- a/hooks/useNotificationListener.ts +++ b/hooks/useNotificationListener.ts @@ -48,26 +48,30 @@ export function useNotificationListener(onItems: (items: Item[]) => void) { } // Convert Expo notification → Item -function mapNotificationToItem(notification: Notifications.Notification): Item | null { +export function mapNotificationToItem(notification: Notifications.Notification): Item | null { try { const content = notification.request.content; - const rawCategory = content.data?.category as Category | undefined; - const category: Category = rawCategory && categoryKeys.includes(rawCategory) ? rawCategory : "home"; + const data = content.data || {}; + + const rawCategory = data.category as string | undefined; + const category: Category = categoryKeys.includes(rawCategory as Category) + ? (rawCategory as Category) + : "utility"; return { - timestamp: Date.now(), // could use backend timestamp if provided + timestamp: data.timestamp ? Number(data.timestamp) : Date.now(), category, title: content.title ?? "No title", - info: content.body ?? "No details", - link: (content.data?.link as string) ?? "", + info: content.body ?? "No description", + link: typeof data.link === "string" ? data.link : "#", }; - } catch (e) { - console.error("Failed to map notification to Item:", e); + } catch (err) { + console.error("Failed to map notification to Item:", err); return null; } } -export { mapNotificationToItem }; +//export { mapNotificationToItem }; // Save a new item into storage and update state async function addItem(item: Item, onItems: (items: Item[]) => void) { diff --git a/package.json b/package.json index 50e02a6..244edad 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,8 @@ "@expo/metro-runtime": "~6.1.2", "@radix-ui/react-dialog": "^1.1.15", "@react-native-async-storage/async-storage": "2.2.0", + "@react-navigation/drawer": "^7.5.0", + "@react-navigation/native": "^7.1.8", "expo": "~54.0.11", "expo-constants": "~18.0.9", "expo-device": "~8.0.9", @@ -22,8 +24,8 @@ "expo-router": "~6.0.9", "expo-splash-screen": "~31.0.10", "expo-system-ui": "~6.0.7", - "react": "^19.2.0", - "react-dom": "^19.2.0", + "react": "19.1.0", + "react-dom": "19.1.0", "react-native": "0.81.4", "react-native-safe-area-context": "~5.6.0", "react-native-screens": "~4.16.0" diff --git a/setup.sh b/setup.sh new file mode 100644 index 0000000..ca6284d --- /dev/null +++ b/setup.sh @@ -0,0 +1 @@ +npx expo install expo-constants expo-linking react-native-safe-area-context react-native-screens expo-system-ui diff --git a/styles/HomeScreen.styles.ts b/styles/HomeScreen.styles.ts index aae826f..2749e2f 100644 --- a/styles/HomeScreen.styles.ts +++ b/styles/HomeScreen.styles.ts @@ -17,6 +17,15 @@ export const styles = StyleSheet.create({ width: 180, paddingHorizontal: 16, }, + //sideMenuOverlay: { + //position: "absolute", + //top: 0, + //left: 0, + //bottom: 0, + //width: "70%", + //zIndex: 10, + //backgroundColor: "#222", + //}, menuItem: { paddingVertical: 16, paddingHorizontal: 8, diff --git a/types/Category.ts b/types/Category.ts index cf998ce..2ecf820 100644 --- a/types/Category.ts +++ b/types/Category.ts @@ -10,6 +10,6 @@ const baseCategories = [ ] as const; -export const categoryKeys: Category[] = baseCategories.map(c => c.key); +export const categoryKeys: Category[] = baseCategories.map(c => c.key) as Category[]; export const categories: { label: string; key: Category }[] = baseCategories.map(c => ({ label: c.label, key: c.key })); export const categoryTitles: Record = Object.fromEntries(baseCategories.map(c => [c.key, c.title])) as Record;