fix: garden ui polish - form styles, back nav, species dropdown, upload limit

This commit is contained in:
ginnoir
2026-06-01 23:38:52 -05:00
parent 6b25d67537
commit dd980c6932
6 changed files with 115 additions and 6 deletions
+14
View File
@@ -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:
+14 -2
View File
@@ -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}` });
+17 -1
View File
@@ -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">
+17 -1
View File
@@ -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>
); );
+51
View File
@@ -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 && (