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 { 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 ${
|
||||||
|
|||||||
@@ -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
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,
|
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);
|
||||||
|
|||||||
Reference in New Issue
Block a user