feat(DailyCheckboxItem, TaskCard, DailyService): enhance task emoji handling and improve data fetching

- Added emoji support in DailyCheckboxItem and TaskCard components using getTaskEmoji.
- Updated DailyService to include taskTags and primaryTag in checkbox data fetching, improving task detail retrieval.
- Refactored mapPrismaCheckbox to handle taskTags and primaryTag extraction for better task representation.
This commit is contained in:
Julien Froidefond
2025-11-03 09:29:37 +01:00
parent 08f3fb6e85
commit 9fc355abad
4 changed files with 299 additions and 38 deletions

View File

@@ -5,6 +5,8 @@ import Link from 'next/link';
import { DailyCheckbox, DailyCheckboxType } from '@/lib/types'; import { DailyCheckbox, DailyCheckboxType } from '@/lib/types';
import { Input } from '@/components/ui/Input'; import { Input } from '@/components/ui/Input';
import { EditCheckboxModal } from './EditCheckboxModal'; import { EditCheckboxModal } from './EditCheckboxModal';
import { getTaskEmoji } from '@/lib/task-emoji';
import { Emoji } from '@/components/ui/Emoji';
interface DailyCheckboxItemProps { interface DailyCheckboxItemProps {
checkbox: DailyCheckbox; checkbox: DailyCheckbox;
@@ -129,6 +131,11 @@ export function DailyCheckboxItem({
// Vérifier si la tâche est archivée // Vérifier si la tâche est archivée
const isArchived = checkbox.isArchived; const isArchived = checkbox.isArchived;
// Obtenir l'emoji de la tâche associée si elle existe
const taskEmoji = checkbox.task
? getTaskEmoji(checkbox.task, undefined)
: null;
return ( return (
<> <>
<div <div
@@ -162,7 +169,14 @@ export function DailyCheckboxItem({
className="flex-1 h-7 text-sm" className="flex-1 h-7 text-sm"
/> />
) : ( ) : (
<div className="flex-1 flex items-center gap-2"> <div className="flex-1 flex items-center gap-0.5">
{/* Emoji de la tâche associée */}
{taskEmoji && (
<span className="text-base flex-shrink-0">
<Emoji emoji={taskEmoji} />
</span>
)}
{/* Texte cliquable pour édition inline */} {/* Texte cliquable pour édition inline */}
<span <span
className={`flex-1 text-sm sm:text-xs font-mono transition-all cursor-pointer hover:bg-[var(--muted)]/50 py-0.5 px-1 rounded ${ className={`flex-1 text-sm sm:text-xs font-mono transition-all cursor-pointer hover:bg-[var(--muted)]/50 py-0.5 px-1 rounded ${

View File

@@ -5,6 +5,7 @@ import { Badge } from './Badge';
import { TagDisplay } from './TagDisplay'; import { TagDisplay } from './TagDisplay';
import { formatDateForDisplay } from '@/lib/date-utils'; import { formatDateForDisplay } from '@/lib/date-utils';
import emojiRegex from 'emoji-regex'; import emojiRegex from 'emoji-regex';
import { getTaskEmoji } from '@/lib/task-emoji';
interface TaskCardProps extends HTMLAttributes<HTMLDivElement> { interface TaskCardProps extends HTMLAttributes<HTMLDivElement> {
// Variants // Variants
@@ -216,35 +217,22 @@ const TaskCard = forwardRef<HTMLDivElement, TaskCardProps>(
return colors[priority as keyof typeof colors] || colors.medium; return colors[priority as keyof typeof colors] || colors.medium;
}; };
// Fonction pour extraire les emojis avec la lib emoji-regex // Utiliser getTaskEmoji avec les propriétés de la tâche disponibles
const extractEmojis = (text: string): string[] => { const taskEmoji = getTaskEmoji(
const regex = emojiRegex(); {
return text.match(regex) || []; title,
}; tags: tags || [],
primaryTagId,
const titleEmojis = extractEmojis(title); tagDetails: availableTags?.filter((tag) => tags?.includes(tag.name)),
primaryTag: primaryTagId
? availableTags?.find((tag) => tag.id === primaryTagId)
: undefined,
},
availableTags
);
const displayEmojis: string[] = taskEmoji ? [taskEmoji] : [];
const titleWithoutEmojis = title.replace(emojiRegex(), '').trim(); const titleWithoutEmojis = title.replace(emojiRegex(), '').trim();
// Si pas d'emoji dans le titre, utiliser l'emoji du tag prioritaire ou du premier tag
let displayEmojis: string[] = titleEmojis;
if (displayEmojis.length === 0 && tags && tags.length > 0) {
// Priorité au tag prioritaire, sinon premier tag
let tagToUse = null;
if (primaryTagId && availableTags) {
tagToUse = availableTags.find((tag) => tag.id === primaryTagId);
}
if (!tagToUse) {
tagToUse = availableTags.find((tag) => tag.name === tags[0]);
}
if (tagToUse) {
const tagEmojis = extractEmojis(tagToUse.name);
if (tagEmojis.length > 0) {
displayEmojis = [tagEmojis[0]]; // Prendre seulement le premier emoji du tag
}
}
}
const sourceStyles = getSourceStyles(); const sourceStyles = getSourceStyles();
const priorityColor = getPriorityColor(priority); const priorityColor = getPriorityColor(priority);

96
src/lib/task-emoji.ts Normal file
View File

@@ -0,0 +1,96 @@
import emojiRegex from 'emoji-regex';
import { Task, Tag } from './types';
/**
* Extrait les emojis d'un texte
*/
export function extractEmojis(text: string): string[] {
const regex = emojiRegex();
return text.match(regex) || [];
}
/**
* Type pour représenter une tâche partielle avec les propriétés nécessaires pour extraire l'emoji
*/
type TaskForEmoji = Pick<Task, 'title'> &
Partial<Pick<Task, 'tags' | 'primaryTagId' | 'tagDetails' | 'primaryTag'>>;
/**
* Extrait l'emoji principal d'une tâche en suivant la logique du TaskCard:
* 1. D'abord, cherche les emojis dans le titre de la tâche
* 2. Si pas trouvé, cherche dans le tag prioritaire (primaryTag)
* 3. Si pas trouvé, cherche dans le premier tag disponible
*
* @param task La tâche (complète ou partielle) à partir de laquelle extraire l'emoji
* @param availableTags Les tags disponibles (optionnel, pour la recherche par tag)
* @returns Le premier emoji trouvé, ou null si aucun emoji n'est trouvé
*/
export function getTaskEmoji(
task: TaskForEmoji | Task | null | undefined,
availableTags?: Tag[]
): string | null {
if (!task || !task.title) {
return null;
}
// 1. Chercher les emojis dans le titre
const titleEmojis = extractEmojis(task.title);
if (titleEmojis.length > 0) {
return titleEmojis[0]; // Prendre seulement le premier emoji
}
// 2. Si pas d'emoji dans le titre, utiliser l'emoji du tag prioritaire ou du premier tag
if (task.tags && task.tags.length > 0 && availableTags) {
// Priorité au tag prioritaire, sinon premier tag
let tagToUse: Tag | null = null;
if (task.primaryTagId) {
tagToUse =
availableTags.find((tag) => tag.id === task.primaryTagId) || null;
}
if (!tagToUse && task.primaryTag) {
tagToUse = task.primaryTag;
}
if (!tagToUse) {
// Chercher par nom du premier tag
const firstTagName = task.tags[0];
tagToUse = availableTags.find((tag) => tag.name === firstTagName) || null;
}
if (tagToUse) {
const tagEmojis = extractEmojis(tagToUse.name);
if (tagEmojis.length > 0) {
return tagEmojis[0]; // Prendre seulement le premier emoji du tag
}
}
}
// 3. Si la tâche a tagDetails directement (sans avoir besoin de availableTags)
if (task.tagDetails && task.tagDetails.length > 0) {
let tagToUse: Tag | null = null;
if (task.primaryTagId) {
tagToUse =
task.tagDetails.find((tag) => tag.id === task.primaryTagId) || null;
}
if (!tagToUse && task.primaryTag) {
tagToUse = task.primaryTag;
}
if (!tagToUse) {
tagToUse = task.tagDetails[0];
}
if (tagToUse) {
const tagEmojis = extractEmojis(tagToUse.name);
if (tagEmojis.length > 0) {
return tagEmojis[0];
}
}
}
return null;
}

View File

@@ -61,7 +61,19 @@ export class DailyService {
date: normalizedDate, date: normalizedDate,
userId: userId, userId: userId,
}, },
include: { task: true, user: true }, include: {
task: {
include: {
taskTags: {
include: {
tag: true,
},
},
primaryTag: true,
},
},
user: true,
},
orderBy: { order: 'asc' }, orderBy: { order: 'asc' },
}); });
@@ -93,7 +105,19 @@ export class DailyService {
order, order,
isChecked: data.isChecked ?? false, isChecked: data.isChecked ?? false,
}, },
include: { task: true, user: true }, include: {
task: {
include: {
taskTags: {
include: {
tag: true,
},
},
primaryTag: true,
},
},
user: true,
},
}); });
return this.mapPrismaCheckbox(checkbox); return this.mapPrismaCheckbox(checkbox);
@@ -124,7 +148,19 @@ export class DailyService {
const checkbox = await prisma.dailyCheckbox.update({ const checkbox = await prisma.dailyCheckbox.update({
where: { id: checkboxId }, where: { id: checkboxId },
data: updateData, data: updateData,
include: { task: true, user: true }, include: {
task: {
include: {
taskTags: {
include: {
tag: true,
},
},
primaryTag: true,
},
},
user: true,
},
}); });
return this.mapPrismaCheckbox(checkbox); return this.mapPrismaCheckbox(checkbox);
@@ -145,7 +181,19 @@ export class DailyService {
const updated = await prisma.dailyCheckbox.update({ const updated = await prisma.dailyCheckbox.update({
where: { id: checkboxId }, where: { id: checkboxId },
data: { isChecked: !existing.isChecked, updatedAt: new Date() }, data: { isChecked: !existing.isChecked, updatedAt: new Date() },
include: { task: true, user: true }, include: {
task: {
include: {
taskTags: {
include: {
tag: true,
},
},
primaryTag: true,
},
},
user: true,
},
}); });
return this.mapPrismaCheckbox(updated); return this.mapPrismaCheckbox(updated);
@@ -195,7 +243,19 @@ export class DailyService {
contains: query, contains: query,
}, },
}, },
include: { task: true, user: true }, include: {
task: {
include: {
taskTags: {
include: {
tag: true,
},
},
primaryTag: true,
},
},
user: true,
},
orderBy: { date: 'desc' }, orderBy: { date: 'desc' },
take: limit, take: limit,
}); });
@@ -271,12 +331,76 @@ export class DailyService {
/** /**
* Mappe une checkbox Prisma vers notre interface * Mappe une checkbox Prisma vers notre interface
* Accepte les checkboxes avec ou sans les relations taskTags et primaryTag
*/ */
private mapPrismaCheckbox( private mapPrismaCheckbox(
checkbox: Prisma.DailyCheckboxGetPayload<{ checkbox: Prisma.DailyCheckboxGetPayload<{
include: { task: true; user: true }; include: {
task:
| true
| {
include: {
taskTags: {
include: {
tag: true;
};
};
primaryTag: true;
};
};
user: true;
};
}> }>
): DailyCheckbox { ): DailyCheckbox {
// Extraire les tags de la tâche si elle existe
let taskTags: string[] = [];
let taskTagDetails:
| Array<{ id: string; name: string; color: string; isPinned?: boolean }>
| undefined = undefined;
let taskPrimaryTag:
| { id: string; name: string; color: string; isPinned?: boolean }
| undefined = undefined;
if (checkbox.task) {
// Vérifier si taskTags est disponible (peut être true ou un objet avec include)
const taskWithTags = checkbox.task as unknown as {
taskTags?: Array<{
tag: { id: string; name: string; color: string; isPinned: boolean };
}>;
primaryTag?: {
id: string;
name: string;
color: string;
isPinned: boolean;
} | null;
};
if (
'taskTags' in taskWithTags &&
taskWithTags.taskTags &&
Array.isArray(taskWithTags.taskTags)
) {
// Utiliser les relations Prisma pour récupérer les noms et détails des tags
taskTags = taskWithTags.taskTags.map((tt) => tt.tag.name);
taskTagDetails = taskWithTags.taskTags.map((tt) => ({
id: tt.tag.id,
name: tt.tag.name,
color: tt.tag.color,
isPinned: tt.tag.isPinned,
}));
}
// Extraire le primaryTag si disponible
if ('primaryTag' in taskWithTags && taskWithTags.primaryTag) {
taskPrimaryTag = {
id: taskWithTags.primaryTag.id,
name: taskWithTags.primaryTag.name,
color: taskWithTags.primaryTag.color,
isPinned: taskWithTags.primaryTag.isPinned,
};
}
}
return { return {
id: checkbox.id, id: checkbox.id,
date: checkbox.date, date: checkbox.date,
@@ -295,7 +419,10 @@ export class DailyService {
priority: checkbox.task.priority as TaskPriority, priority: checkbox.task.priority as TaskPriority,
source: checkbox.task.source as TaskSource, source: checkbox.task.source as TaskSource,
sourceId: checkbox.task.sourceId || undefined, sourceId: checkbox.task.sourceId || undefined,
tags: [], // Les tags seront chargés séparément si nécessaire tags: taskTags,
tagDetails: taskTagDetails,
primaryTagId: checkbox.task.primaryTagId || undefined,
primaryTag: taskPrimaryTag,
dueDate: checkbox.task.dueDate || undefined, dueDate: checkbox.task.dueDate || undefined,
completedAt: checkbox.task.completedAt || undefined, completedAt: checkbox.task.completedAt || undefined,
createdAt: checkbox.task.createdAt, createdAt: checkbox.task.createdAt,
@@ -384,7 +511,19 @@ export class DailyService {
const checkboxes = await prisma.dailyCheckbox.findMany({ const checkboxes = await prisma.dailyCheckbox.findMany({
where: whereConditions, where: whereConditions,
include: { task: true, user: true }, include: {
task: {
include: {
taskTags: {
include: {
tag: true,
},
},
primaryTag: true,
},
},
user: true,
},
orderBy: [{ date: 'desc' }, { order: 'asc' }], orderBy: [{ date: 'desc' }, { order: 'asc' }],
...(options?.limit ? { take: options.limit } : {}), ...(options?.limit ? { take: options.limit } : {}),
}); });
@@ -406,7 +545,19 @@ export class DailyService {
?.text + ' [ARCHIVÉ]', ?.text + ' [ARCHIVÉ]',
updatedAt: new Date(), updatedAt: new Date(),
}, },
include: { task: true, user: true }, include: {
task: {
include: {
taskTags: {
include: {
tag: true,
},
},
primaryTag: true,
},
},
user: true,
},
}); });
return this.mapPrismaCheckbox(checkbox); return this.mapPrismaCheckbox(checkbox);
@@ -450,7 +601,19 @@ export class DailyService {
order: newOrder, order: newOrder,
updatedAt: new Date(), updatedAt: new Date(),
}, },
include: { task: true, user: true }, include: {
task: {
include: {
taskTags: {
include: {
tag: true,
},
},
primaryTag: true,
},
},
user: true,
},
}); });
return this.mapPrismaCheckbox(updatedCheckbox); return this.mapPrismaCheckbox(updatedCheckbox);