Modernize UI, unify product catalog, and backend optimization
- Migrated products.json to structured metadata format. - Refactored ProductDetailPage to eagerly reset state and validate product IDs against local catalog. - Dynamic data fetching in HomePage and StatsBar for total release counts. - Adjusted UI layout in App.tsx and UX states (404/Loading) for missing dependencies. - Shortened backend tracking HTTP timeouts to prevent local Pi-Hole hangs.
This commit is contained in:
parent
085f6b5a25
commit
aac8f5d990
8 changed files with 257 additions and 107 deletions
|
|
@ -53,7 +53,7 @@ func getReferer(productId string) string {
|
|||
// Replicate Fido session tracking
|
||||
func setupSession() (string, error) {
|
||||
sessionID := uuid.New().String()
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
client := &http.Client{Timeout: 3 * time.Second}
|
||||
|
||||
// Step 1: Register session
|
||||
req1, _ := http.NewRequest("GET", fmt.Sprintf("https://vlscppe.microsoft.com/tags?org_id=%s&session_id=%s", ORG_ID, sessionID), nil)
|
||||
|
|
|
|||
|
|
@ -1,18 +1,110 @@
|
|||
{
|
||||
"48": "Windows 8.1 Single Language (9600.17415)",
|
||||
"52": "Windows 8.1 (9600.17415)",
|
||||
"2378": "Windows 10 22H2 Home China (19045.2006)",
|
||||
"2618": "Windows 10 22H2 v1 (19045.2965)",
|
||||
"3113": "Windows 11 24H2 (26100.1742)",
|
||||
"3114": "Windows 11 24H2 Home China (26100.1742)",
|
||||
"3115": "Windows 11 24H2 Pro China (26100.1742)",
|
||||
"3131": "Windows 11 Arm64 24H2 (26100.1742)",
|
||||
"3132": "Windows 11 Arm64 24H2 Home China (26100.1742)",
|
||||
"3133": "Windows 11 Arm64 24H2 Pro China (26100.1742)",
|
||||
"3262": "Windows 11 25H2 (26200.6584)",
|
||||
"3263": "Windows 11 25H2 Home China (26200.6584)",
|
||||
"3264": "Windows 11 25H2 Pro China (26200.6584)",
|
||||
"3265": "Windows 11 Arm64 25H2 (26200.6584)",
|
||||
"3266": "Windows 11 Arm64 25H2 Home China (26200.6584)",
|
||||
"3267": "Windows 11 Arm64 25H2 Pro China (26200.6584)"
|
||||
"48": {
|
||||
"name": "Windows 8.1 Single Language (9600.17415)",
|
||||
"badge": "LEGACY",
|
||||
"archs": ["x64", "x86"],
|
||||
"related": ["52"]
|
||||
},
|
||||
"52": {
|
||||
"name": "Windows 8.1 (9600.17415)",
|
||||
"badge": "LEGACY",
|
||||
"archs": ["x64", "x86"],
|
||||
"related": ["48", "2618"]
|
||||
},
|
||||
"2378": {
|
||||
"name": "Windows 10 22H2 Home China (19045.2006)",
|
||||
"badge": "EOL SOON",
|
||||
"archs": ["x64"],
|
||||
"related": ["2618"]
|
||||
},
|
||||
"2618": {
|
||||
"name": "Windows 10 22H2 v1 (19045.2965)",
|
||||
"badge": "EOL SOON",
|
||||
"archs": ["x64", "x86"],
|
||||
"related": ["3113", "3262", "52"]
|
||||
},
|
||||
"3113": {
|
||||
"name": "Windows 11 24H2 (26100.1742)",
|
||||
"badge": "STABLE",
|
||||
"archs": ["x64"],
|
||||
"related": ["3262", "3131", "2618"]
|
||||
},
|
||||
"3114": {
|
||||
"name": "Windows 11 24H2 Home China (26100.1742)",
|
||||
"badge": "STABLE",
|
||||
"archs": ["x64"],
|
||||
"related": ["3113", "3115"]
|
||||
},
|
||||
"3115": {
|
||||
"name": "Windows 11 24H2 Pro China (26100.1742)",
|
||||
"badge": "STABLE",
|
||||
"archs": ["x64"],
|
||||
"related": ["3113", "3114"]
|
||||
},
|
||||
"3131": {
|
||||
"name": "Windows 11 Arm64 24H2 (26100.1742)",
|
||||
"badge": "STABLE",
|
||||
"archs": ["ARM64"],
|
||||
"related": ["3265", "3113"]
|
||||
},
|
||||
"3132": {
|
||||
"name": "Windows 11 Arm64 24H2 Home China (26100.1742)",
|
||||
"badge": "STABLE",
|
||||
"archs": ["ARM64"],
|
||||
"related": ["3131", "3133"]
|
||||
},
|
||||
"3133": {
|
||||
"name": "Windows 11 Arm64 24H2 Pro China (26100.1742)",
|
||||
"badge": "STABLE",
|
||||
"archs": ["ARM64"],
|
||||
"related": ["3131", "3132"]
|
||||
},
|
||||
"3262": {
|
||||
"name": "Windows 11 25H2 (26200.6584)",
|
||||
"badge": "LATEST",
|
||||
"archs": ["x64"],
|
||||
"related": ["3265", "3321", "3113"]
|
||||
},
|
||||
"3263": {
|
||||
"name": "Windows 11 25H2 Home China (26200.6584)",
|
||||
"badge": "LATEST",
|
||||
"archs": ["x64"],
|
||||
"related": ["3262", "3264"]
|
||||
},
|
||||
"3264": {
|
||||
"name": "Windows 11 25H2 Pro China (26200.6584)",
|
||||
"badge": "LATEST",
|
||||
"archs": ["x64"],
|
||||
"related": ["3262", "3263"]
|
||||
},
|
||||
"3265": {
|
||||
"name": "Windows 11 Arm64 25H2 (26200.6584)",
|
||||
"badge": "LATEST",
|
||||
"archs": ["ARM64"],
|
||||
"related": ["3262", "3321", "3131"]
|
||||
},
|
||||
"3266": {
|
||||
"name": "Windows 11 Arm64 25H2 Home China (26200.6584)",
|
||||
"badge": "LATEST",
|
||||
"archs": ["ARM64"],
|
||||
"related": ["3265", "3267"]
|
||||
},
|
||||
"3267": {
|
||||
"name": "Windows 11 Arm64 25H2 Pro China (26200.6584)",
|
||||
"badge": "LATEST",
|
||||
"archs": ["ARM64"],
|
||||
"related": ["3265", "3266"]
|
||||
},
|
||||
"3321": {
|
||||
"name": "Windows 11 25H2 (Updated Oct)",
|
||||
"badge": "LATEST",
|
||||
"archs": ["x64"],
|
||||
"related": ["3262", "3324", "3113"]
|
||||
},
|
||||
"3324": {
|
||||
"name": "Windows 11 Arm64 25H2 (Updated Oct)",
|
||||
"badge": "LATEST",
|
||||
"archs": ["ARM64"],
|
||||
"related": ["3321", "3262", "3265"]
|
||||
}
|
||||
}
|
||||
|
|
@ -12,9 +12,9 @@ import ScrollToTop from './components/ScrollToTop'
|
|||
|
||||
export default function App() {
|
||||
return (
|
||||
<div className="min-h-screen pb-20 sm:pb-24" style={{ overflowX: 'hidden', maxWidth: '100vw' }}>
|
||||
<div className="min-h-screen pb-20 sm:pb-24 flex flex-col" style={{ overflowX: 'hidden', maxWidth: '100vw' }}>
|
||||
<ScrollToTop />
|
||||
<main>
|
||||
<main className="flex-1">
|
||||
<Routes>
|
||||
<Route path="/" element={<HomePage />} />
|
||||
<Route path="/products" element={<ProductsPage />} />
|
||||
|
|
|
|||
|
|
@ -1,17 +1,27 @@
|
|||
import { motion } from 'motion/react'
|
||||
import { useState, useEffect } from 'react'
|
||||
|
||||
interface Stat {
|
||||
value: string
|
||||
label: string
|
||||
}
|
||||
|
||||
const stats: Stat[] = [
|
||||
{ value: '17', label: 'releases available' },
|
||||
{ value: '38', label: 'languages' },
|
||||
{ value: '0', label: 'intermediaries' },
|
||||
]
|
||||
|
||||
export default function StatsBar() {
|
||||
const [totalReleases, setTotalReleases] = useState('17')
|
||||
|
||||
useEffect(() => {
|
||||
fetch('/data/products.json')
|
||||
.then(r => r.json())
|
||||
.then(data => setTotalReleases(Object.keys(data).length.toString()))
|
||||
.catch(() => {})
|
||||
}, [])
|
||||
|
||||
const stats: Stat[] = [
|
||||
{ value: totalReleases, label: 'releases available' },
|
||||
{ value: '38', label: 'languages' },
|
||||
{ value: '0', label: 'intermediaries' },
|
||||
]
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
className="flex flex-wrap items-center justify-center gap-x-4 gap-y-1.5 text-[11px] font-mono text-zinc-600"
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useEffect } from 'react'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { motion } from 'motion/react'
|
||||
import { ArrowRight } from 'lucide-react'
|
||||
|
|
@ -58,6 +58,7 @@ const cardVariant = {
|
|||
}
|
||||
|
||||
export default function HomePage() {
|
||||
const [totalReleases, setTotalReleases] = useState(16)
|
||||
|
||||
useEffect(() => {
|
||||
document.title = "Windows ISO Downloader | Official Microsoft Images"
|
||||
|
|
@ -65,6 +66,11 @@ export default function HomePage() {
|
|||
if (metaDesc) {
|
||||
metaDesc.setAttribute('content', "Download official Microsoft Windows ISO files. Fast, free, no registration required.")
|
||||
}
|
||||
|
||||
fetch('/data/products.json')
|
||||
.then(r => r.json())
|
||||
.then(data => setTotalReleases(Object.keys(data).length))
|
||||
.catch(() => {})
|
||||
}, [])
|
||||
|
||||
return (
|
||||
|
|
@ -147,7 +153,7 @@ export default function HomePage() {
|
|||
to="/products"
|
||||
className="inline-flex items-center gap-2 px-5 py-2.5 rounded-xl border border-white/8 bg-white/4 text-sm text-zinc-400 hover:text-white hover:border-white/15 hover:bg-white/7 transition-all duration-200"
|
||||
>
|
||||
Browse all {featured.length > 0 ? '16' : ''} releases
|
||||
Browse all {totalReleases} releases
|
||||
<ArrowRight size={14} />
|
||||
</Link>
|
||||
</motion.div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useState, useEffect } from 'react'
|
||||
import { useParams, useNavigate } from 'react-router-dom'
|
||||
import { useParams, useNavigate, Link } from 'react-router-dom'
|
||||
import { motion, AnimatePresence } from 'motion/react'
|
||||
import * as Select from '@radix-ui/react-select'
|
||||
import { ArrowLeft, ChevronDown, Download, AlertTriangle, Check, ExternalLink } from 'lucide-react'
|
||||
|
|
@ -11,61 +11,11 @@ import RelatedReleases from '../components/RelatedReleases'
|
|||
|
||||
const API_BASE = import.meta.env.VITE_API_URL ?? 'http://localhost:3002'
|
||||
|
||||
// Map product ID → metadata
|
||||
const PRODUCT_META: Record<string, { badge: string; archs: string[] }> = {
|
||||
'3262': { badge: 'LATEST', archs: ['x64'] },
|
||||
'3265': { badge: 'LATEST', archs: ['ARM64'] },
|
||||
'3321': { badge: 'LATEST', archs: ['x64'] },
|
||||
'3324': { badge: 'LATEST', archs: ['ARM64'] },
|
||||
'3113': { badge: 'STABLE', archs: ['x64'] },
|
||||
'3131': { badge: 'STABLE', archs: ['ARM64'] },
|
||||
'2618': { badge: 'EOL SOON', archs: ['x64', 'x86'] },
|
||||
'2378': { badge: 'EOL SOON', archs: ['x64'] },
|
||||
'52': { badge: 'LEGACY', archs: ['x64', 'x86'] },
|
||||
'48': { badge: 'LEGACY', archs: ['x64', 'x86'] },
|
||||
}
|
||||
|
||||
// Sibling release groups for "Also available"
|
||||
const RELATED_GROUPS: Record<string, { id: string; label: string }[]> = {
|
||||
'3262': [
|
||||
{ id: '3265', label: 'Win 11 25H2 ARM64' },
|
||||
{ id: '3321', label: 'Win 11 25H2 (updated)' },
|
||||
{ id: '3113', label: 'Win 11 24H2' },
|
||||
],
|
||||
'3265': [
|
||||
{ id: '3262', label: 'Win 11 25H2 x64' },
|
||||
{ id: '3321', label: 'Win 11 25H2 (updated)' },
|
||||
{ id: '3131', label: 'Win 11 24H2 ARM64' },
|
||||
],
|
||||
'3321': [
|
||||
{ id: '3262', label: 'Win 11 25H2' },
|
||||
{ id: '3324', label: 'Win 11 25H2 ARM64' },
|
||||
{ id: '3113', label: 'Win 11 24H2' },
|
||||
],
|
||||
'3324': [
|
||||
{ id: '3321', label: 'Win 11 25H2 x64' },
|
||||
{ id: '3262', label: 'Win 11 25H2' },
|
||||
{ id: '3265', label: 'Win 11 25H2 ARM64 (alt)' },
|
||||
],
|
||||
'3113': [
|
||||
{ id: '3262', label: 'Win 11 25H2' },
|
||||
{ id: '3131', label: 'Win 11 24H2 ARM64' },
|
||||
{ id: '2618', label: 'Win 10 22H2' },
|
||||
],
|
||||
'3131': [
|
||||
{ id: '3265', label: 'Win 11 25H2 ARM64' },
|
||||
{ id: '3113', label: 'Win 11 24H2 x64' },
|
||||
],
|
||||
'2618': [
|
||||
{ id: '3113', label: 'Win 11 24H2' },
|
||||
{ id: '3262', label: 'Win 11 25H2' },
|
||||
{ id: '52', label: 'Win 8.1' },
|
||||
],
|
||||
|
||||
'52': [
|
||||
{ id: '48', label: 'Win 8.1 Single Language' },
|
||||
{ id: '2618', label: 'Win 10 22H2' },
|
||||
],
|
||||
interface ProductCatalogEntry {
|
||||
name: string
|
||||
badge: string
|
||||
archs: string[]
|
||||
related: string[]
|
||||
}
|
||||
|
||||
function WinLogo({ className }: { className?: string }) {
|
||||
|
|
@ -102,17 +52,45 @@ export default function ProductDetailPage() {
|
|||
const [isFetching, setIsFetching] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [copiedUri, setCopiedUri] = useState<string | null>(null)
|
||||
|
||||
const meta = PRODUCT_META[productId!] ?? { badge: '', archs: ['x64'] }
|
||||
const related = RELATED_GROUPS[productId!] ?? []
|
||||
const [isNotFound, setIsNotFound] = useState(false)
|
||||
const [isValidated, setIsValidated] = useState(false)
|
||||
|
||||
const [meta, setMeta] = useState<{ badge: string; archs: string[] }>({ badge: '', archs: [] })
|
||||
const [related, setRelated] = useState<{ id: string; label: string }[]>([])
|
||||
|
||||
// Load product name + build string
|
||||
useEffect(() => {
|
||||
setBuildStr('')
|
||||
setIsNotFound(false)
|
||||
setIsValidated(false)
|
||||
setProductName('')
|
||||
setMeta({ badge: '', archs: [] })
|
||||
setRelated([])
|
||||
|
||||
fetch('/data/products.json')
|
||||
.then(r => r.json())
|
||||
.then((data: Record<string, string>) => {
|
||||
const name = data[productId!] ?? `Product ${productId}`
|
||||
.then((data: Record<string, ProductCatalogEntry>) => {
|
||||
const product = data[productId!]
|
||||
|
||||
if (!product || !product.name) {
|
||||
setIsNotFound(true)
|
||||
setProductName('Product Not Found')
|
||||
document.title = 'Product Not Found | Windows ISO Downloader'
|
||||
const descMeta = document.querySelector('meta[name="description"]')
|
||||
if (descMeta) descMeta.setAttribute('content', 'The requested product could not be found.')
|
||||
return
|
||||
}
|
||||
|
||||
const name = product.name
|
||||
setProductName(name)
|
||||
setMeta({ badge: product.badge || '', archs: product.archs || [] })
|
||||
|
||||
setRelated((product.related || []).map(rId => ({
|
||||
id: rId,
|
||||
label: data[rId]?.name || `Product ${rId}`
|
||||
})))
|
||||
|
||||
setIsValidated(true)
|
||||
|
||||
// Dynamic SEO injection
|
||||
document.title = `${name} ISO Download | Windows ISO Downloader`
|
||||
|
|
@ -125,12 +103,15 @@ export default function ProductDetailPage() {
|
|||
const match = name.match(/\(([^)]+)\)/)
|
||||
if (match) setBuildStr(match[1])
|
||||
})
|
||||
.catch(() => setProductName(`Product ${productId}`))
|
||||
.catch(() => {
|
||||
setIsNotFound(true)
|
||||
setProductName('Error Loading Product')
|
||||
})
|
||||
}, [productId])
|
||||
|
||||
// Fetch languages on mount
|
||||
useEffect(() => {
|
||||
if (!productId) return
|
||||
if (!productId || !isValidated) return
|
||||
setIsLoadingLangs(true)
|
||||
setError(null)
|
||||
setDownloadLinks([])
|
||||
|
|
@ -151,7 +132,7 @@ export default function ProductDetailPage() {
|
|||
})
|
||||
.catch(e => setError(e.message))
|
||||
.finally(() => setIsLoadingLangs(false))
|
||||
}, [productId])
|
||||
}, [productId, isValidated])
|
||||
|
||||
async function handleGetLinks() {
|
||||
if (!selectedSku || !productId) return
|
||||
|
|
@ -230,11 +211,37 @@ export default function ProductDetailPage() {
|
|||
|
||||
{/* Main card */}
|
||||
<div className="rounded-2xl border border-white/7 bg-[#111113] p-6 space-y-5">
|
||||
{isLoadingLangs ? (
|
||||
<div className="space-y-3">
|
||||
<div className="h-3 w-20 bg-white/6 rounded animate-pulse" />
|
||||
<div className="h-11 bg-white/6 rounded-xl animate-pulse" />
|
||||
<div className="h-11 bg-white/5 rounded-xl animate-pulse" />
|
||||
{isNotFound ? (
|
||||
<div className="flex flex-col items-center justify-center py-6 text-center">
|
||||
<AlertTriangle size={32} className="text-zinc-600 mb-4" />
|
||||
<h2 className="text-lg font-semibold text-white mb-2">Product Not Found</h2>
|
||||
<p className="text-zinc-500 text-sm max-w-xs px-4 leading-relaxed mb-6">
|
||||
The product ID you requested ({productId}) does not exist or has been removed from the catalog.
|
||||
</p>
|
||||
<div className="flex items-center justify-center gap-3 w-full">
|
||||
<Link to="/" className="flex-1 max-w-[140px] px-4 py-2.5 rounded-xl bg-white/5 border border-white/8 text-sm font-medium text-white hover:bg-white/10 transition-colors">
|
||||
Home
|
||||
</Link>
|
||||
<Link to="/products" className="flex-1 max-w-[140px] px-4 py-2.5 rounded-xl bg-blue-500 text-sm font-medium text-white hover:bg-blue-600 transition-colors shadow-lg">
|
||||
Browse all
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
) : isLoadingLangs ? (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-2.5 text-xs text-zinc-400 font-medium tracking-wide">
|
||||
<motion.span
|
||||
animate={{ rotate: 360 }}
|
||||
transition={{ duration: 1, repeat: Infinity, ease: 'linear' }}
|
||||
className="inline-block w-4 h-4 border-2 border-zinc-500/30 border-t-zinc-500 rounded-full"
|
||||
/>
|
||||
Connecting to Microsoft CDN...
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<div className="h-3 w-20 bg-white/6 rounded animate-pulse" />
|
||||
<div className="h-11 bg-white/6 rounded-xl animate-pulse" />
|
||||
<div className="h-11 bg-white/5 rounded-xl animate-pulse" />
|
||||
</div>
|
||||
</div>
|
||||
) : error && !languages.length ? (
|
||||
<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">
|
||||
|
|
@ -396,15 +403,19 @@ export default function ProductDetailPage() {
|
|||
</AnimatePresence>
|
||||
</div>
|
||||
|
||||
{/* System requirements */}
|
||||
<SystemRequirements isWin11={isWin11(productId!)} />
|
||||
{!isNotFound && (
|
||||
<>
|
||||
{/* System requirements */}
|
||||
<SystemRequirements isWin11={isWin11(productId!)} />
|
||||
|
||||
{/* aria2 tip */}
|
||||
<Aria2Tip downloadUrl={firstUri} />
|
||||
{/* aria2 tip */}
|
||||
<Aria2Tip downloadUrl={firstUri} />
|
||||
|
||||
{/* Related releases */}
|
||||
{related.length > 0 && (
|
||||
<RelatedReleases current={productId!} items={related} />
|
||||
{/* Related releases */}
|
||||
{related.length > 0 && (
|
||||
<RelatedReleases current={productId!} items={related} />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</motion.div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -22,8 +22,8 @@ export default function ProductsPage() {
|
|||
|
||||
fetch('/data/products.json')
|
||||
.then(r => r.json())
|
||||
.then((data: Record<string, string>) => {
|
||||
setProducts(Object.entries(data).map(([id, name]) => ({ id, name })))
|
||||
.then((data: Record<string, { name: string }>) => {
|
||||
setProducts(Object.entries(data).map(([id, product]) => ({ id, name: product.name })))
|
||||
})
|
||||
.finally(() => setIsLoading(false))
|
||||
}, [])
|
||||
|
|
|
|||
35
msdls_v3.py
35
msdls_v3.py
|
|
@ -101,8 +101,39 @@ if __name__ == "__main__":
|
|||
time.sleep(0.5)
|
||||
|
||||
if args.write:
|
||||
import os
|
||||
catalog = {}
|
||||
if os.path.exists(args.write):
|
||||
try:
|
||||
with open(args.write, 'r', encoding='utf-8') as f:
|
||||
catalog = json.load(f)
|
||||
except Exception as e:
|
||||
logging.error(f"Failed to read existing catalog: {e}")
|
||||
|
||||
# Merge new products, preserving existing extra metadata if present
|
||||
for pid, name in products.items():
|
||||
if pid in catalog:
|
||||
# Update name but keep other fields like badge, archs, etc.
|
||||
if isinstance(catalog[pid], dict):
|
||||
catalog[pid]["name"] = name
|
||||
else:
|
||||
# Upgrade from old flat format
|
||||
catalog[pid] = {
|
||||
"name": name,
|
||||
"badge": "",
|
||||
"archs": [],
|
||||
"related": []
|
||||
}
|
||||
else:
|
||||
catalog[pid] = {
|
||||
"name": name,
|
||||
"badge": "",
|
||||
"archs": [],
|
||||
"related": []
|
||||
}
|
||||
|
||||
with open(args.write, 'w', encoding='utf-8') as f:
|
||||
json.dump(products, f, indent=4)
|
||||
logging.info(f"\nSaved {len(products)} products to {args.write}")
|
||||
json.dump(catalog, f, indent=4)
|
||||
logging.info(f"\nSaved {len(catalog)} products to {args.write} (merged with existing)")
|
||||
|
||||
logging.info("Done.")
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue