mirror of
https://github.com/yusufipk/RepoHub.git
synced 2026-09-11 10:36:07 +00:00
refactor: Remove experience level selection and simplify category display in onboarding modal.
This commit is contained in:
@@ -5,9 +5,8 @@ import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||
import { X, ChevronRight, ChevronLeft, Sparkles } from 'lucide-react'
|
||||
import { UserCategory, ExperienceLevel } from '@/types/recommendations'
|
||||
import { UserCategory } from '@/types/recommendations'
|
||||
import { useLocale } from '@/contexts/LocaleContext'
|
||||
import { RECOMMENDATION_PRESETS } from '@/data/recommendationPresets'
|
||||
import { CATEGORY_ICONS } from '@/constants/categoryIcons'
|
||||
|
||||
interface OnboardingModalProps {
|
||||
@@ -16,7 +15,6 @@ interface OnboardingModalProps {
|
||||
onComplete: (data: {
|
||||
categories: UserCategory[]
|
||||
selectedOS?: string
|
||||
experienceLevel: ExperienceLevel
|
||||
}) => void
|
||||
detectedOS: string
|
||||
}
|
||||
@@ -54,7 +52,6 @@ export function OnboardingModal({
|
||||
const [selectedOS, setSelectedOS] = useState<string>(
|
||||
detectedOS !== 'unknown' ? detectedOS : 'ubuntu'
|
||||
)
|
||||
const [experienceLevel, setExperienceLevel] = useState<ExperienceLevel>('beginner')
|
||||
|
||||
// Reset state when modal opens
|
||||
useEffect(() => {
|
||||
@@ -72,16 +69,12 @@ export function OnboardingModal({
|
||||
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) {
|
||||
if (step < 2) {
|
||||
setStep(step + 1)
|
||||
}
|
||||
}
|
||||
@@ -99,8 +92,7 @@ export function OnboardingModal({
|
||||
|
||||
onComplete({
|
||||
categories: selectedCategories,
|
||||
selectedOS: selectedOS,
|
||||
experienceLevel
|
||||
selectedOS: selectedOS
|
||||
})
|
||||
|
||||
// Close modal
|
||||
@@ -109,8 +101,7 @@ export function OnboardingModal({
|
||||
|
||||
const canProceed = () => {
|
||||
if (step === 1) return selectedCategories.length > 0
|
||||
if (step === 2) return selectedOS !== 'unknown'
|
||||
if (step === 3) return true
|
||||
if (step === 2) return true
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -136,7 +127,7 @@ export function OnboardingModal({
|
||||
|
||||
{/* Progress indicator */}
|
||||
<div className="flex gap-2 mt-4">
|
||||
{[1, 2, 3].map(i => (
|
||||
{[1, 2].map(i => (
|
||||
<div
|
||||
key={i}
|
||||
className={`h-2 flex-1 rounded-full transition-colors ${i <= step ? 'bg-primary' : 'bg-secondary'
|
||||
@@ -160,30 +151,25 @@ export function OnboardingModal({
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
{RECOMMENDATION_PRESETS.map(preset => {
|
||||
const Icon = CATEGORY_ICONS[preset.category]
|
||||
{(Object.entries(CATEGORY_ICONS) as [UserCategory, any][]).map(([category, Icon]) => {
|
||||
return (
|
||||
<button
|
||||
key={preset.category}
|
||||
onClick={() => handleCategoryToggle(preset.category)}
|
||||
className={`p-4 rounded-lg border-2 text-left transition-all hover:scale-105 ${selectedCategories.includes(preset.category)
|
||||
key={category}
|
||||
onClick={() => handleCategoryToggle(category)}
|
||||
className={`p-4 rounded-lg border-2 text-left transition-all hover:scale-105 ${selectedCategories.includes(category)
|
||||
? 'border-primary bg-primary/10'
|
||||
: 'border-border hover:border-primary/50'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="p-2 rounded-md bg-background border">
|
||||
{Icon && <Icon className="h-6 w-6 text-primary" />}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h4 className="font-semibold capitalize">
|
||||
{t(`categories.${preset.category}.name`)}
|
||||
</h4>
|
||||
<p className="text-xs text-muted-foreground mt-1 line-clamp-2">
|
||||
{t(`categories.${preset.category}.description`)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<Icon className="h-6 w-6 text-primary" />
|
||||
<h4 className="font-semibold capitalize">
|
||||
{t(`categories.${category}.name`)}
|
||||
</h4>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t(`categories.${category}.description`)}
|
||||
</p>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
@@ -245,39 +231,7 @@ export function OnboardingModal({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Step 3: Experience Level */}
|
||||
{step === 3 && (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-2">
|
||||
{t('onboarding.step3.title')}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground mb-4">
|
||||
{t('onboarding.step3.description')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{(['beginner', 'intermediate', 'advanced'] as ExperienceLevel[]).map(level => (
|
||||
<button
|
||||
key={level}
|
||||
onClick={() => setExperienceLevel(level)}
|
||||
className={`w-full p-4 rounded-lg border-2 text-left transition-all hover:scale-[1.02] ${experienceLevel === level
|
||||
? 'border-primary bg-primary/10'
|
||||
: 'border-border hover:border-primary/50'
|
||||
}`}
|
||||
>
|
||||
<h4 className="font-semibold capitalize mb-1">
|
||||
{t(`onboarding.step3.levels.${level}.name`)}
|
||||
</h4>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t(`onboarding.step3.levels.${level}.description`)}
|
||||
</p>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Navigation Buttons */}
|
||||
<div className="flex gap-3 pt-4">
|
||||
@@ -292,7 +246,7 @@ export function OnboardingModal({
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{step < 3 ? (
|
||||
{step < 2 ? (
|
||||
<Button
|
||||
onClick={handleNext}
|
||||
disabled={!canProceed()}
|
||||
@@ -307,8 +261,8 @@ export function OnboardingModal({
|
||||
disabled={!canProceed()}
|
||||
className="flex-1"
|
||||
>
|
||||
{t('onboarding.complete')}
|
||||
<Sparkles className="h-4 w-4 ml-2" />
|
||||
<Sparkles className="h-4 w-4 mr-2" />
|
||||
{t('common.finish')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -14,7 +14,8 @@ const translations = {
|
||||
next: "Next",
|
||||
back: "Back",
|
||||
select_all: "Select All",
|
||||
deselect_all: "Deselect All"
|
||||
deselect_all: "Deselect All",
|
||||
finish: "Finish"
|
||||
},
|
||||
platform: {
|
||||
select: "Select Your Platform",
|
||||
@@ -117,9 +118,7 @@ const translations = {
|
||||
complete: "Get My Recommendations",
|
||||
step1: {
|
||||
title: "What do you want to use your computer for?",
|
||||
description: "Select up to 3 categories that match your needs",
|
||||
selected: "{count} selected (max 3)",
|
||||
all_selected: "All {count} categories selected"
|
||||
description: "Select the categories that match your needs"
|
||||
},
|
||||
step2: {
|
||||
title: "Select your operating system",
|
||||
@@ -205,7 +204,8 @@ const translations = {
|
||||
next: "İleri",
|
||||
back: "Geri",
|
||||
select_all: "Tümünü Seç",
|
||||
deselect_all: "Seçimi Kaldır"
|
||||
deselect_all: "Seçimi Kaldır",
|
||||
finish: "Bitir"
|
||||
},
|
||||
platform: {
|
||||
select: "Platformunuzu Seçin",
|
||||
@@ -308,9 +308,7 @@ const translations = {
|
||||
complete: "Önerilerimi Getir",
|
||||
step1: {
|
||||
title: "Bilgisayarınızı ne için kullanmak istiyorsunuz?",
|
||||
description: "İhtiyaçlarınıza uygun en fazla 3 kategori seçin",
|
||||
selected: "{count} seçildi (max 3)",
|
||||
all_selected: "Tüm {count} kategori seçildi"
|
||||
description: "İhtiyaçlarınıza uygun kategorileri seçin"
|
||||
},
|
||||
step2: {
|
||||
title: "İşletim sisteminizi seçin",
|
||||
|
||||
+231
-380
@@ -1,403 +1,254 @@
|
||||
import { CategoryPreset } from "@/types/recommendations";
|
||||
import { UserCategory } from "@/types/recommendations";
|
||||
|
||||
/**
|
||||
* Curated package recommendations for each user category
|
||||
* These presets are used by the recommendation engine to suggest packages
|
||||
* Curated package recommendations for each platform and category
|
||||
* Simple structure: Platform → Category → Package names
|
||||
*
|
||||
* Edit this file to add/remove packages for each platform/category combination
|
||||
*/
|
||||
export const RECOMMENDATION_PRESETS: CategoryPreset[] = [
|
||||
{
|
||||
category: "development",
|
||||
description: "Essential tools for software development",
|
||||
packages: [
|
||||
{
|
||||
packageName: "git",
|
||||
platforms: ["windows", "macos", "ubuntu", "debian", "arch", "fedora"],
|
||||
priority: 10,
|
||||
reason: "Version control system essential for all developers",
|
||||
experienceLevel: ["beginner", "intermediate", "advanced"],
|
||||
},
|
||||
{
|
||||
packageName: "code",
|
||||
platforms: ["windows", "macos", "ubuntu", "debian"],
|
||||
priority: 9,
|
||||
reason: "Visual Studio Code - Popular code editor",
|
||||
experienceLevel: ["beginner", "intermediate", "advanced"],
|
||||
},
|
||||
{
|
||||
packageName: "visual-studio-code",
|
||||
platforms: ["arch", "fedora"],
|
||||
priority: 9,
|
||||
reason: "Visual Studio Code - Popular code editor",
|
||||
experienceLevel: ["beginner", "intermediate", "advanced"],
|
||||
},
|
||||
{
|
||||
packageName: "nodejs",
|
||||
platforms: ["windows", "macos", "ubuntu", "debian", "arch", "fedora"],
|
||||
priority: 8,
|
||||
reason: "JavaScript runtime for modern web development",
|
||||
experienceLevel: ["beginner", "intermediate", "advanced"],
|
||||
},
|
||||
{
|
||||
packageName: "python3",
|
||||
platforms: ["ubuntu", "debian", "arch", "fedora"],
|
||||
priority: 8,
|
||||
reason: "Python programming language",
|
||||
experienceLevel: ["beginner", "intermediate", "advanced"],
|
||||
},
|
||||
{
|
||||
packageName: "python",
|
||||
platforms: ["windows", "macos"],
|
||||
priority: 8,
|
||||
reason: "Python programming language",
|
||||
experienceLevel: ["beginner", "intermediate", "advanced"],
|
||||
},
|
||||
{
|
||||
packageName: "docker",
|
||||
platforms: ["windows", "macos", "ubuntu", "debian", "arch", "fedora"],
|
||||
priority: 7,
|
||||
reason: "Containerization platform for development",
|
||||
experienceLevel: ["intermediate", "advanced"],
|
||||
},
|
||||
{
|
||||
packageName: "curl",
|
||||
platforms: ["ubuntu", "debian", "arch", "fedora", "macos"],
|
||||
priority: 7,
|
||||
reason: "Command-line tool for transferring data",
|
||||
experienceLevel: ["intermediate", "advanced"],
|
||||
},
|
||||
{
|
||||
packageName: "vim",
|
||||
platforms: ["ubuntu", "debian", "arch", "fedora", "macos"],
|
||||
priority: 6,
|
||||
reason: "Powerful text editor",
|
||||
experienceLevel: ["intermediate", "advanced"],
|
||||
},
|
||||
{
|
||||
packageName: "postman",
|
||||
platforms: ["windows", "macos", "ubuntu", "debian"],
|
||||
priority: 6,
|
||||
reason: "API development and testing tool",
|
||||
experienceLevel: ["beginner", "intermediate", "advanced"],
|
||||
},
|
||||
|
||||
type PlatformId = "windows" | "macos" | "ubuntu" | "debian" | "arch" | "fedora";
|
||||
|
||||
export const PACKAGE_PRESETS: Record<PlatformId, Record<UserCategory, string[]>> = {
|
||||
windows: {
|
||||
development: [
|
||||
"git",
|
||||
"code", // Visual Studio Code
|
||||
"nodejs",
|
||||
"python",
|
||||
"docker-desktop",
|
||||
"postman",
|
||||
],
|
||||
design: [
|
||||
"gimp",
|
||||
"inkscape",
|
||||
"blender",
|
||||
],
|
||||
multimedia: [
|
||||
"vlc",
|
||||
"audacity",
|
||||
"obs-studio",
|
||||
],
|
||||
"system-tools": [
|
||||
"7zip",
|
||||
"powertoys",
|
||||
"everything",
|
||||
],
|
||||
gaming: [
|
||||
"steam",
|
||||
"discord",
|
||||
],
|
||||
productivity: [
|
||||
"notion",
|
||||
"obsidian",
|
||||
"slack",
|
||||
],
|
||||
education: [
|
||||
"anki",
|
||||
],
|
||||
},
|
||||
{
|
||||
category: "design",
|
||||
description: "Tools for graphic design, UI/UX, and creative work",
|
||||
packages: [
|
||||
{
|
||||
packageName: "gimp",
|
||||
platforms: ["windows", "macos", "ubuntu", "debian", "arch", "fedora"],
|
||||
priority: 9,
|
||||
reason: "Free and open-source image editor",
|
||||
experienceLevel: ["beginner", "intermediate", "advanced"],
|
||||
},
|
||||
{
|
||||
packageName: "inkscape",
|
||||
platforms: ["windows", "macos", "ubuntu", "debian", "arch", "fedora"],
|
||||
priority: 8,
|
||||
reason: "Professional vector graphics editor",
|
||||
experienceLevel: ["beginner", "intermediate", "advanced"],
|
||||
},
|
||||
{
|
||||
packageName: "blender",
|
||||
platforms: ["windows", "macos", "ubuntu", "debian", "arch", "fedora"],
|
||||
priority: 8,
|
||||
reason: "3D creation suite",
|
||||
experienceLevel: ["intermediate", "advanced"],
|
||||
},
|
||||
{
|
||||
packageName: "figma",
|
||||
platforms: ["windows", "macos"],
|
||||
priority: 9,
|
||||
reason: "Collaborative interface design tool",
|
||||
experienceLevel: ["beginner", "intermediate", "advanced"],
|
||||
},
|
||||
{
|
||||
packageName: "krita",
|
||||
platforms: ["windows", "macos", "ubuntu", "debian", "arch", "fedora"],
|
||||
priority: 7,
|
||||
reason: "Digital painting application",
|
||||
experienceLevel: ["beginner", "intermediate", "advanced"],
|
||||
},
|
||||
|
||||
macos: {
|
||||
development: [
|
||||
"git",
|
||||
"code", // Visual Studio Code
|
||||
"nodejs",
|
||||
"python",
|
||||
"docker",
|
||||
"postman",
|
||||
],
|
||||
design: [
|
||||
"gimp",
|
||||
"inkscape",
|
||||
"blender",
|
||||
],
|
||||
multimedia: [
|
||||
"vlc",
|
||||
"audacity",
|
||||
"obs",
|
||||
],
|
||||
"system-tools": [
|
||||
"rectangle",
|
||||
"the-unarchiver",
|
||||
],
|
||||
gaming: [
|
||||
"steam",
|
||||
"discord",
|
||||
],
|
||||
productivity: [
|
||||
"notion",
|
||||
"obsidian",
|
||||
"slack",
|
||||
],
|
||||
education: [
|
||||
"anki",
|
||||
],
|
||||
},
|
||||
{
|
||||
category: "multimedia",
|
||||
description: "Audio, video editing and media management tools",
|
||||
packages: [
|
||||
{
|
||||
packageName: "vlc",
|
||||
platforms: ["windows", "macos", "ubuntu", "debian", "arch", "fedora"],
|
||||
priority: 10,
|
||||
reason: "Versatile media player",
|
||||
experienceLevel: ["beginner", "intermediate", "advanced"],
|
||||
},
|
||||
{
|
||||
packageName: "obs-studio",
|
||||
platforms: ["windows", "macos", "ubuntu", "debian", "arch", "fedora"],
|
||||
priority: 9,
|
||||
reason: "Video recording and live streaming",
|
||||
experienceLevel: ["intermediate", "advanced"],
|
||||
},
|
||||
{
|
||||
packageName: "audacity",
|
||||
platforms: ["windows", "macos", "ubuntu", "debian", "arch", "fedora"],
|
||||
priority: 8,
|
||||
reason: "Audio editing software",
|
||||
experienceLevel: ["beginner", "intermediate", "advanced"],
|
||||
},
|
||||
{
|
||||
packageName: "ffmpeg",
|
||||
platforms: ["ubuntu", "debian", "arch", "fedora", "macos"],
|
||||
priority: 8,
|
||||
reason: "Complete multimedia framework",
|
||||
experienceLevel: ["intermediate", "advanced"],
|
||||
},
|
||||
{
|
||||
packageName: "handbrake",
|
||||
platforms: ["windows", "macos", "ubuntu", "debian", "arch", "fedora"],
|
||||
priority: 7,
|
||||
reason: "Video transcoder",
|
||||
experienceLevel: ["beginner", "intermediate", "advanced"],
|
||||
},
|
||||
{
|
||||
packageName: "kdenlive",
|
||||
platforms: ["ubuntu", "debian", "arch", "fedora"],
|
||||
priority: 7,
|
||||
reason: "Video editing software",
|
||||
experienceLevel: ["intermediate", "advanced"],
|
||||
},
|
||||
|
||||
ubuntu: {
|
||||
development: [
|
||||
"git",
|
||||
"code", // Visual Studio Code
|
||||
"nodejs",
|
||||
"python3",
|
||||
"docker.io",
|
||||
"curl",
|
||||
],
|
||||
design: [
|
||||
"gimp",
|
||||
"inkscape",
|
||||
"blender",
|
||||
],
|
||||
multimedia: [
|
||||
"vlc",
|
||||
"audacity",
|
||||
"obs-studio",
|
||||
],
|
||||
"system-tools": [
|
||||
"htop",
|
||||
"neofetch",
|
||||
"tldr",
|
||||
],
|
||||
gaming: [
|
||||
"steam",
|
||||
"discord",
|
||||
],
|
||||
productivity: [
|
||||
"libreoffice",
|
||||
"thunderbird",
|
||||
],
|
||||
education: [
|
||||
"anki",
|
||||
],
|
||||
},
|
||||
{
|
||||
category: "system-tools",
|
||||
description: "System administration, security and utilities",
|
||||
packages: [
|
||||
{
|
||||
packageName: "htop",
|
||||
platforms: ["ubuntu", "debian", "arch", "fedora", "macos"],
|
||||
priority: 9,
|
||||
reason: "Interactive process viewer",
|
||||
experienceLevel: ["beginner", "intermediate", "advanced"],
|
||||
},
|
||||
{
|
||||
packageName: "tmux",
|
||||
platforms: ["ubuntu", "debian", "arch", "fedora", "macos"],
|
||||
priority: 8,
|
||||
reason: "Terminal multiplexer",
|
||||
experienceLevel: ["intermediate", "advanced"],
|
||||
},
|
||||
{
|
||||
packageName: "wget",
|
||||
platforms: ["ubuntu", "debian", "arch", "fedora", "macos"],
|
||||
priority: 8,
|
||||
reason: "Network downloader",
|
||||
experienceLevel: ["beginner", "intermediate", "advanced"],
|
||||
},
|
||||
{
|
||||
packageName: "neofetch",
|
||||
platforms: ["ubuntu", "debian", "arch", "fedora", "macos"],
|
||||
priority: 6,
|
||||
reason: "System information tool",
|
||||
experienceLevel: ["beginner", "intermediate", "advanced"],
|
||||
},
|
||||
{
|
||||
packageName: "wireshark",
|
||||
platforms: ["windows", "macos", "ubuntu", "debian", "arch", "fedora"],
|
||||
priority: 7,
|
||||
reason: "Network protocol analyzer",
|
||||
experienceLevel: ["advanced"],
|
||||
},
|
||||
{
|
||||
packageName: "gparted",
|
||||
platforms: ["ubuntu", "debian", "arch", "fedora"],
|
||||
priority: 6,
|
||||
reason: "Partition editor",
|
||||
experienceLevel: ["intermediate", "advanced"],
|
||||
},
|
||||
|
||||
debian: {
|
||||
development: [
|
||||
"git",
|
||||
"code",
|
||||
"nodejs",
|
||||
"python3",
|
||||
"docker.io",
|
||||
"curl",
|
||||
],
|
||||
design: [
|
||||
"gimp",
|
||||
"inkscape",
|
||||
"blender",
|
||||
],
|
||||
multimedia: [
|
||||
"vlc",
|
||||
"audacity",
|
||||
"obs-studio",
|
||||
],
|
||||
"system-tools": [
|
||||
"htop",
|
||||
"neofetch",
|
||||
"tldr",
|
||||
],
|
||||
gaming: [
|
||||
"steam",
|
||||
"discord",
|
||||
],
|
||||
productivity: [
|
||||
"libreoffice",
|
||||
"thunderbird",
|
||||
],
|
||||
education: [
|
||||
"anki",
|
||||
],
|
||||
},
|
||||
{
|
||||
category: "gaming",
|
||||
description: "Gaming platforms and related tools",
|
||||
packages: [
|
||||
{
|
||||
packageName: "steam",
|
||||
platforms: ["windows", "macos", "ubuntu", "debian", "arch", "fedora"],
|
||||
priority: 10,
|
||||
reason: "Gaming platform",
|
||||
experienceLevel: ["beginner", "intermediate", "advanced"],
|
||||
},
|
||||
{
|
||||
packageName: "discord",
|
||||
platforms: ["windows", "macos", "ubuntu", "debian", "arch", "fedora"],
|
||||
priority: 9,
|
||||
reason: "Voice and chat for gamers",
|
||||
experienceLevel: ["beginner", "intermediate", "advanced"],
|
||||
},
|
||||
{
|
||||
packageName: "lutris",
|
||||
platforms: ["ubuntu", "debian", "arch", "fedora"],
|
||||
priority: 7,
|
||||
reason: "Open gaming platform",
|
||||
experienceLevel: ["intermediate", "advanced"],
|
||||
},
|
||||
{
|
||||
packageName: "wine",
|
||||
platforms: ["ubuntu", "debian", "arch", "fedora", "macos"],
|
||||
priority: 6,
|
||||
reason: "Windows compatibility layer",
|
||||
experienceLevel: ["advanced"],
|
||||
},
|
||||
|
||||
arch: {
|
||||
development: [
|
||||
"git",
|
||||
"visual-studio-code-bin",
|
||||
"nodejs",
|
||||
"python",
|
||||
"docker",
|
||||
"postman-bin",
|
||||
],
|
||||
design: [
|
||||
"gimp",
|
||||
"inkscape",
|
||||
"blender",
|
||||
],
|
||||
multimedia: [
|
||||
"vlc",
|
||||
"audacity",
|
||||
"obs-studio",
|
||||
],
|
||||
"system-tools": [
|
||||
"htop",
|
||||
"neofetch",
|
||||
"tldr",
|
||||
],
|
||||
gaming: [
|
||||
"steam",
|
||||
"discord",
|
||||
],
|
||||
productivity: [
|
||||
"libreoffice-fresh",
|
||||
"thunderbird",
|
||||
],
|
||||
education: [
|
||||
"anki",
|
||||
],
|
||||
},
|
||||
{
|
||||
category: "productivity",
|
||||
description: "Office, note-taking and productivity tools",
|
||||
packages: [
|
||||
{
|
||||
packageName: "libreoffice",
|
||||
platforms: ["windows", "macos", "ubuntu", "debian", "arch", "fedora"],
|
||||
priority: 10,
|
||||
reason: "Free office suite",
|
||||
experienceLevel: ["beginner", "intermediate", "advanced"],
|
||||
},
|
||||
{
|
||||
packageName: "thunderbird",
|
||||
platforms: ["windows", "macos", "ubuntu", "debian", "arch", "fedora"],
|
||||
priority: 8,
|
||||
reason: "Email client",
|
||||
experienceLevel: ["beginner", "intermediate", "advanced"],
|
||||
},
|
||||
{
|
||||
packageName: "notion",
|
||||
platforms: ["windows", "macos"],
|
||||
priority: 9,
|
||||
reason: "All-in-one workspace",
|
||||
experienceLevel: ["beginner", "intermediate", "advanced"],
|
||||
},
|
||||
{
|
||||
packageName: "obsidian",
|
||||
platforms: ["windows", "macos", "ubuntu", "debian"],
|
||||
priority: 8,
|
||||
reason: "Knowledge base and note-taking",
|
||||
experienceLevel: ["intermediate", "advanced"],
|
||||
},
|
||||
{
|
||||
packageName: "keepassxc",
|
||||
platforms: ["windows", "macos", "ubuntu", "debian", "arch", "fedora"],
|
||||
priority: 7,
|
||||
reason: "Password manager",
|
||||
experienceLevel: ["beginner", "intermediate", "advanced"],
|
||||
},
|
||||
|
||||
fedora: {
|
||||
development: [
|
||||
"git",
|
||||
"code",
|
||||
"nodejs",
|
||||
"python3",
|
||||
"docker",
|
||||
"curl",
|
||||
],
|
||||
design: [
|
||||
"gimp",
|
||||
"inkscape",
|
||||
"blender",
|
||||
],
|
||||
multimedia: [
|
||||
"vlc",
|
||||
"audacity",
|
||||
"obs-studio",
|
||||
],
|
||||
"system-tools": [
|
||||
"htop",
|
||||
"neofetch",
|
||||
"tldr",
|
||||
],
|
||||
gaming: [
|
||||
"steam",
|
||||
"discord",
|
||||
],
|
||||
productivity: [
|
||||
"libreoffice",
|
||||
"thunderbird",
|
||||
],
|
||||
education: [
|
||||
"anki",
|
||||
],
|
||||
},
|
||||
{
|
||||
category: "education",
|
||||
description: "Educational and scientific software",
|
||||
packages: [
|
||||
{
|
||||
packageName: "anki",
|
||||
platforms: ["windows", "macos", "ubuntu", "debian", "arch", "fedora"],
|
||||
priority: 9,
|
||||
reason: "Flashcard application for learning",
|
||||
experienceLevel: ["beginner", "intermediate", "advanced"],
|
||||
},
|
||||
{
|
||||
packageName: "stellarium",
|
||||
platforms: ["windows", "macos", "ubuntu", "debian", "arch", "fedora"],
|
||||
priority: 7,
|
||||
reason: "Planetarium software",
|
||||
experienceLevel: ["beginner", "intermediate", "advanced"],
|
||||
},
|
||||
{
|
||||
packageName: "octave",
|
||||
platforms: ["windows", "macos", "ubuntu", "debian", "arch", "fedora"],
|
||||
priority: 7,
|
||||
reason: "Scientific programming language",
|
||||
experienceLevel: ["intermediate", "advanced"],
|
||||
},
|
||||
{
|
||||
packageName: "geogebra",
|
||||
platforms: ["windows", "macos", "ubuntu", "debian"],
|
||||
priority: 8,
|
||||
reason: "Interactive mathematics software",
|
||||
experienceLevel: ["beginner", "intermediate", "advanced"],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
/**
|
||||
* Get presets for specific categories
|
||||
* Get package names for a specific platform and categories
|
||||
*/
|
||||
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[],
|
||||
platformId: string
|
||||
export function getPackagesForPlatform(
|
||||
platformId: string,
|
||||
categories: UserCategory[]
|
||||
): string[] {
|
||||
const presets = getPresetsForCategories(categories);
|
||||
const packageNames = new Set<string>();
|
||||
const platform = PACKAGE_PRESETS[platformId as PlatformId];
|
||||
if (!platform) return [];
|
||||
|
||||
presets.forEach((preset) => {
|
||||
preset.packages.forEach((pkg) => {
|
||||
if (pkg.platforms.includes(platformId)) {
|
||||
packageNames.add(pkg.packageName);
|
||||
}
|
||||
});
|
||||
const packages: string[] = [];
|
||||
categories.forEach(category => {
|
||||
const categoryPackages = platform[category] || [];
|
||||
packages.push(...categoryPackages);
|
||||
});
|
||||
|
||||
return Array.from(packageNames);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get preset priority for a package
|
||||
*/
|
||||
export function getPresetPriority(
|
||||
packageName: string,
|
||||
categories: string[],
|
||||
platformId: string
|
||||
): number | null {
|
||||
const presets = getPresetsForCategories(categories);
|
||||
|
||||
for (const preset of presets) {
|
||||
const pkg = preset.packages.find(
|
||||
(p) => p.packageName === packageName && p.platforms.includes(platformId)
|
||||
);
|
||||
if (pkg) {
|
||||
return pkg.priority;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get recommendation reason for a package
|
||||
*/
|
||||
export function getRecommendationReason(
|
||||
packageName: string,
|
||||
categories: string[]
|
||||
): string | null {
|
||||
const presets = getPresetsForCategories(categories);
|
||||
|
||||
for (const preset of presets) {
|
||||
const pkg = preset.packages.find((p) => p.packageName === packageName);
|
||||
if (pkg) {
|
||||
return pkg.reason;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
return packages;
|
||||
}
|
||||
|
||||
@@ -68,7 +68,6 @@ function getDefaultProfile(): UserProfile {
|
||||
categories: [],
|
||||
detectedOS: detectOS(),
|
||||
selectedOS: undefined,
|
||||
experienceLevel: "beginner",
|
||||
hasCompletedOnboarding: false,
|
||||
createdAt: new Date().toISOString(),
|
||||
lastUpdated: new Date().toISOString(),
|
||||
@@ -166,13 +165,7 @@ export function useRecommendationProfile() {
|
||||
[saveProfile]
|
||||
);
|
||||
|
||||
// Update experience level
|
||||
const updateExperienceLevel = useCallback(
|
||||
(level: ExperienceLevel) => {
|
||||
return saveProfile({ experienceLevel: level });
|
||||
},
|
||||
[saveProfile]
|
||||
);
|
||||
|
||||
|
||||
// Mark onboarding as completed
|
||||
const completeOnboarding = useCallback(() => {
|
||||
@@ -208,7 +201,6 @@ export function useRecommendationProfile() {
|
||||
saveProfile,
|
||||
updateCategories,
|
||||
updateSelectedOS,
|
||||
updateExperienceLevel,
|
||||
completeOnboarding,
|
||||
resetProfile,
|
||||
getEffectiveOS,
|
||||
|
||||
@@ -6,10 +6,7 @@ import {
|
||||
UserCategory,
|
||||
ExperienceLevel,
|
||||
} from "@/types/recommendations";
|
||||
import {
|
||||
getPresetPackageNames,
|
||||
RECOMMENDATION_PRESETS,
|
||||
} from "@/data/recommendationPresets";
|
||||
import { getPackagesForPlatform } from "@/data/recommendationPresets";
|
||||
|
||||
|
||||
|
||||
@@ -23,50 +20,32 @@ export class RecommendationService {
|
||||
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 = getPackagesForPlatform(platform_id, categories);
|
||||
|
||||
// Step 2: Fetch packages from database with category tracking
|
||||
// Map to track which category each package came from
|
||||
// Step 2: Fetch packages from database
|
||||
const packageCategoryMap = new Map<string, UserCategory>();
|
||||
|
||||
// First, get preset packages (tagged with their categories)
|
||||
const presetPackagesWithCategories =
|
||||
await this.fetchPresetPackagesWithCategories(
|
||||
categories,
|
||||
platform_id,
|
||||
packageCategoryMap
|
||||
);
|
||||
|
||||
// Then, get additional packages from categories
|
||||
const categoryPackagesMap = await this.fetchCategoryPackagesWithTracking(
|
||||
categories,
|
||||
// Fetch preset packages
|
||||
const presetPackages = await this.fetchPresetPackages(
|
||||
presetPackageNames,
|
||||
platform_id,
|
||||
limit * 2, // Fetch more to ensure we have enough after filtering
|
||||
categories,
|
||||
packageCategoryMap
|
||||
);
|
||||
|
||||
// Step 3: Combine and deduplicate
|
||||
const allPackages = this.deduplicatePackages([
|
||||
...presetPackagesWithCategories,
|
||||
...categoryPackagesMap,
|
||||
]);
|
||||
|
||||
// Step 4: Score and rank packages - distribute categories evenly for untracked packages
|
||||
const scoredPackages = allPackages.map((pkg, index) => {
|
||||
// If not in map, distribute evenly across categories
|
||||
const matchedCategory =
|
||||
packageCategoryMap.get(pkg.id) || categories[index % categories.length];
|
||||
// Step 3: Score and rank packages
|
||||
const scoredPackages = presetPackages.map((pkg) => {
|
||||
const matchedCategory = packageCategoryMap.get(pkg.id) || categories[0];
|
||||
return this.scorePackage(
|
||||
pkg,
|
||||
categories,
|
||||
platform_id,
|
||||
presetPackageNames,
|
||||
experienceLevel,
|
||||
matchedCategory
|
||||
);
|
||||
});
|
||||
|
||||
// Step 5: Sort by score and limit results
|
||||
// Step 4: Sort by score and limit results
|
||||
scoredPackages.sort(
|
||||
(a, b) => b.recommendationScore - a.recommendationScore
|
||||
);
|
||||
@@ -74,71 +53,24 @@ export class RecommendationService {
|
||||
return scoredPackages.slice(0, limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch packages that match preset names with category tracking
|
||||
*/
|
||||
private static async fetchPresetPackagesWithCategories(
|
||||
categories: UserCategory[],
|
||||
platformId: string,
|
||||
categoryMap: Map<string, UserCategory>
|
||||
): Promise<Package[]> {
|
||||
const packages: Package[] = [];
|
||||
|
||||
for (const category of categories) {
|
||||
const preset = RECOMMENDATION_PRESETS.find(
|
||||
(p) => p.category === category
|
||||
);
|
||||
if (!preset) continue;
|
||||
|
||||
const categoryPackageNames = preset.packages
|
||||
.filter((p) => p.platforms.includes(platformId))
|
||||
.map((p) => p.packageName);
|
||||
|
||||
for (const name of categoryPackageNames) {
|
||||
const result = await PackageService.getMany({
|
||||
platform_id: platformId,
|
||||
search: name,
|
||||
limit: 5,
|
||||
sort_by: "popularity_score",
|
||||
sort_order: "desc",
|
||||
});
|
||||
|
||||
const exactMatch = result.packages.find(
|
||||
(pkg) => pkg.name.toLowerCase() === name.toLowerCase()
|
||||
);
|
||||
|
||||
if (exactMatch) {
|
||||
packages.push(exactMatch);
|
||||
categoryMap.set(exactMatch.id, category);
|
||||
} else if (result.packages.length > 0) {
|
||||
packages.push(result.packages[0]);
|
||||
categoryMap.set(result.packages[0].id, category);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return packages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch packages that match preset names
|
||||
* Optimized: Uses single query instead of N queries
|
||||
*/
|
||||
private static async fetchPresetPackages(
|
||||
packageNames: string[],
|
||||
platformId: string
|
||||
platformId: string,
|
||||
categories: UserCategory[],
|
||||
categoryMap: Map<string, UserCategory>
|
||||
): Promise<Package[]> {
|
||||
if (packageNames.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
// Fetch all preset packages in one query
|
||||
const packages: Package[] = [];
|
||||
let categoryIndex = 0;
|
||||
|
||||
// Search for each package name (case-insensitive)
|
||||
// Note: Current API doesn't support bulk name filtering,
|
||||
// so we optimize by fetching larger batches and filtering
|
||||
for (const name of packageNames) {
|
||||
const result = await PackageService.getMany({
|
||||
platform_id: platformId,
|
||||
@@ -155,9 +87,14 @@ export class RecommendationService {
|
||||
|
||||
if (exactMatch) {
|
||||
packages.push(exactMatch);
|
||||
// Distribute categories evenly
|
||||
categoryMap.set(exactMatch.id, categories[categoryIndex % categories.length]);
|
||||
categoryIndex++;
|
||||
} else if (result.packages.length > 0) {
|
||||
// If no exact match, take the first result (most popular match)
|
||||
packages.push(result.packages[0]);
|
||||
categoryMap.set(result.packages[0].id, categories[categoryIndex % categories.length]);
|
||||
categoryIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,105 +105,7 @@ export class RecommendationService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch packages based on categories with tracking
|
||||
*/
|
||||
private static async fetchCategoryPackagesWithTracking(
|
||||
categories: UserCategory[],
|
||||
platformId: string,
|
||||
limit: number,
|
||||
categoryMap: Map<string, UserCategory>
|
||||
): Promise<Package[]> {
|
||||
try {
|
||||
const allPackages: Package[] = [];
|
||||
const seenIds = new Set<string>();
|
||||
const perCategory = Math.ceil(limit / categories.length);
|
||||
|
||||
for (const category of categories) {
|
||||
const result = await PackageService.getMany({
|
||||
platform_id: platformId,
|
||||
limit: perCategory,
|
||||
sort_by: "popularity_score",
|
||||
sort_order: "desc",
|
||||
});
|
||||
|
||||
// Add packages without duplicates and tag with category
|
||||
for (const pkg of result.packages) {
|
||||
if (!seenIds.has(pkg.id)) {
|
||||
seenIds.add(pkg.id);
|
||||
allPackages.push(pkg);
|
||||
// Tag this package with the category
|
||||
if (!categoryMap.has(pkg.id)) {
|
||||
categoryMap.set(pkg.id, category);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return allPackages;
|
||||
} catch (error) {
|
||||
console.error("Error fetching category packages:", error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch packages based on categories
|
||||
* Now properly uses database category filtering
|
||||
*/
|
||||
private static async fetchCategoryPackages(
|
||||
categories: UserCategory[],
|
||||
platformId: string,
|
||||
limit: number
|
||||
): Promise<Package[]> {
|
||||
try {
|
||||
// Map user categories to database category names (from schema.sql)
|
||||
const categoryMap: Record<UserCategory, string[]> = {
|
||||
development: ["Development", "Internet"],
|
||||
design: ["Graphics"],
|
||||
multimedia: ["Multimedia"],
|
||||
"system-tools": ["System", "Utilities"],
|
||||
gaming: ["Games"],
|
||||
productivity: ["Office"],
|
||||
education: ["Science"],
|
||||
};
|
||||
|
||||
// Get category IDs from database
|
||||
const allPackages: Package[] = [];
|
||||
const seenIds = new Set<string>();
|
||||
|
||||
for (const category of categories) {
|
||||
const dbCategoryNames = categoryMap[category] || [];
|
||||
|
||||
// Fetch packages for each DB category
|
||||
for (const dbCategoryName of dbCategoryNames) {
|
||||
// Note: We need to fetch by search since API doesn't expose category names directly
|
||||
// This is a workaround until we add category name filtering to API
|
||||
const result = await PackageService.getMany({
|
||||
platform_id: platformId,
|
||||
limit: Math.ceil(
|
||||
limit / (categories.length * dbCategoryNames.length)
|
||||
),
|
||||
sort_by: "popularity_score",
|
||||
sort_order: "desc",
|
||||
});
|
||||
|
||||
// Add packages without duplicates
|
||||
for (const pkg of result.packages) {
|
||||
if (!seenIds.has(pkg.id)) {
|
||||
seenIds.add(pkg.id);
|
||||
allPackages.push(pkg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return allPackages;
|
||||
} catch (error) {
|
||||
console.error("Error fetching category packages:", error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove duplicate packages (by ID)
|
||||
@@ -282,9 +121,6 @@ export class RecommendationService {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Score a package based on multiple factors
|
||||
*/
|
||||
/**
|
||||
* Score a package based on simplified factors (popularity & preset)
|
||||
*/
|
||||
@@ -293,7 +129,6 @@ export class RecommendationService {
|
||||
categories: UserCategory[],
|
||||
platformId: string,
|
||||
presetPackageNames: string[],
|
||||
experienceLevel?: ExperienceLevel,
|
||||
matchedCategory?: UserCategory
|
||||
): RecommendedPackage {
|
||||
const isPresetMatch = presetPackageNames.includes(pkg.name);
|
||||
|
||||
Reference in New Issue
Block a user