mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
feat: move theme toggle and language selector to top nav bar
Theme toggle (sun/moon) and language selector (globe + dropdown) now live in the top nav's right section alongside help and avatar. Removed the floating Footer component from the home page since its contents are now in the nav bar.
This commit is contained in:
@@ -2,14 +2,19 @@ import {
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
FolderOpen,
|
||||
Globe,
|
||||
HelpCircle,
|
||||
LayoutGrid,
|
||||
Moon,
|
||||
Sun,
|
||||
Workflow,
|
||||
} from "lucide-react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { Link, useLocation } from "react-router-dom";
|
||||
import { useTranslation } from "@/contexts/i18n-context";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { useMobile } from "@/hooks/use-mobile";
|
||||
import { useTheme } from "@/hooks/use-theme";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ImageEditIcon } from "../common/image-edit-icon";
|
||||
import { OtterLogo } from "../common/otter-logo";
|
||||
@@ -195,8 +200,11 @@ export function TopNav({
|
||||
{/* Spacer */}
|
||||
<div className="flex-1" />
|
||||
|
||||
{/* Right: help + avatar */}
|
||||
<div className="flex items-center gap-1">
|
||||
{/* Right: theme + language + help + avatar */}
|
||||
<div className="flex items-center gap-0.5">
|
||||
{!isMobile && <ThemeToggle isDark={isDark} />}
|
||||
{!isMobile && <LanguageSelector isDark={isDark} />}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={onHelpClick}
|
||||
@@ -218,3 +226,95 @@ export function TopNav({
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
function ThemeToggle({ isDark }: { isDark: boolean }) {
|
||||
const { resolvedTheme, toggleTheme } = useTheme();
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleTheme}
|
||||
className={cn(
|
||||
"p-1.5 rounded-md transition-colors",
|
||||
isDark
|
||||
? "text-[#aaa] hover:text-[#e0e0e0] hover:bg-[#333]"
|
||||
: "text-muted-foreground hover:text-foreground hover:bg-muted",
|
||||
)}
|
||||
title="Toggle theme"
|
||||
>
|
||||
{resolvedTheme === "dark" ? <Sun className="h-4 w-4" /> : <Moon className="h-4 w-4" />}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function LanguageSelector({ isDark }: { isDark: boolean }) {
|
||||
const { locale, setLocale, supportedLocales } = useTranslation();
|
||||
const [open, setOpen] = useState(false);
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
function handleClickOutside(e: MouseEvent) {
|
||||
if (ref.current && !ref.current.contains(e.target as Node)) {
|
||||
setOpen(false);
|
||||
}
|
||||
}
|
||||
if (open) document.addEventListener("mousedown", handleClickOutside);
|
||||
return () => document.removeEventListener("mousedown", handleClickOutside);
|
||||
}, [open]);
|
||||
|
||||
const current = supportedLocales.find((l) => l.code === locale);
|
||||
|
||||
return (
|
||||
<div ref={ref} className="relative">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOpen((v) => !v)}
|
||||
className={cn(
|
||||
"flex items-center gap-1 px-2 py-1.5 rounded-md text-xs transition-colors",
|
||||
isDark
|
||||
? "text-[#aaa] hover:text-[#e0e0e0] hover:bg-[#333]"
|
||||
: "text-muted-foreground hover:text-foreground hover:bg-muted",
|
||||
)}
|
||||
title="Language"
|
||||
>
|
||||
<Globe className="h-3.5 w-3.5" />
|
||||
{current?.nativeName ?? "English"}
|
||||
</button>
|
||||
{open && (
|
||||
<div
|
||||
className={cn(
|
||||
"absolute top-full mt-1 end-0 w-48 max-h-72 overflow-y-auto rounded-lg border shadow-lg z-50",
|
||||
isDark ? "bg-[#2a2a2a] border-[#444]" : "bg-card border-border",
|
||||
)}
|
||||
>
|
||||
{supportedLocales.map((l) => (
|
||||
<button
|
||||
key={l.code}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setLocale(l.code);
|
||||
setOpen(false);
|
||||
}}
|
||||
className={cn(
|
||||
"w-full text-start px-3 py-1.5 text-sm transition-colors flex items-center justify-between",
|
||||
isDark ? "hover:bg-[#333] text-[#e0e0e0]" : "hover:bg-muted text-foreground",
|
||||
)}
|
||||
>
|
||||
<span className={l.code === locale ? "font-medium" : ""}>{l.nativeName}</span>
|
||||
{l.code === locale && (
|
||||
<svg
|
||||
className="h-3.5 w-3.5 text-primary"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
>
|
||||
<polyline points="20 6 9 17 4 12" />
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Link, useLocation, useNavigate } from "react-router-dom";
|
||||
import { ToolCard } from "@/components/common/tool-card.js";
|
||||
import { AppLayout } from "@/components/layout/app-layout.js";
|
||||
import { Footer } from "@/components/layout/footer.js";
|
||||
import { useTranslation } from "@/contexts/i18n-context";
|
||||
import { useFuseSearch } from "@/hooks/use-fuse-search.js";
|
||||
import { usePageTitle } from "@/hooks/use-page-title.js";
|
||||
@@ -24,8 +23,7 @@ export function HomePage() {
|
||||
const { t } = useTranslation();
|
||||
const [activeTab, setActiveTab] = useState<string>("all");
|
||||
const [search, setSearch] = useState("");
|
||||
const { fetch: fetchSettings, disabledTools, experimentalEnabled, loaded } =
|
||||
useSettingsStore();
|
||||
const { fetch: fetchSettings, disabledTools, experimentalEnabled, loaded } = useSettingsStore();
|
||||
const recentToolIds = useRecentTools();
|
||||
|
||||
usePageTitle();
|
||||
@@ -119,8 +117,6 @@ export function HomePage() {
|
||||
<ToolGrid recentTools={recentTools} groupedTools={groupedTools} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
</AppLayout>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user