import React, { useState } from 'react';
import {
DndContext,
closestCenter,
KeyboardSensor,
PointerSensor,
useSensor,
useSensors,
DragEndEvent,
DragOverlay,
DragStartEvent,
} from '@dnd-kit/core';
import {
arrayMove,
SortableContext,
sortableKeyboardCoordinates,
rectSortingStrategy,
} from '@dnd-kit/sortable';
import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
// Widget interface
interface Widget {
id: string;
title: string;
content: React.ReactNode;
size: 'small' | 'medium' | 'large';
color: string;
icon: string;
}
// Grid item component
function GridItem({
widget,
isDragging,
}: {
widget: Widget;
isDragging?: boolean;
}) {
const {
attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging: localIsDragging,
} = useSortable({ id: widget.id });
const style = {
transform: CSS.Transform.toString(transform),
transition,
opacity: isDragging || localIsDragging ? 0.5 : 1,
gridColumn: widget.size === 'large' ? 'span 2' : 'span 1',
gridRow: widget.size === 'large' ? 'span 2' : 'span 1',
};
return (
{widget.icon}
{widget.title}
{widget.content}
{/* Resize handle */}
);
}
// Main grid component
export function GridReorder() {
const [widgets, setWidgets] = useState([
{
id: 'widget-1',
title: 'Analytics',
content: (
),
size: 'large',
color: '#3b82f6',
icon: '๐',
},
{
id: 'widget-2',
title: 'Users',
content: 1,234
,
size: 'small',
color: '#10b981',
icon: '๐ฅ',
},
{
id: 'widget-3',
title: 'Revenue',
content: $54,321
,
size: 'small',
color: '#f59e0b',
icon: '๐ฐ',
},
{
id: 'widget-4',
title: 'Activity',
content: (
User signed up
New order received
Payment processed
),
size: 'medium',
color: '#8b5cf6',
icon: '๐',
},
{
id: 'widget-5',
title: 'Tasks',
content: (
In Progress
5
Completed
12
),
size: 'small',
color: '#ef4444',
icon: 'โ',
},
{
id: 'widget-6',
title: 'Calendar',
content: (
November 2024
{[...Array(30)].map((_, i) => (
{i + 1}
))}
),
size: 'medium',
color: '#ec4899',
icon: '๐
',
},
]);
const [activeId, setActiveId] = useState(null);
const [columns, setColumns] = useState(4);
const sensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: {
distance: 10,
},
}),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
})
);
function handleDragStart(event: DragStartEvent) {
setActiveId(event.active.id as string);
}
function handleDragEnd(event: DragEndEvent) {
const { active, over } = event;
if (over && active.id !== over.id) {
setWidgets((items) => {
const oldIndex = items.findIndex((item) => item.id === active.id);
const newIndex = items.findIndex((item) => item.id === over.id);
return arrayMove(items, oldIndex, newIndex);
});
}
setActiveId(null);
}
// Responsive columns
const handleColumnChange = (newColumns: number) => {
setColumns(newColumns);
};
const activeWidget = activeId
? widgets.find((w) => w.id === activeId)
: null;
return (
Dashboard Grid
{widgets.map((widget) => (
))}
{activeWidget && (
)}
{/* Instructions */}
Drag widgets to reorder โข
Resize using corner handle โข
Keyboard: Tab to focus, Space to grab, Arrows to move
);
}
// Styles
const styles = `
.grid-container {
padding: 2rem;
background: var(--color-gray-50);
min-height: 100vh;
}
.grid-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
}
.grid-controls {
display: flex;
gap: 1rem;
align-items: center;
}
.column-selector {
display: flex;
gap: 0.5rem;
align-items: center;
}
.column-selector label {
font-size: 0.875rem;
color: var(--color-text-secondary);
}
.column-selector button {
padding: 0.5rem 0.75rem;
background: var(--color-white);
border: 1px solid var(--color-border);
cursor: pointer;
transition: all 0.2s;
}
.column-selector button:hover {
background: var(--color-gray-100);
}
.column-selector button.active {
background: var(--color-primary);
color: white;
border-color: var(--color-primary);
}
.add-widget-btn {
padding: 0.5rem 1rem;
background: var(--color-primary);
color: white;
border: none;
border-radius: var(--radius-md);
cursor: pointer;
transition: all 0.2s;
}
.add-widget-btn:hover {
background: var(--color-primary-600);
}
.widget-grid {
display: grid;
gap: 1rem;
grid-auto-rows: minmax(120px, 1fr);
grid-auto-flow: dense;
}
.grid-item {
cursor: grab;
transition: transform 0.2s, opacity 0.2s;
}
.grid-item:active {
cursor: grabbing;
}
.widget {
height: 100%;
background: var(--color-white);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-sm);
padding: 1rem;
display: flex;
flex-direction: column;
position: relative;
transition: all 0.2s;
}
.widget:hover {
box-shadow: var(--shadow-md);
transform: translateY(-2px);
}
.widget-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 1rem;
}
.widget-icon {
font-size: 1.25rem;
}
.widget-title {
flex: 1;
font-size: 0.875rem;
font-weight: 600;
margin: 0;
}
.widget-menu {
padding: 0.25rem;
background: transparent;
border: none;
cursor: pointer;
color: var(--color-text-tertiary);
}
.widget-content {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.resize-handle {
position: absolute;
bottom: 0.5rem;
right: 0.5rem;
width: 1.5rem;
height: 1.5rem;
background: transparent;
border: none;
cursor: nwse-resize;
color: var(--color-text-tertiary);
opacity: 0;
transition: opacity 0.2s;
}
.widget:hover .resize-handle {
opacity: 1;
}
/* Widget content styles */
.metric {
font-size: 2rem;
font-weight: bold;
color: var(--color-text-primary);
}
.chart-placeholder {
display: flex;
gap: 0.5rem;
align-items: flex-end;
height: 100px;
width: 100%;
}
.bar {
flex: 1;
background: var(--color-primary-400);
border-radius: var(--radius-sm) var(--radius-sm) 0 0;
}
.activity-list {
width: 100%;
font-size: 0.813rem;
}
.activity-item {
padding: 0.5rem;
border-bottom: 1px solid var(--color-border);
}
.task-progress {
width: 100%;
}
.progress-item {
display: flex;
justify-content: space-between;
padding: 0.5rem;
font-size: 0.875rem;
}
.mini-calendar {
width: 100%;
font-size: 0.75rem;
}
.calendar-header {
text-align: center;
font-weight: 600;
margin-bottom: 0.5rem;
}
.calendar-grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 2px;
}
.calendar-day {
aspect-ratio: 1;
display: flex;
align-items: center;
justify-content: center;
background: var(--color-gray-50);
border-radius: 2px;
font-size: 0.625rem;
}
.grid-instructions {
margin-top: 2rem;
padding: 1rem;
background: var(--color-white);
border-radius: var(--radius-md);
text-align: center;
font-size: 0.875rem;
color: var(--color-text-secondary);
}
/* Responsive */
@media (max-width: 768px) {
.widget-grid {
grid-template-columns: repeat(2, 1fr) !important;
}
.grid-item.size-large {
grid-column: span 2;
}
}
@media (max-width: 480px) {
.widget-grid {
grid-template-columns: 1fr !important;
}
.grid-item.size-large,
.grid-item.size-medium {
grid-column: span 1;
grid-row: span 1;
}
}
/* Drag overlay */
.grid-item[aria-grabbed="true"] {
opacity: 0.5;
}
/* Focus styles */
.grid-item:focus-visible {
outline: 2px solid var(--color-primary);
outline-offset: 2px;
}
`;