From bcf865272824a956366c8fd0c8695d96ab662332 Mon Sep 17 00:00:00 2001 From: ersaayan Date: Sat, 22 Nov 2025 16:21:15 +0300 Subject: [PATCH] refactor: standardize string quotes and improve code readability across multiple files - Updated string quotes from single to double in useRecommendationProfile.ts, client.ts, recommendationService.ts, and recommendations.ts for consistency. - Enhanced error handling and logging in API client and recommendation service. - Improved code structure and formatting for better readability and maintainability. - Ensured consistent use of semicolons and spacing throughout the codebase. --- src/app/api/recommendations/route.ts | 170 ++++---- src/components/OnboardingModal.tsx | 480 +++++++++++----------- src/components/RecommendationsSection.tsx | 423 ++++++++++--------- src/components/RepoHubApp.tsx | 4 +- src/contexts/LocaleContext.tsx | 4 +- src/data/recommendationPresets.ts | 452 ++++++++++---------- src/hooks/useRecommendationProfile.ts | 180 ++++---- src/lib/api/client.ts | 132 +++--- src/services/recommendationService.ts | 230 ++++++----- src/types/recommendations.ts | 108 ++--- 10 files changed, 1129 insertions(+), 1054 deletions(-) diff --git a/src/app/api/recommendations/route.ts b/src/app/api/recommendations/route.ts index 356b3df..523d3d6 100644 --- a/src/app/api/recommendations/route.ts +++ b/src/app/api/recommendations/route.ts @@ -1,70 +1,82 @@ -import { NextRequest, NextResponse } from 'next/server' -import { RecommendationService } from '@/services/recommendationService' -import { RecommendationRequest } from '@/types/recommendations' +import { NextRequest, NextResponse } from "next/server"; +import { RecommendationService } from "@/services/recommendationService"; +import { RecommendationRequest } from "@/types/recommendations"; export async function POST(request: NextRequest) { try { - const body: RecommendationRequest = await request.json() + const body: RecommendationRequest = await request.json(); // Validate required fields if (!body.platform_id) { return NextResponse.json( - { error: 'platform_id is required' }, + { error: "platform_id is required" }, { status: 400 } - ) + ); } if (!body.categories || body.categories.length === 0) { return NextResponse.json( - { error: 'At least one category is required' }, + { error: "At least one category is required" }, { status: 400 } - ) + ); } // Validate platform_id - const validPlatforms = ['windows', 'macos', 'ubuntu', 'debian', 'arch', 'fedora'] + const validPlatforms = [ + "windows", + "macos", + "ubuntu", + "debian", + "arch", + "fedora", + ]; if (!validPlatforms.includes(body.platform_id)) { return NextResponse.json( - { error: `Invalid platform_id. Must be one of: ${validPlatforms.join(', ')}` }, + { + error: `Invalid platform_id. Must be one of: ${validPlatforms.join( + ", " + )}`, + }, { status: 400 } - ) + ); } // Validate categories const validCategories = [ - 'development', - 'design', - 'multimedia', - 'system-tools', - 'gaming', - 'productivity', - 'education' - ] + "development", + "design", + "multimedia", + "system-tools", + "gaming", + "productivity", + "education", + ]; const invalidCategories = body.categories.filter( - cat => !validCategories.includes(cat) - ) + (cat) => !validCategories.includes(cat) + ); if (invalidCategories.length > 0) { return NextResponse.json( - { - error: `Invalid categories: ${invalidCategories.join(', ')}`, - validCategories + { + error: `Invalid categories: ${invalidCategories.join(", ")}`, + validCategories, }, { status: 400 } - ) + ); } // Set default limit - const limit = body.limit && body.limit > 0 && body.limit <= 50 - ? body.limit - : 20 + const limit = + body.limit && body.limit > 0 && body.limit <= 50 ? body.limit : 20; // Generate recommendations - const recommendations = await RecommendationService.generateRecommendations({ - platform_id: body.platform_id, - categories: body.categories, - experienceLevel: body.experienceLevel, - limit - }) + const recommendations = await RecommendationService.generateRecommendations( + { + platform_id: body.platform_id, + categories: body.categories, + experienceLevel: body.experienceLevel, + limit, + } + ); return NextResponse.json({ recommendations, @@ -72,68 +84,82 @@ export async function POST(request: NextRequest) { userProfile: { categories: body.categories, platform: body.platform_id, - experienceLevel: body.experienceLevel - } - }) + experienceLevel: body.experienceLevel, + }, + }); } catch (error) { - console.error('Error generating recommendations:', error) + console.error("Error generating recommendations:", error); return NextResponse.json( - { - error: 'Failed to generate recommendations', - details: error instanceof Error ? error.message : 'Unknown error' + { + error: "Failed to generate recommendations", + details: error instanceof Error ? error.message : "Unknown error", }, { status: 500 } - ) + ); } } export async function GET(request: NextRequest) { try { - const { searchParams } = new URL(request.url) - const platformId = searchParams.get('platform_id') - const categoriesParam = searchParams.get('categories') - const experienceLevel = searchParams.get('experience_level') - const limit = searchParams.get('limit') + const { searchParams } = new URL(request.url); + const platformId = searchParams.get("platform_id"); + const categoriesParam = searchParams.get("categories"); + const experienceLevel = searchParams.get("experience_level"); + const limit = searchParams.get("limit"); // Validate required fields if (!platformId) { return NextResponse.json( - { error: 'platform_id query parameter is required' }, + { error: "platform_id query parameter is required" }, { status: 400 } - ) + ); } if (!categoriesParam) { return NextResponse.json( - { error: 'categories query parameter is required (comma-separated)' }, + { error: "categories query parameter is required (comma-separated)" }, { status: 400 } - ) + ); } // Parse categories - const categories = categoriesParam.split(',').map(c => c.trim()) + const categories = categoriesParam.split(",").map((c) => c.trim()); // Validate platform_id - const validPlatforms = ['windows', 'macos', 'ubuntu', 'debian', 'arch', 'fedora'] + const validPlatforms = [ + "windows", + "macos", + "ubuntu", + "debian", + "arch", + "fedora", + ]; if (!validPlatforms.includes(platformId)) { return NextResponse.json( - { error: `Invalid platform_id. Must be one of: ${validPlatforms.join(', ')}` }, + { + error: `Invalid platform_id. Must be one of: ${validPlatforms.join( + ", " + )}`, + }, { status: 400 } - ) + ); } // Set default limit - const parsedLimit = limit && parseInt(limit) > 0 && parseInt(limit) <= 50 - ? parseInt(limit) - : 20 + const parsedLimit = + limit && parseInt(limit) > 0 && parseInt(limit) <= 50 + ? parseInt(limit) + : 20; // Generate recommendations - const recommendations = await RecommendationService.generateRecommendations({ - platform_id: platformId, - categories: categories as any, - experienceLevel: experienceLevel as any, - limit: parsedLimit - }) + const recommendations = await RecommendationService.generateRecommendations( + { + platform_id: platformId, + categories: categories as any, + experienceLevel: experienceLevel as any, + limit: parsedLimit, + } + ); return NextResponse.json({ recommendations, @@ -141,17 +167,17 @@ export async function GET(request: NextRequest) { userProfile: { categories, platform: platformId, - experienceLevel - } - }) + experienceLevel, + }, + }); } catch (error) { - console.error('Error generating recommendations:', error) + console.error("Error generating recommendations:", error); return NextResponse.json( - { - error: 'Failed to generate recommendations', - details: error instanceof Error ? error.message : 'Unknown error' + { + error: "Failed to generate recommendations", + details: error instanceof Error ? error.message : "Unknown error", }, { status: 500 } - ) + ); } } diff --git a/src/components/OnboardingModal.tsx b/src/components/OnboardingModal.tsx index ed855e2..ebed585 100644 --- a/src/components/OnboardingModal.tsx +++ b/src/components/OnboardingModal.tsx @@ -10,270 +10,266 @@ import { useLocale } from '@/contexts/LocaleContext' import { RECOMMENDATION_PRESETS } from '@/data/recommendationPresets' interface OnboardingModalProps { - isOpen: boolean - onClose: () => void - onComplete: (data: { - categories: UserCategory[] - selectedOS?: string - experienceLevel: ExperienceLevel - }) => void - detectedOS: string + isOpen: boolean + onClose: () => void + onComplete: (data: { + categories: UserCategory[] + selectedOS?: string + experienceLevel: ExperienceLevel + }) => void + detectedOS: string } const PLATFORMS = [ - { id: 'windows', name: 'Windows', icon: '🪟' }, - { id: 'macos', name: 'macOS', icon: '🍎' }, - { id: 'ubuntu', name: 'Ubuntu', icon: '🐧' }, - { id: 'debian', name: 'Debian', icon: '🐧' }, - { id: 'arch', name: 'Arch Linux', icon: '🏛️' }, - { id: 'fedora', name: 'Fedora', icon: '🎩' } + { id: 'windows', name: 'Windows', icon: '🪟' }, + { id: 'macos', name: 'macOS', icon: '🍎' }, + { id: 'ubuntu', name: 'Ubuntu', icon: '🐧' }, + { id: 'debian', name: 'Debian', icon: '🐧' }, + { id: 'arch', name: 'Arch Linux', icon: '🏛️' }, + { id: 'fedora', name: 'Fedora', icon: '🎩' } ] -export function OnboardingModal({ - isOpen, - onClose, - onComplete, - detectedOS +export function OnboardingModal({ + isOpen, + onClose, + onComplete, + detectedOS }: OnboardingModalProps) { - const { t } = useLocale() - const [step, setStep] = useState(1) - const [selectedCategories, setSelectedCategories] = useState([]) - const [selectedOS, setSelectedOS] = useState(detectedOS) - const [experienceLevel, setExperienceLevel] = useState('beginner') + const { t } = useLocale() + const [step, setStep] = useState(1) + const [selectedCategories, setSelectedCategories] = useState([]) + const [selectedOS, setSelectedOS] = useState(detectedOS) + const [experienceLevel, setExperienceLevel] = useState('beginner') - if (!isOpen) return null + if (!isOpen) return null - const handleCategoryToggle = (category: UserCategory) => { - setSelectedCategories(prev => { - if (prev.includes(category)) { - return prev.filter(c => c !== category) - } - // Limit to 3 categories - if (prev.length >= 3) { - return [...prev.slice(1), category] - } - return [...prev, category] - }) - } - - const handleNext = () => { - if (step < 3) { - setStep(step + 1) + const handleCategoryToggle = (category: UserCategory) => { + setSelectedCategories(prev => { + if (prev.includes(category)) { + return prev.filter(c => c !== category) + } + // Limit to 3 categories + if (prev.length >= 3) { + return [...prev.slice(1), category] + } + return [...prev, category] + }) } - } - const handleBack = () => { - if (step > 1) { - setStep(step - 1) + const handleNext = () => { + if (step < 3) { + setStep(step + 1) + } } - } - const handleComplete = () => { - if (selectedCategories.length === 0) { - return + const handleBack = () => { + if (step > 1) { + setStep(step - 1) + } } - - onComplete({ - categories: selectedCategories, - selectedOS: selectedOS !== detectedOS ? selectedOS : undefined, - experienceLevel - }) - onClose() - } - const canProceed = () => { - if (step === 1) return selectedCategories.length > 0 - if (step === 2) return selectedOS !== 'unknown' - if (step === 3) return true - return false - } + const handleComplete = () => { + if (selectedCategories.length === 0) { + return + } - return ( -
- - - -
- - - {t('onboarding.title')} - -
- - {t('onboarding.subtitle')} - - - {/* Progress indicator */} -
- {[1, 2, 3].map(i => ( -
- ))} -
- + onComplete({ + categories: selectedCategories, + selectedOS: selectedOS !== detectedOS ? selectedOS : undefined, + experienceLevel + }) + onClose() + } - - {/* Step 1: Categories */} - {step === 1 && ( -
-
-

- {t('onboarding.step1.title')} -

-

- {t('onboarding.step1.description')} -

-
+ const canProceed = () => { + if (step === 1) return selectedCategories.length > 0 + if (step === 2) return selectedOS !== 'unknown' + if (step === 3) return true + return false + } -
- {RECOMMENDATION_PRESETS.map(preset => ( - +
+ + + {t('onboarding.title')} +
- - ))} -
+ + {t('onboarding.subtitle')} + - {selectedCategories.length > 0 && ( -

- {t('onboarding.step1.selected', { count: selectedCategories.length })} -

- )} -
- )} + {/* Progress indicator */} +
+ {[1, 2, 3].map(i => ( +
+ ))} +
+ - {/* Step 2: Operating System */} - {step === 2 && ( -
-
-

- {t('onboarding.step2.title')} -

-

- {t('onboarding.step2.description')} -

- {detectedOS !== 'unknown' && ( -

- {t('onboarding.step2.detected', { os: detectedOS })} -

- )} -
+ + {/* Step 1: Categories */} + {step === 1 && ( +
+
+

+ {t('onboarding.step1.title')} +

+

+ {t('onboarding.step1.description')} +

+
-
- {PLATFORMS.map(platform => ( - - ))} -
-
- )} +
+ {RECOMMENDATION_PRESETS.map(preset => ( + + ))} +
- {/* Step 3: Experience Level */} - {step === 3 && ( -
-
-

- {t('onboarding.step3.title')} -

-

- {t('onboarding.step3.description')} -

-
+ {selectedCategories.length > 0 && ( +

+ {t('onboarding.step1.selected', { count: selectedCategories.length })} +

+ )} +
+ )} -
- {(['beginner', 'intermediate', 'advanced'] as ExperienceLevel[]).map(level => ( - - ))} -
-
- )} + {/* Step 2: Operating System */} + {step === 2 && ( +
+
+

+ {t('onboarding.step2.title')} +

+

+ {t('onboarding.step2.description')} +

+ {detectedOS !== 'unknown' && ( +

+ {t('onboarding.step2.detected', { os: detectedOS })} +

+ )} +
- {/* Navigation Buttons */} -
- {step > 1 && ( - - )} - - {step < 3 ? ( - - ) : ( - - )} -
- - -
- ) +
+ {PLATFORMS.map(platform => ( + + ))} +
+
+ )} + + {/* Step 3: Experience Level */} + {step === 3 && ( +
+
+

+ {t('onboarding.step3.title')} +

+

+ {t('onboarding.step3.description')} +

+
+ +
+ {(['beginner', 'intermediate', 'advanced'] as ExperienceLevel[]).map(level => ( + + ))} +
+
+ )} + + {/* Navigation Buttons */} +
+ {step > 1 && ( + + )} + + {step < 3 ? ( + + ) : ( + + )} +
+
+ +
+ ) } diff --git a/src/components/RecommendationsSection.tsx b/src/components/RecommendationsSection.tsx index 039f169..5c6fe70 100644 --- a/src/components/RecommendationsSection.tsx +++ b/src/components/RecommendationsSection.tsx @@ -10,232 +10,231 @@ import { useLocale } from '@/contexts/LocaleContext' import { useRecommendationProfile } from '@/hooks/useRecommendationProfile' interface RecommendationsSectionProps { - onPackageToggle: (pkg: Package) => void - selectedPackages: Package[] - onCustomizeClick: () => void + onPackageToggle: (pkg: Package) => void + selectedPackages: Package[] + onCustomizeClick: () => void } export function RecommendationsSection({ - onPackageToggle, - selectedPackages, - onCustomizeClick + onPackageToggle, + selectedPackages, + onCustomizeClick }: RecommendationsSectionProps) { - const { t } = useLocale() - const { profile, getEffectiveOS, isProfileComplete } = useRecommendationProfile() - const [recommendations, setRecommendations] = useState([]) - const [loading, setLoading] = useState(false) - const [error, setError] = useState(null) + const { t } = useLocale() + const { profile, getEffectiveOS, isProfileComplete } = useRecommendationProfile() + const [recommendations, setRecommendations] = useState([]) + const [loading, setLoading] = useState(false) + const [error, setError] = useState(null) + + const fetchRecommendations = async () => { + if (!isProfileComplete()) { + return + } + + setLoading(true) + setError(null) + + try { + const response = await fetch('/api/recommendations', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + platform_id: getEffectiveOS(), + categories: profile.categories, + experienceLevel: profile.experienceLevel, + limit: 12 + }) + }) + + if (!response.ok) { + throw new Error('Failed to fetch recommendations') + } + + const data = await response.json() + setRecommendations(data.recommendations || []) + } catch (err) { + console.error('Error fetching recommendations:', err) + setError(err instanceof Error ? err.message : 'Unknown error') + } finally { + setLoading(false) + } + } + + // Fetch recommendations on mount and when profile changes + useEffect(() => { + if (isProfileComplete()) { + fetchRecommendations() + } + }, [profile.categories, profile.selectedOS, profile.experienceLevel]) + + const isPackageSelected = (pkg: RecommendedPackage) => { + return selectedPackages.some(selected => selected.id === pkg.id) + } - const fetchRecommendations = async () => { if (!isProfileComplete()) { - return + return null } - setLoading(true) - setError(null) + return ( + + +
+
+ +
+ + {t('recommendations.title')} + + + {t('recommendations.subtitle')} + +
+
+
+ + +
+
- try { - const response = await fetch('/api/recommendations', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - platform_id: getEffectiveOS(), - categories: profile.categories, - experienceLevel: profile.experienceLevel, - limit: 12 - }) - }) - - if (!response.ok) { - throw new Error('Failed to fetch recommendations') - } - - const data = await response.json() - setRecommendations(data.recommendations || []) - } catch (err) { - console.error('Error fetching recommendations:', err) - setError(err instanceof Error ? err.message : 'Unknown error') - } finally { - setLoading(false) - } - } - - // Fetch recommendations on mount and when profile changes - useEffect(() => { - if (isProfileComplete()) { - fetchRecommendations() - } - }, [profile.categories, profile.selectedOS, profile.experienceLevel]) - - const isPackageSelected = (pkg: RecommendedPackage) => { - return selectedPackages.some(selected => selected.id === pkg.id) - } - - if (!isProfileComplete()) { - return null - } - - return ( - - -
-
- -
- - {t('recommendations.title')} - - - {t('recommendations.subtitle')} - -
-
-
- - -
-
- - {/* Show user profile info */} -
- - {getEffectiveOS()} - - {profile.categories.map(cat => ( - - {t(`categories.${cat}.name`)} - - ))} -
-
- - - {loading && ( -
- -

{t('recommendations.loading')}

-
- )} - - {error && ( -
-

{error}

- -
- )} - - {!loading && !error && recommendations.length === 0 && ( -
- -

- {t('recommendations.no_recommendations')} -

- -
- )} - - {!loading && !error && recommendations.length > 0 && ( -
- {recommendations.map(pkg => ( - onPackageToggle(pkg)} - > - {pkg.presetMatch && ( -
- - - {t('recommendations.preset_badge')} + {/* Show user profile info */} +
+ + {getEffectiveOS()} -
+ {profile.categories.map(cat => ( + + {t(`categories.${cat}.name`)} + + ))} +
+ + + + {loading && ( +
+ +

{t('recommendations.loading')}

+
)} - -
- -
-

{pkg.name}

-

{pkg.version}

+ {error && ( +
+

{error}

+
-
+ )} -

- {pkg.description} -

- - {/* Recommendation score and reason */} -
-
- - {t('recommendations.score')} - -
-
-
-
- {pkg.recommendationScore}% -
-
- - {pkg.recommendationReason && ( -
-

- {t('recommendations.reason')} - {' '} - {pkg.recommendationReason} + {!loading && !error && recommendations.length === 0 && ( +

+ +

+ {t('recommendations.no_recommendations')}

-
- )} -
+ +
+ )} - - - - ))} -
- )} - - - ) + {!loading && !error && recommendations.length > 0 && ( +
+ {recommendations.map(pkg => ( + onPackageToggle(pkg)} + > + {pkg.presetMatch && ( +
+ + + {t('recommendations.preset_badge')} + +
+ )} + + +
+ +
+

{pkg.name}

+

{pkg.version}

+
+
+ +

+ {pkg.description} +

+ + {/* Recommendation score and reason */} +
+
+ + {t('recommendations.score')} + +
+
+
+
+ {pkg.recommendationScore}% +
+
+ + {pkg.recommendationReason && ( +
+

+ {t('recommendations.reason')} + {' '} + {pkg.recommendationReason} +

+
+ )} +
+ + + + + ))} +
+ )} +
+
+ ) } diff --git a/src/components/RepoHubApp.tsx b/src/components/RepoHubApp.tsx index 95c8026..85b9fa3 100644 --- a/src/components/RepoHubApp.tsx +++ b/src/components/RepoHubApp.tsx @@ -20,7 +20,7 @@ function RepoHubAppContent({ cryptomusEnabled }: { cryptomusEnabled: boolean }) const [selectedPlatform, setSelectedPlatform] = useState(null) const [selectedPackages, setSelectedPackages] = useState([]) const [generatedScript, setGeneratedScript] = useState(null) - + // Recommendation profile management const { profile, @@ -30,7 +30,7 @@ function RepoHubAppContent({ cryptomusEnabled }: { cryptomusEnabled: boolean }) saveProfile, detectedOS } = useRecommendationProfile() - + const [showOnboarding, setShowOnboarding] = useState(false) // Show onboarding modal on first visit diff --git a/src/contexts/LocaleContext.tsx b/src/contexts/LocaleContext.tsx index a55736d..937a6ba 100644 --- a/src/contexts/LocaleContext.tsx +++ b/src/contexts/LocaleContext.tsx @@ -396,7 +396,7 @@ export function LocaleProvider({ children }: { children: ReactNode }) { if (browserLang.startsWith('tr')) { return 'tr' } - + return 'en' } @@ -415,7 +415,7 @@ export function LocaleProvider({ children }: { children: ReactNode }) { const t = (key: string, params?: Record) => { const keys = key.split('.') let value: any = translations[locale] - + for (const k of keys) { value = value?.[k] } diff --git a/src/data/recommendationPresets.ts b/src/data/recommendationPresets.ts index bfa745e..fb36e7b 100644 --- a/src/data/recommendationPresets.ts +++ b/src/data/recommendationPresets.ts @@ -1,4 +1,4 @@ -import { CategoryPreset } from '@/types/recommendations' +import { CategoryPreset } from "@/types/recommendations"; /** * Curated package recommendations for each user category @@ -6,364 +6,366 @@ import { CategoryPreset } from '@/types/recommendations' */ export const RECOMMENDATION_PRESETS: CategoryPreset[] = [ { - category: 'development', - description: 'Essential tools for software development', - icon: '💻', + category: "development", + description: "Essential tools for software development", + icon: "💻", packages: [ { - packageName: 'git', - platforms: ['windows', 'macos', 'ubuntu', 'debian', 'arch', 'fedora'], + packageName: "git", + platforms: ["windows", "macos", "ubuntu", "debian", "arch", "fedora"], priority: 10, - reason: 'Version control system essential for all developers', - experienceLevel: ['beginner', 'intermediate', 'advanced'] + reason: "Version control system essential for all developers", + experienceLevel: ["beginner", "intermediate", "advanced"], }, { - packageName: 'code', - platforms: ['windows', 'macos', 'ubuntu', 'debian'], + packageName: "code", + platforms: ["windows", "macos", "ubuntu", "debian"], priority: 9, - reason: 'Visual Studio Code - Popular code editor', - experienceLevel: ['beginner', 'intermediate', 'advanced'] + reason: "Visual Studio Code - Popular code editor", + experienceLevel: ["beginner", "intermediate", "advanced"], }, { - packageName: 'visual-studio-code', - platforms: ['arch', 'fedora'], + packageName: "visual-studio-code", + platforms: ["arch", "fedora"], priority: 9, - reason: 'Visual Studio Code - Popular code editor', - experienceLevel: ['beginner', 'intermediate', 'advanced'] + reason: "Visual Studio Code - Popular code editor", + experienceLevel: ["beginner", "intermediate", "advanced"], }, { - packageName: 'nodejs', - platforms: ['windows', 'macos', 'ubuntu', 'debian', 'arch', 'fedora'], + packageName: "nodejs", + platforms: ["windows", "macos", "ubuntu", "debian", "arch", "fedora"], priority: 8, - reason: 'JavaScript runtime for modern web development', - experienceLevel: ['beginner', 'intermediate', 'advanced'] + reason: "JavaScript runtime for modern web development", + experienceLevel: ["beginner", "intermediate", "advanced"], }, { - packageName: 'python3', - platforms: ['ubuntu', 'debian', 'arch', 'fedora'], + packageName: "python3", + platforms: ["ubuntu", "debian", "arch", "fedora"], priority: 8, - reason: 'Python programming language', - experienceLevel: ['beginner', 'intermediate', 'advanced'] + reason: "Python programming language", + experienceLevel: ["beginner", "intermediate", "advanced"], }, { - packageName: 'python', - platforms: ['windows', 'macos'], + packageName: "python", + platforms: ["windows", "macos"], priority: 8, - reason: 'Python programming language', - experienceLevel: ['beginner', 'intermediate', 'advanced'] + reason: "Python programming language", + experienceLevel: ["beginner", "intermediate", "advanced"], }, { - packageName: 'docker', - platforms: ['windows', 'macos', 'ubuntu', 'debian', 'arch', 'fedora'], + packageName: "docker", + platforms: ["windows", "macos", "ubuntu", "debian", "arch", "fedora"], priority: 7, - reason: 'Containerization platform for development', - experienceLevel: ['intermediate', 'advanced'] + reason: "Containerization platform for development", + experienceLevel: ["intermediate", "advanced"], }, { - packageName: 'curl', - platforms: ['ubuntu', 'debian', 'arch', 'fedora', 'macos'], + packageName: "curl", + platforms: ["ubuntu", "debian", "arch", "fedora", "macos"], priority: 7, - reason: 'Command-line tool for transferring data', - experienceLevel: ['intermediate', 'advanced'] + reason: "Command-line tool for transferring data", + experienceLevel: ["intermediate", "advanced"], }, { - packageName: 'vim', - platforms: ['ubuntu', 'debian', 'arch', 'fedora', 'macos'], + packageName: "vim", + platforms: ["ubuntu", "debian", "arch", "fedora", "macos"], priority: 6, - reason: 'Powerful text editor', - experienceLevel: ['intermediate', 'advanced'] + reason: "Powerful text editor", + experienceLevel: ["intermediate", "advanced"], }, { - packageName: 'postman', - platforms: ['windows', 'macos', 'ubuntu', 'debian'], + packageName: "postman", + platforms: ["windows", "macos", "ubuntu", "debian"], priority: 6, - reason: 'API development and testing tool', - experienceLevel: ['beginner', 'intermediate', 'advanced'] - } - ] + reason: "API development and testing tool", + experienceLevel: ["beginner", "intermediate", "advanced"], + }, + ], }, { - category: 'design', - description: 'Tools for graphic design, UI/UX, and creative work', - icon: '🎨', + category: "design", + description: "Tools for graphic design, UI/UX, and creative work", + icon: "🎨", packages: [ { - packageName: 'gimp', - platforms: ['windows', 'macos', 'ubuntu', 'debian', 'arch', 'fedora'], + packageName: "gimp", + platforms: ["windows", "macos", "ubuntu", "debian", "arch", "fedora"], priority: 9, - reason: 'Free and open-source image editor', - experienceLevel: ['beginner', 'intermediate', 'advanced'] + reason: "Free and open-source image editor", + experienceLevel: ["beginner", "intermediate", "advanced"], }, { - packageName: 'inkscape', - platforms: ['windows', 'macos', 'ubuntu', 'debian', 'arch', 'fedora'], + packageName: "inkscape", + platforms: ["windows", "macos", "ubuntu", "debian", "arch", "fedora"], priority: 8, - reason: 'Professional vector graphics editor', - experienceLevel: ['beginner', 'intermediate', 'advanced'] + reason: "Professional vector graphics editor", + experienceLevel: ["beginner", "intermediate", "advanced"], }, { - packageName: 'blender', - platforms: ['windows', 'macos', 'ubuntu', 'debian', 'arch', 'fedora'], + packageName: "blender", + platforms: ["windows", "macos", "ubuntu", "debian", "arch", "fedora"], priority: 8, - reason: '3D creation suite', - experienceLevel: ['intermediate', 'advanced'] + reason: "3D creation suite", + experienceLevel: ["intermediate", "advanced"], }, { - packageName: 'figma', - platforms: ['windows', 'macos'], + packageName: "figma", + platforms: ["windows", "macos"], priority: 9, - reason: 'Collaborative interface design tool', - experienceLevel: ['beginner', 'intermediate', 'advanced'] + reason: "Collaborative interface design tool", + experienceLevel: ["beginner", "intermediate", "advanced"], }, { - packageName: 'krita', - platforms: ['windows', 'macos', 'ubuntu', 'debian', 'arch', 'fedora'], + packageName: "krita", + platforms: ["windows", "macos", "ubuntu", "debian", "arch", "fedora"], priority: 7, - reason: 'Digital painting application', - experienceLevel: ['beginner', 'intermediate', 'advanced'] - } - ] + reason: "Digital painting application", + experienceLevel: ["beginner", "intermediate", "advanced"], + }, + ], }, { - category: 'multimedia', - description: 'Audio, video editing and media management tools', - icon: '🎬', + category: "multimedia", + description: "Audio, video editing and media management tools", + icon: "🎬", packages: [ { - packageName: 'vlc', - platforms: ['windows', 'macos', 'ubuntu', 'debian', 'arch', 'fedora'], + packageName: "vlc", + platforms: ["windows", "macos", "ubuntu", "debian", "arch", "fedora"], priority: 10, - reason: 'Versatile media player', - experienceLevel: ['beginner', 'intermediate', 'advanced'] + reason: "Versatile media player", + experienceLevel: ["beginner", "intermediate", "advanced"], }, { - packageName: 'obs-studio', - platforms: ['windows', 'macos', 'ubuntu', 'debian', 'arch', 'fedora'], + packageName: "obs-studio", + platforms: ["windows", "macos", "ubuntu", "debian", "arch", "fedora"], priority: 9, - reason: 'Video recording and live streaming', - experienceLevel: ['intermediate', 'advanced'] + reason: "Video recording and live streaming", + experienceLevel: ["intermediate", "advanced"], }, { - packageName: 'audacity', - platforms: ['windows', 'macos', 'ubuntu', 'debian', 'arch', 'fedora'], + packageName: "audacity", + platforms: ["windows", "macos", "ubuntu", "debian", "arch", "fedora"], priority: 8, - reason: 'Audio editing software', - experienceLevel: ['beginner', 'intermediate', 'advanced'] + reason: "Audio editing software", + experienceLevel: ["beginner", "intermediate", "advanced"], }, { - packageName: 'ffmpeg', - platforms: ['ubuntu', 'debian', 'arch', 'fedora', 'macos'], + packageName: "ffmpeg", + platforms: ["ubuntu", "debian", "arch", "fedora", "macos"], priority: 8, - reason: 'Complete multimedia framework', - experienceLevel: ['intermediate', 'advanced'] + reason: "Complete multimedia framework", + experienceLevel: ["intermediate", "advanced"], }, { - packageName: 'handbrake', - platforms: ['windows', 'macos', 'ubuntu', 'debian', 'arch', 'fedora'], + packageName: "handbrake", + platforms: ["windows", "macos", "ubuntu", "debian", "arch", "fedora"], priority: 7, - reason: 'Video transcoder', - experienceLevel: ['beginner', 'intermediate', 'advanced'] + reason: "Video transcoder", + experienceLevel: ["beginner", "intermediate", "advanced"], }, { - packageName: 'kdenlive', - platforms: ['ubuntu', 'debian', 'arch', 'fedora'], + packageName: "kdenlive", + platforms: ["ubuntu", "debian", "arch", "fedora"], priority: 7, - reason: 'Video editing software', - experienceLevel: ['intermediate', 'advanced'] - } - ] + reason: "Video editing software", + experienceLevel: ["intermediate", "advanced"], + }, + ], }, { - category: 'system-tools', - description: 'System administration, security and utilities', - icon: '⚙️', + category: "system-tools", + description: "System administration, security and utilities", + icon: "⚙️", packages: [ { - packageName: 'htop', - platforms: ['ubuntu', 'debian', 'arch', 'fedora', 'macos'], + packageName: "htop", + platforms: ["ubuntu", "debian", "arch", "fedora", "macos"], priority: 9, - reason: 'Interactive process viewer', - experienceLevel: ['beginner', 'intermediate', 'advanced'] + reason: "Interactive process viewer", + experienceLevel: ["beginner", "intermediate", "advanced"], }, { - packageName: 'tmux', - platforms: ['ubuntu', 'debian', 'arch', 'fedora', 'macos'], + packageName: "tmux", + platforms: ["ubuntu", "debian", "arch", "fedora", "macos"], priority: 8, - reason: 'Terminal multiplexer', - experienceLevel: ['intermediate', 'advanced'] + reason: "Terminal multiplexer", + experienceLevel: ["intermediate", "advanced"], }, { - packageName: 'wget', - platforms: ['ubuntu', 'debian', 'arch', 'fedora', 'macos'], + packageName: "wget", + platforms: ["ubuntu", "debian", "arch", "fedora", "macos"], priority: 8, - reason: 'Network downloader', - experienceLevel: ['beginner', 'intermediate', 'advanced'] + reason: "Network downloader", + experienceLevel: ["beginner", "intermediate", "advanced"], }, { - packageName: 'neofetch', - platforms: ['ubuntu', 'debian', 'arch', 'fedora', 'macos'], + packageName: "neofetch", + platforms: ["ubuntu", "debian", "arch", "fedora", "macos"], priority: 6, - reason: 'System information tool', - experienceLevel: ['beginner', 'intermediate', 'advanced'] + reason: "System information tool", + experienceLevel: ["beginner", "intermediate", "advanced"], }, { - packageName: 'wireshark', - platforms: ['windows', 'macos', 'ubuntu', 'debian', 'arch', 'fedora'], + packageName: "wireshark", + platforms: ["windows", "macos", "ubuntu", "debian", "arch", "fedora"], priority: 7, - reason: 'Network protocol analyzer', - experienceLevel: ['advanced'] + reason: "Network protocol analyzer", + experienceLevel: ["advanced"], }, { - packageName: 'gparted', - platforms: ['ubuntu', 'debian', 'arch', 'fedora'], + packageName: "gparted", + platforms: ["ubuntu", "debian", "arch", "fedora"], priority: 6, - reason: 'Partition editor', - experienceLevel: ['intermediate', 'advanced'] - } - ] + reason: "Partition editor", + experienceLevel: ["intermediate", "advanced"], + }, + ], }, { - category: 'gaming', - description: 'Gaming platforms and related tools', - icon: '🎮', + category: "gaming", + description: "Gaming platforms and related tools", + icon: "🎮", packages: [ { - packageName: 'steam', - platforms: ['windows', 'macos', 'ubuntu', 'debian', 'arch', 'fedora'], + packageName: "steam", + platforms: ["windows", "macos", "ubuntu", "debian", "arch", "fedora"], priority: 10, - reason: 'Gaming platform', - experienceLevel: ['beginner', 'intermediate', 'advanced'] + reason: "Gaming platform", + experienceLevel: ["beginner", "intermediate", "advanced"], }, { - packageName: 'discord', - platforms: ['windows', 'macos', 'ubuntu', 'debian', 'arch', 'fedora'], + packageName: "discord", + platforms: ["windows", "macos", "ubuntu", "debian", "arch", "fedora"], priority: 9, - reason: 'Voice and chat for gamers', - experienceLevel: ['beginner', 'intermediate', 'advanced'] + reason: "Voice and chat for gamers", + experienceLevel: ["beginner", "intermediate", "advanced"], }, { - packageName: 'lutris', - platforms: ['ubuntu', 'debian', 'arch', 'fedora'], + packageName: "lutris", + platforms: ["ubuntu", "debian", "arch", "fedora"], priority: 7, - reason: 'Open gaming platform', - experienceLevel: ['intermediate', 'advanced'] + reason: "Open gaming platform", + experienceLevel: ["intermediate", "advanced"], }, { - packageName: 'wine', - platforms: ['ubuntu', 'debian', 'arch', 'fedora', 'macos'], + packageName: "wine", + platforms: ["ubuntu", "debian", "arch", "fedora", "macos"], priority: 6, - reason: 'Windows compatibility layer', - experienceLevel: ['advanced'] - } - ] + reason: "Windows compatibility layer", + experienceLevel: ["advanced"], + }, + ], }, { - category: 'productivity', - description: 'Office, note-taking and productivity tools', - icon: '📝', + category: "productivity", + description: "Office, note-taking and productivity tools", + icon: "📝", packages: [ { - packageName: 'libreoffice', - platforms: ['windows', 'macos', 'ubuntu', 'debian', 'arch', 'fedora'], + packageName: "libreoffice", + platforms: ["windows", "macos", "ubuntu", "debian", "arch", "fedora"], priority: 10, - reason: 'Free office suite', - experienceLevel: ['beginner', 'intermediate', 'advanced'] + reason: "Free office suite", + experienceLevel: ["beginner", "intermediate", "advanced"], }, { - packageName: 'thunderbird', - platforms: ['windows', 'macos', 'ubuntu', 'debian', 'arch', 'fedora'], + packageName: "thunderbird", + platforms: ["windows", "macos", "ubuntu", "debian", "arch", "fedora"], priority: 8, - reason: 'Email client', - experienceLevel: ['beginner', 'intermediate', 'advanced'] + reason: "Email client", + experienceLevel: ["beginner", "intermediate", "advanced"], }, { - packageName: 'notion', - platforms: ['windows', 'macos'], + packageName: "notion", + platforms: ["windows", "macos"], priority: 9, - reason: 'All-in-one workspace', - experienceLevel: ['beginner', 'intermediate', 'advanced'] + reason: "All-in-one workspace", + experienceLevel: ["beginner", "intermediate", "advanced"], }, { - packageName: 'obsidian', - platforms: ['windows', 'macos', 'ubuntu', 'debian'], + packageName: "obsidian", + platforms: ["windows", "macos", "ubuntu", "debian"], priority: 8, - reason: 'Knowledge base and note-taking', - experienceLevel: ['intermediate', 'advanced'] + reason: "Knowledge base and note-taking", + experienceLevel: ["intermediate", "advanced"], }, { - packageName: 'keepassxc', - platforms: ['windows', 'macos', 'ubuntu', 'debian', 'arch', 'fedora'], + packageName: "keepassxc", + platforms: ["windows", "macos", "ubuntu", "debian", "arch", "fedora"], priority: 7, - reason: 'Password manager', - experienceLevel: ['beginner', 'intermediate', 'advanced'] - } - ] + reason: "Password manager", + experienceLevel: ["beginner", "intermediate", "advanced"], + }, + ], }, { - category: 'education', - description: 'Educational and scientific software', - icon: '🎓', + category: "education", + description: "Educational and scientific software", + icon: "🎓", packages: [ { - packageName: 'anki', - platforms: ['windows', 'macos', 'ubuntu', 'debian', 'arch', 'fedora'], + packageName: "anki", + platforms: ["windows", "macos", "ubuntu", "debian", "arch", "fedora"], priority: 9, - reason: 'Flashcard application for learning', - experienceLevel: ['beginner', 'intermediate', 'advanced'] + reason: "Flashcard application for learning", + experienceLevel: ["beginner", "intermediate", "advanced"], }, { - packageName: 'stellarium', - platforms: ['windows', 'macos', 'ubuntu', 'debian', 'arch', 'fedora'], + packageName: "stellarium", + platforms: ["windows", "macos", "ubuntu", "debian", "arch", "fedora"], priority: 7, - reason: 'Planetarium software', - experienceLevel: ['beginner', 'intermediate', 'advanced'] + reason: "Planetarium software", + experienceLevel: ["beginner", "intermediate", "advanced"], }, { - packageName: 'octave', - platforms: ['windows', 'macos', 'ubuntu', 'debian', 'arch', 'fedora'], + packageName: "octave", + platforms: ["windows", "macos", "ubuntu", "debian", "arch", "fedora"], priority: 7, - reason: 'Scientific programming language', - experienceLevel: ['intermediate', 'advanced'] + reason: "Scientific programming language", + experienceLevel: ["intermediate", "advanced"], }, { - packageName: 'geogebra', - platforms: ['windows', 'macos', 'ubuntu', 'debian'], + packageName: "geogebra", + platforms: ["windows", "macos", "ubuntu", "debian"], priority: 8, - reason: 'Interactive mathematics software', - experienceLevel: ['beginner', 'intermediate', 'advanced'] - } - ] - } -] + reason: "Interactive mathematics software", + experienceLevel: ["beginner", "intermediate", "advanced"], + }, + ], + }, +]; /** * Get presets for specific categories */ -export function getPresetsForCategories(categories: string[]): CategoryPreset[] { - return RECOMMENDATION_PRESETS.filter(preset => +export function getPresetsForCategories( + categories: string[] +): CategoryPreset[] { + return RECOMMENDATION_PRESETS.filter((preset) => categories.includes(preset.category) - ) + ); } /** * Get all package names from presets for a specific platform */ export function getPresetPackageNames( - categories: string[], + categories: string[], platformId: string ): string[] { - const presets = getPresetsForCategories(categories) - const packageNames = new Set() - - presets.forEach(preset => { - preset.packages.forEach(pkg => { + const presets = getPresetsForCategories(categories); + const packageNames = new Set(); + + presets.forEach((preset) => { + preset.packages.forEach((pkg) => { if (pkg.platforms.includes(platformId)) { - packageNames.add(pkg.packageName) + packageNames.add(pkg.packageName); } - }) - }) - - return Array.from(packageNames) + }); + }); + + return Array.from(packageNames); } /** @@ -374,18 +376,18 @@ export function getPresetPriority( categories: string[], platformId: string ): number | null { - const presets = getPresetsForCategories(categories) - + const presets = getPresetsForCategories(categories); + for (const preset of presets) { const pkg = preset.packages.find( - p => p.packageName === packageName && p.platforms.includes(platformId) - ) + (p) => p.packageName === packageName && p.platforms.includes(platformId) + ); if (pkg) { - return pkg.priority + return pkg.priority; } } - - return null + + return null; } /** @@ -395,14 +397,14 @@ export function getRecommendationReason( packageName: string, categories: string[] ): string | null { - const presets = getPresetsForCategories(categories) - + const presets = getPresetsForCategories(categories); + for (const preset of presets) { - const pkg = preset.packages.find(p => p.packageName === packageName) + const pkg = preset.packages.find((p) => p.packageName === packageName); if (pkg) { - return pkg.reason + return pkg.reason; } } - - return null + + return null; } diff --git a/src/hooks/useRecommendationProfile.ts b/src/hooks/useRecommendationProfile.ts index 62901d6..b0e2797 100644 --- a/src/hooks/useRecommendationProfile.ts +++ b/src/hooks/useRecommendationProfile.ts @@ -1,56 +1,60 @@ -"use client" +"use client"; -import { useState, useEffect, useCallback } from 'react' -import { UserProfile, UserCategory, ExperienceLevel } from '@/types/recommendations' +import { useState, useEffect, useCallback } from "react"; +import { + UserProfile, + UserCategory, + ExperienceLevel, +} from "@/types/recommendations"; -const STORAGE_KEY = 'repohub_user_profile' +const STORAGE_KEY = "repohub_user_profile"; /** * Detect user's operating system from browser */ function detectOS(): string { - if (typeof window === 'undefined') { - return 'unknown' + if (typeof window === "undefined") { + return "unknown"; } - const userAgent = window.navigator.userAgent.toLowerCase() - const platform = window.navigator.platform.toLowerCase() + const userAgent = window.navigator.userAgent.toLowerCase(); + const platform = window.navigator.platform.toLowerCase(); // Windows - if (userAgent.indexOf('win') !== -1 || platform.indexOf('win') !== -1) { - return '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 + userAgent.indexOf("mac") !== -1 || + platform.indexOf("mac") !== -1 || + userAgent.indexOf("darwin") !== -1 ) { - return 'macos' + return "macos"; } // Linux distros - if (userAgent.indexOf('linux') !== -1 || platform.indexOf('linux') !== -1) { + 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("ubuntu") !== -1) { + return "ubuntu"; } - if (userAgent.indexOf('fedora') !== -1) { - return 'fedora' + if (userAgent.indexOf("fedora") !== -1) { + return "fedora"; } - if (userAgent.indexOf('arch') !== -1) { - return 'arch' + if (userAgent.indexOf("arch") !== -1) { + return "arch"; } - if (userAgent.indexOf('debian') !== -1) { - return 'debian' + if (userAgent.indexOf("debian") !== -1) { + return "debian"; } - + // Default to Ubuntu for generic Linux - return 'ubuntu' + return "ubuntu"; } - return 'unknown' + return "unknown"; } /** @@ -61,106 +65,118 @@ function getDefaultProfile(): UserProfile { categories: [], detectedOS: detectOS(), selectedOS: undefined, - experienceLevel: 'beginner', + experienceLevel: "beginner", hasCompletedOnboarding: false, createdAt: new Date().toISOString(), - lastUpdated: new Date().toISOString() - } + lastUpdated: new Date().toISOString(), + }; } /** * Hook for managing user recommendation profile in localStorage */ export function useRecommendationProfile() { - const [profile, setProfile] = useState(getDefaultProfile()) - const [isLoading, setIsLoading] = useState(true) + const [profile, setProfile] = useState(getDefaultProfile()); + const [isLoading, setIsLoading] = useState(true); // Load profile from localStorage on mount useEffect(() => { try { - const stored = localStorage.getItem(STORAGE_KEY) + const stored = localStorage.getItem(STORAGE_KEY); if (stored) { - const parsed = JSON.parse(stored) as UserProfile - + const parsed = JSON.parse(stored) as UserProfile; + // Update detectedOS if it changed - const currentOS = detectOS() + const currentOS = detectOS(); if (parsed.detectedOS !== currentOS) { - parsed.detectedOS = currentOS + parsed.detectedOS = currentOS; } - - setProfile(parsed) + + setProfile(parsed); } else { // First time user - save default profile - const defaultProfile = getDefaultProfile() - setProfile(defaultProfile) - localStorage.setItem(STORAGE_KEY, JSON.stringify(defaultProfile)) + const defaultProfile = getDefaultProfile(); + setProfile(defaultProfile); + localStorage.setItem(STORAGE_KEY, JSON.stringify(defaultProfile)); } } catch (error) { - console.error('Error loading user profile:', error) + console.error("Error loading user profile:", error); } finally { - setIsLoading(false) + setIsLoading(false); } - }, []) + }, []); // Save profile to localStorage - const saveProfile = useCallback((newProfile: Partial) => { - try { - const updated: UserProfile = { - ...profile, - ...newProfile, - lastUpdated: new Date().toISOString() + const saveProfile = useCallback( + (newProfile: Partial) => { + 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; } - setProfile(updated) - localStorage.setItem(STORAGE_KEY, JSON.stringify(updated)) - return true - } catch (error) { - console.error('Error saving user profile:', error) - return false - } - }, [profile]) + }, + [profile] + ); // Update categories - const updateCategories = useCallback((categories: UserCategory[]) => { - return saveProfile({ categories }) - }, [saveProfile]) + const updateCategories = useCallback( + (categories: UserCategory[]) => { + return saveProfile({ categories }); + }, + [saveProfile] + ); // Update selected OS (manual override) - const updateSelectedOS = useCallback((os: string) => { - return saveProfile({ selectedOS: os }) - }, [saveProfile]) + const updateSelectedOS = useCallback( + (os: string) => { + return saveProfile({ selectedOS: os }); + }, + [saveProfile] + ); // Update experience level - const updateExperienceLevel = useCallback((level: ExperienceLevel) => { - return saveProfile({ experienceLevel: level }) - }, [saveProfile]) + const updateExperienceLevel = useCallback( + (level: ExperienceLevel) => { + return saveProfile({ experienceLevel: level }); + }, + [saveProfile] + ); // Mark onboarding as completed const completeOnboarding = useCallback(() => { - return saveProfile({ hasCompletedOnboarding: true }) - }, [saveProfile]) + 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 + 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 + 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]) + 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.categories.length > 0 && getEffectiveOS() !== "unknown"; + }, [profile, getEffectiveOS]); return { profile, @@ -174,6 +190,6 @@ export function useRecommendationProfile() { getEffectiveOS, isProfileComplete, detectedOS: profile.detectedOS, - hasCompletedOnboarding: profile.hasCompletedOnboarding - } + hasCompletedOnboarding: profile.hasCompletedOnboarding, + }; } diff --git a/src/lib/api/client.ts b/src/lib/api/client.ts index 9c11a54..f2493c1 100644 --- a/src/lib/api/client.ts +++ b/src/lib/api/client.ts @@ -1,101 +1,127 @@ -import { Platform, Package, FilterOptions } from '@/types' -import { RecommendationRequest, RecommendationResponse } from '@/types/recommendations' +import { Platform, Package, FilterOptions } from "@/types"; +import { + RecommendationRequest, + RecommendationResponse, +} from "@/types/recommendations"; -const API_BASE_URL = (process.env.NEXT_PUBLIC_API_URL && process.env.NEXT_PUBLIC_API_URL.trim() !== '') - ? process.env.NEXT_PUBLIC_API_URL.replace(/\/$/, '') - : '/api' +const API_BASE_URL = + process.env.NEXT_PUBLIC_API_URL && + process.env.NEXT_PUBLIC_API_URL.trim() !== "" + ? process.env.NEXT_PUBLIC_API_URL.replace(/\/$/, "") + : "/api"; class ApiClient { - private async request(endpoint: string, options: RequestInit = {}): Promise { - const url = `${API_BASE_URL}${endpoint}` - + private async request( + endpoint: string, + options: RequestInit = {} + ): Promise { + const url = `${API_BASE_URL}${endpoint}`; + const response = await fetch(url, { headers: { - 'Content-Type': 'application/json', + "Content-Type": "application/json", ...options.headers, }, ...options, - }) + }); if (!response.ok) { - throw new Error(`API request failed: ${response.statusText}`) + throw new Error(`API request failed: ${response.statusText}`); } - return response.json() + return response.json(); } // Platform operations async getPlatforms(): Promise { - return this.request('/platforms') + return this.request("/platforms"); } async getPlatform(id: string): Promise { try { - return await this.request(`/platforms/${id}`) + return await this.request(`/platforms/${id}`); } catch (error) { - return null + return null; } } // Package operations - async getPackages(filters: FilterOptions = {}): Promise<{ packages: Package[], total: number }> { - const params = new URLSearchParams() - - if (filters.platform_id) params.append('platform_id', filters.platform_id) - if (filters.category_id) params.append('category_id', filters.category_id.toString()) - if (filters.type) params.append('type', filters.type) - if (filters.repository) params.append('repository', filters.repository) - if (filters.search) params.append('search', filters.search) - if (filters.limit) params.append('limit', filters.limit.toString()) - if (filters.offset) params.append('offset', filters.offset.toString()) - if (filters.sort_by) params.append('sort_by', filters.sort_by) - if (filters.sort_order) params.append('sort_order', filters.sort_order) + async getPackages( + filters: FilterOptions = {} + ): Promise<{ packages: Package[]; total: number }> { + const params = new URLSearchParams(); - const query = params.toString() ? `?${params.toString()}` : '' - return this.request<{ packages: Package[], total: number }>(`/packages${query}`) + if (filters.platform_id) params.append("platform_id", filters.platform_id); + if (filters.category_id) + params.append("category_id", filters.category_id.toString()); + if (filters.type) params.append("type", filters.type); + if (filters.repository) params.append("repository", filters.repository); + if (filters.search) params.append("search", filters.search); + if (filters.limit) params.append("limit", filters.limit.toString()); + if (filters.offset) params.append("offset", filters.offset.toString()); + if (filters.sort_by) params.append("sort_by", filters.sort_by); + if (filters.sort_order) params.append("sort_order", filters.sort_order); + + const query = params.toString() ? `?${params.toString()}` : ""; + return this.request<{ packages: Package[]; total: number }>( + `/packages${query}` + ); } async getPackage(id: string): Promise { try { - return await this.request(`/packages/${id}`) + return await this.request(`/packages/${id}`); } catch (error) { - return null + return null; } } // Sync operations - async syncDebianPackages(): Promise<{ message: string, timestamp: string }> { - return this.request<{ message: string, timestamp: string }>('/sync', { - method: 'POST', - body: JSON.stringify({ source: 'debian-official' }), - }) + async syncDebianPackages(): Promise<{ message: string; timestamp: string }> { + return this.request<{ message: string; timestamp: string }>("/sync", { + method: "POST", + body: JSON.stringify({ source: "debian-official" }), + }); } - async syncUbuntuPackages(): Promise<{ message: string, timestamp: string }> { - return this.request<{ message: string, timestamp: string }>('/sync', { - method: 'POST', - body: JSON.stringify({ source: 'ubuntu-official' }), - }) + async syncUbuntuPackages(): Promise<{ message: string; timestamp: string }> { + return this.request<{ message: string; timestamp: string }>("/sync", { + method: "POST", + body: JSON.stringify({ source: "ubuntu-official" }), + }); } - async syncAllDebianPackages(): Promise<{ message: string, timestamp: string }> { - return this.request<{ message: string, timestamp: string }>('/sync', { - method: 'POST', - body: JSON.stringify({ source: 'all-official' }), - }) + async syncAllDebianPackages(): Promise<{ + message: string; + timestamp: string; + }> { + return this.request<{ message: string; timestamp: string }>("/sync", { + method: "POST", + body: JSON.stringify({ source: "all-official" }), + }); } - async getSyncStatus(): Promise<{ status: string, last_sync: string | null, platforms: string[] }> { - return this.request<{ status: string, last_sync: string | null, platforms: string[] }>('/sync') + async getSyncStatus(): Promise<{ + status: string; + last_sync: string | null; + platforms: string[]; + }> { + return this.request<{ + status: string; + last_sync: string | null; + platforms: string[]; + }>("/sync"); } // Recommendation operations - async getRecommendations(request: RecommendationRequest): Promise { - return this.request('/recommendations', { - method: 'POST', + async getRecommendations( + request: RecommendationRequest + ): Promise { + return this.request("/recommendations", { + method: "POST", body: JSON.stringify(request), - }) + }); } } -export const apiClient = new ApiClient() +export const apiClient = new ApiClient(); diff --git a/src/services/recommendationService.ts b/src/services/recommendationService.ts index 361a9bd..5822238 100644 --- a/src/services/recommendationService.ts +++ b/src/services/recommendationService.ts @@ -1,16 +1,16 @@ -import { PackageService } from './packageService' -import { Package } from '@/models/Package' -import { - RecommendationRequest, - RecommendedPackage, +import { PackageService } from "./packageService"; +import { Package } from "@/models/Package"; +import { + RecommendationRequest, + RecommendedPackage, UserCategory, - ExperienceLevel -} from '@/types/recommendations' -import { - getPresetPackageNames, - getPresetPriority, - getRecommendationReason -} from '@/data/recommendationPresets' + ExperienceLevel, +} from "@/types/recommendations"; +import { + getPresetPackageNames, + getPresetPriority, + getRecommendationReason, +} from "@/data/recommendationPresets"; /** * Recommendation scoring weights @@ -19,8 +19,8 @@ const SCORING_WEIGHTS = { CATEGORY_MATCH: 0.4, POPULARITY: 0.3, OS_COMPATIBILITY: 0.2, - PRESET_BOOST: 0.1 -} + PRESET_BOOST: 0.1, +}; export class RecommendationService { /** @@ -29,40 +29,48 @@ export class RecommendationService { static async generateRecommendations( request: RecommendationRequest ): Promise { - const { platform_id, categories, experienceLevel, limit = 20 } = request + const { platform_id, categories, experienceLevel, limit = 20 } = request; // Step 1: Get preset package names for the user's categories and platform - const presetPackageNames = getPresetPackageNames(categories, platform_id) + const presetPackageNames = getPresetPackageNames(categories, platform_id); // Step 2: Fetch packages from database // First, get preset packages const presetPackages = await this.fetchPresetPackages( - presetPackageNames, + presetPackageNames, platform_id - ) + ); // Then, get additional packages from categories const categoryPackages = await this.fetchCategoryPackages( - categories, - platform_id, + categories, + platform_id, limit * 2 // Fetch more to ensure we have enough after filtering - ) + ); // Step 3: Combine and deduplicate const allPackages = this.deduplicatePackages([ ...presetPackages, - ...categoryPackages - ]) + ...categoryPackages, + ]); // Step 4: Score and rank packages - const scoredPackages = allPackages.map(pkg => - this.scorePackage(pkg, categories, platform_id, presetPackageNames, experienceLevel) - ) + const scoredPackages = allPackages.map((pkg) => + this.scorePackage( + pkg, + categories, + platform_id, + presetPackageNames, + experienceLevel + ) + ); // Step 5: Sort by score and limit results - scoredPackages.sort((a, b) => b.recommendationScore - a.recommendationScore) + scoredPackages.sort( + (a, b) => b.recommendationScore - a.recommendationScore + ); - return scoredPackages.slice(0, limit) + return scoredPackages.slice(0, limit); } /** @@ -73,32 +81,32 @@ export class RecommendationService { platformId: string ): Promise { if (packageNames.length === 0) { - return [] + return []; } try { // Fetch packages by exact name match - const packages: Package[] = [] - + const packages: Package[] = []; + for (const name of packageNames) { const result = await PackageService.getMany({ platform_id: platformId, search: name, limit: 1, - sort_by: 'popularity_score', - sort_order: 'desc' - }) - + sort_by: "popularity_score", + sort_order: "desc", + }); + // Only add if exact match if (result.packages.length > 0 && result.packages[0].name === name) { - packages.push(result.packages[0]) + packages.push(result.packages[0]); } } - - return packages + + return packages; } catch (error) { - console.error('Error fetching preset packages:', error) - return [] + console.error("Error fetching preset packages:", error); + return []; } } @@ -113,38 +121,38 @@ export class RecommendationService { try { // Map user categories to database categories const categoryMap: Record = { - 'development': ['Development', 'Internet'], - 'design': ['Graphics', 'Multimedia'], - 'multimedia': ['Multimedia', 'Graphics'], - 'system-tools': ['System', 'Utilities'], - 'gaming': ['Games'], - 'productivity': ['Office', 'Utilities'], - 'education': ['Science', 'Education'] - } + development: ["Development", "Internet"], + design: ["Graphics", "Multimedia"], + multimedia: ["Multimedia", "Graphics"], + "system-tools": ["System", "Utilities"], + gaming: ["Games"], + productivity: ["Office", "Utilities"], + education: ["Science", "Education"], + }; // Get all matching packages - const packages: Package[] = [] - + const packages: Package[] = []; + for (const category of categories) { - const dbCategories = categoryMap[category] || [] - + const dbCategories = categoryMap[category] || []; + // Note: Since we don't have category filtering in current API, // we'll fetch by popularity and filter client-side // This is a limitation of current schema - categories are not well-utilized const result = await PackageService.getMany({ platform_id: platformId, limit: Math.ceil(limit / categories.length), - sort_by: 'popularity_score', - sort_order: 'desc' - }) - - packages.push(...result.packages) + sort_by: "popularity_score", + sort_order: "desc", + }); + + packages.push(...result.packages); } - - return packages + + return packages; } catch (error) { - console.error('Error fetching category packages:', error) - return [] + console.error("Error fetching category packages:", error); + return []; } } @@ -152,14 +160,14 @@ export class RecommendationService { * Remove duplicate packages (by ID) */ private static deduplicatePackages(packages: Package[]): Package[] { - const seen = new Set() - return packages.filter(pkg => { + const seen = new Set(); + return packages.filter((pkg) => { if (seen.has(pkg.id)) { - return false + return false; } - seen.add(pkg.id) - return true - }) + seen.add(pkg.id); + return true; + }); } /** @@ -172,65 +180,67 @@ export class RecommendationService { presetPackageNames: string[], experienceLevel?: ExperienceLevel ): RecommendedPackage { - let score = 0 - let reason = '' - const isPresetMatch = presetPackageNames.includes(pkg.name) + let score = 0; + let reason = ""; + const isPresetMatch = presetPackageNames.includes(pkg.name); // 1. Category Match Score (40%) // For preset packages, this is always high - const categoryScore = isPresetMatch ? 1.0 : 0.5 - score += categoryScore * SCORING_WEIGHTS.CATEGORY_MATCH + const categoryScore = isPresetMatch ? 1.0 : 0.5; + score += categoryScore * SCORING_WEIGHTS.CATEGORY_MATCH; // 2. Popularity Score (30%) // Normalize popularity_score (0-100) to 0-1 - const popularityScore = (pkg.popularity_score || 0) / 100 - score += popularityScore * SCORING_WEIGHTS.POPULARITY + const popularityScore = (pkg.popularity_score || 0) / 100; + score += popularityScore * SCORING_WEIGHTS.POPULARITY; // 3. OS Compatibility Score (20%) // All packages from DB should be compatible, so this is always 1.0 - const osScore = 1.0 - score += osScore * SCORING_WEIGHTS.OS_COMPATIBILITY + const osScore = 1.0; + score += osScore * SCORING_WEIGHTS.OS_COMPATIBILITY; // 4. Preset Boost (10%) // Extra boost for preset packages based on priority - let presetBoost = 0 + let presetBoost = 0; if (isPresetMatch) { - const priority = getPresetPriority(pkg.name, categories, platformId) + const priority = getPresetPriority(pkg.name, categories, platformId); if (priority !== null) { - presetBoost = priority / 10 // Normalize 1-10 to 0.1-1.0 - + presetBoost = priority / 10; // Normalize 1-10 to 0.1-1.0 + // Get recommendation reason from preset - const presetReason = getRecommendationReason(pkg.name, categories) + const presetReason = getRecommendationReason(pkg.name, categories); if (presetReason) { - reason = presetReason + reason = presetReason; } } } - score += presetBoost * SCORING_WEIGHTS.PRESET_BOOST + score += presetBoost * SCORING_WEIGHTS.PRESET_BOOST; // Default reason if not from preset if (!reason) { if (pkg.popularity_score && pkg.popularity_score > 70) { - reason = 'Popular choice in the community' + reason = "Popular choice in the community"; } else { - reason = 'Recommended for your selected categories' + reason = "Recommended for your selected categories"; } } // Normalize final score to 0-100 - const finalScore = Math.round(score * 100) + const finalScore = Math.round(score * 100); return { id: pkg.id, name: pkg.name, - description: pkg.description || 'No description available', - version: pkg.version || 'latest', - category: typeof pkg.category === 'string' ? pkg.category : pkg.category?.name, - license: typeof pkg.license === 'string' ? pkg.license : pkg.license?.name, - type: pkg.type || 'cli', + description: pkg.description || "No description available", + version: pkg.version || "latest", + category: + typeof pkg.category === "string" ? pkg.category : pkg.category?.name, + license: + typeof pkg.license === "string" ? pkg.license : pkg.license?.name, + type: pkg.type || "cli", platform: pkg.platform, platform_id: pkg.platform_id, - repository: pkg.repository || 'official', + repository: pkg.repository || "official", download_url: pkg.download_url, lastUpdated: pkg.last_updated ? pkg.last_updated.toString() : undefined, downloads: pkg.downloads_count, @@ -239,8 +249,8 @@ export class RecommendationService { tags: pkg.tags, recommendationScore: finalScore, recommendationReason: reason, - presetMatch: isPresetMatch - } + presetMatch: isPresetMatch, + }; } /** @@ -253,8 +263,8 @@ export class RecommendationService { return this.generateRecommendations({ platform_id: platformId, categories: [primaryCategory], - limit: 5 - }) + limit: 5, + }); } /** @@ -265,30 +275,30 @@ export class RecommendationService { categories: UserCategory[], totalLimit: number = 20 ): Promise { - const perCategory = Math.ceil(totalLimit / categories.length) - const allRecommendations: RecommendedPackage[] = [] + const perCategory = Math.ceil(totalLimit / categories.length); + const allRecommendations: RecommendedPackage[] = []; for (const category of categories) { const recommendations = await this.generateRecommendations({ platform_id: platformId, categories: [category], - limit: perCategory - }) - allRecommendations.push(...recommendations) + limit: perCategory, + }); + allRecommendations.push(...recommendations); } // Deduplicate by ID and re-sort - const seen = new Set() - const deduplicated = allRecommendations.filter(pkg => { + const seen = new Set(); + const deduplicated = allRecommendations.filter((pkg) => { if (seen.has(pkg.id)) { - return false + return false; } - seen.add(pkg.id) - return true - }) - - deduplicated.sort((a, b) => b.recommendationScore - a.recommendationScore) + seen.add(pkg.id); + return true; + }); - return deduplicated.slice(0, totalLimit) + deduplicated.sort((a, b) => b.recommendationScore - a.recommendationScore); + + return deduplicated.slice(0, totalLimit); } } diff --git a/src/types/recommendations.ts b/src/types/recommendations.ts index 94ee7ab..6b5ace4 100644 --- a/src/types/recommendations.ts +++ b/src/types/recommendations.ts @@ -1,99 +1,99 @@ -import { Package, Platform } from './index' +import { Package, Platform } from "./index"; /** * User category types for package recommendations */ -export type UserCategory = - | 'development' - | 'design' - | 'multimedia' - | 'system-tools' - | 'gaming' - | 'productivity' - | 'education' +export type UserCategory = + | "development" + | "design" + | "multimedia" + | "system-tools" + | "gaming" + | "productivity" + | "education"; /** * User experience level */ -export type ExperienceLevel = 'beginner' | 'intermediate' | 'advanced' +export type ExperienceLevel = "beginner" | "intermediate" | "advanced"; /** * User profile stored in localStorage */ export interface UserProfile { - categories: UserCategory[] - detectedOS?: string - selectedOS?: string // Manual override - experienceLevel?: ExperienceLevel - hasCompletedOnboarding: boolean - createdAt: string - lastUpdated: string + categories: UserCategory[]; + detectedOS?: string; + selectedOS?: string; // Manual override + experienceLevel?: ExperienceLevel; + hasCompletedOnboarding: boolean; + createdAt: string; + lastUpdated: string; } /** * Request payload for recommendation API */ export interface RecommendationRequest { - platform_id: string - categories: UserCategory[] - experienceLevel?: ExperienceLevel - limit?: number + platform_id: string; + categories: UserCategory[]; + experienceLevel?: ExperienceLevel; + limit?: number; } /** * Recommended package with score */ export interface RecommendedPackage { - id: string - name: string - description: string - version: string - category?: string - license?: string - type: 'gui' | 'cli' - platform?: string | any - platform_id?: string - repository: 'official' | 'third-party' | 'aur' - download_url?: string - lastUpdated?: string - downloads?: number - popularity?: number - popularity_score?: number - tags?: string[] - recommendationScore: number - recommendationReason: string - presetMatch?: boolean + id: string; + name: string; + description: string; + version: string; + category?: string; + license?: string; + type: "gui" | "cli"; + platform?: string | any; + platform_id?: string; + repository: "official" | "third-party" | "aur"; + download_url?: string; + lastUpdated?: string; + downloads?: number; + popularity?: number; + popularity_score?: number; + tags?: string[]; + recommendationScore: number; + recommendationReason: string; + presetMatch?: boolean; } /** * Preset package configuration */ export interface PackagePreset { - packageName: string - platforms: string[] // ['windows', 'macos', 'ubuntu', 'arch', 'fedora'] - priority: number // 1-10, higher = more important - reason: string // Why this package is recommended - experienceLevel?: ExperienceLevel[] // Target experience levels + packageName: string; + platforms: string[]; // ['windows', 'macos', 'ubuntu', 'arch', 'fedora'] + priority: number; // 1-10, higher = more important + reason: string; // Why this package is recommended + experienceLevel?: ExperienceLevel[]; // Target experience levels } /** * Category preset configuration */ export interface CategoryPreset { - category: UserCategory - packages: PackagePreset[] - description: string - icon: string + category: UserCategory; + packages: PackagePreset[]; + description: string; + icon: string; } /** * Recommendation response */ export interface RecommendationResponse { - recommendations: RecommendedPackage[] - total: number + recommendations: RecommendedPackage[]; + total: number; userProfile: { - categories: UserCategory[] - platform: string - } + categories: UserCategory[]; + platform: string; + }; }