This commit is contained in:
Tomas Dvorak
2026-02-24 10:33:08 +01:00
parent b083dac3f0
commit 55d0284b2a
90 changed files with 27855 additions and 1940 deletions
@@ -1,5 +1,4 @@
import React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { useAuth } from '../services/AuthContext';
import { useServerConfig } from '../services/ServerConfigContext';
@@ -1,6 +1,5 @@
import React from 'react';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import Icon from 'react-native-vector-icons/MaterialCommunityIcons';
import { useOffline } from '../services/OfflineContext';
import { useTheme } from 'react-native-paper';
@@ -28,13 +27,9 @@ export type MainTabParamList = {
const Tab = createBottomTabNavigator<MainTabParamList>();
const TabNavigator: React.FC = () => {
const { isOnline, pendingChanges } = useOffline();
const { pendingChanges } = useOffline();
const theme = useTheme();
const getTabBarIcon = (name: string, color: string) => (
<Icon name={name} size={24} color={color} />
);
return (
<Tab.Navigator
screenOptions={({ route }) => ({
@@ -8,16 +8,12 @@ import {
} from 'react-native';
import {
Text,
Card,
Title,
Paragraph,
TextInput,
Button,
FAB,
IconButton,
Avatar,
Chip,
Divider,
} from 'react-native-paper';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { useRealtimeUpdates } from '../services/RealtimeSyncContext';
@@ -7,19 +7,16 @@ import {
Alert,
} from 'react-native';
import {
Text,
Card,
Title,
Paragraph,
TextInput,
Button,
ActivityIndicator,
HelperText,
} from 'react-native-paper';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useServerConfig } from '../services/ServerConfigContext';
import { updateAPIBaseURL } from '../services/api';
import { useNavigation } from '@react-navigation/native';
interface ServerConfig {
baseUrl: string;
@@ -37,7 +34,6 @@ const ServerSetupScreen: React.FC = () => {
const [errors, setErrors] = useState<Partial<ServerConfig>>({});
const { setConfig: saveConfig } = useServerConfig();
const navigation = useNavigation();
const validateConfig = (): boolean => {
const newErrors: Partial<ServerConfig> = {};
@@ -9,7 +9,6 @@ import {
import {
TextInput,
Button,
Text,
Card,
Title,
Paragraph,
@@ -1,6 +1,6 @@
import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
import { View, Alert, Platform } from 'react-native';
import { Camera, useCameraDevices } from 'react-native-vision-camera';
import { Alert, Platform } from 'react-native';
import { useCameraDevices } from 'react-native-vision-camera';
import { request, PERMISSIONS, RESULTS } from 'react-native-permissions';
interface CameraContextType {
@@ -1,6 +1,6 @@
import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
import PushNotification from 'react-native-push-notification';
import { Platform, PermissionsAndroid, Alert } from 'react-native';
import { Platform, Alert } from 'react-native';
import { request, PERMISSIONS, RESULTS } from 'react-native-permissions';
interface Notification {
@@ -1,5 +1,5 @@
import React, { createContext, useContext, useState, useEffect, ReactNode, useCallback } from 'react';
import { NetInfoState, useNetInfo } from '@react-native-community/netinfo';
import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
import { useNetInfo } from '@react-native-community/netinfo';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { useServerConfig } from './ServerConfigContext';
import { DeviceEventEmitter } from 'react-native';
@@ -1,5 +1,5 @@
import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
import { Alert, Platform, PermissionsAndroid } from 'react-native';
import { Alert, Platform } from 'react-native';
import { request, PERMISSIONS, RESULTS } from 'react-native-permissions';
import Voice from 'react-native-voice';
-1
View File
@@ -1,7 +1,6 @@
import axios, { AxiosInstance, AxiosRequestConfig } from 'axios';
import { ApiResponse, User, Bookmark, Task, Note, TimeEntry, CalendarEvent, SearchFilters, SavedSearch } from '../types';
import { getStoredAuthData } from '../utils/storage';
import { useServerConfig } from './ServerConfigContext';
let API_BASE_URL = __DEV__
? 'http://localhost:8080/api'
+1 -1
View File
@@ -1,5 +1,5 @@
import { getOfflineData, clearOfflineChanges, addOfflineChange } from './storage';
import { authAPI, bookmarksAPI, tasksAPI, notesAPI, timeEntriesAPI } from '../services/api';
import { bookmarksAPI, tasksAPI, notesAPI, timeEntriesAPI } from '../services/api';
interface OfflineChange {
id: string;
+217 -3
View File
@@ -4,12 +4,13 @@ package main
import (
"context"
"crypto/rand"
cryptorand "crypto/rand"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"log"
"math/rand"
"net/http"
"net/url"
"os"
@@ -279,6 +280,15 @@ func main() {
courses.POST("/:id/resources", addCourseResource) // Admin/Instructor only
}
// Learning paths (alias for courses with learning path specific endpoints)
learningPaths := api.Group("/learning-paths")
{
learningPaths.GET("", getLearningPaths)
learningPaths.GET("/categories", getLearningPathCategories)
learningPaths.POST("/:id/enroll", enrollInLearningPath)
learningPaths.GET("/:id", getLearningPath)
}
// User progress
progress := api.Group("/progress")
{
@@ -781,14 +791,14 @@ func generateJWTWithEmailToken(userSession map[string]interface{}) string {
func generateRandomString(length int) string {
bytes := make([]byte, length)
rand.Read(bytes)
cryptorand.Read(bytes)
return hex.EncodeToString(bytes)
}
func generateVerificationCode() string {
// Generate 6-digit verification code
codeBytes := make([]byte, 3)
rand.Read(codeBytes)
cryptorand.Read(codeBytes)
code := int(codeBytes[0])*10000 + int(codeBytes[1])*100 + int(codeBytes[2])
code = (code % 900000) + 100000
return fmt.Sprintf("%06d", code)
@@ -988,6 +998,210 @@ func addCourseResource(c *gin.Context) {
c.JSON(http.StatusCreated, resource)
}
// Learning Paths Handlers (frontend-specific format)
func getLearningPaths(c *gin.Context) {
// Get query parameters
search := c.Query("search")
category := c.Query("category")
difficulty := c.Query("difficulty")
var learningPaths []gin.H
for _, course := range courses {
if !course.IsActive {
continue
}
// Apply filters
if search != "" && !containsIgnoreCase(course.Title, search) && !containsIgnoreCase(course.Description, search) {
continue
}
if category != "" && !containsIgnoreCase(course.Category, category) {
continue
}
if difficulty != "" && !containsIgnoreCase(course.Difficulty, difficulty) {
continue
}
// Convert to frontend format
resources := courseResources[course.ID]
tags := make([]gin.H, len(course.Tags))
for i, tag := range course.Tags {
tags[i] = gin.H{
"name": tag,
"color": "#3b82f6", // Blue color for all tags
}
}
modules := make([]gin.H, len(resources))
for i, resource := range resources {
modules[i] = gin.H{
"id": fmt.Sprintf("module_%d", resource.ID),
"title": resource.Title,
"description": resource.Description,
"completed": false,
"resources": []gin.H{
{
"type": string(resource.Type),
"title": resource.Title,
"url": resource.URL,
},
},
}
}
learningPath := gin.H{
"id": course.ID,
"title": course.Title,
"description": course.Description,
"category": course.Category,
"difficulty": course.Difficulty,
"duration": fmt.Sprintf("%d hours", course.Duration),
"thumbnail": course.Thumbnail,
"is_featured": course.ID <= 2, // First 2 courses are featured
"enrollment_count": rand.Intn(2000) + 200,
"rating": 4.0 + rand.Float64(),
"review_count": rand.Intn(200) + 20,
"creator": gin.H{
"username": "instructor",
"full_name": "Expert Instructor",
},
"tags": tags,
"modules": modules,
"createdAt": course.CreatedAt.Format("2006-01-02T15:04:05Z"),
}
learningPaths = append(learningPaths, learningPath)
}
c.JSON(http.StatusOK, learningPaths)
}
func getLearningPathCategories(c *gin.Context) {
categories := []string{
"Web Development",
"Mobile Development",
"Programming",
"DevOps",
"Data Science",
"Design",
"Business",
"Cybersecurity",
}
c.JSON(http.StatusOK, gin.H{"categories": categories})
}
func enrollInLearningPath(c *gin.Context) {
pathID := parseInt(c.Param("id"))
userID := getUserIDFromToken(c)
if userID == 0 {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
return
}
// Check if course exists
course, exists := courses[pathID]
if !exists || !course.IsActive {
c.JSON(http.StatusNotFound, gin.H{"error": "Learning path not found"})
return
}
// Create or update progress
key := fmt.Sprintf("%d_%d", userID, pathID)
progress, exists := userProgress[key]
if !exists {
progress = UserProgress{
UserID: userID,
CourseID: pathID,
Status: "in_progress",
Progress: 0.0,
StartedAt: time.Now(),
LastAccessed: time.Now(),
}
} else {
progress.Status = "in_progress"
progress.LastAccessed = time.Now()
}
userProgress[key] = progress
c.JSON(http.StatusOK, gin.H{
"message": "Successfully enrolled in learning path",
"enrolled": true,
"progress": progress,
})
}
func getLearningPath(c *gin.Context) {
pathID := parseInt(c.Param("id"))
course, exists := courses[pathID]
if !exists || !course.IsActive {
c.JSON(http.StatusNotFound, gin.H{"error": "Learning path not found"})
return
}
// Get resources
resources := courseResources[pathID]
course.Resources = resources
// Convert to frontend format
tags := make([]gin.H, len(course.Tags))
for i, tag := range course.Tags {
tags[i] = gin.H{
"name": tag,
"color": "#3b82f6",
}
}
modules := make([]gin.H, len(resources))
for i, resource := range resources {
modules[i] = gin.H{
"id": fmt.Sprintf("module_%d", resource.ID),
"title": resource.Title,
"description": resource.Description,
"completed": false,
"resources": []gin.H{
{
"type": string(resource.Type),
"title": resource.Title,
"url": resource.URL,
},
},
}
}
learningPath := gin.H{
"id": course.ID,
"title": course.Title,
"description": course.Description,
"category": course.Category,
"difficulty": course.Difficulty,
"duration": fmt.Sprintf("%d hours", course.Duration),
"thumbnail": course.Thumbnail,
"is_featured": course.ID <= 2,
"enrollment_count": rand.Intn(2000) + 200,
"rating": 4.0 + rand.Float64(),
"review_count": rand.Intn(200) + 20,
"creator": gin.H{
"username": "instructor",
"full_name": "Expert Instructor",
},
"tags": tags,
"modules": modules,
"createdAt": course.CreatedAt.Format("2006-01-02T15:04:05Z"),
}
c.JSON(http.StatusOK, learningPath)
}
// Helper function for case-insensitive contains
func containsIgnoreCase(s, substr string) bool {
return strings.Contains(strings.ToLower(s), strings.ToLower(substr))
}
// User Progress Handlers
func getUserProgress(c *gin.Context) {