PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.10
Yatra – Travel Booking & Tour Operator Software v3.0.10
3.0.15 3.0.14 3.0.14.1 3.0.14.2 3.0.12 3.0.13 3.0.11 3.0.10 3.0.9 3.0.8 3.0.7 3.0.6 3.0.5 3.0.5.1 3.0.4 3.0.3 3.0.2.9 3.0.2.7 3.0.2.8 3.0.2.6 trunk 1.0.0 2.0.0 2.0.1 2.0.10 All 83 releases
yatra / resources / js / components / common / StatCard.tsx

StatCard.tsx in Yatra – Travel Booking & Tour Operator Software 3.0.10, at resources/js/components/common/StatCard.tsx

142 lines 5.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Stat Card — KPI tile used on Dashboard and Reports pages.
3 *
4 * Two-line content layout: caption (label) on top, value below. A
5 * coloured pill icon sits flush-right. When period-over-period
6 * comparison data is available, a small delta line appears under the
7 * value with up/down arrow + colour-coded percentage and an optional
8 * "vs {period}" caption (previously hard-coded to "vs last month").
9 *
10 * Optional `tooltip` surfaces a `?` icon next to the title; the
11 * tooltip text appears on hover. Used by Dashboard to disambiguate
12 * confusable metrics ("Booked Revenue" vs "Collected Revenue") that
13 * operators routinely mix up.
14 */
15
16 import React from "react";
17 import { LucideIcon } from "lucide-react";
18 import { Tooltip } from "../ui/tooltip";
19
20 interface StatCardProps {
21 title: string;
22 value: string | number;
23 icon: LucideIcon;
24 trend?: {
25 value: number;
26 isPositive: boolean;
27 /** Caption next to the percentage. Defaults to "vs prev. period". */
28 label?: string;
29 };
30 color?: "blue" | "green" | "purple" | "orange" | "red";
31 loading?: boolean;
32 /** Hover-tooltip text. Renders a `?` icon next to the title. */
33 tooltip?: string;
34 /** Click handler — turns the card into a navigable surface. */
35 onClick?: () => void;
36 }
37
38 /**
39 * Stat Card Component — clean SaaS-style KPI tile.
40 */
41 export const StatCard: React.FC<StatCardProps> = ({
42 title,
43 value,
44 icon: Icon,
45 trend,
46 color = "blue",
47 loading = false,
48 tooltip,
49 onClick,
50 }) => {
51 const interactive = !!onClick;
52 return (
53 <div
54 className={`group relative bg-white dark:bg-gray-800 rounded-lg border border-gray-100 dark:border-gray-700/50 p-3 transition-colors ${
55 interactive
56 ? "cursor-pointer hover:border-blue-300 dark:hover:border-blue-600/70"
57 : "hover:border-gray-200 dark:hover:border-gray-600"
58 }`}
59 onClick={onClick}
60 role={interactive ? "button" : undefined}
61 tabIndex={interactive ? 0 : undefined}
62 onKeyDown={
63 interactive
64 ? (e) => {
65 if (e.key === "Enter" || e.key === " ") {
66 e.preventDefault();
67 onClick?.();
68 }
69 }
70 : undefined
71 }
72 >
73 <div className="flex items-start justify-between gap-2">
74 <div className="flex-1 min-w-0">
75 <div className="flex items-center gap-1.5 mb-1">
76 <p className="text-xs text-gray-500 dark:text-gray-400 truncate">
77 {title}
78 </p>
79 {tooltip && (
80 // Real hover-popover (see components/ui/tooltip.tsx). Native
81 // `title=` was previously used here but had a ~700ms delay
82 // and an easy-to-miss OS-default style — operators reported
83 // the `?` icon "didn't show anything". The new tooltip
84 // appears instantly on hover/focus and matches admin theme.
85 <Tooltip content={tooltip} side="top">
86 <span
87 className="inline-flex h-3.5 w-3.5 shrink-0 cursor-help items-center justify-center rounded-full bg-gray-100 text-[10px] font-bold leading-none text-gray-500 transition-colors hover:bg-gray-200 hover:text-gray-700 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600 dark:hover:text-gray-100"
88 aria-label={typeof tooltip === "string" ? tooltip : undefined}
89 role="img"
90 >
91 ?
92 </span>
93 </Tooltip>
94 )}
95 </div>
96 {loading ? (
97 <div className="h-6 w-20 bg-gray-100 dark:bg-gray-700 rounded animate-pulse" />
98 ) : (
99 <p className="text-xl font-semibold tabular-nums text-gray-900 dark:text-white truncate">
100 {value}
101 </p>
102 )}
103 {trend && !loading && (
104 <div className="flex items-center gap-1 mt-2">
105 <span
106 className={`text-xs font-medium tabular-nums ${
107 trend.isPositive
108 ? "text-green-600 dark:text-green-400"
109 : "text-red-600 dark:text-red-400"
110 }`}
111 >
112 {trend.isPositive ? "" : ""}{" "}
113 {Math.abs(trend.value).toFixed(1)}%
114 </span>
115 <span className="text-xs text-gray-400 dark:text-gray-500">
116 {trend.label || "vs prev. period"}
117 </span>
118 </div>
119 )}
120 </div>
121 <div className="p-2.5 rounded-lg bg-gray-50 dark:bg-gray-700/50 shrink-0">
122 <Icon
123 className={`w-4 h-4 ${
124 color === "blue"
125 ? "text-blue-600 dark:text-blue-400"
126 : color === "green"
127 ? "text-green-600 dark:text-green-400"
128 : color === "purple"
129 ? "text-purple-600 dark:text-purple-400"
130 : color === "red"
131 ? "text-red-600 dark:text-red-400"
132 : "text-orange-600 dark:text-orange-400"
133 }`}
134 />
135 </div>
136 </div>
137 </div>
138 );
139 };
140
141 export default StatCard;
142