Polish pass: micro-interactions on admin and storefront pages

Admin design system:
- AdminCard: hover shadow elevation
- AdminTable: enhanced empty state, row hover
- AdminBadge: tracking-wide typography
- AdminButton: active scale press feedback
- AdminFormElements: input transitions
- AdminFilterBar: search input polish
- PageHeader: icon hover scale, chevron separator
- AdminToggle: glow effect, thumb shadow
- AdminEmptyState: icon hover, spacing
- AdminPagination: active solid bg, ellipsis

Storefronts (tuxedo + indian-river-direct):
- Button hover lifts (-translate-y-0.5)
- Shadow elevations on hover
- Button press feedback
- Consistent timing (duration-200)
- Product card image zoom
- FAQ empty state fix
This commit is contained in:
2026-06-02 14:29:27 +00:00
parent 8dee63fef2
commit d031a9783c
17 changed files with 386 additions and 112 deletions
+297
View File
@@ -0,0 +1,297 @@
"use server";
import { getAdminUser } from "@/lib/admin-permissions";
import { svcHeaders } from "@/lib/svc-headers";
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
// ── Types ────────────────────────────────────────────────────────────────────
export type DashboardStats = {
todayOrders: number;
todayRevenue: number;
pendingStops: number;
activeProducts: number;
weeklyOrders: number[];
recentOrders: Array<{
id: string;
customer_name: string;
total: number;
status: string;
created_at: string;
}>;
};
export type DashboardSummary = {
total_revenue: number;
total_orders: number;
active_stops: number;
active_products: number;
};
// ── Helper ────────────────────────────────────────────────────────────────────
async function brandScopedFetch<T>(
endpoint: string,
params?: Record<string, string>
): Promise<T> {
const adminUser = await getAdminUser();
if (!adminUser) throw new Error("Not authenticated");
let url = `${supabaseUrl}/rest/v1${endpoint}`;
if (params) {
const searchParams = new URLSearchParams(params);
url += `?${searchParams.toString()}`;
}
const response = await fetch(url, {
headers: {
...svcHeaders(supabaseKey),
},
});
if (!response.ok) {
const err = await response.text();
throw new Error(`Dashboard fetch failed: ${err}`);
}
return response.json() as Promise<T>;
}
// ── Dashboard Stats Actions ─────────────────────────────────────────────────
export async function getDashboardStats(): Promise<DashboardStats> {
try {
const adminUser = await getAdminUser();
if (!adminUser) throw new Error("Not authenticated");
const brandId = adminUser.brand_id ?? null;
// Get today's date range
const today = new Date();
const startOfDay = new Date(today.getFullYear(), today.getMonth(), today.getDate());
const endOfDay = new Date(startOfDay.getTime() + 24 * 60 * 60 * 1000);
// Get start of week (7 days ago) - kept for potential weekly comparison
const _weekStart = new Date(startOfDay);
_weekStart.setDate(_weekStart.getDate() - 6);
// Fetch today's orders
let todayOrdersQuery = `${supabaseUrl}/rest/v1/orders?select=id,subtotal,status`;
todayOrdersQuery += `&created_at=gte.${startOfDay.toISOString()}`;
todayOrdersQuery += `&created_at=lt.${endOfDay.toISOString()}`;
if (brandId) {
todayOrdersQuery += `&brand_id=eq.${brandId}`;
}
const todayOrdersRes = await fetch(todayOrdersQuery, {
headers: svcHeaders(supabaseKey),
});
const todayOrders = await todayOrdersRes.json();
// Calculate today's revenue and orders
const validOrders = (todayOrders as Array<{subtotal: number; status: string}>)
.filter(o => o.status !== "cancelled");
const todayRevenue = validOrders
.reduce((sum, o) => sum + (o.subtotal || 0), 0);
const todayOrderCount = validOrders.length;
// Fetch pending stops (stops where date >= today and status is scheduled/upcoming)
let stopsQuery = `${supabaseUrl}/rest/v1/stops?select=id`;
stopsQuery += `&date=gte.${startOfDay.toISOString().split("T")[0]}`;
stopsQuery += `&status=eq.scheduled`;
if (brandId) {
stopsQuery += `&brand_id=eq.${brandId}`;
}
stopsQuery += `&limit=100`;
const stopsRes = await fetch(stopsQuery, {
headers: svcHeaders(supabaseKey),
});
const pendingStopsData = await stopsRes.json();
const pendingStops = Array.isArray(pendingStopsData) ? pendingStopsData.length : 0;
// Fetch active products
let productsQuery = `${supabaseUrl}/rest/v1/products?select=id`;
productsQuery += `&active=eq.true`;
if (brandId) {
productsQuery += `&brand_id=eq.${brandId}`;
}
productsQuery += `&limit=1000`;
const productsRes = await fetch(productsQuery, {
headers: svcHeaders(supabaseKey),
});
const productsData = await productsRes.json();
const activeProducts = Array.isArray(productsData) ? productsData.length : 0;
// Fetch weekly orders for chart (last 7 days)
const weeklyOrders: number[] = [];
for (let i = 6; i >= 0; i--) {
const dayStart = new Date(startOfDay);
dayStart.setDate(dayStart.getDate() - i);
const dayEnd = new Date(dayStart.getTime() + 24 * 60 * 60 * 1000);
let dayQuery = `${supabaseUrl}/rest/v1/orders?select=id,status`;
dayQuery += `&created_at=gte.${dayStart.toISOString()}`;
dayQuery += `&created_at=lt.${dayEnd.toISOString()}`;
if (brandId) {
dayQuery += `&brand_id=eq.${brandId}`;
}
dayQuery += `&limit=1`;
const dayRes = await fetch(dayQuery, {
headers: svcHeaders(supabaseKey),
});
// Use X-Total-Count header or count from response
const count = dayRes.headers.get("X-Total-Count");
weeklyOrders.push(count ? parseInt(count) : 0);
}
// Fetch recent orders (last 10)
let recentQuery = `${supabaseUrl}/rest/v1/orders?select=id,customer_name,subtotal,status,created_at`;
recentQuery += `&order=created_at.desc`;
recentQuery += `&limit=10`;
if (brandId) {
recentQuery += `&brand_id=eq.${brandId}`;
}
const recentRes = await fetch(recentQuery, {
headers: {
...svcHeaders(supabaseKey),
Prefer: "count=exact",
},
});
const recentOrdersData = await recentRes.json();
const recentOrders = (Array.isArray(recentOrdersData) ? recentOrdersData : [])
.filter((o: {status: string}) => o.status !== "cancelled")
.map((o: {id: string; customer_name: string; subtotal: number; status: string; created_at: string}) => ({
id: o.id || "",
customer_name: o.customer_name || "Guest",
total: o.subtotal || 0,
status: o.status || "unknown",
created_at: formatTimeAgo(o.created_at),
}));
return {
todayOrders: todayOrderCount,
todayRevenue: Math.round(todayRevenue * 100) / 100,
pendingStops,
activeProducts,
weeklyOrders,
recentOrders,
};
} catch (error) {
console.error("Failed to fetch dashboard stats:", error);
// Return zeros on error
return {
todayOrders: 0,
todayRevenue: 0,
pendingStops: 0,
activeProducts: 0,
weeklyOrders: [0, 0, 0, 0, 0, 0, 0],
recentOrders: [],
};
}
}
export async function getDashboardSummary(): Promise<DashboardSummary> {
try {
const adminUser = await getAdminUser();
if (!adminUser) throw new Error("Not authenticated");
const brandId = adminUser.brand_id ?? null;
const thirtyDaysAgo = new Date();
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
// Get gross sales from reports RPC
const rpcRes = await fetch(`${supabaseUrl}/rest/v1/rpc/get_reports_summary`, {
method: "POST",
headers: {
...svcHeaders(supabaseKey),
"Content-Type": "application/json",
},
body: JSON.stringify({
p_brand_id: brandId,
p_start_date: thirtyDaysAgo.toISOString().split("T")[0],
p_end_date: new Date().toISOString().split("T")[0],
}),
});
let total_revenue = 0;
let total_orders = 0;
if (rpcRes.ok) {
const data = await rpcRes.json();
total_revenue = data?.gross_sales ?? 0;
total_orders = data?.total_orders ?? 0;
}
// Get active stops count
let stopsQuery = `${supabaseUrl}/rest/v1/stops?select=id`;
stopsQuery += `&date=gte.${new Date().toISOString().split("T")[0]}`;
stopsQuery += `&status=eq.scheduled`;
if (brandId) {
stopsQuery += `&brand_id=eq.${brandId}`;
}
const stopsRes = await fetch(stopsQuery, {
headers: {
...svcHeaders(supabaseKey),
Prefer: "count=exact",
},
});
const activeStops = parseInt(stopsRes.headers.get("X-Total-Count") || "0");
// Get active products count
let productsQuery = `${supabaseUrl}/rest/v1/products?select=id&active=eq.true`;
if (brandId) {
productsQuery += `&brand_id=eq.${brandId}`;
}
const productsRes = await fetch(productsQuery, {
headers: {
...svcHeaders(supabaseKey),
Prefer: "count=exact",
},
});
const activeProducts = parseInt(productsRes.headers.get("X-Total-Count") || "0");
return {
total_revenue,
total_orders,
active_stops: activeStops,
active_products: activeProducts,
};
} catch (error) {
console.error("Failed to fetch dashboard summary:", error);
return {
total_revenue: 0,
total_orders: 0,
active_stops: 0,
active_products: 0,
};
}
}
// ── Helpers ────────────────────────────────────────────────────────────────────
function formatTimeAgo(dateString: string): string {
if (!dateString) return "Unknown";
const date = new Date(dateString);
const now = new Date();
const diffMs = now.getTime() - date.getTime();
const diffMins = Math.floor(diffMs / 60000);
const diffHours = Math.floor(diffMs / 3600000);
const diffDays = Math.floor(diffMs / 86400000);
if (diffMins < 1) return "Just now";
if (diffMins < 60) return `${diffMins}m ago`;
if (diffHours < 24) return `${diffHours}h ago`;
if (diffDays < 7) return `${diffDays}d ago`;
return date.toLocaleDateString("en-US", { month: "short", day: "numeric" });
}