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,69 @@
"use client";
import { useState, useRef } from "react";
import { Input } from "~/components/ui/input";
import { Card } from "~/components/ui/card";
interface AddressAutocompleteProps {
value: string;
onChange: (value: string) => void;
onSelect: (value: string) => void;
placeholder?: string;
}
export function AddressAutocomplete({ value, onChange, onSelect, placeholder }: AddressAutocompleteProps) {
const [suggestions, setSuggestions] = useState<any[]>([]);
const [showSuggestions, setShowSuggestions] = useState(false);
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
const fetchSuggestions = async (query: string) => {
if (!query) {
setSuggestions([]);
return;
}
const res = await fetch(`https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(query)}`);
const data = await res.json();
setSuggestions(data);
};
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const val = e.target.value;
onChange(val);
setShowSuggestions(true);
if (timeoutRef.current) clearTimeout(timeoutRef.current);
timeoutRef.current = setTimeout(() => fetchSuggestions(val), 300);
};
const handleSelect = (address: string) => {
onSelect(address);
setShowSuggestions(false);
setSuggestions([]);
};
return (
<div className="relative">
<Input
value={value}
onChange={handleInputChange}
placeholder={placeholder || "Start typing address..."}
autoComplete="off"
onFocus={() => value && setShowSuggestions(true)}
onBlur={() => setTimeout(() => setShowSuggestions(false), 150)}
/>
{showSuggestions && suggestions.length > 0 && (
<Card className="absolute z-10 mt-1 w-full max-h-60 overflow-auto shadow-lg border bg-white">
<ul>
{suggestions.map((s, i) => (
<li
key={s.place_id}
className="px-4 py-2 cursor-pointer hover:bg-muted text-sm"
onMouseDown={() => handleSelect(s.display_name)}
>
{s.display_name}
</li>
))}
</ul>
</Card>
)}
</div>
);
}
+27
View File
@@ -0,0 +1,27 @@
import Image from "next/image";
import { cn } from "~/lib/utils";
interface LogoProps {
className?: string;
size?: "sm" | "md" | "lg";
}
export function Logo({ className, size = "md" }: LogoProps) {
const sizeClasses = {
sm: { width: 120, height: 32 },
md: { width: 160, height: 42 },
lg: { width: 240, height: 64 },
};
const { width, height } = sizeClasses[size];
return (
<Image
src="/beenvoice-logo.svg"
alt="beenvoice logo"
width={width}
height={height}
className={className}
priority
/>
);
}