202 lines
7.8 KiB
TypeScript
202 lines
7.8 KiB
TypeScript
|
|
import { useState, useEffect } from "react";
|
||
|
|
import { useTranslation } from "react-i18next";
|
||
|
|
import { useLocation } from "wouter";
|
||
|
|
import {
|
||
|
|
useListApps,
|
||
|
|
getListAppsQueryKey,
|
||
|
|
useGetHomeStats,
|
||
|
|
getGetHomeStatsQueryKey,
|
||
|
|
useListNotifications,
|
||
|
|
getListNotificationsQueryKey,
|
||
|
|
useLogout,
|
||
|
|
} from "@workspace/api-client-react";
|
||
|
|
import { useQueryClient } from "@tanstack/react-query";
|
||
|
|
import { useAuth } from "@/contexts/AuthContext";
|
||
|
|
import { Bell, LogOut, Globe } from "lucide-react";
|
||
|
|
import * as LucideIcons from "lucide-react";
|
||
|
|
import i18n from "@/i18n";
|
||
|
|
|
||
|
|
function AppIcon({ app, onClick }: { app: { nameAr: string; nameEn: string; iconName: string; color: string; route: string }; onClick: () => void }) {
|
||
|
|
const { i18n: i18nInstance } = useTranslation();
|
||
|
|
const lang = i18nInstance.language;
|
||
|
|
const name = lang === "ar" ? app.nameAr : app.nameEn;
|
||
|
|
|
||
|
|
const Icon = (LucideIcons as unknown as Record<string, React.ComponentType<{ size?: number; color?: string }>>)[app.iconName]
|
||
|
|
?? LucideIcons.Grid2X2;
|
||
|
|
|
||
|
|
return (
|
||
|
|
<button
|
||
|
|
onClick={onClick}
|
||
|
|
className="flex flex-col items-center gap-2 group"
|
||
|
|
>
|
||
|
|
<div
|
||
|
|
className="w-16 h-16 rounded-2xl flex items-center justify-center glass-panel shadow-lg group-hover:scale-110 transition-transform duration-200"
|
||
|
|
style={{ backgroundColor: `${app.color}30`, borderColor: `${app.color}50` }}
|
||
|
|
>
|
||
|
|
<Icon size={28} color={app.color} />
|
||
|
|
</div>
|
||
|
|
<span className="text-xs text-foreground/80 font-medium text-center leading-tight max-w-16 truncate">
|
||
|
|
{name}
|
||
|
|
</span>
|
||
|
|
</button>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
export default function HomePage() {
|
||
|
|
const { t, i18n: i18nInstance } = useTranslation();
|
||
|
|
const [, setLocation] = useLocation();
|
||
|
|
const { user } = useAuth();
|
||
|
|
const queryClient = useQueryClient();
|
||
|
|
const [time, setTime] = useState(new Date());
|
||
|
|
const lang = i18nInstance.language;
|
||
|
|
|
||
|
|
const { data: apps } = useListApps({ query: { queryKey: getListAppsQueryKey() } });
|
||
|
|
const { data: stats } = useGetHomeStats({ query: { queryKey: getGetHomeStatsQueryKey() } });
|
||
|
|
const { data: notifications } = useListNotifications({ query: { queryKey: getListNotificationsQueryKey() } });
|
||
|
|
|
||
|
|
const logout = useLogout();
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
const timer = setInterval(() => setTime(new Date()), 1000);
|
||
|
|
return () => clearInterval(timer);
|
||
|
|
}, []);
|
||
|
|
|
||
|
|
const unreadCount = notifications?.filter((n) => !n.isRead).length ?? stats?.unreadNotifications ?? 0;
|
||
|
|
|
||
|
|
const displayName = lang === "ar"
|
||
|
|
? (user?.displayNameAr ?? user?.username)
|
||
|
|
: (user?.displayNameEn ?? user?.username);
|
||
|
|
|
||
|
|
const hour = time.getHours();
|
||
|
|
const greeting = hour < 12 ? t("home.goodMorning") : hour < 17 ? t("home.goodAfternoon") : t("home.goodEvening");
|
||
|
|
|
||
|
|
const handleLogout = () => {
|
||
|
|
logout.mutate(undefined, {
|
||
|
|
onSuccess: () => {
|
||
|
|
queryClient.clear();
|
||
|
|
setLocation("/login");
|
||
|
|
},
|
||
|
|
});
|
||
|
|
};
|
||
|
|
|
||
|
|
const toggleLanguage = () => {
|
||
|
|
const newLang = lang === "ar" ? "en" : "ar";
|
||
|
|
i18n.changeLanguage(newLang);
|
||
|
|
};
|
||
|
|
|
||
|
|
const dockApps = apps?.filter((a) => ["services", "chat", "notifications", "admin"].includes(a.slug)) ?? [];
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="min-h-screen os-bg flex flex-col overflow-hidden relative">
|
||
|
|
{/* Status Bar */}
|
||
|
|
<div className="glass-panel border-b border-white/10 px-4 py-3 flex items-center justify-between z-10 sticky top-0">
|
||
|
|
<div className="flex items-center gap-3">
|
||
|
|
<span className="text-foreground font-medium text-sm truncate max-w-32">{displayName}</span>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="text-foreground font-mono text-sm font-bold">
|
||
|
|
{time.toLocaleTimeString(lang === "ar" ? "ar-SA" : "en-US", {
|
||
|
|
hour: "2-digit",
|
||
|
|
minute: "2-digit",
|
||
|
|
})}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="flex items-center gap-2">
|
||
|
|
<button
|
||
|
|
onClick={toggleLanguage}
|
||
|
|
className="text-muted-foreground hover:text-foreground transition-colors text-xs font-medium px-2 py-1 rounded-lg hover:bg-white/10"
|
||
|
|
>
|
||
|
|
{t("common.language")}
|
||
|
|
</button>
|
||
|
|
<button
|
||
|
|
onClick={() => setLocation("/notifications")}
|
||
|
|
className="relative p-1.5 rounded-lg hover:bg-white/10 text-muted-foreground hover:text-foreground transition-colors"
|
||
|
|
>
|
||
|
|
<Bell size={18} />
|
||
|
|
{unreadCount > 0 && (
|
||
|
|
<span className="absolute -top-1 -right-1 bg-destructive text-destructive-foreground text-xs rounded-full w-4 h-4 flex items-center justify-center font-bold">
|
||
|
|
{unreadCount > 9 ? "9+" : unreadCount}
|
||
|
|
</span>
|
||
|
|
)}
|
||
|
|
</button>
|
||
|
|
<button
|
||
|
|
onClick={handleLogout}
|
||
|
|
className="p-1.5 rounded-lg hover:bg-white/10 text-muted-foreground hover:text-foreground transition-colors"
|
||
|
|
>
|
||
|
|
<LogOut size={18} />
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Main Content */}
|
||
|
|
<div className="flex-1 p-6 pb-28 overflow-y-auto">
|
||
|
|
{/* Greeting */}
|
||
|
|
<div className="mb-8">
|
||
|
|
<h2 className="text-3xl font-bold text-foreground">{greeting}</h2>
|
||
|
|
<p className="text-muted-foreground mt-1">{t("home.welcome")}</p>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Stats Row */}
|
||
|
|
{stats && (
|
||
|
|
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3 mb-8">
|
||
|
|
<div className="glass-panel rounded-2xl p-3 text-center">
|
||
|
|
<div className="text-2xl font-bold text-primary">{stats.totalApps}</div>
|
||
|
|
<div className="text-xs text-muted-foreground mt-1">{t("home.myApps")}</div>
|
||
|
|
</div>
|
||
|
|
<div className="glass-panel rounded-2xl p-3 text-center">
|
||
|
|
<div className="text-2xl font-bold text-secondary">{stats.totalServices}</div>
|
||
|
|
<div className="text-xs text-muted-foreground mt-1">{t("nav.services")}</div>
|
||
|
|
</div>
|
||
|
|
<div className="glass-panel rounded-2xl p-3 text-center">
|
||
|
|
<div className="text-2xl font-bold text-accent">{stats.unreadMessages}</div>
|
||
|
|
<div className="text-xs text-muted-foreground mt-1">{t("nav.chat")}</div>
|
||
|
|
</div>
|
||
|
|
<div className="glass-panel rounded-2xl p-3 text-center">
|
||
|
|
<div className="text-2xl font-bold text-destructive">{stats.unreadNotifications}</div>
|
||
|
|
<div className="text-xs text-muted-foreground mt-1">{t("nav.notifications")}</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{/* Apps Grid */}
|
||
|
|
<div className="mb-4">
|
||
|
|
<h3 className="text-sm font-medium text-muted-foreground mb-4">{t("home.myApps")}</h3>
|
||
|
|
<div className="grid grid-cols-4 sm:grid-cols-5 md:grid-cols-6 gap-4">
|
||
|
|
{apps?.map((app) => (
|
||
|
|
<AppIcon
|
||
|
|
key={app.id}
|
||
|
|
app={app}
|
||
|
|
onClick={() => setLocation(app.route)}
|
||
|
|
/>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Bottom Dock */}
|
||
|
|
<div className="fixed bottom-0 left-0 right-0 p-4 z-10">
|
||
|
|
<div className="glass-panel rounded-3xl px-6 py-3 flex items-center justify-around max-w-sm mx-auto">
|
||
|
|
{dockApps.slice(0, 4).map((app) => {
|
||
|
|
const Icon = (LucideIcons as unknown as Record<string, React.ComponentType<{ size?: number; color?: string }>>)[app.iconName]
|
||
|
|
?? LucideIcons.Grid2X2;
|
||
|
|
return (
|
||
|
|
<button
|
||
|
|
key={app.id}
|
||
|
|
onClick={() => setLocation(app.route)}
|
||
|
|
className="flex flex-col items-center gap-1 group"
|
||
|
|
>
|
||
|
|
<div
|
||
|
|
className="w-12 h-12 rounded-xl flex items-center justify-center group-hover:scale-110 transition-transform"
|
||
|
|
style={{ backgroundColor: `${app.color}25` }}
|
||
|
|
>
|
||
|
|
<Icon size={22} color={app.color} />
|
||
|
|
</div>
|
||
|
|
</button>
|
||
|
|
);
|
||
|
|
})}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|