fix: show all tools on All tab, remove Popular and Browse sections

All tab now shows the full tool list grouped by category (same as
modality tabs). Removed Getting Started, Popular, and Browse by
Category sections for a simpler, direct experience.
This commit is contained in:
SnapOtter
2026-06-14 20:40:45 +08:00
parent 287f6f15de
commit b166f47de5
+19 -129
View File
@@ -1,6 +1,6 @@
import type { Tool } from "@snapotter/shared"; import type { Tool } from "@snapotter/shared";
import { CATEGORIES, MODALITIES, TOOLS } from "@snapotter/shared"; import { CATEGORIES, TOOLS } from "@snapotter/shared";
import { FileImage, Search, X } from "lucide-react"; import { Search, X } from "lucide-react";
import { useEffect, useMemo, useRef, useState } from "react"; import { useEffect, useMemo, useRef, useState } from "react";
import { Link, useLocation, useNavigate } from "react-router-dom"; import { Link, useLocation, useNavigate } from "react-router-dom";
import { ToolCard } from "@/components/common/tool-card.js"; import { ToolCard } from "@/components/common/tool-card.js";
@@ -10,26 +10,13 @@ import { useTranslation } from "@/contexts/i18n-context";
import { useFuseSearch } from "@/hooks/use-fuse-search.js"; import { useFuseSearch } from "@/hooks/use-fuse-search.js";
import { usePageTitle } from "@/hooks/use-page-title.js"; import { usePageTitle } from "@/hooks/use-page-title.js";
import { useRecentTools } from "@/hooks/use-recent-tools.js"; import { useRecentTools } from "@/hooks/use-recent-tools.js";
import { apiGet } from "@/lib/api.js";
import { format } from "@/lib/format.js"; import { format } from "@/lib/format.js";
import { ICON_MAP } from "@/lib/icon-map.js";
import { getCategoryName, getToolName } from "@/lib/tool-i18n.js"; import { getCategoryName, getToolName } from "@/lib/tool-i18n.js";
import { cn } from "@/lib/utils.js"; import { cn } from "@/lib/utils.js";
import { useSettingsStore } from "@/stores/settings-store"; import { useSettingsStore } from "@/stores/settings-store";
// ── Constants ──────────────────────────────────────────────────── // ── Constants ────────────────────────────────────────────────────
const FALLBACK_POPULAR_IDS = [
"resize",
"crop",
"compress",
"convert",
"remove-background",
"upscale",
"merge-pdf",
"watermark-text",
];
interface TabDef { interface TabDef {
key: string; key: string;
label: string; label: string;
@@ -126,33 +113,6 @@ export function HomePage() {
[recentToolIds, visibleTools], [recentToolIds, visibleTools],
); );
// ── Popular tools (fetch from API, cache result) ────────────
const [popularIds, setPopularIds] = useState<string[]>(FALLBACK_POPULAR_IDS);
useEffect(() => {
let cancelled = false;
apiGet<{ tools: string[] }>("/v1/tools/popular")
.then((data) => {
if (!cancelled && data.tools.length > 0) setPopularIds(data.tools);
})
.catch(() => {
// keep fallback
});
return () => {
cancelled = true;
};
}, []);
const popularTools = useMemo(
() =>
popularIds
.map((id) => visibleTools.find((tool) => tool.id === id))
.filter((tool): tool is Tool => tool != null)
.slice(0, 12),
[popularIds, visibleTools],
);
// ── Render ────────────────────────────────────────────────── // ── Render ──────────────────────────────────────────────────
return ( return (
@@ -182,10 +142,7 @@ export function HomePage() {
) : activeTab === "all" ? ( ) : activeTab === "all" ? (
<AllTabContent <AllTabContent
recentTools={recentTools} recentTools={recentTools}
popularTools={popularTools} groupedTools={groupedTools}
visibleTools={visibleTools}
tabCounts={tabCounts}
onTabChange={setActiveTab}
/> />
) : ( ) : (
<ModalityTabContent groupedTools={groupedTools} /> <ModalityTabContent groupedTools={groupedTools} />
@@ -330,25 +287,19 @@ function SearchResults({
function AllTabContent({ function AllTabContent({
recentTools, recentTools,
popularTools, groupedTools,
visibleTools,
tabCounts,
onTabChange,
}: { }: {
recentTools: Tool[]; recentTools: Tool[];
popularTools: Tool[]; groupedTools: Map<string, Tool[]>;
visibleTools: Tool[];
tabCounts: Record<string, number>;
onTabChange: (key: string) => void;
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<div className="space-y-10"> <div className="space-y-8">
{/* Recent tools (only shown if user has history) */} {/* Recent tools (only shown if user has history) */}
{recentTools.length > 0 && ( {recentTools.length > 0 && (
<section> <section>
<h2 className="text-sm font-semibold uppercase text-muted-foreground tracking-wider mb-3"> <h2 className="text-xs font-semibold uppercase text-muted-foreground tracking-wider mb-2">
{t.homePage.recent} {t.homePage.recent}
</h2> </h2>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
@@ -365,81 +316,20 @@ function AllTabContent({
</section> </section>
)} )}
{/* Popular */} {/* All tools grouped by category */}
{popularTools.length > 0 && ( {CATEGORIES.filter((cat) => groupedTools.has(cat.id)).map((category) => {
<section> const tools = groupedTools.get(category.id) ?? [];
<h2 className="text-sm font-semibold uppercase text-muted-foreground tracking-wider mb-3">
{t.homePage.popular}
</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-1">
{popularTools.map((tool) => (
<ToolCard key={tool.id} tool={tool} />
))}
</div>
</section>
)}
{/* Browse by Category */}
<section>
<h2 className="text-sm font-semibold uppercase text-muted-foreground tracking-wider mb-3">
{t.homePage.browseByCategory}
</h2>
<BrowseByCategoryGrid tabCounts={tabCounts} onTabChange={onTabChange} />
</section>
</div>
);
}
// ── Browse by Category Grid ──────────────────────────────────────
const BROWSE_TABS: Array<{
key: string;
modalityId: string;
label: string;
}> = [
{ key: "image", modalityId: "image", label: "Image" },
{ key: "video", modalityId: "video", label: "Video" },
{ key: "audio", modalityId: "audio", label: "Audio" },
{ key: "document", modalityId: "document", label: "Documents" },
{ key: "data", modalityId: "file", label: "Data" },
];
function BrowseByCategoryGrid({
tabCounts,
onTabChange,
}: {
tabCounts: Record<string, number>;
onTabChange: (key: string) => void;
}) {
const { t } = useTranslation();
return (
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-3">
{BROWSE_TABS.map((tab) => {
const modality = MODALITIES.find((m) => m.id === tab.modalityId);
const IconComponent = modality
? ((ICON_MAP[modality.icon] as React.ComponentType<{ className?: string }>) ?? FileImage)
: FileImage;
const count = tabCounts[tab.key] ?? 0;
return ( return (
<button <section key={category.id}>
key={tab.key} <h2 className="text-xs font-semibold uppercase text-muted-foreground tracking-wider mb-2">
type="button" {getCategoryName(t, category.id, category.name)}
onClick={() => onTabChange(tab.key)} </h2>
className="flex flex-col items-center gap-2 p-4 rounded-xl border border-border bg-card hover:bg-muted transition-colors text-center" <div className="grid grid-cols-1 sm:grid-cols-2 gap-1">
> {tools.map((tool) => (
<div <ToolCard key={tool.id} tool={tool} variant="descriptive" />
className="p-2.5 rounded-lg" ))}
style={{ backgroundColor: `${modality?.color ?? "#6B7280"}15` }}
>
<IconComponent className="h-5 w-5" style={{ color: modality?.color ?? "#6B7280" }} />
</div> </div>
<span className="text-sm font-medium text-foreground">{tab.label}</span> </section>
<span className="text-xs text-muted-foreground">
{format(t.homePage.toolCount, { count })}
</span>
</button>
); );
})} })}
</div> </div>