ea41328626
Removes Arabic and English description fields from the admin form, the services display component, and the seed data in `scripts/src/seed.ts`, and removes the corresponding columns from the database. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 5f1c43b0-7465-4e56-bb0e-896a4df38886 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/dnVFHsG Replit-Helium-Checkpoint-Created: true
144 lines
5.0 KiB
TypeScript
144 lines
5.0 KiB
TypeScript
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>
|
|
)}
|
|
</div>
|
|
|
|
</div>
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<OrderServiceModal
|
|
open={orderTarget !== null}
|
|
onOpenChange={(o) => {
|
|
if (!o) setOrderTarget(null);
|
|
}}
|
|
service={orderTarget}
|
|
resolveImage={resolveServiceImageUrl}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|