feat: TFS Sync
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { useState, useEffect, useRef, useTransition } from 'react';
|
||||
import { Task } from '@/lib/types';
|
||||
import { TfsConfig } from '@/services/tfs';
|
||||
import { formatDistanceToNow } from 'date-fns';
|
||||
import { fr } from 'date-fns/locale';
|
||||
import { Card } from '@/components/ui/Card';
|
||||
@@ -24,7 +25,7 @@ export function TaskCard({ task, onEdit, compactView = false }: TaskCardProps) {
|
||||
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const { tags: availableTags, refreshTasks } = useTasksContext();
|
||||
const { preferences } = useUserPreferences();
|
||||
|
||||
|
||||
// Classes CSS pour les différentes tailles de police
|
||||
const getFontSizeClasses = () => {
|
||||
switch (preferences.viewPreferences.fontSize) {
|
||||
@@ -32,23 +33,23 @@ export function TaskCard({ task, onEdit, compactView = false }: TaskCardProps) {
|
||||
return {
|
||||
title: 'text-xs',
|
||||
description: 'text-xs',
|
||||
meta: 'text-xs'
|
||||
meta: 'text-xs',
|
||||
};
|
||||
case 'large':
|
||||
return {
|
||||
title: 'text-base',
|
||||
description: 'text-sm',
|
||||
meta: 'text-sm'
|
||||
meta: 'text-sm',
|
||||
};
|
||||
default: // medium
|
||||
return {
|
||||
title: 'text-sm',
|
||||
description: 'text-xs',
|
||||
meta: 'text-xs'
|
||||
meta: 'text-xs',
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const fontClasses = getFontSizeClasses();
|
||||
|
||||
// Helper pour construire l'URL Jira
|
||||
@@ -58,16 +59,24 @@ export function TaskCard({ task, onEdit, compactView = false }: TaskCardProps) {
|
||||
return `${baseUrl}/browse/${jiraKey}`;
|
||||
};
|
||||
|
||||
// Helper pour construire l'URL TFS Pull Request
|
||||
const getTfsPullRequestUrl = (
|
||||
tfsPullRequestId: number,
|
||||
tfsProject: string,
|
||||
tfsRepository: string
|
||||
): string => {
|
||||
const tfsConfig = preferences.tfsConfig as TfsConfig;
|
||||
const baseUrl = tfsConfig?.organizationUrl;
|
||||
if (!baseUrl || !tfsPullRequestId || !tfsProject || !tfsRepository)
|
||||
return '';
|
||||
return `${baseUrl}/${encodeURIComponent(tfsProject)}/_git/${tfsRepository}/pullrequest/${tfsPullRequestId}`;
|
||||
};
|
||||
|
||||
// Configuration du draggable
|
||||
const {
|
||||
attributes,
|
||||
listeners,
|
||||
setNodeRef,
|
||||
transform,
|
||||
isDragging,
|
||||
} = useDraggable({
|
||||
id: task.id,
|
||||
});
|
||||
const { attributes, listeners, setNodeRef, transform, isDragging } =
|
||||
useDraggable({
|
||||
id: task.id,
|
||||
});
|
||||
|
||||
// Mettre à jour le titre local quand la tâche change
|
||||
useEffect(() => {
|
||||
@@ -76,9 +85,10 @@ export function TaskCard({ task, onEdit, compactView = false }: TaskCardProps) {
|
||||
|
||||
// Nettoyer le timeout au démontage
|
||||
useEffect(() => {
|
||||
const currentTimeout = timeoutRef.current;
|
||||
return () => {
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
if (currentTimeout) {
|
||||
clearTimeout(currentTimeout);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
@@ -86,7 +96,7 @@ export function TaskCard({ task, onEdit, compactView = false }: TaskCardProps) {
|
||||
const handleDelete = async (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
|
||||
if (window.confirm('Êtes-vous sûr de vouloir supprimer cette tâche ?')) {
|
||||
startTransition(async () => {
|
||||
const result = await deleteTask(task.id);
|
||||
@@ -151,35 +161,36 @@ export function TaskCard({ task, onEdit, compactView = false }: TaskCardProps) {
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Style de transformation pour le drag
|
||||
const style = transform ? {
|
||||
transform: `translate3d(${transform.x}px, ${transform.y}px, 0)`,
|
||||
} : undefined;
|
||||
const style = transform
|
||||
? {
|
||||
transform: `translate3d(${transform.x}px, ${transform.y}px, 0)`,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
// Extraire les emojis du titre pour les afficher comme tags visuels
|
||||
const emojiRegex = /(?:[\u{1F600}-\u{1F64F}]|[\u{1F300}-\u{1F5FF}]|[\u{1F680}-\u{1F6FF}]|[\u{1F900}-\u{1F9FF}]|[\u{1F1E0}-\u{1F1FF}]|[\u{2600}-\u{26FF}]|[\u{2700}-\u{27BF}])(?:[\u{200D}][\u{1F600}-\u{1F64F}]|[\u{1F300}-\u{1F5FF}]|[\u{1F680}-\u{1F6FF}]|[\u{1F900}-\u{1F9FF}]|[\u{1F1E0}-\u{1F1FF}]|[\u{2600}-\u{26FF}]|[\u{2700}-\u{27BF}]|[\u{FE0F}])*/gu;
|
||||
// Extraire les emojis du titre pour les afficher comme tags visuels
|
||||
const emojiRegex =
|
||||
/(?:[\u{1F600}-\u{1F64F}]|[\u{1F300}-\u{1F5FF}]|[\u{1F680}-\u{1F6FF}]|[\u{1F900}-\u{1F9FF}]|[\u{1F1E0}-\u{1F1FF}]|[\u{2600}-\u{26FF}]|[\u{2700}-\u{27BF}])(?:[\u{200D}][\u{1F600}-\u{1F64F}]|[\u{1F300}-\u{1F5FF}]|[\u{1F680}-\u{1F6FF}]|[\u{1F900}-\u{1F9FF}]|[\u{1F1E0}-\u{1F1FF}]|[\u{2600}-\u{26FF}]|[\u{2700}-\u{27BF}]|[\u{FE0F}])*/gu;
|
||||
const titleEmojis = task.title.match(emojiRegex) || [];
|
||||
const titleWithoutEmojis = task.title.replace(emojiRegex, '').trim();
|
||||
|
||||
// Composant titre avec tooltip
|
||||
const TitleWithTooltip = () => (
|
||||
<div className="relative flex-1">
|
||||
<h4
|
||||
<h4
|
||||
className={`font-mono ${fontClasses.title} font-medium text-[var(--foreground)] leading-tight line-clamp-2 cursor-pointer hover:text-[var(--primary)] transition-colors`}
|
||||
onClick={handleTitleClick}
|
||||
title="Cliquer pour éditer"
|
||||
>
|
||||
{titleWithoutEmojis}
|
||||
</h4>
|
||||
|
||||
</div>
|
||||
);
|
||||
|
||||
|
||||
// Si pas d'emoji dans le titre, utiliser l'emoji du premier tag
|
||||
let displayEmojis: string[] = titleEmojis;
|
||||
if (displayEmojis.length === 0 && task.tags && task.tags.length > 0) {
|
||||
const firstTag = availableTags.find(tag => tag.name === task.tags[0]);
|
||||
const firstTag = availableTags.find((tag) => tag.name === task.tags[0]);
|
||||
if (firstTag) {
|
||||
const tagEmojis = firstTag.name.match(emojiRegex);
|
||||
if (tagEmojis && tagEmojis.length > 0) {
|
||||
@@ -188,30 +199,44 @@ export function TaskCard({ task, onEdit, compactView = false }: TaskCardProps) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Styles spéciaux pour les tâches Jira
|
||||
const isJiraTask = task.source === 'jira';
|
||||
const jiraStyles = isJiraTask ? {
|
||||
border: '1px solid rgba(0, 130, 201, 0.3)',
|
||||
borderLeft: '3px solid #0082C9',
|
||||
background: 'linear-gradient(135deg, rgba(0, 130, 201, 0.05) 0%, rgba(0, 130, 201, 0.02) 100%)'
|
||||
} : {};
|
||||
const jiraStyles = isJiraTask
|
||||
? {
|
||||
border: '1px solid rgba(0, 130, 201, 0.3)',
|
||||
borderLeft: '3px solid #0082C9',
|
||||
background:
|
||||
'linear-gradient(135deg, rgba(0, 130, 201, 0.05) 0%, rgba(0, 130, 201, 0.02) 100%)',
|
||||
}
|
||||
: {};
|
||||
|
||||
// Styles spéciaux pour les tâches TFS
|
||||
const isTfsTask = task.source === 'tfs';
|
||||
const tfsStyles = isTfsTask
|
||||
? {
|
||||
border: '1px solid rgba(255, 165, 0, 0.3)',
|
||||
borderLeft: '3px solid #FFA500',
|
||||
background:
|
||||
'linear-gradient(135deg, rgba(255, 165, 0, 0.05) 0%, rgba(255, 165, 0, 0.02) 100%)',
|
||||
}
|
||||
: {};
|
||||
|
||||
// Combiner les styles spéciaux
|
||||
const specialStyles = { ...jiraStyles, ...tfsStyles };
|
||||
|
||||
// Vue compacte : seulement le titre
|
||||
if (compactView) {
|
||||
return (
|
||||
<Card
|
||||
<Card
|
||||
ref={setNodeRef}
|
||||
style={{ ...style, ...jiraStyles }}
|
||||
style={{ ...style, ...specialStyles }}
|
||||
className={`p-2 hover:border-[var(--primary)]/30 hover:shadow-lg hover:shadow-[var(--primary)]/10 transition-all duration-300 cursor-pointer group ${
|
||||
isDragging ? 'opacity-50 rotate-3 scale-105' : ''
|
||||
} ${
|
||||
task.status === 'done' ? 'opacity-60' : ''
|
||||
} ${
|
||||
} ${task.status === 'done' ? 'opacity-60' : ''} ${
|
||||
isJiraTask ? 'jira-task' : ''
|
||||
} ${
|
||||
isPending ? 'opacity-70 pointer-events-none' : ''
|
||||
}`}
|
||||
isTfsTask ? 'tfs-task' : ''
|
||||
} ${isPending ? 'opacity-70 pointer-events-none' : ''}`}
|
||||
{...attributes}
|
||||
{...(isEditingTitle ? {} : listeners)}
|
||||
>
|
||||
@@ -219,12 +244,13 @@ export function TaskCard({ task, onEdit, compactView = false }: TaskCardProps) {
|
||||
{displayEmojis.length > 0 && (
|
||||
<div className="flex gap-1 flex-shrink-0">
|
||||
{displayEmojis.slice(0, 1).map((emoji, index) => (
|
||||
<span
|
||||
key={index}
|
||||
<span
|
||||
key={index}
|
||||
className="text-base opacity-90 font-emoji"
|
||||
style={{
|
||||
fontFamily: 'Apple Color Emoji, Segoe UI Emoji, Noto Color Emoji, sans-serif',
|
||||
fontVariantEmoji: 'normal'
|
||||
style={{
|
||||
fontFamily:
|
||||
'Apple Color Emoji, Segoe UI Emoji, Noto Color Emoji, sans-serif',
|
||||
fontVariantEmoji: 'normal',
|
||||
}}
|
||||
>
|
||||
{emoji}
|
||||
@@ -232,7 +258,7 @@ export function TaskCard({ task, onEdit, compactView = false }: TaskCardProps) {
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
{isEditingTitle ? (
|
||||
<input
|
||||
type="text"
|
||||
@@ -246,7 +272,7 @@ export function TaskCard({ task, onEdit, compactView = false }: TaskCardProps) {
|
||||
) : (
|
||||
<TitleWithTooltip />
|
||||
)}
|
||||
|
||||
|
||||
<div className="flex items-center gap-1 flex-shrink-0">
|
||||
{/* Boutons d'action compacts - masqués en mode édition */}
|
||||
{!isEditingTitle && onEdit && (
|
||||
@@ -259,7 +285,7 @@ export function TaskCard({ task, onEdit, compactView = false }: TaskCardProps) {
|
||||
✎
|
||||
</button>
|
||||
)}
|
||||
|
||||
|
||||
{!isEditingTitle && (
|
||||
<button
|
||||
onClick={handleDelete}
|
||||
@@ -270,11 +296,15 @@ export function TaskCard({ task, onEdit, compactView = false }: TaskCardProps) {
|
||||
×
|
||||
</button>
|
||||
)}
|
||||
|
||||
|
||||
{/* Indicateur de priorité compact */}
|
||||
<div
|
||||
className="w-1.5 h-1.5 rounded-full"
|
||||
style={{ backgroundColor: getPriorityColorHex(getPriorityConfig(task.priority).color) }}
|
||||
<div
|
||||
className="w-1.5 h-1.5 rounded-full"
|
||||
style={{
|
||||
backgroundColor: getPriorityColorHex(
|
||||
getPriorityConfig(task.priority).color
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -284,18 +314,16 @@ export function TaskCard({ task, onEdit, compactView = false }: TaskCardProps) {
|
||||
|
||||
// Vue détaillée : version complète
|
||||
return (
|
||||
<Card
|
||||
<Card
|
||||
ref={setNodeRef}
|
||||
style={{ ...style, ...jiraStyles }}
|
||||
className={`p-3 hover:border-[var(--primary)]/30 hover:shadow-lg hover:shadow-[var(--primary)]/10 transition-all duration-300 cursor-pointer group ${
|
||||
style={{ ...style, ...specialStyles }}
|
||||
className={`p-3 hover:border-[var(--primary)]/30 hover:shadow-lg hover:shadow-[var(--primary)]/10 transition-all duration-300 cursor-pointer group ${
|
||||
isDragging ? 'opacity-50 rotate-3 scale-105' : ''
|
||||
} ${
|
||||
task.status === 'done' ? 'opacity-60' : ''
|
||||
} ${
|
||||
} ${task.status === 'done' ? 'opacity-60' : ''} ${
|
||||
isJiraTask ? 'jira-task' : ''
|
||||
} ${
|
||||
isPending ? 'opacity-70 pointer-events-none' : ''
|
||||
}`}
|
||||
isTfsTask ? 'tfs-task' : ''
|
||||
} ${isPending ? 'opacity-70 pointer-events-none' : ''}`}
|
||||
{...attributes}
|
||||
{...(isEditingTitle ? {} : listeners)}
|
||||
>
|
||||
@@ -304,12 +332,13 @@ export function TaskCard({ task, onEdit, compactView = false }: TaskCardProps) {
|
||||
{displayEmojis.length > 0 && (
|
||||
<div className="flex gap-1 flex-shrink-0">
|
||||
{displayEmojis.slice(0, 2).map((emoji, index) => (
|
||||
<span
|
||||
key={index}
|
||||
<span
|
||||
key={index}
|
||||
className="text-sm opacity-80 font-emoji"
|
||||
style={{
|
||||
fontFamily: 'Apple Color Emoji, Segoe UI Emoji, Noto Color Emoji, sans-serif',
|
||||
fontVariantEmoji: 'normal'
|
||||
style={{
|
||||
fontFamily:
|
||||
'Apple Color Emoji, Segoe UI Emoji, Noto Color Emoji, sans-serif',
|
||||
fontVariantEmoji: 'normal',
|
||||
}}
|
||||
>
|
||||
{emoji}
|
||||
@@ -317,7 +346,7 @@ export function TaskCard({ task, onEdit, compactView = false }: TaskCardProps) {
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
{isEditingTitle ? (
|
||||
<input
|
||||
type="text"
|
||||
@@ -331,7 +360,7 @@ export function TaskCard({ task, onEdit, compactView = false }: TaskCardProps) {
|
||||
) : (
|
||||
<TitleWithTooltip />
|
||||
)}
|
||||
|
||||
|
||||
<div className="flex items-center gap-1 flex-shrink-0">
|
||||
{/* Bouton d'édition discret - masqué en mode édition */}
|
||||
{!isEditingTitle && onEdit && (
|
||||
@@ -344,7 +373,7 @@ export function TaskCard({ task, onEdit, compactView = false }: TaskCardProps) {
|
||||
✎
|
||||
</button>
|
||||
)}
|
||||
|
||||
|
||||
{/* Bouton de suppression discret - masqué en mode édition */}
|
||||
{!isEditingTitle && (
|
||||
<button
|
||||
@@ -356,13 +385,15 @@ export function TaskCard({ task, onEdit, compactView = false }: TaskCardProps) {
|
||||
×
|
||||
</button>
|
||||
)}
|
||||
|
||||
|
||||
{/* Indicateur de priorité tech */}
|
||||
<div
|
||||
<div
|
||||
className="w-2 h-2 rounded-full animate-pulse shadow-sm"
|
||||
style={{
|
||||
backgroundColor: getPriorityColorHex(getPriorityConfig(task.priority).color),
|
||||
boxShadow: `0 0 4px ${getPriorityColorHex(getPriorityConfig(task.priority).color)}50`
|
||||
style={{
|
||||
backgroundColor: getPriorityColorHex(
|
||||
getPriorityConfig(task.priority).color
|
||||
),
|
||||
boxShadow: `0 0 4px ${getPriorityColorHex(getPriorityConfig(task.priority).color)}50`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -370,18 +401,24 @@ export function TaskCard({ task, onEdit, compactView = false }: TaskCardProps) {
|
||||
|
||||
{/* Description tech */}
|
||||
{task.description && (
|
||||
<p className={`${fontClasses.description} text-[var(--muted-foreground)] mb-3 line-clamp-1 font-mono`}>
|
||||
<p
|
||||
className={`${fontClasses.description} text-[var(--muted-foreground)] mb-3 line-clamp-1 font-mono`}
|
||||
>
|
||||
{task.description}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Tags avec couleurs */}
|
||||
{task.tags && task.tags.length > 0 && (
|
||||
<div className={
|
||||
(task.dueDate || (task.source && task.source !== 'manual') || task.completedAt)
|
||||
? "mb-3"
|
||||
: "mb-0"
|
||||
}>
|
||||
<div
|
||||
className={
|
||||
task.dueDate ||
|
||||
(task.source && task.source !== 'manual') ||
|
||||
task.completedAt
|
||||
? 'mb-3'
|
||||
: 'mb-0'
|
||||
}
|
||||
>
|
||||
<TagDisplay
|
||||
tags={task.tags}
|
||||
availableTags={availableTags}
|
||||
@@ -393,24 +430,29 @@ export function TaskCard({ task, onEdit, compactView = false }: TaskCardProps) {
|
||||
)}
|
||||
|
||||
{/* Footer tech avec séparateur néon - seulement si des données à afficher */}
|
||||
{(task.dueDate || (task.source && task.source !== 'manual') || task.completedAt) && (
|
||||
{(task.dueDate ||
|
||||
(task.source && task.source !== 'manual') ||
|
||||
task.completedAt) && (
|
||||
<div className="pt-2 border-t border-[var(--border)]/50">
|
||||
<div className={`flex items-center justify-between ${fontClasses.meta}`}>
|
||||
<div
|
||||
className={`flex items-center justify-between ${fontClasses.meta}`}
|
||||
>
|
||||
{task.dueDate ? (
|
||||
<span className="flex items-center gap-1 text-[var(--muted-foreground)] font-mono">
|
||||
<span className="text-[var(--primary)]">⏰</span>
|
||||
{formatDistanceToNow(task.dueDate, {
|
||||
addSuffix: true,
|
||||
locale: fr
|
||||
{formatDistanceToNow(task.dueDate, {
|
||||
addSuffix: true,
|
||||
locale: fr,
|
||||
})}
|
||||
</span>
|
||||
) : (
|
||||
<div></div>
|
||||
)}
|
||||
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
{task.source !== 'manual' && task.source && (
|
||||
task.source === 'jira' && task.jiraKey ? (
|
||||
{task.source !== 'manual' &&
|
||||
task.source &&
|
||||
(task.source === 'jira' && task.jiraKey ? (
|
||||
preferences.jiraConfig.baseUrl ? (
|
||||
<a
|
||||
href={getJiraTicketUrl(task.jiraKey)}
|
||||
@@ -419,7 +461,11 @@ export function TaskCard({ task, onEdit, compactView = false }: TaskCardProps) {
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="hover:scale-105 transition-transform"
|
||||
>
|
||||
<Badge variant="outline" size="sm" className="hover:bg-blue-500/10 hover:border-blue-400/50 cursor-pointer">
|
||||
<Badge
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="hover:bg-blue-500/10 hover:border-blue-400/50 cursor-pointer"
|
||||
>
|
||||
{task.jiraKey}
|
||||
</Badge>
|
||||
</a>
|
||||
@@ -428,27 +474,74 @@ export function TaskCard({ task, onEdit, compactView = false }: TaskCardProps) {
|
||||
{task.jiraKey}
|
||||
</Badge>
|
||||
)
|
||||
) : task.source === 'tfs' && task.tfsPullRequestId ? (
|
||||
preferences.tfsConfig &&
|
||||
(preferences.tfsConfig as TfsConfig).organizationUrl ? (
|
||||
<a
|
||||
href={getTfsPullRequestUrl(
|
||||
task.tfsPullRequestId,
|
||||
task.tfsProject || '',
|
||||
task.tfsRepository || ''
|
||||
)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="hover:scale-105 transition-transform"
|
||||
>
|
||||
<Badge
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="hover:bg-orange-500/10 hover:border-orange-400/50 cursor-pointer"
|
||||
>
|
||||
PR-{task.tfsPullRequestId}
|
||||
</Badge>
|
||||
</a>
|
||||
) : (
|
||||
<Badge variant="outline" size="sm">
|
||||
PR-{task.tfsPullRequestId}
|
||||
</Badge>
|
||||
)
|
||||
) : (
|
||||
<Badge variant="outline" size="sm">
|
||||
{task.source}
|
||||
</Badge>
|
||||
)
|
||||
)}
|
||||
|
||||
{task.jiraProject && (
|
||||
<Badge variant="outline" size="sm" className="text-blue-400 border-blue-400/30">
|
||||
{task.jiraProject}
|
||||
</Badge>
|
||||
)}
|
||||
))}
|
||||
|
||||
{/* Badges spécifiques TFS */}
|
||||
{task.tfsRepository && (
|
||||
<Badge
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="text-orange-400 border-orange-400/30"
|
||||
>
|
||||
{task.tfsRepository}
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
{task.jiraProject && (
|
||||
<Badge
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="text-blue-400 border-blue-400/30"
|
||||
>
|
||||
{task.jiraProject}
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
{task.jiraType && (
|
||||
<Badge
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="text-purple-400 border-purple-400/30"
|
||||
>
|
||||
{task.jiraType}
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
{task.jiraType && (
|
||||
<Badge variant="outline" size="sm" className="text-purple-400 border-purple-400/30">
|
||||
{task.jiraType}
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
{task.completedAt && (
|
||||
<span className="text-emerald-400 font-mono font-bold">✓ DONE</span>
|
||||
<span className="text-emerald-400 font-mono font-bold">
|
||||
✓ DONE
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user