page.tsx 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153
  1. "use client";
  2. import { useState, useCallback, useEffect } from "react";
  3. import {
  4. DndContext,
  5. closestCenter,
  6. KeyboardSensor,
  7. PointerSensor,
  8. useSensor,
  9. useSensors,
  10. type DragEndEvent,
  11. } from "@dnd-kit/core";
  12. import {
  13. arrayMove,
  14. SortableContext,
  15. sortableKeyboardCoordinates,
  16. rectSortingStrategy,
  17. } from "@dnd-kit/sortable";
  18. import { Widget } from "@/components/widget";
  19. import { Header } from "@/components/header";
  20. import { AddWidgetCard } from "@/components/add-widget-card";
  21. import { SortableWidget, type SavedWidget } from "@/components/sortable-widget";
  22. function DashboardContent() {
  23. const [savedWidgets, setSavedWidgets] = useState<SavedWidget[]>([]);
  24. const [newWidgetCount, setNewWidgetCount] = useState(0);
  25. const [isLoading, setIsLoading] = useState(true);
  26. const sensors = useSensors(
  27. useSensor(PointerSensor),
  28. useSensor(KeyboardSensor, {
  29. coordinateGetter: sortableKeyboardCoordinates,
  30. }),
  31. );
  32. // Load saved widgets on mount
  33. useEffect(() => {
  34. async function loadWidgets() {
  35. try {
  36. const res = await fetch("/api/v1/widgets");
  37. if (!res.ok) throw new Error(`HTTP ${res.status}`);
  38. const data = await res.json();
  39. setSavedWidgets(data.data || []);
  40. } catch (err) {
  41. console.error("Failed to load widgets:", err);
  42. } finally {
  43. setIsLoading(false);
  44. }
  45. }
  46. loadWidgets();
  47. }, []);
  48. // eslint-disable-next-line @typescript-eslint/no-unused-vars
  49. const handleWidgetSaved = useCallback((_id: string) => {
  50. // Reload saved widgets to get the new one
  51. fetch("/api/v1/widgets")
  52. .then((res) => (res.ok ? res.json() : { data: [] }))
  53. .then((data) => {
  54. setSavedWidgets(data.data || []);
  55. // Remove the new widget slot since it's now saved
  56. setNewWidgetCount((prev) => Math.max(0, prev - 1));
  57. });
  58. }, []);
  59. const handleAddWidget = useCallback(() => {
  60. setNewWidgetCount((prev) => prev + 1);
  61. }, []);
  62. const handleClearWidget = useCallback(async (id?: string) => {
  63. if (id) {
  64. // Delete from database
  65. await fetch(`/api/v1/widgets/${id}`, { method: "DELETE" });
  66. setSavedWidgets((prev) => prev.filter((w) => w.id !== id));
  67. } else {
  68. // Just remove the new widget slot
  69. setNewWidgetCount((prev) => Math.max(0, prev - 1));
  70. }
  71. }, []);
  72. const handleDragEnd = useCallback(async (event: DragEndEvent) => {
  73. const { active, over } = event;
  74. if (over && active.id !== over.id) {
  75. setSavedWidgets((items) => {
  76. const oldIndex = items.findIndex((item) => item.id === active.id);
  77. const newIndex = items.findIndex((item) => item.id === over.id);
  78. const newItems = arrayMove(items, oldIndex, newIndex);
  79. // Persist the new order to the server
  80. fetch("/api/v1/widgets/reorder", {
  81. method: "POST",
  82. headers: { "Content-Type": "application/json" },
  83. body: JSON.stringify({ orderedIds: newItems.map((w) => w.id) }),
  84. }).catch((err) => console.error("Failed to save widget order:", err));
  85. return newItems;
  86. });
  87. }
  88. }, []);
  89. if (isLoading) {
  90. return (
  91. <div className="min-h-screen flex items-center justify-center">
  92. <p className="text-muted-foreground">Loading...</p>
  93. </div>
  94. );
  95. }
  96. return (
  97. <div className="min-h-screen flex flex-col">
  98. <Header />
  99. <main className="flex-1 p-6">
  100. <DndContext
  101. sensors={sensors}
  102. collisionDetection={closestCenter}
  103. onDragEnd={handleDragEnd}
  104. >
  105. <SortableContext
  106. items={savedWidgets.map((w) => w.id)}
  107. strategy={rectSortingStrategy}
  108. >
  109. <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
  110. {/* Saved widgets */}
  111. {savedWidgets.map((widget) => (
  112. <SortableWidget
  113. key={widget.id}
  114. widget={widget}
  115. onDeleted={() => handleClearWidget(widget.id)}
  116. />
  117. ))}
  118. {/* New empty widgets */}
  119. {Array.from({ length: newWidgetCount }).map((_, i) => (
  120. <Widget
  121. key={`new-${i}`}
  122. onSaved={handleWidgetSaved}
  123. onDeleted={() => handleClearWidget()}
  124. />
  125. ))}
  126. {/* Add widget card */}
  127. <AddWidgetCard onClick={handleAddWidget} />
  128. </div>
  129. </SortableContext>
  130. </DndContext>
  131. </main>
  132. </div>
  133. );
  134. }
  135. export default function DashboardPage() {
  136. return <DashboardContent />;
  137. }