wulf-pulse/components/kiosk/ticker.tsx
lorentz 3c13defacb feat: kiosk UI updates - new cards, gauge chart, performance improvements
- Updated kiosk stats API with expanded metrics
- New components: company-tickets-card, gauge-chart, service-desk-card, ticket-leaders-card
- Updated cycling-display, kpi-card, and ticker components
- Added performance.css for kiosk optimizations
- Added Wulf logo asset
2026-02-19 15:31:23 -05:00

139 lines
4.5 KiB
TypeScript

'use client';
import { useState, useEffect, useRef } from 'react';
interface TickerActivity {
ticketNumber: string;
title: string;
priority: number;
statusLabel: string;
companyName: string;
}
interface TickerProps {
activities: TickerActivity[];
speed: number;
}
export function Ticker({ activities, speed }: TickerProps) {
const [isPaused, setIsPaused] = useState(false);
const contentRef = useRef<HTMLDivElement>(null);
const animationRef = useRef<number | null>(null);
const positionRef = useRef(0);
const lastTimeRef = useRef<number>(0);
// Simple requestAnimationFrame animation that respects speed
useEffect(() => {
if (!contentRef.current) return;
const animate = (timestamp: number) => {
if (lastTimeRef.current === 0) {
lastTimeRef.current = timestamp;
}
const deltaTime = timestamp - lastTimeRef.current;
lastTimeRef.current = timestamp;
if (!isPaused && contentRef.current) {
// Move based on speed - higher speed = faster movement
// Speed 10 = 20px/s, Speed 60 = 120px/s, Speed 100 = 200px/s
const pixelsPerSecond = speed * 2;
positionRef.current -= (pixelsPerSecond * deltaTime) / 1000;
// Get the width of the first set
const contentWidth = contentRef.current.scrollWidth / 2;
// Reset when we've scrolled past the first set
if (Math.abs(positionRef.current) >= contentWidth) {
positionRef.current = 0;
}
contentRef.current.style.transform = `translateX(${positionRef.current}px)`;
}
animationRef.current = requestAnimationFrame(animate);
};
animationRef.current = requestAnimationFrame(animate);
return () => {
if (animationRef.current) {
cancelAnimationFrame(animationRef.current);
}
};
}, [speed, isPaused]);
const getPriorityColor = (priority: number) => {
if (priority <= 3) return 'text-red-500';
if (priority <= 5) return 'text-orange-500';
return 'text-blue-500';
};
const getPriorityIcon = (priority: number) => {
if (priority <= 3) return '🔴';
if (priority <= 5) return '🟠';
return '🔵';
};
const getStatusColor = (statusLabel: string) => {
const status = statusLabel?.toLowerCase() || '';
if (status.includes('new') || status.includes('resource requested') || status.includes('end user note added')) {
return 'text-red-500 font-bold';
}
if (status.includes('in progress')) {
return 'text-green-500 font-bold';
}
return 'text-gray-500';
};
if (activities.length === 0) {
return (
<div className="fixed bottom-0 left-0 right-0 bg-black border-t-4 border-blue-600 h-24 flex items-center justify-center">
<span className="text-gray-500">No recent ticket activity</span>
</div>
);
}
// Limit to 20 items for better performance on low-end hardware
const limitedActivities = activities.slice(0, 20);
return (
<div
className="fixed bottom-0 left-0 right-0 bg-black border-t-4 border-blue-600 h-24 overflow-hidden"
onMouseEnter={() => setIsPaused(true)}
onMouseLeave={() => setIsPaused(false)}
>
<div
ref={contentRef}
className="flex items-center h-full whitespace-nowrap"
>
{/* Duplicate content for seamless loop */}
{[...limitedActivities, ...limitedActivities].map((activity, index) => (
<div
key={`${activity.ticketNumber}-${index}`}
className="inline-flex items-center mx-8 flex-shrink-0"
>
<div className="flex flex-col gap-1">
{/* Line 1: Priority - Ticket Number - Company Name */}
<div className="flex items-center text-xl">
<span className="mr-2 text-2xl">{getPriorityIcon(activity.priority)}</span>
<span className={`font-bold mr-2 ${getPriorityColor(activity.priority)}`}>
{activity.ticketNumber}
</span>
<span className="text-white mr-2">-</span>
<span className="text-white font-semibold">
{activity.companyName}
</span>
</div>
{/* Line 2: Title - Status */}
<div className="flex items-center text-base">
<span className="text-gray-400">{activity.title?.substring(0, 60) || ''}</span>
<span className={`ml-2 ${getStatusColor(activity.statusLabel)}`}>- {activity.statusLabel || 'Unknown'}</span>
</div>
</div>
</div>
))}
</div>
</div>
);
}