Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# TeachLink Mobile Environment Variables

# API Configuration
API_BASE_URL=https://api.teachlink.com
API_TIMEOUT=30000

# Expo Configuration
EXPO_PROJECT_ID=your-expo-project-id

# Push Notifications
# For production, configure these in EAS Build
# EXPO_PUSH_TOKEN_URL=https://exp.host/--/api/v2/push/send

# Socket.IO (Real-time features)
SOCKET_URL=https://socket.teachlink.com

# Feature Flags
ENABLE_PUSH_NOTIFICATIONS=true
ENABLE_ANALYTICS=false
45 changes: 45 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Dependencies
node_modules/

# Expo
.expo/
dist/
web-build/

# Native builds
*.orig.*
*.jks
*.p8
*.p12
*.key
*.mobileprovision

# Metro
.metro-health-check*

# Environment
.env
.env.local
.env.*.local

# IDE
.idea/
.vscode/
*.swp
*.swo
.DS_Store

# Testing
coverage/

# TypeScript
*.tsbuildinfo

# Logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Firebase
google-services.json
GoogleService-Info.plist
183 changes: 183 additions & 0 deletions App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
import React, { useEffect, useState, useRef } from 'react';
import { NavigationContainer, NavigationContainerRef } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { StatusBar } from 'expo-status-bar';
import { View, Text, TouchableOpacity } from 'react-native';

// Notification imports
import { linking, RootStackParamList, setupNotificationNavigation } from './src/navigation/linking';
import { setNavigationRef, handleNotificationReceived } from './src/utils/notificationHandlers';
import {
addNotificationReceivedListener,
getLastNotificationResponse,
removeNotificationListener,
} from './src/services/pushNotifications';
import { NotificationPrompt } from './src/components/mobile/NotificationPrompt';
import { useNotificationStore } from './src/store/notificationStore';

const Stack = createNativeStackNavigator<RootStackParamList>();

// Placeholder screens - replace with your actual screens
function HomeScreen() {
const [showPrompt, setShowPrompt] = useState(false);
const { hasPromptedForPermission, unreadCount } = useNotificationStore();

useEffect(() => {
// Show notification prompt on first launch (after a short delay)
if (!hasPromptedForPermission) {
const timer = setTimeout(() => setShowPrompt(true), 2000);
return () => clearTimeout(timer);
}
}, [hasPromptedForPermission]);

return (
<View className="flex-1 items-center justify-center bg-white dark:bg-gray-900">
<Text className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
TeachLink Home
</Text>
{unreadCount > 0 && (
<Text className="text-indigo-600 mb-4">
You have {unreadCount} unread notification{unreadCount > 1 ? 's' : ''}
</Text>
)}
<TouchableOpacity
onPress={() => setShowPrompt(true)}
className="bg-indigo-600 px-6 py-3 rounded-xl"
>
<Text className="text-white font-semibold">Test Notification Prompt</Text>
</TouchableOpacity>

<NotificationPrompt
visible={showPrompt}
onClose={() => setShowPrompt(false)}
onPermissionGranted={() => console.log('Permission granted!')}
onPermissionDenied={() => console.log('Permission denied')}
/>
</View>
);
}

function CourseDetailScreen({ route }: { route: { params: { courseId: string } } }) {
return (
<View className="flex-1 items-center justify-center bg-white dark:bg-gray-900">
<Text className="text-xl text-gray-900 dark:text-white">
Course: {route.params.courseId}
</Text>
</View>
);
}

function ChatScreen({ route }: { route: { params: { conversationId: string } } }) {
return (
<View className="flex-1 items-center justify-center bg-white dark:bg-gray-900">
<Text className="text-xl text-gray-900 dark:text-white">
Chat: {route.params.conversationId}
</Text>
</View>
);
}

function LearningScreen() {
return (
<View className="flex-1 items-center justify-center bg-white dark:bg-gray-900">
<Text className="text-xl text-gray-900 dark:text-white">Learning Dashboard</Text>
</View>
);
}

function AchievementDetailScreen({ route }: { route: { params: { achievementId: string } } }) {
return (
<View className="flex-1 items-center justify-center bg-white dark:bg-gray-900">
<Text className="text-xl text-gray-900 dark:text-white">
Achievement: {route.params.achievementId}
</Text>
</View>
);
}

function CommunityPostScreen({ route }: { route: { params: { postId: string } } }) {
return (
<View className="flex-1 items-center justify-center bg-white dark:bg-gray-900">
<Text className="text-xl text-gray-900 dark:text-white">
Post: {route.params.postId}
</Text>
</View>
);
}

