Add Chromebook compatibility fixes and optimizations

Implemented comprehensive Chromebook-specific fixes including viewport adjustments, enhanced touch and keyboard detection, improved scrolling and keyboard navigation hooks, and extensive CSS optimizations for better usability. Updated chat and dashboard interfaces for larger touch targets, better focus management, and responsive layouts. Added documentation in docs/CHROMEBOOK-FIXES.md and new hooks for Chromebook scroll and keyboard handling.
This commit is contained in:
NotII
2025-10-26 18:29:23 +00:00
parent 1fc29e6cbf
commit 130ecac208
27 changed files with 691 additions and 65 deletions

View File

@@ -240,7 +240,7 @@ export default function AdminAnalytics() {
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
{/* Orders Card */}
<Card>
<CardHeader className="pb-2">

View File

@@ -196,7 +196,7 @@ export default function AnalyticsDashboard({ initialData }: AnalyticsDashboardPr
</div>
{/* Key Metrics Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4 lg:gap-6">
{isLoading ? (
[...Array(4)].map((_, i) => (
<MetricsCardSkeleton key={i} />
@@ -272,7 +272,7 @@ export default function AnalyticsDashboard({ initialData }: AnalyticsDashboardPr
{/* Analytics Tabs */}
<div className="space-y-6">
<Tabs defaultValue="revenue" className="space-y-6">
<TabsList className="grid w-full grid-cols-5">
<TabsList className="grid w-full grid-cols-2 sm:grid-cols-3 lg:grid-cols-5">
<TabsTrigger value="revenue" className="flex items-center gap-2">
<TrendingUp className="h-4 w-4" />
Revenue

View File

@@ -14,7 +14,7 @@ export default function AnalyticsDashboardSkeleton() {
return (
<div className="space-y-6">
{/* Key Metrics Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4 lg:gap-6">
{[...Array(4)].map((_, i) => (
<MetricsCardSkeleton key={i} />
))}

View File

@@ -77,7 +77,7 @@ export default function ProfitAnalyticsChart({ timeRange, hideNumbers = false }:
<CardContent>
<div className="space-y-6">
{/* Summary Cards Skeleton */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
{[...Array(4)].map((_, i) => (
<Card key={i}>
<CardHeader className="pb-2">
@@ -197,7 +197,7 @@ export default function ProfitAnalyticsChart({ timeRange, hideNumbers = false }:
return (
<div className="space-y-6">
{/* Summary Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium">Revenue (Tracked)</CardTitle>

View File

@@ -15,6 +15,8 @@ import { getCookie, clientFetch } from "@/lib/api";
import { ImageViewerModal } from "@/components/modals/image-viewer-modal";
import BuyerOrderInfo from "./BuyerOrderInfo";
import { useIsTouchDevice } from "@/hooks/use-mobile";
import { useChromebookScroll, useSmoothScrollToBottom } from "@/hooks/use-chromebook-scroll";
import { useChromebookKeyboard, useChatFocus } from "@/hooks/use-chromebook-keyboard";
interface Message {
_id: string;
@@ -100,10 +102,18 @@ export default function ChatDetail({ chatId }: { chatId: string }) {
const [selectedAttachmentIndex, setSelectedAttachmentIndex] = useState<number | null>(null);
const seenMessageIdsRef = useRef<Set<string>>(new Set());
const isTouchDevice = useIsTouchDevice();
const scrollContainerRef = useChromebookScroll();
const { scrollToBottom, scrollToBottomInstant } = useSmoothScrollToBottom();
useChromebookKeyboard();
const { focusMessageInput, focusNextMessage, focusPreviousMessage } = useChatFocus();
// Scroll to bottom utility functions
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
const scrollToBottomHandler = () => {
if (scrollContainerRef.current) {
scrollToBottom(scrollContainerRef.current);
} else {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
}
};
const isNearBottom = () => {
@@ -262,7 +272,7 @@ export default function ChatDetail({ chatId }: { chatId: string }) {
// Scroll to bottom on initial load
setTimeout(() => {
scrollToBottom();
scrollToBottomHandler();
}, 100);
} catch (error) {
console.error("Error fetching chat data:", error);
@@ -363,13 +373,33 @@ export default function ChatDetail({ chatId }: { chatId: string }) {
} else if (e.key === 'Escape') {
// Clear the input on Escape
setMessage('');
focusMessageInput();
} else if (e.key === 'ArrowUp' && message === '') {
// Load previous message on Arrow Up when input is empty
e.preventDefault();
const lastVendorMessage = [...messages].reverse().find(msg => msg.sender === 'vendor');
if (lastVendorMessage) {
setMessage(lastVendorMessage.content);
} else {
focusPreviousMessage();
}
} else if (e.key === 'ArrowDown' && message === '') {
// Focus next message
e.preventDefault();
focusNextMessage();
} else if (e.key === 'Tab') {
// Enhanced tab navigation for Chromebooks
e.preventDefault();
const focusableElements = document.querySelectorAll(
'button:not([disabled]), input:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
) as NodeListOf<HTMLElement>;
const currentIndex = Array.from(focusableElements).indexOf(document.activeElement as HTMLElement);
const nextIndex = e.shiftKey
? (currentIndex > 0 ? currentIndex - 1 : focusableElements.length - 1)
: (currentIndex < focusableElements.length - 1 ? currentIndex + 1 : 0);
focusableElements[nextIndex]?.focus();
}
};
@@ -601,11 +631,19 @@ export default function ChatDetail({ chatId }: { chatId: string }) {
</div>
<div
className="flex-1 overflow-y-auto p-2 space-y-2 pb-[80px]"
ref={scrollContainerRef}
className={cn(
"flex-1 overflow-y-auto space-y-2 pb-[80px]",
isTouchDevice ? "p-3" : "p-2"
)}
role="log"
aria-label="Chat messages"
aria-live="polite"
aria-atomic="false"
style={{
WebkitOverflowScrolling: 'touch',
overscrollBehavior: 'contain'
}}
>
{chat.messages.length === 0 ? (
<div className="h-full flex items-center justify-center">
@@ -624,7 +662,8 @@ export default function ChatDetail({ chatId }: { chatId: string }) {
>
<div
className={cn(
"max-w-[90%] rounded-lg p-3",
"max-w-[90%] rounded-lg chat-message",
isTouchDevice ? "p-4" : "p-3",
msg.sender === "vendor"
? "bg-primary text-primary-foreground"
: "bg-muted"
@@ -738,8 +777,8 @@ export default function ChatDetail({ chatId }: { chatId: string }) {
placeholder="Type your message..."
disabled={sending}
className={cn(
"flex-1 text-base transition-all duration-200",
isTouchDevice ? "min-h-[48px] text-lg" : "min-h-[44px]"
"flex-1 text-base transition-all duration-200 form-input",
isTouchDevice ? "min-h-[52px] text-lg" : "min-h-[48px]"
)}
onKeyDown={handleKeyDown}
autoFocus
@@ -749,15 +788,23 @@ export default function ChatDetail({ chatId }: { chatId: string }) {
autoComplete="off"
spellCheck="true"
maxLength={2000}
style={{
WebkitAppearance: 'none',
borderRadius: '0.5rem'
}}
/>
<Button
type="submit"
disabled={sending || !message.trim()}
aria-label={sending ? "Sending message" : "Send message"}
className={cn(
"transition-all duration-200",
isTouchDevice ? "min-h-[48px] min-w-[48px]" : "min-h-[44px] min-w-[44px]"
"transition-all duration-200 btn-chromebook",
isTouchDevice ? "min-h-[52px] min-w-[52px]" : "min-h-[48px] min-w-[48px]"
)}
style={{
WebkitAppearance: 'none',
touchAction: 'manipulation'
}}
>
{sending ? <RefreshCw className="h-4 w-4 animate-spin" /> : <Send className="h-4 w-4" />}
</Button>

View File

@@ -80,7 +80,7 @@ export default function Content({ username, orderStats }: ContentProps) {
</div>
{/* Order Statistics */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4 lg:gap-6">
{statsConfig.map((stat) => (
<OrderStats
key={stat.title}

View File

@@ -57,7 +57,7 @@ export default function PageLoading({
)}
{layout === 'grid' && (
<div className="p-6 grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4 opacity-30">
<div className="p-6 grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4 opacity-30">
{[...Array(itemsCount)].map((_, i) => (
<Card key={i} className="p-4">
<Skeleton className="h-32 w-full rounded-md mb-3" />

View File

@@ -60,7 +60,7 @@ const Sidebar: React.FC = () => {
<nav
className={`
fixed inset-y-0 left-0 z-[70] w-64 bg-background transform transition-transform duration-200 ease-in-out
lg:translate-x-0 lg:static lg:w-64 border-r border-border
lg:translate-x-0 lg:static lg:w-56 xl:w-64 border-r border-border
${isMobileMenuOpen ? "translate-x-0" : "-translate-x-full"}
`}
>

View File

@@ -169,7 +169,7 @@ export const ProductModal: React.FC<ProductModalProps> = ({
</DialogTitle>
</DialogHeader>
<div className="grid grid-cols-1 lg:grid-cols-[2fr_1fr] gap-8 py-4">
<div className="grid grid-cols-1 xl:grid-cols-[2fr_1fr] gap-6 lg:gap-8 py-4">
<ProductBasicInfo
productData={productData}
handleChange={handleChange}

View File

@@ -372,8 +372,8 @@ export default function OrderTable() {
<div className="border border-zinc-800 rounded-md bg-black/40 overflow-hidden">
{/* Filters header */}
<div className="p-4 border-b border-zinc-800 bg-black/60">
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
<div className="flex flex-col sm:flex-row gap-2 sm:items-center">
<div className="flex flex-col lg:flex-row justify-between items-start lg:items-center gap-4">
<div className="flex flex-col sm:flex-row gap-2 sm:items-center w-full lg:w-auto">
<StatusFilter
currentStatus={statusFilter}
onChange={setStatusFilter}
@@ -386,7 +386,7 @@ export default function OrderTable() {
/>
</div>
<div className="flex items-center gap-2 self-end sm:self-auto">
<div className="flex items-center gap-2 self-end lg:self-auto">
<AlertDialog>
<AlertDialogTrigger asChild>
<Button disabled={selectedOrders.size === 0 || isShipping}>
@@ -441,17 +441,17 @@ export default function OrderTable() {
<TableHead className="cursor-pointer" onClick={() => handleSort("totalPrice")}>
Total <ArrowUpDown className="ml-2 inline h-4 w-4" />
</TableHead>
<TableHead>Promotion</TableHead>
<TableHead className="hidden lg:table-cell">Promotion</TableHead>
<TableHead className="cursor-pointer" onClick={() => handleSort("status")}>
Status <ArrowUpDown className="ml-2 inline h-4 w-4" />
</TableHead>
<TableHead className="cursor-pointer" onClick={() => handleSort("orderDate")}>
<TableHead className="hidden md:table-cell cursor-pointer" onClick={() => handleSort("orderDate")}>
Order Date <ArrowUpDown className="ml-2 inline h-4 w-4" />
</TableHead>
<TableHead className="cursor-pointer" onClick={() => handleSort("paidAt")}>
<TableHead className="hidden xl:table-cell cursor-pointer" onClick={() => handleSort("paidAt")}>
Paid At <ArrowUpDown className="ml-2 inline h-4 w-4" />
</TableHead>
<TableHead>Buyer</TableHead>
<TableHead className="hidden lg:table-cell">Buyer</TableHead>
<TableHead className="w-24 text-center">Actions</TableHead>
</TableRow>
</TableHeader>
@@ -480,7 +480,7 @@ export default function OrderTable() {
)}
</div>
</TableCell>
<TableCell>
<TableCell className="hidden lg:table-cell">
{order.promotionCode ? (
<div className="flex flex-col gap-1">
<div className="flex items-center gap-1">
@@ -521,7 +521,7 @@ export default function OrderTable() {
)}
</div>
</TableCell>
<TableCell>
<TableCell className="hidden md:table-cell">
{new Date(order.orderDate).toLocaleDateString("en-GB", {
day: '2-digit',
month: 'short',
@@ -531,7 +531,7 @@ export default function OrderTable() {
hour12: false
})}
</TableCell>
<TableCell>
<TableCell className="hidden xl:table-cell">
{order.paidAt ? new Date(order.paidAt).toLocaleDateString("en-GB", {
day: '2-digit',
month: 'short',
@@ -541,7 +541,7 @@ export default function OrderTable() {
hour12: false
}) : "-"}
</TableCell>
<TableCell>
<TableCell className="hidden lg:table-cell">
{order.telegramUsername ? `@${order.telegramUsername}` : "-"}
</TableCell>
<TableCell className="text-center">

View File

@@ -49,10 +49,10 @@ const ProductTable = ({
<TableHeader className="bg-gray-50 dark:bg-zinc-800/50">
<TableRow className="hover:bg-transparent">
<TableHead className="w-[200px]">Product</TableHead>
<TableHead className="text-center">Category</TableHead>
<TableHead className="text-center">Unit</TableHead>
<TableHead className="hidden sm:table-cell text-center">Category</TableHead>
<TableHead className="hidden md:table-cell text-center">Unit</TableHead>
<TableHead className="text-center">Stock</TableHead>
<TableHead className="text-center">Enabled</TableHead>
<TableHead className="hidden lg:table-cell text-center">Enabled</TableHead>
<TableHead className="text-right">Actions</TableHead>
</TableRow>
</TableHeader>
@@ -73,9 +73,12 @@ const ProductTable = ({
<TableRow key={product._id} className="transition-colors hover:bg-gray-50 dark:hover:bg-zinc-800/70">
<TableCell>
<div className="font-medium truncate max-w-[180px]">{product.name}</div>
<div className="hidden sm:block text-sm text-muted-foreground mt-1">
{getCategoryNameById(product.category)}
</div>
</TableCell>
<TableCell className="text-center">{getCategoryNameById(product.category)}</TableCell>
<TableCell className="text-center">{product.unitType}</TableCell>
<TableCell className="hidden sm:table-cell text-center">{getCategoryNameById(product.category)}</TableCell>
<TableCell className="hidden md:table-cell text-center">{product.unitType}</TableCell>
<TableCell className="text-center">
{product.stockTracking ? (
<div className="flex items-center justify-center gap-1">
@@ -88,7 +91,7 @@ const ProductTable = ({
<Badge variant="outline" className="text-xs">Not Tracked</Badge>
)}
</TableCell>
<TableCell className="text-center">
<TableCell className="hidden lg:table-cell text-center">
<Switch
checked={product.enabled !== false}
onCheckedChange={(checked) => onToggleEnabled(product._id as string, checked)}