diff --git a/artifacts/teaboy-os/src/assets/login-art.png b/artifacts/teaboy-os/src/assets/login-art.png
new file mode 100644
index 00000000..465daa5b
Binary files /dev/null and b/artifacts/teaboy-os/src/assets/login-art.png differ
diff --git a/artifacts/teaboy-os/src/locales/ar.json b/artifacts/teaboy-os/src/locales/ar.json
index 0c166b33..706bff69 100644
--- a/artifacts/teaboy-os/src/locales/ar.json
+++ b/artifacts/teaboy-os/src/locales/ar.json
@@ -110,6 +110,8 @@
"siteNameEn": "اسم الموقع (إنجليزي)",
"registrationOpen": "السماح بإنشاء حسابات",
"registrationOpenHint": "عند الإيقاف، الأدمن فقط يقدر ينشئ مستخدمين جدد.",
+ "footerTextAr": "نص التذييل (عربي)",
+ "footerTextEn": "نص التذييل (إنجليزي)",
"nav": {
"dashboard": "الرئيسية",
"apps": "التطبيقات",
diff --git a/artifacts/teaboy-os/src/locales/en.json b/artifacts/teaboy-os/src/locales/en.json
index 0e69a2ae..bfc6b059 100644
--- a/artifacts/teaboy-os/src/locales/en.json
+++ b/artifacts/teaboy-os/src/locales/en.json
@@ -110,6 +110,8 @@
"siteNameEn": "Site Name (English)",
"registrationOpen": "Allow public registration",
"registrationOpenHint": "When off, only admins can create new users.",
+ "footerTextAr": "Footer Text (Arabic)",
+ "footerTextEn": "Footer Text (English)",
"nav": {
"dashboard": "Dashboard",
"apps": "Apps",
diff --git a/artifacts/teaboy-os/src/pages/admin.tsx b/artifacts/teaboy-os/src/pages/admin.tsx
index 18222edf..70f682cc 100644
--- a/artifacts/teaboy-os/src/pages/admin.tsx
+++ b/artifacts/teaboy-os/src/pages/admin.tsx
@@ -595,13 +595,21 @@ function SiteSettingsPanel() {
const queryClient = useQueryClient();
const { data } = useGetAppSettings({ query: { queryKey: getGetAppSettingsQueryKey() } });
const update = useUpdateAppSettings();
- const [form, setForm] = useState({ siteNameAr: "", siteNameEn: "", registrationOpen: true });
+ const [form, setForm] = useState({
+ siteNameAr: "",
+ siteNameEn: "",
+ registrationOpen: true,
+ footerTextAr: "",
+ footerTextEn: "",
+ });
useEffect(() => {
if (data) setForm({
siteNameAr: data.siteNameAr,
siteNameEn: data.siteNameEn,
registrationOpen: data.registrationOpen,
+ footerTextAr: data.footerTextAr,
+ footerTextEn: data.footerTextEn,
});
}, [data]);
@@ -638,6 +646,24 @@ function SiteSettingsPanel() {
dir="ltr"
/>
+
+
+ setForm({ ...form, footerTextAr: e.target.value })}
+ className="bg-white/70 border-slate-200"
+ dir="rtl"
+ />
+
+
+
+ setForm({ ...form, footerTextEn: e.target.value })}
+ className="bg-white/70 border-slate-200"
+ dir="ltr"
+ />
+
diff --git a/artifacts/teaboy-os/src/pages/login.tsx b/artifacts/teaboy-os/src/pages/login.tsx
index 44734330..4bf8789e 100644
--- a/artifacts/teaboy-os/src/pages/login.tsx
+++ b/artifacts/teaboy-os/src/pages/login.tsx
@@ -5,14 +5,8 @@ import { useLogin, getGetMeQueryKey } from "@workspace/api-client-react";
import { useQueryClient } from "@tanstack/react-query";
import { useAppName, useAppSettings } from "@/hooks/use-app-settings";
import i18n from "@/i18n";
-import {
- Sparkles,
- CheckCircle2,
- Eye,
- EyeOff,
- Loader2,
- AlertCircle,
-} from "lucide-react";
+import { Eye, EyeOff, Loader2, AlertCircle } from "lucide-react";
+import loginArt from "@/assets/login-art.png";
export default function LoginPage() {
const { t, i18n: i18nInstance } = useTranslation();
@@ -56,57 +50,27 @@ export default function LoginPage() {
const toggleLanguage = () => i18n.changeLanguage(lang === "ar" ? "en" : "ar");
- const features = [
- t("auth.feat1"),
- t("auth.feat2"),
- t("auth.feat3"),
- ];
+ const footerText =
+ lang === "ar" ? settings?.footerTextAr : settings?.footerTextEn;
return (
- {/* Left brand panel — hidden on small screens */}
-
-
+

+ {/* Soft brand-tinted overlay so the art feels integrated with the OS palette */}
+
-
-
-
-
-
-
- {t("auth.heroTitle")}
-
-
- {t("auth.heroSubtitle")}
-
-
- {features.map((feat) => (
-
-
- {feat}
-
- ))}
-
-
-
-
-
- © {new Date().getFullYear()} {appName}. {t("auth.rights")}
-
{/* Right form panel */}
@@ -126,30 +90,7 @@ export default function LoginPage() {
- {/* Mobile brand */}
-
-
-
-
- {t("auth.welcomeBack")}
-
-
- {t("auth.subtitle")}
-
-
-
+
+ {/* Admin-editable footer */}
+ {footerText && (
+
+ © {new Date().getFullYear()} {appName}. {footerText}
+
+ )}
);
diff --git a/lib/api-client-react/src/generated/api.schemas.ts b/lib/api-client-react/src/generated/api.schemas.ts
index 0cfaa30d..4b3fac2d 100644
--- a/lib/api-client-react/src/generated/api.schemas.ts
+++ b/lib/api-client-react/src/generated/api.schemas.ts
@@ -261,6 +261,8 @@ export interface AppSettings {
siteNameAr: string;
siteNameEn: string;
registrationOpen: boolean;
+ footerTextAr: string;
+ footerTextEn: string;
updatedAt?: string;
}
@@ -281,6 +283,16 @@ export interface UpdateAppSettingsBody {
*/
siteNameEn?: string;
registrationOpen?: boolean;
+ /**
+ * @minLength 1
+ * @maxLength 300
+ */
+ footerTextAr?: string;
+ /**
+ * @minLength 1
+ * @maxLength 300
+ */
+ footerTextEn?: string;
}
export interface RequestUploadUrlBody {
diff --git a/lib/api-spec/openapi.yaml b/lib/api-spec/openapi.yaml
index 64286138..ca863342 100644
--- a/lib/api-spec/openapi.yaml
+++ b/lib/api-spec/openapi.yaml
@@ -1219,7 +1219,7 @@ components:
AppSettings:
type: object
- required: [siteNameAr, siteNameEn, registrationOpen]
+ required: [siteNameAr, siteNameEn, registrationOpen, footerTextAr, footerTextEn]
properties:
siteNameAr:
type: string
@@ -1227,6 +1227,10 @@ components:
type: string
registrationOpen:
type: boolean
+ footerTextAr:
+ type: string
+ footerTextEn:
+ type: string
updatedAt:
type: string
format: date-time
@@ -1254,6 +1258,14 @@ components:
maxLength: 200
registrationOpen:
type: boolean
+ footerTextAr:
+ type: string
+ minLength: 1
+ maxLength: 300
+ footerTextEn:
+ type: string
+ minLength: 1
+ maxLength: 300
RequestUploadUrlBody:
type: object
diff --git a/lib/api-zod/src/generated/api.ts b/lib/api-zod/src/generated/api.ts
index 37bebe32..7a1fe599 100644
--- a/lib/api-zod/src/generated/api.ts
+++ b/lib/api-zod/src/generated/api.ts
@@ -231,6 +231,8 @@ export const GetAppSettingsResponse = zod.object({
siteNameAr: zod.string(),
siteNameEn: zod.string(),
registrationOpen: zod.boolean(),
+ footerTextAr: zod.string(),
+ footerTextEn: zod.string(),
updatedAt: zod.coerce.date().optional(),
});
@@ -241,6 +243,10 @@ export const updateAppSettingsBodySiteNameArMax = 200;
export const updateAppSettingsBodySiteNameEnMax = 200;
+export const updateAppSettingsBodyFooterTextArMax = 300;
+
+export const updateAppSettingsBodyFooterTextEnMax = 300;
+
export const UpdateAppSettingsBody = zod.object({
siteNameAr: zod
.string()
@@ -253,12 +259,24 @@ export const UpdateAppSettingsBody = zod.object({
.max(updateAppSettingsBodySiteNameEnMax)
.optional(),
registrationOpen: zod.boolean().optional(),
+ footerTextAr: zod
+ .string()
+ .min(1)
+ .max(updateAppSettingsBodyFooterTextArMax)
+ .optional(),
+ footerTextEn: zod
+ .string()
+ .min(1)
+ .max(updateAppSettingsBodyFooterTextEnMax)
+ .optional(),
});
export const UpdateAppSettingsResponse = zod.object({
siteNameAr: zod.string(),
siteNameEn: zod.string(),
registrationOpen: zod.boolean(),
+ footerTextAr: zod.string(),
+ footerTextEn: zod.string(),
updatedAt: zod.coerce.date().optional(),
});
diff --git a/lib/db/src/schema/settings.ts b/lib/db/src/schema/settings.ts
index 4271a87b..df51a5f7 100644
--- a/lib/db/src/schema/settings.ts
+++ b/lib/db/src/schema/settings.ts
@@ -7,6 +7,8 @@ export const appSettingsTable = pgTable("app_settings", {
siteNameAr: varchar("site_name_ar", { length: 200 }).notNull().default("نظام TeaBoy"),
siteNameEn: varchar("site_name_en", { length: 200 }).notNull().default("TeaBoy OS"),
registrationOpen: boolean("registration_open").notNull().default(true),
+ footerTextAr: varchar("footer_text_ar", { length: 300 }).notNull().default("جميع الحقوق محفوظة."),
+ footerTextEn: varchar("footer_text_en", { length: 300 }).notNull().default("All rights reserved."),
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow().$onUpdate(() => new Date()),
});