Skip to content

Commit 05cd1bc

Browse files
committed
fix: admin error handling, login page, add Arabic i18n support
1 parent 01cb009 commit 05cd1bc

7 files changed

Lines changed: 293 additions & 60 deletions

File tree

app/admin/page.tsx

Lines changed: 67 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,50 @@ import { isAdminEmail } from "@/lib/access";
55
import { listUsers } from "@/lib/db";
66

77
export default async function AdminPage() {
8-
const session = await getServerSession(authOptions);
8+
let session;
9+
try {
10+
session = await getServerSession(authOptions);
11+
} catch {
12+
return (
13+
<div className="min-h-screen bg-[#09090f] text-white p-8 flex items-center justify-center">
14+
<div className="text-center max-w-md">
15+
<h1 className="text-xl font-bold text-red-400 mb-3">Configuration Error</h1>
16+
<p className="text-zinc-400 text-sm">
17+
AUTH_SECRET / NEXTAUTH_SECRET must be set in environment variables.
18+
</p>
19+
</div>
20+
</div>
21+
);
22+
}
923

1024
if (!isAdminEmail(session?.user?.email)) {
1125
redirect("/dashboard");
1226
}
1327

14-
// Fetch users from our updated db utility
15-
const users = await listUsers();
28+
let users: any[] = [];
29+
try {
30+
users = await listUsers();
31+
} catch {
32+
return (
33+
<div className="min-h-screen bg-[#09090f] text-white p-8 flex items-center justify-center">
34+
<div className="text-center max-w-md">
35+
<h1 className="text-xl font-bold text-red-400 mb-3">Database Error</h1>
36+
<p className="text-zinc-400 text-sm">
37+
Could not connect to database. Check DATABASE_URL.
38+
</p>
39+
</div>
40+
</div>
41+
);
42+
}
1643

17-
const totalMRR = users.reduce((sum, user) => {
44+
const totalMRR = users.reduce((sum: number, user: any) => {
1845
if (user.plan === "agency") return sum + 99;
1946
if (user.plan === "pro") return sum + 29;
2047
return sum;
2148
}, 0);
2249

23-
const activeUsers = users.filter((user) => user.plan !== "starter").length;
24-
const trialUsers = users.filter((user) => user.plan === "starter").length;
50+
const activeUsers = users.filter((user: any) => user.plan !== "free").length;
51+
const trialUsers = users.filter((user: any) => user.plan === "free").length;
2552

2653
return (
2754
<main className="min-h-screen bg-[#09090f] p-6 text-white">
@@ -30,26 +57,21 @@ export default async function AdminPage() {
3057
Admin
3158
</div>
3259
<h1 className="text-4xl font-semibold">Admin dashboard</h1>
33-
<p className="mt-2 text-zinc-400">Operational snapshot for X-Teos Pro</p>
60+
<p className="mt-2 text-zinc-400">Operational snapshot for Teos AI Engine</p>
3461
</header>
3562

3663
<div className="mx-auto mb-8 grid max-w-7xl grid-cols-1 gap-4 md:grid-cols-4">
37-
<div className="rounded-3xl border border-white/10 bg-white/5 p-5">
38-
<div className="text-sm text-zinc-400">Monthly MRR</div>
39-
<div className="mt-2 text-3xl font-semibold text-violet-300">${totalMRR}</div>
40-
</div>
41-
<div className="rounded-3xl border border-white/10 bg-white/5 p-5">
42-
<div className="text-sm text-zinc-400">Active users</div>
43-
<div className="mt-2 text-3xl font-semibold text-emerald-300">{activeUsers}</div>
44-
</div>
45-
<div className="rounded-3xl border border-white/10 bg-white/5 p-5">
46-
<div className="text-sm text-zinc-400">Trials</div>
47-
<div className="mt-2 text-3xl font-semibold text-amber-300">{trialUsers}</div>
48-
</div>
49-
<div className="rounded-3xl border border-white/10 bg-white/5 p-5">
50-
<div className="text-sm text-zinc-400">Total users</div>
51-
<div className="mt-2 text-3xl font-semibold text-cyan-300">{users.length}</div>
52-
</div>
64+
{[
65+
{ label: "Monthly MRR", value: `$${totalMRR}`, color: "text-violet-300" },
66+
{ label: "Active users", value: activeUsers, color: "text-emerald-300" },
67+
{ label: "Trials", value: trialUsers, color: "text-amber-300" },
68+
{ label: "Total users", value: users.length, color: "text-cyan-300" },
69+
].map(s => (
70+
<div key={s.label} className="rounded-3xl border border-white/10 bg-white/5 p-5">
71+
<div className="text-sm text-zinc-400">{s.label}</div>
72+
<div className={`mt-2 text-3xl font-semibold ${s.color}`}>{s.value}</div>
73+
</div>
74+
))}
5375
</div>
5476

5577
<div className="mx-auto max-w-7xl overflow-hidden rounded-3xl border border-white/10 bg-white/5">
@@ -64,24 +86,28 @@ export default async function AdminPage() {
6486
</tr>
6587
</thead>
6688
<tbody>
67-
{(users as any[]).map((user) => (
68-
<tr key={user.id} className="border-t border-white/5">
69-
<td className="px-4 py-4">
70-
<div className="font-medium">{user.name}</div>
71-
<div className="text-sm text-zinc-400">{user.email}</div>
72-
</td>
73-
<td className="px-4 py-4 capitalize">{user.plan}</td>
74-
<td className="px-4 py-4 capitalize">
75-
{user.plan === "starter" ? "trial" : "active"}
76-
</td>
77-
<td className="px-4 py-4 text-sm text-zinc-400">
78-
{new Date(user.createdAt).toLocaleDateString()}
79-
</td>
80-
<td className="px-4 py-4 text-sm text-zinc-400">
81-
{user.posts?.length ?? 0}
82-
</td>
83-
</tr>
84-
))}
89+
{users.length === 0 ? (
90+
<tr><td colSpan={5} className="text-center p-8 text-zinc-500">No users yet</td></tr>
91+
) : (
92+
users.map((user: any) => (
93+
<tr key={user.id} className="border-t border-white/5">
94+
<td className="px-4 py-4">
95+
<div className="font-medium">{user.name || "—"}</div>
96+
<div className="text-sm text-zinc-400">{user.email}</div>
97+
</td>
98+
<td className="px-4 py-4 capitalize">{user.plan}</td>
99+
<td className="px-4 py-4 capitalize">
100+
{user.plan === "free" ? "trial" : "active"}
101+
</td>
102+
<td className="px-4 py-4 text-sm text-zinc-400">
103+
{user.createdAt ? new Date(user.createdAt).toLocaleDateString() : "—"}
104+
</td>
105+
<td className="px-4 py-4 text-sm text-zinc-400">
106+
{user.posts?.length ?? user.totalPostsUsed ?? 0}
107+
</td>
108+
</tr>
109+
))
110+
)}
85111
</tbody>
86112
</table>
87113
</div>

app/layout.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { Metadata } from "next";
22
import "./globals.css";
33
import Providers from "@/components/Providers";
4+
import LocaleProvider from "@/components/LocaleProvider";
45

56
const siteUrl = process.env.NEXT_PUBLIC_APP_URL || "https://teos-ai-engine.vercel.app";
67

@@ -94,17 +95,21 @@ export default function RootLayout({
9495
children: React.ReactNode;
9596
}) {
9697
return (
97-
<html lang="en" className="scroll-smooth">
98+
<html lang="en" className="scroll-smooth" dir="ltr" suppressHydrationWarning>
9899
<head>
99100
<link rel="preconnect" href="https://fonts.googleapis.com" />
100101
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
101102
<link
102-
href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=Playfair+Display:wght@500;600;700&family=JetBrains+Mono:wght@400;500&display=swap"
103+
href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=Playfair+Display:wght@500;600;700&family=JetBrains+Mono:wght@400;500&family=Noto+Kufi+Arabic:wght@400;500;600;700&display=swap"
103104
rel="stylesheet"
104105
/>
105106
</head>
106107
<body className="min-h-screen bg-bg antialiased">
107-
<Providers>{children}</Providers>
108+
<Providers>
109+
<LocaleProvider>
110+
{children}
111+
</LocaleProvider>
112+
</Providers>
108113
</body>
109114
</html>
110115
);

app/login/page.tsx

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import Link from "next/link";
88
export default function LoginPage() {
99
const router = useRouter();
1010
const [email, setEmail] = useState("");
11-
const [password, setPassword] = useState("");
1211
const [error, setError] = useState("");
1312
const [loading, setLoading] = useState(false);
1413

@@ -25,19 +24,19 @@ export default function LoginPage() {
2524
});
2625

2726
if (res?.error) {
28-
setError("Login failed. Check your credentials.");
27+
setError(res.error);
2928
} else {
3029
router.push("/dashboard");
3130
}
3231
} catch {
33-
setError("An unexpected error occurred.");
32+
setError("Connection error. Please try again.");
3433
} finally {
3534
setLoading(false);
3635
}
3736
}
3837

3938
return (
40-
<div className="min-h-screen bg-bg flex items-center justify-center p-4">
39+
<div className="min-h-screen bg-bg flex items-center justify-center p-4" dir="ltr">
4140
<div className="w-full max-w-sm">
4241
<div className="text-center mb-8">
4342
<Link href="/" className="inline-flex items-center gap-2 mb-6">
@@ -55,24 +54,17 @@ export default function LoginPage() {
5554
value={email}
5655
onChange={e => setEmail(e.target.value)}
5756
required
58-
className="w-full rounded-xl border border-white/[0.1] bg-white/[0.03] px-4 py-3 text-sm text-[#e8e6f0] placeholder-[#5a5870] outline-none focus:border-gold-500/40"
59-
/>
60-
<input
61-
type="password"
62-
placeholder="Password"
63-
value={password}
64-
onChange={e => setPassword(e.target.value)}
65-
required
57+
autoFocus
6658
className="w-full rounded-xl border border-white/[0.1] bg-white/[0.03] px-4 py-3 text-sm text-[#e8e6f0] placeholder-[#5a5870] outline-none focus:border-gold-500/40"
6759
/>
6860

6961
{error && (
7062
<div className="p-3 rounded-xl text-sm bg-red-500/10 border border-red-500/20 text-red-400">{error}</div>
7163
)}
7264

73-
<button type="submit" disabled={loading}
65+
<button type="submit" disabled={loading || !email.trim()}
7466
className="btn-primary w-full text-sm py-3 disabled:opacity-40">
75-
{loading ? "Signing in..." : "Sign in"}
67+
{loading ? "Signing in..." : "Sign in with email"}
7668
</button>
7769
</form>
7870

components/LocaleProvider.tsx

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
"use client";
2+
3+
import { createContext, useContext, useState, useEffect, useCallback, type ReactNode } from "react";
4+
import type { Locale } from "@/lib/i18n";
5+
import { translations } from "@/lib/i18n";
6+
7+
interface LocaleContextType {
8+
locale: Locale;
9+
setLocale: (l: Locale) => void;
10+
t: (key: string) => string;
11+
dir: "ltr" | "rtl";
12+
}
13+
14+
const LocaleContext = createContext<LocaleContextType>({
15+
locale: "en",
16+
setLocale: () => {},
17+
t: (k: string) => k,
18+
dir: "ltr",
19+
});
20+
21+
export function useLocale() {
22+
return useContext(LocaleContext);
23+
}
24+
25+
const COOKIE = "teos_locale";
26+
27+
function getCookie(name: string): string | null {
28+
if (typeof document === "undefined") return null;
29+
const match = document.cookie.match(new RegExp(`(^| )${name}=([^;]+)`));
30+
return match ? decodeURIComponent(match[2]) : null;
31+
}
32+
33+
function setCookie(name: string, value: string) {
34+
document.cookie = `${name}=${encodeURIComponent(value)};path=/;max-age=31536000;SameSite=Lax`;
35+
}
36+
37+
export default function LocaleProvider({ children }: { children: ReactNode }) {
38+
const [locale, setLocaleState] = useState<Locale>("en");
39+
40+
useEffect(() => {
41+
const saved = getCookie(COOKIE) as Locale | null;
42+
if (saved === "en" || saved === "ar") {
43+
setLocaleState(saved);
44+
}
45+
}, []);
46+
47+
const setLocale = useCallback((l: Locale) => {
48+
setLocaleState(l);
49+
setCookie(COOKIE, l);
50+
document.documentElement.dir = l === "ar" ? "rtl" : "ltr";
51+
document.documentElement.lang = l;
52+
}, []);
53+
54+
const t = useCallback(
55+
(key: string) => translations[locale]?.[key] ?? key,
56+
[locale]
57+
);
58+
59+
const dir: "ltr" | "rtl" = locale === "ar" ? "rtl" : "ltr";
60+
61+
return (
62+
<LocaleContext.Provider value={{ locale, setLocale, t, dir }}>
63+
{children}
64+
</LocaleContext.Provider>
65+
);
66+
}

components/LocaleSwitcher.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
"use client";
2+
3+
import { useLocale } from "./LocaleProvider";
4+
5+
export default function LocaleSwitcher() {
6+
const { locale, setLocale } = useLocale();
7+
8+
return (
9+
<button
10+
onClick={() => setLocale(locale === "en" ? "ar" : "en")}
11+
className="text-xs text-[#5a5870] hover:text-[#e8e6f0] transition-colors duration-200 px-2 py-1 rounded-md border border-white/[0.06] hover:border-white/[0.15]"
12+
title={locale === "en" ? "Switch to Arabic" : "التبديل إلى الإنجليزية"}
13+
>
14+
{locale === "en" ? "AR" : "EN"}
15+
</button>
16+
);
17+
}

components/landing/Navigation.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"use client";
22

33
import { useState, useEffect } from "react";
4+
import LocaleSwitcher from "@/components/LocaleSwitcher";
45

56
const navLinks = [
67
{ label: "Features", href: "#features" },
@@ -35,7 +36,7 @@ export default function Navigation() {
3536
</span>
3637
</a>
3738

38-
<nav className="hidden md:flex items-center gap-8">
39+
<nav className="hidden md:flex items-center gap-6">
3940
{navLinks.map((link) => (
4041
<a
4142
key={link.href}
@@ -45,7 +46,8 @@ export default function Navigation() {
4546
{link.label}
4647
</a>
4748
))}
48-
<a href="/admin" className="text-[10px] text-[#5a5870] hover:text-[#8a88a0] transition-colors duration-200" title="Admin">
49+
<LocaleSwitcher />
50+
<a href="/admin" className="text-xs text-[#5a5870] hover:text-[#8a88a0] transition-colors duration-200" title="Admin">
4951
5052
</a>
5153
<a href="/login" className="btn-teal text-xs px-5 py-2.5">
@@ -89,6 +91,7 @@ export default function Navigation() {
8991
{link.label}
9092
</a>
9193
))}
94+
<LocaleSwitcher />
9295
<a
9396
href="/admin"
9497
onClick={() => setMobileOpen(false)}

0 commit comments

Comments
 (0)