mirror of
https://github.com/Dvorinka/Dash.git
synced 2026-06-03 23:12:56 +00:00
refactor(frontend): restructure project layout and update API schema
Relocate frontend source code from `next-app/` to `frontend/` to align with the new project structure. This includes removing the old Next.js boilerplate files and establishing a cleaner workspace. Additionally, updates the OpenAPI specification to include support for the `immich` widget type and its corresponding configuration schema. - Move frontend files to `frontend/` - Delete obsolete `next-app/` directory and its configuration - Add `immich` widget type to `openapi.yaml` - Update `FrontendPlan.md` with dashboard refactor and UX direction
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import type { Group } from "@/lib/api/schema";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { useCreateGroup, useUpdateGroup } from "@/lib/api/hooks";
|
||||
|
||||
interface GroupFormProps {
|
||||
group?: Group | null;
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}
|
||||
|
||||
export function GroupForm({ group, open, onOpenChange }: GroupFormProps) {
|
||||
const isEdit = !!group;
|
||||
const createMut = useCreateGroup();
|
||||
const updateMut = useUpdateGroup();
|
||||
const [name, setName] = useState(group?.name || "");
|
||||
const [error, setError] = useState("");
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!name.trim()) {
|
||||
setError("Name is required");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (isEdit && group) {
|
||||
await updateMut.mutateAsync({ id: group.id, name: name.trim() });
|
||||
} else {
|
||||
await createMut.mutateAsync({ name: name.trim() });
|
||||
}
|
||||
onOpenChange(false);
|
||||
setName("");
|
||||
setError("");
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Failed");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{isEdit ? "Rename Group" : "Create Group"}</DialogTitle>
|
||||
<DialogDescription>{isEdit ? "Update group name" : "Add a new group for organizing apps"}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="flex flex-col gap-2 py-2">
|
||||
<Label htmlFor="group-name">Name</Label>
|
||||
<Input id="group-name" value={name} onChange={(e) => setName(e.target.value)} placeholder="Infrastructure" />
|
||||
{error && <span className="text-xs text-destructive">{error}</span>}
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)}>Cancel</Button>
|
||||
<Button onClick={handleSubmit} disabled={createMut.isPending || updateMut.isPending}>
|
||||
{isEdit ? "Save" : "Create"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
"use client";
|
||||
|
||||
import type { Group, Service } from "@/lib/api/schema";
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ServiceCard } from "@/components/services/service-card";
|
||||
import { ChevronDown, MoreVertical, Pencil, Trash2, GripVertical, FolderOpen } from "lucide-react";
|
||||
import { useUpdateGroup, useDeleteGroup } from "@/lib/api/hooks";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useState } from "react";
|
||||
import { useTheme } from "@/components/providers";
|
||||
|
||||
interface GroupSectionProps {
|
||||
group: Group;
|
||||
onEditService: (s: Service) => void;
|
||||
onDeleteService: (id: string) => void;
|
||||
onEditGroup: (g: Group) => void;
|
||||
dragHandleProps?: React.HTMLAttributes<HTMLDivElement>;
|
||||
}
|
||||
|
||||
export function GroupSection({ group, onEditService, onDeleteService, onEditGroup, dragHandleProps }: GroupSectionProps) {
|
||||
const updateGroup = useUpdateGroup();
|
||||
const deleteGroup = useDeleteGroup();
|
||||
const [open, setOpen] = useState(!group.collapsed);
|
||||
const { theme } = useTheme();
|
||||
const isCasaOS = theme === "casaos";
|
||||
|
||||
const handleToggle = () => {
|
||||
const next = !open;
|
||||
setOpen(next);
|
||||
updateGroup.mutate({ id: group.id, collapsed: !next });
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
if (group.services.length > 0) {
|
||||
deleteGroup.mutate({ id: group.id, moveServices: true });
|
||||
} else {
|
||||
deleteGroup.mutate({ id: group.id });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Collapsible open={open} onOpenChange={setOpen}>
|
||||
<div className={cn("mb-5 rounded-2xl group/group", isCasaOS && "bg-card border border-border")}>
|
||||
{/* Group header */}
|
||||
<div className="flex items-center gap-2 px-3 py-2.5">
|
||||
{dragHandleProps && (
|
||||
<div
|
||||
{...dragHandleProps}
|
||||
className="cursor-grab rounded-md p-1 opacity-0 transition-opacity hover:bg-accent group-hover/group:opacity-60"
|
||||
>
|
||||
<GripVertical className="h-4 w-4 text-muted-foreground" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<CollapsibleTrigger asChild>
|
||||
<button
|
||||
className="flex flex-1 items-center gap-2.5 group/title min-w-0"
|
||||
onClick={handleToggle}
|
||||
>
|
||||
<div className={cn(
|
||||
"flex h-7 w-7 items-center justify-center rounded-lg transition-colors",
|
||||
isCasaOS ? "bg-white/10" : "bg-accent"
|
||||
)}>
|
||||
<FolderOpen className={cn("h-3.5 w-3.5", isCasaOS ? "text-blue-300" : "text-accent-foreground")} />
|
||||
</div>
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<span className="text-sm font-semibold truncate">{group.name}</span>
|
||||
<span className="text-xs text-muted-foreground font-mono">{group.services.length}</span>
|
||||
</div>
|
||||
<ChevronDown className={cn(
|
||||
"ml-auto h-4 w-4 text-muted-foreground transition-transform duration-200 shrink-0",
|
||||
!open && "-rotate-90"
|
||||
)} />
|
||||
</button>
|
||||
</CollapsibleTrigger>
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon" className="h-7 w-7 rounded-lg shrink-0 hover:bg-accent">
|
||||
<MoreVertical className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-40 rounded-xl">
|
||||
<DropdownMenuItem onClick={() => onEditGroup(group)} className="gap-2 text-xs">
|
||||
<Pencil className="h-3.5 w-3.5" /> Rename
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem className="gap-2 text-xs text-destructive" onClick={handleDelete}>
|
||||
<Trash2 className="h-3.5 w-3.5" /> Delete
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
|
||||
{/* Divider */}
|
||||
<div className={cn("mx-3 h-px", isCasaOS ? "bg-white/5" : "bg-border/40")} />
|
||||
|
||||
{/* Services grid */}
|
||||
<CollapsibleContent>
|
||||
<div className="p-3 pt-2">
|
||||
<div className="grid grid-cols-3 gap-3 sm:grid-cols-4 md:grid-cols-5 lg:grid-cols-6 xl:grid-cols-8">
|
||||
{group.services.map((s) => (
|
||||
<ServiceCard key={s.id} service={s} onEdit={onEditService} onDelete={onDeleteService} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
</div>
|
||||
</Collapsible>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user