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 */} -
- - {/* Mobile brand */} -
-
- -
- {appName} -
-
-
-

- {t("auth.welcomeBack")} -

-

- {t("auth.subtitle")} -

-
-
{/* Username */} @@ -259,6 +200,13 @@ export default function LoginPage() {
+ + {/* 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()), });