fix: react-doctor 72→73 (skeletons restructured to divs, video tabindex)
This commit is contained in:
@@ -413,7 +413,7 @@ export default function BillingClientPage({ overview }: Props) {
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
<tr className="border-t-2 border-[var(--admin-border)] bg-[var(--admin-bg-subtle)]">
|
<tr className="border-t-2 border-[var(--admin-border)] bg-[var(--admin-bg-subtle)]">
|
||||||
<td className="pt-4 pr-6" aria-hidden="true" />
|
<td className="pt-4 pr-6" aria-label="Feature name" />
|
||||||
{(["starter", "farm", "enterprise"] as const).map((tier) => {
|
{(["starter", "farm", "enterprise"] as const).map((tier) => {
|
||||||
const isCurrent = tier === planTier;
|
const isCurrent = tier === planTier;
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -94,33 +94,21 @@ function EmptyState({ hasFilters }: { hasFilters: boolean }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Loading skeleton
|
// Loading skeleton (rendered as a div so it doesn't get parsed as table rows)
|
||||||
function ContactSkeleton() {
|
function ContactSkeleton() {
|
||||||
return (
|
return (
|
||||||
<>
|
<div role="status" aria-live="polite" aria-busy="true" aria-label="Loading contacts" className="overflow-hidden rounded-xl border border-[var(--admin-border)] bg-white">
|
||||||
{[1, 2, 3, 4, 5].map((i) => (
|
{[1, 2, 3, 4, 5].map((i) => (
|
||||||
<tr key={i} className="border-b border-[var(--admin-border)]" aria-hidden="true">
|
<div key={i} className="flex items-center gap-4 border-b border-[var(--admin-border)] px-4 py-3 last:border-b-0">
|
||||||
<td className="px-4 py-3" aria-hidden="true">
|
<div className="h-4 w-24 bg-stone-200 rounded animate-pulse" />
|
||||||
<div className="h-4 w-24 bg-stone-200 rounded animate-pulse" aria-hidden="true" />
|
<div className="h-4 w-32 bg-stone-200 rounded animate-pulse" />
|
||||||
</td>
|
<div className="h-4 w-20 bg-stone-200 rounded animate-pulse" />
|
||||||
<td className="px-4 py-3" aria-hidden="true">
|
<div className="h-5 w-16 bg-stone-200 rounded-full animate-pulse" />
|
||||||
<div className="h-4 w-32 bg-stone-200 rounded animate-pulse" aria-hidden="true" />
|
<div className="h-4 w-16 bg-stone-200 rounded animate-pulse" />
|
||||||
</td>
|
<div className="h-4 w-12 bg-stone-200 rounded animate-pulse" />
|
||||||
<td className="px-4 py-3" aria-hidden="true">
|
</div>
|
||||||
<div className="h-4 w-20 bg-stone-200 rounded animate-pulse" aria-hidden="true" />
|
|
||||||
</td>
|
|
||||||
<td className="px-4 py-3" aria-hidden="true">
|
|
||||||
<div className="h-5 w-16 bg-stone-200 rounded-full animate-pulse" aria-hidden="true" />
|
|
||||||
</td>
|
|
||||||
<td className="px-4 py-3" aria-hidden="true">
|
|
||||||
<div className="h-4 w-16 bg-stone-200 rounded animate-pulse" aria-hidden="true" />
|
|
||||||
</td>
|
|
||||||
<td className="px-4 py-3" aria-hidden="true">
|
|
||||||
<div className="h-4 w-12 bg-stone-200 rounded animate-pulse" aria-hidden="true" />
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
))}
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -269,24 +257,7 @@ export default function ContactListPanel({
|
|||||||
|
|
||||||
{/* Table */}
|
{/* Table */}
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="overflow-hidden rounded-xl border border-[var(--admin-border)]">
|
<ContactSkeleton />
|
||||||
<table className="w-full text-sm">
|
|
||||||
<thead className="bg-stone-50 border-b border-[var(--admin-border)]">
|
|
||||||
<tr>
|
|
||||||
<th className="text-left px-4 py-3 font-semibold text-stone-500 text-xs uppercase tracking-wider">Name</th>
|
|
||||||
<th className="text-left px-4 py-3 font-semibold text-stone-500 text-xs uppercase tracking-wider">Email</th>
|
|
||||||
<th className="text-left px-4 py-3 font-semibold text-stone-500 text-xs uppercase tracking-wider">Phone</th>
|
|
||||||
<th className="text-left px-4 py-3 font-semibold text-stone-500 text-xs uppercase tracking-wider">Source</th>
|
|
||||||
<th className="text-left px-4 py-3 font-semibold text-stone-500 text-xs uppercase tracking-wider">Email Opt</th>
|
|
||||||
<th className="text-left px-4 py-3 font-semibold text-stone-500 text-xs uppercase tracking-wider">Unsub</th>
|
|
||||||
<th className="text-right px-4 py-3" aria-label="Actions"></th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody className="bg-white">
|
|
||||||
<ContactSkeleton />
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
) : contacts.length === 0 ? (
|
) : contacts.length === 0 ? (
|
||||||
<div className="overflow-hidden rounded-xl border border-[var(--admin-border)]">
|
<div className="overflow-hidden rounded-xl border border-[var(--admin-border)]">
|
||||||
<EmptyState hasFilters={hasFilters} />
|
<EmptyState hasFilters={hasFilters} />
|
||||||
@@ -333,14 +304,14 @@ export default function ContactListPanel({
|
|||||||
<td className="px-4 py-3.5">
|
<td className="px-4 py-3.5">
|
||||||
{c.email_opt_in ? (
|
{c.email_opt_in ? (
|
||||||
<span className="inline-flex items-center gap-1 text-emerald-600 text-xs font-semibold">
|
<span className="inline-flex items-center gap-1 text-emerald-600 text-xs font-semibold">
|
||||||
<svg aria-hidden="true" className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="2">
|
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="2">
|
||||||
<polyline points="20 6 9 17 4 12"/>
|
<polyline points="20 6 9 17 4 12"/>
|
||||||
</svg>
|
</svg>
|
||||||
Opted in
|
Opted in
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<span className="inline-flex items-center gap-1 text-red-500 text-xs font-semibold">
|
<span className="inline-flex items-center gap-1 text-red-500 text-xs font-semibold">
|
||||||
<svg aria-hidden="true" className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="2">
|
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="2">
|
||||||
<line x1="18" y1="6" x2="6" y2="18"/>
|
<line x1="18" y1="6" x2="6" y2="18"/>
|
||||||
<line x1="6" y1="6" x2="18" y2="18"/>
|
<line x1="6" y1="6" x2="18" y2="18"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
@@ -124,33 +124,21 @@ function LogsEmptyState({ hasFilters }: { hasFilters: boolean }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Loading skeleton
|
// Loading skeleton (rendered as a div so it doesn't get parsed as table rows)
|
||||||
function LogSkeleton() {
|
function LogSkeleton() {
|
||||||
return (
|
return (
|
||||||
<>
|
<div role="status" aria-live="polite" aria-busy="true" aria-label="Loading messages" className="overflow-hidden rounded-xl border border-[var(--admin-border)] bg-white">
|
||||||
{[1, 2, 3, 4, 5].map((i) => (
|
{[1, 2, 3, 4, 5].map((i) => (
|
||||||
<tr key={i} className="border-b border-[var(--admin-border)]" aria-hidden="true">
|
<div key={i} className="flex items-center gap-4 border-b border-[var(--admin-border)] px-4 py-3 last:border-b-0">
|
||||||
<td className="px-4 py-3" aria-hidden="true">
|
<div className="h-3 w-20 bg-stone-200 rounded animate-pulse" />
|
||||||
<div className="h-3 w-20 bg-stone-200 rounded animate-pulse" aria-hidden="true" />
|
<div className="h-3 w-32 bg-stone-200 rounded animate-pulse" />
|
||||||
</td>
|
<div className="h-5 w-14 bg-stone-200 rounded-full animate-pulse" />
|
||||||
<td className="px-4 py-3" aria-hidden="true">
|
<div className="h-3 w-40 bg-stone-200 rounded animate-pulse" />
|
||||||
<div className="h-3 w-32 bg-stone-200 rounded animate-pulse" aria-hidden="true" />
|
<div className="h-5 w-20 bg-stone-200 rounded-full animate-pulse" />
|
||||||
</td>
|
<div className="h-5 w-24 bg-stone-200 rounded-full animate-pulse" />
|
||||||
<td className="px-4 py-3" aria-hidden="true">
|
</div>
|
||||||
<div className="h-5 w-14 bg-stone-200 rounded-full animate-pulse" aria-hidden="true" />
|
|
||||||
</td>
|
|
||||||
<td className="px-4 py-3" aria-hidden="true">
|
|
||||||
<div className="h-3 w-40 bg-stone-200 rounded animate-pulse" aria-hidden="true" />
|
|
||||||
</td>
|
|
||||||
<td className="px-4 py-3" aria-hidden="true">
|
|
||||||
<div className="h-5 w-20 bg-stone-200 rounded-full animate-pulse" aria-hidden="true" />
|
|
||||||
</td>
|
|
||||||
<td className="px-4 py-3" aria-hidden="true">
|
|
||||||
<div className="h-5 w-24 bg-stone-200 rounded-full animate-pulse" aria-hidden="true" />
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
))}
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -386,9 +374,9 @@ export default function MessageLogPanel({ brandId }: { brandId?: string }) {
|
|||||||
<th className="text-left px-4 py-3.5 font-semibold text-stone-500 text-xs uppercase tracking-wider">Engagement</th>
|
<th className="text-left px-4 py-3.5 font-semibold text-stone-500 text-xs uppercase tracking-wider">Engagement</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="bg-white">
|
{isLoading ? (
|
||||||
<LogSkeleton />
|
<tbody className="bg-white" aria-busy="true"><tr><td colSpan={6}><LogSkeleton /></td></tr></tbody>
|
||||||
</tbody>
|
) : null}
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
) : paginatedLogs.length === 0 ? (
|
) : paginatedLogs.length === 0 ? (
|
||||||
|
|||||||
@@ -158,11 +158,12 @@ export default function TuxedoVideoHero({
|
|||||||
{/* Background video with parallax */}
|
{/* Background video with parallax */}
|
||||||
<video
|
<video
|
||||||
ref={videoRef}
|
ref={videoRef}
|
||||||
aria-hidden="true"
|
|
||||||
autoPlay
|
autoPlay
|
||||||
muted
|
muted
|
||||||
loop
|
loop
|
||||||
playsInline
|
playsInline
|
||||||
|
tabIndex={-1}
|
||||||
|
aria-hidden="true"
|
||||||
className="absolute inset-0 w-full h-[120%] object-cover"
|
className="absolute inset-0 w-full h-[120%] object-cover"
|
||||||
style={{ zIndex: 0 }}
|
style={{ zIndex: 0 }}
|
||||||
src={VIDEO_URL}
|
src={VIDEO_URL}
|
||||||
|
|||||||
@@ -731,15 +731,21 @@ export default function WaterAdminClient() {
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{initialLoading ? (
|
{initialLoading ? (
|
||||||
Array.from({ length: 8 }).map((_, i) => (
|
<tr>
|
||||||
<tr key={i} aria-hidden="true">
|
<td colSpan={5} className="py-0">
|
||||||
<td className="py-3" aria-hidden="true"><div className="h-3 w-20 bg-stone-100 rounded animate-pulse" aria-hidden="true" /></td>
|
<div role="status" aria-live="polite" aria-busy="true" aria-label="Loading water log entries" className="divide-y divide-stone-100">
|
||||||
<td className="py-3" aria-hidden="true"><div className="h-3 w-16 bg-stone-100 rounded animate-pulse hidden sm:table-cell" aria-hidden="true" /></td>
|
{Array.from({ length: 8 }).map((_, i) => (
|
||||||
<td className="py-3" aria-hidden="true"><div className="h-3 w-14 bg-stone-100 rounded animate-pulse" aria-hidden="true" /></td>
|
<div key={i} className="flex items-center gap-4 px-4 py-3">
|
||||||
<td className="py-3" aria-hidden="true"><div className="h-3 w-10 bg-stone-100 rounded animate-pulse ml-auto" aria-hidden="true" /></td>
|
<div className="h-3 w-20 bg-stone-100 rounded animate-pulse" />
|
||||||
<td className="py-3" aria-hidden="true"><div className="h-5 w-8 bg-stone-100 rounded animate-pulse ml-auto" aria-hidden="true" /></td>
|
<div className="h-3 w-16 bg-stone-100 rounded animate-pulse hidden sm:block" />
|
||||||
</tr>
|
<div className="h-3 w-14 bg-stone-100 rounded animate-pulse" />
|
||||||
))
|
<div className="h-3 w-10 bg-stone-100 rounded animate-pulse ml-auto" />
|
||||||
|
<div className="h-5 w-8 bg-stone-100 rounded animate-pulse ml-auto" />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
) : filteredEntries.length === 0 ? (
|
) : filteredEntries.length === 0 ? (
|
||||||
<tr><td colSpan={5} className="py-4 text-center text-stone-400">{allEntries.length === 0 ? t.noEntries : "No matches"}</td></tr>
|
<tr><td colSpan={5} className="py-4 text-center text-stone-400">{allEntries.length === 0 ? t.noEntries : "No matches"}</td></tr>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
Reference in New Issue
Block a user