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:
@@ -5,6 +5,8 @@ import Link from 'next/link';
|
||||
import { DailyCheckbox, DailyCheckboxType } from '@/lib/types';
|
||||
import { Input } from '@/components/ui/Input';
|
||||
import { EditCheckboxModal } from './EditCheckboxModal';
|
||||
import { getTaskEmoji } from '@/lib/task-emoji';
|
||||
import { Emoji } from '@/components/ui/Emoji';
|
||||
|
||||
interface DailyCheckboxItemProps {
|
||||
checkbox: DailyCheckbox;
|
||||
@@ -129,6 +131,11 @@ export function DailyCheckboxItem({
|
||||
// Vérifier si la tâche est archivée
|
||||
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 (
|
||||
<>
|
||||
<div
|
||||
@@ -162,7 +169,14 @@ export function DailyCheckboxItem({
|
||||
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 */}
|
||||
<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 ${
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Badge } from './Badge';
|
||||
import { TagDisplay } from './TagDisplay';
|
||||
import { formatDateForDisplay } from '@/lib/date-utils';
|
||||
import emojiRegex from 'emoji-regex';
|
||||
import { getTaskEmoji } from '@/lib/task-emoji';
|
||||
|
||||
interface TaskCardProps extends HTMLAttributes<HTMLDivElement> {
|
||||
// Variants
|
||||
@@ -216,35 +217,22 @@ const TaskCard = forwardRef<HTMLDivElement, TaskCardProps>(
|
||||
return colors[priority as keyof typeof colors] || colors.medium;
|
||||
};
|
||||
|
||||
// Fonction pour extraire les emojis avec la lib emoji-regex
|
||||
const extractEmojis = (text: string): string[] => {
|
||||
const regex = emojiRegex();
|
||||
return text.match(regex) || [];
|
||||
};
|
||||
|
||||
const titleEmojis = extractEmojis(title);
|
||||
// Utiliser getTaskEmoji avec les propriétés de la tâche disponibles
|
||||
const taskEmoji = getTaskEmoji(
|
||||
{
|
||||
title,
|
||||
tags: tags || [],
|
||||
primaryTagId,
|
||||
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();
|
||||
|
||||
// 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 priorityColor = getPriorityColor(priority);
|
||||
|
||||
|
||||
96
src/lib/task-emoji.ts
Normal file
96
src/lib/task-emoji.ts
Normal 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;
|
||||
}
|
||||
@@ -61,7 +61,19 @@ export class DailyService {
|
||||
date: normalizedDate,
|
||||
userId: userId,
|
||||
},
|
||||
include: { task: true, user: true },
|
||||
include: {
|
||||
task: {
|
||||
include: {
|
||||
taskTags: {
|
||||
include: {
|
||||
tag: true,
|
||||
},
|
||||
},
|
||||
primaryTag: true,
|
||||
},
|
||||
},
|
||||
user: true,
|
||||
},
|
||||
orderBy: { order: 'asc' },
|
||||
});
|
||||
|
||||
@@ -93,7 +105,19 @@ export class DailyService {
|
||||
order,
|
||||
isChecked: data.isChecked ?? false,
|
||||
},
|
||||
include: { task: true, user: true },
|
||||
include: {
|
||||
task: {
|
||||
include: {
|
||||
taskTags: {
|
||||
include: {
|
||||
tag: true,
|
||||
},
|
||||
},
|
||||
primaryTag: true,
|
||||
},
|
||||
},
|
||||
user: true,
|
||||
},
|
||||
});
|
||||
|
||||
return this.mapPrismaCheckbox(checkbox);
|
||||
@@ -124,7 +148,19 @@ export class DailyService {
|
||||
const checkbox = await prisma.dailyCheckbox.update({
|
||||
where: { id: checkboxId },
|
||||
data: updateData,
|
||||
include: { task: true, user: true },
|
||||
include: {
|
||||
task: {
|
||||
include: {
|
||||
taskTags: {
|
||||
include: {
|
||||
tag: true,
|
||||
},
|
||||
},
|
||||
primaryTag: true,
|
||||
},
|
||||
},
|
||||
user: true,
|
||||
},
|
||||
});
|
||||
|
||||
return this.mapPrismaCheckbox(checkbox);
|
||||
@@ -145,7 +181,19 @@ export class DailyService {
|
||||
const updated = await prisma.dailyCheckbox.update({
|
||||
where: { id: checkboxId },
|
||||
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);
|
||||
@@ -195,7 +243,19 @@ export class DailyService {
|
||||
contains: query,
|
||||
},
|
||||
},
|
||||
include: { task: true, user: true },
|
||||
include: {
|
||||
task: {
|
||||
include: {
|
||||
taskTags: {
|
||||
include: {
|
||||
tag: true,
|
||||
},
|
||||
},
|
||||
primaryTag: true,
|
||||
},
|
||||
},
|
||||
user: true,
|
||||
},
|
||||
orderBy: { date: 'desc' },
|
||||
take: limit,
|
||||
});
|
||||
@@ -271,12 +331,76 @@ export class DailyService {
|
||||
|
||||
/**
|
||||
* Mappe une checkbox Prisma vers notre interface
|
||||
* Accepte les checkboxes avec ou sans les relations taskTags et primaryTag
|
||||
*/
|
||||
private mapPrismaCheckbox(
|
||||
checkbox: Prisma.DailyCheckboxGetPayload<{
|
||||
include: { task: true; user: true };
|
||||
include: {
|
||||
task:
|
||||
| true
|
||||
| {
|
||||
include: {
|
||||
taskTags: {
|
||||
include: {
|
||||
tag: true;
|
||||
};
|
||||
};
|
||||
primaryTag: true;
|
||||
};
|
||||
};
|
||||
user: true;
|
||||
};
|
||||
}>
|
||||
): 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 {
|
||||
id: checkbox.id,
|
||||
date: checkbox.date,
|
||||
@@ -295,7 +419,10 @@ export class DailyService {
|
||||
priority: checkbox.task.priority as TaskPriority,
|
||||
source: checkbox.task.source as TaskSource,
|
||||
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,
|
||||
completedAt: checkbox.task.completedAt || undefined,
|
||||
createdAt: checkbox.task.createdAt,
|
||||
@@ -384,7 +511,19 @@ export class DailyService {
|
||||
|
||||
const checkboxes = await prisma.dailyCheckbox.findMany({
|
||||
where: whereConditions,
|
||||
include: { task: true, user: true },
|
||||
include: {
|
||||
task: {
|
||||
include: {
|
||||
taskTags: {
|
||||
include: {
|
||||
tag: true,
|
||||
},
|
||||
},
|
||||
primaryTag: true,
|
||||
},
|
||||
},
|
||||
user: true,
|
||||
},
|
||||
orderBy: [{ date: 'desc' }, { order: 'asc' }],
|
||||
...(options?.limit ? { take: options.limit } : {}),
|
||||
});
|
||||
@@ -406,7 +545,19 @@ export class DailyService {
|
||||
?.text + ' [ARCHIVÉ]',
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
include: { task: true, user: true },
|
||||
include: {
|
||||
task: {
|
||||
include: {
|
||||
taskTags: {
|
||||
include: {
|
||||
tag: true,
|
||||
},
|
||||
},
|
||||
primaryTag: true,
|
||||
},
|
||||
},
|
||||
user: true,
|
||||
},
|
||||
});
|
||||
|
||||
return this.mapPrismaCheckbox(checkbox);
|
||||
@@ -450,7 +601,19 @@ export class DailyService {
|
||||
order: newOrder,
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
include: { task: true, user: true },
|
||||
include: {
|
||||
task: {
|
||||
include: {
|
||||
taskTags: {
|
||||
include: {
|
||||
tag: true,
|
||||
},
|
||||
},
|
||||
primaryTag: true,
|
||||
},
|
||||
},
|
||||
user: true,
|
||||
},
|
||||
});
|
||||
|
||||
return this.mapPrismaCheckbox(updatedCheckbox);
|
||||
|
||||
Reference in New Issue
Block a user