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;