feat: implement Year Review feature with session management, item categorization, and real-time collaboration
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 6m7s
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 6m7s
This commit is contained in:
94
src/components/year-review/YearReviewBoard.tsx
Normal file
94
src/components/year-review/YearReviewBoard.tsx
Normal file
@@ -0,0 +1,94 @@
|
||||
'use client';
|
||||
|
||||
import { useTransition } from 'react';
|
||||
import { DragDropContext, Droppable, Draggable, DropResult } from '@hello-pangea/dnd';
|
||||
import type { YearReviewItem, YearReviewCategory } from '@prisma/client';
|
||||
import { YearReviewSection } from './YearReviewSection';
|
||||
import { YearReviewCard } from './YearReviewCard';
|
||||
import { moveYearReviewItem, reorderYearReviewItems } from '@/actions/year-review';
|
||||
import { YEAR_REVIEW_SECTIONS } from '@/lib/types';
|
||||
|
||||
interface YearReviewBoardProps {
|
||||
sessionId: string;
|
||||
items: YearReviewItem[];
|
||||
}
|
||||
|
||||
export function YearReviewBoard({ sessionId, items }: YearReviewBoardProps) {
|
||||
const [isPending, startTransition] = useTransition();
|
||||
|
||||
const itemsByCategory = YEAR_REVIEW_SECTIONS.reduce(
|
||||
(acc, section) => {
|
||||
acc[section.category] = items
|
||||
.filter((item) => item.category === section.category)
|
||||
.sort((a, b) => a.order - b.order);
|
||||
return acc;
|
||||
},
|
||||
{} as Record<YearReviewCategory, YearReviewItem[]>
|
||||
);
|
||||
|
||||
function handleDragEnd(result: DropResult) {
|
||||
if (!result.destination) return;
|
||||
|
||||
const { source, destination, draggableId } = result;
|
||||
const sourceCategory = source.droppableId as YearReviewCategory;
|
||||
const destCategory = destination.droppableId as YearReviewCategory;
|
||||
|
||||
// If same position, do nothing
|
||||
if (sourceCategory === destCategory && source.index === destination.index) {
|
||||
return;
|
||||
}
|
||||
|
||||
startTransition(async () => {
|
||||
if (sourceCategory === destCategory) {
|
||||
// Same category - just reorder
|
||||
const categoryItems = itemsByCategory[sourceCategory];
|
||||
const itemIds = categoryItems.map((item) => item.id);
|
||||
const [removed] = itemIds.splice(source.index, 1);
|
||||
itemIds.splice(destination.index, 0, removed);
|
||||
await reorderYearReviewItems(sessionId, sourceCategory, itemIds);
|
||||
} else {
|
||||
// Different category - move item
|
||||
await moveYearReviewItem(draggableId, sessionId, destCategory, destination.index);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`space-y-6 ${isPending ? 'opacity-70 pointer-events-none' : ''}`}>
|
||||
{/* Year Review Sections */}
|
||||
<DragDropContext onDragEnd={handleDragEnd}>
|
||||
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
|
||||
{YEAR_REVIEW_SECTIONS.map((section) => (
|
||||
<Droppable key={section.category} droppableId={section.category}>
|
||||
{(provided, snapshot) => (
|
||||
<YearReviewSection
|
||||
category={section.category}
|
||||
sessionId={sessionId}
|
||||
isDraggingOver={snapshot.isDraggingOver}
|
||||
ref={provided.innerRef}
|
||||
{...provided.droppableProps}
|
||||
>
|
||||
{itemsByCategory[section.category].map((item, index) => (
|
||||
<Draggable key={item.id} draggableId={item.id} index={index}>
|
||||
{(dragProvided, dragSnapshot) => (
|
||||
<YearReviewCard
|
||||
item={item}
|
||||
sessionId={sessionId}
|
||||
isDragging={dragSnapshot.isDragging}
|
||||
ref={dragProvided.innerRef}
|
||||
{...dragProvided.draggableProps}
|
||||
{...dragProvided.dragHandleProps}
|
||||
/>
|
||||
)}
|
||||
</Draggable>
|
||||
))}
|
||||
{provided.placeholder}
|
||||
</YearReviewSection>
|
||||
)}
|
||||
</Droppable>
|
||||
))}
|
||||
</div>
|
||||
</DragDropContext>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user