PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.16
Yatra – Travel Booking & Tour Operator Software v3.0.16
3.0.16 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 All 84 releases
yatra / resources / js / components / ui / module-skeleton.tsx

module-skeleton.tsx in Yatra – Travel Booking & Tour Operator Software 3.0.16, at resources/js/components/ui/module-skeleton.tsx

264 lines 8.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Skeleton primitives for Yatra Pro module pages.
3 *
4 * Replaces the generic centered <Loader2 className="animate-spin" />
5 * pattern that all six Growth/Agency module pages used while their
6 * meta endpoint was in-flight. A spinning icon over an empty page
7 * gives no sense of what's about to render — a layout-shaped
8 * skeleton lets the operator anticipate the page and feels faster
9 * even when the actual fetch time is unchanged.
10 *
11 * Each export below maps to a real shape used by one or more of
12 * the module pages (page meta gate, tabbed content, settings form,
13 * stat-grid section). Reuse aggressively — every module page
14 * follows the same PageHeader + intro Card + content body shape.
15 */
16
17 import React from "react";
18 import { Card, CardContent, CardHeader } from "./card";
19
20 const Bar: React.FC<{ className?: string }> = ({ className = "" }) => (
21 <div
22 className={`animate-pulse rounded bg-gray-200 dark:bg-gray-700 ${className}`}
23 />
24 );
25
26 /**
27 * Page-level skeleton for the initial meta/license check on every
28 * Pro module page. Mirrors the universal Header + intro Alert +
29 * content area shape so the page doesn't visually "jump" once
30 * data arrives.
31 */
32 export const ModulePageSkeleton: React.FC<{
33 /** Variant of the content body skeleton to render. */
34 variant?: "tabs" | "form" | "list";
35 }> = ({ variant = "tabs" }) => {
36 return (
37 <div className="space-y-4">
38 {/* Page header (title + subtitle) */}
39 <div className="flex items-start justify-between gap-4">
40 <div className="space-y-2 flex-1">
41 <Bar className="h-7 w-48" />
42 <Bar className="h-4 w-96 max-w-full" />
43 </div>
44 <div className="flex gap-2">
45 <Bar className="h-9 w-28" />
46 <Bar className="h-9 w-24" />
47 </div>
48 </div>
49
50 {/* Intro/about info card — every Pro module renders one of these
51 above the main content (tier badge, description, doc link). */}
52 <Card>
53 <CardContent className="p-4 space-y-2">
54 <div className="flex items-center gap-2">
55 <Bar className="h-5 w-5 rounded-full" />
56 <Bar className="h-4 w-40" />
57 </div>
58 <Bar className="h-3 w-full max-w-2xl" />
59 <Bar className="h-3 w-3/4 max-w-xl" />
60 </CardContent>
61 </Card>
62
63 {variant === "tabs" && <ModuleTabsSkeleton />}
64 {variant === "form" && <ModuleFormSkeleton />}
65 {variant === "list" && <ModuleListSkeleton />}
66 </div>
67 );
68 };
69
70 /**
71 * Tabbed content card with a faux tab strip + 5-row table preview.
72 * Matches Webhooks, Channel Manager, Team, WhatsApp layouts.
73 */
74 export const ModuleTabsSkeleton: React.FC<{
75 /** Number of tabs to render in the faux strip. */
76 tabCount?: number;
77 /** Number of rows in the faux table body. */
78 rows?: number;
79 /** Number of columns in the faux table body. */
80 columns?: number;
81 }> = ({ tabCount = 4, rows = 5, columns = 5 }) => {
82 return (
83 <Card>
84 {/* Tab strip */}
85 <CardHeader className="border-b border-gray-200 dark:border-gray-700 pb-0">
86 <div className="flex items-center gap-1 -mb-px">
87 {Array.from({ length: tabCount }).map((_, i) => (
88 <div
89 key={i}
90 className={`px-4 py-2.5 ${
91 i === 0 ? "border-b-2 border-gray-300 dark:border-gray-600" : ""
92 }`}
93 >
94 <Bar className="h-4 w-20" />
95 </div>
96 ))}
97 </div>
98 </CardHeader>
99 <CardContent className="p-4">
100 <ModuleTableSkeleton rows={rows} columns={columns} />
101 </CardContent>
102 </Card>
103 );
104 };
105
106 /**
107 * Table-shaped skeleton with header row + body rows. Drop-in
108 * replacement for the centered spinners that previously sat inside
109 * tab content panels.
110 */
111 export const ModuleTableSkeleton: React.FC<{
112 rows?: number;
113 columns?: number;
114 }> = ({ rows = 5, columns = 5 }) => {
115 const cols = Math.max(1, columns);
116 return (
117 <div className="space-y-3 animate-pulse">
118 {/* Toolbar row (search + filters that most module tabs have) */}
119 <div className="flex items-center gap-2 pb-2">
120 <Bar className="h-9 w-64" />
121 <Bar className="h-9 w-32" />
122 <div className="flex-1" />
123 <Bar className="h-9 w-28" />
124 </div>
125
126 {/* Header */}
127 <div
128 className="grid gap-3 border-b border-gray-200 dark:border-gray-700 pb-2"
129 style={{ gridTemplateColumns: `repeat(${cols}, minmax(0, 1fr))` }}
130 >
131 {Array.from({ length: cols }).map((_, i) => (
132 <Bar key={i} className="h-3.5" />
133 ))}
134 </div>
135
136 {/* Rows */}
137 {Array.from({ length: rows }).map((_, rowIndex) => (
138 <div
139 key={rowIndex}
140 className="grid gap-3 py-2"
141 style={{ gridTemplateColumns: `repeat(${cols}, minmax(0, 1fr))` }}
142 >
143 {Array.from({ length: cols }).map((_, colIndex) => (
144 <Bar
145 key={colIndex}
146 className={`h-4 ${
147 colIndex === 0
148 ? "w-3/4"
149 : colIndex === cols - 1
150 ? "w-1/2"
151 : "w-full"
152 }`}
153 />
154 ))}
155 </div>
156 ))}
157 </div>
158 );
159 };
160
161 /**
162 * Settings-form-shaped skeleton: label/input pairs in a single
163 * column. Used for AI Assistant key form, WhatsApp credentials,
164 * White Label settings.
165 */
166 export const ModuleFormSkeleton: React.FC<{
167 /** Number of label+input rows. */
168 rows?: number;
169 }> = ({ rows = 6 }) => {
170 return (
171 <Card>
172 <CardContent className="p-6 space-y-5">
173 {Array.from({ length: rows }).map((_, i) => (
174 <div key={i} className="space-y-2">
175 <Bar className="h-3.5 w-32" />
176 <Bar className="h-10 w-full" />
177 <Bar className="h-3 w-72 max-w-full" />
178 </div>
179 ))}
180 <div className="flex justify-end gap-2 pt-2">
181 <Bar className="h-10 w-24" />
182 <Bar className="h-10 w-28" />
183 </div>
184 </CardContent>
185 </Card>
186 );
187 };
188
189 /**
190 * Card-list shape — for tabs that render channels, members, or
191 * other entity cards instead of a strict table (Channel Manager's
192 * Channels tab, Team Members layout in card mode, etc.).
193 */
194 export const ModuleListSkeleton: React.FC<{
195 /** Number of list rows. */
196 rows?: number;
197 }> = ({ rows = 4 }) => {
198 return (
199 <div className="space-y-3 animate-pulse">
200 {Array.from({ length: rows }).map((_, i) => (
201 <Card key={i}>
202 <CardContent className="p-4 flex items-center gap-4">
203 <Bar className="h-10 w-10 rounded-full flex-shrink-0" />
204 <div className="flex-1 space-y-2 min-w-0">
205 <Bar className="h-4 w-1/3" />
206 <Bar className="h-3 w-2/3 max-w-md" />
207 </div>
208 <div className="flex items-center gap-2 flex-shrink-0">
209 <Bar className="h-6 w-16 rounded-full" />
210 <Bar className="h-9 w-9" />
211 <Bar className="h-9 w-9" />
212 </div>
213 </CardContent>
214 </Card>
215 ))}
216 </div>
217 );
218 };
219
220 /**
221 * Stat-grid skeleton — for analytics/usage sections (AI Assistant
222 * usage tab, WhatsApp templates count, etc.). 4 stat tiles in a
223 * responsive grid.
224 */
225 export const ModuleStatGridSkeleton: React.FC<{
226 /** Number of stat tiles. */
227 tiles?: number;
228 }> = ({ tiles = 4 }) => {
229 return (
230 <div className="grid grid-cols-2 lg:grid-cols-4 gap-3 animate-pulse">
231 {Array.from({ length: tiles }).map((_, i) => (
232 <Card key={i}>
233 <CardContent className="p-4 space-y-2">
234 <Bar className="h-3 w-20" />
235 <Bar className="h-7 w-24" />
236 <Bar className="h-3 w-32 max-w-full" />
237 </CardContent>
238 </Card>
239 ))}
240 </div>
241 );
242 };
243
244 /**
245 * Compact section-content skeleton — for in-card content loads
246 * where the surrounding card chrome is already rendered (e.g.
247 * Webhooks certificate section, AI Assistant prompt detail panel).
248 */
249 export const ModuleSectionSkeleton: React.FC<{
250 /** Number of label/value lines. */
251 lines?: number;
252 }> = ({ lines = 4 }) => {
253 return (
254 <div className="space-y-3 animate-pulse">
255 {Array.from({ length: lines }).map((_, i) => (
256 <div key={i} className="space-y-1.5">
257 <Bar className="h-3 w-28" />
258 <Bar className="h-9 w-full" />
259 </div>
260 ))}
261 </div>
262 );
263 };
264