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.
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
@@ -110,6 +110,8 @@
|
||||
"siteNameEn": "اسم الموقع (إنجليزي)",
|
||||
"registrationOpen": "السماح بإنشاء حسابات",
|
||||
"registrationOpenHint": "عند الإيقاف، الأدمن فقط يقدر ينشئ مستخدمين جدد.",
|
||||
"footerTextAr": "نص التذييل (عربي)",
|
||||
"footerTextEn": "نص التذييل (إنجليزي)",
|
||||
"nav": {
|
||||
"dashboard": "الرئيسية",
|
||||
"apps": "التطبيقات",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
© {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">
|
||||
© {new Date().getFullYear()} {appName}. {footerText}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(),
|
||||
});
|
||||
|
||||
|
||||
@@ -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()),
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user