mirror of
https://github.com/yusufipk/RepoHub.git
synced 2026-09-11 10:36:07 +00:00
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.
This commit is contained in:
@@ -1,70 +1,82 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server'
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
import { RecommendationService } from '@/services/recommendationService'
|
import { RecommendationService } from "@/services/recommendationService";
|
||||||
import { RecommendationRequest } from '@/types/recommendations'
|
import { RecommendationRequest } from "@/types/recommendations";
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
const body: RecommendationRequest = await request.json()
|
const body: RecommendationRequest = await request.json();
|
||||||
|
|
||||||
// Validate required fields
|
// Validate required fields
|
||||||
if (!body.platform_id) {
|
if (!body.platform_id) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: 'platform_id is required' },
|
{ error: "platform_id is required" },
|
||||||
{ status: 400 }
|
{ status: 400 }
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!body.categories || body.categories.length === 0) {
|
if (!body.categories || body.categories.length === 0) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: 'At least one category is required' },
|
{ error: "At least one category is required" },
|
||||||
{ status: 400 }
|
{ status: 400 }
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate platform_id
|
// 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)) {
|
if (!validPlatforms.includes(body.platform_id)) {
|
||||||
return NextResponse.json(
|
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 }
|
{ status: 400 }
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate categories
|
// Validate categories
|
||||||
const validCategories = [
|
const validCategories = [
|
||||||
'development',
|
"development",
|
||||||
'design',
|
"design",
|
||||||
'multimedia',
|
"multimedia",
|
||||||
'system-tools',
|
"system-tools",
|
||||||
'gaming',
|
"gaming",
|
||||||
'productivity',
|
"productivity",
|
||||||
'education'
|
"education",
|
||||||
]
|
];
|
||||||
const invalidCategories = body.categories.filter(
|
const invalidCategories = body.categories.filter(
|
||||||
cat => !validCategories.includes(cat)
|
(cat) => !validCategories.includes(cat)
|
||||||
)
|
);
|
||||||
if (invalidCategories.length > 0) {
|
if (invalidCategories.length > 0) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{
|
{
|
||||||
error: `Invalid categories: ${invalidCategories.join(', ')}`,
|
error: `Invalid categories: ${invalidCategories.join(", ")}`,
|
||||||
validCategories
|
validCategories,
|
||||||
},
|
},
|
||||||
{ status: 400 }
|
{ status: 400 }
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set default limit
|
// Set default limit
|
||||||
const limit = body.limit && body.limit > 0 && body.limit <= 50
|
const limit =
|
||||||
? body.limit
|
body.limit && body.limit > 0 && body.limit <= 50 ? body.limit : 20;
|
||||||
: 20
|
|
||||||
|
|
||||||
// Generate recommendations
|
// Generate recommendations
|
||||||
const recommendations = await RecommendationService.generateRecommendations({
|
const recommendations = await RecommendationService.generateRecommendations(
|
||||||
|
{
|
||||||
platform_id: body.platform_id,
|
platform_id: body.platform_id,
|
||||||
categories: body.categories,
|
categories: body.categories,
|
||||||
experienceLevel: body.experienceLevel,
|
experienceLevel: body.experienceLevel,
|
||||||
limit
|
limit,
|
||||||
})
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
recommendations,
|
recommendations,
|
||||||
@@ -72,68 +84,82 @@ export async function POST(request: NextRequest) {
|
|||||||
userProfile: {
|
userProfile: {
|
||||||
categories: body.categories,
|
categories: body.categories,
|
||||||
platform: body.platform_id,
|
platform: body.platform_id,
|
||||||
experienceLevel: body.experienceLevel
|
experienceLevel: body.experienceLevel,
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error generating recommendations:', error)
|
console.error("Error generating recommendations:", error);
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{
|
{
|
||||||
error: 'Failed to generate recommendations',
|
error: "Failed to generate recommendations",
|
||||||
details: error instanceof Error ? error.message : 'Unknown error'
|
details: error instanceof Error ? error.message : "Unknown error",
|
||||||
},
|
},
|
||||||
{ status: 500 }
|
{ status: 500 }
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
const { searchParams } = new URL(request.url)
|
const { searchParams } = new URL(request.url);
|
||||||
const platformId = searchParams.get('platform_id')
|
const platformId = searchParams.get("platform_id");
|
||||||
const categoriesParam = searchParams.get('categories')
|
const categoriesParam = searchParams.get("categories");
|
||||||
const experienceLevel = searchParams.get('experience_level')
|
const experienceLevel = searchParams.get("experience_level");
|
||||||
const limit = searchParams.get('limit')
|
const limit = searchParams.get("limit");
|
||||||
|
|
||||||
// Validate required fields
|
// Validate required fields
|
||||||
if (!platformId) {
|
if (!platformId) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: 'platform_id query parameter is required' },
|
{ error: "platform_id query parameter is required" },
|
||||||
{ status: 400 }
|
{ status: 400 }
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!categoriesParam) {
|
if (!categoriesParam) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: 'categories query parameter is required (comma-separated)' },
|
{ error: "categories query parameter is required (comma-separated)" },
|
||||||
{ status: 400 }
|
{ status: 400 }
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse categories
|
// Parse categories
|
||||||
const categories = categoriesParam.split(',').map(c => c.trim())
|
const categories = categoriesParam.split(",").map((c) => c.trim());
|
||||||
|
|
||||||
// Validate platform_id
|
// Validate platform_id
|
||||||
const validPlatforms = ['windows', 'macos', 'ubuntu', 'debian', 'arch', 'fedora']
|
const validPlatforms = [
|
||||||
|
"windows",
|
||||||
|
"macos",
|
||||||
|
"ubuntu",
|
||||||
|
"debian",
|
||||||
|
"arch",
|
||||||
|
"fedora",
|
||||||
|
];
|
||||||
if (!validPlatforms.includes(platformId)) {
|
if (!validPlatforms.includes(platformId)) {
|
||||||
return NextResponse.json(
|
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 }
|
{ status: 400 }
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set default limit
|
// Set default limit
|
||||||
const parsedLimit = limit && parseInt(limit) > 0 && parseInt(limit) <= 50
|
const parsedLimit =
|
||||||
|
limit && parseInt(limit) > 0 && parseInt(limit) <= 50
|
||||||
? parseInt(limit)
|
? parseInt(limit)
|
||||||
: 20
|
: 20;
|
||||||
|
|
||||||
// Generate recommendations
|
// Generate recommendations
|
||||||
const recommendations = await RecommendationService.generateRecommendations({
|
const recommendations = await RecommendationService.generateRecommendations(
|
||||||
|
{
|
||||||
platform_id: platformId,
|
platform_id: platformId,
|
||||||
categories: categories as any,
|
categories: categories as any,
|
||||||
experienceLevel: experienceLevel as any,
|
experienceLevel: experienceLevel as any,
|
||||||
limit: parsedLimit
|
limit: parsedLimit,
|
||||||
})
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
recommendations,
|
recommendations,
|
||||||
@@ -141,17 +167,17 @@ export async function GET(request: NextRequest) {
|
|||||||
userProfile: {
|
userProfile: {
|
||||||
categories,
|
categories,
|
||||||
platform: platformId,
|
platform: platformId,
|
||||||
experienceLevel
|
experienceLevel,
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error generating recommendations:', error)
|
console.error("Error generating recommendations:", error);
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{
|
{
|
||||||
error: 'Failed to generate recommendations',
|
error: "Failed to generate recommendations",
|
||||||
details: error instanceof Error ? error.message : 'Unknown error'
|
details: error instanceof Error ? error.message : "Unknown error",
|
||||||
},
|
},
|
||||||
{ status: 500 }
|
{ status: 500 }
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -113,8 +113,7 @@ export function OnboardingModal({
|
|||||||
{[1, 2, 3].map(i => (
|
{[1, 2, 3].map(i => (
|
||||||
<div
|
<div
|
||||||
key={i}
|
key={i}
|
||||||
className={`h-2 flex-1 rounded-full transition-colors ${
|
className={`h-2 flex-1 rounded-full transition-colors ${i <= step ? 'bg-primary' : 'bg-secondary'
|
||||||
i <= step ? 'bg-primary' : 'bg-secondary'
|
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
@@ -139,8 +138,7 @@ export function OnboardingModal({
|
|||||||
<button
|
<button
|
||||||
key={preset.category}
|
key={preset.category}
|
||||||
onClick={() => handleCategoryToggle(preset.category)}
|
onClick={() => handleCategoryToggle(preset.category)}
|
||||||
className={`p-4 rounded-lg border-2 text-left transition-all hover:scale-105 ${
|
className={`p-4 rounded-lg border-2 text-left transition-all hover:scale-105 ${selectedCategories.includes(preset.category)
|
||||||
selectedCategories.includes(preset.category)
|
|
||||||
? 'border-primary bg-primary/10'
|
? 'border-primary bg-primary/10'
|
||||||
: 'border-border hover:border-primary/50'
|
: 'border-border hover:border-primary/50'
|
||||||
}`}
|
}`}
|
||||||
@@ -190,8 +188,7 @@ export function OnboardingModal({
|
|||||||
<button
|
<button
|
||||||
key={platform.id}
|
key={platform.id}
|
||||||
onClick={() => setSelectedOS(platform.id)}
|
onClick={() => setSelectedOS(platform.id)}
|
||||||
className={`p-4 rounded-lg border-2 text-center transition-all hover:scale-105 ${
|
className={`p-4 rounded-lg border-2 text-center transition-all hover:scale-105 ${selectedOS === platform.id
|
||||||
selectedOS === platform.id
|
|
||||||
? 'border-primary bg-primary/10'
|
? 'border-primary bg-primary/10'
|
||||||
: 'border-border hover:border-primary/50'
|
: 'border-border hover:border-primary/50'
|
||||||
}`}
|
}`}
|
||||||
@@ -221,8 +218,7 @@ export function OnboardingModal({
|
|||||||
<button
|
<button
|
||||||
key={level}
|
key={level}
|
||||||
onClick={() => setExperienceLevel(level)}
|
onClick={() => setExperienceLevel(level)}
|
||||||
className={`w-full p-4 rounded-lg border-2 text-left transition-all hover:scale-[1.02] ${
|
className={`w-full p-4 rounded-lg border-2 text-left transition-all hover:scale-[1.02] ${experienceLevel === level
|
||||||
experienceLevel === level
|
|
||||||
? 'border-primary bg-primary/10'
|
? 'border-primary bg-primary/10'
|
||||||
: 'border-border hover:border-primary/50'
|
: 'border-border hover:border-primary/50'
|
||||||
}`}
|
}`}
|
||||||
|
|||||||
@@ -164,8 +164,7 @@ export function RecommendationsSection({
|
|||||||
{recommendations.map(pkg => (
|
{recommendations.map(pkg => (
|
||||||
<Card
|
<Card
|
||||||
key={pkg.id}
|
key={pkg.id}
|
||||||
className={`relative overflow-hidden transition-all hover:shadow-lg cursor-pointer ${
|
className={`relative overflow-hidden transition-all hover:shadow-lg cursor-pointer ${isPackageSelected(pkg) ? 'ring-2 ring-primary' : ''
|
||||||
isPackageSelected(pkg) ? 'ring-2 ring-primary' : ''
|
|
||||||
}`}
|
}`}
|
||||||
onClick={() => onPackageToggle(pkg)}
|
onClick={() => onPackageToggle(pkg)}
|
||||||
>
|
>
|
||||||
|
|||||||
+220
-218
@@ -1,4 +1,4 @@
|
|||||||
import { CategoryPreset } from '@/types/recommendations'
|
import { CategoryPreset } from "@/types/recommendations";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Curated package recommendations for each user category
|
* Curated package recommendations for each user category
|
||||||
@@ -6,343 +6,345 @@ import { CategoryPreset } from '@/types/recommendations'
|
|||||||
*/
|
*/
|
||||||
export const RECOMMENDATION_PRESETS: CategoryPreset[] = [
|
export const RECOMMENDATION_PRESETS: CategoryPreset[] = [
|
||||||
{
|
{
|
||||||
category: 'development',
|
category: "development",
|
||||||
description: 'Essential tools for software development',
|
description: "Essential tools for software development",
|
||||||
icon: '💻',
|
icon: "💻",
|
||||||
packages: [
|
packages: [
|
||||||
{
|
{
|
||||||
packageName: 'git',
|
packageName: "git",
|
||||||
platforms: ['windows', 'macos', 'ubuntu', 'debian', 'arch', 'fedora'],
|
platforms: ["windows", "macos", "ubuntu", "debian", "arch", "fedora"],
|
||||||
priority: 10,
|
priority: 10,
|
||||||
reason: 'Version control system essential for all developers',
|
reason: "Version control system essential for all developers",
|
||||||
experienceLevel: ['beginner', 'intermediate', 'advanced']
|
experienceLevel: ["beginner", "intermediate", "advanced"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
packageName: 'code',
|
packageName: "code",
|
||||||
platforms: ['windows', 'macos', 'ubuntu', 'debian'],
|
platforms: ["windows", "macos", "ubuntu", "debian"],
|
||||||
priority: 9,
|
priority: 9,
|
||||||
reason: 'Visual Studio Code - Popular code editor',
|
reason: "Visual Studio Code - Popular code editor",
|
||||||
experienceLevel: ['beginner', 'intermediate', 'advanced']
|
experienceLevel: ["beginner", "intermediate", "advanced"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
packageName: 'visual-studio-code',
|
packageName: "visual-studio-code",
|
||||||
platforms: ['arch', 'fedora'],
|
platforms: ["arch", "fedora"],
|
||||||
priority: 9,
|
priority: 9,
|
||||||
reason: 'Visual Studio Code - Popular code editor',
|
reason: "Visual Studio Code - Popular code editor",
|
||||||
experienceLevel: ['beginner', 'intermediate', 'advanced']
|
experienceLevel: ["beginner", "intermediate", "advanced"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
packageName: 'nodejs',
|
packageName: "nodejs",
|
||||||
platforms: ['windows', 'macos', 'ubuntu', 'debian', 'arch', 'fedora'],
|
platforms: ["windows", "macos", "ubuntu", "debian", "arch", "fedora"],
|
||||||
priority: 8,
|
priority: 8,
|
||||||
reason: 'JavaScript runtime for modern web development',
|
reason: "JavaScript runtime for modern web development",
|
||||||
experienceLevel: ['beginner', 'intermediate', 'advanced']
|
experienceLevel: ["beginner", "intermediate", "advanced"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
packageName: 'python3',
|
packageName: "python3",
|
||||||
platforms: ['ubuntu', 'debian', 'arch', 'fedora'],
|
platforms: ["ubuntu", "debian", "arch", "fedora"],
|
||||||
priority: 8,
|
priority: 8,
|
||||||
reason: 'Python programming language',
|
reason: "Python programming language",
|
||||||
experienceLevel: ['beginner', 'intermediate', 'advanced']
|
experienceLevel: ["beginner", "intermediate", "advanced"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
packageName: 'python',
|
packageName: "python",
|
||||||
platforms: ['windows', 'macos'],
|
platforms: ["windows", "macos"],
|
||||||
priority: 8,
|
priority: 8,
|
||||||
reason: 'Python programming language',
|
reason: "Python programming language",
|
||||||
experienceLevel: ['beginner', 'intermediate', 'advanced']
|
experienceLevel: ["beginner", "intermediate", "advanced"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
packageName: 'docker',
|
packageName: "docker",
|
||||||
platforms: ['windows', 'macos', 'ubuntu', 'debian', 'arch', 'fedora'],
|
platforms: ["windows", "macos", "ubuntu", "debian", "arch", "fedora"],
|
||||||
priority: 7,
|
priority: 7,
|
||||||
reason: 'Containerization platform for development',
|
reason: "Containerization platform for development",
|
||||||
experienceLevel: ['intermediate', 'advanced']
|
experienceLevel: ["intermediate", "advanced"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
packageName: 'curl',
|
packageName: "curl",
|
||||||
platforms: ['ubuntu', 'debian', 'arch', 'fedora', 'macos'],
|
platforms: ["ubuntu", "debian", "arch", "fedora", "macos"],
|
||||||
priority: 7,
|
priority: 7,
|
||||||
reason: 'Command-line tool for transferring data',
|
reason: "Command-line tool for transferring data",
|
||||||
experienceLevel: ['intermediate', 'advanced']
|
experienceLevel: ["intermediate", "advanced"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
packageName: 'vim',
|
packageName: "vim",
|
||||||
platforms: ['ubuntu', 'debian', 'arch', 'fedora', 'macos'],
|
platforms: ["ubuntu", "debian", "arch", "fedora", "macos"],
|
||||||
priority: 6,
|
priority: 6,
|
||||||
reason: 'Powerful text editor',
|
reason: "Powerful text editor",
|
||||||
experienceLevel: ['intermediate', 'advanced']
|
experienceLevel: ["intermediate", "advanced"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
packageName: 'postman',
|
packageName: "postman",
|
||||||
platforms: ['windows', 'macos', 'ubuntu', 'debian'],
|
platforms: ["windows", "macos", "ubuntu", "debian"],
|
||||||
priority: 6,
|
priority: 6,
|
||||||
reason: 'API development and testing tool',
|
reason: "API development and testing tool",
|
||||||
experienceLevel: ['beginner', 'intermediate', 'advanced']
|
experienceLevel: ["beginner", "intermediate", "advanced"],
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
category: 'design',
|
category: "design",
|
||||||
description: 'Tools for graphic design, UI/UX, and creative work',
|
description: "Tools for graphic design, UI/UX, and creative work",
|
||||||
icon: '🎨',
|
icon: "🎨",
|
||||||
packages: [
|
packages: [
|
||||||
{
|
{
|
||||||
packageName: 'gimp',
|
packageName: "gimp",
|
||||||
platforms: ['windows', 'macos', 'ubuntu', 'debian', 'arch', 'fedora'],
|
platforms: ["windows", "macos", "ubuntu", "debian", "arch", "fedora"],
|
||||||
priority: 9,
|
priority: 9,
|
||||||
reason: 'Free and open-source image editor',
|
reason: "Free and open-source image editor",
|
||||||
experienceLevel: ['beginner', 'intermediate', 'advanced']
|
experienceLevel: ["beginner", "intermediate", "advanced"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
packageName: 'inkscape',
|
packageName: "inkscape",
|
||||||
platforms: ['windows', 'macos', 'ubuntu', 'debian', 'arch', 'fedora'],
|
platforms: ["windows", "macos", "ubuntu", "debian", "arch", "fedora"],
|
||||||
priority: 8,
|
priority: 8,
|
||||||
reason: 'Professional vector graphics editor',
|
reason: "Professional vector graphics editor",
|
||||||
experienceLevel: ['beginner', 'intermediate', 'advanced']
|
experienceLevel: ["beginner", "intermediate", "advanced"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
packageName: 'blender',
|
packageName: "blender",
|
||||||
platforms: ['windows', 'macos', 'ubuntu', 'debian', 'arch', 'fedora'],
|
platforms: ["windows", "macos", "ubuntu", "debian", "arch", "fedora"],
|
||||||
priority: 8,
|
priority: 8,
|
||||||
reason: '3D creation suite',
|
reason: "3D creation suite",
|
||||||
experienceLevel: ['intermediate', 'advanced']
|
experienceLevel: ["intermediate", "advanced"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
packageName: 'figma',
|
packageName: "figma",
|
||||||
platforms: ['windows', 'macos'],
|
platforms: ["windows", "macos"],
|
||||||
priority: 9,
|
priority: 9,
|
||||||
reason: 'Collaborative interface design tool',
|
reason: "Collaborative interface design tool",
|
||||||
experienceLevel: ['beginner', 'intermediate', 'advanced']
|
experienceLevel: ["beginner", "intermediate", "advanced"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
packageName: 'krita',
|
packageName: "krita",
|
||||||
platforms: ['windows', 'macos', 'ubuntu', 'debian', 'arch', 'fedora'],
|
platforms: ["windows", "macos", "ubuntu", "debian", "arch", "fedora"],
|
||||||
priority: 7,
|
priority: 7,
|
||||||
reason: 'Digital painting application',
|
reason: "Digital painting application",
|
||||||
experienceLevel: ['beginner', 'intermediate', 'advanced']
|
experienceLevel: ["beginner", "intermediate", "advanced"],
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
category: 'multimedia',
|
category: "multimedia",
|
||||||
description: 'Audio, video editing and media management tools',
|
description: "Audio, video editing and media management tools",
|
||||||
icon: '🎬',
|
icon: "🎬",
|
||||||
packages: [
|
packages: [
|
||||||
{
|
{
|
||||||
packageName: 'vlc',
|
packageName: "vlc",
|
||||||
platforms: ['windows', 'macos', 'ubuntu', 'debian', 'arch', 'fedora'],
|
platforms: ["windows", "macos", "ubuntu", "debian", "arch", "fedora"],
|
||||||
priority: 10,
|
priority: 10,
|
||||||
reason: 'Versatile media player',
|
reason: "Versatile media player",
|
||||||
experienceLevel: ['beginner', 'intermediate', 'advanced']
|
experienceLevel: ["beginner", "intermediate", "advanced"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
packageName: 'obs-studio',
|
packageName: "obs-studio",
|
||||||
platforms: ['windows', 'macos', 'ubuntu', 'debian', 'arch', 'fedora'],
|
platforms: ["windows", "macos", "ubuntu", "debian", "arch", "fedora"],
|
||||||
priority: 9,
|
priority: 9,
|
||||||
reason: 'Video recording and live streaming',
|
reason: "Video recording and live streaming",
|
||||||
experienceLevel: ['intermediate', 'advanced']
|
experienceLevel: ["intermediate", "advanced"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
packageName: 'audacity',
|
packageName: "audacity",
|
||||||
platforms: ['windows', 'macos', 'ubuntu', 'debian', 'arch', 'fedora'],
|
platforms: ["windows", "macos", "ubuntu", "debian", "arch", "fedora"],
|
||||||
priority: 8,
|
priority: 8,
|
||||||
reason: 'Audio editing software',
|
reason: "Audio editing software",
|
||||||
experienceLevel: ['beginner', 'intermediate', 'advanced']
|
experienceLevel: ["beginner", "intermediate", "advanced"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
packageName: 'ffmpeg',
|
packageName: "ffmpeg",
|
||||||
platforms: ['ubuntu', 'debian', 'arch', 'fedora', 'macos'],
|
platforms: ["ubuntu", "debian", "arch", "fedora", "macos"],
|
||||||
priority: 8,
|
priority: 8,
|
||||||
reason: 'Complete multimedia framework',
|
reason: "Complete multimedia framework",
|
||||||
experienceLevel: ['intermediate', 'advanced']
|
experienceLevel: ["intermediate", "advanced"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
packageName: 'handbrake',
|
packageName: "handbrake",
|
||||||
platforms: ['windows', 'macos', 'ubuntu', 'debian', 'arch', 'fedora'],
|
platforms: ["windows", "macos", "ubuntu", "debian", "arch", "fedora"],
|
||||||
priority: 7,
|
priority: 7,
|
||||||
reason: 'Video transcoder',
|
reason: "Video transcoder",
|
||||||
experienceLevel: ['beginner', 'intermediate', 'advanced']
|
experienceLevel: ["beginner", "intermediate", "advanced"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
packageName: 'kdenlive',
|
packageName: "kdenlive",
|
||||||
platforms: ['ubuntu', 'debian', 'arch', 'fedora'],
|
platforms: ["ubuntu", "debian", "arch", "fedora"],
|
||||||
priority: 7,
|
priority: 7,
|
||||||
reason: 'Video editing software',
|
reason: "Video editing software",
|
||||||
experienceLevel: ['intermediate', 'advanced']
|
experienceLevel: ["intermediate", "advanced"],
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
category: 'system-tools',
|
category: "system-tools",
|
||||||
description: 'System administration, security and utilities',
|
description: "System administration, security and utilities",
|
||||||
icon: '⚙️',
|
icon: "⚙️",
|
||||||
packages: [
|
packages: [
|
||||||
{
|
{
|
||||||
packageName: 'htop',
|
packageName: "htop",
|
||||||
platforms: ['ubuntu', 'debian', 'arch', 'fedora', 'macos'],
|
platforms: ["ubuntu", "debian", "arch", "fedora", "macos"],
|
||||||
priority: 9,
|
priority: 9,
|
||||||
reason: 'Interactive process viewer',
|
reason: "Interactive process viewer",
|
||||||
experienceLevel: ['beginner', 'intermediate', 'advanced']
|
experienceLevel: ["beginner", "intermediate", "advanced"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
packageName: 'tmux',
|
packageName: "tmux",
|
||||||
platforms: ['ubuntu', 'debian', 'arch', 'fedora', 'macos'],
|
platforms: ["ubuntu", "debian", "arch", "fedora", "macos"],
|
||||||
priority: 8,
|
priority: 8,
|
||||||
reason: 'Terminal multiplexer',
|
reason: "Terminal multiplexer",
|
||||||
experienceLevel: ['intermediate', 'advanced']
|
experienceLevel: ["intermediate", "advanced"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
packageName: 'wget',
|
packageName: "wget",
|
||||||
platforms: ['ubuntu', 'debian', 'arch', 'fedora', 'macos'],
|
platforms: ["ubuntu", "debian", "arch", "fedora", "macos"],
|
||||||
priority: 8,
|
priority: 8,
|
||||||
reason: 'Network downloader',
|
reason: "Network downloader",
|
||||||
experienceLevel: ['beginner', 'intermediate', 'advanced']
|
experienceLevel: ["beginner", "intermediate", "advanced"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
packageName: 'neofetch',
|
packageName: "neofetch",
|
||||||
platforms: ['ubuntu', 'debian', 'arch', 'fedora', 'macos'],
|
platforms: ["ubuntu", "debian", "arch", "fedora", "macos"],
|
||||||
priority: 6,
|
priority: 6,
|
||||||
reason: 'System information tool',
|
reason: "System information tool",
|
||||||
experienceLevel: ['beginner', 'intermediate', 'advanced']
|
experienceLevel: ["beginner", "intermediate", "advanced"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
packageName: 'wireshark',
|
packageName: "wireshark",
|
||||||
platforms: ['windows', 'macos', 'ubuntu', 'debian', 'arch', 'fedora'],
|
platforms: ["windows", "macos", "ubuntu", "debian", "arch", "fedora"],
|
||||||
priority: 7,
|
priority: 7,
|
||||||
reason: 'Network protocol analyzer',
|
reason: "Network protocol analyzer",
|
||||||
experienceLevel: ['advanced']
|
experienceLevel: ["advanced"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
packageName: 'gparted',
|
packageName: "gparted",
|
||||||
platforms: ['ubuntu', 'debian', 'arch', 'fedora'],
|
platforms: ["ubuntu", "debian", "arch", "fedora"],
|
||||||
priority: 6,
|
priority: 6,
|
||||||
reason: 'Partition editor',
|
reason: "Partition editor",
|
||||||
experienceLevel: ['intermediate', 'advanced']
|
experienceLevel: ["intermediate", "advanced"],
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
category: 'gaming',
|
category: "gaming",
|
||||||
description: 'Gaming platforms and related tools',
|
description: "Gaming platforms and related tools",
|
||||||
icon: '🎮',
|
icon: "🎮",
|
||||||
packages: [
|
packages: [
|
||||||
{
|
{
|
||||||
packageName: 'steam',
|
packageName: "steam",
|
||||||
platforms: ['windows', 'macos', 'ubuntu', 'debian', 'arch', 'fedora'],
|
platforms: ["windows", "macos", "ubuntu", "debian", "arch", "fedora"],
|
||||||
priority: 10,
|
priority: 10,
|
||||||
reason: 'Gaming platform',
|
reason: "Gaming platform",
|
||||||
experienceLevel: ['beginner', 'intermediate', 'advanced']
|
experienceLevel: ["beginner", "intermediate", "advanced"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
packageName: 'discord',
|
packageName: "discord",
|
||||||
platforms: ['windows', 'macos', 'ubuntu', 'debian', 'arch', 'fedora'],
|
platforms: ["windows", "macos", "ubuntu", "debian", "arch", "fedora"],
|
||||||
priority: 9,
|
priority: 9,
|
||||||
reason: 'Voice and chat for gamers',
|
reason: "Voice and chat for gamers",
|
||||||
experienceLevel: ['beginner', 'intermediate', 'advanced']
|
experienceLevel: ["beginner", "intermediate", "advanced"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
packageName: 'lutris',
|
packageName: "lutris",
|
||||||
platforms: ['ubuntu', 'debian', 'arch', 'fedora'],
|
platforms: ["ubuntu", "debian", "arch", "fedora"],
|
||||||
priority: 7,
|
priority: 7,
|
||||||
reason: 'Open gaming platform',
|
reason: "Open gaming platform",
|
||||||
experienceLevel: ['intermediate', 'advanced']
|
experienceLevel: ["intermediate", "advanced"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
packageName: 'wine',
|
packageName: "wine",
|
||||||
platforms: ['ubuntu', 'debian', 'arch', 'fedora', 'macos'],
|
platforms: ["ubuntu", "debian", "arch", "fedora", "macos"],
|
||||||
priority: 6,
|
priority: 6,
|
||||||
reason: 'Windows compatibility layer',
|
reason: "Windows compatibility layer",
|
||||||
experienceLevel: ['advanced']
|
experienceLevel: ["advanced"],
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
category: 'productivity',
|
category: "productivity",
|
||||||
description: 'Office, note-taking and productivity tools',
|
description: "Office, note-taking and productivity tools",
|
||||||
icon: '📝',
|
icon: "📝",
|
||||||
packages: [
|
packages: [
|
||||||
{
|
{
|
||||||
packageName: 'libreoffice',
|
packageName: "libreoffice",
|
||||||
platforms: ['windows', 'macos', 'ubuntu', 'debian', 'arch', 'fedora'],
|
platforms: ["windows", "macos", "ubuntu", "debian", "arch", "fedora"],
|
||||||
priority: 10,
|
priority: 10,
|
||||||
reason: 'Free office suite',
|
reason: "Free office suite",
|
||||||
experienceLevel: ['beginner', 'intermediate', 'advanced']
|
experienceLevel: ["beginner", "intermediate", "advanced"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
packageName: 'thunderbird',
|
packageName: "thunderbird",
|
||||||
platforms: ['windows', 'macos', 'ubuntu', 'debian', 'arch', 'fedora'],
|
platforms: ["windows", "macos", "ubuntu", "debian", "arch", "fedora"],
|
||||||
priority: 8,
|
priority: 8,
|
||||||
reason: 'Email client',
|
reason: "Email client",
|
||||||
experienceLevel: ['beginner', 'intermediate', 'advanced']
|
experienceLevel: ["beginner", "intermediate", "advanced"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
packageName: 'notion',
|
packageName: "notion",
|
||||||
platforms: ['windows', 'macos'],
|
platforms: ["windows", "macos"],
|
||||||
priority: 9,
|
priority: 9,
|
||||||
reason: 'All-in-one workspace',
|
reason: "All-in-one workspace",
|
||||||
experienceLevel: ['beginner', 'intermediate', 'advanced']
|
experienceLevel: ["beginner", "intermediate", "advanced"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
packageName: 'obsidian',
|
packageName: "obsidian",
|
||||||
platforms: ['windows', 'macos', 'ubuntu', 'debian'],
|
platforms: ["windows", "macos", "ubuntu", "debian"],
|
||||||
priority: 8,
|
priority: 8,
|
||||||
reason: 'Knowledge base and note-taking',
|
reason: "Knowledge base and note-taking",
|
||||||
experienceLevel: ['intermediate', 'advanced']
|
experienceLevel: ["intermediate", "advanced"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
packageName: 'keepassxc',
|
packageName: "keepassxc",
|
||||||
platforms: ['windows', 'macos', 'ubuntu', 'debian', 'arch', 'fedora'],
|
platforms: ["windows", "macos", "ubuntu", "debian", "arch", "fedora"],
|
||||||
priority: 7,
|
priority: 7,
|
||||||
reason: 'Password manager',
|
reason: "Password manager",
|
||||||
experienceLevel: ['beginner', 'intermediate', 'advanced']
|
experienceLevel: ["beginner", "intermediate", "advanced"],
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
category: 'education',
|
category: "education",
|
||||||
description: 'Educational and scientific software',
|
description: "Educational and scientific software",
|
||||||
icon: '🎓',
|
icon: "🎓",
|
||||||
packages: [
|
packages: [
|
||||||
{
|
{
|
||||||
packageName: 'anki',
|
packageName: "anki",
|
||||||
platforms: ['windows', 'macos', 'ubuntu', 'debian', 'arch', 'fedora'],
|
platforms: ["windows", "macos", "ubuntu", "debian", "arch", "fedora"],
|
||||||
priority: 9,
|
priority: 9,
|
||||||
reason: 'Flashcard application for learning',
|
reason: "Flashcard application for learning",
|
||||||
experienceLevel: ['beginner', 'intermediate', 'advanced']
|
experienceLevel: ["beginner", "intermediate", "advanced"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
packageName: 'stellarium',
|
packageName: "stellarium",
|
||||||
platforms: ['windows', 'macos', 'ubuntu', 'debian', 'arch', 'fedora'],
|
platforms: ["windows", "macos", "ubuntu", "debian", "arch", "fedora"],
|
||||||
priority: 7,
|
priority: 7,
|
||||||
reason: 'Planetarium software',
|
reason: "Planetarium software",
|
||||||
experienceLevel: ['beginner', 'intermediate', 'advanced']
|
experienceLevel: ["beginner", "intermediate", "advanced"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
packageName: 'octave',
|
packageName: "octave",
|
||||||
platforms: ['windows', 'macos', 'ubuntu', 'debian', 'arch', 'fedora'],
|
platforms: ["windows", "macos", "ubuntu", "debian", "arch", "fedora"],
|
||||||
priority: 7,
|
priority: 7,
|
||||||
reason: 'Scientific programming language',
|
reason: "Scientific programming language",
|
||||||
experienceLevel: ['intermediate', 'advanced']
|
experienceLevel: ["intermediate", "advanced"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
packageName: 'geogebra',
|
packageName: "geogebra",
|
||||||
platforms: ['windows', 'macos', 'ubuntu', 'debian'],
|
platforms: ["windows", "macos", "ubuntu", "debian"],
|
||||||
priority: 8,
|
priority: 8,
|
||||||
reason: 'Interactive mathematics software',
|
reason: "Interactive mathematics software",
|
||||||
experienceLevel: ['beginner', 'intermediate', 'advanced']
|
experienceLevel: ["beginner", "intermediate", "advanced"],
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
}
|
},
|
||||||
]
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get presets for specific categories
|
* Get presets for specific categories
|
||||||
*/
|
*/
|
||||||
export function getPresetsForCategories(categories: string[]): CategoryPreset[] {
|
export function getPresetsForCategories(
|
||||||
return RECOMMENDATION_PRESETS.filter(preset =>
|
categories: string[]
|
||||||
|
): CategoryPreset[] {
|
||||||
|
return RECOMMENDATION_PRESETS.filter((preset) =>
|
||||||
categories.includes(preset.category)
|
categories.includes(preset.category)
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -352,18 +354,18 @@ export function getPresetPackageNames(
|
|||||||
categories: string[],
|
categories: string[],
|
||||||
platformId: string
|
platformId: string
|
||||||
): string[] {
|
): string[] {
|
||||||
const presets = getPresetsForCategories(categories)
|
const presets = getPresetsForCategories(categories);
|
||||||
const packageNames = new Set<string>()
|
const packageNames = new Set<string>();
|
||||||
|
|
||||||
presets.forEach(preset => {
|
presets.forEach((preset) => {
|
||||||
preset.packages.forEach(pkg => {
|
preset.packages.forEach((pkg) => {
|
||||||
if (pkg.platforms.includes(platformId)) {
|
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[],
|
categories: string[],
|
||||||
platformId: string
|
platformId: string
|
||||||
): number | null {
|
): number | null {
|
||||||
const presets = getPresetsForCategories(categories)
|
const presets = getPresetsForCategories(categories);
|
||||||
|
|
||||||
for (const preset of presets) {
|
for (const preset of presets) {
|
||||||
const pkg = preset.packages.find(
|
const pkg = preset.packages.find(
|
||||||
p => p.packageName === packageName && p.platforms.includes(platformId)
|
(p) => p.packageName === packageName && p.platforms.includes(platformId)
|
||||||
)
|
);
|
||||||
if (pkg) {
|
if (pkg) {
|
||||||
return pkg.priority
|
return pkg.priority;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -395,14 +397,14 @@ export function getRecommendationReason(
|
|||||||
packageName: string,
|
packageName: string,
|
||||||
categories: string[]
|
categories: string[]
|
||||||
): string | null {
|
): string | null {
|
||||||
const presets = getPresetsForCategories(categories)
|
const presets = getPresetsForCategories(categories);
|
||||||
|
|
||||||
for (const preset of presets) {
|
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) {
|
if (pkg) {
|
||||||
return pkg.reason
|
return pkg.reason;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,56 +1,60 @@
|
|||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect, useCallback } from 'react'
|
import { useState, useEffect, useCallback } from "react";
|
||||||
import { UserProfile, UserCategory, ExperienceLevel } from '@/types/recommendations'
|
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
|
* Detect user's operating system from browser
|
||||||
*/
|
*/
|
||||||
function detectOS(): string {
|
function detectOS(): string {
|
||||||
if (typeof window === 'undefined') {
|
if (typeof window === "undefined") {
|
||||||
return 'unknown'
|
return "unknown";
|
||||||
}
|
}
|
||||||
|
|
||||||
const userAgent = window.navigator.userAgent.toLowerCase()
|
const userAgent = window.navigator.userAgent.toLowerCase();
|
||||||
const platform = window.navigator.platform.toLowerCase()
|
const platform = window.navigator.platform.toLowerCase();
|
||||||
|
|
||||||
// Windows
|
// Windows
|
||||||
if (userAgent.indexOf('win') !== -1 || platform.indexOf('win') !== -1) {
|
if (userAgent.indexOf("win") !== -1 || platform.indexOf("win") !== -1) {
|
||||||
return 'windows'
|
return "windows";
|
||||||
}
|
}
|
||||||
|
|
||||||
// macOS
|
// macOS
|
||||||
if (
|
if (
|
||||||
userAgent.indexOf('mac') !== -1 ||
|
userAgent.indexOf("mac") !== -1 ||
|
||||||
platform.indexOf('mac') !== -1 ||
|
platform.indexOf("mac") !== -1 ||
|
||||||
userAgent.indexOf('darwin') !== -1
|
userAgent.indexOf("darwin") !== -1
|
||||||
) {
|
) {
|
||||||
return 'macos'
|
return "macos";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Linux distros
|
// 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)
|
// Try to detect specific distro from user agent (rare but possible)
|
||||||
if (userAgent.indexOf('ubuntu') !== -1) {
|
if (userAgent.indexOf("ubuntu") !== -1) {
|
||||||
return 'ubuntu'
|
return "ubuntu";
|
||||||
}
|
}
|
||||||
if (userAgent.indexOf('fedora') !== -1) {
|
if (userAgent.indexOf("fedora") !== -1) {
|
||||||
return 'fedora'
|
return "fedora";
|
||||||
}
|
}
|
||||||
if (userAgent.indexOf('arch') !== -1) {
|
if (userAgent.indexOf("arch") !== -1) {
|
||||||
return 'arch'
|
return "arch";
|
||||||
}
|
}
|
||||||
if (userAgent.indexOf('debian') !== -1) {
|
if (userAgent.indexOf("debian") !== -1) {
|
||||||
return 'debian'
|
return "debian";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default to Ubuntu for generic Linux
|
// Default to Ubuntu for generic Linux
|
||||||
return 'ubuntu'
|
return "ubuntu";
|
||||||
}
|
}
|
||||||
|
|
||||||
return 'unknown'
|
return "unknown";
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -61,106 +65,118 @@ function getDefaultProfile(): UserProfile {
|
|||||||
categories: [],
|
categories: [],
|
||||||
detectedOS: detectOS(),
|
detectedOS: detectOS(),
|
||||||
selectedOS: undefined,
|
selectedOS: undefined,
|
||||||
experienceLevel: 'beginner',
|
experienceLevel: "beginner",
|
||||||
hasCompletedOnboarding: false,
|
hasCompletedOnboarding: false,
|
||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
lastUpdated: new Date().toISOString()
|
lastUpdated: new Date().toISOString(),
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hook for managing user recommendation profile in localStorage
|
* Hook for managing user recommendation profile in localStorage
|
||||||
*/
|
*/
|
||||||
export function useRecommendationProfile() {
|
export function useRecommendationProfile() {
|
||||||
const [profile, setProfile] = useState<UserProfile>(getDefaultProfile())
|
const [profile, setProfile] = useState<UserProfile>(getDefaultProfile());
|
||||||
const [isLoading, setIsLoading] = useState(true)
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
|
||||||
// Load profile from localStorage on mount
|
// Load profile from localStorage on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
try {
|
try {
|
||||||
const stored = localStorage.getItem(STORAGE_KEY)
|
const stored = localStorage.getItem(STORAGE_KEY);
|
||||||
if (stored) {
|
if (stored) {
|
||||||
const parsed = JSON.parse(stored) as UserProfile
|
const parsed = JSON.parse(stored) as UserProfile;
|
||||||
|
|
||||||
// Update detectedOS if it changed
|
// Update detectedOS if it changed
|
||||||
const currentOS = detectOS()
|
const currentOS = detectOS();
|
||||||
if (parsed.detectedOS !== currentOS) {
|
if (parsed.detectedOS !== currentOS) {
|
||||||
parsed.detectedOS = currentOS
|
parsed.detectedOS = currentOS;
|
||||||
}
|
}
|
||||||
|
|
||||||
setProfile(parsed)
|
setProfile(parsed);
|
||||||
} else {
|
} else {
|
||||||
// First time user - save default profile
|
// First time user - save default profile
|
||||||
const defaultProfile = getDefaultProfile()
|
const defaultProfile = getDefaultProfile();
|
||||||
setProfile(defaultProfile)
|
setProfile(defaultProfile);
|
||||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(defaultProfile))
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(defaultProfile));
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading user profile:', error)
|
console.error("Error loading user profile:", error);
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false)
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
}, [])
|
}, []);
|
||||||
|
|
||||||
// Save profile to localStorage
|
// Save profile to localStorage
|
||||||
const saveProfile = useCallback((newProfile: Partial<UserProfile>) => {
|
const saveProfile = useCallback(
|
||||||
|
(newProfile: Partial<UserProfile>) => {
|
||||||
try {
|
try {
|
||||||
const updated: UserProfile = {
|
const updated: UserProfile = {
|
||||||
...profile,
|
...profile,
|
||||||
...newProfile,
|
...newProfile,
|
||||||
lastUpdated: new Date().toISOString()
|
lastUpdated: new Date().toISOString(),
|
||||||
}
|
};
|
||||||
setProfile(updated)
|
setProfile(updated);
|
||||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(updated))
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(updated));
|
||||||
return true
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error saving user profile:', error)
|
console.error("Error saving user profile:", error);
|
||||||
return false
|
return false;
|
||||||
}
|
}
|
||||||
}, [profile])
|
},
|
||||||
|
[profile]
|
||||||
|
);
|
||||||
|
|
||||||
// Update categories
|
// Update categories
|
||||||
const updateCategories = useCallback((categories: UserCategory[]) => {
|
const updateCategories = useCallback(
|
||||||
return saveProfile({ categories })
|
(categories: UserCategory[]) => {
|
||||||
}, [saveProfile])
|
return saveProfile({ categories });
|
||||||
|
},
|
||||||
|
[saveProfile]
|
||||||
|
);
|
||||||
|
|
||||||
// Update selected OS (manual override)
|
// Update selected OS (manual override)
|
||||||
const updateSelectedOS = useCallback((os: string) => {
|
const updateSelectedOS = useCallback(
|
||||||
return saveProfile({ selectedOS: os })
|
(os: string) => {
|
||||||
}, [saveProfile])
|
return saveProfile({ selectedOS: os });
|
||||||
|
},
|
||||||
|
[saveProfile]
|
||||||
|
);
|
||||||
|
|
||||||
// Update experience level
|
// Update experience level
|
||||||
const updateExperienceLevel = useCallback((level: ExperienceLevel) => {
|
const updateExperienceLevel = useCallback(
|
||||||
return saveProfile({ experienceLevel: level })
|
(level: ExperienceLevel) => {
|
||||||
}, [saveProfile])
|
return saveProfile({ experienceLevel: level });
|
||||||
|
},
|
||||||
|
[saveProfile]
|
||||||
|
);
|
||||||
|
|
||||||
// Mark onboarding as completed
|
// Mark onboarding as completed
|
||||||
const completeOnboarding = useCallback(() => {
|
const completeOnboarding = useCallback(() => {
|
||||||
return saveProfile({ hasCompletedOnboarding: true })
|
return saveProfile({ hasCompletedOnboarding: true });
|
||||||
}, [saveProfile])
|
}, [saveProfile]);
|
||||||
|
|
||||||
// Reset profile
|
// Reset profile
|
||||||
const resetProfile = useCallback(() => {
|
const resetProfile = useCallback(() => {
|
||||||
try {
|
try {
|
||||||
const defaultProfile = getDefaultProfile()
|
const defaultProfile = getDefaultProfile();
|
||||||
setProfile(defaultProfile)
|
setProfile(defaultProfile);
|
||||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(defaultProfile))
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(defaultProfile));
|
||||||
return true
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error resetting user profile:', error)
|
console.error("Error resetting user profile:", error);
|
||||||
return false
|
return false;
|
||||||
}
|
}
|
||||||
}, [])
|
}, []);
|
||||||
|
|
||||||
// Get effective OS (selectedOS or detectedOS)
|
// Get effective OS (selectedOS or detectedOS)
|
||||||
const getEffectiveOS = useCallback((): string => {
|
const getEffectiveOS = useCallback((): string => {
|
||||||
return profile.selectedOS || profile.detectedOS || 'ubuntu'
|
return profile.selectedOS || profile.detectedOS || "ubuntu";
|
||||||
}, [profile])
|
}, [profile]);
|
||||||
|
|
||||||
// Check if profile is complete enough for recommendations
|
// Check if profile is complete enough for recommendations
|
||||||
const isProfileComplete = useCallback((): boolean => {
|
const isProfileComplete = useCallback((): boolean => {
|
||||||
return profile.categories.length > 0 && getEffectiveOS() !== 'unknown'
|
return profile.categories.length > 0 && getEffectiveOS() !== "unknown";
|
||||||
}, [profile, getEffectiveOS])
|
}, [profile, getEffectiveOS]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
profile,
|
profile,
|
||||||
@@ -174,6 +190,6 @@ export function useRecommendationProfile() {
|
|||||||
getEffectiveOS,
|
getEffectiveOS,
|
||||||
isProfileComplete,
|
isProfileComplete,
|
||||||
detectedOS: profile.detectedOS,
|
detectedOS: profile.detectedOS,
|
||||||
hasCompletedOnboarding: profile.hasCompletedOnboarding
|
hasCompletedOnboarding: profile.hasCompletedOnboarding,
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
+77
-51
@@ -1,101 +1,127 @@
|
|||||||
import { Platform, Package, FilterOptions } from '@/types'
|
import { Platform, Package, FilterOptions } from "@/types";
|
||||||
import { RecommendationRequest, RecommendationResponse } from '@/types/recommendations'
|
import {
|
||||||
|
RecommendationRequest,
|
||||||
|
RecommendationResponse,
|
||||||
|
} from "@/types/recommendations";
|
||||||
|
|
||||||
const API_BASE_URL = (process.env.NEXT_PUBLIC_API_URL && process.env.NEXT_PUBLIC_API_URL.trim() !== '')
|
const API_BASE_URL =
|
||||||
? process.env.NEXT_PUBLIC_API_URL.replace(/\/$/, '')
|
process.env.NEXT_PUBLIC_API_URL &&
|
||||||
: '/api'
|
process.env.NEXT_PUBLIC_API_URL.trim() !== ""
|
||||||
|
? process.env.NEXT_PUBLIC_API_URL.replace(/\/$/, "")
|
||||||
|
: "/api";
|
||||||
|
|
||||||
class ApiClient {
|
class ApiClient {
|
||||||
private async request<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
|
private async request<T>(
|
||||||
const url = `${API_BASE_URL}${endpoint}`
|
endpoint: string,
|
||||||
|
options: RequestInit = {}
|
||||||
|
): Promise<T> {
|
||||||
|
const url = `${API_BASE_URL}${endpoint}`;
|
||||||
|
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
"Content-Type": "application/json",
|
||||||
...options.headers,
|
...options.headers,
|
||||||
},
|
},
|
||||||
...options,
|
...options,
|
||||||
})
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
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
|
// Platform operations
|
||||||
async getPlatforms(): Promise<Platform[]> {
|
async getPlatforms(): Promise<Platform[]> {
|
||||||
return this.request<Platform[]>('/platforms')
|
return this.request<Platform[]>("/platforms");
|
||||||
}
|
}
|
||||||
|
|
||||||
async getPlatform(id: string): Promise<Platform | null> {
|
async getPlatform(id: string): Promise<Platform | null> {
|
||||||
try {
|
try {
|
||||||
return await this.request<Platform>(`/platforms/${id}`)
|
return await this.request<Platform>(`/platforms/${id}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return null
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Package operations
|
// Package operations
|
||||||
async getPackages(filters: FilterOptions = {}): Promise<{ packages: Package[], total: number }> {
|
async getPackages(
|
||||||
const params = new URLSearchParams()
|
filters: FilterOptions = {}
|
||||||
|
): Promise<{ packages: Package[]; total: number }> {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
|
||||||
if (filters.platform_id) params.append('platform_id', filters.platform_id)
|
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.category_id)
|
||||||
if (filters.type) params.append('type', filters.type)
|
params.append("category_id", filters.category_id.toString());
|
||||||
if (filters.repository) params.append('repository', filters.repository)
|
if (filters.type) params.append("type", filters.type);
|
||||||
if (filters.search) params.append('search', filters.search)
|
if (filters.repository) params.append("repository", filters.repository);
|
||||||
if (filters.limit) params.append('limit', filters.limit.toString())
|
if (filters.search) params.append("search", filters.search);
|
||||||
if (filters.offset) params.append('offset', filters.offset.toString())
|
if (filters.limit) params.append("limit", filters.limit.toString());
|
||||||
if (filters.sort_by) params.append('sort_by', filters.sort_by)
|
if (filters.offset) params.append("offset", filters.offset.toString());
|
||||||
if (filters.sort_order) params.append('sort_order', filters.sort_order)
|
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()}` : ''
|
const query = params.toString() ? `?${params.toString()}` : "";
|
||||||
return this.request<{ packages: Package[], total: number }>(`/packages${query}`)
|
return this.request<{ packages: Package[]; total: number }>(
|
||||||
|
`/packages${query}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getPackage(id: string): Promise<Package | null> {
|
async getPackage(id: string): Promise<Package | null> {
|
||||||
try {
|
try {
|
||||||
return await this.request<Package>(`/packages/${id}`)
|
return await this.request<Package>(`/packages/${id}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return null
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sync operations
|
// Sync operations
|
||||||
async syncDebianPackages(): Promise<{ message: string, timestamp: string }> {
|
async syncDebianPackages(): Promise<{ message: string; timestamp: string }> {
|
||||||
return this.request<{ message: string, timestamp: string }>('/sync', {
|
return this.request<{ message: string; timestamp: string }>("/sync", {
|
||||||
method: 'POST',
|
method: "POST",
|
||||||
body: JSON.stringify({ source: 'debian-official' }),
|
body: JSON.stringify({ source: "debian-official" }),
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async syncUbuntuPackages(): Promise<{ message: string, timestamp: string }> {
|
async syncUbuntuPackages(): Promise<{ message: string; timestamp: string }> {
|
||||||
return this.request<{ message: string, timestamp: string }>('/sync', {
|
return this.request<{ message: string; timestamp: string }>("/sync", {
|
||||||
method: 'POST',
|
method: "POST",
|
||||||
body: JSON.stringify({ source: 'ubuntu-official' }),
|
body: JSON.stringify({ source: "ubuntu-official" }),
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async syncAllDebianPackages(): Promise<{ message: string, timestamp: string }> {
|
async syncAllDebianPackages(): Promise<{
|
||||||
return this.request<{ message: string, timestamp: string }>('/sync', {
|
message: string;
|
||||||
method: 'POST',
|
timestamp: string;
|
||||||
body: JSON.stringify({ source: 'all-official' }),
|
}> {
|
||||||
})
|
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[] }> {
|
async getSyncStatus(): Promise<{
|
||||||
return this.request<{ status: string, last_sync: string | null, platforms: string[] }>('/sync')
|
status: string;
|
||||||
|
last_sync: string | null;
|
||||||
|
platforms: string[];
|
||||||
|
}> {
|
||||||
|
return this.request<{
|
||||||
|
status: string;
|
||||||
|
last_sync: string | null;
|
||||||
|
platforms: string[];
|
||||||
|
}>("/sync");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Recommendation operations
|
// Recommendation operations
|
||||||
async getRecommendations(request: RecommendationRequest): Promise<RecommendationResponse> {
|
async getRecommendations(
|
||||||
return this.request<RecommendationResponse>('/recommendations', {
|
request: RecommendationRequest
|
||||||
method: 'POST',
|
): Promise<RecommendationResponse> {
|
||||||
|
return this.request<RecommendationResponse>("/recommendations", {
|
||||||
|
method: "POST",
|
||||||
body: JSON.stringify(request),
|
body: JSON.stringify(request),
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const apiClient = new ApiClient()
|
export const apiClient = new ApiClient();
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
import { PackageService } from './packageService'
|
import { PackageService } from "./packageService";
|
||||||
import { Package } from '@/models/Package'
|
import { Package } from "@/models/Package";
|
||||||
import {
|
import {
|
||||||
RecommendationRequest,
|
RecommendationRequest,
|
||||||
RecommendedPackage,
|
RecommendedPackage,
|
||||||
UserCategory,
|
UserCategory,
|
||||||
ExperienceLevel
|
ExperienceLevel,
|
||||||
} from '@/types/recommendations'
|
} from "@/types/recommendations";
|
||||||
import {
|
import {
|
||||||
getPresetPackageNames,
|
getPresetPackageNames,
|
||||||
getPresetPriority,
|
getPresetPriority,
|
||||||
getRecommendationReason
|
getRecommendationReason,
|
||||||
} from '@/data/recommendationPresets'
|
} from "@/data/recommendationPresets";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Recommendation scoring weights
|
* Recommendation scoring weights
|
||||||
@@ -19,8 +19,8 @@ const SCORING_WEIGHTS = {
|
|||||||
CATEGORY_MATCH: 0.4,
|
CATEGORY_MATCH: 0.4,
|
||||||
POPULARITY: 0.3,
|
POPULARITY: 0.3,
|
||||||
OS_COMPATIBILITY: 0.2,
|
OS_COMPATIBILITY: 0.2,
|
||||||
PRESET_BOOST: 0.1
|
PRESET_BOOST: 0.1,
|
||||||
}
|
};
|
||||||
|
|
||||||
export class RecommendationService {
|
export class RecommendationService {
|
||||||
/**
|
/**
|
||||||
@@ -29,40 +29,48 @@ export class RecommendationService {
|
|||||||
static async generateRecommendations(
|
static async generateRecommendations(
|
||||||
request: RecommendationRequest
|
request: RecommendationRequest
|
||||||
): Promise<RecommendedPackage[]> {
|
): Promise<RecommendedPackage[]> {
|
||||||
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
|
// 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
|
// Step 2: Fetch packages from database
|
||||||
// First, get preset packages
|
// First, get preset packages
|
||||||
const presetPackages = await this.fetchPresetPackages(
|
const presetPackages = await this.fetchPresetPackages(
|
||||||
presetPackageNames,
|
presetPackageNames,
|
||||||
platform_id
|
platform_id
|
||||||
)
|
);
|
||||||
|
|
||||||
// Then, get additional packages from categories
|
// Then, get additional packages from categories
|
||||||
const categoryPackages = await this.fetchCategoryPackages(
|
const categoryPackages = await this.fetchCategoryPackages(
|
||||||
categories,
|
categories,
|
||||||
platform_id,
|
platform_id,
|
||||||
limit * 2 // Fetch more to ensure we have enough after filtering
|
limit * 2 // Fetch more to ensure we have enough after filtering
|
||||||
)
|
);
|
||||||
|
|
||||||
// Step 3: Combine and deduplicate
|
// Step 3: Combine and deduplicate
|
||||||
const allPackages = this.deduplicatePackages([
|
const allPackages = this.deduplicatePackages([
|
||||||
...presetPackages,
|
...presetPackages,
|
||||||
...categoryPackages
|
...categoryPackages,
|
||||||
])
|
]);
|
||||||
|
|
||||||
// Step 4: Score and rank packages
|
// Step 4: Score and rank packages
|
||||||
const scoredPackages = allPackages.map(pkg =>
|
const scoredPackages = allPackages.map((pkg) =>
|
||||||
this.scorePackage(pkg, categories, platform_id, presetPackageNames, experienceLevel)
|
this.scorePackage(
|
||||||
|
pkg,
|
||||||
|
categories,
|
||||||
|
platform_id,
|
||||||
|
presetPackageNames,
|
||||||
|
experienceLevel
|
||||||
)
|
)
|
||||||
|
);
|
||||||
|
|
||||||
// Step 5: Sort by score and limit results
|
// 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
|
platformId: string
|
||||||
): Promise<Package[]> {
|
): Promise<Package[]> {
|
||||||
if (packageNames.length === 0) {
|
if (packageNames.length === 0) {
|
||||||
return []
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Fetch packages by exact name match
|
// Fetch packages by exact name match
|
||||||
const packages: Package[] = []
|
const packages: Package[] = [];
|
||||||
|
|
||||||
for (const name of packageNames) {
|
for (const name of packageNames) {
|
||||||
const result = await PackageService.getMany({
|
const result = await PackageService.getMany({
|
||||||
platform_id: platformId,
|
platform_id: platformId,
|
||||||
search: name,
|
search: name,
|
||||||
limit: 1,
|
limit: 1,
|
||||||
sort_by: 'popularity_score',
|
sort_by: "popularity_score",
|
||||||
sort_order: 'desc'
|
sort_order: "desc",
|
||||||
})
|
});
|
||||||
|
|
||||||
// Only add if exact match
|
// Only add if exact match
|
||||||
if (result.packages.length > 0 && result.packages[0].name === name) {
|
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) {
|
} catch (error) {
|
||||||
console.error('Error fetching preset packages:', error)
|
console.error("Error fetching preset packages:", error);
|
||||||
return []
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,20 +121,20 @@ export class RecommendationService {
|
|||||||
try {
|
try {
|
||||||
// Map user categories to database categories
|
// Map user categories to database categories
|
||||||
const categoryMap: Record<UserCategory, string[]> = {
|
const categoryMap: Record<UserCategory, string[]> = {
|
||||||
'development': ['Development', 'Internet'],
|
development: ["Development", "Internet"],
|
||||||
'design': ['Graphics', 'Multimedia'],
|
design: ["Graphics", "Multimedia"],
|
||||||
'multimedia': ['Multimedia', 'Graphics'],
|
multimedia: ["Multimedia", "Graphics"],
|
||||||
'system-tools': ['System', 'Utilities'],
|
"system-tools": ["System", "Utilities"],
|
||||||
'gaming': ['Games'],
|
gaming: ["Games"],
|
||||||
'productivity': ['Office', 'Utilities'],
|
productivity: ["Office", "Utilities"],
|
||||||
'education': ['Science', 'Education']
|
education: ["Science", "Education"],
|
||||||
}
|
};
|
||||||
|
|
||||||
// Get all matching packages
|
// Get all matching packages
|
||||||
const packages: Package[] = []
|
const packages: Package[] = [];
|
||||||
|
|
||||||
for (const category of categories) {
|
for (const category of categories) {
|
||||||
const dbCategories = categoryMap[category] || []
|
const dbCategories = categoryMap[category] || [];
|
||||||
|
|
||||||
// Note: Since we don't have category filtering in current API,
|
// Note: Since we don't have category filtering in current API,
|
||||||
// we'll fetch by popularity and filter client-side
|
// we'll fetch by popularity and filter client-side
|
||||||
@@ -134,17 +142,17 @@ export class RecommendationService {
|
|||||||
const result = await PackageService.getMany({
|
const result = await PackageService.getMany({
|
||||||
platform_id: platformId,
|
platform_id: platformId,
|
||||||
limit: Math.ceil(limit / categories.length),
|
limit: Math.ceil(limit / categories.length),
|
||||||
sort_by: 'popularity_score',
|
sort_by: "popularity_score",
|
||||||
sort_order: 'desc'
|
sort_order: "desc",
|
||||||
})
|
});
|
||||||
|
|
||||||
packages.push(...result.packages)
|
packages.push(...result.packages);
|
||||||
}
|
}
|
||||||
|
|
||||||
return packages
|
return packages;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching category packages:', error)
|
console.error("Error fetching category packages:", error);
|
||||||
return []
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,14 +160,14 @@ export class RecommendationService {
|
|||||||
* Remove duplicate packages (by ID)
|
* Remove duplicate packages (by ID)
|
||||||
*/
|
*/
|
||||||
private static deduplicatePackages(packages: Package[]): Package[] {
|
private static deduplicatePackages(packages: Package[]): Package[] {
|
||||||
const seen = new Set<string>()
|
const seen = new Set<string>();
|
||||||
return packages.filter(pkg => {
|
return packages.filter((pkg) => {
|
||||||
if (seen.has(pkg.id)) {
|
if (seen.has(pkg.id)) {
|
||||||
return false
|
return false;
|
||||||
}
|
}
|
||||||
seen.add(pkg.id)
|
seen.add(pkg.id);
|
||||||
return true
|
return true;
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -172,65 +180,67 @@ export class RecommendationService {
|
|||||||
presetPackageNames: string[],
|
presetPackageNames: string[],
|
||||||
experienceLevel?: ExperienceLevel
|
experienceLevel?: ExperienceLevel
|
||||||
): RecommendedPackage {
|
): RecommendedPackage {
|
||||||
let score = 0
|
let score = 0;
|
||||||
let reason = ''
|
let reason = "";
|
||||||
const isPresetMatch = presetPackageNames.includes(pkg.name)
|
const isPresetMatch = presetPackageNames.includes(pkg.name);
|
||||||
|
|
||||||
// 1. Category Match Score (40%)
|
// 1. Category Match Score (40%)
|
||||||
// For preset packages, this is always high
|
// For preset packages, this is always high
|
||||||
const categoryScore = isPresetMatch ? 1.0 : 0.5
|
const categoryScore = isPresetMatch ? 1.0 : 0.5;
|
||||||
score += categoryScore * SCORING_WEIGHTS.CATEGORY_MATCH
|
score += categoryScore * SCORING_WEIGHTS.CATEGORY_MATCH;
|
||||||
|
|
||||||
// 2. Popularity Score (30%)
|
// 2. Popularity Score (30%)
|
||||||
// Normalize popularity_score (0-100) to 0-1
|
// Normalize popularity_score (0-100) to 0-1
|
||||||
const popularityScore = (pkg.popularity_score || 0) / 100
|
const popularityScore = (pkg.popularity_score || 0) / 100;
|
||||||
score += popularityScore * SCORING_WEIGHTS.POPULARITY
|
score += popularityScore * SCORING_WEIGHTS.POPULARITY;
|
||||||
|
|
||||||
// 3. OS Compatibility Score (20%)
|
// 3. OS Compatibility Score (20%)
|
||||||
// All packages from DB should be compatible, so this is always 1.0
|
// All packages from DB should be compatible, so this is always 1.0
|
||||||
const osScore = 1.0
|
const osScore = 1.0;
|
||||||
score += osScore * SCORING_WEIGHTS.OS_COMPATIBILITY
|
score += osScore * SCORING_WEIGHTS.OS_COMPATIBILITY;
|
||||||
|
|
||||||
// 4. Preset Boost (10%)
|
// 4. Preset Boost (10%)
|
||||||
// Extra boost for preset packages based on priority
|
// Extra boost for preset packages based on priority
|
||||||
let presetBoost = 0
|
let presetBoost = 0;
|
||||||
if (isPresetMatch) {
|
if (isPresetMatch) {
|
||||||
const priority = getPresetPriority(pkg.name, categories, platformId)
|
const priority = getPresetPriority(pkg.name, categories, platformId);
|
||||||
if (priority !== null) {
|
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
|
// Get recommendation reason from preset
|
||||||
const presetReason = getRecommendationReason(pkg.name, categories)
|
const presetReason = getRecommendationReason(pkg.name, categories);
|
||||||
if (presetReason) {
|
if (presetReason) {
|
||||||
reason = presetReason
|
reason = presetReason;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
score += presetBoost * SCORING_WEIGHTS.PRESET_BOOST
|
score += presetBoost * SCORING_WEIGHTS.PRESET_BOOST;
|
||||||
|
|
||||||
// Default reason if not from preset
|
// Default reason if not from preset
|
||||||
if (!reason) {
|
if (!reason) {
|
||||||
if (pkg.popularity_score && pkg.popularity_score > 70) {
|
if (pkg.popularity_score && pkg.popularity_score > 70) {
|
||||||
reason = 'Popular choice in the community'
|
reason = "Popular choice in the community";
|
||||||
} else {
|
} else {
|
||||||
reason = 'Recommended for your selected categories'
|
reason = "Recommended for your selected categories";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Normalize final score to 0-100
|
// Normalize final score to 0-100
|
||||||
const finalScore = Math.round(score * 100)
|
const finalScore = Math.round(score * 100);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: pkg.id,
|
id: pkg.id,
|
||||||
name: pkg.name,
|
name: pkg.name,
|
||||||
description: pkg.description || 'No description available',
|
description: pkg.description || "No description available",
|
||||||
version: pkg.version || 'latest',
|
version: pkg.version || "latest",
|
||||||
category: typeof pkg.category === 'string' ? pkg.category : pkg.category?.name,
|
category:
|
||||||
license: typeof pkg.license === 'string' ? pkg.license : pkg.license?.name,
|
typeof pkg.category === "string" ? pkg.category : pkg.category?.name,
|
||||||
type: pkg.type || 'cli',
|
license:
|
||||||
|
typeof pkg.license === "string" ? pkg.license : pkg.license?.name,
|
||||||
|
type: pkg.type || "cli",
|
||||||
platform: pkg.platform,
|
platform: pkg.platform,
|
||||||
platform_id: pkg.platform_id,
|
platform_id: pkg.platform_id,
|
||||||
repository: pkg.repository || 'official',
|
repository: pkg.repository || "official",
|
||||||
download_url: pkg.download_url,
|
download_url: pkg.download_url,
|
||||||
lastUpdated: pkg.last_updated ? pkg.last_updated.toString() : undefined,
|
lastUpdated: pkg.last_updated ? pkg.last_updated.toString() : undefined,
|
||||||
downloads: pkg.downloads_count,
|
downloads: pkg.downloads_count,
|
||||||
@@ -239,8 +249,8 @@ export class RecommendationService {
|
|||||||
tags: pkg.tags,
|
tags: pkg.tags,
|
||||||
recommendationScore: finalScore,
|
recommendationScore: finalScore,
|
||||||
recommendationReason: reason,
|
recommendationReason: reason,
|
||||||
presetMatch: isPresetMatch
|
presetMatch: isPresetMatch,
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -253,8 +263,8 @@ export class RecommendationService {
|
|||||||
return this.generateRecommendations({
|
return this.generateRecommendations({
|
||||||
platform_id: platformId,
|
platform_id: platformId,
|
||||||
categories: [primaryCategory],
|
categories: [primaryCategory],
|
||||||
limit: 5
|
limit: 5,
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -265,30 +275,30 @@ export class RecommendationService {
|
|||||||
categories: UserCategory[],
|
categories: UserCategory[],
|
||||||
totalLimit: number = 20
|
totalLimit: number = 20
|
||||||
): Promise<RecommendedPackage[]> {
|
): Promise<RecommendedPackage[]> {
|
||||||
const perCategory = Math.ceil(totalLimit / categories.length)
|
const perCategory = Math.ceil(totalLimit / categories.length);
|
||||||
const allRecommendations: RecommendedPackage[] = []
|
const allRecommendations: RecommendedPackage[] = [];
|
||||||
|
|
||||||
for (const category of categories) {
|
for (const category of categories) {
|
||||||
const recommendations = await this.generateRecommendations({
|
const recommendations = await this.generateRecommendations({
|
||||||
platform_id: platformId,
|
platform_id: platformId,
|
||||||
categories: [category],
|
categories: [category],
|
||||||
limit: perCategory
|
limit: perCategory,
|
||||||
})
|
});
|
||||||
allRecommendations.push(...recommendations)
|
allRecommendations.push(...recommendations);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deduplicate by ID and re-sort
|
// Deduplicate by ID and re-sort
|
||||||
const seen = new Set<string>()
|
const seen = new Set<string>();
|
||||||
const deduplicated = allRecommendations.filter(pkg => {
|
const deduplicated = allRecommendations.filter((pkg) => {
|
||||||
if (seen.has(pkg.id)) {
|
if (seen.has(pkg.id)) {
|
||||||
return false
|
return false;
|
||||||
}
|
}
|
||||||
seen.add(pkg.id)
|
seen.add(pkg.id);
|
||||||
return true
|
return true;
|
||||||
})
|
});
|
||||||
|
|
||||||
deduplicated.sort((a, b) => b.recommendationScore - a.recommendationScore)
|
deduplicated.sort((a, b) => b.recommendationScore - a.recommendationScore);
|
||||||
|
|
||||||
return deduplicated.slice(0, totalLimit)
|
return deduplicated.slice(0, totalLimit);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,99 +1,99 @@
|
|||||||
import { Package, Platform } from './index'
|
import { Package, Platform } from "./index";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* User category types for package recommendations
|
* User category types for package recommendations
|
||||||
*/
|
*/
|
||||||
export type UserCategory =
|
export type UserCategory =
|
||||||
| 'development'
|
| "development"
|
||||||
| 'design'
|
| "design"
|
||||||
| 'multimedia'
|
| "multimedia"
|
||||||
| 'system-tools'
|
| "system-tools"
|
||||||
| 'gaming'
|
| "gaming"
|
||||||
| 'productivity'
|
| "productivity"
|
||||||
| 'education'
|
| "education";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* User experience level
|
* User experience level
|
||||||
*/
|
*/
|
||||||
export type ExperienceLevel = 'beginner' | 'intermediate' | 'advanced'
|
export type ExperienceLevel = "beginner" | "intermediate" | "advanced";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* User profile stored in localStorage
|
* User profile stored in localStorage
|
||||||
*/
|
*/
|
||||||
export interface UserProfile {
|
export interface UserProfile {
|
||||||
categories: UserCategory[]
|
categories: UserCategory[];
|
||||||
detectedOS?: string
|
detectedOS?: string;
|
||||||
selectedOS?: string // Manual override
|
selectedOS?: string; // Manual override
|
||||||
experienceLevel?: ExperienceLevel
|
experienceLevel?: ExperienceLevel;
|
||||||
hasCompletedOnboarding: boolean
|
hasCompletedOnboarding: boolean;
|
||||||
createdAt: string
|
createdAt: string;
|
||||||
lastUpdated: string
|
lastUpdated: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Request payload for recommendation API
|
* Request payload for recommendation API
|
||||||
*/
|
*/
|
||||||
export interface RecommendationRequest {
|
export interface RecommendationRequest {
|
||||||
platform_id: string
|
platform_id: string;
|
||||||
categories: UserCategory[]
|
categories: UserCategory[];
|
||||||
experienceLevel?: ExperienceLevel
|
experienceLevel?: ExperienceLevel;
|
||||||
limit?: number
|
limit?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Recommended package with score
|
* Recommended package with score
|
||||||
*/
|
*/
|
||||||
export interface RecommendedPackage {
|
export interface RecommendedPackage {
|
||||||
id: string
|
id: string;
|
||||||
name: string
|
name: string;
|
||||||
description: string
|
description: string;
|
||||||
version: string
|
version: string;
|
||||||
category?: string
|
category?: string;
|
||||||
license?: string
|
license?: string;
|
||||||
type: 'gui' | 'cli'
|
type: "gui" | "cli";
|
||||||
platform?: string | any
|
platform?: string | any;
|
||||||
platform_id?: string
|
platform_id?: string;
|
||||||
repository: 'official' | 'third-party' | 'aur'
|
repository: "official" | "third-party" | "aur";
|
||||||
download_url?: string
|
download_url?: string;
|
||||||
lastUpdated?: string
|
lastUpdated?: string;
|
||||||
downloads?: number
|
downloads?: number;
|
||||||
popularity?: number
|
popularity?: number;
|
||||||
popularity_score?: number
|
popularity_score?: number;
|
||||||
tags?: string[]
|
tags?: string[];
|
||||||
recommendationScore: number
|
recommendationScore: number;
|
||||||
recommendationReason: string
|
recommendationReason: string;
|
||||||
presetMatch?: boolean
|
presetMatch?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Preset package configuration
|
* Preset package configuration
|
||||||
*/
|
*/
|
||||||
export interface PackagePreset {
|
export interface PackagePreset {
|
||||||
packageName: string
|
packageName: string;
|
||||||
platforms: string[] // ['windows', 'macos', 'ubuntu', 'arch', 'fedora']
|
platforms: string[]; // ['windows', 'macos', 'ubuntu', 'arch', 'fedora']
|
||||||
priority: number // 1-10, higher = more important
|
priority: number; // 1-10, higher = more important
|
||||||
reason: string // Why this package is recommended
|
reason: string; // Why this package is recommended
|
||||||
experienceLevel?: ExperienceLevel[] // Target experience levels
|
experienceLevel?: ExperienceLevel[]; // Target experience levels
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Category preset configuration
|
* Category preset configuration
|
||||||
*/
|
*/
|
||||||
export interface CategoryPreset {
|
export interface CategoryPreset {
|
||||||
category: UserCategory
|
category: UserCategory;
|
||||||
packages: PackagePreset[]
|
packages: PackagePreset[];
|
||||||
description: string
|
description: string;
|
||||||
icon: string
|
icon: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Recommendation response
|
* Recommendation response
|
||||||
*/
|
*/
|
||||||
export interface RecommendationResponse {
|
export interface RecommendationResponse {
|
||||||
recommendations: RecommendedPackage[]
|
recommendations: RecommendedPackage[];
|
||||||
total: number
|
total: number;
|
||||||
userProfile: {
|
userProfile: {
|
||||||
categories: UserCategory[]
|
categories: UserCategory[];
|
||||||
platform: string
|
platform: string;
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user