add labels logic
This commit is contained in:
parent
d295a00e2f
commit
e69dd6a56d
6 changed files with 464 additions and 22 deletions
79
frontend/src/components/CardLabels.tsx
Normal file
79
frontend/src/components/CardLabels.tsx
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
import { useModal } from '../context/modals/useModal';
|
||||||
|
import { CreateLabelModal } from './CreateLabelModal';
|
||||||
|
import { LabelDropdown } from './LabelDropdown';
|
||||||
|
import TagIcon from './icons/TagIcon';
|
||||||
|
import { Label } from '../types/kanban';
|
||||||
|
|
||||||
|
interface CardLabelsProps {
|
||||||
|
labels: Label[];
|
||||||
|
cardLabels: Label[];
|
||||||
|
addLabel: (name: string, color: string) => Promise<boolean>;
|
||||||
|
addLabelToCard: (labelId: number) => Promise<boolean>;
|
||||||
|
removeLabelFromCard: (labelId: number) => Promise<boolean>;
|
||||||
|
refetchLabels: () => Promise<Label[]>;
|
||||||
|
refetchCard: () => Promise<any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CardLabels({
|
||||||
|
labels,
|
||||||
|
cardLabels,
|
||||||
|
addLabel,
|
||||||
|
addLabelToCard,
|
||||||
|
removeLabelFromCard,
|
||||||
|
refetchLabels,
|
||||||
|
refetchCard,
|
||||||
|
}: CardLabelsProps) {
|
||||||
|
const { openModal } = useModal();
|
||||||
|
|
||||||
|
const handleCreateLabel = () => {
|
||||||
|
openModal((props) => (
|
||||||
|
<CreateLabelModal
|
||||||
|
onSave={async (name, color) => {
|
||||||
|
const success = await addLabel(name, color);
|
||||||
|
if (success) {
|
||||||
|
// Refetch both labels and card data to show new labels
|
||||||
|
await refetchLabels();
|
||||||
|
await refetchCard();
|
||||||
|
}
|
||||||
|
return success;
|
||||||
|
}}
|
||||||
|
onClose={props.onClose}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-gray-800 rounded-lg p-6">
|
||||||
|
<div className="flex justify-between items-center mb-4">
|
||||||
|
<h2 className="text-xl font-bold text-white flex items-center gap-2">
|
||||||
|
<span className="w-5 h-5">
|
||||||
|
<TagIcon />
|
||||||
|
</span>
|
||||||
|
Labels
|
||||||
|
</h2>
|
||||||
|
<LabelDropdown
|
||||||
|
labels={labels}
|
||||||
|
cardLabels={cardLabels}
|
||||||
|
onAddLabel={addLabelToCard}
|
||||||
|
onRemoveLabel={removeLabelFromCard}
|
||||||
|
onCreateLabel={handleCreateLabel}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{cardLabels && cardLabels.length > 0 ? (
|
||||||
|
cardLabels.map((label) => (
|
||||||
|
<span
|
||||||
|
key={label.id}
|
||||||
|
className="px-3 py-1 rounded-full text-sm font-medium"
|
||||||
|
style={{ backgroundColor: label.color, color: 'white' }}
|
||||||
|
>
|
||||||
|
{label.name}
|
||||||
|
</span>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<p className="text-gray-400 text-sm">No labels added to this card</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
97
frontend/src/components/CreateLabelModal.tsx
Normal file
97
frontend/src/components/CreateLabelModal.tsx
Normal file
|
|
@ -0,0 +1,97 @@
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
interface CreateLabelModalProps {
|
||||||
|
onSave: (name: string, color: string) => Promise<boolean>;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const COLORS = [
|
||||||
|
{ name: 'Red', value: '#ef4444' },
|
||||||
|
{ name: 'Orange', value: '#f97316' },
|
||||||
|
{ name: 'Yellow', value: '#eab308' },
|
||||||
|
{ name: 'Green', value: '#22c55e' },
|
||||||
|
{ name: 'Blue', value: '#3b82f6' },
|
||||||
|
{ name: 'Purple', value: '#a855f7' },
|
||||||
|
{ name: 'Pink', value: '#ec4899' },
|
||||||
|
{ name: 'Gray', value: '#6b7280' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export function CreateLabelModal({ onSave, onClose }: CreateLabelModalProps) {
|
||||||
|
const [name, setName] = useState('');
|
||||||
|
const [color, setColor] = useState(COLORS[0].value);
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!name.trim()) return;
|
||||||
|
|
||||||
|
const success = await onSave(name, color);
|
||||||
|
if (success) {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-gray-800 rounded-lg p-6 max-w-md w-full">
|
||||||
|
<h3 className="text-xl font-bold text-white mb-4">Create Label</h3>
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-gray-300 text-sm font-medium mb-2">Label Name</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={name}
|
||||||
|
onChange={(e) => setName(e.target.value)}
|
||||||
|
className="w-full bg-gray-700 text-white rounded-lg p-3 border border-gray-600 focus:border-blue-500 focus:outline-none"
|
||||||
|
placeholder="Enter label name..."
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-gray-300 text-sm font-medium mb-2">Color</label>
|
||||||
|
<div className="grid grid-cols-4 gap-2">
|
||||||
|
{COLORS.map((colorOption) => (
|
||||||
|
<button
|
||||||
|
key={colorOption.value}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setColor(colorOption.value)}
|
||||||
|
className={`h-10 rounded-lg border-2 transition-all ${
|
||||||
|
color === colorOption.value
|
||||||
|
? 'border-white ring-2 ring-white/50'
|
||||||
|
: 'border-transparent hover:border-gray-500'
|
||||||
|
}`}
|
||||||
|
style={{ backgroundColor: colorOption.value }}
|
||||||
|
title={colorOption.name}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-gray-300 text-sm font-medium mb-2">Preview</label>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span
|
||||||
|
className="px-3 py-1 rounded-full text-sm font-medium text-white"
|
||||||
|
style={{ backgroundColor: color }}
|
||||||
|
>
|
||||||
|
{name || 'Label Preview'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2 pt-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onClose}
|
||||||
|
className="flex-1 bg-gray-700 hover:bg-gray-600 text-white font-medium py-2 px-4 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={!name.trim()}
|
||||||
|
className="flex-1 bg-blue-600 hover:bg-blue-700 disabled:bg-gray-600 disabled:cursor-not-allowed text-white font-medium py-2 px-4 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
Create
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
129
frontend/src/components/LabelDropdown.tsx
Normal file
129
frontend/src/components/LabelDropdown.tsx
Normal file
|
|
@ -0,0 +1,129 @@
|
||||||
|
import { useState, useRef, useEffect } from 'react';
|
||||||
|
import { Label } from '../types/kanban';
|
||||||
|
import PlusIcon from './icons/PlusIcon';
|
||||||
|
|
||||||
|
interface LabelDropdownProps {
|
||||||
|
labels: Label[];
|
||||||
|
cardLabels: Label[];
|
||||||
|
onAddLabel: (labelId: number) => Promise<boolean>;
|
||||||
|
onRemoveLabel: (labelId: number) => Promise<boolean>;
|
||||||
|
onCreateLabel: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LabelDropdown({
|
||||||
|
labels,
|
||||||
|
cardLabels,
|
||||||
|
onAddLabel,
|
||||||
|
onRemoveLabel,
|
||||||
|
onCreateLabel,
|
||||||
|
}: LabelDropdownProps) {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
|
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
||||||
|
setIsOpen(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isOpen) {
|
||||||
|
document.addEventListener('mousedown', handleClickOutside);
|
||||||
|
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||||
|
}
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
const cardLabelIds = new Set(cardLabels.map((l) => l.id));
|
||||||
|
const availableLabels = labels.filter((l) => !cardLabelIds.has(l.id));
|
||||||
|
|
||||||
|
const handleToggleLabel = async (labelId: number, isAdded: boolean) => {
|
||||||
|
let success = false;
|
||||||
|
if (isAdded) {
|
||||||
|
success = await onRemoveLabel(labelId);
|
||||||
|
} else {
|
||||||
|
success = await onAddLabel(labelId);
|
||||||
|
}
|
||||||
|
if (success) {
|
||||||
|
setIsOpen(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative" ref={dropdownRef}>
|
||||||
|
<button
|
||||||
|
onClick={() => setIsOpen(!isOpen)}
|
||||||
|
className="flex items-center gap-2 bg-gray-700 hover:bg-gray-600 text-gray-300 hover:text-white px-3 py-1.5 rounded-lg transition-colors text-sm"
|
||||||
|
>
|
||||||
|
<span className="w-4 h-4">
|
||||||
|
<PlusIcon />
|
||||||
|
</span>
|
||||||
|
Add Label
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{isOpen && (
|
||||||
|
<div className="absolute top-full left-0 mt-2 w-72 bg-gray-800 rounded-lg shadow-xl border border-gray-700 z-50">
|
||||||
|
<div className="p-4">
|
||||||
|
<h4 className="text-white font-medium mb-3">Labels</h4>
|
||||||
|
|
||||||
|
{/* Add new label button */}
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
onCreateLabel();
|
||||||
|
setIsOpen(false);
|
||||||
|
}}
|
||||||
|
className="w-full mb-3 flex items-center gap-2 bg-blue-600 hover:bg-blue-700 text-white px-3 py-2 rounded-lg transition-colors text-sm"
|
||||||
|
>
|
||||||
|
<span className="w-4 h-4">
|
||||||
|
<PlusIcon />
|
||||||
|
</span>
|
||||||
|
Create new label
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Available labels */}
|
||||||
|
{availableLabels.length > 0 ? (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p className="text-gray-400 text-xs uppercase tracking-wide">Available Labels</p>
|
||||||
|
{availableLabels.map((label) => (
|
||||||
|
<button
|
||||||
|
key={label.id}
|
||||||
|
onClick={() => handleToggleLabel(label.id, false)}
|
||||||
|
className="w-full flex items-center gap-2 px-3 py-2 rounded-lg hover:bg-gray-700 transition-colors text-left"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="w-6 h-6 rounded-full"
|
||||||
|
style={{ backgroundColor: label.color }}
|
||||||
|
/>
|
||||||
|
<span className="text-gray-200 text-sm">{label.name}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className="text-gray-400 text-sm">No labels available</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Current labels (can be removed) */}
|
||||||
|
{cardLabels.length > 0 && (
|
||||||
|
<div className="mt-4 pt-4 border-t border-gray-700 space-y-2">
|
||||||
|
<p className="text-gray-400 text-xs uppercase tracking-wide">Current Labels</p>
|
||||||
|
{cardLabels.map((label) => (
|
||||||
|
<button
|
||||||
|
key={label.id}
|
||||||
|
onClick={() => handleToggleLabel(label.id, true)}
|
||||||
|
className="w-full flex items-center gap-2 px-3 py-2 rounded-lg hover:bg-gray-700 transition-colors text-left"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="w-6 h-6 rounded-full border-2 border-white"
|
||||||
|
style={{ backgroundColor: label.color }}
|
||||||
|
/>
|
||||||
|
<span className="text-gray-200 text-sm">{label.name}</span>
|
||||||
|
<span className="ml-auto text-xs text-gray-400">Remove</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
93
frontend/src/hooks/useLabelMutations.ts
Normal file
93
frontend/src/hooks/useLabelMutations.ts
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
import { useApi } from './useApi';
|
||||||
|
import { useLoader } from '../context/loaders/useLoader';
|
||||||
|
import { useToast } from '../context/toasts/useToast';
|
||||||
|
|
||||||
|
export function useLabelMutations(boardId: number, onUpdate: () => void) {
|
||||||
|
const { createLabel } = useApi();
|
||||||
|
const { withLoader } = useLoader();
|
||||||
|
const { addNotification } = useToast();
|
||||||
|
|
||||||
|
const addLabel = async (name: string, color: string) => {
|
||||||
|
try {
|
||||||
|
await withLoader(() => createLabel(boardId, { name, color }), 'Creating label...');
|
||||||
|
onUpdate();
|
||||||
|
addNotification({
|
||||||
|
type: 'success',
|
||||||
|
title: 'Label Created',
|
||||||
|
message: 'Label has been created successfully.',
|
||||||
|
duration: 3000,
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
const errorMessage = err instanceof Error ? err.message : 'Failed to create label';
|
||||||
|
addNotification({
|
||||||
|
type: 'error',
|
||||||
|
title: 'Error',
|
||||||
|
message: errorMessage,
|
||||||
|
duration: 5000,
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
addLabel,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useCardLabelMutations(cardId: number, onUpdate: () => void) {
|
||||||
|
const { addLabelToCard, removeLabelFromCard } = useApi();
|
||||||
|
const { withLoader } = useLoader();
|
||||||
|
const { addNotification } = useToast();
|
||||||
|
|
||||||
|
const addLabelToCardMutation = async (labelId: number) => {
|
||||||
|
try {
|
||||||
|
await withLoader(() => addLabelToCard(cardId, labelId), 'Adding label...');
|
||||||
|
onUpdate();
|
||||||
|
addNotification({
|
||||||
|
type: 'success',
|
||||||
|
title: 'Label Added',
|
||||||
|
message: 'Label has been added to the card.',
|
||||||
|
duration: 3000,
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
const errorMessage = err instanceof Error ? err.message : 'Failed to add label';
|
||||||
|
addNotification({
|
||||||
|
type: 'error',
|
||||||
|
title: 'Error',
|
||||||
|
message: errorMessage,
|
||||||
|
duration: 5000,
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeLabelFromCardMutation = async (labelId: number) => {
|
||||||
|
try {
|
||||||
|
await withLoader(() => removeLabelFromCard(cardId, labelId), 'Removing label...');
|
||||||
|
onUpdate();
|
||||||
|
addNotification({
|
||||||
|
type: 'success',
|
||||||
|
title: 'Label Removed',
|
||||||
|
message: 'Label has been removed from the card.',
|
||||||
|
duration: 3000,
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
const errorMessage = err instanceof Error ? err.message : 'Failed to remove label';
|
||||||
|
addNotification({
|
||||||
|
type: 'error',
|
||||||
|
title: 'Error',
|
||||||
|
message: errorMessage,
|
||||||
|
duration: 5000,
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
addLabelToCardMutation,
|
||||||
|
removeLabelFromCardMutation,
|
||||||
|
};
|
||||||
|
}
|
||||||
46
frontend/src/hooks/useLabels.ts
Normal file
46
frontend/src/hooks/useLabels.ts
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
|
import { useApi } from './useApi';
|
||||||
|
import { useLoader } from '../context/loaders/useLoader';
|
||||||
|
import { useToast } from '../context/toasts/useToast';
|
||||||
|
import { Label } from '../types/kanban';
|
||||||
|
|
||||||
|
export function useLabels(boardId: number) {
|
||||||
|
const [labels, setLabels] = useState<Label[]>([]);
|
||||||
|
const [error, setError] = useState<Error | null>(null);
|
||||||
|
|
||||||
|
const { getLabels } = useApi();
|
||||||
|
const { withLoader } = useLoader();
|
||||||
|
const { addNotification } = useToast();
|
||||||
|
|
||||||
|
const fetchLabels = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
setError(null);
|
||||||
|
const data = await withLoader(() => getLabels(boardId), 'Loading labels...');
|
||||||
|
setLabels(data);
|
||||||
|
return data;
|
||||||
|
} catch (err) {
|
||||||
|
const errorMessage = err instanceof Error ? err.message : 'Failed to load labels';
|
||||||
|
setError(err instanceof Error ? err : new Error(errorMessage));
|
||||||
|
addNotification({
|
||||||
|
type: 'error',
|
||||||
|
title: 'Error Loading Labels',
|
||||||
|
message: errorMessage,
|
||||||
|
duration: 5000,
|
||||||
|
});
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}, [boardId, getLabels, withLoader, addNotification]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (boardId) {
|
||||||
|
fetchLabels();
|
||||||
|
}
|
||||||
|
}, [boardId, fetchLabels]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
labels,
|
||||||
|
error, // For debugging, not for UI display
|
||||||
|
loading: false, // Loading is handled by global loader
|
||||||
|
refetch: fetchLabels,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -2,13 +2,15 @@ import { useParams, Link, useNavigate } from 'react-router-dom';
|
||||||
import { useCard } from '../hooks/useCard';
|
import { useCard } from '../hooks/useCard';
|
||||||
import { useCardDetailMutations } from '../hooks/useCardDetailMutations';
|
import { useCardDetailMutations } from '../hooks/useCardDetailMutations';
|
||||||
import { useChecklistMutations } from '../hooks/useChecklistMutations';
|
import { useChecklistMutations } from '../hooks/useChecklistMutations';
|
||||||
|
import { useLabels } from '../hooks/useLabels';
|
||||||
|
import { useLabelMutations, useCardLabelMutations } from '../hooks/useLabelMutations';
|
||||||
import { useModal } from '../context/modals/useModal';
|
import { useModal } from '../context/modals/useModal';
|
||||||
import { CardSidebar } from '../components/CardSidebar';
|
import { CardSidebar } from '../components/CardSidebar';
|
||||||
import { CardComments } from '../components/CardComments';
|
import { CardComments } from '../components/CardComments';
|
||||||
import { CardChecklists } from '../components/CardChecklists';
|
import { CardChecklists } from '../components/CardChecklists';
|
||||||
|
import { CardLabels } from '../components/CardLabels';
|
||||||
import { EditCardModal } from '../components/EditCardModal';
|
import { EditCardModal } from '../components/EditCardModal';
|
||||||
import { DeleteCardModal } from '../components/DeleteCardModal';
|
import { DeleteCardModal } from '../components/DeleteCardModal';
|
||||||
import TagIcon from '../components/icons/TagIcon';
|
|
||||||
import Trash2Icon from '../components/icons/Trash2Icon';
|
import Trash2Icon from '../components/icons/Trash2Icon';
|
||||||
import ArrowLeftIcon from '../components/icons/ArrowLeftIcon';
|
import ArrowLeftIcon from '../components/icons/ArrowLeftIcon';
|
||||||
import Edit2Icon from '../components/icons/Edit2Icon';
|
import Edit2Icon from '../components/icons/Edit2Icon';
|
||||||
|
|
@ -28,6 +30,14 @@ export function CardDetail() {
|
||||||
const { openModal } = useModal();
|
const { openModal } = useModal();
|
||||||
const checklistMutations = useChecklistMutations(parseInt(cardId || '0'), fetchCard);
|
const checklistMutations = useChecklistMutations(parseInt(cardId || '0'), fetchCard);
|
||||||
|
|
||||||
|
// Labels functionality
|
||||||
|
const { labels, refetch: refetchLabels } = useLabels(parseInt(boardId || '0'));
|
||||||
|
const { addLabel } = useLabelMutations(parseInt(boardId || '0'), refetchLabels);
|
||||||
|
const { addLabelToCardMutation, removeLabelFromCardMutation } = useCardLabelMutations(
|
||||||
|
parseInt(cardId || '0'),
|
||||||
|
fetchCard
|
||||||
|
);
|
||||||
|
|
||||||
const handleEditCard = () => {
|
const handleEditCard = () => {
|
||||||
if (!card) return;
|
if (!card) return;
|
||||||
|
|
||||||
|
|
@ -132,27 +142,15 @@ export function CardDetail() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Labels Section */}
|
{/* Labels Section */}
|
||||||
{card.labels && card.labels.length > 0 && (
|
<CardLabels
|
||||||
<div className="bg-gray-800 rounded-lg p-6">
|
labels={labels}
|
||||||
<h2 className="text-xl font-bold text-white flex items-center gap-2 mb-4">
|
cardLabels={card.labels || []}
|
||||||
<span className="w-5 h-5">
|
addLabel={addLabel}
|
||||||
<TagIcon />
|
addLabelToCard={addLabelToCardMutation}
|
||||||
</span>
|
removeLabelFromCard={removeLabelFromCardMutation}
|
||||||
Labels
|
refetchLabels={refetchLabels}
|
||||||
</h2>
|
refetchCard={fetchCard}
|
||||||
<div className="flex flex-wrap gap-2">
|
/>
|
||||||
{card.labels.map((label: any) => (
|
|
||||||
<span
|
|
||||||
key={label.id}
|
|
||||||
className="px-3 py-1 rounded-full text-sm font-medium"
|
|
||||||
style={{ backgroundColor: label.color, color: 'white' }}
|
|
||||||
>
|
|
||||||
{label.name}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Checklists Section */}
|
{/* Checklists Section */}
|
||||||
<CardChecklists
|
<CardChecklists
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue