page.tsx 4.5 KB

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