mirror of
https://github.com/ChiChou/entdb.git
synced 2026-06-10 23:07:47 +02:00
UI improvements: theme support, version history, layout fixes
- Add dark/light mode toggle with next-themes - Redesign binary detail page with version history sidebar and diff support - Improve keys page with multi-column grid layout - Update navtop styling to match content padding - Replace checkbox with segmented button for "Latest Only" toggle - Upgrade to Next.js 16, React 19, TypeScript 6
This commit is contained in:
Generated
+1923
-785
File diff suppressed because it is too large
Load Diff
+12
-11
@@ -9,6 +9,7 @@
|
||||
"lint": "eslint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@base-ui/react": "^1.4.0",
|
||||
"@radix-ui/react-accordion": "^1.2.12",
|
||||
"@radix-ui/react-checkbox": "^1.3.3",
|
||||
"@radix-ui/react-collapsible": "^1.1.12",
|
||||
@@ -21,12 +22,12 @@
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
"lucide-react": "^0.541.0",
|
||||
"next": "15.5.0",
|
||||
"lucide-react": "^1.8.0",
|
||||
"next": "^16.2.3",
|
||||
"next-themes": "^0.4.6",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0",
|
||||
"react-syntax-highlighter": "^15.6.3",
|
||||
"react": "^19.2.5",
|
||||
"react-dom": "^19.2.5",
|
||||
"react-syntax-highlighter": "^16.1.1",
|
||||
"sonner": "^2.0.7",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"use-debounce": "^10.0.5"
|
||||
@@ -34,14 +35,14 @@
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3",
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"@types/node": "^25.6.0",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@types/react-syntax-highlighter": "^15.5.13",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "15.5.0",
|
||||
"eslint": "^10.2.0",
|
||||
"eslint-config-next": "^16.2.3",
|
||||
"tailwindcss": "^4",
|
||||
"tw-animate-css": "^1.3.7",
|
||||
"typescript": "^5"
|
||||
"typescript": "^6.0.2"
|
||||
}
|
||||
}
|
||||
|
||||
+9
-6
@@ -1,5 +1,6 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import { ThemeProvider } from "next-themes";
|
||||
import "./globals.css";
|
||||
|
||||
import { NavTop } from "@/components/navtop";
|
||||
@@ -28,15 +29,17 @@ export default function RootLayout({
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<body
|
||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||
>
|
||||
<NavTop />
|
||||
<Toaster />
|
||||
<Suspense>
|
||||
<main className="flex flex-col">{children}</main>
|
||||
</Suspense>
|
||||
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
|
||||
<NavTop />
|
||||
<Toaster />
|
||||
<Suspense>
|
||||
<main className="flex flex-col">{children}</main>
|
||||
</Suspense>
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
+120
-107
@@ -1,13 +1,15 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState, useMemo } from "react";
|
||||
import { redirect, useSearchParams } from "next/navigation";
|
||||
import { redirect, useSearchParams, useRouter } from "next/navigation";
|
||||
import {
|
||||
createElement,
|
||||
Prism as SyntaxHighlighter,
|
||||
} from "react-syntax-highlighter";
|
||||
import { tomorrow } from "react-syntax-highlighter/dist/esm/styles/prism";
|
||||
|
||||
import Link from "next/link";
|
||||
import { GitCompare } from "lucide-react";
|
||||
import { CopyButton } from "@/components/copy-button";
|
||||
import { DiffViewer } from "@/components/diff-viewer";
|
||||
|
||||
@@ -18,9 +20,9 @@ import { normalizePlist } from "@/lib/plist";
|
||||
|
||||
export default function BinaryDetail() {
|
||||
const params = useSearchParams();
|
||||
const router = useRouter();
|
||||
const os = params.get("os");
|
||||
const path = params.get("path");
|
||||
const compareWith = params.get("compare");
|
||||
|
||||
const [group, build] = os ? os.split("/") : ["", ""];
|
||||
|
||||
@@ -38,9 +40,9 @@ export default function BinaryDetail() {
|
||||
const [xml, setXML] = useState<string>("");
|
||||
const [xmlKeys, setXMLKeys] = useState<Set<string>>(new Set());
|
||||
const [history, setHistory] = useState<PathHistory[]>([]);
|
||||
const [compareWith, setCompareWith] = useState<string | null>(null);
|
||||
const [compareXml, setCompareXml] = useState<string>("");
|
||||
const [compareLoading, setCompareLoading] = useState(false);
|
||||
const [compareError, setCompareError] = useState<string>("");
|
||||
|
||||
useEffect(() => {
|
||||
async function load() {
|
||||
@@ -70,10 +72,10 @@ export default function BinaryDetail() {
|
||||
}, [group, build, path]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!compareWith || !group) return;
|
||||
|
||||
setCompareError("");
|
||||
setCompareXml("");
|
||||
if (!compareWith || !group) {
|
||||
setCompareXml("");
|
||||
return;
|
||||
}
|
||||
|
||||
async function loadCompare() {
|
||||
const engine = await createEngine(group);
|
||||
@@ -84,27 +86,12 @@ export default function BinaryDetail() {
|
||||
|
||||
setCompareLoading(true);
|
||||
loadCompare()
|
||||
.catch((err) => {
|
||||
setCompareError(err.message || "Failed to load comparison");
|
||||
})
|
||||
.catch(() => setCompareXml(""))
|
||||
.finally(() => setCompareLoading(false));
|
||||
}, [group, compareWith, path]);
|
||||
|
||||
const normalizedXml = useMemo(
|
||||
() => (xml ? normalizePlist(xml) : ""),
|
||||
[xml],
|
||||
);
|
||||
const normalizedCompareXml = useMemo(
|
||||
() => (compareXml ? normalizePlist(compareXml) : ""),
|
||||
[compareXml],
|
||||
);
|
||||
|
||||
const availableHistory = history.filter((h) => h.available);
|
||||
const currentOs = history.find(
|
||||
(h) => h.os.build === build || `${h.os.version}_${h.os.build}` === build,
|
||||
);
|
||||
|
||||
// Group versions by major version
|
||||
const groupedHistory = useMemo(() => {
|
||||
const groups: { [major: string]: typeof availableHistory } = {};
|
||||
for (const h of availableHistory) {
|
||||
@@ -115,40 +102,11 @@ export default function BinaryDetail() {
|
||||
return Object.entries(groups).sort(([a], [b]) => Number(b) - Number(a));
|
||||
}, [availableHistory]);
|
||||
|
||||
const renderVersionLink = (h: typeof availableHistory[0]) => {
|
||||
const isCurrent =
|
||||
h.os.build === build || `${h.os.version}_${h.os.build}` === build;
|
||||
const isComparing = compareWith === `${h.os.version}_${h.os.build}`;
|
||||
const versionTag = `${h.os.version}_${h.os.build}`;
|
||||
|
||||
if (isCurrent) {
|
||||
return (
|
||||
<span
|
||||
key={h.os.build}
|
||||
className="block px-2 py-1 text-xs rounded bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200 font-medium"
|
||||
>
|
||||
{h.os.version} (current)
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
const href = isComparing
|
||||
? addBasePath(`/os/bin?os=${encodeURIComponent(os!)}&path=${encodeURIComponent(path!)}`)
|
||||
: addBasePath(`/os/bin?os=${encodeURIComponent(os!)}&path=${encodeURIComponent(path!)}&compare=${encodeURIComponent(versionTag)}`);
|
||||
|
||||
return (
|
||||
<a
|
||||
key={h.os.build}
|
||||
href={href}
|
||||
className={`block px-2 py-1 text-xs rounded transition-colors ${
|
||||
isComparing
|
||||
? "bg-yellow-100 dark:bg-yellow-900 text-yellow-800 dark:text-yellow-200"
|
||||
: "hover:bg-accent"
|
||||
}`}
|
||||
>
|
||||
{h.os.version}
|
||||
{isComparing && " (comparing)"}
|
||||
</a>
|
||||
const switchVersion = (versionTag: string) => {
|
||||
router.push(
|
||||
addBasePath(
|
||||
`/os/bin?os=${encodeURIComponent(group + "/" + versionTag)}&path=${encodeURIComponent(path!)}`
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
@@ -156,6 +114,100 @@ export default function BinaryDetail() {
|
||||
|
||||
return (
|
||||
<div className="flex flex-col lg:flex-row gap-6">
|
||||
{/* Version history sidebar - now on left and wider */}
|
||||
{hasVersionHistory && (
|
||||
<aside className="lg:w-64 shrink-0 order-2 lg:order-first">
|
||||
<div className="lg:sticky lg:top-4">
|
||||
<h3 className="text-sm font-semibold mb-3 text-muted-foreground">
|
||||
Version History ({availableHistory.length})
|
||||
</h3>
|
||||
<div className="space-y-1 max-h-[70vh] overflow-y-auto pr-2">
|
||||
{groupedHistory.map(([major, versions]) => (
|
||||
<details
|
||||
key={major}
|
||||
open={versions.some(
|
||||
(h) =>
|
||||
h.os.build === build ||
|
||||
`${h.os.version}_${h.os.build}` === build
|
||||
)}
|
||||
className="group"
|
||||
>
|
||||
<summary className="cursor-pointer text-sm font-medium text-muted-foreground hover:text-foreground flex items-center gap-2 py-1.5 px-2 rounded hover:bg-accent">
|
||||
<span className="group-open:rotate-90 transition-transform text-xs">
|
||||
▶
|
||||
</span>
|
||||
<span className="flex-1">{group === "iOS" ? "iOS" : group} {major}.x</span>
|
||||
<span className="text-xs text-muted-foreground/60">
|
||||
{versions.length}
|
||||
</span>
|
||||
</summary>
|
||||
<div className="ml-4 mt-1 space-y-0.5 border-l border-border pl-3">
|
||||
{versions.map((h) => {
|
||||
const isCurrent =
|
||||
h.os.build === build ||
|
||||
`${h.os.version}_${h.os.build}` === build;
|
||||
const versionTag = `${h.os.version}_${h.os.build}`;
|
||||
const isComparing = compareWith === versionTag;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={h.os.build}
|
||||
className={`flex items-center gap-1 px-2 py-1 text-sm rounded transition-colors ${
|
||||
isCurrent
|
||||
? "bg-primary text-primary-foreground font-medium"
|
||||
: isComparing
|
||||
? "bg-yellow-100 dark:bg-yellow-900/50"
|
||||
: "hover:bg-accent"
|
||||
}`}
|
||||
>
|
||||
<button
|
||||
onClick={() => !isCurrent && switchVersion(versionTag)}
|
||||
disabled={isCurrent}
|
||||
className={`flex-1 text-left ${
|
||||
isCurrent
|
||||
? "cursor-default"
|
||||
: "text-muted-foreground hover:text-foreground"
|
||||
}`}
|
||||
>
|
||||
{h.os.version}
|
||||
{isCurrent && (
|
||||
<span className="ml-1 text-xs opacity-70">
|
||||
(current)
|
||||
</span>
|
||||
)}
|
||||
{isComparing && (
|
||||
<span className="ml-1 text-xs text-yellow-700 dark:text-yellow-300">
|
||||
(diff)
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
{!isCurrent && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setCompareWith(isComparing ? null : versionTag);
|
||||
}}
|
||||
className={`p-1 rounded transition-colors ${
|
||||
isComparing
|
||||
? "text-yellow-700 dark:text-yellow-300 hover:bg-yellow-200 dark:hover:bg-yellow-800"
|
||||
: "text-muted-foreground hover:text-foreground hover:bg-accent"
|
||||
}`}
|
||||
title={isComparing ? "Close diff" : `Compare with ${h.os.version}`}
|
||||
>
|
||||
<GitCompare className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</details>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
)}
|
||||
|
||||
{/* Main content */}
|
||||
<main className="flex-1 min-w-0 space-y-4">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
@@ -176,19 +228,16 @@ export default function BinaryDetail() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!loading && compareWith && compareError && (
|
||||
<div className="border border-red-300 bg-red-50 dark:bg-red-900/20 dark:border-red-800 rounded-lg p-4 text-red-700 dark:text-red-300">
|
||||
<p className="font-medium">Comparison failed</p>
|
||||
<p className="text-sm mt-1">{compareError}</p>
|
||||
</div>
|
||||
{!loading && compareWith && compareLoading && (
|
||||
<div className="h-64 bg-muted rounded animate-pulse" />
|
||||
)}
|
||||
|
||||
{!loading && compareWith && !compareLoading && !compareError && normalizedCompareXml && (
|
||||
{!loading && compareWith && !compareLoading && compareXml && (
|
||||
<DiffViewer
|
||||
oldXml={normalizedCompareXml}
|
||||
newXml={normalizedXml}
|
||||
oldLabel={`${compareWith}`}
|
||||
newLabel={currentOs ? `${currentOs.os.version}_${currentOs.os.build}` : build}
|
||||
oldXml={compareXml}
|
||||
newXml={xml}
|
||||
oldLabel={compareWith}
|
||||
newLabel={build}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -258,48 +307,12 @@ export default function BinaryDetail() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{compareLoading && (
|
||||
<div className="h-64 bg-muted rounded animate-pulse" />
|
||||
{!loading && !xml && (
|
||||
<div className="text-center py-12 text-muted-foreground">
|
||||
<p>No entitlement data found for this binary.</p>
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
|
||||
{/* Version history sidebar */}
|
||||
{hasVersionHistory && (
|
||||
<aside className="lg:w-48 shrink-0">
|
||||
<div className="lg:sticky lg:top-4">
|
||||
<h3 className="text-sm font-semibold mb-2 text-muted-foreground">
|
||||
History ({availableHistory.length})
|
||||
</h3>
|
||||
<div className="space-y-2 max-h-[60vh] overflow-y-auto pr-1">
|
||||
{groupedHistory.map(([major, versions]) => (
|
||||
<details
|
||||
key={major}
|
||||
open={versions.some(
|
||||
(h) =>
|
||||
h.os.build === build ||
|
||||
`${h.os.version}_${h.os.build}` === build ||
|
||||
compareWith === `${h.os.version}_${h.os.build}`
|
||||
)}
|
||||
className="group"
|
||||
>
|
||||
<summary className="cursor-pointer text-xs font-medium text-muted-foreground hover:text-foreground flex items-center gap-1 py-1">
|
||||
<span className="group-open:rotate-90 transition-transform">
|
||||
▶
|
||||
</span>
|
||||
iOS {major}.x
|
||||
<span className="ml-auto text-muted-foreground/60">
|
||||
{versions.length}
|
||||
</span>
|
||||
</summary>
|
||||
<div className="ml-3 mt-1 space-y-0.5 border-l pl-2">
|
||||
{versions.map(renderVersionLink)}
|
||||
</div>
|
||||
</details>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+43
-14
@@ -62,18 +62,18 @@ function KeyBadge({
|
||||
return (
|
||||
<Link
|
||||
href={`/os/find?key=${encodeURIComponent(keyName)}&os=${os}`}
|
||||
className="inline-block px-2 py-1 bg-muted hover:bg-accent rounded text-sm font-mono transition-colors group"
|
||||
className="block py-1 font-mono text-muted-foreground hover:text-foreground transition-colors group truncate"
|
||||
title={keyName}
|
||||
>
|
||||
{suffix ? (
|
||||
<>
|
||||
<span className="text-muted-foreground group-hover:text-muted-foreground/70 text-xs">
|
||||
<span className="text-muted-foreground/60 group-hover:text-muted-foreground text-sm">
|
||||
{prefix}
|
||||
</span>
|
||||
<span className="text-foreground font-medium">{suffix}</span>
|
||||
<span className="text-foreground/80 group-hover:text-foreground">{suffix}</span>
|
||||
</>
|
||||
) : (
|
||||
<span className="text-foreground">{keyName}</span>
|
||||
<span>{keyName}</span>
|
||||
)}
|
||||
</Link>
|
||||
);
|
||||
@@ -292,16 +292,45 @@ export default function Keys() {
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-0.5">
|
||||
{sortedPrefixes.map((prefix) => (
|
||||
<KeyGroup
|
||||
key={prefix}
|
||||
prefix={prefix}
|
||||
keys={grouped[prefix]}
|
||||
os={os}
|
||||
forceOpen={forceOpen}
|
||||
/>
|
||||
))}
|
||||
<div>
|
||||
{/* Single keys in multi-column grid */}
|
||||
{(() => {
|
||||
const singleKeys = sortedPrefixes.filter(
|
||||
(p) => grouped[p].length === 1 && grouped[p][0] === p
|
||||
);
|
||||
const groupKeys = sortedPrefixes.filter(
|
||||
(p) => grouped[p].length > 1 || grouped[p][0] !== p
|
||||
);
|
||||
return (
|
||||
<>
|
||||
{singleKeys.length > 0 && (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-1.5 mb-4">
|
||||
{singleKeys.map((prefix) => (
|
||||
<KeyBadge
|
||||
key={prefix}
|
||||
keyName={grouped[prefix][0]}
|
||||
prefix=""
|
||||
os={os}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{groupKeys.length > 0 && (
|
||||
<div className="space-y-0.5">
|
||||
{groupKeys.map((prefix) => (
|
||||
<KeyGroup
|
||||
key={prefix}
|
||||
prefix={prefix}
|
||||
keys={grouped[prefix]}
|
||||
os={os}
|
||||
forceOpen={forceOpen}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -3,9 +3,6 @@ import OSList from "@/components/oslist";
|
||||
export default async function Home() {
|
||||
return (
|
||||
<div className="font-sans">
|
||||
<h1 className="text-2xl md:text-4xl text-center md-mt-16 mt-8">
|
||||
Entitlement Database
|
||||
</h1>
|
||||
<div className="items-center justify-center m-4 md:m-16">
|
||||
<OSList />
|
||||
</div>
|
||||
|
||||
@@ -81,20 +81,15 @@ function Tree({
|
||||
expandAll={expandAll}
|
||||
/>
|
||||
))}
|
||||
{files.length > 0 && (
|
||||
<li>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-x-4">
|
||||
{files.map(([key, value]) => (
|
||||
<FileItem
|
||||
key={value as string}
|
||||
name={key}
|
||||
fullPath={value as string}
|
||||
os={os}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{files.map(([key, value]) => (
|
||||
<li key={value as string}>
|
||||
<FileItem
|
||||
name={key}
|
||||
fullPath={value as string}
|
||||
os={os}
|
||||
/>
|
||||
</li>
|
||||
)}
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
@@ -139,18 +134,16 @@ function TreeFolder({
|
||||
</span>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<div className={isShallow ? "ml-6 mt-1" : "ml-4 pl-2 border-l border-border"}>
|
||||
<div className={isShallow ? "ml-6 mt-1 space-y-0.5" : "ml-4 pl-2 border-l border-border"}>
|
||||
{isShallow ? (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-x-4">
|
||||
{Object.entries(item).map(([key, value]) => (
|
||||
<FileItem
|
||||
key={value as string}
|
||||
name={key}
|
||||
fullPath={value as string}
|
||||
os={os}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
Object.entries(item).map(([key, value]) => (
|
||||
<FileItem
|
||||
key={value as string}
|
||||
name={key}
|
||||
fullPath={value as string}
|
||||
os={os}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<Tree item={item} os={os} depth={depth + 1} expandAll={expandAll} />
|
||||
)}
|
||||
|
||||
@@ -1,19 +1,51 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useTheme } from "next-themes";
|
||||
import { Moon, Sun } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
function ThemeToggle() {
|
||||
const { theme, setTheme } = useTheme();
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
if (!mounted) {
|
||||
return <div className="w-8 h-8" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
|
||||
className="p-2 rounded-md hover:bg-accent text-muted-foreground hover:text-foreground transition-colors"
|
||||
title={theme === "dark" ? "Switch to light mode" : "Switch to dark mode"}
|
||||
>
|
||||
{theme === "dark" ? (
|
||||
<Sun className="h-4 w-4" />
|
||||
) : (
|
||||
<Moon className="h-4 w-4" />
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export function NavTop() {
|
||||
return (
|
||||
<header className="flex flex-row justify-between items-center p-4 w-full bg-gray-900 text-white">
|
||||
<header className="flex flex-row justify-between items-center px-4 md:px-8 py-4 w-full border-b border-border bg-background text-foreground">
|
||||
<h1 className="text-2xl font-bold">
|
||||
<Link href="/" className="hover:text-gray-300">
|
||||
<Link href="/" className="hover:text-muted-foreground">
|
||||
entdb
|
||||
</Link>
|
||||
</h1>
|
||||
<nav className="flex gap-4 text-sm">
|
||||
<nav className="flex items-center gap-4 text-sm">
|
||||
<a
|
||||
href="https://github.com/chichou/entdb-indexer"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="hover:text-gray-300"
|
||||
className="text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
GitHub
|
||||
</a>
|
||||
@@ -21,10 +53,11 @@ export function NavTop() {
|
||||
href="https://infosec.exchange/@codecolorist"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="hover:text-gray-300"
|
||||
className="text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
Mastodon
|
||||
</a>
|
||||
<ThemeToggle />
|
||||
</nav>
|
||||
</header>
|
||||
);
|
||||
|
||||
+27
-12
@@ -123,16 +123,29 @@ export default function OSList() {
|
||||
)}
|
||||
|
||||
{!loading && (
|
||||
<header className="mb-4">
|
||||
<Checkbox
|
||||
id="select-all"
|
||||
className="mr-2"
|
||||
checked={showLess}
|
||||
onCheckedChange={(checked) => setShowLess(Boolean(checked))}
|
||||
/>
|
||||
<label htmlFor="select-all" className="text-lg font-medium">
|
||||
Show Less
|
||||
</label>
|
||||
<header className="mb-6 flex items-center gap-2">
|
||||
<div className="inline-flex rounded-lg border border-border p-1 bg-muted/30">
|
||||
<button
|
||||
onClick={() => setShowLess(true)}
|
||||
className={`px-3 py-1.5 text-sm rounded-md transition-colors ${
|
||||
showLess
|
||||
? "bg-background text-foreground shadow-sm font-medium"
|
||||
: "text-muted-foreground hover:text-foreground"
|
||||
}`}
|
||||
>
|
||||
Latest Only
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowLess(false)}
|
||||
className={`px-3 py-1.5 text-sm rounded-md transition-colors ${
|
||||
!showLess
|
||||
? "bg-background text-foreground shadow-sm font-medium"
|
||||
: "text-muted-foreground hover:text-foreground"
|
||||
}`}
|
||||
>
|
||||
All Versions
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
)}
|
||||
|
||||
@@ -146,11 +159,13 @@ export default function OSList() {
|
||||
<li key={index} className="list-none">
|
||||
<Link
|
||||
href={`/os/keys?os=${group.name}/${os.version}_${os.build}`}
|
||||
className="block p-4 border rounded-lg shadow-sm hover:shadow-md transition-all hover:bg-gray-50"
|
||||
className="block p-4 border border-border rounded-lg hover:border-foreground/20 transition-colors hover:bg-accent/50"
|
||||
>
|
||||
<div className="flex justify-between items-center">
|
||||
<h2 className="text-lg">{os.name}</h2>
|
||||
<div className="text-sm text-gray-500">{os.build}</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{os.build}
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</li>
|
||||
|
||||
+22
-5
@@ -1,7 +1,11 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
@@ -11,7 +15,7 @@
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"jsx": "react-jsx",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
@@ -19,9 +23,22 @@
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
"@/*": [
|
||||
"./src/*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules", "public/data/**", "data/**", "out/**"]
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts",
|
||||
".next/dev/types/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"public/data/**",
|
||||
"data/**",
|
||||
"out/**"
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user