├── index.html ├── styles.css └── script.js /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | تذكيراتي 7 | 8 | 9 | 10 |
11 |
12 |
13 | أقرب تذكير: 14 | لا يوجد تذكيرات 15 |
16 | 17 | 18 |
19 |
20 | 21 | 22 | 102 | 103 |
104 | 105 |
106 |
107 |

قائمة التذكيراتتواصل معنا

108 |
109 |
110 |
111 |

التذكيرات المنتهية

112 |
113 |
114 |
115 |
116 | 117 | 118 | 119 | 131 | 132 | 158 | 159 | 160 | 161 | 162 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | -------------------------------------------------------------------------------- /styles.css: -------------------------------------------------------------------------------- 1 | @font-face{font-family:system-ui2;src:local("Segoe UI"),local("Tahoma"),local("Arial");} 2 | :root{--bg:#0f1216;--card:#151a21;--text:#e6e9ef;--muted:#9aa4b2;--accent:#5b9cff;--accent-2:#7c5bff;--danger:#ff6b6b;--ok:#6bff9d;--border:#222832} 3 | *{box-sizing:border-box} 4 | html,body{height:100%} 5 | body{margin:0;background:linear-gradient(180deg,#0e1216 0%,#0b1014 100%);color:var(--text);font-family:system-ui2,system-ui,-apple-system,"Segoe UI",Roboto,sans-serif} 6 | .app-header{display:flex;justify-content:flex-start;align-items:center;padding:20px 24px;border-bottom:1px solid var(--border);background:rgba(21,26,33,.6);backdrop-filter:saturate(130%) blur(6px);position:sticky;top:0;z-index:10} 7 | .app-header h1{display:none} 8 | .header-right{display:flex;gap:12px;align-items:center;flex:1} 9 | .next-banner{display:flex;gap:10px;align-items:center;font-size:14px;color:var(--muted)} 10 | .next-banner{margin-inline-end:auto} 11 | .next-banner{text-align:start} 12 | .next-banner strong{color:var(--accent)} 13 | .container{max-width:1040px;margin:24px auto;padding:0 16px} 14 | .lists-grid{display:grid;grid-template-columns:1fr 1fr;gap:16px} 15 | @media (max-width: 900px){.lists-grid{grid-template-columns:1fr}} 16 | .card{background:transparent;border:none;border-radius:14px;margin-bottom:20px;overflow:hidden} 17 | .card-title{margin:0;padding:16px 18px;border-bottom:1px solid var(--border);font-size:18px;display:flex;align-items:center;justify-content:space-between} 18 | .contact-inline{display:flex;align-items:center;gap:8px} 19 | .contact-hint{font-size:12px;color:var(--muted);font-weight:600;cursor:pointer} 20 | #contactIcon{color:#fff} 21 | .grid{display:grid;grid-template-columns:repeat(2,1fr);gap:14px;padding:16px} 22 | .field{display:flex;flex-direction:column;gap:8px} 23 | .inline{display:flex;gap:16px} 24 | .hidden{display:none} 25 | label{font-size:13px;color:var(--muted)} 26 | input,select,textarea{border:1px solid var(--border);background:#0f141b;color:var(--text);border-radius:10px;padding:10px 12px;font-size:14px;outline:none} 27 | input:focus,select:focus,textarea:focus{border-color:var(--accent)} 28 | .span-2{grid-column:span 2} 29 | .actions{display:flex;gap:10px;justify-content:flex-start} 30 | button{appearance:none;border:none;border-radius:10px;padding:10px 14px;font-size:14px;cursor:pointer} 31 | .primary{background:linear-gradient(90deg,var(--accent),var(--accent-2));color:#fff} 32 | .ghost{background:transparent;border:1px solid var(--border);color:var(--muted)} 33 | .lang-switch{background:#0f141b;color:var(--text);border:1px solid var(--border);border-radius:10px;padding:8px 10px;font-size:14px} 34 | .icon-btn{width:32px;height:32px;display:grid;place-items:center;background:transparent;color:var(--text);border:none;border-radius:8px;font-size:18px;cursor:pointer;padding:0} 35 | .overlay{position:fixed;inset:0;background:rgba(0,0,0,.35);backdrop-filter:blur(2px);z-index:15} 36 | .popover{position:fixed;top:72px;right:24px;width:560px;max-width:calc(100% - 48px);max-height:70vh;overflow:auto;background:var(--card);border:1px solid var(--border);border-radius:16px;z-index:20;box-shadow:0 12px 40px rgba(0,0,0,.45)} 37 | .popover-title{margin:0;padding:12px 16px;border-bottom:1px solid var(--border);font-size:16px} 38 | .popover .grid{padding:16px} 39 | .list{display:flex;flex-direction:column;padding:12px} 40 | .group-divider{border-top:1px solid #ffffff;margin:10px 0;opacity:.65} 41 | .group-header{display:flex;justify-content:flex-end;padding:6px 8px;color:#fff;opacity:.95;font-size:16px;font-weight:700} 42 | .group-header .date{margin-inline-start:8px} 43 | .reminder-item{border:1px solid var(--border);border-radius:12px;margin-bottom:12px;overflow:hidden;background:#0f141b} 44 | .reminder-item.p-critical{border-color:var(--danger);background:rgba(255,107,107,.12)} 45 | .reminder-item.p-low{border-color:rgba(107,255,157,.35);background:rgba(22,43,29,.5)} 46 | .reminder-head{display:flex;justify-content:space-between;align-items:center;padding:12px 14px;gap:10px} 47 | .title-type{display:flex;gap:8px;align-items:center;justify-content:flex-start;direction:ltr;width:100%} 48 | .title{flex:1;text-align:left;font-weight:600} 49 | .type{margin-left:auto} 50 | .type{font-size:12px;color:var(--muted);background:#0c1117;border:1px solid var(--border);padding:4px 8px;border-radius:999px;white-space:nowrap} 51 | .meta{display:flex;gap:10px;align-items:center} 52 | .due{font-size:15px;color:var(--muted);font-weight:700} 53 | .countdown{font-weight:600;color:var(--accent);margin:0} 54 | .timers{display:flex;gap:10px;align-items:baseline;margin-bottom:8px} 55 | .toggle{width:32px;height:32px;display:grid;place-items:center;background:#0c1117;color:var(--text);border:1px solid var(--border)} 56 | .toggle.active{transform:rotate(180deg)} 57 | .expand-toggle{appearance:none;background:transparent;border:none;color:var(--muted);font-size:12px;display:flex;align-items:center;gap:6px;padding:6px 14px;margin-top:4px;cursor:pointer} 58 | .expand-icon{font-size:12px;transition:transform .25s ease} 59 | .expand-toggle.active .expand-icon{transform:rotate(180deg)} 60 | .expandable{overflow:hidden;max-height:0;transition:max-height .3s ease,opacity .3s ease;opacity:.0} 61 | .expandable.open{max-height:600px;opacity:1} 62 | .reminder-body{padding:12px 14px;border-top:1px dashed var(--border)} 63 | .row{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:8px} 64 | .chip{font-size:12px;color:#fff;background:#1a2330;border:1px solid var(--border);padding:6px 8px;border-radius:999px} 65 | .chip.points{background:#162b1d} 66 | .chip.delivery{background:#231a2f} 67 | .chip.room{background:#1f2430} 68 | .details{white-space:pre-wrap;line-height:1.7;margin:8px 0;color:#d4d9e1} 69 | .images{display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:10px} 70 | .images img{width:100%;height:140px;object-fit:cover;border-radius:10px;border:1px solid var(--border)} 71 | .overdue{color:var(--danger)} 72 | .ok{color:var(--ok)} 73 | .app-footer{padding:16px;border-top:1px solid var(--border);text-align:center;color:var(--muted);position:fixed;left:0;right:0;bottom:0;background:rgba(21,26,33,.6);backdrop-filter:saturate(130%) blur(6px);z-index:5} 74 | .cta-list{display:flex;flex-direction:column;gap:12px;padding:16px;align-items:center} 75 | .cta-subtitle{padding:0 2px;color:var(--muted);font-weight:700} 76 | .cta-box{display:flex;align-items:center;justify-content:center;text-align:center;gap:10px;border:1px solid var(--border);border-radius:14px;padding:10px 12px;cursor:pointer;font-weight:600;width:360px;max-width:90%;transition:box-shadow .25s ease,transform .15s ease,filter .15s ease} 77 | .cta-box.green{background:#162b1d;color:#c6ffd7;border-color:rgba(107,255,157,.35)} 78 | .cta-box.blue{background:#13243a;color:#c7e2ff;border-color:#2d4a73} 79 | .cta-icon{font-size:18px} 80 | .cta-number{margin:0;font-weight:700} 81 | .close-btn{appearance:none;border:none;background:transparent;color:var(--muted);font-size:18px;border-radius:8px;cursor:pointer} 82 | .icon{width:20px;height:20px;border-radius:999px;display:inline-block;background-size:cover;background-position:center;background-repeat:no-repeat} 83 | .cta-box:hover{transform:translateY(-1px)} 84 | .cta-box.green:hover{box-shadow:0 0 0 2px rgba(37,211,102,.35),0 0 12px rgba(37,211,102,.25)} 85 | .cta-box.blue:hover{box-shadow:0 0 0 2px rgba(42,171,238,.35),0 0 12px rgba(42,171,238,.25)} 86 | .cta-box:active{filter:brightness(.9)} 87 | .glow{animation:pulseGlow 1.6s ease-in-out infinite} 88 | .glow.green{--g:#25D366} 89 | .glow.blue{--g:#2AABEE} 90 | @keyframes pulseGlow{0%{box-shadow:0 0 0 0 rgba(0,0,0,0)}50%{box-shadow:0 0 0 2px color-mix(in oklab,var(--g) 50%, transparent),0 0 18px color-mix(in oklab,var(--g) 35%, transparent)}100%{box-shadow:0 0 0 0 rgba(0,0,0,0)}} 91 | .icon-whatsapp{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Ccircle cx='12' cy='12' r='12' fill='%2325D366'/%3E%3Cpath fill='%23fff' d='M16.56 14.3c-.26-.13-1.53-.76-1.77-.84-.24-.09-.41-.13-.58.13-.17.26-.67.84-.82 1.02-.15.17-.3.19-.56.06-.26-.13-1.09-.4-2.08-1.29-.77-.68-1.29-1.52-1.44-1.78-.15-.26-.02-.4.11-.53.11-.11.26-.28.39-.43.13-.15.17-.26.26-.43.09-.17.04-.32-.02-.45-.06-.13-.58-1.39-.8-1.9-.21-.5-.43-.43-.58-.44-.15-.01-.32-.01-.49-.01-.17 0-.45.06-.69.32-.24.26-.9.88-.9 2.14s.93 2.49 1.06 2.66c.13.17 1.84 2.81 4.46 3.93.62.27 1.11.43 1.49.55.63.2 1.21.17 1.67.1.51-.08 1.53-.62 1.75-1.22.22-.6.22-1.11.15-1.22-.06-.11-.24-.17-.5-.3z'/%3E%3C/svg%3E")} 92 | .icon-telegram{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Ccircle cx='12' cy='12' r='12' fill='%232AABEE'/%3E%3Cpath fill='%23fff' d='M18.9 7.4c-.2-.2-.5-.2-.8-.1L5.6 12.1c-.3.1-.5.3-.5.6 0 .3.2.5.5.6l3.4 1.1 1.4 3.5c.1.3.3.5.6.5h.1c.3 0 .5-.2.6-.4l1.5-2.5 3.5 2.4c.1.1.3.1.4.1.1 0 .3 0 .4-.1.2-.1.3-.3.4-.5l2-8c.1-.3 0-.6-.3-.8zM9.7 13.9l6.7-4.2-4.9 5.6c-.1.1-.2.3-.2.4l-.7 1.2-.9-2.2c0-.3.1-.5.3-.8z'/%3E%3C/svg%3E")} 93 | .img-preview-modal{position:fixed;inset:0;display:flex;align-items:center;justify-content:center;z-index:20;background:transparent;padding:16px} 94 | .img-preview-modal img{max-width:92vw;max-height:92vh;border-radius:12px;border:1px solid var(--border);box-shadow:0 12px 32px rgba(0,0,0,.5)} 95 | .img-preview-close{position:fixed;top:16px;right:20px;background:transparent;color:#fff;border:none;border-radius:8px;width:32px;height:32px;display:grid;place-items:center;cursor:pointer} 96 | .dev-badge{position:fixed;top:10px;left:10px;background:#4a0f14;color:#fff;border:1px solid rgba(255,0,0,.6);border-radius:10px;padding:6px 10px;font-size:12px;font-weight:700;z-index:30} 97 | .delete-btn{color:var(--danger);background:transparent;border:none;font-size:16px;cursor:pointer} 98 | .force-ltr{direction:ltr;unicode-bidi:plaintext;text-align:left} 99 | -------------------------------------------------------------------------------- /script.js: -------------------------------------------------------------------------------- 1 | 2 | const $=s=>document.querySelector(s);const $$=s=>Array.from(document.querySelectorAll(s)); 3 | const i18n={ 4 | ar:{app_title:'التذكيرات',next_label:'أقرب تذكير:',add_button:'إضافة تذكير',popover_title:'إضافة تذكير',list_title:'قائمة التذكيرات',title_label:'العنوان',type_label:'النوع',start_label:'البداية',start_started:'بدأت',start_schedule:'تاريخ بداية',due_label:'آخر موعد للتسليم',delivery_label:'طريقة التسليم',delivery_inperson:'حضوري',room_placeholder:'رقم القاعة',points_label:'الدرجة',points_placeholder:'مثلاً 25%',priority_label:'الأهمية',priority_critical:'مهم جداً',priority_normal:'مهم',priority_low:'غير مهم',details_label:'التفاصيل/المطلوب',details_placeholder:'اكتب المطلوب',images_label:'صور للمطلوب',submit:'إضافة',reset:'مسح',loading_text:'جاري التحميل…',sync_failed:'فشلت المزامنة',due_prefix:'التسليم: ',delivery_prefix:'التسليم: ',room_prefix:'القاعة: ',points_prefix:'الدرجة: ',left_prefix:'متبقي: ',seconds_suffix:' ثانية',overdue:'متأخر',no_reminders:'لا يوجد تذكيرات',toggle_aria:'عرض التفاصيل',type_exam:'اختبار',type_assignment:'اسايمنت/واجبات',type_report:'ريبورت/تقرير',type_final:'اختبار نهائي',contact_title:'معلومات التواصل مع المطور AHMAD',wa_label:'واتساب',telegram_try:'جرّب أيضاً بوت التحضير التلقائي',telegram_label:'UAttend'}, 5 | en:{app_title:'Reminders',next_label:'Next due:',add_button:'Add Reminder',popover_title:'Add Reminder',list_title:'Reminders',title_label:'Title',type_label:'Type',start_label:'Start',start_started:'Started',start_schedule:'Start date',due_label:'Due date',delivery_label:'Delivery',delivery_inperson:'In person',room_placeholder:'Room number',points_label:'Points',points_placeholder:'e.g. 25%',priority_label:'Priority',priority_critical:'Very important',priority_normal:'Important',priority_low:'Not important',details_label:'Details',details_placeholder:'Describe requirements',images_label:'Images',submit:'Add',reset:'Clear',loading_text:'Loading…',sync_failed:'Sync failed',due_prefix:'Due: ',delivery_prefix:'Delivery: ',room_prefix:'Room: ',points_prefix:'Points: ',left_prefix:'Left: ',seconds_suffix:' s',overdue:'Overdue',no_reminders:'No reminders',toggle_aria:'Show details',type_exam:'Exam',type_assignment:'Assignment',type_report:'Report',type_final:'Final',contact_title:'Contact with developer AHMAD',wa_label:'WhatsApp',telegram_try:'Try also the auto attendance bot',telegram_label:'UAttend'} 6 | }; 7 | // overrides 8 | i18n.ar.title_label='العنوان (اختياري)'; 9 | i18n.ar.subject_label='اسم المادة'; 10 | i18n.ar.type_exam='اختبار شهري'; 11 | i18n.ar.type_presentation='برزنتيشن/عرض تقديمي'; 12 | i18n.ar.day_suffix=' يوم '; 13 | i18n.ar.hour_suffix=' ساعة '; 14 | i18n.ar.minute_suffix=' دقيقة '; 15 | i18n.en.title_label='Title (optional)'; 16 | i18n.en.subject_label='Subject'; 17 | i18n.en.type_exam='Monthly Exam'; 18 | i18n.en.type_presentation='Presentation'; 19 | i18n.ar.type_project='مشروع/بروجكت'; 20 | i18n.en.type_project='Project'; 21 | i18n.en.day_suffix=' d '; 22 | i18n.en.hour_suffix=' h '; 23 | i18n.en.minute_suffix=' m '; 24 | // labels for details expansion 25 | i18n.ar.more_details='المزيد من التفاصيل'; 26 | i18n.en.more_details='More details'; 27 | i18n.ar.no_details='لا يوجد تفاصيل'; 28 | i18n.en.no_details='No details'; 29 | i18n.ar.expired_title='التذكيرات المنتهية'; 30 | i18n.en.expired_title='Expired Reminders'; 31 | i18n.ar.elapsed_prefix='منذ: '; 32 | i18n.en.elapsed_prefix='Elapsed: '; 33 | i18n.ar.no_expired='لا يوجد تذكيرات منتهية'; 34 | i18n.en.no_expired='No expired reminders'; 35 | i18n.ar.contact_hint='تواصل معنا'; 36 | i18n.en.contact_hint='Contact Us'; 37 | i18n.ar.footer_rights='جميع الحقوق محفوظة لـ Ahmad Alhomsi © 2025'; 38 | i18n.en.footer_rights='All rights reserved to Ahmad Alhomsi © 2025'; 39 | let fsdb=null;let authReady=Promise.resolve(); 40 | 41 | // labels for expired section and elapsed 42 | i18n.ar.expired_title='التذكيرات المنتهية'; 43 | i18n.en.expired_title='Expired Reminders'; 44 | i18n.ar.elapsed_prefix='منذ: '; 45 | i18n.en.elapsed_prefix='Elapsed: '; 46 | i18n.ar.no_expired='لا يوجد تذكيرات منتهية'; 47 | i18n.en.no_expired='No expired reminders'; 48 | const state={reminders:[],map:new Map(),expiredMap:new Map(),lang:localStorage.getItem('lang')||'ar',dev:false}; 49 | let uiBound=false; 50 | const fmtDate=t=>new Date(t).toLocaleString(state.lang==='ar'?'ar-SA':'en-US',{hour12:false}); 51 | const fmtTime=t=>new Date(t).toLocaleTimeString(state.lang==='ar'?'ar-SA':'en-US',{hour12:false,hour:'2-digit',minute:'2-digit'}); 52 | const fmtTimeEn=t=>new Date(t).toLocaleTimeString('en-US',{hour12:false,hour:'2-digit',minute:'2-digit'}); 53 | const fmtDateOnly=t=>{const d=new Date(t);const dd=String(d.getDate()).padStart(2,'0');const mm=String(d.getMonth()+1).padStart(2,'0');const yy=d.getFullYear();return `${dd}/${mm}/${yy}`}; 54 | const fmtDayName=t=>new Date(t).toLocaleDateString(state.lang==='ar'?'ar-SA':'en-US',{weekday:'long'}); 55 | const untilSeconds=t=>Math.floor((new Date(t).getTime()-Date.now())/1000); 56 | const id=()=>Math.random().toString(36).slice(2)+Date.now().toString(36); 57 | const toDataUrls=files=>Promise.all(Array.from(files).map(f=>new Promise(r=>{const fr=new FileReader();fr.onload=e=>r(e.target.result);fr.readAsDataURL(f);}))); 58 | const toBase64s=files=>Promise.all(Array.from(files).map(f=>new Promise(r=>{const fr=new FileReader();fr.onload=e=>{const s=String(e.target.result||'');const b=s.includes(',')?s.split(',')[1]:s;r({base64:b,name:f.name})};fr.readAsDataURL(f);}))); 59 | const uploadImages=async(files)=>{if(!files||!files.length)return[];const key=(window.imgbbKey||localStorage.getItem('imgbbKey')||'').trim();const b64=await toBase64s(files);const durls=await toDataUrls(files);if(!key)return durls;const urls=[];for(let i=0;i{try{const res=await fetch('reminders.json',{cache:'no-store'});if(!res.ok)return null;const arr=await res.json();return Array.isArray(arr)?arr:null}catch(_){return null}}; 61 | const initFirebase=()=>{try{const cfg=window.firebaseConfig||{};if(!cfg||!cfg.projectId){fsdb=null;return}firebase.initializeApp(cfg);fsdb=firebase.firestore();try{fsdb.settings({experimentalForceLongPolling:true})}catch(_){ }authReady=firebase.auth().signInAnonymously().catch(()=>{})}catch(_){fsdb=null}}; 62 | const dbOps={init:async()=>{},getAll:async()=>{if(!fsdb)return [];const snap=await fsdb.collection('reminders').orderBy('dueAt','asc').get();return snap.docs.map(d=>d.data())},add:async(obj)=>{if(!fsdb)return;await fsdb.collection('reminders').doc(String(obj.id)).set(obj,{merge:true})},delete:async(id)=>{if(!fsdb)return;await fsdb.collection('reminders').doc(String(id)).delete()}}; 63 | const typeLabel=v=>{const mapAr={'اختبار شهري':'type_exam','اسايمنت':'type_assignment','ريبورت':'type_report','برزنتيشن/عرض تقديمي':'type_presentation','اختبار نهائي':'type_final','مشروع':'type_project','بروجكت':'type_project'};const key=mapAr[v]||null;return key?i18n[state.lang][key]:(v||'')}; 64 | const render=()=>{ 65 | const list=$('#reminders'); 66 | const expiredList=$('#expired'); 67 | list.innerHTML=''; 68 | expiredList.innerHTML=''; 69 | state.map.clear(); 70 | state.expiredMap.clear(); 71 | const rank=p=>p==='critical'?0:p==='normal'?1:2; 72 | const now=Date.now(); 73 | const sorted=[...state.reminders].sort((a,b)=>{const ta=new Date(a.dueAt).getTime();const tb=new Date(b.dueAt).getTime();if(ta!==tb)return ta-tb;return rank(a.priority)-rank(b.priority)}); 74 | const upcoming=sorted.filter(r=>new Date(r.dueAt).getTime()>=now); 75 | const expired=sorted.filter(r=>new Date(r.dueAt).getTime(){ 77 | if(items.length===0)return; 78 | const groups=new Map(); 79 | items.forEach(rem=>{const key=new Date(rem.dueAt).toISOString().slice(0,10);if(!groups.has(key))groups.set(key,[]);groups.get(key).push(rem)}); 80 | let first=true; 81 | groups.forEach((gi)=>{ 82 | if(!first){const sep=document.createElement('div');sep.className='group-divider';rootList.appendChild(sep)} 83 | first=false; 84 | const header=document.createElement('div'); 85 | header.className='group-header'; 86 | const date=fmtDateOnly(gi[0].dueAt); 87 | const day=fmtDayName(gi[0].dueAt); 88 | header.innerHTML=`${day}${date}`; 89 | rootList.appendChild(header); 90 | gi.forEach(rem=>{ 91 | const tpl=document.getElementById('reminderTemplate'); 92 | const node=tpl.content.cloneNode(true); 93 | const root=node.querySelector('.reminder-item'); 94 | const title=node.querySelector('.title'); 95 | const type=node.querySelector('.type'); 96 | const due=node.querySelector('.due'); 97 | const countdown=node.querySelector('.countdown'); 98 | const expandToggle=node.querySelector('.expand-toggle'); 99 | const expandable=node.querySelector('.expandable'); 100 | const delivery=node.querySelector('.delivery'); 101 | const room=node.querySelector('.room'); 102 | const points=node.querySelector('.points'); 103 | const details=node.querySelector('.details'); 104 | const images=node.querySelector('.images'); 105 | title.textContent=rem.title; 106 | type.textContent=typeLabel(rem.type); 107 | due.textContent=fmtTimeEn(rem.dueAt); 108 | delivery.textContent=i18n[state.lang].delivery_prefix+(rem.delivery==='حضوري'?i18n[state.lang].delivery_inperson:rem.delivery); 109 | room.textContent=rem.delivery==='حضوري'&&rem.room?(i18n[state.lang].room_prefix+rem.room):''; 110 | points.textContent=i18n[state.lang].points_prefix+rem.points+'%'; 111 | delivery.classList.toggle('hidden',!delivery.textContent); 112 | room.classList.toggle('hidden',!room.textContent); 113 | points.classList.toggle('hidden',!points.textContent); 114 | details.textContent=rem.details||''; 115 | images.innerHTML=(rem.images||[]).map(src=>``).join(''); 116 | images.querySelectorAll('img').forEach(img=>img.addEventListener('click',()=>openImgPreview(img.src))); 117 | const expandText=node.querySelector('.expand-text'); 118 | if(expandText)expandText.textContent=i18n[state.lang].more_details; 119 | expandToggle?.addEventListener('click',()=>{ 120 | expandToggle.classList.toggle('active'); 121 | const open=expandable.classList.toggle('open'); 122 | details.classList.toggle('hidden',!open); 123 | images.classList.toggle('hidden',!open); 124 | if(open){if(!details.textContent && !images.innerHTML){details.textContent=i18n[state.lang].no_details}} 125 | }); 126 | root.dataset.id=rem.id; 127 | root.dataset.due=rem.dueAt; 128 | countdown.textContent=''; 129 | if(rem.priority==='critical')root.classList.add('p-critical'); 130 | else if(rem.priority==='low')root.classList.add('p-low'); 131 | if(state.dev){const del=document.createElement('button');del.className='delete-btn';del.textContent='🗑';del.addEventListener('click',async()=>{try{await dbOps.delete(rem.id);state.reminders=await dbOps.getAll();render()}catch(_){}});const head=root.querySelector('.reminder-head');head?.appendChild(del)} 132 | rootList.appendChild(node); 133 | if(isExpired){state.expiredMap.set(rem.id,{countdownEl:countdown,dueAt:rem.dueAt})} 134 | else {state.map.set(rem.id,{countdownEl:countdown,dueAt:rem.dueAt})} 135 | }); 136 | }); 137 | }; 138 | renderGroup(upcoming,list,false); 139 | renderGroup(expired,expiredList,true); 140 | updateTicks(); 141 | }; 142 | let popoverEl=null,overlayEl=null,imgPreviewEl=null,contactEl=null; 143 | const updateOverlay=()=>{const anyOpen=(popoverEl && !popoverEl.classList.contains('hidden'))||(imgPreviewEl && !imgPreviewEl.classList.contains('hidden'))||(contactEl && !contactEl.classList.contains('hidden'));if(overlayEl){overlayEl.classList.toggle('hidden',!anyOpen)}}; 144 | const openPop=()=>{popoverEl?.classList.remove('hidden');updateOverlay()}; 145 | const closePop=()=>{popoverEl?.classList.add('hidden');overlayEl?.classList.add('hidden')}; 146 | const updateTicks=()=>{const now=Date.now();let next=null;const pad=n=>String(n).padStart(2,'0');state.map.forEach((v)=>{const secs=Math.floor((new Date(v.dueAt).getTime()-now)/1000);if(secs>0){const d=Math.floor(secs/86400);let r=secs%86400;const h=Math.floor(r/3600);r%=3600;const m=Math.floor(r/60);const s=r%60;const text=i18n[state.lang].left_prefix+d+i18n[state.lang].day_suffix+pad(h)+':'+pad(m)+':'+pad(s);v.countdownEl.textContent=text;v.countdownEl.classList.remove('overdue');v.countdownEl.classList.add('ok');if(next===null||secs{const secs=Math.floor((now - new Date(v.dueAt).getTime())/1000);const d=Math.floor(secs/86400);let r=secs%86400;const h=Math.floor(r/3600);r%=3600;const m=Math.floor(r/60);const s=r%60;const text=i18n[state.lang].elapsed_prefix+d+i18n[state.lang].day_suffix+pad(h)+':'+pad(m)+':'+pad(s);v.countdownEl.textContent=text;v.countdownEl.classList.add('overdue');v.countdownEl.classList.remove('ok')});const nextEl=$('#nextCountdown');if(next===null){nextEl.textContent=i18n[state.lang].no_reminders}else{const d=Math.floor(next/86400);let r=next%86400;const h=Math.floor(r/3600);r%=3600;const m=Math.floor(r/60);const s=r%60;nextEl.textContent=d+i18n[state.lang].day_suffix+String(h).padStart(2,'0')+':'+String(m).padStart(2,'0')+':'+String(s).padStart(2,'0')}}; 147 | const tick=()=>{updateTicks()}; 148 | const bindForm=()=>{ 149 | const f=$('#reminderForm'); 150 | const btnSubmit=$('#btnSubmit'); 151 | const btnReset=$('#btnReset'); 152 | let isSubmitting=false; 153 | const startModeEls=$$('input[name="startMode"]'); 154 | const startAtEl=$('#startAt'); 155 | const deliveryEls=$$('input[name="delivery"]'); 156 | const roomEl=$('#room'); 157 | const toggleStart=()=>{const mode=startModeEls.find(x=>x.checked)?.value;if(mode==='schedule'){startAtEl.classList.remove('hidden');startAtEl.required=true}else{startAtEl.classList.add('hidden');startAtEl.required=false}}; 158 | const toggleRoom=()=>{const del=deliveryEls.find(x=>x.checked)?.value;if(del==='حضوري'){roomEl.disabled=false;roomEl.placeholder=i18n[state.lang].room_placeholder}else{roomEl.disabled=true;roomEl.value=''}}; 159 | startModeEls.forEach(x=>x.addEventListener('change',toggleStart)); 160 | deliveryEls.forEach(x=>x.addEventListener('change',toggleRoom)); 161 | toggleStart(); 162 | toggleRoom(); 163 | f.addEventListener('submit',async e=>{ 164 | e.preventDefault(); 165 | if(isSubmitting)return; isSubmitting=true; 166 | if(btnSubmit){btnSubmit.disabled=true;btnSubmit.textContent=i18n[state.lang].loading_text} 167 | if(btnReset){btnReset.disabled=true} 168 | const subject=f.subject.value.trim(); 169 | const title=f.title.value.trim(); 170 | const type=f.type.value; 171 | const startMode=f.querySelector('input[name="startMode"]:checked').value; 172 | const startAt=startMode==='schedule'?f.startAt.value:null; 173 | const dueAt=f.dueAt.value; 174 | const delivery=f.querySelector('input[name="delivery"]:checked').value; 175 | const room=delivery==='حضوري'?f.room.value.trim():''; 176 | const points=Number(f.points.value||0); 177 | const priority=f.priority.value; 178 | const details=f.details.value.trim(); 179 | const files=f.images.files; 180 | if(!(subject||title)||!dueAt){ 181 | isSubmitting=false; 182 | if(btnSubmit){btnSubmit.disabled=false;btnSubmit.textContent=i18n[state.lang].submit} 183 | if(btnReset){btnReset.disabled=false} 184 | return; 185 | } 186 | const imgs=files&&files.length?await uploadImages(files):[]; 187 | const obj={id:id(),subject,title:(subject?(subject+(title?' — '+title:'')):title),type,startAt:startMode==='started'?new Date().toISOString():startAt,dueAt:new Date(dueAt).toISOString(),delivery,room,points,priority,details,images:imgs}; 188 | try{ 189 | await dbOps.add(obj); 190 | state.reminders=await dbOps.getAll(); 191 | render(); 192 | f.reset(); 193 | toggleStart(); 194 | toggleRoom(); 195 | closePop(); 196 | if(btnSubmit)btnSubmit.textContent=i18n[state.lang].submit; 197 | }catch(_){ 198 | if(btnSubmit)btnSubmit.textContent=i18n[state.lang].sync_failed; 199 | } 200 | isSubmitting=false; 201 | if(btnSubmit)btnSubmit.disabled=false; 202 | if(btnReset)btnReset.disabled=false; 203 | }); 204 | }; 205 | const bindPopover=()=>{popoverEl=$('#addPopover');overlayEl=$('#overlay');const btn=$('#openAdd');const addClose=$('#addClose');if(btn&&!btn.dataset.bound){btn.dataset.bound='1';btn.addEventListener('click',()=>openPop())}if(addClose&&!addClose.dataset.bound){addClose.dataset.bound='1';addClose.addEventListener('click',()=>closePop())}overlayEl?.addEventListener('click',()=>{closePop();closeImgPreview();closeContact();overlayEl.classList.add('hidden')});document.addEventListener('keydown',e=>{if(e.key==='Escape'){closePop();closeImgPreview();closeContact();overlayEl.classList.add('hidden')}})}; 206 | const openImgPreview=(src)=>{if(!imgPreviewEl){imgPreviewEl=document.createElement('div');imgPreviewEl.id='imgPreview';imgPreviewEl.className='img-preview-modal hidden';document.body.appendChild(imgPreviewEl);imgPreviewEl.addEventListener('click',e=>{if(e.target===imgPreviewEl)closeImgPreview()})}imgPreviewEl.innerHTML='';const btn=imgPreviewEl.querySelector('.img-preview-close');btn?.addEventListener('click',()=>closeImgPreview());imgPreviewEl.classList.remove('hidden');overlayEl?.classList.remove('hidden')}; 207 | const closeImgPreview=()=>{if(imgPreviewEl){try{imgPreviewEl.remove()}catch(_){imgPreviewEl.classList.add('hidden')}imgPreviewEl=null}if(overlayEl){overlayEl.classList.add('hidden');overlayEl.style.display='none'}}; 208 | const openContact=()=>{contactEl=$('#contactPopover');if(contactEl){const t=$('#contactTitle');const wl1=$('#wa1Label');const wl2=$('#wa2Label');const tl=$('#tgLabel');if(t)t.textContent=i18n[state.lang].contact_title;if(wl1)wl1.textContent=i18n[state.lang].wa_label;if(wl2)wl2.textContent=i18n[state.lang].wa_label;if(tl)tl.textContent=i18n[state.lang].telegram_label;contactEl.classList.remove('hidden');updateOverlay()}}; 209 | const closeContact=()=>{contactEl=$('#contactPopover');if(contactEl)contactEl.classList.add('hidden');overlayEl?.classList.add('hidden')}; 210 | const applyFooter=()=>{const ft=document.getElementById('footerRights');if(ft)ft.textContent=i18n[state.lang].footer_rights}; 211 | const bindDevActivator=()=>{const ft=document.getElementById('footerRights');const badge=document.getElementById('devBadge');let clicks=[];if(ft&&!ft.dataset.bound){ft.dataset.bound='1';ft.addEventListener('click',()=>{const now=Date.now();clicks=clicks.filter(t=>now-t<=3000);clicks.push(now);if(clicks.length>=5){state.dev=true;badge?.classList.remove('hidden');render()}})}}; 212 | const applyContactI18n=()=>{const t=$('#contactTitle');const wl1=$('#wa1Label');const wl2=$('#wa2Label');const tl=$('#tgLabel');const tt=$('#tgTry');const ch=$('#contactHint');if(t)t.textContent=i18n[state.lang].contact_title;if(wl1)wl1.textContent=i18n[state.lang].wa_label;if(wl2)wl2.textContent=i18n[state.lang].wa_label;if(tl)tl.textContent=i18n[state.lang].telegram_label;if(tt)tt.textContent=i18n[state.lang].telegram_try;if(ch)ch.textContent=i18n[state.lang].contact_hint}; 213 | const applyI18n=()=>{document.documentElement.lang=state.lang;document.documentElement.dir=state.lang==='ar'?'rtl':'ltr';document.title=i18n[state.lang].app_title;const appTitle=$('#appTitle');const nextLabel=$('#nextLabel');const openAdd=$('#openAdd');const langIcon=$('#langIcon');const popTitle=$('#popoverTitle');const lblTitle=$('#lblTitle');const lblType=$('#lblType');const lblStart=$('#lblStart');const lblStartStarted=$('#lblStartStarted');const lblStartSchedule=$('#lblStartSchedule');const lblDue=$('#lblDue');const lblDelivery=$('#lblDelivery');const lblDeliveryInPerson=$('#lblDeliveryInPerson');const lblPoints=$('#lblPoints');const lblPriority=$('#lblPriority');const lblDetails=$('#lblDetails');const lblImages=$('#lblImages');const btnSubmit=$('#btnSubmit');const btnReset=$('#btnReset');const listTitle=$('#listTitle');const roomEl=$('#room');const pointsEl=$('#points');const detailsEl=$('#details');const optExam=$('#optExam');const optAssignment=$('#optAssignment');const optReport=$('#optReport');const optFinal=$('#optFinal');const optPC=$('#optPriorityCritical');const optPN=$('#optPriorityNormal');const optPL=$('#optPriorityLow');if(appTitle)appTitle.textContent=i18n[state.lang].app_title;if(nextLabel)nextLabel.textContent=i18n[state.lang].next_label;if(openAdd)openAdd.textContent=i18n[state.lang].add_button;if(langIcon)langIcon.textContent='🌐';if(popTitle)popTitle.textContent=i18n[state.lang].popover_title;if(lblTitle)lblTitle.textContent=i18n[state.lang].title_label;if(lblType)lblType.textContent=i18n[state.lang].type_label;if(lblStart)lblStart.textContent=i18n[state.lang].start_label;if(lblStartStarted)lblStartStarted.textContent=i18n[state.lang].start_started;if(lblStartSchedule)lblStartSchedule.textContent=i18n[state.lang].start_schedule;if(lblDue)lblDue.textContent=i18n[state.lang].due_label;if(lblDelivery)lblDelivery.textContent=i18n[state.lang].delivery_label;if(lblDeliveryInPerson)lblDeliveryInPerson.textContent=i18n[state.lang].delivery_inperson;if(lblPoints)lblPoints.textContent=i18n[state.lang].points_label;if(lblPriority)lblPriority.textContent=i18n[state.lang].priority_label;if(lblDetails)lblDetails.textContent=i18n[state.lang].details_label;if(lblImages)lblImages.textContent=i18n[state.lang].images_label;if(btnSubmit)btnSubmit.textContent=i18n[state.lang].submit;if(btnReset)btnReset.textContent=i18n[state.lang].reset;if(listTitle)listTitle.textContent=i18n[state.lang].list_title;if(roomEl)roomEl.placeholder=i18n[state.lang].room_placeholder;if(pointsEl)pointsEl.placeholder=i18n[state.lang].points_placeholder;if(detailsEl)detailsEl.placeholder=i18n[state.lang].details_placeholder;if(optExam)optExam.textContent=i18n[state.lang].type_exam;if(optAssignment)optAssignment.textContent=i18n[state.lang].type_assignment;if(optReport)optReport.textContent=i18n[state.lang].type_report;if(optFinal)optFinal.textContent=i18n[state.lang].type_final;if(optPC)optPC.textContent=i18n[state.lang].priority_critical;if(optPN)optPN.textContent=i18n[state.lang].priority_normal;if(optPL)optPL.textContent=i18n[state.lang].priority_low}; 214 | const applySubjectLabel=()=>{const lblSubject=$('#lblSubject');if(lblSubject)lblSubject.textContent=i18n[state.lang].subject_label;const lblTitle=$('#lblTitle');if(lblTitle)lblTitle.textContent=i18n[state.lang].title_label;const optExam=$('#optExam');if(optExam)optExam.textContent=i18n[state.lang].type_exam;const optAssign=$('#optAssignment');if(optAssign)optAssign.textContent=i18n[state.lang].type_assignment;const optReport=$('#optReport');if(optReport)optReport.textContent=i18n[state.lang].type_report;const optPresent=$('#optPresentation');if(optPresent)optPresent.textContent=i18n[state.lang].type_presentation;const optFinal=$('#optFinal');if(optFinal)optFinal.textContent=i18n[state.lang].type_final;const optProject=$('#optProject');if(optProject)optProject.textContent=i18n[state.lang].type_project;const expiredTitle=$('#expiredTitle');if(expiredTitle)expiredTitle.textContent=i18n[state.lang].expired_title}; 215 | const bindLangIcon=()=>{const btn=$('#langIcon');if(btn){btn.addEventListener('click',()=>{state.lang=state.lang==='ar'?'en':'ar';localStorage.setItem('lang',state.lang);applyI18n();applySubjectLabel();applyContactI18n();applyFooter();render()})}}; 216 | const bindContact=()=>{const hint=$('#contactHint');const close=$('#contactClose');const wa1=$('#wa1');const wa2=$('#wa2');const tg=$('#tg');if(hint)hint.addEventListener('click',()=>openContact());if(close)close.addEventListener('click',()=>closeContact());const openLink=u=>window.open(u,'_blank');if(wa1)wa1.addEventListener('click',()=>{wa1.classList.add('glow','green');openLink('https://wa.me/966500900329')});if(wa2)wa2.addEventListener('click',()=>{wa2.classList.add('glow','green');openLink('https://wa.me/601120721104')});if(tg)tg.addEventListener('click',()=>{tg.classList.add('glow','blue');openLink('https://t.me/UAttend_bot')})}; 217 | const startRealtime=()=>{try{fsdb.collection('reminders').orderBy('dueAt','asc').onSnapshot(snap=>{state.reminders=snap.docs.map(d=>d.data());render()})}catch(_){}} 218 | const boot=async()=>{await dbOps.init();applyI18n();applySubjectLabel();applyFooter();applyContactI18n();if(!uiBound){bindForm();bindPopover();bindLangIcon();bindContact();bindDevActivator();uiBound=true}initFirebase();await authReady;startRealtime();state.reminders=await dbOps.getAll();render();setInterval(tick,1000)}; 219 | document.addEventListener('DOMContentLoaded',boot); 220 | --------------------------------------------------------------------------------