lint frontend, fix drag and drop bug
This commit is contained in:
parent
1bcad71f23
commit
1628677222
13 changed files with 382 additions and 52 deletions
120
frontend/src/components/kanban/CreateCardModal.tsx
Normal file
120
frontend/src/components/kanban/CreateCardModal.tsx
Normal file
|
|
@ -0,0 +1,120 @@
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { ModalContentProps } from '../../types';
|
||||||
|
import { useToast } from '../../context/toasts/useToast';
|
||||||
|
|
||||||
|
const cardSchema = z.object({
|
||||||
|
name: z
|
||||||
|
.string()
|
||||||
|
.min(1, 'Card name is required')
|
||||||
|
.max(100, 'Card name must be less than 100 characters'),
|
||||||
|
description: z.string().max(2000, 'Description must be less than 2000 characters').optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
type CardFormData = z.infer<typeof cardSchema>;
|
||||||
|
|
||||||
|
interface CreateCardModalProps extends ModalContentProps {
|
||||||
|
onCreate: (data: { name: string; description?: string }) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CreateCardModal({ onClose, onCreate }: CreateCardModalProps) {
|
||||||
|
const { addNotification } = useToast();
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
handleSubmit,
|
||||||
|
formState: { errors },
|
||||||
|
} = useForm<CardFormData>({
|
||||||
|
resolver: zodResolver(cardSchema),
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSubmit = async (data: CardFormData) => {
|
||||||
|
try {
|
||||||
|
await onCreate(data);
|
||||||
|
onClose();
|
||||||
|
} catch (err) {
|
||||||
|
const errorMessage = err instanceof Error ? err.message : 'Failed to create card';
|
||||||
|
addNotification({
|
||||||
|
type: 'error',
|
||||||
|
title: 'Error Creating Card',
|
||||||
|
message: errorMessage,
|
||||||
|
duration: 5000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-gray-800 rounded-lg shadow-xl w-full max-w-md border border-gray-700">
|
||||||
|
<div className="flex justify-between items-center p-6 border-b border-gray-700">
|
||||||
|
<h2 id="modal-title" className="text-xl font-bold text-white">
|
||||||
|
Create Card
|
||||||
|
</h2>
|
||||||
|
<button onClick={onClose} className="text-gray-400 hover:text-white transition-colors">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
>
|
||||||
|
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||||
|
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit(onSubmit)} className="p-6">
|
||||||
|
<div className="mb-4">
|
||||||
|
<label htmlFor="name" className="block text-sm font-medium text-gray-300 mb-2">
|
||||||
|
Card Name <span className="text-red-400">*</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="name"
|
||||||
|
type="text"
|
||||||
|
{...register('name')}
|
||||||
|
className="w-full px-4 py-3 bg-gray-700 border border-gray-600 rounded-lg text-white focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
|
placeholder="Enter card name..."
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
{errors.name && <p className="mt-1 text-sm text-red-400">{errors.name.message}</p>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-6">
|
||||||
|
<label htmlFor="description" className="block text-sm font-medium text-gray-300 mb-2">
|
||||||
|
Description
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
id="description"
|
||||||
|
rows={4}
|
||||||
|
{...register('description')}
|
||||||
|
className="w-full px-4 py-3 bg-gray-700 border border-gray-600 rounded-lg text-white focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-none"
|
||||||
|
placeholder="Optional card description..."
|
||||||
|
/>
|
||||||
|
{errors.description && (
|
||||||
|
<p className="mt-1 text-sm text-red-400">{errors.description.message}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-3">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onClose}
|
||||||
|
className="px-6 py-3 text-gray-300 hover:text-white transition-colors"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="bg-blue-600 hover:bg-blue-700 text-white font-medium py-3 px-6 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
Create Card
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
106
frontend/src/components/kanban/CreateListModal.tsx
Normal file
106
frontend/src/components/kanban/CreateListModal.tsx
Normal file
|
|
@ -0,0 +1,106 @@
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { ModalContentProps } from '../../types';
|
||||||
|
import { useToast } from '@/context/toasts';
|
||||||
|
|
||||||
|
const listSchema = z.object({
|
||||||
|
name: z
|
||||||
|
.string()
|
||||||
|
.min(1, 'List name is required')
|
||||||
|
.max(100, 'List name must be less than 100 characters'),
|
||||||
|
});
|
||||||
|
|
||||||
|
type ListFormData = z.infer<typeof listSchema>;
|
||||||
|
|
||||||
|
interface CreateListModalProps extends ModalContentProps {
|
||||||
|
onCreate: (name: string) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CreateListModal({ onClose, onCreate }: CreateListModalProps) {
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
handleSubmit,
|
||||||
|
formState: { errors },
|
||||||
|
} = useForm<ListFormData>({
|
||||||
|
resolver: zodResolver(listSchema),
|
||||||
|
});
|
||||||
|
|
||||||
|
const { addNotification } = useToast();
|
||||||
|
|
||||||
|
const onSubmit = async (data: ListFormData) => {
|
||||||
|
try {
|
||||||
|
await onCreate(data.name);
|
||||||
|
onClose();
|
||||||
|
} catch (err) {
|
||||||
|
// Error is handled by caller
|
||||||
|
|
||||||
|
const errorMessage = err instanceof Error ? err.message : 'Failed to create card';
|
||||||
|
addNotification({
|
||||||
|
type: 'error',
|
||||||
|
title: 'Error Register',
|
||||||
|
message: errorMessage,
|
||||||
|
duration: 5000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-gray-800 rounded-lg shadow-xl w-full max-w-md border border-gray-700">
|
||||||
|
<div className="flex justify-between items-center p-6 border-b border-gray-700">
|
||||||
|
<h2 id="modal-title" className="text-xl font-bold text-white">
|
||||||
|
Create List
|
||||||
|
</h2>
|
||||||
|
<button onClick={onClose} className="text-gray-400 hover:text-white transition-colors">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
>
|
||||||
|
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||||
|
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit(onSubmit)} className="p-6">
|
||||||
|
<div className="mb-6">
|
||||||
|
<label htmlFor="name" className="block text-sm font-medium text-gray-300 mb-2">
|
||||||
|
List Name <span className="text-red-400">*</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="name"
|
||||||
|
type="text"
|
||||||
|
{...register('name')}
|
||||||
|
className="w-full px-4 py-3 bg-gray-700 border border-gray-600 rounded-lg text-white focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
|
placeholder="e.g., To Do, In Progress, Done"
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
{errors.name && <p className="mt-1 text-sm text-red-400">{errors.name.message}</p>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-3">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onClose}
|
||||||
|
className="px-6 py-3 text-gray-300 hover:text-white transition-colors"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="bg-blue-600 hover:bg-blue-700 text-white font-medium py-3 px-6 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
Create List
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -9,7 +9,7 @@ interface KanbanCardProps {
|
||||||
|
|
||||||
export function KanbanCard({ card, onClick }: KanbanCardProps) {
|
export function KanbanCard({ card, onClick }: KanbanCardProps) {
|
||||||
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
|
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
|
||||||
id: card.id.toString(),
|
id: `CARD_${card.id}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
const style = {
|
const style = {
|
||||||
|
|
|
||||||
|
|
@ -2,20 +2,29 @@ import { useDroppable } from '@dnd-kit/core';
|
||||||
import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable';
|
import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable';
|
||||||
import { ListWithCards, Card as CardType } from '../../types/kanban';
|
import { ListWithCards, Card as CardType } from '../../types/kanban';
|
||||||
import { KanbanCard } from './KanbanCard';
|
import { KanbanCard } from './KanbanCard';
|
||||||
|
import { CreateCardModal } from './CreateCardModal';
|
||||||
|
import { useModal } from '../../context/modals/useModal';
|
||||||
|
|
||||||
interface KanbanColumnProps {
|
interface KanbanColumnProps {
|
||||||
list: ListWithCards;
|
list: ListWithCards;
|
||||||
cards: CardType[];
|
cards: CardType[];
|
||||||
onCardClick: (card: CardType) => void;
|
onCardClick: (card: CardType) => void;
|
||||||
|
onCardCreate: (data: { name: string; description?: string }) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function KanbanColumn({ list, cards, onCardClick }: KanbanColumnProps) {
|
export function KanbanColumn({ list, cards, onCardClick, onCardCreate }: KanbanColumnProps) {
|
||||||
const { setNodeRef, isOver } = useDroppable({
|
const { setNodeRef, isOver } = useDroppable({
|
||||||
id: list.id.toString(),
|
id: `LIST_${list.id}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { openModal } = useModal();
|
||||||
|
|
||||||
|
const handleAddCard = () => {
|
||||||
|
openModal((props) => <CreateCardModal {...props} onCreate={onCardCreate} />);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-gray-800 rounded-lg p-4 min-w-[300px] max-w-[300px] border border-gray-700">
|
<div className="bg-gray-800 rounded-lg p-4 min-w-[300px] max-w-[300px] border border-gray-700 flex flex-col">
|
||||||
<h2 className="text-white font-bold text-lg mb-4 flex items-center justify-between">
|
<h2 className="text-white font-bold text-lg mb-4 flex items-center justify-between">
|
||||||
{list.name}
|
{list.name}
|
||||||
<span className="bg-gray-600 text-gray-300 text-xs px-2 py-1 rounded-full">
|
<span className="bg-gray-600 text-gray-300 text-xs px-2 py-1 rounded-full">
|
||||||
|
|
@ -30,13 +39,34 @@ export function KanbanColumn({ list, cards, onCardClick }: KanbanColumnProps) {
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
ref={setNodeRef}
|
ref={setNodeRef}
|
||||||
className={`min-h-[200px] transition-colors ${isOver ? 'bg-gray-750' : ''}`}
|
className={`min-h-[200px] flex-1 transition-colors ${isOver ? 'bg-gray-750' : ''}`}
|
||||||
>
|
>
|
||||||
{cards.map((card) => (
|
{cards.map((card) => (
|
||||||
<KanbanCard key={card.id} card={card} onClick={() => onCardClick(card)} />
|
<KanbanCard key={card.id} card={card} onClick={() => onCardClick(card)} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</SortableContext>
|
</SortableContext>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={handleAddCard}
|
||||||
|
className="mt-3 w-full py-2 px-4 bg-gray-700 hover:bg-gray-600 text-gray-300 hover:text-white rounded-lg transition-colors flex items-center justify-center gap-2 text-sm font-medium"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
>
|
||||||
|
<line x1="12" y1="5" x2="12" y2="19"></line>
|
||||||
|
<line x1="5" y1="12" x2="19" y2="12"></line>
|
||||||
|
</svg>
|
||||||
|
Add Card
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
import { useApi } from './useApi';
|
import { useApi } from './useApi';
|
||||||
import { useLoader } from '../context/loaders/useLoader';
|
import { useLoader } from '../context/loaders/useLoader';
|
||||||
import { useToast } from '../context/toasts/useToast';
|
import { useToast } from '../context/toasts/useToast';
|
||||||
|
|
@ -12,7 +12,7 @@ export function useBoard(boardId: number) {
|
||||||
const { withLoader } = useLoader();
|
const { withLoader } = useLoader();
|
||||||
const { addNotification } = useToast();
|
const { addNotification } = useToast();
|
||||||
|
|
||||||
const fetchBoard = async () => {
|
const fetchBoard = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
setError(null);
|
setError(null);
|
||||||
const data = await withLoader(() => getBoard(boardId), 'Loading board...');
|
const data = await withLoader(() => getBoard(boardId), 'Loading board...');
|
||||||
|
|
@ -29,7 +29,7 @@ export function useBoard(boardId: number) {
|
||||||
});
|
});
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
};
|
}, [boardId, getBoard, withLoader, addNotification]);
|
||||||
|
|
||||||
const updateBoardData = (updatedBoard: BoardWithDetails) => {
|
const updateBoardData = (updatedBoard: BoardWithDetails) => {
|
||||||
setBoard(updatedBoard);
|
setBoard(updatedBoard);
|
||||||
|
|
@ -37,7 +37,7 @@ export function useBoard(boardId: number) {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchBoard();
|
fetchBoard();
|
||||||
}, [boardId]);
|
}, [boardId, fetchBoard]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
board,
|
board,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
import { useApi } from './useApi';
|
import { useApi } from './useApi';
|
||||||
import { useLoader } from '../context/loaders/useLoader';
|
import { useLoader } from '../context/loaders/useLoader';
|
||||||
import { useToast } from '../context/toasts/useToast';
|
import { useToast } from '../context/toasts/useToast';
|
||||||
|
|
@ -12,7 +12,7 @@ export function useBoards() {
|
||||||
const { withLoader } = useLoader();
|
const { withLoader } = useLoader();
|
||||||
const { addNotification } = useToast();
|
const { addNotification } = useToast();
|
||||||
|
|
||||||
const fetchBoards = async () => {
|
const fetchBoards = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
setError(null);
|
setError(null);
|
||||||
const data = await withLoader(() => getBoards(), 'Loading boards...');
|
const data = await withLoader(() => getBoards(), 'Loading boards...');
|
||||||
|
|
@ -29,7 +29,7 @@ export function useBoards() {
|
||||||
});
|
});
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
};
|
}, [getBoards, withLoader, addNotification]);
|
||||||
|
|
||||||
const createNewBoard = async (boardData: { name: string; description?: string }) => {
|
const createNewBoard = async (boardData: { name: string; description?: string }) => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -110,7 +110,7 @@ export function useBoards() {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchBoards();
|
fetchBoards();
|
||||||
}, []);
|
}, [fetchBoards]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
boards,
|
boards,
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import { useApi } from './useApi';
|
import { useApi } from './useApi';
|
||||||
import { useLoader } from '../context/loaders/useLoader';
|
import { useLoader } from '../context/loaders/useLoader';
|
||||||
import { useToast } from '../context/toasts/useToast';
|
import { useToast } from '../context/toasts/useToast';
|
||||||
import { BoardWithDetails, Card, List } from '../types/kanban';
|
import { Card } from '../types/kanban';
|
||||||
|
|
||||||
export function useCardMutations(boardId: number, onUpdate: (board: BoardWithDetails) => void) {
|
export function useCardMutations(boardId: number, onUpdate: () => void) {
|
||||||
const { createCard, updateCard, deleteCard } = useApi();
|
const { createCard, updateCard, deleteCard } = useApi();
|
||||||
const { withLoader } = useLoader();
|
const { withLoader } = useLoader();
|
||||||
const { addNotification } = useToast();
|
const { addNotification } = useToast();
|
||||||
|
|
@ -68,18 +68,20 @@ export function useCardMutations(boardId: number, onUpdate: (board: BoardWithDet
|
||||||
};
|
};
|
||||||
|
|
||||||
const moveCard = async (
|
const moveCard = async (
|
||||||
cardId: number,
|
card: Card,
|
||||||
fromListId: number,
|
fromListId: number,
|
||||||
toListId: number,
|
toListId: number,
|
||||||
newPosition: number
|
newPosition: number
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
// Optimistic update - don't show loader for drag operations
|
// Optimistic update - don't show loader for drag operations
|
||||||
await updateCard(cardId, {
|
await updateCard(card.id, {
|
||||||
name: '', // Placeholder, will be filled by server
|
name: card.name, // Placeholder, will be filled by server
|
||||||
pos: newPosition,
|
pos: newPosition,
|
||||||
list_id: toListId,
|
list_id: toListId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
onUpdate();
|
||||||
addNotification({
|
addNotification({
|
||||||
type: 'success',
|
type: 'success',
|
||||||
title: 'Card Moved',
|
title: 'Card Moved',
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
import { useApi } from './useApi';
|
import { useApi } from './useApi';
|
||||||
import { useLoader } from '../context/loaders/useLoader';
|
import { useLoader } from '../context/loaders/useLoader';
|
||||||
import { useToast } from '../context/toasts/useToast';
|
import { useToast } from '../context/toasts/useToast';
|
||||||
import { BoardWithDetails } from '../types/kanban';
|
|
||||||
|
|
||||||
export function useListMutations(boardId: number, onUpdate: (board: BoardWithDetails) => void) {
|
export function useListMutations(boardId: number, onUpdate: () => void) {
|
||||||
const { createList, updateList, deleteList } = useApi();
|
const { createList, updateList, deleteList } = useApi();
|
||||||
const { withLoader } = useLoader();
|
const { withLoader } = useLoader();
|
||||||
const { addNotification } = useToast();
|
const { addNotification } = useToast();
|
||||||
|
|
@ -11,6 +10,7 @@ export function useListMutations(boardId: number, onUpdate: (board: BoardWithDet
|
||||||
const createNewList = async (name: string, pos: number) => {
|
const createNewList = async (name: string, pos: number) => {
|
||||||
try {
|
try {
|
||||||
const data = await withLoader(() => createList(boardId, { name, pos }), 'Creating list...');
|
const data = await withLoader(() => createList(boardId, { name, pos }), 'Creating list...');
|
||||||
|
onUpdate();
|
||||||
addNotification({
|
addNotification({
|
||||||
type: 'success',
|
type: 'success',
|
||||||
title: 'List Created',
|
title: 'List Created',
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { useNavigate, Link } from 'react-router-dom';
|
import { useNavigate, Link } from 'react-router-dom';
|
||||||
import { useBoards } from '../hooks/useBoards';
|
import { useBoards } from '../hooks/useBoards';
|
||||||
|
import { useToast } from '@/context/toasts';
|
||||||
|
|
||||||
const boardSchema = z.object({
|
const boardSchema = z.object({
|
||||||
name: z
|
name: z
|
||||||
|
|
@ -17,6 +18,7 @@ type BoardFormData = z.infer<typeof boardSchema>;
|
||||||
export function BoardCreate() {
|
export function BoardCreate() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { createBoard } = useBoards();
|
const { createBoard } = useBoards();
|
||||||
|
const { addNotification } = useToast();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
|
|
@ -28,10 +30,22 @@ export function BoardCreate() {
|
||||||
|
|
||||||
const onSubmit = async (data: BoardFormData) => {
|
const onSubmit = async (data: BoardFormData) => {
|
||||||
try {
|
try {
|
||||||
await createBoard(data);
|
const newBoard = await createBoard(data);
|
||||||
navigate('/boards');
|
addNotification({
|
||||||
|
type: 'success',
|
||||||
|
title: 'Board Created',
|
||||||
|
message: `Board "${newBoard.name}" created successfully.`,
|
||||||
|
duration: 3000,
|
||||||
|
});
|
||||||
|
navigate(`/boards/${newBoard.id}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Error is handled by the hook
|
const errorMessage = err instanceof Error ? err.message : 'Failed to create board';
|
||||||
|
addNotification({
|
||||||
|
type: 'error',
|
||||||
|
title: 'Error Creating Board',
|
||||||
|
message: errorMessage,
|
||||||
|
duration: 5000,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@ import { useBoard } from '../hooks/useBoard';
|
||||||
import { useCardMutations } from '../hooks/useCardMutations';
|
import { useCardMutations } from '../hooks/useCardMutations';
|
||||||
import { useListMutations } from '../hooks/useListMutations';
|
import { useListMutations } from '../hooks/useListMutations';
|
||||||
import { KanbanColumn } from '../components/kanban/KanbanColumn';
|
import { KanbanColumn } from '../components/kanban/KanbanColumn';
|
||||||
|
import { CreateListModal } from '../components/kanban/CreateListModal';
|
||||||
|
import { useModal } from '../context/modals/useModal';
|
||||||
import {
|
import {
|
||||||
DndContext,
|
DndContext,
|
||||||
DragEndEvent,
|
DragEndEvent,
|
||||||
|
|
@ -14,15 +16,16 @@ import {
|
||||||
useSensors,
|
useSensors,
|
||||||
closestCenter,
|
closestCenter,
|
||||||
} from '@dnd-kit/core';
|
} from '@dnd-kit/core';
|
||||||
import { Card as CardType, BoardWithDetails } from '../types/kanban';
|
import { Card as CardType } from '../types/kanban';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
export function BoardDetail() {
|
export function BoardDetail() {
|
||||||
const { id } = useParams<{ id: string }>();
|
const { id } = useParams<{ id: string }>();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { board, fetchBoard } = useBoard(parseInt(id || '0'));
|
const { board, fetchBoard } = useBoard(parseInt(id || '0'));
|
||||||
const { moveCard } = useCardMutations(parseInt(id || '0'), fetchBoard);
|
const { createCard, moveCard } = useCardMutations(parseInt(id || '0'), fetchBoard);
|
||||||
const { createList } = useListMutations(parseInt(id || '0'), fetchBoard);
|
const { createList } = useListMutations(parseInt(id || '0'), fetchBoard);
|
||||||
|
const { openModal } = useModal();
|
||||||
|
|
||||||
const [activeCard, setActiveCard] = useState<CardType | null>(null);
|
const [activeCard, setActiveCard] = useState<CardType | null>(null);
|
||||||
|
|
||||||
|
|
@ -36,11 +39,16 @@ export function BoardDetail() {
|
||||||
|
|
||||||
const handleDragStart = (event: DragStartEvent) => {
|
const handleDragStart = (event: DragStartEvent) => {
|
||||||
const { active } = event;
|
const { active } = event;
|
||||||
const cardId = parseInt(active.id as string);
|
|
||||||
|
const activeIdStr = (active.id as string).split('_')[1];
|
||||||
|
|
||||||
|
const cardId = parseInt(activeIdStr as string);
|
||||||
|
|
||||||
if (board) {
|
if (board) {
|
||||||
const card = board.lists.flatMap((list) => list.cards).find((c) => c.id === cardId);
|
const card = board.lists.flatMap((list) => list.cards).find((c) => c.id === cardId);
|
||||||
|
|
||||||
|
// console.log('---handleDragStart', event, card)
|
||||||
|
// console.log('---handleDragStart.board', board)
|
||||||
if (card) {
|
if (card) {
|
||||||
setActiveCard(card);
|
setActiveCard(card);
|
||||||
}
|
}
|
||||||
|
|
@ -50,19 +58,24 @@ export function BoardDetail() {
|
||||||
const handleDragOver = (event: DragOverEvent) => {
|
const handleDragOver = (event: DragOverEvent) => {
|
||||||
const { active, over } = event;
|
const { active, over } = event;
|
||||||
|
|
||||||
|
console.log('---handleDragOver', event);
|
||||||
if (!over) return;
|
if (!over) return;
|
||||||
|
|
||||||
const activeId = parseInt(active.id as string);
|
// const activeId = parseInt(active.id as string);
|
||||||
const overId = parseInt(over.id as string);
|
// const overId = parseInt(over.id as string);
|
||||||
|
|
||||||
|
const overIdStr = (over.id as string).split('_')[1];
|
||||||
|
const overId = parseInt(overIdStr, 10);
|
||||||
|
const activeIdStr = (active.id as string).split('_')[1];
|
||||||
|
const activeId = parseInt(activeIdStr, 10);
|
||||||
if (activeId === overId) return;
|
if (activeId === overId) return;
|
||||||
|
|
||||||
// Find the active card and its current list
|
// Find active card and its current list
|
||||||
if (!board) return;
|
if (!board) return;
|
||||||
|
|
||||||
const activeList = board.lists.find((list) => list.cards.some((card) => card.id === activeId));
|
const activeList = board.lists.find((list) => list.cards.some((card) => card.id === activeId));
|
||||||
|
|
||||||
// If we're hovering over a card in the same list, do nothing
|
// If we're hovering over a card in same list, do nothing
|
||||||
if (activeList) {
|
if (activeList) {
|
||||||
const overCard = activeList.cards.find((card) => card.id === overId);
|
const overCard = activeList.cards.find((card) => card.id === overId);
|
||||||
if (overCard) return;
|
if (overCard) return;
|
||||||
|
|
@ -75,12 +88,17 @@ export function BoardDetail() {
|
||||||
|
|
||||||
if (!over || !board) return;
|
if (!over || !board) return;
|
||||||
|
|
||||||
const activeId = parseInt(active.id as string);
|
// console.log('--------------over', over)
|
||||||
const overId = parseInt(over.id as string);
|
// console.log('--------------board', board)
|
||||||
|
const [overType, overIdStr] = (over.id as string).split('_');
|
||||||
|
const overId = parseInt(overIdStr, 10);
|
||||||
|
const activeIdStr = (active.id as string).split('_')[1];
|
||||||
|
const activeId = parseInt(activeIdStr, 10);
|
||||||
|
// debugger
|
||||||
|
|
||||||
if (activeId === overId) return;
|
if (active.id === over.id) return;
|
||||||
|
|
||||||
// Find the active card
|
// Find active card
|
||||||
let activeCard: CardType | undefined;
|
let activeCard: CardType | undefined;
|
||||||
let activeList: (typeof board.lists)[0] | undefined;
|
let activeList: (typeof board.lists)[0] | undefined;
|
||||||
|
|
||||||
|
|
@ -96,13 +114,14 @@ export function BoardDetail() {
|
||||||
if (!activeCard || !activeList) return;
|
if (!activeCard || !activeList) return;
|
||||||
|
|
||||||
// Check if we're dropping on a list or a card
|
// Check if we're dropping on a list or a card
|
||||||
const overList = board.lists.find((list) => list.id === overId);
|
// debugger
|
||||||
|
if (overType.toLocaleLowerCase() === 'list') {
|
||||||
|
const overList = board.lists.find((list) => list.id === overId);
|
||||||
|
// Dropping on a list - append to end
|
||||||
|
|
||||||
if (overList) {
|
if (!overList || overList.id === activeList.id) return; // Same list, do nothing
|
||||||
// Dropping on a list - append to the end
|
|
||||||
if (overList.id === activeList.id) return; // Same list, do nothing
|
|
||||||
|
|
||||||
await moveCard(activeCard.id, activeList.id, overList.id, overList.cards.length);
|
await moveCard(activeCard, activeList.id, overList.id, overList.cards.length);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -124,7 +143,7 @@ export function BoardDetail() {
|
||||||
// Calculate new position
|
// Calculate new position
|
||||||
const overCardIndex = overListContainingCard.cards.findIndex((c) => c.id === overId);
|
const overCardIndex = overListContainingCard.cards.findIndex((c) => c.id === overId);
|
||||||
|
|
||||||
// If dropping on the same list and after the same card, do nothing
|
// If dropping on to same list and after of same card, do nothing
|
||||||
if (
|
if (
|
||||||
overListContainingCard.id === activeList.id &&
|
overListContainingCard.id === activeList.id &&
|
||||||
overCardIndex === activeList.cards.findIndex((c) => c.id === activeId) + 1
|
overCardIndex === activeList.cards.findIndex((c) => c.id === activeId) + 1
|
||||||
|
|
@ -132,24 +151,35 @@ export function BoardDetail() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await moveCard(activeCard.id, activeList.id, overListContainingCard.id, overCardIndex);
|
await moveCard(activeCard, activeList.id, overListContainingCard.id, overCardIndex);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCardClick = (card: CardType) => {
|
const handleCardClick = (card: CardType) => {
|
||||||
navigate(`/boards/${id}/cards/${card.id}`);
|
navigate(`/boards/${id}/cards/${card.id}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAddList = async () => {
|
const handleAddList = () => {
|
||||||
const listName = prompt('Enter list name:');
|
openModal((props) => (
|
||||||
if (listName && listName.trim()) {
|
<CreateListModal
|
||||||
try {
|
{...props}
|
||||||
const newList = await createList(listName.trim(), board ? board.lists.length : 0);
|
onCreate={async (name) => {
|
||||||
} catch (err) {
|
await createList(name, board ? board.lists.length : 0);
|
||||||
// Error handled by hook
|
fetchBoard(); // Refresh board data
|
||||||
}
|
}}
|
||||||
}
|
/>
|
||||||
|
));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleAddCard =
|
||||||
|
(listId: number) => async (data: { name: string; description?: string }) => {
|
||||||
|
await createCard(listId, {
|
||||||
|
name: data.name,
|
||||||
|
description: data.description,
|
||||||
|
pos: board ? board.lists.find((list) => list.id === listId)?.cards.length || 0 : 0,
|
||||||
|
});
|
||||||
|
fetchBoard(); // Refresh board data
|
||||||
|
};
|
||||||
|
|
||||||
if (!board) {
|
if (!board) {
|
||||||
return <div className="text-white">Loading...</div>;
|
return <div className="text-white">Loading...</div>;
|
||||||
}
|
}
|
||||||
|
|
@ -194,6 +224,7 @@ export function BoardDetail() {
|
||||||
list={list}
|
list={list}
|
||||||
cards={list.cards}
|
cards={list.cards}
|
||||||
onCardClick={handleCardClick}
|
onCardClick={handleCardClick}
|
||||||
|
onCardCreate={handleAddCard(list.id)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import { useNavigate, Link, useParams } from 'react-router-dom';
|
||||||
import { useBoard } from '../hooks/useBoard';
|
import { useBoard } from '../hooks/useBoard';
|
||||||
import { useBoards } from '../hooks/useBoards';
|
import { useBoards } from '../hooks/useBoards';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
|
import { useToast } from '@/context/toasts';
|
||||||
|
|
||||||
const boardSchema = z.object({
|
const boardSchema = z.object({
|
||||||
name: z
|
name: z
|
||||||
|
|
@ -22,6 +23,7 @@ export function BoardEdit() {
|
||||||
const { board } = useBoard(parseInt(id || '0'));
|
const { board } = useBoard(parseInt(id || '0'));
|
||||||
const { updateBoard } = useBoards();
|
const { updateBoard } = useBoards();
|
||||||
|
|
||||||
|
const { addNotification } = useToast();
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
|
|
@ -46,6 +48,13 @@ export function BoardEdit() {
|
||||||
navigate(`/boards/${id}`);
|
navigate(`/boards/${id}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Error is handled by the hook
|
// Error is handled by the hook
|
||||||
|
const errorMessage = err instanceof Error ? err.message : 'Failed to create card';
|
||||||
|
addNotification({
|
||||||
|
type: 'error',
|
||||||
|
title: 'Error Login',
|
||||||
|
message: errorMessage,
|
||||||
|
duration: 5000,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,28 @@
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { useAuth } from '../hooks/useAuth';
|
import { useAuth } from '../hooks/useAuth';
|
||||||
|
import { useToast } from '@/context/toasts';
|
||||||
|
|
||||||
export default function Login() {
|
export default function Login() {
|
||||||
const [email, setEmail] = useState('');
|
const [email, setEmail] = useState('');
|
||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState('');
|
||||||
const { login: handleLogin } = useAuth();
|
const { login: handleLogin } = useAuth();
|
||||||
|
|
||||||
|
const { addNotification } = useToast();
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
try {
|
try {
|
||||||
await handleLogin(email, password);
|
await handleLogin(email, password);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Error is handled by the hook (toast shown)
|
// Error is handled by the hook (toast shown)
|
||||||
|
const errorMessage = err instanceof Error ? err.message : 'Failed to create card';
|
||||||
|
addNotification({
|
||||||
|
type: 'error',
|
||||||
|
title: 'Error Login',
|
||||||
|
message: errorMessage,
|
||||||
|
duration: 5000,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -58,7 +68,7 @@ export default function Login() {
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<p className="mt-6 text-center text-gray-400">
|
<p className="mt-6 text-center text-gray-400">
|
||||||
Don't have an account?
|
Don't have an account?
|
||||||
<Link to="/register" className="ml-2 text-blue-400 hover:text-blue-300">
|
<Link to="/register" className="ml-2 text-blue-400 hover:text-blue-300">
|
||||||
Register
|
Register
|
||||||
</Link>
|
</Link>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { useAuth } from '../hooks/useAuth';
|
import { useAuth } from '../hooks/useAuth';
|
||||||
|
import { useToast } from '../context/toasts/useToast';
|
||||||
|
|
||||||
interface FormData {
|
interface FormData {
|
||||||
email: string;
|
email: string;
|
||||||
|
|
@ -22,6 +23,7 @@ export function Register() {
|
||||||
});
|
});
|
||||||
|
|
||||||
const { register: handleRegister } = useAuth();
|
const { register: handleRegister } = useAuth();
|
||||||
|
const { addNotification } = useToast();
|
||||||
|
|
||||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
setFormData({
|
setFormData({
|
||||||
|
|
@ -50,7 +52,13 @@ export function Register() {
|
||||||
last_name: formData.last_name,
|
last_name: formData.last_name,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Error is handled by the hook (toast shown)
|
const errorMessage = err instanceof Error ? err.message : 'Failed to register';
|
||||||
|
addNotification({
|
||||||
|
type: 'error',
|
||||||
|
title: 'Registration Error',
|
||||||
|
message: errorMessage,
|
||||||
|
duration: 5000,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue