Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .claude/settings.local.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
"allow": [
"Bash(git mv:*)",
"Bash(git pull *)",
"Bash(node -e ' *)"
"Bash(node -e ' *)",
"Bash(curl -s -o /tmp/preview2.html -w \"%{http_code}\" http://localhost:3000/preview-next-job --max-time 90)",
"Read(//tmp/**)"
]
}
}
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ yarn-error.log*
.env.local
database.sql

# database backups (contain guest PII + password hashes — never commit)
/backups
_diag_booking.js

# vercel
.vercel

Expand Down
105 changes: 105 additions & 0 deletions Components/GlobalTooltip.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
"use client";

import { useEffect, useState } from "react";

interface TipState {
text: string;
left: number;
top: number;
placement: "top" | "bottom";
}

/**
* GlobalTooltip
* Replaces the slow, unreliable native `title` tooltip (which only appears after a
* ~1s hover and resets when the mouse moves) with an instant custom tooltip.
*
* It works app-wide via event delegation: on hover, it moves the element's `title`
* into `data-tooltip` (suppressing the browser's built-in tooltip) and renders a
* styled tooltip immediately. No per-button changes are required — every element
* that already has a `title` attribute is covered automatically.
*/
export default function GlobalTooltip() {
const [tip, setTip] = useState<TipState | null>(null);

useEffect(() => {
let activeEl: HTMLElement | null = null;

const show = (el: HTMLElement) => {
// Move native title into data-tooltip so the browser's own tooltip never shows
const native = el.getAttribute("title");
if (native && native.trim()) {
el.setAttribute("data-tooltip", native);
el.removeAttribute("title");
}

const text = el.getAttribute("data-tooltip");
if (!text || !text.trim()) return;

activeEl = el;
const rect = el.getBoundingClientRect();
const placement: "top" | "bottom" = rect.top < 44 ? "bottom" : "top";
setTip({
text,
left: rect.left + rect.width / 2,
top: placement === "top" ? rect.top : rect.bottom,
placement,
});
};

const hide = () => {
activeEl = null;
setTip(null);
};

const onOver = (e: MouseEvent) => {
const target = e.target as HTMLElement | null;
if (!target || typeof target.closest !== "function") return;
const el = target.closest("[title], [data-tooltip]") as HTMLElement | null;
if (!el || el === activeEl) return;
show(el);
};

const onOut = (e: MouseEvent) => {
if (!activeEl) return;
// Ignore movement to a child of the active element (prevents flicker)
const related = e.relatedTarget as Node | null;
if (related && activeEl.contains(related)) return;
hide();
};

document.addEventListener("mouseover", onOver, true);
document.addEventListener("mouseout", onOut, true);
// Hide on scroll/click so the tooltip never lingers in the wrong place
window.addEventListener("scroll", hide, true);
window.addEventListener("click", hide, true);

return () => {
document.removeEventListener("mouseover", onOver, true);
document.removeEventListener("mouseout", onOut, true);
window.removeEventListener("scroll", hide, true);
window.removeEventListener("click", hide, true);
};
}, []);

if (!tip) return null;

return (
<div
style={{
position: "fixed",
left: tip.left,
top: tip.placement === "top" ? tip.top - 8 : tip.top + 8,
transform:
tip.placement === "top"
? "translate(-50%, -100%)"
: "translate(-50%, 0)",
zIndex: 1000000,
}}
role="tooltip"
className="pointer-events-none whitespace-nowrap rounded-md bg-gray-900 px-2.5 py-1.5 text-xs font-medium text-white shadow-lg dark:bg-gray-700"
>
{tip.text}
</div>
);
}
2 changes: 2 additions & 0 deletions Components/Providers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { ThemeProvider as NextThemesProvider } from "next-themes";
import { Toaster } from "react-hot-toast";
import { useInactivityLogout } from '@/hooks/useInactivityLogout';
import ConditionalLayout from './ConditionalLayout';
import GlobalTooltip from './GlobalTooltip';