export default function App() {
const navigationRef = useRef<NavigationContainerRef<RootStackParamList>>(null);

useEffect(() => {
// Set up notification navigation handler
const cleanup = setupNotificationNavigation();

// Listen for notifications received while app is foregrounded
const subscription = addNotificationReceivedListener(handleNotificationReceived);

// Check if app was launched from a notification
getLastNotificationResponse().then((response) => {
if (response) {
console.log('App launched from notification:', response);
}
});

return () => {
cleanup();
removeNotificationListener(subscription);
};
}, []);

return (
<>
<StatusBar style="auto" />
<NavigationContainer
ref={navigationRef}
linking={linking}
onReady={() => {
// Set navigation ref for notification handlers
if (navigationRef.current) {
setNavigationRef({
navigate: (screen, params) => {
navigationRef.current?.navigate(screen as keyof RootStackParamList, params as any);
},
isReady: () => navigationRef.current?.isReady() ?? false,
});
}
}}
>
<Stack.Navigator
initialRouteName="Home"
screenOptions={{
headerStyle: { backgroundColor: '#4F46E5' },
headerTintColor: '#fff',
headerTitleStyle: { fontWeight: 'bold' },
}}
>
<Stack.Screen name="Home" component={HomeScreen} options={{ title: 'TeachLink' }} />
<Stack.Screen
name="CourseDetail"
component={CourseDetailScreen}
options={{ title: 'Course' }}
/>
<Stack.Screen name="Chat" component={ChatScreen} options={{ title: 'Chat' }} />
<Stack.Screen
name="Learning"
component={LearningScreen}
options={{ title: 'Learning' }}
/>
<Stack.Screen
name="AchievementDetail"
component={AchievementDetailScreen}
options={{ title: 'Achievement' }}
/>
<Stack.Screen
name="CommunityPost"
component={CommunityPostScreen}
options={{ title: 'Post' }}
/>
</Stack.Navigator>
</NavigationContainer>
</>
);
}
64 changes: 64 additions & 0 deletions app.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
{
"expo": {
"name": "TeachLink",
"slug": "teachlink",
"version": "1.0.0",
"orientation": "portrait",
"icon": "./assets/icon.png",
"userInterfaceStyle": "automatic",
"scheme": "teachlink",
"splash": {
"image": "./assets/splash.png",
"resizeMode": "contain",
"backgroundColor": "#4F46E5"
},
"assetBundlePatterns": ["**/*"],
"ios": {
"supportsTablet": true,
"bundleIdentifier": "com.teachlink.mobile",
"infoPlist": {
"NSCameraUsageDescription": "TeachLink needs camera access for profile photos and content creation.",
"NSPhotoLibraryUsageDescription": "TeachLink needs photo library access to upload images.",
"UIBackgroundModes": ["remote-notification"]
}
},
"android": {
"adaptiveIcon": {
"foregroundImage": "./assets/adaptive-icon.png",
"backgroundColor": "#4F46E5"
},
"package": "com.teachlink.mobile",
"googleServicesFile": "./google-services.json",
"permissions": [
"RECEIVE_BOOT_COMPLETED",
"VIBRATE",
"WAKE_LOCK"
]
},
"web": {
"favicon": "./assets/favicon.png"
},
"plugins": [
[
"expo-notifications",
{
"icon": "./assets/notification-icon.png",
"color": "#4F46E5",
"sounds": ["./assets/sounds/notification.wav"],
"mode": "production"
}
]
],
"notification": {
"icon": "./assets/notification-icon.png",
"color": "#4F46E5",
"androidMode": "default",
"androidCollapsedTitle": "TeachLink"
},
"extra": {
"eas": {
"projectId": "your-project-id"
}
}
}
}
46 changes: 46 additions & 0 deletions assets/ASSETS_README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# TeachLink Asset Requirements

Replace the placeholder images with your actual brand assets.

## Required Assets

| File | Size | Description |
|------|------|-------------|
| `icon.png` | 1024x1024 | App icon (iOS & Android) |
| `adaptive-icon.png` | 1024x1024 | Android adaptive icon foreground |
| `splash.png` | 1284x2778 | Splash screen image |
| `notification-icon.png` | 96x96 | Android notification icon (white on transparent) |
| `favicon.png` | 48x48 | Web favicon |

## Optional Assets

| File | Description |
|------|-------------|
| `sounds/notification.wav` | Custom notification sound |

## Design Guidelines

### App Icon (`icon.png`)
- Square format, no transparency
- Simple, recognizable at small sizes
- Brand color: #4F46E5 (Indigo)

### Notification Icon (`notification-icon.png`)
- Must be white silhouette on transparent background
- Android will apply the accent color (#4F46E5)
- Keep it simple - fine details won't be visible

### Splash Screen (`splash.png`)
- Center your logo
- Background color is set in app.json (#4F46E5)
- Image will be centered and scaled to fit

## Quick Start

You can generate placeholder assets using Expo's asset generator:

```bash
npx expo-asset@latest generate
```

Or use your design tool (Figma, Sketch) to export at the correct sizes.
Binary file added assets/adaptive-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/favicon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/notification-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/splash.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 7 additions & 0 deletions babel.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module.exports = function(api) {
api.cache(true);
return {
presets: ['babel-preset-expo'],
plugins: ['nativewind/babel'],
};
};
26 changes: 26 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
module.exports = {
testEnvironment: 'node',
roots: ['<rootDir>/src'],
transform: {
'^.+\\.(ts|tsx)$': ['ts-jest', { tsconfig: 'tsconfig.json' }],
},
testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.(ts|tsx)$',
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
'^@components/(.*)$': '<rootDir>/src/components/$1',
'^@hooks/(.*)$': '<rootDir>/src/hooks/$1',
'^@services/(.*)$': '<rootDir>/src/services/$1',
'^@store/(.*)$': '<rootDir>/src/store/$1',
'^@types/(.*)$': '<rootDir>/src/types/$1',
'^@utils/(.*)$': '<rootDir>/src/utils/$1',
'^react-native$': '<rootDir>/node_modules/react-native',
},
collectCoverageFrom: [
'src/**/*.{ts,tsx}',
'!src/**/*.d.ts',
'!src/**/index.ts',
],
testPathIgnorePatterns: ['/node_modules/'],
};
Loading