mirror of
https://github.com/yusufipk/RepoHub.git
synced 2026-09-11 18:46:07 +00:00
feat: Implement platform locking with shadcn-ui tooltips and remove preset match badge.
This commit is contained in:
@@ -7,16 +7,26 @@ import { apiClient } from '@/lib/api/client'
|
||||
import { useLocale } from '@/contexts/LocaleContext'
|
||||
import { Platform } from '@/types'
|
||||
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
import { Lock } from 'lucide-react'
|
||||
|
||||
interface PlatformSelectorProps {
|
||||
selectedPlatform: Platform | null
|
||||
onPlatformSelect: (platform: Platform) => void
|
||||
isLocked?: boolean
|
||||
lockedMessage?: string
|
||||
}
|
||||
|
||||
export function PlatformSelector({ selectedPlatform, onPlatformSelect }: PlatformSelectorProps) {
|
||||
export function PlatformSelector({
|
||||
selectedPlatform,
|
||||
onPlatformSelect,
|
||||
isLocked = false,
|
||||
lockedMessage = "Platform selection is locked"
|
||||
}: PlatformSelectorProps) {
|
||||
const { t } = useLocale()
|
||||
const [platforms, setPlatforms] = useState<Platform[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
|
||||
const iconSlug: Record<string, string> = {
|
||||
debian: 'debian',
|
||||
ubuntu: 'ubuntu',
|
||||
@@ -66,43 +76,73 @@ export function PlatformSelector({ selectedPlatform, onPlatformSelect }: Platfor
|
||||
return (
|
||||
<Card className="w-full">
|
||||
<CardHeader className="pb-4">
|
||||
<CardTitle className="text-lg">{t('platform.select')}</CardTitle>
|
||||
<CardTitle className="text-lg flex items-center gap-2">
|
||||
{t('platform.select')}
|
||||
{isLocked && <Lock className="h-4 w-4 text-muted-foreground" />}
|
||||
</CardTitle>
|
||||
<CardDescription className="text-sm">
|
||||
{t('platform.description')}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-0">
|
||||
<div className="grid grid-cols-3 sm:grid-cols-4 lg:grid-cols-6 gap-2">
|
||||
{platforms.map((platform) => (
|
||||
<Button
|
||||
key={platform.id}
|
||||
variant="outline"
|
||||
className={`h-12 px-2 py-1 flex flex-col items-center justify-center gap-1 rounded-md border transition-colors
|
||||
${selectedPlatform?.id === platform.id
|
||||
? 'border-primary ring-2 ring-primary/60 bg-primary/5'
|
||||
: 'border-border hover:bg-secondary/60'}`}
|
||||
onClick={() => onPlatformSelect(platform)}
|
||||
>
|
||||
<div
|
||||
className={`h-5 w-5 ${selectedPlatform?.id === platform.id ? 'text-foreground' : 'text-muted-foreground'}`}
|
||||
style={{
|
||||
WebkitMaskImage: `url(${iconBase(iconSlug[platform.id] || 'linux')})`,
|
||||
maskImage: `url(${iconBase(iconSlug[platform.id] || 'linux')})`,
|
||||
WebkitMaskRepeat: 'no-repeat',
|
||||
maskRepeat: 'no-repeat',
|
||||
WebkitMaskSize: 'contain',
|
||||
maskSize: 'contain',
|
||||
WebkitMaskPosition: 'center',
|
||||
maskPosition: 'center',
|
||||
backgroundColor: 'currentColor'
|
||||
} as React.CSSProperties}
|
||||
/>
|
||||
<div className="text-center">
|
||||
<div className="font-medium text-xs leading-tight truncate max-w-[90px]">{platform.name}</div>
|
||||
</div>
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
<TooltipProvider>
|
||||
<div className="grid grid-cols-3 sm:grid-cols-4 lg:grid-cols-6 gap-2">
|
||||
{platforms.map((platform) => {
|
||||
const isSelected = selectedPlatform?.id === platform.id
|
||||
const isDisabled = isLocked && !isSelected
|
||||
|
||||
const ButtonContent = (
|
||||
<Button
|
||||
key={platform.id}
|
||||
variant="outline"
|
||||
className={`h-12 px-2 py-1 flex flex-col items-center justify-center gap-1 rounded-md border transition-colors w-full
|
||||
${isSelected
|
||||
? 'border-primary ring-2 ring-primary/60 bg-primary/5'
|
||||
: 'border-border hover:bg-secondary/60'}
|
||||
${isDisabled ? 'opacity-50 cursor-not-allowed' : ''}
|
||||
`}
|
||||
onClick={() => !isLocked && onPlatformSelect(platform)}
|
||||
disabled={isDisabled}
|
||||
>
|
||||
<div
|
||||
className={`h-5 w-5 ${isSelected ? 'text-foreground' : 'text-muted-foreground'}`}
|
||||
style={{
|
||||
WebkitMaskImage: `url(${iconBase(iconSlug[platform.id] || 'linux')})`,
|
||||
maskImage: `url(${iconBase(iconSlug[platform.id] || 'linux')})`,
|
||||
WebkitMaskRepeat: 'no-repeat',
|
||||
maskRepeat: 'no-repeat',
|
||||
WebkitMaskSize: 'contain',
|
||||
maskSize: 'contain',
|
||||
WebkitMaskPosition: 'center',
|
||||
maskPosition: 'center',
|
||||
backgroundColor: 'currentColor'
|
||||
} as React.CSSProperties}
|
||||
/>
|
||||
<div className="text-center">
|
||||
<div className="font-medium text-xs leading-tight truncate max-w-[90px]">{platform.name}</div>
|
||||
</div>
|
||||
</Button>
|
||||
)
|
||||
|
||||
if (isLocked && !isSelected) {
|
||||
return (
|
||||
<Tooltip key={platform.id}>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="w-full cursor-not-allowed">
|
||||
{ButtonContent}
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{lockedMessage}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
return ButtonContent
|
||||
})}
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Package as PackageIcon, Star } from 'lucide-react'
|
||||
import { Package as PackageIcon } from 'lucide-react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent } from '@/components/ui/card'
|
||||
import { RecommendedPackage } from '@/types/recommendations'
|
||||
@@ -19,14 +19,7 @@ export function RecommendationCard({ pkg, isSelected, onToggle }: Recommendation
|
||||
}`}
|
||||
onClick={() => onToggle(pkg)}
|
||||
>
|
||||
{pkg.presetMatch && (
|
||||
<div className="absolute top-2 right-2">
|
||||
<span className="inline-flex items-center gap-1 px-2 py-1 rounded-full text-xs font-semibold bg-primary text-primary-foreground">
|
||||
<Star className="h-3 w-3" />
|
||||
{t('recommendations.preset_badge')}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-start gap-3 mb-3">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Package as PackageIcon, Star, Info } from 'lucide-react'
|
||||
import { Package as PackageIcon } from 'lucide-react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { RecommendedPackage } from '@/types/recommendations'
|
||||
|
||||
@@ -24,12 +24,6 @@ export function RecommendationListItem({ pkg, isSelected, onToggle }: Recommenda
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<h4 className="font-semibold text-sm truncate">{pkg.name}</h4>
|
||||
{pkg.presetMatch && (
|
||||
<span className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-xs font-semibold bg-primary text-primary-foreground">
|
||||
<Star className="h-2.5 w-2.5" />
|
||||
Essential
|
||||
</span>
|
||||
)}
|
||||
<span className="text-xs text-muted-foreground">{pkg.version}</span>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground truncate">{pkg.description}</p>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import { LocaleProvider } from '@/contexts/LocaleContext'
|
||||
import { Header } from './Header'
|
||||
import { PlatformSelector } from './PlatformSelector'
|
||||
@@ -28,7 +28,8 @@ function RepoHubAppContent({ cryptomusEnabled }: { cryptomusEnabled: boolean })
|
||||
isLoading: isProfileLoading,
|
||||
hasCompletedOnboarding,
|
||||
saveProfile,
|
||||
detectedOS
|
||||
detectedOS,
|
||||
getEffectiveOS
|
||||
} = useRecommendationProfile()
|
||||
|
||||
const [showOnboarding, setShowOnboarding] = useState(false)
|
||||
@@ -148,7 +149,24 @@ function RepoHubAppContent({ cryptomusEnabled }: { cryptomusEnabled: boolean })
|
||||
const handleCloseScriptPreview = () => {
|
||||
setGeneratedScript(null)
|
||||
}
|
||||
// Track previous effective OS to detect profile changes
|
||||
const prevEffectiveOS = useRef<string | null>(null)
|
||||
|
||||
// Calculate current effective OS
|
||||
const effectiveOS = getEffectiveOS()
|
||||
|
||||
// Sync platform selection with profile changes
|
||||
useEffect(() => {
|
||||
if (hasCompletedOnboarding && availablePlatforms.length > 0) {
|
||||
const platform = availablePlatforms.find(p => p.id === effectiveOS)
|
||||
|
||||
// If profile OS changed, or if no platform is selected yet, update selection
|
||||
if (platform && (effectiveOS !== prevEffectiveOS.current || !selectedPlatform)) {
|
||||
setSelectedPlatform(platform)
|
||||
prevEffectiveOS.current = effectiveOS
|
||||
}
|
||||
}
|
||||
}, [hasCompletedOnboarding, effectiveOS, availablePlatforms, selectedPlatform])
|
||||
return (
|
||||
<div key={locale} className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 dark:from-gray-900 dark:to-gray-800">
|
||||
<Header
|
||||
@@ -183,10 +201,16 @@ function RepoHubAppContent({ cryptomusEnabled }: { cryptomusEnabled: boolean })
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
{/* Platform Selector */}
|
||||
<PlatformSelector
|
||||
selectedPlatform={selectedPlatform}
|
||||
onPlatformSelect={handlePlatformSelect}
|
||||
isLocked={selectedPackages.length > 0}
|
||||
lockedMessage="Clear your selection to switch platforms"
|
||||
/>
|
||||
|
||||
{/* Package Browser */}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const TooltipProvider = TooltipPrimitive.Provider
|
||||
|
||||
const Tooltip = TooltipPrimitive.Root
|
||||
|
||||
const TooltipTrigger = TooltipPrimitive.Trigger
|
||||
|
||||
const TooltipContent = React.forwardRef<
|
||||
React.ElementRef<typeof TooltipPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
|
||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||
<TooltipPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TooltipContent.displayName = TooltipPrimitive.Content.displayName
|
||||
|
||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
|
||||
Reference in New Issue
Block a user