mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
feat: mobile-responsive settings dialog, homepage, nav, and toast
- Settings dialog: full-screen on mobile with horizontal pill nav, card-based user/team tables, compact audit log, stacked SettingRow - HomePage: stacked mobile layout with horizontal quick actions, tablet-friendly panel widths (w-64 lg:w-80) - Extract MobileBottomNav component with safe-area-inset padding - Add MobileBottomNav to fullscreen grid page - Larger touch targets on hamburger, sidebar close, bottom nav items - Toast repositioned to top-center on mobile (avoids bottom nav overlap) - PWA viewport-fit=cover for notch devices - Fix useMediaQuery null guard for test environment compatibility
This commit is contained in:
@@ -4,7 +4,9 @@ import { Eye, EyeOff, FileImage, LayoutGrid, List, Search } from "lucide-react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import { OtterLogo } from "@/components/common/otter-logo";
|
||||
import { MobileBottomNav } from "@/components/layout/mobile-bottom-nav";
|
||||
import { useTranslation } from "@/contexts/i18n-context";
|
||||
import { useMobile } from "@/hooks/use-mobile";
|
||||
import { track } from "@/lib/analytics";
|
||||
import { apiGet } from "@/lib/api";
|
||||
import { ICON_MAP } from "@/lib/icon-map";
|
||||
@@ -14,6 +16,7 @@ import { useFeaturesStore } from "@/stores/features-store";
|
||||
|
||||
export function FullscreenGridPage() {
|
||||
const { t } = useTranslation();
|
||||
const isMobile = useMobile();
|
||||
const [search, setSearch] = useState("");
|
||||
const [showDetails, setShowDetails] = useState(true);
|
||||
const navigate = useNavigate();
|
||||
@@ -79,7 +82,7 @@ export function FullscreenGridPage() {
|
||||
const activeCategories = CATEGORIES.filter((cat) => groupedTools.has(cat.id));
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background text-foreground">
|
||||
<div className={cn("min-h-screen bg-background text-foreground", isMobile && "pb-20")}>
|
||||
{/* Top bar */}
|
||||
<header className="sticky top-0 z-30 bg-background/95 backdrop-blur-sm border-b border-border">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 py-3 flex items-center gap-4">
|
||||
@@ -154,6 +157,7 @@ export function FullscreenGridPage() {
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
{isMobile && <MobileBottomNav />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { ImageViewer } from "@/components/common/image-viewer";
|
||||
import { MultiImageViewer } from "@/components/common/multi-image-viewer";
|
||||
import { AppLayout } from "@/components/layout/app-layout";
|
||||
import { useTranslation } from "@/contexts/i18n-context";
|
||||
import { useMobile } from "@/hooks/use-mobile";
|
||||
import { ICON_MAP } from "@/lib/icon-map";
|
||||
import { getCategoryName, getToolName } from "@/lib/tool-i18n";
|
||||
import { useFeaturesStore } from "@/stores/features-store";
|
||||
@@ -32,6 +33,7 @@ export function HomePage() {
|
||||
const location = useLocation();
|
||||
const { fetch: fetchSettings, defaultToolView, loaded: settingsLoaded } = useSettingsStore();
|
||||
const { fetch: fetchFeatures, bundles, installing, queued } = useFeaturesStore();
|
||||
const isMobile = useMobile();
|
||||
|
||||
useEffect(() => {
|
||||
if (location.state?.fromLibrary) {
|
||||
@@ -93,12 +95,96 @@ export function HomePage() {
|
||||
return <AppLayout onFiles={handleFiles} onUrlImport={handleUrlImport} />;
|
||||
}
|
||||
|
||||
// File uploaded — show tool selector on left, image preview on right
|
||||
// File uploaded — mobile: stacked layout
|
||||
if (isMobile && hasFile) {
|
||||
return (
|
||||
<AppLayout showToolPanel={false} onFiles={handleFiles}>
|
||||
<div className="flex flex-col h-full w-full">
|
||||
{/* File info bar */}
|
||||
<div className="flex items-center gap-2 px-4 py-3 border-b border-border">
|
||||
<ICON_MAP.CheckCircle2 className="h-4 w-4 text-green-500 shrink-0" />
|
||||
<span className="truncate text-sm font-medium text-foreground">
|
||||
{selectedFileName ?? files[0].name}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground shrink-0">
|
||||
{selectedFileSize ? `${(selectedFileSize / 1024).toFixed(1)} KB` : ""}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={reset}
|
||||
className="text-xs text-muted-foreground hover:text-foreground ms-auto shrink-0"
|
||||
>
|
||||
{t.homePage.changeFile}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Quick action buttons - horizontal scroll */}
|
||||
<div className="flex overflow-x-auto gap-2 px-4 py-3 border-b border-border scrollbar-none">
|
||||
{QUICK_ACTION_IDS.map((id) => {
|
||||
const tool = TOOLS.find((t) => t.id === id);
|
||||
if (!tool) return null;
|
||||
const Icon =
|
||||
(ICON_MAP[tool.icon] as React.ComponentType<{ className?: string }>) ??
|
||||
ICON_MAP.FileImage;
|
||||
const status = getToolStatus(id);
|
||||
return (
|
||||
<button
|
||||
key={id}
|
||||
type="button"
|
||||
onClick={() => navigate(tool.route)}
|
||||
className="flex items-center gap-2 px-3 py-2 rounded-xl border border-border hover:border-primary hover:bg-primary/5 transition-colors shrink-0"
|
||||
>
|
||||
<div className="p-1 rounded-lg bg-primary/10 text-primary">
|
||||
<Icon className="h-4 w-4" />
|
||||
</div>
|
||||
<span className="text-xs font-medium text-foreground whitespace-nowrap">
|
||||
{getToolName(t, tool.id, tool.name)}
|
||||
</span>
|
||||
{status === "not_installed" && (
|
||||
<Download className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
)}
|
||||
{status === "queued" && <Clock className="h-3.5 w-3.5 text-muted-foreground" />}
|
||||
{status === "installing" && (
|
||||
<Loader2 className="h-3.5 w-3.5 text-muted-foreground animate-spin" />
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Full-width image preview */}
|
||||
<div className="flex-1 flex items-center justify-center p-4 min-h-0">
|
||||
{files.length > 1 ? (
|
||||
<MultiImageViewer />
|
||||
) : currentEntry?.previewLoading ? (
|
||||
<div className="flex flex-col items-center justify-center h-full gap-3 text-center">
|
||||
<Loader2 className="h-8 w-8 text-muted-foreground animate-spin" />
|
||||
<p className="text-sm text-muted-foreground">{t.homePage.generatingPreview}</p>
|
||||
<p className="text-xs text-muted-foreground/60">{selectedFileName}</p>
|
||||
</div>
|
||||
) : originalBlobUrl ? (
|
||||
<ImageViewer
|
||||
src={originalBlobUrl}
|
||||
filename={selectedFileName ?? files[0].name}
|
||||
fileSize={selectedFileSize ?? files[0].size}
|
||||
/>
|
||||
) : (
|
||||
<div className="text-center text-muted-foreground">
|
||||
<p>{t.homePage.loadingPreview}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</AppLayout>
|
||||
);
|
||||
}
|
||||
|
||||
// File uploaded — desktop: tool selector on left, image preview on right
|
||||
return (
|
||||
<AppLayout showToolPanel={false} onFiles={handleFiles}>
|
||||
<div className="flex h-full w-full">
|
||||
{/* Left panel: Tool selector */}
|
||||
<div className="w-80 border-r border-border overflow-y-auto shrink-0">
|
||||
<div className="w-64 lg:w-80 border-r border-border overflow-y-auto shrink-0">
|
||||
{/* File info */}
|
||||
<div className="p-4 border-b border-border">
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
} from "lucide-react";
|
||||
import { Suspense, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import type { Crop } from "react-image-crop";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { Link, useParams } from "react-router-dom";
|
||||
import { BeforeAfterSlider } from "@/components/common/before-after-slider";
|
||||
import { BottomSheet } from "@/components/common/bottom-sheet";
|
||||
import { Dropzone } from "@/components/common/dropzone";
|
||||
@@ -347,8 +347,14 @@ export function ToolPage() {
|
||||
if (!tool || !registryEntry) {
|
||||
return (
|
||||
<AppLayout>
|
||||
<div className="flex items-center justify-center h-full text-muted-foreground">
|
||||
{t.toolPage.notFound}
|
||||
<div className="flex flex-col items-center justify-center h-full gap-4 text-muted-foreground">
|
||||
<p className="text-lg font-medium">{t.toolPage.notFound}</p>
|
||||
<Link
|
||||
to="/"
|
||||
className="px-4 py-2 rounded-lg bg-primary text-primary-foreground text-sm font-medium"
|
||||
>
|
||||
{t.common.goHome}
|
||||
</Link>
|
||||
</div>
|
||||
</AppLayout>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user