mirror of
https://github.com/yusufipk/RepoHub.git
synced 2026-09-11 10:36:07 +00:00
- Fix N+1 query problem in preset package fetching * Search with limit 5 to find best matches * Case-insensitive package name matching * Fallback to most popular when no exact match - Improve category filtering accuracy * Update category mapping to match database schema * Remove duplicate packages in category fetching * Better distribution across categories - Add version control for localStorage profile * Support future schema migrations * Auto-migrate old profiles to new version * Persist version in localStorage - Enhance error handling * Specific error messages for 400/500/404 responses * Network error detection * User-friendly error messages in UI - Fix OS detection fallback * Default to ubuntu when detection returns unknown * Prevent empty OS selection in onboarding - Add better API validation * Explicit limit range validation (1-50) * Clear error messages for invalid inputs
208 lines
5.4 KiB
TypeScript
208 lines
5.4 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useEffect, useCallback } from "react";
|
|
import {
|
|
UserProfile,
|
|
UserCategory,
|
|
ExperienceLevel,
|
|
} from "@/types/recommendations";
|
|
|
|
const STORAGE_KEY = "repohub_user_profile";
|
|
|
|
/**
|
|
* Detect user's operating system from browser
|
|
*/
|
|
function detectOS(): string {
|
|
if (typeof window === "undefined") {
|
|
return "unknown";
|
|
}
|
|
|
|
const userAgent = window.navigator.userAgent.toLowerCase();
|
|
const platform = window.navigator.platform.toLowerCase();
|
|
|
|
// Windows
|
|
if (userAgent.indexOf("win") !== -1 || platform.indexOf("win") !== -1) {
|
|
return "windows";
|
|
}
|
|
|
|
// macOS
|
|
if (
|
|
userAgent.indexOf("mac") !== -1 ||
|
|
platform.indexOf("mac") !== -1 ||
|
|
userAgent.indexOf("darwin") !== -1
|
|
) {
|
|
return "macos";
|
|
}
|
|
|
|
// Linux distros
|
|
if (userAgent.indexOf("linux") !== -1 || platform.indexOf("linux") !== -1) {
|
|
// Try to detect specific distro from user agent (rare but possible)
|
|
if (userAgent.indexOf("ubuntu") !== -1) {
|
|
return "ubuntu";
|
|
}
|
|
if (userAgent.indexOf("fedora") !== -1) {
|
|
return "fedora";
|
|
}
|
|
if (userAgent.indexOf("arch") !== -1) {
|
|
return "arch";
|
|
}
|
|
if (userAgent.indexOf("debian") !== -1) {
|
|
return "debian";
|
|
}
|
|
|
|
// Default to Ubuntu for generic Linux
|
|
return "ubuntu";
|
|
}
|
|
|
|
return "unknown";
|
|
}
|
|
|
|
const CURRENT_PROFILE_VERSION = 1;
|
|
|
|
/**
|
|
* Get default user profile
|
|
*/
|
|
function getDefaultProfile(): UserProfile {
|
|
return {
|
|
version: CURRENT_PROFILE_VERSION,
|
|
categories: [],
|
|
detectedOS: detectOS(),
|
|
selectedOS: undefined,
|
|
experienceLevel: "beginner",
|
|
hasCompletedOnboarding: false,
|
|
createdAt: new Date().toISOString(),
|
|
lastUpdated: new Date().toISOString(),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Hook for managing user recommendation profile in localStorage
|
|
*/
|
|
export function useRecommendationProfile() {
|
|
const [profile, setProfile] = useState<UserProfile>(getDefaultProfile());
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
|
|
// Load profile from localStorage on mount
|
|
useEffect(() => {
|
|
try {
|
|
const stored = localStorage.getItem(STORAGE_KEY);
|
|
if (stored) {
|
|
const parsed = JSON.parse(stored) as UserProfile;
|
|
|
|
// Handle version migration
|
|
if (!parsed.version || parsed.version < CURRENT_PROFILE_VERSION) {
|
|
console.log("Migrating profile from version", parsed.version || 0, "to", CURRENT_PROFILE_VERSION);
|
|
// Add migration logic here when schema changes in the future
|
|
parsed.version = CURRENT_PROFILE_VERSION;
|
|
}
|
|
|
|
// Update detectedOS if it changed
|
|
const currentOS = detectOS();
|
|
if (parsed.detectedOS !== currentOS) {
|
|
parsed.detectedOS = currentOS;
|
|
}
|
|
|
|
setProfile(parsed);
|
|
// Save migrated profile
|
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(parsed));
|
|
} else {
|
|
// First time user - save default profile
|
|
const defaultProfile = getDefaultProfile();
|
|
setProfile(defaultProfile);
|
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(defaultProfile));
|
|
}
|
|
} catch (error) {
|
|
console.error("Error loading user profile:", error);
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
// Save profile to localStorage
|
|
const saveProfile = useCallback(
|
|
(newProfile: Partial<UserProfile>) => {
|
|
try {
|
|
const updated: UserProfile = {
|
|
...profile,
|
|
...newProfile,
|
|
lastUpdated: new Date().toISOString(),
|
|
};
|
|
setProfile(updated);
|
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(updated));
|
|
return true;
|
|
} catch (error) {
|
|
console.error("Error saving user profile:", error);
|
|
return false;
|
|
}
|
|
},
|
|
[profile]
|
|
);
|
|
|
|
// Update categories
|
|
const updateCategories = useCallback(
|
|
(categories: UserCategory[]) => {
|
|
return saveProfile({ categories });
|
|
},
|
|
[saveProfile]
|
|
);
|
|
|
|
// Update selected OS (manual override)
|
|
const updateSelectedOS = useCallback(
|
|
(os: string) => {
|
|
return saveProfile({ selectedOS: os });
|
|
},
|
|
[saveProfile]
|
|
);
|
|
|
|
// Update experience level
|
|
const updateExperienceLevel = useCallback(
|
|
(level: ExperienceLevel) => {
|
|
return saveProfile({ experienceLevel: level });
|
|
},
|
|
[saveProfile]
|
|
);
|
|
|
|
// Mark onboarding as completed
|
|
const completeOnboarding = useCallback(() => {
|
|
return saveProfile({ hasCompletedOnboarding: true });
|
|
}, [saveProfile]);
|
|
|
|
// Reset profile
|
|
const resetProfile = useCallback(() => {
|
|
try {
|
|
const defaultProfile = getDefaultProfile();
|
|
setProfile(defaultProfile);
|
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(defaultProfile));
|
|
return true;
|
|
} catch (error) {
|
|
console.error("Error resetting user profile:", error);
|
|
return false;
|
|
}
|
|
}, []);
|
|
|
|
// Get effective OS (selectedOS or detectedOS)
|
|
const getEffectiveOS = useCallback((): string => {
|
|
return profile.selectedOS || profile.detectedOS || "ubuntu";
|
|
}, [profile]);
|
|
|
|
// Check if profile is complete enough for recommendations
|
|
const isProfileComplete = useCallback((): boolean => {
|
|
return profile.categories.length > 0 && getEffectiveOS() !== "unknown";
|
|
}, [profile, getEffectiveOS]);
|
|
|
|
return {
|
|
profile,
|
|
isLoading,
|
|
saveProfile,
|
|
updateCategories,
|
|
updateSelectedOS,
|
|
updateExperienceLevel,
|
|
completeOnboarding,
|
|
resetProfile,
|
|
getEffectiveOS,
|
|
isProfileComplete,
|
|
detectedOS: profile.detectedOS,
|
|
hasCompletedOnboarding: profile.hasCompletedOnboarding,
|
|
};
|
|
}
|