Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions app/(tabs)/_layout.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Tabs } from "expo-router";
import { useTranslation } from "react-i18next";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import {
Home,
Search,
Expand All @@ -10,6 +11,11 @@ import {

export default function TabLayout() {
const { t } = useTranslation();
const insets = useSafeAreaInsets();
// Without a bottom safe-area inset (web, or a device with no home indicator)
// the labels sit flush against the bottom edge and get clipped. Reserve a
// floor of bottom padding so the descenders always clear the edge.
const bottomPad = Math.max(insets.bottom, 12);
return (
<Tabs
screenOptions={{
Expand All @@ -22,10 +28,14 @@ export default function TabLayout() {
tabBarStyle: {
borderTopColor: "#e2e8f0",
backgroundColor: "#ffffff",
height: 56 + bottomPad,
paddingBottom: bottomPad,
paddingTop: 6,
},
tabBarLabelStyle: {
fontSize: 11,
fontWeight: "600",
lineHeight: 14,
},
}}
>
Expand Down
64 changes: 55 additions & 9 deletions app/(tabs)/index.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,29 @@
import { View, Text, ScrollView, RefreshControl } from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
import { LayoutDashboard, ChevronRight, Inbox, AlertCircle } from "lucide-react-native";
import {
LayoutDashboard,
ChevronRight,
Inbox,
AlertCircle,
Sparkles,
} from "lucide-react-native";
import { useClient } from "@objectstack/client-react";
import { useRouter } from "expo-router";
import { useTranslation } from "react-i18next";
import { useCallback, useEffect, useState } from "react";
import { authClient } from "~/lib/auth-client";
import { PressableCard } from "~/components/ui/PressableCard";
import { EmptyState } from "~/components/ui/EmptyState";
import { ListSkeleton } from "~/components/ui/ListSkeleton";
import { useApps } from "~/hooks/useApps";

/** Pick a time-of-day greeting i18n key from the local hour. */
function greetingKey(hour: number): "greetingMorning" | "greetingAfternoon" | "greetingEvening" {
if (hour < 12) return "greetingMorning";
if (hour < 18) return "greetingAfternoon";
return "greetingEvening";
}

interface DashboardEntry {
/** App name — the route segment dashboards open under. */
appId: string;
Expand All @@ -27,8 +42,14 @@ interface DashboardEntry {
export default function HomeScreen() {
const client = useClient();
const router = useRouter();
const { t } = useTranslation();
const { data: session } = authClient.useSession();
const { apps, isLoading: appsLoading, refetch: refetchApps } = useApps();

const firstName = (session?.user?.name ?? "").trim().split(/\s+/)[0];
const greeting = t(`home.${greetingKey(new Date().getHours())}`);
const heading = firstName ? `${greeting}, ${firstName}` : greeting;

const [dashboards, setDashboards] = useState<DashboardEntry[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [isRefreshing, setIsRefreshing] = useState(false);
Expand Down Expand Up @@ -100,35 +121,60 @@ export default function HomeScreen() {
}
>
<View className="mb-5">
<Text className="text-2xl font-bold text-foreground">Dashboard</Text>
<Text className="text-2xl font-bold text-foreground">{heading}</Text>
<Text className="mt-1 text-sm text-muted-foreground">
Welcome back. Here&apos;s your overview.
{t("home.subtitle")}
</Text>
</View>

{/* AI Assistant quick entry — surfaces the assistant on the home screen
instead of burying it two levels deep under More. */}
<PressableCard
className="mb-5 flex-row items-center p-4"
onPress={() => router.push("/ai")}
accessibilityRole="button"
accessibilityLabel={t("home.assistantTitle")}
>
<View className="rounded-xl bg-primary/10 p-3">
<Sparkles size={24} color="#1e40af" />
</View>
<View className="ml-4 flex-1">
<Text className="text-base font-semibold text-card-foreground">
{t("home.assistantTitle")}
</Text>
<Text className="mt-0.5 text-sm text-muted-foreground">
{t("home.assistantSubtitle")}
</Text>
</View>
<ChevronRight size={20} color="#94a3b8" />
</PressableCard>

{loading ? (
<ListSkeleton count={4} />
) : error ? (
<View className="pt-20">
<View className="pt-16">
<EmptyState
icon={AlertCircle}
variant="error"
title="Couldn't Load Dashboards"
title={t("home.loadErrorTitle")}
description={error.message}
actionLabel="Retry"
actionLabel={t("common.retry")}
onAction={() => void fetchDashboards()}
/>
</View>
) : dashboards.length === 0 ? (
<View className="pt-20">
<View className="pt-16">
<EmptyState
icon={Inbox}
title="No Dashboards"
description="None of your installed apps publish a dashboard yet."
title={t("home.noDashboardsTitle")}
description={t("home.noDashboardsDesc")}
/>
</View>
) : (
<View className="gap-3">
<Text className="mb-1 text-xs font-semibold uppercase tracking-wide text-muted-foreground">
{t("home.dashboardsTitle")}
</Text>
{dashboards.map((d) => (
<PressableCard
key={`${d.appId}:${d.name}`}
Expand Down
12 changes: 12 additions & 0 deletions locales/ar.json
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,18 @@
"more": "المزيد",
"profile": "الملف الشخصي"
},
"home": {
"greetingMorning": "صباح الخير",
"greetingAfternoon": "مساء الخير",
"greetingEvening": "مساء الخير",
"subtitle": "إليك نظرة عامة.",
"assistantTitle": "المساعد الذكي",
"assistantSubtitle": "اسأل أي شيء عن بياناتك",
"dashboardsTitle": "لوحات المعلومات",
"noDashboardsTitle": "لا توجد لوحات معلومات",
"noDashboardsDesc": "لم تنشر أي من تطبيقاتك المثبتة لوحة معلومات بعد.",
"loadErrorTitle": "تعذّر تحميل لوحات المعلومات"
},
"search": {
"title": "بحث",
"placeholder": "ابحث في كل السجلات…",
Expand Down
12 changes: 12 additions & 0 deletions locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,18 @@
"more": "More",
"profile": "Profile"
},
"home": {
"greetingMorning": "Good morning",
"greetingAfternoon": "Good afternoon",
"greetingEvening": "Good evening",
"subtitle": "Here's your overview.",
"assistantTitle": "AI Assistant",
"assistantSubtitle": "Ask anything about your data",
"dashboardsTitle": "Dashboards",
"noDashboardsTitle": "No Dashboards",
"noDashboardsDesc": "None of your installed apps publish a dashboard yet.",
"loadErrorTitle": "Couldn't Load Dashboards"
},
"search": {
"title": "Search",
"placeholder": "Search across all records…",
Expand Down
12 changes: 12 additions & 0 deletions locales/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,18 @@
"more": "更多",
"profile": "个人资料"
},
"home": {
"greetingMorning": "早上好",
"greetingAfternoon": "下午好",
"greetingEvening": "晚上好",
"subtitle": "这是你的概览。",
"assistantTitle": "AI 助手",
"assistantSubtitle": "随时询问与你数据相关的问题",
"dashboardsTitle": "仪表盘",
"noDashboardsTitle": "暂无仪表盘",
"noDashboardsDesc": "你已安装的应用尚未发布任何仪表盘。",
"loadErrorTitle": "无法加载仪表盘"
},
"search": {
"title": "搜索",
"placeholder": "搜索所有记录…",
Expand Down
Loading