feat: Phase 4 — CLI showcase, CliHandoff, CORS fix, recently viewed clear
Merges feat/resilience-phase4 into main. - CliHandoff component: shows install + msdl command when Microsoft WAF blocks backend - /cli page: install guide, usage examples, contribute explanation - CLI nav dock entry and homepage promo strip - Recently viewed clear button - Backend CORS fix for Tailscale/LAN subnets
This commit is contained in:
commit
9c7ae6e6cb
8 changed files with 431 additions and 12 deletions
|
|
@ -940,7 +940,7 @@ func cleanupSessions() {
|
|||
func enableCORS(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
origin := r.Header.Get("Origin")
|
||||
if origin == "https://msdl.tech-latest.com" || strings.HasPrefix(origin, "http://localhost:") {
|
||||
if origin == "https://msdl.tech-latest.com" || strings.HasPrefix(origin, "http://localhost:") || strings.HasPrefix(origin, "http://100.") || strings.HasPrefix(origin, "http://192.168.") {
|
||||
w.Header().Set("Access-Control-Allow-Origin", origin)
|
||||
}
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import PrivacyPolicyPage from './pages/PrivacyPolicyPage'
|
|||
import DisclaimerPage from './pages/DisclaimerPage'
|
||||
import EvalPage from './pages/EvalPage'
|
||||
import EvalDetailPage from './pages/EvalDetailPage'
|
||||
import CliPage from './pages/CliPage'
|
||||
import NotFoundPage from './pages/NotFoundPage'
|
||||
import { evalSlugSet } from './data/evalProducts'
|
||||
import { useParams } from 'react-router-dom'
|
||||
|
|
@ -31,6 +32,7 @@ export default function App() {
|
|||
<Route path="/products" element={<ProductsPage />} />
|
||||
<Route path="/product/:productId" element={<ProductRouter />} />
|
||||
<Route path="/eval" element={<EvalPage />} />
|
||||
<Route path="/cli" element={<CliPage />} />
|
||||
<Route path="/about" element={<AboutPage />} />
|
||||
<Route path="/privacy-policy" element={<PrivacyPolicyPage />} />
|
||||
<Route path="/disclaimer" element={<DisclaimerPage />} />
|
||||
|
|
|
|||
142
frontend/src/components/CliHandoff.tsx
Normal file
142
frontend/src/components/CliHandoff.tsx
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
import { useState } from 'react'
|
||||
import { motion, AnimatePresence } from 'motion/react'
|
||||
import { Terminal, ChevronRight, Copy, Check, ExternalLink } from 'lucide-react'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
interface Props {
|
||||
productId: string
|
||||
langName?: string
|
||||
langDisplay?: string
|
||||
defaultOpen?: boolean
|
||||
highlight?: boolean
|
||||
}
|
||||
|
||||
const RELEASES_URL = 'https://github.com/starkSV/windows-iso-downloader/releases/latest'
|
||||
|
||||
function InstallSteps() {
|
||||
return (
|
||||
<div className="space-y-1.5">
|
||||
<p className="text-[10px] font-mono font-semibold uppercase tracking-widest text-zinc-600">1. Install</p>
|
||||
<a
|
||||
href={RELEASES_URL}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-2 px-3 py-2 rounded-lg bg-white/4 border border-white/7 text-[12px] text-zinc-400 hover:text-zinc-200 hover:border-white/12 transition-colors"
|
||||
>
|
||||
<ExternalLink size={12} className="flex-shrink-0 text-zinc-600" />
|
||||
Download msdl from GitHub Releases
|
||||
</a>
|
||||
<p className="text-[12px] text-zinc-400 leading-relaxed pt-0.5">
|
||||
Rename the downloaded file and add its folder to your PATH:<br />
|
||||
<span className="text-zinc-500">Windows →</span> <code className="font-mono text-zinc-300 bg-white/5 px-1 rounded">msdl.exe</code>
|
||||
<span className="mx-2 text-zinc-700">·</span>
|
||||
<span className="text-zinc-500">macOS/Linux →</span> <code className="font-mono text-zinc-300 bg-white/5 px-1 rounded">msdl</code>
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function RunStep({ command, langDisplay, onCopy, copied }: {
|
||||
command: string
|
||||
langDisplay?: string
|
||||
onCopy: () => void
|
||||
copied: boolean
|
||||
}) {
|
||||
return (
|
||||
<div className="space-y-1.5">
|
||||
<p className="text-[10px] font-mono font-semibold uppercase tracking-widest text-zinc-600">2. Run</p>
|
||||
<div className="rounded-lg border border-white/7 bg-[#0a0a0c] overflow-hidden">
|
||||
<div className="flex items-center justify-between px-3 py-1.5 border-b border-white/5">
|
||||
<span className="text-[10px] font-mono text-zinc-600">{langDisplay ?? 'will prompt for language'}</span>
|
||||
<button
|
||||
onClick={onCopy}
|
||||
className="flex items-center gap-1.5 text-[11px] text-zinc-500 hover:text-white transition-colors"
|
||||
>
|
||||
{copied ? <Check size={11} className="text-green-400" /> : <Copy size={11} />}
|
||||
<span>{copied ? 'Copied' : 'Copy'}</span>
|
||||
</button>
|
||||
</div>
|
||||
<div className="px-3 py-2.5 overflow-x-auto">
|
||||
<code className="text-[12px] font-mono text-zinc-300 whitespace-nowrap">
|
||||
<span className="text-zinc-600 select-none">$ </span>
|
||||
{command}
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function CliHandoff({ productId, langName, langDisplay, defaultOpen = false, highlight = false }: Props) {
|
||||
const [open, setOpen] = useState(defaultOpen)
|
||||
const [copied, setCopied] = useState(false)
|
||||
|
||||
const command = langName
|
||||
? `msdl --id ${productId} --lang "${langName}"`
|
||||
: `msdl --id ${productId}`
|
||||
|
||||
function handleCopy() {
|
||||
navigator.clipboard.writeText(command)
|
||||
setCopied(true)
|
||||
toast.success('Command copied!')
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
}
|
||||
|
||||
if (highlight) {
|
||||
return (
|
||||
<div className="rounded-xl border border-blue-500/25 bg-blue-500/5 p-4 space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Terminal size={14} className="text-blue-400 flex-shrink-0" />
|
||||
<p className="text-[12px] font-semibold text-blue-300">Use the MSDL CLI instead</p>
|
||||
<span className="ml-auto text-[10px] font-mono font-semibold px-1.5 py-0.5 rounded bg-blue-500/15 border border-blue-500/20 text-blue-400 uppercase tracking-wide">
|
||||
Recommended
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-[12px] text-zinc-500 leading-relaxed">
|
||||
Our server is currently blocked by Microsoft. The CLI fetches the link directly from your machine — no server involved.
|
||||
</p>
|
||||
<InstallSteps />
|
||||
<RunStep command={command} langDisplay={langDisplay} onCopy={handleCopy} copied={copied} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mt-3">
|
||||
<button
|
||||
onClick={() => setOpen(!open)}
|
||||
className="flex items-center gap-2 text-[12px] text-zinc-600 hover:text-zinc-400 transition-colors py-1"
|
||||
>
|
||||
<Terminal size={13} />
|
||||
<span>Get it from your machine via CLI</span>
|
||||
<motion.span
|
||||
animate={{ rotate: open ? 90 : 0 }}
|
||||
transition={{ type: 'spring', stiffness: 400, damping: 30 }}
|
||||
>
|
||||
<ChevronRight size={13} />
|
||||
</motion.span>
|
||||
</button>
|
||||
|
||||
<AnimatePresence initial={false}>
|
||||
{open && (
|
||||
<motion.div
|
||||
key="cli-handoff"
|
||||
initial={{ height: 0, opacity: 0 }}
|
||||
animate={{ height: 'auto', opacity: 1 }}
|
||||
exit={{ height: 0, opacity: 0 }}
|
||||
transition={{ type: 'spring', stiffness: 300, damping: 30 }}
|
||||
className="overflow-hidden"
|
||||
>
|
||||
<div className="mt-2 rounded-xl border border-white/7 bg-[#0d0d0f] p-4 space-y-3">
|
||||
<p className="text-[12px] text-zinc-500 leading-relaxed">
|
||||
The MSDL CLI runs the lookup directly from your machine — no server in the middle.
|
||||
</p>
|
||||
<InstallSteps />
|
||||
<RunStep command={command} langDisplay={langDisplay} onCopy={handleCopy} copied={copied} />
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { useState, useEffect } from 'react'
|
||||
import { useLocation, useNavigate } from 'react-router-dom'
|
||||
import { motion } from 'motion/react'
|
||||
import { Home, LayoutGrid, Info, ExternalLink, Shield } from 'lucide-react'
|
||||
import { Home, LayoutGrid, Info, ExternalLink, Shield, Terminal } from 'lucide-react'
|
||||
|
||||
interface DockItem {
|
||||
label: string
|
||||
|
|
@ -13,6 +13,7 @@ interface DockItem {
|
|||
const items: DockItem[] = [
|
||||
{ label: 'Home', icon: <Home size={19} />, path: '/' },
|
||||
{ label: 'Products', icon: <LayoutGrid size={19} />, path: '/products' },
|
||||
{ label: 'CLI', icon: <Terminal size={19} />, path: '/cli' },
|
||||
{ label: 'About', icon: <Info size={19} />, path: '/about' },
|
||||
{ label: 'Privacy', icon: <Shield size={19} />, path: '/privacy-policy' },
|
||||
{ label: 'TechLatest', icon: <ExternalLink size={19} />, href: 'https://tech-latest.com' },
|
||||
|
|
|
|||
|
|
@ -97,6 +97,11 @@ export default function RecentlyViewed() {
|
|||
|
||||
if (entries.length === 0) return null
|
||||
|
||||
function handleClear() {
|
||||
localStorage.removeItem(STORAGE_KEY)
|
||||
setEntries([])
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mb-8">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
|
|
@ -104,6 +109,12 @@ export default function RecentlyViewed() {
|
|||
<span className="text-[10px] font-mono font-semibold uppercase tracking-widest text-zinc-600">
|
||||
Recently viewed
|
||||
</span>
|
||||
<button
|
||||
onClick={handleClear}
|
||||
className="ml-auto text-[10px] text-zinc-700 hover:text-zinc-400 transition-colors"
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
||||
{entries.map(entry => (
|
||||
|
|
|
|||
210
frontend/src/pages/CliPage.tsx
Normal file
210
frontend/src/pages/CliPage.tsx
Normal file
|
|
@ -0,0 +1,210 @@
|
|||
import { useState } from 'react'
|
||||
import { motion } from 'motion/react'
|
||||
import { Terminal, Download, Copy, Check, ExternalLink, Zap, WifiOff, Code } from 'lucide-react'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
const RELEASES_URL = 'https://github.com/starkSV/windows-iso-downloader/releases/latest'
|
||||
const SITE_URL = 'https://msdl.tech-latest.com'
|
||||
|
||||
function CodeBlock({ code, label }: { code: string; label?: string }) {
|
||||
const [copied, setCopied] = useState(false)
|
||||
|
||||
function handleCopy() {
|
||||
navigator.clipboard.writeText(code)
|
||||
setCopied(true)
|
||||
toast.success('Copied!')
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rounded-xl border border-white/7 bg-[#0a0a0c] overflow-hidden">
|
||||
{label && (
|
||||
<div className="flex items-center justify-between px-4 py-2 border-b border-white/5">
|
||||
<span className="text-[10px] font-mono text-zinc-600">{label}</span>
|
||||
<button
|
||||
onClick={handleCopy}
|
||||
className="flex items-center gap-1.5 text-[11px] text-zinc-500 hover:text-white transition-colors"
|
||||
>
|
||||
{copied ? <Check size={11} className="text-green-400" /> : <Copy size={11} />}
|
||||
<span>{copied ? 'Copied' : 'Copy'}</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<div className="px-4 py-3 overflow-x-auto flex items-center justify-between gap-3">
|
||||
<code className="text-[13px] font-mono text-zinc-300 whitespace-nowrap">
|
||||
<span className="text-zinc-600 select-none">$ </span>
|
||||
{code}
|
||||
</code>
|
||||
{!label && (
|
||||
<button
|
||||
onClick={handleCopy}
|
||||
className="flex-shrink-0 text-zinc-600 hover:text-white transition-colors"
|
||||
>
|
||||
{copied ? <Check size={13} className="text-green-400" /> : <Copy size={13} />}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function CliPage() {
|
||||
return (
|
||||
<>
|
||||
<title>msdl CLI — Windows ISO downloader for the command line</title>
|
||||
<meta name="description" content="Download Windows ISO links directly from your machine using the msdl command-line tool. No browser, no server — works even when the web service is blocked." />
|
||||
<link rel="canonical" href={`${SITE_URL}/cli`} />
|
||||
|
||||
<div className="max-w-2xl mx-auto px-5 pt-12 pb-16">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 14 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.35 }}
|
||||
className="space-y-10"
|
||||
>
|
||||
|
||||
{/* Hero */}
|
||||
<div>
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="w-10 h-10 rounded-xl bg-emerald-500/10 border border-emerald-500/20 flex items-center justify-center flex-shrink-0">
|
||||
<Terminal size={20} className="text-emerald-400" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-[10px] font-mono font-semibold tracking-widest uppercase text-emerald-500 mb-0.5">Command-line tool</p>
|
||||
<h1 className="text-2xl font-bold text-white leading-tight">msdl CLI</h1>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-zinc-400 leading-relaxed">
|
||||
Get official Windows ISO download links directly from your machine — no browser, no server in the middle. The same links as the web app, fetched straight from Microsoft.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Why */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
|
||||
{[
|
||||
{ icon: <WifiOff size={15} />, title: 'Server-independent', desc: "Works even when msdl.tech is blocked by Microsoft's WAF." },
|
||||
{ icon: <Code size={15} />, title: 'Scriptable', desc: 'Pipe the URL directly into wget, curl, or aria2 for automation.' },
|
||||
{ icon: <Zap size={15} />, title: 'Fast', desc: 'No page load, no UI. Just the link.' },
|
||||
].map(item => (
|
||||
<div key={item.title} className="p-4 rounded-xl border border-white/7 bg-white/2 space-y-1.5">
|
||||
<div className="text-zinc-500">{item.icon}</div>
|
||||
<p className="text-sm font-medium text-white">{item.title}</p>
|
||||
<p className="text-[12px] text-zinc-500 leading-relaxed">{item.desc}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Install */}
|
||||
<div className="space-y-4">
|
||||
<h2 className="text-base font-semibold text-white">Install</h2>
|
||||
|
||||
<div className="rounded-xl border border-white/7 bg-[#111113] p-4 space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-[12px] text-zinc-400">Download the latest binary for your platform:</p>
|
||||
<a
|
||||
href={RELEASES_URL}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-1.5 text-[12px] text-emerald-400 hover:text-emerald-300 transition-colors flex-shrink-0"
|
||||
>
|
||||
<Download size={13} />
|
||||
GitHub Releases
|
||||
<ExternalLink size={11} className="opacity-60" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2 text-[12px]">
|
||||
{[
|
||||
{ os: 'Windows', file: 'msdl-windows-amd64.exe', rename: 'msdl.exe' },
|
||||
{ os: 'macOS (Apple Silicon)', file: 'msdl-darwin-arm64', rename: 'msdl' },
|
||||
{ os: 'macOS (Intel)', file: 'msdl-darwin-amd64', rename: 'msdl' },
|
||||
{ os: 'Linux', file: 'msdl-linux-amd64', rename: 'msdl' },
|
||||
].map(p => (
|
||||
<div key={p.os} className="px-3 py-2.5 rounded-lg bg-white/3 border border-white/5 space-y-1">
|
||||
<p className="text-zinc-500">{p.os}</p>
|
||||
<p className="font-mono text-zinc-300">{p.file}</p>
|
||||
<p className="text-zinc-600">Rename to <code className="text-zinc-400">{p.rename}</code> · add to PATH</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 pt-1 border-t border-white/5">
|
||||
<span className="text-[11px] text-zinc-600">winget install starkSV.msdl</span>
|
||||
<span className="text-[10px] font-mono px-1.5 py-0.5 rounded bg-amber-500/10 border border-amber-500/20 text-amber-500">coming soon</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Usage */}
|
||||
<div className="space-y-4">
|
||||
<h2 className="text-base font-semibold text-white">Usage</h2>
|
||||
|
||||
<div className="space-y-3">
|
||||
<p className="text-[12px] text-zinc-500 font-mono uppercase tracking-widest">Interactive — pick product and language</p>
|
||||
<CodeBlock code="msdl" />
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<p className="text-[12px] text-zinc-500 font-mono uppercase tracking-widest">Skip the picker with flags</p>
|
||||
<CodeBlock code='msdl --id 3262 --lang "English"' label="Windows 11 25H2 · English" />
|
||||
<CodeBlock code='msdl --id 3262' label="Windows 11 25H2 · prompts for language" />
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<p className="text-[12px] text-zinc-500 font-mono uppercase tracking-widest">Evaluation / Server ISOs</p>
|
||||
<CodeBlock code="msdl --eval server-2025" label="Windows Server 2025" />
|
||||
<CodeBlock code="msdl --eval win11-ent" label="Windows 11 Enterprise" />
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<p className="text-[12px] text-zinc-500 font-mono uppercase tracking-widest">Pipe directly into a download tool</p>
|
||||
<CodeBlock code='msdl --id 3262 --lang "English" | wget -i -' label="wget" />
|
||||
<CodeBlock code='msdl --id 3262 --lang "English" | xargs curl -L -O' label="curl" />
|
||||
<CodeBlock code='msdl --id 3262 --lang "English" | xargs aria2c -x 16 -s 16' label="aria2c" />
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<p className="text-[12px] text-zinc-500 font-mono uppercase tracking-widest">List all available products</p>
|
||||
<CodeBlock code="msdl --list" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Contribute */}
|
||||
<div className="space-y-3">
|
||||
<h2 className="text-base font-semibold text-white">Sharing links with the web app</h2>
|
||||
<div className="rounded-xl border border-white/7 bg-[#111113] p-4 space-y-2">
|
||||
<p className="text-[12px] text-zinc-400 leading-relaxed">
|
||||
By default, each successful fetch is shared with the <a href={SITE_URL} className="text-blue-400 hover:text-blue-300 underline underline-offset-2">msdl web app</a> to warm its cache — so the next person who visits gets a cached link instead of hitting Microsoft's API cold.
|
||||
</p>
|
||||
<p className="text-[12px] text-zinc-500 leading-relaxed">
|
||||
This is a background POST to our backend. No personal data is sent — only the product ID, SKU ID, and the raw Microsoft JSON response.
|
||||
</p>
|
||||
<div className="pt-2 border-t border-white/5">
|
||||
<p className="text-[12px] text-zinc-600 mb-1.5">To opt out:</p>
|
||||
<CodeBlock code="msdl --id 3262 --lang English --no-contribute" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Open source */}
|
||||
<div className="rounded-xl border border-white/7 bg-white/2 p-4 flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-white mb-0.5">Open source</p>
|
||||
<p className="text-[12px] text-zinc-500">Built in Go. MIT licensed. Contributions welcome.</p>
|
||||
</div>
|
||||
<a
|
||||
href="https://github.com/starkSV/windows-iso-downloader"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-1.5 px-3 py-2 rounded-lg bg-white/5 border border-white/8 text-[12px] text-zinc-400 hover:text-white hover:border-white/15 transition-all flex-shrink-0"
|
||||
>
|
||||
GitHub
|
||||
<ExternalLink size={11} />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</motion.div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { useState, useEffect } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { motion } from 'motion/react'
|
||||
import { ArrowRight, Server } from 'lucide-react'
|
||||
import { ArrowRight, Server, Terminal } from 'lucide-react'
|
||||
import ProductCard from '../components/ProductCard'
|
||||
import StatsBar from '../components/StatsBar'
|
||||
import HowItWorks from '../components/HowItWorks'
|
||||
|
|
@ -209,6 +209,33 @@ export default function HomePage() {
|
|||
</Link>
|
||||
</motion.div>
|
||||
|
||||
{/* CLI promo */}
|
||||
<motion.div
|
||||
className="mt-6"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 0.55 }}
|
||||
>
|
||||
<Link
|
||||
to="/cli"
|
||||
className="group block px-4 py-3 rounded-xl border border-white/6 bg-[#0d0d0f] hover:border-emerald-500/20 hover:bg-[#0a0f0b] transition-all duration-200"
|
||||
>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Terminal size={11} className="text-emerald-500/50" />
|
||||
<span className="text-[10px] font-mono text-zinc-600 uppercase tracking-widest">CLI tool</span>
|
||||
</div>
|
||||
<span className="flex items-center gap-1 text-[11px] text-zinc-600 group-hover:text-emerald-400 transition-colors">
|
||||
Learn more
|
||||
<ArrowRight size={10} className="group-hover:translate-x-0.5 transition-transform" />
|
||||
</span>
|
||||
</div>
|
||||
<code className="text-[13px] font-mono text-zinc-300">
|
||||
<span className="text-zinc-600">$ </span>msdl --id 3262 --lang "English"
|
||||
</code>
|
||||
</Link>
|
||||
</motion.div>
|
||||
|
||||
{/* How it works */}
|
||||
<HowItWorks />
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import SystemRequirements from '../components/SystemRequirements'
|
|||
import CliCommand from '../components/CliCommand'
|
||||
import RelatedReleases from '../components/RelatedReleases'
|
||||
import OfficialFallback from '../components/OfficialFallback'
|
||||
import CliHandoff from '../components/CliHandoff'
|
||||
import { addRecentEntry, updateRecentExpiry } from '../components/RecentlyViewed'
|
||||
|
||||
const API_BASE = import.meta.env.VITE_API_URL ?? 'http://localhost:3002'
|
||||
|
|
@ -526,10 +527,21 @@ export default function ProductDetailPage() {
|
|||
initial={{ opacity: 0, height: 0 }}
|
||||
animate={{ opacity: 1, height: 'auto' }}
|
||||
exit={{ opacity: 0, height: 0 }}
|
||||
className="flex items-start gap-3 p-4 rounded-xl bg-red-500/8 border border-red-500/15 text-red-400 text-sm"
|
||||
className="space-y-3"
|
||||
>
|
||||
<AlertTriangle size={15} className="flex-shrink-0 mt-0.5" />
|
||||
<span>{error}</span>
|
||||
<div className="flex items-start gap-3 p-4 rounded-xl bg-red-500/8 border border-red-500/15 text-red-400 text-sm">
|
||||
<AlertTriangle size={15} className="flex-shrink-0 mt-0.5" />
|
||||
<span>{error}</span>
|
||||
</div>
|
||||
{selectedSku && (
|
||||
<CliHandoff
|
||||
productId={productId!}
|
||||
langName={selectedSku.Language}
|
||||
langDisplay={selectedSku.LocalizedLanguage}
|
||||
highlight
|
||||
defaultOpen
|
||||
/>
|
||||
)}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
|
@ -637,12 +649,26 @@ export default function ProductDetailPage() {
|
|||
|
||||
{(!isNotFound && !hasCatalogError) && (
|
||||
<>
|
||||
{/* Official fallback — shown when backend is WAF-blocked (stale) */}
|
||||
{meta.active && linkStatus === 'stale' && selectedSku && (
|
||||
<OfficialFallback
|
||||
productId={productId!}
|
||||
languageName={selectedSku.LocalizedLanguage}
|
||||
/>
|
||||
{/* Fallbacks — only on stale or error */}
|
||||
{meta.active && (linkStatus === 'stale' || error !== null) && (
|
||||
<>
|
||||
<OfficialFallback
|
||||
productId={productId!}
|
||||
languageName={selectedSku?.LocalizedLanguage ?? ''}
|
||||
/>
|
||||
{/* Collapsed CLI only when highlighted card isn't already shown inside the card */}
|
||||
{!(error !== null && languages.length > 0) && (
|
||||
selectedSku ? (
|
||||
<CliHandoff
|
||||
productId={productId!}
|
||||
langName={selectedSku.Language}
|
||||
langDisplay={selectedSku.LocalizedLanguage}
|
||||
/>
|
||||
) : (
|
||||
<CliHandoff productId={productId!} />
|
||||
)
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* System requirements */}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue