Files
TX/artifacts/tx-os/src/pages/services.tsx
T

144 lines
5.0 KiB
TypeScript
Raw Normal View History

import { useState } from "react";
import { useTranslation } from "react-i18next";
import { useLocation } from "wouter";
import {
useListServices,
getListServicesQueryKey,
} from "@workspace/api-client-react";
import { ArrowRight, ArrowLeft, Coffee, ImageIcon, Inbox } from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { resolveServiceImageUrl } from "@/lib/image-url";
import { OrderServiceModal } from "@/components/order-service-modal";
type CatalogService = {
id: number;
nameAr: string;
nameEn: string;
imageUrl?: string | null;
};
function ServiceCardImage({ src, alt }: { src: string | null; alt: string }) {
const [errored, setErrored] = useState(false);
const showImage = src && !errored;
return (
<div className="w-full aspect-square bg-slate-100 overflow-hidden flex items-center justify-center">
{showImage ? (
<img
src={src}
alt={alt}
className="w-full h-full object-cover"
onError={() => setErrored(true)}
/>
) : (
<ImageIcon size={24} className="text-slate-300" />
)}
</div>
);
}
export default function ServicesPage() {
const { t, i18n } = useTranslation();
const [, setLocation] = useLocation();
const lang = i18n.language;
const isRtl = lang === "ar";
const { data: services, isLoading } = useListServices({
query: { queryKey: getListServicesQueryKey() },
});
const [orderTarget, setOrderTarget] = useState<CatalogService | null>(null);
const BackIcon = isRtl ? ArrowRight : ArrowLeft;
return (
<div className="min-h-screen os-bg flex flex-col">
{/* Header */}
<div className="glass-panel border-b border-slate-200/70 px-4 py-4 flex items-center gap-3 sticky top-0 z-10">
<button
onClick={() => setLocation("/")}
className="p-2 rounded-xl hover:bg-slate-100 text-muted-foreground hover:text-foreground transition-colors"
>
<BackIcon size={20} />
</button>
<div className="flex items-center gap-2">
<Coffee size={20} className="text-amber-400" />
<h1 className="text-lg font-semibold text-foreground">{t("services.title")}</h1>
</div>
<div className="ms-auto">
<Button
variant="ghost"
size="sm"
className="gap-1.5"
onClick={() => setLocation("/my-orders")}
>
<Inbox size={16} />
<span className="hidden sm:inline">{t("services.myOrdersLink")}</span>
</Button>
</div>
</div>
<div className="flex-1 p-4 pb-8">
{isLoading ? (
<div className="flex items-center justify-center py-20 text-muted-foreground">
{t("common.loading")}
</div>
) : !services?.length ? (
<div className="flex items-center justify-center py-20 text-muted-foreground">
{t("services.noServices")}
</div>
) : (
<div className="grid grid-cols-3 sm:grid-cols-4 md:grid-cols-5 lg:grid-cols-7 xl:grid-cols-8 gap-2">
{services.map((service) => {
const name = lang === "ar" ? service.nameAr : service.nameEn;
const resolvedImage = resolveServiceImageUrl(service.imageUrl);
const openOrder = () => {
if (!service.isAvailable) return;
setOrderTarget({
id: service.id,
nameAr: service.nameAr,
nameEn: service.nameEn,
imageUrl: service.imageUrl,
});
};
return (
<button
key={service.id}
type="button"
onClick={openOrder}
disabled={!service.isAvailable}
aria-label={name}
className="glass-panel rounded-lg overflow-hidden flex flex-col card-hover text-start disabled:opacity-60 disabled:cursor-not-allowed focus:outline-none focus-visible:ring-2 focus-visible:ring-primary"
>
<ServiceCardImage src={resolvedImage} alt={name} />
<div className="p-2 flex flex-col gap-0.5 flex-1">
<div className="flex items-start justify-between gap-1">
<h3 className="text-xs font-semibold text-foreground leading-tight truncate">{name}</h3>
{!service.isAvailable && (
<Badge variant="secondary" className="text-[9px] px-1 py-0 shrink-0">
{t("services.unavailable")}
</Badge>
)}
2026-04-20 10:49:57 +00:00
</div>
</div>
</button>
);
})}
</div>
)}
</div>
<OrderServiceModal
open={orderTarget !== null}
onOpenChange={(o) => {
if (!o) setOrderTarget(null);
}}
service={orderTarget}
resolveImage={resolveServiceImageUrl}
/>
</div>
);
}