fix: garden ui polish - form styles, back nav, species dropdown, upload limit
This commit is contained in:
@@ -11,5 +11,19 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- famapp-db-data:/var/lib/postgresql/data
|
- famapp-db-data:/var/lib/postgresql/data
|
||||||
|
|
||||||
|
minio:
|
||||||
|
image: minio/minio:latest
|
||||||
|
command: server /data --console-address ":9001"
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
MINIO_ROOT_USER: famapp
|
||||||
|
MINIO_ROOT_PASSWORD: changeme
|
||||||
|
ports:
|
||||||
|
- "9000:9000"
|
||||||
|
- "9001:9001"
|
||||||
|
volumes:
|
||||||
|
- famapp-minio-data:/data
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
famapp-db-data:
|
famapp-db-data:
|
||||||
|
famapp-minio-data:
|
||||||
|
|||||||
@@ -4,8 +4,13 @@ import { getCurrentSession } from "@/lib/session";
|
|||||||
import { ensureBucket, minioClient, MINIO_BUCKET } from "@/lib/minio";
|
import { ensureBucket, minioClient, MINIO_BUCKET } from "@/lib/minio";
|
||||||
|
|
||||||
export const runtime = "nodejs";
|
export const runtime = "nodejs";
|
||||||
|
export const maxDuration = 60;
|
||||||
|
|
||||||
const MAX_FILE_SIZE = 5 * 1024 * 1024;
|
export const config = {
|
||||||
|
api: { bodyParser: { sizeLimit: "100mb" } },
|
||||||
|
};
|
||||||
|
|
||||||
|
const MAX_FILE_SIZE = 100 * 1024 * 1024;
|
||||||
|
|
||||||
export async function POST(request: Request) {
|
export async function POST(request: Request) {
|
||||||
let session: Awaited<ReturnType<typeof getCurrentSession>>;
|
let session: Awaited<ReturnType<typeof getCurrentSession>>;
|
||||||
@@ -49,8 +54,15 @@ export async function POST(request: Request) {
|
|||||||
"Content-Type": file.type,
|
"Content-Type": file.type,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
const msg = err instanceof Error ? err.message : String(err);
|
||||||
console.error("Upload failed", err);
|
console.error("Upload failed", err);
|
||||||
return NextResponse.json({ error: "Upload service unavailable" }, { status: 503 });
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
error: "Upload service unavailable",
|
||||||
|
detail: process.env.NODE_ENV !== "production" ? msg : undefined,
|
||||||
|
},
|
||||||
|
{ status: 503 },
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return NextResponse.json({ url: `/api/uploads/${key}` });
|
return NextResponse.json({ url: `/api/uploads/${key}` });
|
||||||
|
|||||||
@@ -14,7 +14,23 @@ export default function NewContainerPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="page-content max-w-lg">
|
<div className="page-content max-w-lg">
|
||||||
<h1 className="page-title">New container</h1>
|
<div className="flex items-center gap-3 mb-1">
|
||||||
|
<a href="/garden" className="btn btn-ghost btn-icon" aria-label="Back to garden">
|
||||||
|
<svg
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
>
|
||||||
|
<polyline points="15 18 9 12 15 6" />
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
<h1 className="page-title">New container</h1>
|
||||||
|
</div>
|
||||||
<form action={handleCreate} className="flex flex-col gap-4 mt-4">
|
<form action={handleCreate} className="flex flex-col gap-4 mt-4">
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<label htmlFor="name" className="text-sm font-medium">
|
<label htmlFor="name" className="text-sm font-medium">
|
||||||
|
|||||||
@@ -11,7 +11,23 @@ export default async function NewPlantPage({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="page-content max-w-xl">
|
<div className="page-content max-w-xl">
|
||||||
<h1 className="page-title">Add Plant</h1>
|
<div className="flex items-center gap-3 mb-1">
|
||||||
|
<a href="/garden" className="btn btn-ghost btn-icon" aria-label="Back to garden">
|
||||||
|
<svg
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
>
|
||||||
|
<polyline points="15 18 9 12 15 6" />
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
<h1 className="page-title">Add Plant</h1>
|
||||||
|
</div>
|
||||||
<PlantForm containers={containers} defaultContainerId={params.containerId ?? null} />
|
<PlantForm containers={containers} defaultContainerId={params.containerId ?? null} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1295,3 +1295,54 @@
|
|||||||
color: var(--ink-mute);
|
color: var(--ink-mute);
|
||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Form controls ──────────────────────────────────────────────── */
|
||||||
|
.input,
|
||||||
|
select {
|
||||||
|
width: 100%;
|
||||||
|
padding: 7px 10px;
|
||||||
|
border-radius: var(--r-sm);
|
||||||
|
border: 1px solid var(--hair-2);
|
||||||
|
background: var(--card);
|
||||||
|
color: var(--ink);
|
||||||
|
font: inherit;
|
||||||
|
font-size: 13.5px;
|
||||||
|
line-height: 1.4;
|
||||||
|
box-shadow: inset 0 1px 2px rgba(31, 27, 22, 0.05);
|
||||||
|
transition:
|
||||||
|
border-color 0.12s,
|
||||||
|
box-shadow 0.12s;
|
||||||
|
appearance: none;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
.input:focus,
|
||||||
|
select:focus {
|
||||||
|
border-color: var(--ink-soft);
|
||||||
|
box-shadow:
|
||||||
|
inset 0 1px 2px rgba(31, 27, 22, 0.05),
|
||||||
|
0 0 0 2px rgba(31, 27, 22, 0.08);
|
||||||
|
}
|
||||||
|
.input::placeholder {
|
||||||
|
color: var(--ink-faint);
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%238a7e6e' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: right 10px center;
|
||||||
|
padding-right: 30px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.dark .input,
|
||||||
|
.dark select {
|
||||||
|
background: var(--paper-2);
|
||||||
|
border-color: var(--hair-2);
|
||||||
|
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.12);
|
||||||
|
}
|
||||||
|
.dark .input:focus,
|
||||||
|
.dark select:focus {
|
||||||
|
border-color: var(--ink-soft);
|
||||||
|
box-shadow:
|
||||||
|
inset 0 1px 2px rgba(0, 0, 0, 0.12),
|
||||||
|
0 0 0 2px rgba(244, 239, 229, 0.08);
|
||||||
|
}
|
||||||
|
|||||||
@@ -83,13 +83,13 @@ export function SpeciesSearch({ onSelect, initialValue = "" }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isOpen && results.length > 0 && (
|
{isOpen && results.length > 0 && (
|
||||||
<ul className="absolute z-50 w-full mt-1 bg-[var(--surface-1)] border border-[var(--ink-faint)] rounded-md shadow-lg max-h-52 overflow-auto">
|
<ul className="absolute z-50 w-full mt-1 bg-[var(--card)] border border-[var(--hair-2)] rounded-md shadow-lg max-h-52 overflow-auto">
|
||||||
{results.map((s) => (
|
{results.map((s) => (
|
||||||
<li key={s.id}>
|
<li key={s.id}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => handleSelect(s)}
|
onClick={() => handleSelect(s)}
|
||||||
className="w-full text-left px-3 py-2 hover:bg-[var(--surface-2)] text-sm"
|
className="w-full text-left px-3 py-2 hover:bg-[var(--paper-2)] text-sm text-[var(--ink)]"
|
||||||
>
|
>
|
||||||
<span className="font-medium">{s.common_name}</span>
|
<span className="font-medium">{s.common_name}</span>
|
||||||
{s.scientific_name && (
|
{s.scientific_name && (
|
||||||
|
|||||||
Reference in New Issue
Block a user