From a3f5cf5e4e61a6320bfdd486d31b9c41c6c37e8f Mon Sep 17 00:00:00 2001 From: Arthur Belleville Date: Sat, 21 Feb 2026 14:35:12 +0100 Subject: [PATCH] Redesign overview dashboard with new project cards and task list - Replace renderTabloListView with reusable ProjectCard / ProjectCardList components - Card layout with status badge, progress bar, date, and delete action - Default view shows 6 tablos with expand/collapse toggle - Add DashboardTaskList component showing tasks assigned to the current user - Toggle done/todo inline; "Add Task" button opens TaskModal with tablo selection - Wire TopBar search input to URL param ?q= to filter tablos on the overview page - Add TopBar component to Layout (was missing) Co-Authored-By: Claude Opus 4.6 --- apps/main/src/components/ActionCard.tsx | 77 ++++ .../src/components/DashboardActionCards.tsx | 62 +++ .../main/src/components/DashboardTaskList.tsx | 191 ++++++++ apps/main/src/components/Layout.tsx | 10 +- apps/main/src/components/NavigationBar.tsx | 26 +- apps/main/src/components/ProjectCard.tsx | 157 +++++++ apps/main/src/components/ProjectCardList.tsx | 78 ++++ apps/main/src/components/TopBar.tsx | 362 +++++++++++++++ apps/main/src/lib/routes.tsx | 5 + apps/main/src/locales/en/navigation.json | 4 +- apps/main/src/locales/en/pages.json | 21 + apps/main/src/locales/fr/navigation.json | 4 +- apps/main/src/locales/fr/pages.json | 21 + apps/main/src/pages/tablo.tsx | 424 +++++++----------- apps/main/src/pages/tablos.tsx | 7 + 15 files changed, 1163 insertions(+), 286 deletions(-) create mode 100644 apps/main/src/components/ActionCard.tsx create mode 100644 apps/main/src/components/DashboardActionCards.tsx create mode 100644 apps/main/src/components/DashboardTaskList.tsx create mode 100644 apps/main/src/components/ProjectCard.tsx create mode 100644 apps/main/src/components/ProjectCardList.tsx create mode 100644 apps/main/src/components/TopBar.tsx create mode 100644 apps/main/src/pages/tablos.tsx diff --git a/apps/main/src/components/ActionCard.tsx b/apps/main/src/components/ActionCard.tsx new file mode 100644 index 0000000..7c6ffd8 --- /dev/null +++ b/apps/main/src/components/ActionCard.tsx @@ -0,0 +1,77 @@ +import { cn } from "@xtablo/shared"; +import { ReactNode } from "react"; + +export interface ActionCardProps { + icon: ReactNode; + label: string; + description: string; + variant?: "primary" | "default"; + isSelected?: boolean; + onClick?: () => void; + className?: string; +} + +export function ActionCard({ + icon, + label, + description, + variant = "default", + isSelected = false, + onClick, + className, +}: ActionCardProps) { + const isPrimary = variant === "primary"; + const isActive = isSelected || isPrimary; + + return ( + + ); +} diff --git a/apps/main/src/components/DashboardActionCards.tsx b/apps/main/src/components/DashboardActionCards.tsx new file mode 100644 index 0000000..55b5406 --- /dev/null +++ b/apps/main/src/components/DashboardActionCards.tsx @@ -0,0 +1,62 @@ +import { FolderPlus, MessageCircle, PlusCircle, UserPlus } from "lucide-react"; +import { useState } from "react"; +import { ActionCard } from "./ActionCard"; + +export interface DashboardActionCardsProps { + onCreateProject?: () => void; + onCreateTask?: () => void; + onInviteTeam?: () => void; + onSendMessage?: () => void; +} + +type CardId = "createProject" | "createTask" | "inviteTeam" | "sendMessage"; + +export function DashboardActionCards({ + onCreateProject, + onCreateTask, + onInviteTeam, + onSendMessage, +}: DashboardActionCardsProps) { + const [selected, setSelected] = useState(null); + + const handleClick = (id: CardId, callback?: () => void) => { + setSelected(id); + callback?.(); + }; + + return ( +
+ } + label="Create Project" + description="Set goals and scope" + isSelected={selected === "createProject"} + onClick={() => handleClick("createProject", onCreateProject)} + /> + + } + label="Create Task" + description="Break work into actions" + isSelected={selected === "createTask"} + onClick={() => handleClick("createTask", onCreateTask)} + /> + + } + label="Invite Team" + description="Add collaborators instantly" + isSelected={selected === "inviteTeam"} + onClick={() => handleClick("inviteTeam", onInviteTeam)} + /> + + } + label="Send Message" + description="Communicate updates fast" + isSelected={selected === "sendMessage"} + onClick={() => handleClick("sendMessage", onSendMessage)} + /> +
+ ); +} diff --git a/apps/main/src/components/DashboardTaskList.tsx b/apps/main/src/components/DashboardTaskList.tsx new file mode 100644 index 0000000..d3211c7 --- /dev/null +++ b/apps/main/src/components/DashboardTaskList.tsx @@ -0,0 +1,191 @@ +import { cn } from "@xtablo/shared"; +import type { KanbanTask, TaskStatus } from "@xtablo/shared-types"; +import { CheckCircle2, Plus } from "lucide-react"; +import { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useNavigate } from "react-router-dom"; +import { useTablosList } from "../hooks/tablos"; +import { useAllTasks, useUpdateTask } from "../hooks/tasks"; +import { useUser } from "../providers/UserStoreProvider"; +import { TaskModal } from "./kanban/TaskModal"; + +type TaskWithTablo = KanbanTask & { + tablos: { id: string; name: string; color: string | null } | null; +}; + +const STATUS_BADGE: Record< + TaskStatus, + { className: string; labelKey: string } +> = { + todo: { + className: "bg-blue-50 text-blue-600", + labelKey: "dashboard.taskList.status.todo", + }, + in_progress: { + className: "bg-yellow-50 text-yellow-600", + labelKey: "dashboard.taskList.status.inProgress", + }, + in_review: { + className: "bg-purple-50 text-purple-600", + labelKey: "dashboard.taskList.status.inReview", + }, + done: { + className: "bg-green-50 text-green-600", + labelKey: "dashboard.taskList.status.done", + }, +}; + +function TaskRow({ + task, + onToggleDone, +}: { + task: TaskWithTablo; + onToggleDone: (task: TaskWithTablo) => void; +}) { + const { t } = useTranslation("pages"); + const navigate = useNavigate(); + const status = task.status ?? "todo"; + const isDone = status === "done"; + const badge = STATUS_BADGE[status]; + + const dateStr = task.updated_at ?? task.created_at; + const formattedDate = dateStr + ? new Intl.DateTimeFormat(undefined, { + month: "short", + day: "numeric", + year: "numeric", + }).format(new Date(dateStr)) + : ""; + + return ( +
{ + if (task.tablos) { + navigate(`/tablos/${task.tablos.id}?section=tasks`); + } + }} + > + {/* Checkbox + Title */} +
+ +

+ {task.title} +

+
+ + {/* Tablo */} + {task.tablos && ( +
+
+ + {task.tablos.name.charAt(0).toUpperCase()} + +
+ + {task.tablos.name} + +
+ )} + + {/* Date */} + + {formattedDate} + + + {/* Status badge */} + + {t(badge.labelKey)} + +
+ ); +} + +export function DashboardTaskList() { + const { t } = useTranslation("pages"); + const user = useUser(); + const { data: allTasks } = useAllTasks(); + const { data: tablos } = useTablosList(); + const updateTask = useUpdateTask(); + const [isTaskModalOpen, setIsTaskModalOpen] = useState(false); + + // Filter to tasks assigned to the current user, limited to recent ones + const myTasks = + allTasks + ?.filter((task) => task.assignee_id === user.id) + .slice(0, 7) ?? []; + + const handleToggleDone = (task: TaskWithTablo) => { + const newStatus: TaskStatus = + task.status === "done" ? "todo" : "done"; + updateTask.mutate({ id: task.id, status: newStatus }); + }; + + if (myTasks.length === 0) return null; + + return ( + <> +
+
+

+ {t("dashboard.taskList.title")} +

+ +
+
+
+ {myTasks.map((task) => ( + + ))} +
+
+
+ + setIsTaskModalOpen(false)} + tablos={tablos} + allowTabloSelection + initialStatus="todo" + /> + + ); +} diff --git a/apps/main/src/components/Layout.tsx b/apps/main/src/components/Layout.tsx index 48d63cb..3c4f7f8 100644 --- a/apps/main/src/components/Layout.tsx +++ b/apps/main/src/components/Layout.tsx @@ -5,6 +5,7 @@ import { Outlet } from "react-router-dom"; import { twMerge } from "tailwind-merge"; import { SideNavigation } from "./NavigationBar"; import { OnboardingModal } from "./OnboardingModal"; +import { TopBar } from "./TopBar"; const ONBOARDING_STORAGE_KEY = "xtablo-onboarding-completed"; @@ -50,9 +51,12 @@ export function Layout() { -
- -
+
+ +
+ +
+
); } diff --git a/apps/main/src/components/NavigationBar.tsx b/apps/main/src/components/NavigationBar.tsx index fef8ca2..bf3bac3 100644 --- a/apps/main/src/components/NavigationBar.tsx +++ b/apps/main/src/components/NavigationBar.tsx @@ -21,6 +21,7 @@ import { CreditCard, // FileTextIcon, // Notes feature temporarily hidden Kanban, + LayersIcon, ListTodo, LogOutIcon, MessageCircleIcon, @@ -42,7 +43,6 @@ import { useCreateCheckoutSession, useTrialExpiration } from "../hooks/stripe"; import { isProd, isStaging } from "../lib/env"; import { useIsReadOnlyUser, useUser } from "../providers/UserStoreProvider"; import { getXtabloIcon } from "../utils/iconHelpers"; -import { NotificationPanel } from "./NotificationPanel"; import { ThemeSwitcher } from "./ThemeSwitcher"; type NavLinkItem = { @@ -56,7 +56,7 @@ function NavLink({ isActive, children }: NavLinkProps) { return (
[data-ui=icon]:not([class*=size-])]:size-4.5", + "group w-full gap-x-3 overflow-hidden px-2.5 py-2 text-nowrap hover:bg-navbar-darker hover:no-underline focus-visible:outline-offset-0 [&>[data-ui=icon]:not([class*=size-])]:size-5", "*:data-[ui=notification-badge]:bg-navbar-darker", "*:data-[ui=notification-badge]:rounded-md", "*:data-[ui=notification-badge]:top-1/2", @@ -282,7 +282,6 @@ export const SideNavigation = ({ isMobileMenuOpen }: { isMobileMenuOpen: boolean isCollapsed ? "pl-2.5 pr-3.5" : "" )} > -
@@ -322,7 +321,7 @@ export function MainNavigation({ isCollapsed }: { isCollapsed: boolean }) { ? [ { path: "/", - label: t("projects"), + label: t("home"), icon: , }, { @@ -334,7 +333,7 @@ export function MainNavigation({ isCollapsed }: { isCollapsed: boolean }) { : [ { path: "/", - label: t("projects"), + label: t("home"), icon: , }, { @@ -355,6 +354,11 @@ export function MainNavigation({ isCollapsed }: { isCollapsed: boolean }) { label: t("tasks"), icon: , }, + { + path: "/tablos", + label: t("tablos"), + icon: , + }, { isHorizontalBar: true }, { path: "/planning", @@ -396,11 +400,11 @@ export function MainNavigation({ isCollapsed }: { isCollapsed: boolean }) { className="w-full" aria-label={isCollapsed ? label : undefined} > -
- {icon} +
+ {icon} -
-