mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
fix: resolve 3 critical UX bugs - home upload, auth, and form submit
1. Home page file drop now shows quick-action tool selector 2. Auth disabled by default in dev (Docker still defaults to true) 3. Tool settings wrapped in forms - Enter key triggers processing
This commit is contained in:
@@ -85,6 +85,11 @@ app.get("/api/v1/health", async () => ({
|
|||||||
ai: {},
|
ai: {},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// Public config endpoint (for frontend to know if auth is required)
|
||||||
|
app.get("/api/v1/config/auth", async () => ({
|
||||||
|
authEnabled: env.AUTH_ENABLED,
|
||||||
|
}));
|
||||||
|
|
||||||
// Serve SPA in production
|
// Serve SPA in production
|
||||||
if (process.env.NODE_ENV === "production") {
|
if (process.env.NODE_ENV === "production") {
|
||||||
await registerStatic(app);
|
await registerStatic(app);
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ const envSchema = z.object({
|
|||||||
PORT: z.coerce.number().default(1350),
|
PORT: z.coerce.number().default(1350),
|
||||||
AUTH_ENABLED: z
|
AUTH_ENABLED: z
|
||||||
.enum(["true", "false"])
|
.enum(["true", "false"])
|
||||||
.default("true")
|
.default("false")
|
||||||
.transform((v) => v === "true"),
|
.transform((v) => v === "true"),
|
||||||
DEFAULT_USERNAME: z.string().default("admin"),
|
DEFAULT_USERNAME: z.string().default("admin"),
|
||||||
DEFAULT_PASSWORD: z.string().default("admin"),
|
DEFAULT_PASSWORD: z.string().default("admin"),
|
||||||
|
|||||||
@@ -176,7 +176,7 @@ function extractToken(request: FastifyRequest): string | null {
|
|||||||
|
|
||||||
// ── Auth middleware ────────────────────────────────────────────────
|
// ── Auth middleware ────────────────────────────────────────────────
|
||||||
|
|
||||||
const PUBLIC_PATHS = ["/api/v1/health", "/api/auth/", "/api/docs"];
|
const PUBLIC_PATHS = ["/api/v1/health", "/api/v1/config/", "/api/auth/", "/api/docs"];
|
||||||
|
|
||||||
function isPublicRoute(url: string): boolean {
|
function isPublicRoute(url: string): boolean {
|
||||||
return PUBLIC_PATHS.some((path) => url.startsWith(path));
|
return PUBLIC_PATHS.some((path) => url.startsWith(path));
|
||||||
|
|||||||
@@ -19,9 +19,10 @@ import { cn } from "@/lib/utils";
|
|||||||
interface AppLayoutProps {
|
interface AppLayoutProps {
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
showToolPanel?: boolean;
|
showToolPanel?: boolean;
|
||||||
|
onFiles?: (files: File[]) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AppLayout({ children, showToolPanel = true }: AppLayoutProps) {
|
export function AppLayout({ children, showToolPanel = true, onFiles }: AppLayoutProps) {
|
||||||
const [settingsOpen, setSettingsOpen] = useState(false);
|
const [settingsOpen, setSettingsOpen] = useState(false);
|
||||||
const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false);
|
const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false);
|
||||||
const isMobile = useMobile();
|
const isMobile = useMobile();
|
||||||
@@ -89,7 +90,7 @@ export function AppLayout({ children, showToolPanel = true }: AppLayoutProps) {
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex-1 overflow-y-auto p-6 flex items-center justify-center">
|
<div className="flex-1 overflow-y-auto p-6 flex items-center justify-center">
|
||||||
{children || <Dropzone />}
|
{children || <Dropzone onFiles={onFiles} accept="image/*" />}
|
||||||
</div>
|
</div>
|
||||||
{!isMobile && (
|
{!isMobile && (
|
||||||
<div className="text-center text-xs text-muted-foreground py-2 border-t border-border">
|
<div className="text-center text-xs text-muted-foreground py-2 border-t border-border">
|
||||||
|
|||||||
@@ -63,12 +63,18 @@ export function ColorSettings({ toolId }: ColorSettingsProps) {
|
|||||||
{ id: "effects", label: "Effects" },
|
{ id: "effects", label: "Effects" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (hasFile && hasChanges && !processing) handleProcess();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
{/* Tabs */}
|
{/* Tabs */}
|
||||||
<div className="flex gap-1">
|
<div className="flex gap-1">
|
||||||
{tabs.map((t) => (
|
{tabs.map((t) => (
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
key={t.id}
|
key={t.id}
|
||||||
onClick={() => setTab(t.id)}
|
onClick={() => setTab(t.id)}
|
||||||
className={`flex-1 text-xs py-1.5 rounded ${
|
className={`flex-1 text-xs py-1.5 rounded ${
|
||||||
@@ -146,6 +152,7 @@ export function ColorSettings({ toolId }: ColorSettingsProps) {
|
|||||||
<div className="grid grid-cols-2 gap-1">
|
<div className="grid grid-cols-2 gap-1">
|
||||||
{(["none", "grayscale", "sepia", "invert"] as const).map((e) => (
|
{(["none", "grayscale", "sepia", "invert"] as const).map((e) => (
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
key={e}
|
key={e}
|
||||||
onClick={() => setEffect(e)}
|
onClick={() => setEffect(e)}
|
||||||
className={`text-xs py-2 rounded capitalize transition-colors ${
|
className={`text-xs py-2 rounded capitalize transition-colors ${
|
||||||
@@ -164,6 +171,7 @@ export function ColorSettings({ toolId }: ColorSettingsProps) {
|
|||||||
{/* Reset button */}
|
{/* Reset button */}
|
||||||
{hasChanges && (
|
{hasChanges && (
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setBrightness(0);
|
setBrightness(0);
|
||||||
setContrast(0);
|
setContrast(0);
|
||||||
@@ -192,7 +200,7 @@ export function ColorSettings({ toolId }: ColorSettingsProps) {
|
|||||||
|
|
||||||
{/* Process */}
|
{/* Process */}
|
||||||
<button
|
<button
|
||||||
onClick={handleProcess}
|
type="submit"
|
||||||
disabled={!hasFile || !hasChanges || processing}
|
disabled={!hasFile || !hasChanges || processing}
|
||||||
className="w-full py-2.5 rounded-lg bg-primary text-primary-foreground font-medium disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
|
className="w-full py-2.5 rounded-lg bg-primary text-primary-foreground font-medium disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
|
||||||
>
|
>
|
||||||
@@ -211,7 +219,7 @@ export function ColorSettings({ toolId }: ColorSettingsProps) {
|
|||||||
Download
|
Download
|
||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
</div>
|
</form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,19 +28,26 @@ export function CompressSettings() {
|
|||||||
const canProcess =
|
const canProcess =
|
||||||
mode === "quality" || (mode === "targetSize" && Number(targetSizeKb) > 0);
|
mode === "quality" || (mode === "targetSize" && Number(targetSizeKb) > 0);
|
||||||
|
|
||||||
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (hasFile && canProcess && !processing) handleProcess();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
{/* Mode toggle */}
|
{/* Mode toggle */}
|
||||||
<div>
|
<div>
|
||||||
<label className="text-sm font-medium text-muted-foreground">Compression Mode</label>
|
<label className="text-sm font-medium text-muted-foreground">Compression Mode</label>
|
||||||
<div className="flex gap-1 mt-1">
|
<div className="flex gap-1 mt-1">
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
onClick={() => setMode("quality")}
|
onClick={() => setMode("quality")}
|
||||||
className={`flex-1 text-xs py-1.5 rounded ${mode === "quality" ? "bg-primary text-primary-foreground" : "bg-muted text-muted-foreground"}`}
|
className={`flex-1 text-xs py-1.5 rounded ${mode === "quality" ? "bg-primary text-primary-foreground" : "bg-muted text-muted-foreground"}`}
|
||||||
>
|
>
|
||||||
Quality
|
Quality
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
onClick={() => setMode("targetSize")}
|
onClick={() => setMode("targetSize")}
|
||||||
className={`flex-1 text-xs py-1.5 rounded ${mode === "targetSize" ? "bg-primary text-primary-foreground" : "bg-muted text-muted-foreground"}`}
|
className={`flex-1 text-xs py-1.5 rounded ${mode === "targetSize" ? "bg-primary text-primary-foreground" : "bg-muted text-muted-foreground"}`}
|
||||||
>
|
>
|
||||||
@@ -102,7 +109,7 @@ export function CompressSettings() {
|
|||||||
|
|
||||||
{/* Process */}
|
{/* Process */}
|
||||||
<button
|
<button
|
||||||
onClick={handleProcess}
|
type="submit"
|
||||||
disabled={!hasFile || !canProcess || processing}
|
disabled={!hasFile || !canProcess || processing}
|
||||||
className="w-full py-2.5 rounded-lg bg-primary text-primary-foreground font-medium disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
|
className="w-full py-2.5 rounded-lg bg-primary text-primary-foreground font-medium disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
|
||||||
>
|
>
|
||||||
@@ -121,6 +128,6 @@ export function CompressSettings() {
|
|||||||
Download
|
Download
|
||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
</div>
|
</form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,8 +32,13 @@ export function ConvertSettings() {
|
|||||||
|
|
||||||
const hasFile = files.length > 0;
|
const hasFile = files.length > 0;
|
||||||
|
|
||||||
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (hasFile && !processing) handleProcess();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
{/* Source format */}
|
{/* Source format */}
|
||||||
{hasFile && (
|
{hasFile && (
|
||||||
<div>
|
<div>
|
||||||
@@ -98,7 +103,7 @@ export function ConvertSettings() {
|
|||||||
|
|
||||||
{/* Process */}
|
{/* Process */}
|
||||||
<button
|
<button
|
||||||
onClick={handleProcess}
|
type="submit"
|
||||||
disabled={!hasFile || processing}
|
disabled={!hasFile || processing}
|
||||||
className="w-full py-2.5 rounded-lg bg-primary text-primary-foreground font-medium disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
|
className="w-full py-2.5 rounded-lg bg-primary text-primary-foreground font-medium disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
|
||||||
>
|
>
|
||||||
@@ -117,6 +122,6 @@ export function ConvertSettings() {
|
|||||||
Download
|
Download
|
||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
</div>
|
</form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,8 +41,13 @@ export function CropSettings() {
|
|||||||
const hasFile = files.length > 0;
|
const hasFile = files.length > 0;
|
||||||
const hasSize = Number(width) > 0 && Number(height) > 0;
|
const hasSize = Number(width) > 0 && Number(height) > 0;
|
||||||
|
|
||||||
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (hasFile && hasSize && !processing) handleProcess();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
{/* Position */}
|
{/* Position */}
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
<div>
|
<div>
|
||||||
@@ -97,6 +102,7 @@ export function CropSettings() {
|
|||||||
<div className="flex gap-1 mt-1">
|
<div className="flex gap-1 mt-1">
|
||||||
{ASPECT_PRESETS.map(({ label, w, h }) => (
|
{ASPECT_PRESETS.map(({ label, w, h }) => (
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
key={label}
|
key={label}
|
||||||
onClick={() => applyAspect(w, h)}
|
onClick={() => applyAspect(w, h)}
|
||||||
className="flex-1 text-xs py-1.5 rounded bg-muted text-muted-foreground hover:bg-primary hover:text-primary-foreground transition-colors"
|
className="flex-1 text-xs py-1.5 rounded bg-muted text-muted-foreground hover:bg-primary hover:text-primary-foreground transition-colors"
|
||||||
@@ -120,7 +126,7 @@ export function CropSettings() {
|
|||||||
|
|
||||||
{/* Process */}
|
{/* Process */}
|
||||||
<button
|
<button
|
||||||
onClick={handleProcess}
|
type="submit"
|
||||||
disabled={!hasFile || !hasSize || processing}
|
disabled={!hasFile || !hasSize || processing}
|
||||||
className="w-full py-2.5 rounded-lg bg-primary text-primary-foreground font-medium disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
|
className="w-full py-2.5 rounded-lg bg-primary text-primary-foreground font-medium disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
|
||||||
>
|
>
|
||||||
@@ -139,6 +145,6 @@ export function CropSettings() {
|
|||||||
Download
|
Download
|
||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
</div>
|
</form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,19 +41,26 @@ export function ResizeSettings() {
|
|||||||
// Group presets by platform
|
// Group presets by platform
|
||||||
const platforms = [...new Set(SOCIAL_MEDIA_PRESETS.map((p) => p.platform))];
|
const platforms = [...new Set(SOCIAL_MEDIA_PRESETS.map((p) => p.platform))];
|
||||||
|
|
||||||
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (hasFile && !processing) handleProcess();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
{/* Mode toggle */}
|
{/* Mode toggle */}
|
||||||
<div>
|
<div>
|
||||||
<label className="text-sm font-medium text-muted-foreground">Resize Mode</label>
|
<label className="text-sm font-medium text-muted-foreground">Resize Mode</label>
|
||||||
<div className="flex gap-1 mt-1">
|
<div className="flex gap-1 mt-1">
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
onClick={() => setMode("pixels")}
|
onClick={() => setMode("pixels")}
|
||||||
className={`flex-1 text-xs py-1.5 rounded ${mode === "pixels" ? "bg-primary text-primary-foreground" : "bg-muted text-muted-foreground"}`}
|
className={`flex-1 text-xs py-1.5 rounded ${mode === "pixels" ? "bg-primary text-primary-foreground" : "bg-muted text-muted-foreground"}`}
|
||||||
>
|
>
|
||||||
Pixels
|
Pixels
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
onClick={() => setMode("percentage")}
|
onClick={() => setMode("percentage")}
|
||||||
className={`flex-1 text-xs py-1.5 rounded ${mode === "percentage" ? "bg-primary text-primary-foreground" : "bg-muted text-muted-foreground"}`}
|
className={`flex-1 text-xs py-1.5 rounded ${mode === "percentage" ? "bg-primary text-primary-foreground" : "bg-muted text-muted-foreground"}`}
|
||||||
>
|
>
|
||||||
@@ -78,6 +85,7 @@ export function ResizeSettings() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
onClick={() => setLockAspect(!lockAspect)}
|
onClick={() => setLockAspect(!lockAspect)}
|
||||||
className="p-1.5 rounded border border-border text-muted-foreground hover:text-foreground"
|
className="p-1.5 rounded border border-border text-muted-foreground hover:text-foreground"
|
||||||
title={lockAspect ? "Unlock aspect ratio" : "Lock aspect ratio"}
|
title={lockAspect ? "Unlock aspect ratio" : "Lock aspect ratio"}
|
||||||
@@ -180,7 +188,7 @@ export function ResizeSettings() {
|
|||||||
|
|
||||||
{/* Process button */}
|
{/* Process button */}
|
||||||
<button
|
<button
|
||||||
onClick={handleProcess}
|
type="submit"
|
||||||
disabled={!hasFile || processing}
|
disabled={!hasFile || processing}
|
||||||
className="w-full py-2.5 rounded-lg bg-primary text-primary-foreground font-medium disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
|
className="w-full py-2.5 rounded-lg bg-primary text-primary-foreground font-medium disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
|
||||||
>
|
>
|
||||||
@@ -199,6 +207,6 @@ export function ResizeSettings() {
|
|||||||
Download
|
Download
|
||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
</div>
|
</form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,13 +33,19 @@ export function RotateSettings() {
|
|||||||
const hasFile = files.length > 0;
|
const hasFile = files.length > 0;
|
||||||
const hasChanges = angle !== 0 || flipH || flipV;
|
const hasChanges = angle !== 0 || flipH || flipV;
|
||||||
|
|
||||||
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (hasFile && hasChanges && !processing) handleProcess();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
{/* Quick rotate buttons */}
|
{/* Quick rotate buttons */}
|
||||||
<div>
|
<div>
|
||||||
<label className="text-xs text-muted-foreground">Quick Rotate</label>
|
<label className="text-xs text-muted-foreground">Quick Rotate</label>
|
||||||
<div className="flex gap-2 mt-1">
|
<div className="flex gap-2 mt-1">
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
onClick={rotateLeft}
|
onClick={rotateLeft}
|
||||||
className="flex-1 flex items-center justify-center gap-1 py-2 rounded bg-muted text-muted-foreground hover:bg-primary hover:text-primary-foreground transition-colors text-sm"
|
className="flex-1 flex items-center justify-center gap-1 py-2 rounded bg-muted text-muted-foreground hover:bg-primary hover:text-primary-foreground transition-colors text-sm"
|
||||||
>
|
>
|
||||||
@@ -47,6 +53,7 @@ export function RotateSettings() {
|
|||||||
90 Left
|
90 Left
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
onClick={rotateRight}
|
onClick={rotateRight}
|
||||||
className="flex-1 flex items-center justify-center gap-1 py-2 rounded bg-muted text-muted-foreground hover:bg-primary hover:text-primary-foreground transition-colors text-sm"
|
className="flex-1 flex items-center justify-center gap-1 py-2 rounded bg-muted text-muted-foreground hover:bg-primary hover:text-primary-foreground transition-colors text-sm"
|
||||||
>
|
>
|
||||||
@@ -77,6 +84,7 @@ export function RotateSettings() {
|
|||||||
<label className="text-xs text-muted-foreground">Flip</label>
|
<label className="text-xs text-muted-foreground">Flip</label>
|
||||||
<div className="flex gap-2 mt-1">
|
<div className="flex gap-2 mt-1">
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
onClick={() => setFlipH(!flipH)}
|
onClick={() => setFlipH(!flipH)}
|
||||||
className={`flex-1 flex items-center justify-center gap-1 py-2 rounded text-sm transition-colors ${
|
className={`flex-1 flex items-center justify-center gap-1 py-2 rounded text-sm transition-colors ${
|
||||||
flipH
|
flipH
|
||||||
@@ -88,6 +96,7 @@ export function RotateSettings() {
|
|||||||
Horizontal
|
Horizontal
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
onClick={() => setFlipV(!flipV)}
|
onClick={() => setFlipV(!flipV)}
|
||||||
className={`flex-1 flex items-center justify-center gap-1 py-2 rounded text-sm transition-colors ${
|
className={`flex-1 flex items-center justify-center gap-1 py-2 rounded text-sm transition-colors ${
|
||||||
flipV
|
flipV
|
||||||
@@ -114,7 +123,7 @@ export function RotateSettings() {
|
|||||||
|
|
||||||
{/* Process */}
|
{/* Process */}
|
||||||
<button
|
<button
|
||||||
onClick={handleProcess}
|
type="submit"
|
||||||
disabled={!hasFile || !hasChanges || processing}
|
disabled={!hasFile || !hasChanges || processing}
|
||||||
className="w-full py-2.5 rounded-lg bg-primary text-primary-foreground font-medium disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
|
className="w-full py-2.5 rounded-lg bg-primary text-primary-foreground font-medium disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
|
||||||
>
|
>
|
||||||
@@ -133,6 +142,6 @@ export function RotateSettings() {
|
|||||||
Download
|
Download
|
||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
</div>
|
</form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,8 +30,13 @@ export function StripMetadataSettings() {
|
|||||||
|
|
||||||
const hasFile = files.length > 0;
|
const hasFile = files.length > 0;
|
||||||
|
|
||||||
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (hasFile && !processing) handleProcess();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
{/* Strip All */}
|
{/* Strip All */}
|
||||||
<label className="flex items-center gap-2 text-sm text-foreground font-medium">
|
<label className="flex items-center gap-2 text-sm text-foreground font-medium">
|
||||||
<input
|
<input
|
||||||
@@ -108,7 +113,7 @@ export function StripMetadataSettings() {
|
|||||||
|
|
||||||
{/* Process */}
|
{/* Process */}
|
||||||
<button
|
<button
|
||||||
onClick={handleProcess}
|
type="submit"
|
||||||
disabled={!hasFile || processing}
|
disabled={!hasFile || processing}
|
||||||
className="w-full py-2.5 rounded-lg bg-primary text-primary-foreground font-medium disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
|
className="w-full py-2.5 rounded-lg bg-primary text-primary-foreground font-medium disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
|
||||||
>
|
>
|
||||||
@@ -127,6 +132,6 @@ export function StripMetadataSettings() {
|
|||||||
Download
|
Download
|
||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
</div>
|
</form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,9 +43,15 @@ export function useToolProcessor(toolId: string) {
|
|||||||
formData.append("file", files[0]);
|
formData.append("file", files[0]);
|
||||||
formData.append("settings", JSON.stringify(settings));
|
formData.append("settings", JSON.stringify(settings));
|
||||||
|
|
||||||
|
const headers: Record<string, string> = {};
|
||||||
|
const token = getToken();
|
||||||
|
if (token) {
|
||||||
|
headers.Authorization = `Bearer ${token}`;
|
||||||
|
}
|
||||||
|
|
||||||
const res = await fetch(`/api/v1/tools/${toolId}`, {
|
const res = await fetch(`/api/v1/tools/${toolId}`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { Authorization: `Bearer ${getToken()}` },
|
headers,
|
||||||
body: formData,
|
body: formData,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,87 @@
|
|||||||
|
import { useCallback, useState } from "react";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
import { AppLayout } from "@/components/layout/app-layout";
|
import { AppLayout } from "@/components/layout/app-layout";
|
||||||
|
import { useFileStore } from "@/stores/file-store";
|
||||||
|
import {
|
||||||
|
Maximize2,
|
||||||
|
Minimize2,
|
||||||
|
FileOutput,
|
||||||
|
Eraser,
|
||||||
|
X,
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
|
const QUICK_ACTIONS = [
|
||||||
|
{ id: "resize", name: "Resize", icon: Maximize2, route: "/resize" },
|
||||||
|
{ id: "compress", name: "Compress", icon: Minimize2, route: "/compress" },
|
||||||
|
{ id: "convert", name: "Convert", icon: FileOutput, route: "/convert" },
|
||||||
|
{ id: "remove-background", name: "Remove Background", icon: Eraser, route: "/remove-background" },
|
||||||
|
] as const;
|
||||||
|
|
||||||
export function HomePage() {
|
export function HomePage() {
|
||||||
return <AppLayout />;
|
const { setFiles, files, reset } = useFileStore();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [showActions, setShowActions] = useState(false);
|
||||||
|
|
||||||
|
const handleFiles = useCallback(
|
||||||
|
(newFiles: File[]) => {
|
||||||
|
reset();
|
||||||
|
setFiles(newFiles);
|
||||||
|
setShowActions(true);
|
||||||
|
},
|
||||||
|
[setFiles, reset],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleAction = (route: string) => {
|
||||||
|
setShowActions(false);
|
||||||
|
navigate(route);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDismiss = () => {
|
||||||
|
setShowActions(false);
|
||||||
|
reset();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AppLayout onFiles={handleFiles}>
|
||||||
|
{/* Quick-action overlay */}
|
||||||
|
{showActions && files.length > 0 && (
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm">
|
||||||
|
<div className="bg-background rounded-2xl shadow-2xl border border-border p-6 max-w-md w-full mx-4 animate-in fade-in zoom-in-95 duration-200">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h2 className="text-lg font-semibold text-foreground">
|
||||||
|
What would you like to do?
|
||||||
|
</h2>
|
||||||
|
<button
|
||||||
|
onClick={handleDismiss}
|
||||||
|
className="p-1.5 rounded-lg hover:bg-muted text-muted-foreground hover:text-foreground transition-colors"
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-sm text-muted-foreground mb-4">
|
||||||
|
<span className="font-medium text-foreground">{files[0].name}</span>
|
||||||
|
{" "}({(files[0].size / 1024).toFixed(1)} KB)
|
||||||
|
{files.length > 1 && ` +${files.length - 1} more`}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
{QUICK_ACTIONS.map(({ id, name, icon: Icon, route }) => (
|
||||||
|
<button
|
||||||
|
key={id}
|
||||||
|
onClick={() => handleAction(route)}
|
||||||
|
className="flex items-center gap-3 p-3 rounded-xl border border-border hover:border-primary hover:bg-primary/5 transition-colors text-left"
|
||||||
|
>
|
||||||
|
<div className="p-2 rounded-lg bg-primary/10 text-primary">
|
||||||
|
<Icon className="h-5 w-5" />
|
||||||
|
</div>
|
||||||
|
<span className="text-sm font-medium text-foreground">{name}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</AppLayout>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user