feat: improve invoice view responsiveness and settings UX

- Replace custom invoice items table with responsive DataTable component
- Fix server/client component error by creating InvoiceItemsTable client
  component
- Merge danger zone with actions sidebar and use destructive button
  variant
- Standardize button text sizing across all action buttons
- Remove false claims from homepage (testimonials, ratings, fake user
  counts)
- Focus homepage messaging on freelancers with honest feature
  descriptions
- Fix dark mode support throughout app by replacing hard-coded colors
  with semantic classes
- Remove aggressive red styling from settings, add subtle red accents
  only
- Align import/export buttons and improve delete confirmation UX
- Update dark mode background to have subtle green tint instead of pure
  black
- Fix HTML nesting error in AlertDialog by using div instead of nested p
  tags

This update makes the invoice view properly responsive, removes
misleading marketing claims, and ensures consistent dark mode support
across the entire application.
This commit is contained in:
2025-07-15 02:35:55 -04:00
parent f331136090
commit c9a664869c
71 changed files with 2795 additions and 3043 deletions
@@ -0,0 +1,105 @@
"use client";
import { useEffect, useRef, useState } from "react";
import { cn } from "~/lib/utils";
interface FloatingActionBarProps {
/** Ref to the element that triggers visibility when scrolled out of view */
triggerRef: React.RefObject<HTMLElement | null>;
/** Title text displayed on the left */
title: string;
/** Action buttons to display on the right */
children: React.ReactNode;
/** Additional className for styling */
className?: string;
/** Whether to show the floating bar (for manual control) */
show?: boolean;
/** Callback when visibility changes */
onVisibilityChange?: (visible: boolean) => void;
}
export function FloatingActionBar({
triggerRef,
title,
children,
className,
show,
onVisibilityChange,
}: FloatingActionBarProps) {
const [isVisible, setIsVisible] = useState(false);
const floatingRef = useRef<HTMLDivElement>(null);
useEffect(() => {
// If show prop is provided, use it instead of auto-detection
if (show !== undefined) {
setIsVisible(show);
onVisibilityChange?.(show);
return;
}
const handleScroll = () => {
if (!triggerRef.current) return;
const rect = triggerRef.current.getBoundingClientRect();
const isInView = rect.top < window.innerHeight && rect.bottom >= 0;
// Show floating bar when trigger element is out of view
const shouldShow = !isInView;
if (shouldShow !== isVisible) {
setIsVisible(shouldShow);
onVisibilityChange?.(shouldShow);
}
};
// Use ResizeObserver and IntersectionObserver for better detection
const observer = new IntersectionObserver(
(entries) => {
const entry = entries[0];
if (entry) {
const shouldShow = !entry.isIntersecting;
if (shouldShow !== isVisible) {
setIsVisible(shouldShow);
onVisibilityChange?.(shouldShow);
}
}
},
{
// Trigger when element is completely out of view
threshold: 0,
rootMargin: "0px 0px -100% 0px",
},
);
// Start observing when trigger element is available
if (triggerRef.current) {
observer.observe(triggerRef.current);
}
// Also add scroll listener as fallback
window.addEventListener("scroll", handleScroll, { passive: true });
// Check initial state
handleScroll();
return () => {
observer.disconnect();
window.removeEventListener("scroll", handleScroll);
};
}, [triggerRef, isVisible, show, onVisibilityChange]);
if (!isVisible) return null;
return (
<div
ref={floatingRef}
className={cn(
"border-border/40 bg-background/60 animate-in slide-in-from-bottom-4 fixed right-3 bottom-3 left-3 z-20 flex items-center justify-between rounded-2xl border p-4 shadow-lg backdrop-blur-xl backdrop-saturate-150 duration-300 md:right-3 md:left-[279px]",
className,
)}
>
<p className="text-muted-foreground text-sm">{title}</p>
<div className="flex items-center gap-3">{children}</div>
</div>
);
}