Files
TX/artifacts/teaboy-os/src/pages/home.tsx
T

202 lines
7.8 KiB
TypeScript
Raw Normal View History

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>
);
}