Update login screen and site settings with editable footer and AI art

Add editable footer text fields to site settings, update OpenAPI schema and API client, refactor the login page to display AI-generated artwork and use dynamic footer text, and remove the old logo and welcome heading.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: a650b496-7c3c-427e-bfbe-77bc8b9b5dd2
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/gdgNDb6
Replit-Helium-Checkpoint-Created: true
This commit is contained in:
riyadhafraa
2026-04-20 12:28:25 +00:00
parent 4741e5c506
commit ab2eac47c0
11 changed files with 107 additions and 77 deletions
+8
View File
@@ -0,0 +1,8 @@
uploads = []
outputs = []
[[generated]]
id = "eD84DGRCWiOa5scCp_Thx"
uri = "file://artifacts/teaboy-os/src/assets/login-art.png"
type = "image"
title = "login art"
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

+2
View File
@@ -110,6 +110,8 @@
"siteNameEn": "اسم الموقع (إنجليزي)",
"registrationOpen": "السماح بإنشاء حسابات",
"registrationOpenHint": "عند الإيقاف، الأدمن فقط يقدر ينشئ مستخدمين جدد.",
"footerTextAr": "نص التذييل (عربي)",
"footerTextEn": "نص التذييل (إنجليزي)",
"nav": {
"dashboard": "الرئيسية",
"apps": "التطبيقات",
+2
View File
@@ -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",
+27 -1
View File
@@ -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"
/>
</div>
<div className="space-y-1">
<Label>{t("admin.footerTextAr")}</Label>
<Input
value={form.footerTextAr}
onChange={(e) => setForm({ ...form, footerTextAr: e.target.value })}
className="bg-white/70 border-slate-200"
dir="rtl"
/>
</div>
<div className="space-y-1">
<Label>{t("admin.footerTextEn")}</Label>
<Input
value={form.footerTextEn}
onChange={(e) => setForm({ ...form, footerTextEn: e.target.value })}
className="bg-white/70 border-slate-200"
dir="ltr"
/>
</div>
<div className="flex items-center justify-between pt-2">
<div>
<Label className="font-medium">{t("admin.registrationOpen")}</Label>
+23 -75
View File
@@ -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 (
<div className="min-h-screen flex w-full text-foreground" dir={dir}>
{/* Left brand panel — hidden on small screens */}
<div className="hidden lg:flex w-1/2 os-bg flex-col justify-between p-12 relative overflow-hidden">
<div className="absolute inset-0 pointer-events-none"
{/* Left art panel — hidden on small screens, no logo, no text */}
<div className="hidden lg:block w-1/2 relative overflow-hidden os-bg">
<img
src={loginArt}
alt=""
aria-hidden="true"
className="absolute inset-0 w-full h-full object-cover"
/>
{/* Soft brand-tinted overlay so the art feels integrated with the OS palette */}
<div
className="absolute inset-0 mix-blend-overlay pointer-events-none"
style={{
background:
"radial-gradient(circle at top right, hsl(var(--primary) / 0.18), transparent 55%), radial-gradient(circle at bottom left, hsl(var(--accent) / 0.16), transparent 55%)",
"linear-gradient(135deg, hsl(var(--primary) / 0.25), transparent 55%, hsl(var(--accent) / 0.25))",
}}
/>
<div className="relative z-10">
<div className="inline-flex items-center gap-3 mb-20">
<div className="icon-tile w-12 h-12 icon-tile-blue">
<Sparkles size={24} color="#FFFFFF" />
</div>
<span className="font-bold text-2xl tracking-tight text-foreground">
{appName}
</span>
</div>
<div className="max-w-md space-y-8">
<h1 className="text-4xl font-black text-foreground leading-tight">
{t("auth.heroTitle")}
</h1>
<p className="text-muted-foreground text-lg leading-relaxed">
{t("auth.heroSubtitle")}
</p>
<div className="space-y-4 pt-4">
{features.map((feat) => (
<div
key={feat}
className="flex items-center gap-3 text-foreground/90 font-medium"
>
<CheckCircle2 className="h-5 w-5 text-primary shrink-0" />
<span>{feat}</span>
</div>
))}
</div>
</div>
</div>
<div className="relative z-10 text-sm text-muted-foreground font-medium">
&copy; {new Date().getFullYear()} {appName}. {t("auth.rights")}
</div>
</div>
{/* Right form panel */}
@@ -126,30 +90,7 @@ export default function LoginPage() {
</button>
</div>
{/* Mobile brand */}
<div className="lg:hidden mb-8 flex items-center gap-2">
<div className="icon-tile w-10 h-10 icon-tile-blue">
<Sparkles size={20} color="#FFFFFF" />
</div>
<span className="font-bold text-2xl tracking-tight">{appName}</span>
</div>
<div className="w-full max-w-[400px]">
<div
className={`mb-8 ${
dir === "rtl"
? "text-center lg:text-right"
: "text-center lg:text-left"
}`}
>
<h2 className="text-2xl font-bold tracking-tight">
{t("auth.welcomeBack")}
</h2>
<p className="text-muted-foreground mt-1 text-sm">
{t("auth.subtitle")}
</p>
</div>
<div className="glass-panel rounded-2xl p-6 sm:p-8">
<form onSubmit={handleSubmit} className="space-y-5" noValidate>
{/* Username */}
@@ -259,6 +200,13 @@ export default function LoginPage() {
</form>
</div>
</div>
{/* Admin-editable footer */}
{footerText && (
<div className="absolute bottom-4 inset-x-0 text-center text-xs text-muted-foreground px-4">
&copy; {new Date().getFullYear()} {appName}. {footerText}
</div>
)}
</div>
</div>
);
Binary file not shown.

After

Width:  |  Height:  |  Size: 219 KiB

@@ -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 {
+13 -1
View File
@@ -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
+18
View File
@@ -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(),
});
+2
View File
@@ -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()),
});