nmhbv/anycoder-48fe0aba
0
1I’m improving your React application and converting it to a modern Next.js + Tailwind setup with an integrated content management panel and API routes. Below are all the necessary files for a clean, production-ready setup.2 3=== Dockerfile ===4FROM node:18-alpine AS deps5WORKDIR /app6COPY package.json package-lock.json* ./7RUN npm ci8 9FROM node:18-alpine AS builder10WORKDIR /app11COPY --from=deps /app/node_modules ./node_modules12COPY . .13RUN npm run build14 15FROM node:18-alpine AS runner16WORKDIR /app17ENV NODE_ENV=production18RUN addgroup --system --gid 1001 nodejs && adduser --system --uid 1001 nextjs19 20COPY --from=builder /app/public ./public21COPY --from=builder /app/.next/standalone ./22COPY --from=builder /app/.next/static ./.next/static23 24USER nextjs25EXPOSE 300026ENV PORT 300027CMD ["node", "server.js"]28 29=== package.json ===30{31 "name": "alandalus-institute",32 "version": "1.0.0",33 "private": true,34 "scripts": {35 "dev": "next dev -p 3000",36 "build": "next build",37 "start": "next start -p 3000"38 },39 "dependencies": {40 "cookie": "^0.6.0",41 "next": "14.2.4",42 "react": "18.3.1",43 "react-dom": "18.3.1"44 },45 "devDependencies": {46 "autoprefixer": "^10.4.20",47 "postcss": "^8.4.41",48 "tailwindcss": "^3.4.10"49 }50}51 52=== next.config.js ===53/** @type {import('next').NextConfig} */54const nextConfig = {55 reactStrictMode: true,56 output: 'standalone',57 images: {58 unoptimized: true59 }60};61 62module.exports = nextConfig;63 64=== tailwind.config.js ===65/** @type {import('tailwindcss').Config} */66module.exports = {67 content: [68 "./pages/**/*.{js,jsx}",69 "./components/**/*.{js,jsx}",70 "./lib/**/*.{js,jsx}"71 ],72 theme: {73 extend: {74 fontFamily: {75 'arabic': ['Noto Sans Arabic', 'system-ui', 'sans-serif'],76 'serif': ['Lora', 'ui-serif', 'serif'],77 'display': ['Cinzel', 'Lora', 'serif']78 },79 colors: {80 brand: {81 crimson: '#8f1d21',82 gold: '#c6a44d',83 teal: '#0f4c5c',84 umber: '#6b3e2e',85 olive: '#6b8a3a',86 sky: '#5d8aa8',87 sand: '#d8c89f',88 ink: '#2b241e',89 inkSoft: '#4a4138',90 base: '#f4ead1',91 paper: '#fcf6e6'92 }93 },94 boxShadow: {95 'paper': '0 12px 40px rgba(27,26,23,.22)'96 },97 borderRadius: {98 'xl': '1rem'99 }100 }101 },102 plugins: []103};104 105=== postcss.config.js ===106module.exports = {107 plugins: {108 tailwindcss: {},109 autoprefixer: {}110 }111};112 113=== styles/globals.css ===114@tailwind base;115@tailwind components;116@tailwind utilities;117 118@layer base {119 html {120 scroll-behavior: smooth;121 }122 body {123 @apply font-arabic text-brand-ink bg-gradient-to-br from-brand-sand/40 via-brand-base to-brand-sand/70;124 -webkit-font-smoothing: antialiased;125 -moz-osx-font-smoothing: grayscale;126 }127}128 129@layer components {130 .skip-link {131 @apply absolute -top-10 left-4 bg-brand-crimson text-white px-3 py-2 rounded-lg z-50 focus:top-4;132 }133 .container {134 @apply max-w-6xl mx-auto;135 }136 .section {137 @apply py-16 px-4 md:py-20;138 }139 .section-title {140 @apply text-3xl md:text-4xl font-display text-brand-ink mb-4;141 }142 .section-subtitle {143 @apply text-brand-inkSoft max-w-3xl mb-8;144 }145 .paper-card {146 @apply relative bg-brand-paper border border-brand-gold/20 rounded-xl shadow-paper overflow-hidden transition-transform duration-200 hover:-translate-y-1 hover:shadow-paper;147 }148 .paper-card::before {149 content: "";150 @apply absolute -right-6 -top-6 w-24 h-24 opacity-40 pointer-events-none;151 background:152 radial-gradient(closest-side, rgba(111,89,54,0.2), transparent 70%),153 conic-gradient(from 0deg, rgba(143,29,33,0.35) 0 25%, transparent 25% 50%, rgba(198,164,77,0.35) 50% 75%, transparent 75% 100%);154 mask: radial-gradient(closest-side, black 70%, transparent 72%);155 transform: rotate(25deg);156 }157 .btn {158 @apply inline-flex items-center gap-2 border border-black/20 px-4 py-2 rounded-xl font-semibold shadow-sm transition-all duration-150 hover:-translate-y-0.5;159 }160 .btn-primary {161 @apply bg-brand-crimson text-white shadow-brand-crimson/30;162 }163 .btn-ghost {164 @apply bg-white/60 text-brand-ink shadow-none hover:bg-white/80;165 }166 .tag {167 @apply inline-flex items-center gap-2 text-sm px-3 py-1 rounded-full border border-dashed border-brand-gold/40 bg-white/60 text-brand-umber;168 }169 .ornament {170 @apply w-full h-10 opacity-70 transform rotate-180;171 }172 .callout {173 @apply grid grid-cols-[auto,1fr] gap-3 p-4 rounded-xl border border-brand-gold/20 bg-white/60 shadow;174 }175 .icon {176 @apply w-10 h-10 grid place-items-center rounded-lg bg-gradient-to-br from-white to-brand-gold/40 border border-brand-gold/40 text-brand-umber font-bold;177 }178 .admin-card {179 @apply bg-brand-paper border border-brand-gold/20 rounded-xl shadow-paper p-4;180 }181 .table {182 @apply w-full border-separate;183 }184 .row-card {185 @apply flex items-center gap-3 p-3 rounded-lg border border-brand-gold/20 bg-white/70;186 }187}188 189=== lib/content.js ===190export function defaultContent() {191 const now = new Date().toISOString();192 return {193 about: {194 text: "تأسس على تلة俯瞰 المدينة القديمة، يمزج حرمنا بين أفنية وحدائق ومشربيات واستوديوهات مشرقة للاكتشاف. نلتزم بالعلوم الكلاسيكية والآداب الرفيعة والبحث المعاصر المنضبط بمبادئ أخلاقية وشعور بالجمال.",195 footerAbout: "موقع تجريبي للتطوير والاختبار.",196 cards: [197 {198 id: "card_1",199 title: "كلاسيكي ومعاصر",200 body: "من Arts الليبرالية إلى علم البيانات، يجمع منهجنا بين تفسير النصوص والمنطق والفلسفة والعلوم الطبيعية والحاسوبية.",201 tag: "منهج • توازن العقل والنقل"202 },203 {204 id: "card_2",205 title: "الفنون والهندسة",206 body: "ندرّب المهندسين والفنانين والمصممين على تقنيات ونظريات تمتد من أنماط الأفنية إلى التصميم الحاسوبي المعاصر.",207 tag: "استوديو • حرفة تلتقي بالكود"208 },209 {210 id: "card_3",211 title: "خدمة ومجتمع",212 body: "عيادات ومساعدة قانونية وميدان يتعاون مع المجتمعات المحلية لمعالجة قضايا العصر بتواضع ورعاية.",213 tag: "تشارك • علم في خدمة"214 }215 ]216 },217 news: [218 { id: "news_1", title: "افتتاح مركز الذكاء الاصطناعي الأخلاقي", body: "تم تدشين مختبر جديد للذكاء الاصطناعي المسؤول في حرم العلوم.", date: now, published: true },219 { id: "news_2", title: "ندوة الربيع للعلوم الإنسانية", body: "ندوة مفتوحة حول أخلاقيات البيانات والمجتمع.", date: now, published: true }220 ],221 contact: [222 { id: "contact_1", label: "البريد", value: "info@alandalus-institute.example", type: "email" },223 { id: "contact_2", label: "الهاتف", value: "+1 555‑0100", type: "phone" },224 { id: "contact_3", label: "العنوان", value: "شارع知识和智慧، المدينة القديمة", type: "text" }225 ],226 donation: [227 { id: "don_1", title: "منح دراسية", desc: "ادعم منح التفوق والاحتياجات", target: "annual", goal: 250000, raised: 82000, link: "#" },228 { id: "don_2", title: "مختبر الحاسوب", desc: "ترقية مختبرات علوم البيانات", target: "lab", goal: 180000, raised: 42000, link: "#" },229 { id: "don_3", title: "ترميم المكتبة", desc: "حفظ التراث وإعادة تأهيل قاعات القراءة", target: "library", goal: 320000, raised: 103000, link: "#" }230 ]231 };232}233 234=== lib/utils.js ===235export function uid(prefix = "id") {236 if (typeof crypto !== "undefined" && crypto.randomUUID) {237 return `${prefix}_${crypto.randomUUID()}`;238 }239 return `${prefix}_${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`;240}241 242export function formatDate(iso) {243 try {244 const d = new Date(iso);245 return new Intl.DateTimeFormat("ar-EG", { dateStyle: "full" }).format(d);246 } catch {247 return iso;248 }249}250 251=== components/Header.js ===252import { useState, useEffect } from "react";253 254export default function Header() {255 const [open, setOpen] = useState(false);256 const [adminMode, setAdminMode] = useState(false);257 const [hash, setHash] = useState("");258 259 useEffect(() => {260 const onHash = () => setHash(location.hash.replace("#", "") || "top");261 onHash();262 window.addEventListener("hashchange", onHash);263 return () => window.removeEventListener("hashchange", onHash);264 }, []);265 266 useEffect(() => {267 setAdminMode(document.body.dataset.adminOpen === "true");268 }, []);269 270 useEffect(() => {271 if (adminMode) {272 document.body.dataset.adminOpen = "true";273 } else {274 document.body.dataset.adminOpen = "false";275 }276 }, [adminMode]);277 278 return (279 <header className="sticky top-0 z-40 bg-white/80 backdrop-blur border-b border-brand-gold/20">280 <div className="container flex items-center justify-between py-3 px-4">281 <a className="flex items-center gap-3 text-brand-ink min-w-0" href="#top" aria-label="الصفحة الرئيسية">282 <div className="w-10 h-10 rounded-xl grid place-items-center font-display font-bold border-2 border-brand-gold/40 bg-gradient-to-br from-brand-gold/60 to-brand-gold text-brand-umber shadow-inner">283 AA284 </div>285 <div className="font-display font-bold text-lg md:text-xl">معهد الأندلس العالي</div>286 </a>287 288 <nav className="hidden md:flex items-center gap-6" aria-label="القائمة الرئيسية">289 <NavLink href="#about" active={hash === "about"}>عن الجامعة</NavLink>290 <NavLink href="#news" active={hash === "news"}>الأخبار</NavLink>291 <NavLink href="#contact" active={hash === "contact"}>تواصل معنا</NavLink>292 <NavLink href="#donate" active={hash === "donate"}>تبرع</NavLink>293 </nav>294 295 <div className="flex items-center gap-2">296 <button className="btn btn-ghost" onClick={() => setAdminMode(!adminMode)} aria-label="فتح نظام الإدارة">297 إدارة298 </button>299 <button300 className="md:hidden btn btn-ghost"301 onClick={() => setOpen(!open)}302 aria-expanded={open}303 aria-controls="mobileMenu"304 aria-label="فتح القائمة"305 >306 ☰ القائمة307 </button>308 </div>309 </div>310 {open && (311 <div id="mobileMenu" className="md:hidden border-t border-brand-gold/20 bg-white/90">312 <div className="container px-4 py-3 flex flex-col gap-2">313 <MobileNav href="#about">عن الجامعة</MobileNav>314 <MobileNav href="#news">الأخبار</MobileNav>315 <MobileNav href="#contact">تواصل معنا</MobileNav>316 <MobileNav href="#donate">تبرع</MobileNav>317 </div>318 </div>319 )}320 </header>321 );322}323 324function NavLink({ href, children, active }) {325 return (326 <a327 href={href}328 className={`px-3 py-2 rounded-lg font-medium transition-colors ${active ? "bg-brand-gold/20 underline underline-offset-4" : "text-brand-inkSoft hover:text-brand-ink hover:bg-brand-gold/10"}`}329 >330 {children}331 </a>332 );333}334 335function MobileNav({ href, children }) {336 return (337 <a href={href} className="px-3 py-2 rounded-lg text-brand-inkSoft hover:text-brand-ink hover:bg-brand-gold/10">338 {children}339 </a>340 );341}342 343=== components/Footer.js ===344export default function Footer() {345 return (346 <footer className="border-t border-brand-gold/20 bg-white/90 mt-16">347 <div className="container grid gap-8 md:grid-cols-3 py-8 px-4">348 <div>349 <h4 className="font-display text-brand-ink text-xl mb-2">معهد الأندلس العالي</h4>350 <p id="footerAbout" className="text-brand-inkSoft">351 {/* Filled by page */}352 </p>353 <p className="text-xs text-brand-inkSoft mt-2">354 مبني بـ{" "}355 <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" rel="noopener" className="underline hover:text-brand-ink">356 anycoder357 </a>358 </p>359 </div>360 <div>361 <h4 className="font-display text-brand-ink text-xl mb-2">روابط سريعة</h4>362 <ul className="space-y-1">363 <li><a href="#about" className="text-brand-inkSoft hover:text-brand-ink">عن الجامعة</a></li>364 <li><a href="#news" className="text-brand-inkSoft hover:text-brand-ink">الأخبار</a></li>365 <li><a href="#contact" className="text-brand-inkSoft hover:text-brand-ink">تواصل معنا</a></li>366 <li><a href="#donate" className="text-brand-inkSoft hover:text-brand-ink">تبرع</a></li>367 </ul>368 </div>369 <div>370 <h4 className="font-display text-brand-ink text-xl mb-2">حقوق</h4>371 <p className="text-brand-inkSoft">© <span id="year">{new Date().getFullYear()}</span> معهد الأندلس العالي</p>372 </div>373 </div>374 <Ornament />375 </footer>376 );377}378 379function Ornament() {380 return (381 <svg className="ornament" viewBox="0 0 1200 60" role="img" aria-hidden="true">382 <defs>383 <pattern id="ornament-border" patternUnits="userSpaceOnUse" width="120" height="40">384 <rect width="120" height="40" fill="none"></rect>385 <g stroke="#7c5c2e" strokeWidth="1.2" opacity="0.7">386 <path d="M10,20 Q20,2 30,20 T50,20 T70,20 T90,20 T110,20"></path>387 <path d="M10,22 Q20,38 30,22 T50,22 T70,22 T90,22 T110,22" opacity="0.6"></path>388 </g>389 <g fill="#b9923c" opacity="0.8">390 <polygon points="20,20 22,14 28,14 23.5,10 25.5,4 20,8 14.5,4 16.5,10 12,14 18,14"></polygon>391 <polygon points="60,20 62,14 68,14 63.5,10 65.5,4 60,8 54.5,4 56.5,10 52,14 58,14"></polygon>392 <polygon points="100,20 102,14 108,14 103.5,10 105.5,4 100,8 94.5,4 96.5,10 92,14 98,14"></polygon>393 </g>394 </pattern>395 </defs>396 <rect x="0" y="0" width="1200" height="60" fill="url(#ornament-border)"></rect>397 </svg>398 );399}400 401=== components/Hero.js ===402import { useEffect, useState } from "react";403 404export default function Hero() {405 const [hijri, setHijri] = useState("جارٍ تحميل التقويم…");406 const [gregorian, setGregorian] = useState("جارٍ تحميل التاريخ…");407 408 useEffect(() => {409 setGregorian(new Date().toLocaleDateString("ar-EG", { dateStyle: "full" }));410 setHijri(""); // You can integrate a Hijri API here if desired411 }, []);412 413 return (414 <section className="relative min-h-[72vh] grid place-items-center overflow-hidden border-b border-brand-gold/20">415 <div className="absolute inset-0 -z-10 bg-[radial-gradient(60%_50%_at_10%_20%,rgba(15,76,92,.25),transparent_60%),radial-gradient(60%_50%_at_90%_30%,rgba(143,29,33,.25),transparent_60%),linear-gradient(180deg,rgba(255,255,255,.75),rgba(255,255,255,.45))]" />416 <div417 className="absolute inset-0 opacity-30 mix-blend-multiply -z-10"418 style={{419 backgroundImage:420 "repeating-linear-gradient(45deg, transparent 0 22px, rgba(111,89,54,0.18) 22px 24px), repeating-linear-gradient(-45deg, transparent 0 22px, rgba(111,89,54,0.18) 22px 24px)",421 animation: "driftPattern 35s linear infinite",422 maskImage: "radial-gradient(75% 55% at 50% 35%, #000 60%, transparent 85%)"423 }}424 />425 <div className="container grid gap-4 px-4">426 <div className="flex flex-wrap gap-2">427 <span className="chip">{hijri}</span>428 <span className="chip">{gregorian}</span>429 </div>430 <h1 className="font-display text-4xl md:text-6xl text-brand-ink leading-tight">431 معهد الأندلس العالي432 </h1>433 <p className="text-brand-inkSoft max-w-3xl">434 حيث يلتقي التراث بالحداثة. مجتمع学者ين وطلاب يبني مستقبلاً أكثر أخلاقاً وجمالاً.435 </p>436 <div className="flex flex-wrap gap-3">437 <a href="#about" className="btn btn-primary">عن الجامعة</a>438 <a href="#donate" className="btn btn-ghost">تبرع الآن</a>439 </div>440 <Ornament />441 </div>442 </section>443 );444}445 446function Ornament() {447 return (448 <svg className="ornament" viewBox="0 0 1200 60" aria-hidden="true">449 <defs>450 <pattern id="ornament-border-hero" patternUnits="userSpaceOnUse" width="120" height="40">451 <rect width="120" height="40" fill="none"></rect>452 <g stroke="#7c5c2e" strokeWidth="1.2" opacity="0.7">453 <path d="M10,20 Q20,2 30,20 T50,20 T70,20 T90,20 T110,20"></path>454 <path d="M10,22 Q20,38 30,22 T50,22 T70,22 T90,22 T110,22" opacity="0.6"></path>455 </g>456 <g fill="#b9923c" opacity="0.8">457 <polygon points="20,20 22,14 28,14 23.5,10 25.5,4 20,8 14.5,4 16.5,10 12,14 18,14"></polygon>458 <polygon points="60,20 62,14 68,14 63.5,10 65.5,4 60,8 54.5,4 56.5,10 52,14 58,14"></polygon>459 <polygon points="100,20 102,14 108,14 103.5,10 105.5,4 100,8 94.5,4 96.5,10 92,14 98,14"></polygon>460 </g>461 </pattern>462 </defs>463 <rect x="0" y="0" width="1200" height="60" fill="url(#ornament-border-hero)"></rect>464 </svg>465 );466}467 468function Chip({ children }) {469 return <span className="px-3 py-1 rounded-full bg-white/70 border border-brand-gold/40 text-brand-umber text-sm">{children}</span>;470}471 472=== components/AdminPanel.js ===473import { useEffect, useMemo, useState } from "react";474import { uid } from "../lib/utils";475import { formatDate } from "../lib/utils";476 477export default function AdminPanel() {478 const [loggedIn, setLoggedIn] = useState(false);479 const [checking, setChecking] = useState(true);480 const [tab, setTab] = useState("about");481 const [envInfo, setEnvInfo] = useState("محلي");482 const [status, setStatus] = useState({}); // status for CRUD483 484 const [about, setAbout] = useState(null);485 const [news, setNews] = useState([]);486 const [contact, setContact] = useState([]);487 const [donation, setDonation] = useState([]);488 489 // seed status490 const [lastBackup, setLastBackup] = useState("");491 492 const load = async () => {493 setChecking(true);494 const res = await fetch("/api/content");495 const data = await res.json();496 setAbout(data.about);497 setNews(data.news || []);498 setContact(data.contact || []);499 setDonation(data.donation || []);500 setLastBackup(data.lastBackup ? formatDate(data.lastBackup) : "لم تُسجل نسخ بعد");501 setChecking(false);502 };503 504 useEffect(() => {505 load();506 }, []);507 508 useEffect(() => {509 // check login510 fetch("/api/admin/status").then(r => r.json()).then(d => {511 setLoggedIn(d.authenticated);512 setEnvInfo(d.env || "محلي");513 setChecking(false);514 });515 }, []);516 517 const onLogin = async (user, pass) => {518 setChecking(true);519 const res = await fetch("/api/admin/login", {520 method: "POST",521 headers: { "content-type": "application/json" },522 body: JSON.stringify({ username: user, password: pass })523 });524 const ok = res.ok;525 if (ok) await load();526 setLoggedIn(ok);527 setChecking(false);528 };529 530 const onLogout = async () => {531 await fetch("/api/admin/logout", { method: "POST" });532 setLoggedIn(false);533 };534 535 const onSaveAbout = async () => {536 setStatus(s => ({ ...s, about: "saving" }));537 const res = await fetch("/api/content/about", {538 method: "PUT",539 headers: { "content-type": "application/json" },540 body: JSON.stringify(about)541 });542 setStatus(s => ({ ...s, about: res.ok ? "saved" : "error" }));543 if (res.ok) await load();544 setTimeout(() => setStatus(s => ({ ...s, about: "" })), 1500);545 };546 547 const addAboutCard = () => {548 setAbout(a => ({ ...a, cards: [...a.cards, { id: uid("card"), title: "", body: "", tag: "" }] }));549 };550 551 const addNews = async () => {552 const item = { id: uid("news"), title: "", body: "", date: new Date().toISOString(), published: true };553 const res = await fetch("/api/content/news", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(item) });554 if (res.ok) await load();555 };556 557 const addContact = async () => {558 const item = { id: uid("contact"), label: "", value: "", type: "text" };559 const res = await fetch("/api/content/contact", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(item) });560 if (res.ok) await load();561 };562 563 const addDonation = async () => {564 const item = { id: uid("don"), title: "", desc: "", target: "general", goal: 0, raised: 0, link: "#" };565 const res = await fetch("/api/content/donation", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(item) });566 if (res.ok) await load();567 };568 569 const updateSection = async (section, item) => {570 const res = await fetch(`/api/content/${section}/${item.id}`, {571 method: "PUT",572 headers: { "content-type": "application/json" },573 body: JSON.stringify(item)574 });575 if (res.ok) await load();576 };577 578 const deleteItem = async (section, id) => {579 const res = await fetch(`/api/content/${section}/${id}`, { method: "DELETE" });580 if (res.ok) await load();581 };582 583 const exportData = async () => {584 const res = await fetch("/api/content");585 const data = await res.json();586 const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json" });587 const url = URL.createObjectURL(blob);588 const a = document.createElement("a");589 a.href = url;590 a.download = `alandalus-backup-${new Date().toISOString().slice(0,10)}.json`;591 a.click();592 URL.revokeObjectURL(url);593 };594 595 const importData = async (file) => {596 const text = await file.text();597 const json = JSON.parse(text);598 await fetch("/api/content", {599 method: "PUT",600 headers: { "content-type": "application/json" },601 body: JSON.stringify(json)602 });603 await load();604 };605 606 const resetDemo = async () => {607 await fetch("/api/content/reset", { method: "POST" });608 await load();609 };610 611 if (checking) {612 return (613 <div className="fixed inset-0 z-50 bg-white/70 backdrop-blur flex items-center justify-center">614 <div className="admin-card">جارٍ التحميل…</div>615 </div>616 );617 }618 619 return (620 <div className="fixed inset-0 z-40 overflow-y-auto bg-white/90 backdrop-blur border-t border-brand-gold/20">621 <div className="sticky top-0 bg-white/95 border-b border-brand-gold/20 px-4 py-3 flex items-center justify-between">622 <div className="flex items-center gap-3">623 <strong className="font-display text-brand-ink">لوحة إدارة المحتوى</strong>624 <span className="px-2 py-1 rounded-full border border-brand-gold/40 text-brand-umber text-xs">{envInfo}</span>625 <span className="px-2 py-1 rounded-full border border-brand-gold/40 text-brand-umber text-xs">626 {loggedIn ? "مسجل الدخول" : "غير مسجل الدخول"}627 </span>628 <span className="px-2 py-1 rounded-full border border-brand-gold/40 text-brand-umber text-xs">629 آخر نسخة: {lastBackup}630 </span>631 </div>632 <div className="flex items-center gap-2">633 <button className="btn btn-ghost" onClick={exportData}>تصدير</button>634 <label className="btn btn-ghost" style={{ cursor: "pointer" }}>635 استيراد636 <input type="file" className="hidden" accept="application/json" onChange={(e) => importData(e.target.files[0])} />637 </label>638 <button className="btn btn-ghost bg-red-50 text-red-700 border-red-200" onClick={resetDemo}>إعادة تعيين</button>639 {loggedIn ? (640 <button className="btn bg-brand-ink text-white" onClick={onLogout}>قفل</button>641 ) : null}642 <a className="btn btn-ghost" href="#top">خروج</a>643 </div>644 </div>645 646 {!loggedIn ? (647 <div className="container px-4 py-6">648 <LoginCard onLogin={onLogin} />649 </div>650 ) : (651 <div className="container px-4 py-6 space-y-6">652 <div className="flex flex-wrap gap-2">653 <button className={`btn ${tab === "about" ? "btn-primary" : "btn-ghost"}`} onClick={() => setTab("about")}>عن الجامعة</button>654 <button className={`btn ${tab === "news" ? "btn-primary" : "btn-ghost"}`} onClick={() => setTab("news")}>الأخبار</button>655 <button className={`btn ${tab === "contact" ? "btn-primary" : "btn-ghost"}`} onClick={() => setTab("contact")}>تواصل معنا</button>656 <button className={`btn ${tab === "donation" ? "btn-primary" : "btn-ghost"}`} onClick={() => setTab("donation")}>التبرع</button>657 </div>658 659 {tab === "about" && about && (660 <section className="admin-card">661 <div className="flex items-center justify-between mb-3">662 <h3 className="font-display text-brand-ink">إدارة: عن الجامعة</h3>663 <button className="btn btn-ghost" onClick={onSaveAbout}>664 حفظ665 {status.about === "saving" && <span className="text-xs">…</span>}666 {status.about === "saved" && <span className="text-xs text-green-700">تم</span>}667 {status.about === "error" && <span className="text-xs text-red-700">خطأ</span>}668 </button>669 </div>670 <div className="grid md:grid-cols-2 gap-6">671 <div className="space-y-3">672 <div>673 <label className="block font-semibold text-brand-inkSoft mb-1">النص التعريفي</label>674 <textarea675 className="w-full p-3 rounded-lg border border-brand-gold/40 bg-white/80"676 rows={6}677 value={about.text}678 onChange={e => setAbout({ ...about, text: e.target.value })}679 />680 </div>681 <div>682 <label className="block font-semibold text-brand-inkSoft mb-1">وصف الفوتر</label>683 <textarea684 className="w-full p-3 rounded-lg border border-brand-gold/40 bg-white/80"685 rows={3}686 value={about.footerAbout}687 onChange={e => setAbout({ ...about, footerAbout: e.target.value })}688 />689 </div>690 </div>691 <div>692 <div className="flex items-center justify-between mb-2">693 <h4 className="font-display text-brand-ink">البطاقات</h4>694 <button className="btn btn-ghost" onClick={addAboutCard}>إضافة بطاقة</button>695 </div>696 <div className="space-y-3">697 {about.cards.map((c, i) => (698 <div key={c.id} className="admin-card">699 <div className="grid md:grid-cols-3 gap-3">700 <div>701 <label className="block text-sm text-brand-inkSoft mb-1">العنوان</label>702 <input className="w-full p-2 rounded-lg border border-brand-gold/40 bg-white/80" value={c.title} onChange={e => {703 const val = e.target.value; setAbout(a => ({ ...a, cards: a.cards.map(x => x.id === c.id ? { ...x, title: val } : x) }));704 }} />705 </div>706 <div className="md:col-span-2">707 <label className="block text-sm text-brand-inkSoft mb-1">الوصف</label>708 <textarea className="w-full p-2 rounded-lg border border-brand-gold/40 bg-white/80" rows={2} value={c.body} onChange={e => {709 const val = e.target.value; setAbout(a => ({ ...a, cards: a.cards.map(x => x.id === c.id ? { ...x, body: val } : x) }));710 }} />711 </div>712 <div>713 <label className="block text-sm text-brand-inkSoft mb-1">الوسم</label>714 <input className="w-full p-2 rounded-lg border border-brand-gold/40 bg-white/80" value={c.tag || ""} onChange={e => {715 const val = e.target.value; setAbout(a => ({ ...a, cards: a.cards.map(x => x.id === c.id ? { ...x, tag: val } : x) }));716 }} />717 </div>718 <div className="flex items-end justify-end">719 <button className="btn btn-ghost bg-red-50 text-red-700 border-red-200" onClick={() => setAbout(a => ({ ...a, cards: a.cards.filter(x => x.id !== c.id) }))}>حذف</button>720 </div>721 </div>722 </div>723 ))}724 </div>725 </div>726 </div>727 </section>728 )}729 730 {tab === "news" && (731 <section className="admin-card">732 <div className="flex items-center justify-between mb-3">733 <h3 className="font-display text-brand-ink">إدارة: الأخبار</h3>734 <button className="btn btn-ghost" onClick={addNews}>خبر جديد</button>735 </div>736 <div className="space-y-3">737 {news.sort((a, b) => new Date(b.date) - new Date(a.date)).map(n => (738 <div key={n.id} className="row-card">739 <div className="flex-1">740 <div className="font-semibold text-brand-ink">{n.title || "بدون عنوان"}</div>741 <div className="text-xs text-brand-inkSoft">{formatDate(n.date)}</div>742 </div>743 <div className="flex items-center gap-2">744 <label className="text-xs text-brand-inkSoft flex items-center gap-1">745 <input746 type="checkbox"747 checked={n.published}748 onChange={e => updateSection("news", { ...n, published: e.target.checked })}749 />750 منشور751 </label>752 <button className="btn btn-ghost bg-red-50 text-red-700 border-red-200" onClick={() => deleteItem("news", n.id)}>حذف</button>753 </div>754 </div>755 ))}756 </div>757 </section>758 )}759 760 {tab === "contact" && (761 <section className="admin-card">762 <div className="flex items-center justify-between mb-3">763 <h3 className="font-display text-brand-ink">إدارة: تواصل معنا</h3>764 <button className="btn btn-ghost" onClick={addContact}>إضافة قناة</button>765 </div>766 <div className="space-y-3">767 {contact.map(c => (768 <div key={c.id} className="row-card">769 <div className="flex-1 grid md:grid-cols-3 gap-3">770 <input className="p-2 rounded-lg border border-brand-gold/40 bg-white/80" value={c.label} onChange={e => updateSection("contact", { ...c, label: e.target.value })} placeholder="التسمية" />771 <input className="p-2 rounded-lg border border-brand-gold/40 bg-white/80" value={c.value} onChange={e => updateSection("contact", { ...c, value: e.target.value })} placeholder="القيمة" />772 <select className="p-2 rounded-lg border border-brand-gold/40 bg-white/80" value={c.type} onChange={e => updateSection("contact", { ...c, type: e.target.value })}>773 <option value="text">نص</option>774 <option value="email">بريد</option>775 <option value="phone">هاتف</option>776 </select>777 </div>778 <button className="btn btn-ghost bg-red-50 text-red-700 border-red-200" onClick={() => deleteItem("contact", c.id)}>حذف</button>779 </div>780 ))}781 </div>782 </section>783 )}784 785 {tab === "donation" && (786 <section className="admin-card">787 <div className="flex items-center justify-between mb-3">788 <h3 className="font-display text-brand-ink">إدارة: التبرع</h3>789 <button className="btn btn-ghost" onClick={addDonation}>إضافة طريقة تبرع</button>790 </div>791 <div className="space-y-3">792 {donation.map(d => (793 <div key={d.id} className="row-card">794 <div className="flex-1 grid md:grid-cols-5 gap-3">795 <input className="p-2 rounded-lg border border-brand-gold/40 bg-white/80" value={d.title} onChange={e => updateSection("donation", { ...d, title: e.target.value })} placeholder