function InactivityLogoutWrapper({ children }: { children: React.ReactNode }) {
useInactivityLogout();
Expand All @@ -40,6 +41,7 @@ export function Providers ({ children }: {children:React.ReactNode}) {
<ConditionalLayout>
{children}
</ConditionalLayout>
<GlobalTooltip />
<Toaster
position="top-center"
containerStyle={{
Expand Down
82 changes: 8 additions & 74 deletions Components/admin/Cleaners/CleanersDashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -483,78 +483,12 @@ useEffect(() => {
);
})}
</nav>

{/* User Profile Section */}
<div className="p-2 border-t border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-900">
{sidebar && (
<div className="mb-2">
{isLoading ? (
<div className="flex items-center gap-3 p-2">
<div className="w-10 h-10 rounded-full bg-gray-200 dark:bg-gray-700 animate-pulse flex-shrink-0"></div>
<div className="flex-1 min-w-0 space-y-2">
<div className="h-4 w-24 bg-gray-200 dark:bg-gray-700 rounded animate-pulse"></div>
<div className="h-3 w-16 bg-gray-200 dark:bg-gray-700 rounded animate-pulse"></div>
</div>
</div>
) : (
<div className="flex items-center gap-3 p-2 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg transition-colors">
{employee?.profile_image_url ? (
<div className="w-10 h-10 rounded-full overflow-hidden flex-shrink-0">
<Image
src={employee.profile_image_url}
alt={cleanerData.name}
width={40}
height={40}
className="w-full h-full object-cover"
key={`${employee.profile_image_url}-${employee.updated_at || ''}`}
/>
</div>
) : (
<div className="w-10 h-10 rounded-full bg-white dark:bg-gray-700 border-2 border-gray-200 dark:border-gray-600 flex-shrink-0 flex items-center justify-center text-gray-700 dark:text-gray-200 font-medium text-lg overflow-hidden">
<span>{cleanerData.name.charAt(0)}</span>
</div>
)}
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-gray-800 dark:text-gray-100 truncate">
{cleanerData.name}
</p>
<p className="text-xs text-gray-500 dark:text-gray-400 truncate">
{cleanerData.role}
</p>
</div>
</div>
)}
<div className="mt-2">
<button
onClick={handleLogout}
className="w-full flex items-center gap-3 px-4 py-3 text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 rounded-lg transition-all font-medium"
>
<LogOut className="w-5 h-5" />
<span className="text-sm">{t.logout}</span>
</button>
</div>
</div>
)}
{!sidebar && (
<div className="flex justify-center py-2">
<button
onClick={handleLogout}
className="p-2 hover:bg-red-50 dark:hover:bg-red-900/20 rounded-lg transition-all"
title="Logout"
>
<LogOut className="w-5 h-5 text-red-600 dark:text-red-400" />
</button>
</div>
)}
</div>
</div>

{/* MAIN CONTENT */}
<div className={`flex-1 flex flex-col min-h-screen ${mobileMenuOpen ? 'overflow-hidden md:overflow-x-hidden md:overflow-y-auto' : 'overflow-x-hidden overflow-y-auto'}`}>
<div className={`flex-1 flex flex-col h-screen min-w-0 ${mobileMenuOpen ? 'overflow-hidden md:overflow-x-hidden md:overflow-y-auto' : 'overflow-x-hidden overflow-y-auto'}`}>
{/* HEADER */}
<div className={`bg-white dark:bg-gray-900 border-b border-gray-200 dark:border-gray-800 px-4 sm:px-6 h-20 min-h-20 flex-shrink-0 flex justify-between items-center fixed top-0 right-0 left-0 z-30 shadow-sm ${
sidebar ? "md:left-20 lg:left-72" : "md:left-20 lg:left-20"
}`}>
<div className="bg-white dark:bg-gray-900 border-b border-gray-200 dark:border-gray-800 px-3 sm:px-6 h-16 sm:h-20 min-h-16 sm:min-h-20 flex-shrink-0 flex justify-between items-center sticky top-0 z-10">
<div className="flex items-center gap-4">
{/* Mobile Menu Button */}
<button
Expand Down Expand Up @@ -657,7 +591,7 @@ useEffect(() => {
onClick={() => setProfileDropdownOpen(!profileDropdownOpen)}
className="flex items-center gap-2 sm:gap-3 p-1 sm:px-2 sm:py-1.5 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg transition-colors"
>
<div className="w-10 h-10 bg-white dark:bg-gray-700 border-2 border-gray-200 dark:border-gray-600 rounded-full overflow-hidden flex items-center justify-center text-gray-700 dark:text-gray-200 font-bold cursor-pointer transition-colors">
<div className="w-10 h-10 bg-white dark:bg-gray-700 border-2 border-brand-primary rounded-full overflow-hidden flex items-center justify-center text-gray-700 dark:text-gray-200 font-bold cursor-pointer transition-colors shadow-sm">
{employee?.profile_image_url ? (
<Image
src={employee.profile_image_url}
Expand Down Expand Up @@ -687,7 +621,7 @@ useEffect(() => {
</button>

{profileDropdownOpen && (
<div className="absolute right-0 mt-2 w-52 sm:w-56 bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 overflow-hidden z-50">
<div className="absolute right-0 mt-2 w-52 sm:w-56 bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-brand-primary/20 dark:border-gray-700 overflow-hidden z-50">
{/* User Info Header */}
<div className="p-4 bg-gray-50 dark:bg-gray-700/50 border-b border-gray-200 dark:border-gray-600">
<div className="flex items-center gap-3">
Expand All @@ -697,12 +631,12 @@ useEffect(() => {
alt={cleanerData.name}
width={40}
height={40}
className="w-10 h-10 rounded-full object-cover ring-2 ring-gray-200 dark:ring-gray-600"
className="w-10 h-10 rounded-full object-cover ring-2 ring-brand-primary"
key={`${employee.profile_image_url}-${employee.updated_at || ''}`}
/>
) : (
<div className="w-10 h-10 rounded-full bg-white dark:bg-gray-700 border-2 border-gray-200 dark:border-gray-600 flex items-center justify-center">
<User className="w-5 h-5 text-gray-700 dark:text-gray-200" />
<div className="w-10 h-10 rounded-full bg-brand-primary flex items-center justify-center">
<User className="w-5 h-5 text-white" />
</div>
)}
<div className="flex-1 min-w-0">
Expand Down Expand Up @@ -816,7 +750,7 @@ useEffect(() => {
)}

{/* PAGE CONTENT */}
<div className="flex-1 p-6 pt-24">
<div className="flex-1 p-6">
<div className="max-w-[1600px] mx-auto w-full">{renderPage()}</div>
</div>

Expand Down
Loading