Apply linter formatting to components, locales, and pages

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Arthur Belleville 2026-02-21 22:02:22 +01:00
parent 24fe6b89a9
commit afe47554c8
No known key found for this signature in database
21 changed files with 408 additions and 217 deletions

View file

@ -32,7 +32,7 @@ export function ActionCard({
? "bg-[rgb(128,78,236)] text-white border-transparent shadow-lg"
: isPrimary
? "bg-primary text-white hover:shadow-lg"
: "bg-white border border-[#EAECF0] hover:shadow-md",
: "bg-white dark:bg-gray-800 border border-[#EAECF0] dark:border-gray-700 hover:shadow-md",
className,
)}
>
@ -40,7 +40,7 @@ export function ActionCard({
<div
className={cn(
"w-10 h-10 rounded-[8px] flex items-center justify-center flex-shrink-0",
isActive ? "bg-white/20" : "bg-[#F4F3FF]",
isActive ? "bg-white/20" : "bg-[#F4F3FF] dark:bg-purple-900/20",
)}
>
<span
@ -57,7 +57,7 @@ export function ActionCard({
<span
className={cn(
"block font-semibold text-lg leading-tight",
isActive ? "text-white" : "text-gray-900",
isActive ? "text-white" : "text-gray-900 dark:text-gray-100",
)}
>
{label}
@ -65,7 +65,7 @@ export function ActionCard({
<p
className={cn(
"text-sm mt-0.5",
isActive ? "text-purple-100" : "text-gray-500",
isActive ? "text-purple-100" : "text-gray-500 dark:text-gray-400",
)}
>
{description}

View file

@ -1,5 +1,6 @@
import { FolderPlus, MessageCircle, PlusCircle, UserPlus } from "lucide-react";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { ActionCard } from "./ActionCard";
export interface DashboardActionCardsProps {
@ -17,6 +18,7 @@ export function DashboardActionCards({
onInviteTeam,
onSendMessage,
}: DashboardActionCardsProps) {
const { t } = useTranslation("pages");
const [selected, setSelected] = useState<CardId | null>(null);
const handleClick = (id: CardId, callback?: () => void) => {
@ -28,32 +30,32 @@ export function DashboardActionCards({
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-5">
<ActionCard
icon={<FolderPlus className="w-6 h-6" />}
label="Create Project"
description="Set goals and scope"
label={t("dashboard.actionCards.createProject.label")}
description={t("dashboard.actionCards.createProject.description")}
isSelected={selected === "createProject"}
onClick={() => handleClick("createProject", onCreateProject)}
/>
<ActionCard
icon={<PlusCircle className="w-6 h-6" />}
label="Create Task"
description="Break work into actions"
label={t("dashboard.actionCards.createTask.label")}
description={t("dashboard.actionCards.createTask.description")}
isSelected={selected === "createTask"}
onClick={() => handleClick("createTask", onCreateTask)}
/>
<ActionCard
icon={<UserPlus className="w-6 h-6" />}
label="Invite Team"
description="Add collaborators instantly"
label={t("dashboard.actionCards.inviteTeam.label")}
description={t("dashboard.actionCards.inviteTeam.description")}
isSelected={selected === "inviteTeam"}
onClick={() => handleClick("inviteTeam", onInviteTeam)}
/>
<ActionCard
icon={<MessageCircle className="w-6 h-6" />}
label="Send Message"
description="Communicate updates fast"
label={t("dashboard.actionCards.sendMessage.label")}
description={t("dashboard.actionCards.sendMessage.description")}
isSelected={selected === "sendMessage"}
onClick={() => handleClick("sendMessage", onSendMessage)}
/>

View file

@ -18,19 +18,23 @@ const STATUS_BADGE: Record<
{ className: string; labelKey: string }
> = {
todo: {
className: "bg-blue-50 text-blue-600",
className:
"bg-blue-50 text-blue-600 dark:bg-blue-950/30 dark:text-blue-400",
labelKey: "dashboard.taskList.status.todo",
},
in_progress: {
className: "bg-yellow-50 text-yellow-600",
className:
"bg-yellow-50 text-yellow-600 dark:bg-yellow-950/30 dark:text-yellow-400",
labelKey: "dashboard.taskList.status.inProgress",
},
in_review: {
className: "bg-purple-50 text-purple-600",
className:
"bg-purple-50 text-purple-600 dark:bg-purple-950/30 dark:text-purple-400",
labelKey: "dashboard.taskList.status.inReview",
},
done: {
className: "bg-green-50 text-green-600",
className:
"bg-green-50 text-green-600 dark:bg-green-950/30 dark:text-green-400",
labelKey: "dashboard.taskList.status.done",
},
};
@ -59,67 +63,71 @@ function TaskRow({
return (
<div
className="flex items-center justify-between gap-4 p-4 hover:bg-gray-50 transition-colors border-b border-gray-200 cursor-pointer"
className="grid grid-cols-[auto_1fr_1fr_auto_auto] items-center gap-4 px-4 py-3 hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors border-b border-gray-200 dark:border-gray-700 cursor-pointer"
onClick={() => {
if (task.tablos) {
navigate(`/tablos/${task.tablos.id}?section=tasks`);
}
}}
>
{/* Checkbox + Title */}
<div className="flex items-center gap-3 min-w-0 flex-1">
<button
className={cn(
"w-6 h-6 rounded-full border-2 flex items-center justify-center shrink-0",
isDone
? "bg-purple-600 border-purple-600"
: "border-gray-300 hover:border-purple-400",
)}
onClick={(e) => {
e.stopPropagation();
onToggleDone(task);
}}
>
{isDone && <CheckCircle2 className="w-4 h-4 text-white" />}
</button>
<p
className={cn(
"text-sm font-medium truncate",
isDone ? "line-through text-gray-400" : "text-gray-900",
)}
>
{task.title}
</p>
</div>
{/* Checkbox */}
<button
className={cn(
"w-6 h-6 rounded-full border-2 flex items-center justify-center shrink-0",
isDone
? "bg-purple-600 border-purple-600"
: "border-gray-300 hover:border-purple-400 dark:border-gray-600 dark:hover:border-purple-500",
)}
onClick={(e) => {
e.stopPropagation();
onToggleDone(task);
}}
>
{isDone && <CheckCircle2 className="w-4 h-4 text-white" />}
</button>
{/* Title */}
<p
className={cn(
"text-sm font-medium truncate",
isDone
? "line-through text-gray-400 dark:text-gray-500"
: "text-gray-900 dark:text-gray-100",
)}
>
{task.title}
</p>
{/* Tablo */}
{task.tablos && (
<div className="flex items-center gap-2 shrink-0">
<div
className={cn(
"w-6 h-6 rounded-lg flex items-center justify-center text-xs shrink-0",
task.tablos.color || "bg-gray-400",
)}
>
<span className="text-white font-bold text-[10px]">
{task.tablos.name.charAt(0).toUpperCase()}
<div className="flex items-center gap-2 min-w-0">
{task.tablos && (
<>
<div
className={cn(
"w-6 h-6 rounded-lg flex items-center justify-center text-xs shrink-0",
task.tablos.color || "bg-gray-400",
)}
>
<span className="text-white font-bold text-[10px]">
{task.tablos.name.charAt(0).toUpperCase()}
</span>
</div>
<span className="text-sm text-gray-700 dark:text-gray-300 hidden sm:inline truncate">
{task.tablos.name}
</span>
</div>
<span className="text-sm text-[#0C111D] hidden sm:inline max-w-[140px] truncate">
{task.tablos.name}
</span>
</div>
)}
</>
)}
</div>
{/* Date */}
<span className="text-sm text-[#0C111D] shrink-0 hidden md:inline">
<span className="text-sm text-gray-500 dark:text-gray-400 hidden md:inline whitespace-nowrap">
{formattedDate}
</span>
{/* Status badge */}
<span
className={cn(
"px-3 py-1 rounded-full text-xs font-medium shrink-0",
"px-3 py-1 rounded-full text-xs font-medium whitespace-nowrap",
badge.className,
)}
>
@ -153,13 +161,13 @@ export function DashboardTaskList() {
return (
<>
<div className="bg-white rounded-2xl border border-gray-100">
<div className="flex items-center justify-between px-4 py-5 border-b border-gray-200">
<h2 className="text-2xl font-semibold text-gray-900">
<div className="bg-white dark:bg-gray-800 rounded-2xl border border-gray-100 dark:border-gray-700">
<div className="flex items-center justify-between px-4 py-5 border-b border-gray-200 dark:border-gray-700">
<h2 className="text-2xl font-semibold text-gray-900 dark:text-gray-100">
{t("dashboard.taskList.title")}
</h2>
<button
className="flex items-center gap-2 px-4 py-2 bg-white rounded-lg border border-gray-200 hover:bg-gray-50"
className="flex items-center gap-2 px-4 py-2 bg-white dark:bg-gray-700 rounded-lg border border-gray-200 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-600 text-gray-700 dark:text-gray-300"
onClick={() => setIsTaskModalOpen(true)}
>
<Plus className="w-4 h-4" />

View file

@ -16,25 +16,29 @@ function useStatusConfig(status: string): StatusConfig {
case "todo":
return {
label: t("tablo.status.todo"),
badgeClass: "bg-blue-50 text-blue-600 border-blue-200",
badgeClass:
"bg-blue-50 text-blue-600 border-blue-200 dark:bg-blue-950/30 dark:text-blue-400 dark:border-blue-800",
progressColor: "bg-blue-500",
};
case "in_progress":
return {
label: t("tablo.status.inProgress"),
badgeClass: "bg-yellow-50 text-yellow-600 border-yellow-200",
badgeClass:
"bg-yellow-50 text-yellow-600 border-yellow-200 dark:bg-yellow-950/30 dark:text-yellow-400 dark:border-yellow-800",
progressColor: "bg-purple-500",
};
case "done":
return {
label: t("tablo.status.done"),
badgeClass: "bg-green-50 text-green-600 border-green-200",
badgeClass:
"bg-green-50 text-green-600 border-green-200 dark:bg-green-950/30 dark:text-green-400 dark:border-green-800",
progressColor: "bg-green-500",
};
default:
return {
label: t("tablo.status.todo"),
badgeClass: "bg-blue-50 text-blue-600 border-blue-200",
badgeClass:
"bg-blue-50 text-blue-600 border-blue-200 dark:bg-blue-950/30 dark:text-blue-400 dark:border-blue-800",
progressColor: "bg-blue-500",
};
}
@ -79,7 +83,7 @@ export function ProjectCard({
return (
<div
className={cn(
"bg-white rounded-2xl p-4 border border-[#EAECF0] hover:shadow-md transition-shadow cursor-pointer",
"bg-white dark:bg-gray-800 rounded-2xl p-4 border border-[#EAECF0] dark:border-gray-700 hover:shadow-md transition-shadow cursor-pointer",
className,
)}
onClick={() => onClick?.(tablo.id)}
@ -95,7 +99,7 @@ export function ProjectCard({
{statusConfig.label}
</span>
<button
className="text-gray-400 hover:text-red-500"
className="text-gray-400 hover:text-red-500 dark:text-gray-500 dark:hover:text-red-400"
onClick={(e) => {
e.stopPropagation();
onMenuClick?.(tablo.id);
@ -125,13 +129,13 @@ export function ProjectCard({
</span>
)}
</div>
<h3 className="font-semibold text-gray-900 flex-1 truncate">
<h3 className="font-semibold text-gray-900 dark:text-gray-100 flex-1 truncate">
{tablo.name}
</h3>
</div>
{/* Date */}
<div className="flex items-center gap-2 text-sm text-gray-500 mb-4">
<div className="flex items-center gap-2 text-sm text-gray-500 dark:text-gray-400 mb-4">
<Calendar className="w-4 h-4" />
<span>{formattedDate}</span>
</div>
@ -139,10 +143,14 @@ export function ProjectCard({
{/* Progress */}
<div className="mb-3">
<div className="flex items-center justify-between text-sm mb-2">
<span className="text-gray-600">{t("tablo.card.progress")}:</span>
<span className="font-semibold text-gray-900">{progress}%</span>
<span className="text-gray-600 dark:text-gray-400">
{t("tablo.card.progress")}:
</span>
<span className="font-semibold text-gray-900 dark:text-gray-100">
{progress}%
</span>
</div>
<div className="w-full bg-gray-100 rounded-full h-2">
<div className="w-full bg-gray-100 dark:bg-gray-700 rounded-full h-2">
<div
className={cn(
"h-2 rounded-full transition-all",

View file

@ -28,12 +28,12 @@ export function ProjectCardList({
return (
<div className="mb-8">
<div className="flex items-center justify-between mb-6">
<h2 className="text-2xl font-semibold text-gray-900">
<h2 className="text-2xl font-semibold text-gray-900 dark:text-gray-100">
{t("tablo.projectList.title")}
</h2>
{onSeeAllClick && (
<button
className="flex items-center gap-1 text-purple-600 hover:text-purple-700 font-medium"
className="flex items-center gap-1 text-purple-600 hover:text-purple-500 dark:text-purple-400 dark:hover:text-purple-300 font-medium"
onClick={onSeeAllClick}
>
{t("tablo.projectList.seeAll")}
@ -54,7 +54,7 @@ export function ProjectCardList({
{hasMore && (
<div className="flex justify-center mt-6">
<button
className="flex items-center gap-1.5 text-purple-600 hover:text-purple-700 font-medium text-sm"
className="flex items-center gap-1.5 text-purple-600 hover:text-purple-500 dark:text-purple-400 dark:hover:text-purple-300 font-medium text-sm"
onClick={() => setExpanded((prev) => !prev)}
>
{expanded ? (

View file

@ -24,7 +24,7 @@
},
"labels": {
"title": "Title *",
"tablo": "Tablo *",
"tablo": "Project *",
"date": "Date *",
"startTime": "Start *",
"endTime": "End",
@ -32,7 +32,7 @@
},
"placeholders": {
"title": "Add a title",
"tablo": "Select a tablo",
"tablo": "Select a project",
"description": "Add a description (optional)"
},
"buttons": {
@ -80,10 +80,10 @@
"required": "*"
},
"deleteTabloModal": {
"title": "Delete tablo",
"title": "Delete project",
"subtitle": "This action is irreversible",
"confirmQuestion": "Are you sure you want to delete the tablo",
"warning": "All data associated with this tablo will be permanently lost.",
"confirmQuestion": "Are you sure you want to delete the project",
"warning": "All data associated with this project will be permanently lost.",
"buttons": {
"cancel": "Cancel",
"delete": "Delete",
@ -105,11 +105,11 @@
"importing": "Importing..."
},
"checkbox": {
"createNewTablo": "Create a new tablo"
"createNewTablo": "Create a new project"
},
"placeholders": {
"newTabloName": "New tablo name",
"selectTablo": "Select an existing tablo"
"newTabloName": "New project name",
"selectTablo": "Select an existing project"
},
"messages": {
"eventsFound": "{{count}} event(s) found",
@ -133,12 +133,12 @@
"description": "No events to import"
},
"tabloRequired": {
"title": "Tablo required",
"description": "Please select a tablo or create a new one"
"title": "Project required",
"description": "Please select a project or create a new one"
},
"tabloNameRequired": {
"title": "Tablo name required",
"description": "Please enter a name for the new tablo"
"title": "Project name required",
"description": "Please enter a name for the new project"
},
"importSuccess": {
"title": "Import successful",

View file

@ -1,7 +1,7 @@
{
"createTablo": {
"title": "Create a new tablo",
"nameLabel": "Tablo name",
"namePlaceholder": "Enter tablo name"
"title": "Create a new project",
"nameLabel": "Project name",
"namePlaceholder": "Enter project name"
}
}

View file

@ -41,8 +41,8 @@
"shareNoteDescription": "Control who can access this note",
"publicAccess": "Public Access",
"publicAccessDescription": "Anyone with the link can view this note",
"shareWithAllTablos": "Share with All Tablos",
"shareWithAllTablosDescription": "Members of all your tablos can view this note",
"shareWithAllTablos": "Share with All Projects",
"shareWithAllTablosDescription": "Members of all your projects can view this note",
"close": "Close",
"saveNoteBeforeSharing": "Please save the note before sharing",
"sharingSettingsUpdated": "Sharing settings updated",

View file

@ -3,14 +3,14 @@
"skipTutorial": "Skip onboarding",
"back": "Back",
"next": "Next",
"getStarted": "Create Tablo",
"getStarted": "Create Project",
"steps": {
"welcome": {
"title": "Welcome to XTablo",
"description": "Here, you will create your first Tablo - a clear, elegant project tracking space shareable with your clients or team.",
"description": "Here, you will create your first project - a clear, elegant project tracking space shareable with your clients or team.",
"content": {
"intro": "In less than 2 minutes, you will have a ready-to-use Tablo, structured around your services.",
"cta": "Create my first Tablo"
"intro": "In less than 2 minutes, you will have a ready-to-use project, structured around your services.",
"cta": "Create my first project"
}
},
"profile": {
@ -24,7 +24,7 @@
},
"service": {
"title": "What is your main service?",
"description": "Define the service you want to track in this Tablo",
"description": "Define the service you want to track in this project",
"question": "What is your service?",
"placeholder": "Ex: Web Development, Consulting..."
},
@ -36,9 +36,9 @@
"step_placeholder": "Step {{index}}"
},
"project": {
"title": "Project Name (Tablo)",
"description": "Give your first Tablo a name",
"question": "What would you like to call this tablo?",
"title": "Project Name",
"description": "Give your first project a name",
"question": "What would you like to call this project?",
"placeholder": "ex: Landing Page - Client X"
}
}

View file

@ -1,12 +1,12 @@
{
"tablo": {
"title": "Tablos",
"subtitle": "Manage your tablos and collaborations",
"createButton": "New tablo",
"title": "Projects",
"subtitle": "Manage your projects and collaborations",
"createButton": "New project",
"emptyState": {
"title": "No tablos found",
"description": "Create your first tablo to start organizing your work",
"button": "Create your first tablo"
"title": "No projects found",
"description": "Create your first project to start organizing your work",
"button": "Create your first project"
},
"filter": {
"all": "All",
@ -30,13 +30,13 @@
"contextMenu": {
"openDiscussions": "Open discussions",
"openPlanning": "Open planning",
"delete": "Delete tablo"
"delete": "Delete project"
},
"card": {
"progress": "Progress"
},
"projectList": {
"title": "My Tablos",
"title": "My Projects",
"seeAll": "See All",
"showAll": "See {{count}} more",
"showLess": "Show less"
@ -62,7 +62,7 @@
"createEventType": "New type",
"search": "Search for an event...",
"filters": {
"allTablos": "All boards",
"allTablos": "All projects",
"upcoming": "Upcoming",
"past": "Past"
},
@ -96,10 +96,10 @@
},
"tasks": {
"title": "My Tasks",
"subtitle": "Manage all your tasks across all your tablos",
"subtitle": "Manage all your tasks across all your projects",
"search": "Search for a task...",
"filters": {
"allTablos": "All boards",
"allTablos": "All projects",
"allAssignees": "All assignees",
"assignedToMe": "Assigned to me",
"unassigned": "Unassigned"
@ -107,7 +107,7 @@
"emptyState": {
"title": "No tasks found",
"noResults": "Try changing your search filters.",
"noTasks": "Start by creating your first task in a tablo."
"noTasks": "Start by creating your first task in a project."
},
"unassigned": "Unassigned",
"pagination": {
@ -124,7 +124,7 @@
},
"view": {
"kanban": "Kanban View",
"aggregated": "By Tablo View"
"aggregated": "By Project View"
},
"createTask": "New Task"
},
@ -161,6 +161,24 @@
}
},
"dashboard": {
"actionCards": {
"createProject": {
"label": "Create Project",
"description": "Set goals and scope"
},
"createTask": {
"label": "Create Task",
"description": "Break work into actions"
},
"inviteTeam": {
"label": "Invite Team",
"description": "Add collaborators instantly"
},
"sendMessage": {
"label": "Send Message",
"description": "Communicate updates fast"
}
},
"taskList": {
"title": "My Tasks",
"addTask": "Add Task",

View file

@ -1,8 +1,8 @@
{
"title": "Planning",
"allEvents": "All events",
"allTablos": "All tablos",
"selectTablo": "Select a tablo",
"allTablos": "All projects",
"selectTablo": "Select a project",
"createEvent": "Create event",
"importPlanning": "Import planning",
"today": "Today",

View file

@ -1,7 +1,7 @@
{
"overview": {
"title": "Overview",
"description": "Configure the Stages of the tablo to clarify the major phases of your tablo.",
"description": "Configure the Stages of the project to clarify the major phases of your project.",
"overallProgress": "Overall Progress",
"progressSummary": "{{done}} of {{total}} task(s) completed"
},
@ -9,10 +9,10 @@
"nameRequired": "The Stage name is required",
"namePlaceholder": "Stage Name",
"deleteConfirm": "Are you sure you want to delete the Stage \"{{name}}\"? Associated tasks will remain available.",
"noEtapes": "No Stages have been defined for this tablo yet.",
"createFirstEtape": "Create your first Stage to structure the tablo tasks.",
"onlyOwnerCanAdd": "Only the tablo owner can add Stages.",
"onlyOwnerCanModify": "Only the tablo owner can modify Stages. Contact the administrator if you need a new Stage.",
"noEtapes": "No Stages have been defined for this project yet.",
"createFirstEtape": "Create your first Stage to structure the project tasks.",
"onlyOwnerCanAdd": "Only the project owner can add Stages.",
"onlyOwnerCanModify": "Only the project owner can modify Stages. Contact the administrator if you need a new Stage.",
"stepNumber": "Stage {{number}}",
"addNew": "New Stage"
},
@ -25,6 +25,6 @@
},
"events": {
"title": "Upcoming events",
"description": "Manage the future events of this tablo"
"description": "Manage the future events of this project"
}
}

View file

@ -24,7 +24,7 @@
},
"labels": {
"title": "Titre *",
"tablo": "Tablo *",
"tablo": "Projet *",
"date": "Date *",
"startTime": "Début *",
"endTime": "Fin",
@ -32,7 +32,7 @@
},
"placeholders": {
"title": "Ajouter un titre",
"tablo": "Sélectionner un tablo",
"tablo": "Sélectionner un projet",
"description": "Ajouter une description (optionnel)"
},
"buttons": {
@ -80,10 +80,10 @@
"required": "*"
},
"deleteTabloModal": {
"title": "Supprimer le tablo",
"title": "Supprimer le projet",
"subtitle": "Cette action est irréversible",
"confirmQuestion": "Êtes-vous sûr de vouloir supprimer le tablo",
"warning": "Toutes les données associées à ce tablo seront perdues définitivement.",
"confirmQuestion": "Êtes-vous sûr de vouloir supprimer le projet",
"warning": "Toutes les données associées à ce projet seront perdues définitivement.",
"buttons": {
"cancel": "Annuler",
"delete": "Supprimer",
@ -105,11 +105,11 @@
"importing": "Import en cours..."
},
"checkbox": {
"createNewTablo": "Créer un nouveau tablo"
"createNewTablo": "Créer un nouveau projet"
},
"placeholders": {
"newTabloName": "Nom du nouveau tablo",
"selectTablo": "Sélectionner un tablo existant"
"newTabloName": "Nom du nouveau projet",
"selectTablo": "Sélectionner un projet existant"
},
"messages": {
"eventsFound": "{{count}} événement(s) trouvé(s)",
@ -133,12 +133,12 @@
"description": "Aucun événement à importer"
},
"tabloRequired": {
"title": "Tablo requis",
"description": "Veuillez sélectionner un tablo ou créer un nouveau"
"title": "Projet requis",
"description": "Veuillez sélectionner un projet ou en créer un nouveau"
},
"tabloNameRequired": {
"title": "Nom du tablo requis",
"description": "Veuillez saisir un nom pour le nouveau tablo"
"title": "Nom du projet requis",
"description": "Veuillez saisir un nom pour le nouveau projet"
},
"importSuccess": {
"title": "Import réussi",

View file

@ -1,7 +1,7 @@
{
"createTablo": {
"title": "Créer un nouveau tablo",
"nameLabel": "Nom du tablo",
"namePlaceholder": "Entrez le nom du tablo"
"title": "Créer un nouveau projet",
"nameLabel": "Nom du projet",
"namePlaceholder": "Entrez le nom du projet"
}
}

View file

@ -41,8 +41,8 @@
"shareNoteDescription": "Contrôlez qui peut accéder à cette note",
"publicAccess": "Accès public",
"publicAccessDescription": "Toute personne avec le lien peut voir cette note",
"shareWithAllTablos": "Partager avec tous les tablos",
"shareWithAllTablosDescription": "Les membres de tous vos tablos peuvent voir cette note",
"shareWithAllTablos": "Partager avec tous les projets",
"shareWithAllTablosDescription": "Les membres de tous vos projets peuvent voir cette note",
"close": "Fermer",
"saveNoteBeforeSharing": "Veuillez enregistrer la note avant de la partager",
"sharingSettingsUpdated": "Paramètres de partage mis à jour",

View file

@ -3,14 +3,14 @@
"skipTutorial": "Passer l'onboarding",
"back": "Retour",
"next": "Suivant",
"getStarted": "Créer le Tablo",
"getStarted": "Créer le projet",
"steps": {
"welcome": {
"title": "Bienvenue sur XTablo",
"description": "Ici, vous allez créer votre premier Tablo - un espace de suivi projet, clair, élégant et partageable avec vos clients.",
"description": "Ici, vous allez créer votre premier projet - un espace de suivi clair, élégant et partageable avec vos clients.",
"content": {
"intro": "En moins de 2 minutes, vous aurez un Tablo prêt à l'emploi, structuré autour de vos services.",
"cta": "Créer mon premier Tablo"
"intro": "En moins de 2 minutes, vous aurez un projet prêt à l'emploi, structuré autour de vos services.",
"cta": "Créer mon premier projet"
}
},
"profile": {
@ -24,7 +24,7 @@
},
"service": {
"title": "Quel est votre service principal ?",
"description": "Définissez le service que vous souhaitez suivre dans ce Tablo",
"description": "Définissez le service que vous souhaitez suivre dans ce projet",
"question": "Quel est votre service ?",
"placeholder": "Ex: Création de site web, Consulting..."
},
@ -36,9 +36,9 @@
"step_placeholder": "Étape {{index}}"
},
"project": {
"title": "Nom du projet (tablo)",
"description": "Donnez un nom à votre premier Tablo",
"question": "Comment souhaitez-vous appeler ce tablo ?",
"title": "Nom du projet",
"description": "Donnez un nom à votre premier projet",
"question": "Comment souhaitez-vous appeler ce projet ?",
"placeholder": "ex : Landing Page - Client X"
}
}

View file

@ -1,12 +1,12 @@
{
"tablo": {
"title": "Tablos",
"subtitle": "Gérez vos tablos et collaborations",
"createButton": "Nouveau tablo",
"title": "Projets",
"subtitle": "Gérez vos projets et collaborations",
"createButton": "Nouveau projet",
"emptyState": {
"title": "Aucun tablo trouvé",
"description": "Créez votre premier tablo pour commencer à organiser votre travail",
"button": "Créer votre premier tablo"
"title": "Aucun projet trouvé",
"description": "Créez votre premier projet pour commencer à organiser votre travail",
"button": "Créer votre premier projet"
},
"filter": {
"all": "Tous",
@ -30,13 +30,13 @@
"contextMenu": {
"openDiscussions": "Ouvrir les discussions",
"openPlanning": "Ouvrir le planning",
"delete": "Supprimer le tablo"
"delete": "Supprimer le projet"
},
"card": {
"progress": "Progression"
},
"projectList": {
"title": "Mes Tablos",
"title": "Mes Projets",
"seeAll": "Voir tout",
"showAll": "Voir {{count}} de plus",
"showLess": "Réduire"
@ -62,7 +62,7 @@
"createEventType": "Nouveau type",
"search": "Rechercher un événement...",
"filters": {
"allTablos": "Tous les tablos",
"allTablos": "Tous les projets",
"upcoming": "À venir",
"past": "Passés"
},
@ -96,10 +96,10 @@
},
"tasks": {
"title": "Mes Tâches",
"subtitle": "Gérez toutes vos tâches à travers tous vos tablos",
"subtitle": "Gérez toutes vos tâches à travers tous vos projets",
"search": "Rechercher une tâche...",
"filters": {
"allTablos": "Tous les tablos",
"allTablos": "Tous les projets",
"allAssignees": "Tous les assignés",
"assignedToMe": "Assignées à moi",
"unassigned": "Non assignées"
@ -107,7 +107,7 @@
"emptyState": {
"title": "Aucune tâche trouvée",
"noResults": "Essayez de modifier vos filtres de recherche.",
"noTasks": "Commencez par créer votre première tâche dans un tablo."
"noTasks": "Commencez par créer votre première tâche dans un projet."
},
"unassigned": "Non assignée",
"pagination": {
@ -124,7 +124,7 @@
},
"view": {
"kanban": "Vue Kanban",
"aggregated": "Vue par tablo"
"aggregated": "Vue par projet"
},
"createTask": "Nouvelle tâche"
},
@ -161,6 +161,24 @@
}
},
"dashboard": {
"actionCards": {
"createProject": {
"label": "Créer un projet",
"description": "Définir les objectifs et le périmètre"
},
"createTask": {
"label": "Créer une tâche",
"description": "Découper le travail en actions"
},
"inviteTeam": {
"label": "Inviter l'équipe",
"description": "Ajouter des collaborateurs"
},
"sendMessage": {
"label": "Envoyer un message",
"description": "Communiquer rapidement"
}
},
"taskList": {
"title": "Mes Tâches",
"addTask": "Ajouter",

View file

@ -1,8 +1,8 @@
{
"title": "Planning",
"allEvents": "Tous les événements",
"allTablos": "Tous les tablos",
"selectTablo": "Sélectionner un tablo",
"allTablos": "Tous les projets",
"selectTablo": "Sélectionner un projet",
"createEvent": "Créer un événement",
"importPlanning": "Importer un planning",
"today": "Aujourd'hui",

View file

@ -1,7 +1,7 @@
{
"overview": {
"title": "Vue d'ensemble",
"description": "Configurez les Étapes du tablo pour clarifier les grandes phases de votre tablo.",
"description": "Configurez les Étapes du projet pour clarifier les grandes phases de votre projet.",
"overallProgress": "Progression globale",
"progressSummary": "{{done}} sur {{total}} tâche(s) terminée(s)"
},
@ -9,10 +9,10 @@
"nameRequired": "Le nom de l'Étape est requis",
"namePlaceholder": "Nom de l'Étape",
"deleteConfirm": "Êtes-vous sûr de vouloir supprimer l'Étape \"{{name}}\" ? Les tâches associées resteront disponibles.",
"noEtapes": "Aucune Étape n'a encore été définie pour ce tablo.",
"createFirstEtape": "Créez votre première Étape pour structurer les tâches du tablo.",
"onlyOwnerCanAdd": "Seul le propriétaire du tablo peut ajouter des Étapes.",
"onlyOwnerCanModify": "Seul le propriétaire du tablo peut modifier les Étapes. Contactez l'administrateur si vous avez besoin d'une nouvelle Étape.",
"noEtapes": "Aucune Étape n'a encore été définie pour ce projet.",
"createFirstEtape": "Créez votre première Étape pour structurer les tâches du projet.",
"onlyOwnerCanAdd": "Seul le propriétaire du projet peut ajouter des Étapes.",
"onlyOwnerCanModify": "Seul le propriétaire du projet peut modifier les Étapes. Contactez l'administrateur si vous avez besoin d'une nouvelle Étape.",
"stepNumber": "Étape {{number}}",
"addNew": "Ajouter l'Étape"
},
@ -25,7 +25,7 @@
},
"events": {
"title": "Événements à venir",
"description": "Gérez les événements futurs de ce tablo",
"description": "Gérez les événements futurs de ce projet",
"createEvent": "Créer un événement"
}
}

View file

@ -16,7 +16,12 @@ import {
UserPlusIcon,
} from "lucide-react";
import { useEffect, useState } from "react";
import { Link, useNavigate, useParams, useSearchParams } from "react-router-dom";
import {
Link,
useNavigate,
useParams,
useSearchParams,
} from "react-router-dom";
import { TabloDiscussionSection } from "../components/TabloDiscussionSection";
import { TabloEventsSection } from "../components/TabloEventsSection";
import { TabloFilesSection } from "../components/TabloFilesSection";
@ -30,19 +35,48 @@ import { useTablosList } from "../hooks/tablos";
function getStatusConfig(status: string) {
switch (status) {
case "in_progress":
return { label: "En cours", badgeClass: "bg-yellow-50 text-yellow-700 border border-yellow-200 dark:bg-yellow-950/30 dark:text-yellow-400 dark:border-yellow-800", progress: 50 };
return {
label: "En cours",
badgeClass:
"bg-yellow-50 text-yellow-700 border border-yellow-200 dark:bg-yellow-950/30 dark:text-yellow-400 dark:border-yellow-800",
progress: 50,
};
case "done":
return { label: "Terminé", badgeClass: "bg-green-50 text-green-600 border border-green-200 dark:bg-green-950/30 dark:text-green-400 dark:border-green-800", progress: 100 };
return {
label: "Terminé",
badgeClass:
"bg-green-50 text-green-600 border border-green-200 dark:bg-green-950/30 dark:text-green-400 dark:border-green-800",
progress: 100,
};
default:
return { label: "À faire", badgeClass: "bg-blue-50 text-blue-600 border border-blue-200 dark:bg-blue-950/30 dark:text-blue-400 dark:border-blue-800", progress: 0 };
return {
label: "À faire",
badgeClass:
"bg-blue-50 text-blue-600 border border-blue-200 dark:bg-blue-950/30 dark:text-blue-400 dark:border-blue-800",
progress: 0,
};
}
}
// ─── Tabs ─────────────────────────────────────────────────────────────────────
type TabSection = "overview" | "board" | "list" | "roadmap" | "calendar" | "files" | "discussion" | "events" | "tasks";
type TabSection =
| "overview"
| "board"
| "list"
| "roadmap"
| "calendar"
| "files"
| "discussion"
| "events"
| "tasks";
const TABS: { id: TabSection; label: string; icon: React.ElementType; disabled?: boolean }[] = [
const TABS: {
id: TabSection;
label: string;
icon: React.ElementType;
disabled?: boolean;
}[] = [
{ id: "overview", label: "Aperçu", icon: LayoutDashboardIcon },
{ id: "tasks", label: "Tâches", icon: KanbanIcon },
{ id: "files", label: "Fichiers", icon: FolderIcon },
@ -74,8 +108,13 @@ export const TabloDetailsPage = () => {
setTablo(found);
} else {
toast.add(
{ title: "Projet introuvable", description: "Le projet demandé n'existe pas ou vous n'y avez pas accès", type: "error" },
{ timeout: 5000 }
{
title: "Projet introuvable",
description:
"Le projet demandé n'existe pas ou vous n'y avez pas accès",
type: "error",
},
{ timeout: 5000 },
);
navigate("/tablos");
}
@ -84,11 +123,15 @@ export const TabloDetailsPage = () => {
// Tasks for this tablo (used in overview)
const { data: allTasks = [] } = useAllTasks();
const tabloTasks = (allTasks as KanbanTask[]).filter((t) => t.tablo_id === tabloId);
const tabloTasks = (allTasks as KanbanTask[]).filter(
(t) => t.tablo_id === tabloId,
);
// Files for this tablo (used in overview)
const { data: filesData } = useTabloFileNames(tabloId ?? "");
const fileNames = (filesData?.fileNames ?? []).filter((f) => !f.startsWith("."));
const fileNames = (filesData?.fileNames ?? []).filter(
(f) => !f.startsWith("."),
);
if (isLoading) {
return (
@ -100,28 +143,38 @@ export const TabloDetailsPage = () => {
if (!tablo) return null;
const { label: statusLabel, badgeClass, progress } = getStatusConfig(tablo.status);
const {
label: statusLabel,
badgeClass,
progress,
} = getStatusConfig(tablo.status);
const isAdmin = tablo.is_admin;
return (
<div>
{/* ── Header ──────────────────────────────────────────────────────── */}
<div className="px-4 pt-10">
<div className="px-4 pt-4">
<div className="flex flex-col md:flex-row items-start justify-between mb-6 border-b border-[#F2F4F7] dark:border-gray-700 pb-5 gap-5 md:gap-0">
<div className="flex items-center gap-4">
<div
className={cn(
"w-12 h-12 rounded-lg flex items-center justify-center shrink-0 overflow-hidden text-white font-bold text-xl",
!tablo.image && (tablo.color || "bg-gray-400")
!tablo.image && (tablo.color || "bg-gray-400"),
)}
>
{tablo.image ? (
<img src={tablo.image} alt={tablo.name} className="w-full h-full object-cover" />
<img
src={tablo.image}
alt={tablo.name}
className="w-full h-full object-cover"
/>
) : (
tablo.name.charAt(0).toUpperCase()
)}
</div>
<h1 className="text-xl md:text-3xl font-bold text-foreground">{tablo.name}</h1>
<h1 className="text-xl md:text-3xl font-bold text-foreground">
{tablo.name}
</h1>
</div>
<div className="flex flex-wrap items-center gap-3">
@ -146,22 +199,38 @@ export const TabloDetailsPage = () => {
<div className="flex flex-wrap items-center gap-6 text-sm border-b border-[#F2F4F7] dark:border-gray-700 pb-4 mb-4">
<div className="flex items-center gap-2 md:border-r border-[#D0D5DD] dark:border-gray-600 pr-4">
<span className="text-muted-foreground">Rôle :</span>
<span className="text-foreground font-medium">{isAdmin ? "Admin" : "Invité"}</span>
<span className="text-foreground font-medium">
{isAdmin ? "Admin" : "Invité"}
</span>
</div>
<div className="flex items-center gap-2 md:border-r border-[#D0D5DD] dark:border-gray-600 pr-4">
<span className="text-muted-foreground">Créé le :</span>
<span className="text-foreground">
{new Intl.DateTimeFormat("fr-FR", { year: "numeric", month: "short", day: "2-digit" }).format(new Date(tablo.created_at))}
{new Intl.DateTimeFormat("fr-FR", {
year: "numeric",
month: "short",
day: "2-digit",
}).format(new Date(tablo.created_at))}
</span>
</div>
<div className="flex items-center gap-2 md:border-r border-[#D0D5DD] dark:border-gray-600 pr-4">
<span className="text-muted-foreground">Statut :</span>
<span className={cn("px-3 py-1 rounded-full text-xs font-medium", badgeClass)}>{statusLabel}</span>
<span
className={cn(
"px-3 py-1 rounded-full text-xs font-medium",
badgeClass,
)}
>
{statusLabel}
</span>
</div>
<div className="flex items-center gap-2">
<span className="text-muted-foreground">Progression :</span>
<div className="w-24 h-2 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden">
<div className="h-full bg-green-500 rounded-full" style={{ width: `${progress}%` }} />
<div
className="h-full bg-green-500 rounded-full"
style={{ width: `${progress}%` }}
/>
</div>
<span className="text-foreground font-medium">{progress}%</span>
</div>
@ -179,13 +248,15 @@ export const TabloDetailsPage = () => {
key={tab.id}
type="button"
disabled={tab.disabled}
onClick={() => !tab.disabled && setSearchParams({ section: tab.id })}
onClick={() =>
!tab.disabled && setSearchParams({ section: tab.id })
}
className={cn(
"flex items-center gap-2 pb-3 px-1 text-sm font-semibold transition-colors border-b-2",
isActive
? "text-[#804EEC] border-[#804EEC]"
: "text-[#667085] border-transparent hover:text-gray-900 dark:hover:text-gray-100",
tab.disabled && "opacity-40 cursor-not-allowed"
tab.disabled && "opacity-40 cursor-not-allowed",
)}
>
<tab.icon className="w-4 h-4" />
@ -210,16 +281,22 @@ export const TabloDetailsPage = () => {
<div className="lg:col-span-2 space-y-6">
{/* Description */}
<div className="bg-white dark:bg-card rounded-xl border border-border p-6 sm:p-8 shadow-sm">
<h2 className="text-xl sm:text-2xl font-bold text-foreground mb-4">Description du projet</h2>
<h2 className="text-xl sm:text-2xl font-bold text-foreground mb-4">
Description du projet
</h2>
<p className="text-muted-foreground leading-relaxed text-sm sm:text-base">
Ce projet regroupe les tâches, fichiers et événements associés. Utilisez les onglets ci-dessus pour naviguer entre les différentes sections.
Ce projet regroupe les tâches, fichiers et événements
associés. Utilisez les onglets ci-dessus pour naviguer entre
les différentes sections.
</p>
</div>
{/* Tasks */}
<div className="bg-white dark:bg-card rounded-xl border border-gray-100 dark:border-gray-700 shadow-sm overflow-hidden">
<div className="flex flex-col sm:flex-row sm:items-center justify-between px-4 sm:px-6 py-4 border-b border-gray-200 dark:border-gray-700 gap-3">
<h2 className="text-xl sm:text-2xl font-semibold text-gray-900 dark:text-gray-100">Mes tâches</h2>
<h2 className="text-xl sm:text-2xl font-semibold text-gray-900 dark:text-gray-100">
Mes tâches
</h2>
<button
type="button"
onClick={() => setSearchParams({ section: "tasks" })}
@ -231,7 +308,9 @@ export const TabloDetailsPage = () => {
</div>
<div className="divide-y divide-gray-200 dark:divide-gray-700">
{tabloTasks.length === 0 ? (
<div className="p-6 text-center text-muted-foreground text-sm">Aucune tâche</div>
<div className="p-6 text-center text-muted-foreground text-sm">
Aucune tâche
</div>
) : (
tabloTasks.slice(0, 5).map((task) => (
<div
@ -244,7 +323,14 @@ export const TabloDetailsPage = () => {
) : (
<div className="w-5 h-5 rounded-full border-2 border-gray-300 dark:border-gray-600 shrink-0" />
)}
<p className={cn("text-sm font-medium truncate", task.status === "done" ? "line-through text-gray-400" : "text-gray-900 dark:text-gray-100")}>
<p
className={cn(
"text-sm font-medium truncate",
task.status === "done"
? "line-through text-gray-400"
: "text-gray-900 dark:text-gray-100",
)}
>
{task.title}
</p>
</div>
@ -268,24 +354,40 @@ export const TabloDetailsPage = () => {
{/* Files */}
<div className="bg-white dark:bg-card rounded-xl border border-border p-5 sm:p-6 shadow-sm">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-bold text-foreground">Fichiers</h3>
<button type="button" onClick={() => setSearchParams({ section: "files" })} className="text-sm text-[#804EEC] hover:underline">
<h3 className="text-lg font-bold text-foreground">
Fichiers
</h3>
<button
type="button"
onClick={() => setSearchParams({ section: "files" })}
className="text-sm text-[#804EEC] hover:underline"
>
Voir tout
</button>
</div>
<div className="space-y-3">
{fileNames.length === 0 ? (
<p className="text-sm text-muted-foreground">Aucun fichier</p>
<p className="text-sm text-muted-foreground">
Aucun fichier
</p>
) : (
fileNames.slice(0, 5).map((fileName) => (
<div key={fileName} className="flex items-start gap-3 p-3 hover:bg-accent dark:hover:bg-gray-800 rounded-lg transition-colors">
<div
key={fileName}
className="flex items-start gap-3 p-3 hover:bg-accent dark:hover:bg-gray-800 rounded-lg transition-colors"
>
<div className="w-10 h-10 bg-red-100 dark:bg-red-900/30 rounded-lg flex items-center justify-center shrink-0">
<FileTextIcon className="w-4 h-4 text-red-500" />
</div>
<div className="flex-1 min-w-0">
<p className="font-medium text-foreground text-sm truncate">{fileName}</p>
<p className="font-medium text-foreground text-sm truncate">
{fileName}
</p>
</div>
<button type="button" className="text-muted-foreground hover:text-foreground p-1 shrink-0">
<button
type="button"
className="text-muted-foreground hover:text-foreground p-1 shrink-0"
>
<EllipsisVerticalIcon className="w-4 h-4" />
</button>
</div>
@ -296,22 +398,57 @@ export const TabloDetailsPage = () => {
{/* Info */}
<div className="bg-white dark:bg-card rounded-xl border border-border p-5 sm:p-6 shadow-sm">
<h3 className="text-lg font-bold text-foreground mb-4">Informations</h3>
<h3 className="text-lg font-bold text-foreground mb-4">
Informations
</h3>
<dl className="space-y-3 text-sm">
<div className="flex justify-between"><dt className="text-muted-foreground">Tâches</dt><dd className="font-medium text-foreground">{tabloTasks.length}</dd></div>
<div className="flex justify-between"><dt className="text-muted-foreground">Fichiers</dt><dd className="font-medium text-foreground">{fileNames.length}</dd></div>
<div className="flex justify-between"><dt className="text-muted-foreground">Statut</dt><dd className={cn("px-2 py-0.5 rounded-full text-xs font-medium", badgeClass)}>{statusLabel}</dd></div>
<div className="flex justify-between"><dt className="text-muted-foreground">Rôle</dt><dd className="font-medium text-foreground">{isAdmin ? "Admin" : "Invité"}</dd></div>
<div className="flex justify-between">
<dt className="text-muted-foreground">Tâches</dt>
<dd className="font-medium text-foreground">
{tabloTasks.length}
</dd>
</div>
<div className="flex justify-between">
<dt className="text-muted-foreground">Fichiers</dt>
<dd className="font-medium text-foreground">
{fileNames.length}
</dd>
</div>
<div className="flex justify-between">
<dt className="text-muted-foreground">Statut</dt>
<dd
className={cn(
"px-2 py-0.5 rounded-full text-xs font-medium",
badgeClass,
)}
>
{statusLabel}
</dd>
</div>
<div className="flex justify-between">
<dt className="text-muted-foreground">Rôle</dt>
<dd className="font-medium text-foreground">
{isAdmin ? "Admin" : "Invité"}
</dd>
</div>
</dl>
</div>
</div>
</div>
)}
{activeSection === "tasks" && <TabloTasksSection tablo={tablo} isAdmin={isAdmin} />}
{activeSection === "files" && <TabloFilesSection tablo={tablo} isAdmin={isAdmin} />}
{activeSection === "discussion" && <TabloDiscussionSection tablo={tablo} isAdmin={isAdmin} />}
{activeSection === "events" && <TabloEventsSection tablo={tablo} isAdmin={isAdmin} />}
{activeSection === "tasks" && (
<TabloTasksSection tablo={tablo} isAdmin={isAdmin} />
)}
{activeSection === "files" && (
<TabloFilesSection tablo={tablo} isAdmin={isAdmin} />
)}
{activeSection === "discussion" && (
<TabloDiscussionSection tablo={tablo} isAdmin={isAdmin} />
)}
{activeSection === "events" && (
<TabloEventsSection tablo={tablo} isAdmin={isAdmin} />
)}
</div>
</div>
);

View file

@ -118,7 +118,7 @@ export const TabloPage = () => {
{
title: t("common:error"),
description:
"Vous êtes en mode lecture seule. Vous ne pouvez pas créer de tablo.",
"Vous êtes en mode lecture seule. Vous ne pouvez pas créer de projet.",
type: "error",
},
{ timeout: 5000 },
@ -327,7 +327,7 @@ export const TabloPage = () => {
</Text>
</div>
<Button onClick={openCreateModal} disabled={isReadOnly}>
<Plus /> Nouveau tablo
<Plus /> Nouveau projet
</Button>
</div>
</div>
@ -336,7 +336,7 @@ export const TabloPage = () => {
<div className="flex justify-center items-center min-h-64">
<div className="text-center">
<p className="text-destructive mb-2">
Erreur lors du chargement des tablos
Erreur lors du chargement des projets
</p>
<p className="text-muted-foreground text-sm">
{error instanceof Error
@ -507,13 +507,13 @@ export const TabloPage = () => {
}}
>
<header className="px-6 pt-6 pb-4">
<p className="text-base text-[#475467] mb-2 font-medium">
<p className="text-base text-[#475467] dark:text-gray-400 mb-2 font-medium">
{formattedDate}
</p>
<div className="flex items-center justify-between flex-wrap gap-4">
<h1 className="text-[24px] font-medium text-[#475467]">
<h1 className="text-[24px] font-medium text-[#475467] dark:text-gray-400">
{getGreeting()},{" "}
<span className="text-gray-900 font-medium">
<span className="text-gray-900 dark:text-gray-100 font-medium">
{user.first_name ?? user.name}
</span>
!