aboalaa147's picture
Initial deployment
eb6a2f9
Raw
History Blame Contribute Delete
64.2 kB
// import { useState, useEffect } from 'react';
// import { useNavigate } from 'react-router-dom';
// import { useLanguage } from '../../lib/languageContext';
// import { LanguageSwitcher } from '../LanguageSwitcher';
// // ─── Types ────────────────────────────────────────────────────
// type InterviewStatus =
// | 'loading' // جاري جلب البيانات
// | 'no_interview' // لا يوجد موعد بعد
// | 'scheduled' // موعد محدد — الزر معطل لحد ما يحين الوقت
// | 'joinable' // حان وقت المقابلة — الزر مفعّل
// | 'completed'; // انتهت المقابلة
// interface InterviewData {
// scheduledAt: string; // ISO datetime e.g. "2026-03-25T14:00:00"
// meetingLink: string; // رابط الاجتماع
// status: 'PENDING' | 'COMPLETED' | 'CANCELLED';
// }
// // ─── Date helpers ─────────────────────────────────────────────
// const AR_DAYS = ['الأحد','الاثنين','الثلاثاء','الأربعاء','الخميس','الجمعة','السبت'];
// const AR_MONTHS = ['يناير','فبراير','مارس','أبريل','مايو','يونيو','يوليو','أغسطس','سبتمبر','أكتوبر','نوفمبر','ديسمبر'];
// function fmtDateAr(d: Date) {
// return `${AR_DAYS[d.getDay()]}، ${d.getDate()} ${AR_MONTHS[d.getMonth()]} ${d.getFullYear()}`;
// }
// function fmtDateEn(d: Date) {
// return d.toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' });
// }
// function fmtTimeAr(d: Date) {
// const h = d.getHours();
// const m = d.getMinutes().toString().padStart(2, '0');
// const h12 = h === 0 ? 12 : h > 12 ? h - 12 : h;
// return `${h12}:${m} ${h < 12 ? 'صباحاً' : 'مساءً'}`;
// }
// function fmtTimeEn(d: Date) {
// return d.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true });
// }
// function timeUntil(d: Date, isAr: boolean): string {
// const diff = d.getTime() - Date.now();
// if (diff <= 0) return '';
// const days = Math.floor(diff / 86400000);
// const hours = Math.floor((diff % 86400000) / 3600000);
// const mins = Math.floor((diff % 3600000) / 60000);
// if (isAr) {
// if (days > 0) return `بعد ${days} يوم`;
// if (hours > 0) return `بعد ${hours} ساعة`;
// return `بعد ${mins} دقيقة`;
// }
// if (days > 0) return `in ${days} day${days > 1 ? 's' : ''}`;
// if (hours > 0) return `in ${hours} hour${hours > 1 ? 's' : ''}`;
// return `in ${mins} minute${mins > 1 ? 's' : ''}`;
// }
// // ─────────────────────────────────────────────────────────────
// // ⚠️ API_PLACEHOLDER — استبدل الداتا الاستاتيك دي بـ API حقيقي
// //
// // Endpoint المطلوب:
// // GET /api/sheikh/interview
// // Headers: Authorization: Bearer <token>
// //
// // Response لو في موعد:
// // {
// // "scheduledAt": "2026-03-25T14:00:00",
// // "meetingLink": "https://meet.google.com/xxx",
// // "status": "PENDING" | "COMPLETED" | "CANCELLED"
// // }
// //
// // Response لو مفيش موعد:
// // 404 Not Found
// //
// // ─────────────────────────────────────────────────────────────
// // ⬇️ الداتا الاستاتيك — احذفها لما تربط الـ API
// const STATIC_INTERVIEW: InterviewData | null = {
// // غيّر 'scheduled' لـ null لو عايز تشوف شاشة "لا يوجد موعد"
// // غيّر status لـ 'COMPLETED' لو عايز تشوف شاشة "انتهت المقابلة"
// scheduledAt: (() => {
// // موعد بعد 2 يوم من دلوقتي للاختبار
// const d = new Date();
// d.setDate(d.getDate() + 2);
// d.setHours(14, 0, 0, 0);
// return d.toISOString();
// })(),
// meetingLink: 'https://meet.google.com/abc-defg-hij',
// status: 'PENDING',
// };
// // ─── Main Component ───────────────────────────────────────────
// export function InterviewSchedule() {
// const { language } = useLanguage();
// const navigate = useNavigate();
// const isAr = language === 'ar';
// const [status, setStatus] = useState<InterviewStatus>('loading');
// const [interview, setInterview] = useState<InterviewData | null>(null);
// const [error, setError] = useState('');
// // ─────────────────────────────────────────────────────────
// // ⚠️ API_PLACEHOLDER — جلب بيانات المقابلة
// // استبدل الكود ده بـ:
// //
// // const token = localStorage.getItem('authToken') ?? '';
// // const res = await fetch('/api/sheikh/interview', {
// // headers: { Authorization: `Bearer ${token}` }
// // });
// // if (res.status === 404) { setStatus('no_interview'); return; }
// // if (!res.ok) throw new Error(`${res.status}`);
// // const data: InterviewData = await res.json();
// // processInterviewData(data);
// // ─────────────────────────────────────────────────────────
// useEffect(() => {
// // محاكاة loading لمدة ثانية ونص للاختبار
// const timer = setTimeout(() => {
// try {
// // ⬇️ ابدأ من هنا لما تربط الـ API — شيل الـ STATIC_INTERVIEW واستخدم response الـ API
// const data = STATIC_INTERVIEW;
// if (!data) {
// setStatus('no_interview');
// return;
// }
// processInterviewData(data);
// } catch (e: any) {
// setError(e.message || 'Unknown error');
// setStatus('no_interview');
// }
// }, 1200);
// return () => clearTimeout(timer);
// }, []);
// function processInterviewData(data: InterviewData) {
// setInterview(data);
// if (data.status === 'COMPLETED') { setStatus('completed'); return; }
// if (data.status === 'CANCELLED') { setStatus('no_interview'); return; }
// const scheduled = new Date(data.scheduledAt);
// // يصبح joinable لو الموعد جه أو باقي 5 دقائق أو أقل
// if (scheduled.getTime() - Date.now() <= 5 * 60 * 1000) {
// setStatus('joinable');
// } else {
// setStatus('scheduled');
// }
// }
// // Tick كل 30 ثانية عشان يتحول لـ joinable تلقائياً لما يحين الوقت
// useEffect(() => {
// if (!interview || status !== 'scheduled') return;
// const timer = setInterval(() => {
// const scheduled = new Date(interview.scheduledAt);
// if (scheduled.getTime() - Date.now() <= 5 * 60 * 1000) {
// setStatus('joinable');
// clearInterval(timer);
// }
// }, 30_000);
// return () => clearInterval(timer);
// }, [interview, status]);
// const scheduledDate = interview ? new Date(interview.scheduledAt) : null;
// const remaining = scheduledDate && status === 'scheduled'
// ? timeUntil(scheduledDate, isAr) : '';
// // ─── Render ─────────────────────────────────────────────────
// return (
// <>
// <style>{CSS}</style>
// <div className="iv-root" dir={isAr ? 'rtl' : 'ltr'}>
// <div className="iv-bg-circles" aria-hidden="true">
// <div className="iv-circle iv-c1" />
// <div className="iv-circle iv-c2" />
// <div className="iv-circle iv-c3" />
// </div>
// <div className="iv-grid-pattern" aria-hidden="true" />
// <div className="iv-lang"><LanguageSwitcher /></div>
// <div className="iv-card">
// {/* ══════════════ LOADING ══════════════ */}
// {status === 'loading' && (
// <div className="iv-center">
// <div className="iv-spinner" />
// <p className="iv-muted-sm">
// {isAr ? 'جاري تحميل بيانات المقابلة...' : 'Loading interview details...'}
// </p>
// </div>
// )}
// {/* ══════════════ NO INTERVIEW ══════════════ */}
// {status === 'no_interview' && (
// <div className="iv-center">
// <div className="iv-badge">
// {isAr ? 'المرحلة الأخيرة' : 'Final Step'}
// </div>
// <div className="iv-ornament"><span /><div className="iv-diamond" /><span /></div>
// <h1 className="iv-h1">
// {isAr ? 'مقابلة التحقق' : 'Verification Interview'}
// </h1>
// <p className="iv-sub">
// {isAr
// ? 'أنت على وشك إتمام انضمامك للمنصة. فريقنا سيحدد لك موعد المقابلة وسيصلك إشعار فور تحديده.'
// : "You're almost there. Our team will schedule your verification interview and notify you as soon as it's confirmed."}
// </p>
// <div className="iv-pills">
// {INFO_PILLS.map((p, i) => (
// <div className="iv-pill" key={i}>
// <span className="iv-pill-icon">{p.icon}</span>
// <span className="iv-pill-val">{isAr ? p.ar : p.en}</span>
// <span className="iv-pill-lbl">{isAr ? p.lAr : p.lEn}</span>
// </div>
// ))}
// </div>
// {/* Waiting indicator */}
// <div className="iv-waiting-box">
// <div className="iv-dots">
// <span className="iv-dot iv-dot-1" />
// <span className="iv-dot iv-dot-2" />
// <span className="iv-dot iv-dot-3" />
// </div>
// <p className="iv-waiting-txt">
// {isAr
// ? 'في انتظار تحديد الموعد من قِبل الفريق...'
// : 'Waiting for the team to schedule your slot...'}
// </p>
// </div>
// <p className="iv-footnote">
// {isAr
// ? 'ستصلك رسالة بريد إلكتروني فور تحديد الموعد'
// : "You'll receive an email once your slot is confirmed"}
// </p>
// </div>
// )}
// {/* ══════════════ SCHEDULED / JOINABLE ══════════════ */}
// {(status === 'scheduled' || status === 'joinable') && scheduledDate && (
// <div className="iv-center">
// <div className="iv-badge iv-badge-green">
// {isAr ? 'تم تحديد موعدك' : 'Interview Scheduled'}
// </div>
// <div className="iv-ornament"><span /><div className="iv-diamond" /><span /></div>
// <h1 className="iv-h1">
// {isAr ? 'مقابلة التحقق' : 'Verification Interview'}
// </h1>
// <p className="iv-sub">
// {isAr
// ? 'تم تحديد موعد مقابلتك. يُرجى الحضور في الوقت المحدد واستعداد كاميرتك وميكروفونك.'
// : 'Your interview has been scheduled. Please be ready on time with your camera and microphone.'}
// </p>
// {/* ───────────────────────────────────────────────
// Slot card
// البيانات دي جايه من:
// scheduledDate ← interview.scheduledAt (من الـ API)
// remaining ← محسوب تلقائياً من الوقت الحالي
// ─────────────────────────────────────────────── */}
// <div className="iv-slot-card">
// <div className="iv-slot-icon">
// <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
// <rect x="3" y="4" width="18" height="18" rx="3"/>
// <path d="M16 2v4M8 2v4M3 10h18"/>
// </svg>
// </div>
// <div className="iv-slot-info">
// <div className="iv-slot-date">
// {isAr ? fmtDateAr(scheduledDate) : fmtDateEn(scheduledDate)}
// </div>
// <div className="iv-slot-time">
// {isAr ? fmtTimeAr(scheduledDate) : fmtTimeEn(scheduledDate)}
// </div>
// {remaining && (
// <div className="iv-slot-badge">{remaining}</div>
// )}
// </div>
// </div>
// <div className="iv-pills">
// {INFO_PILLS.map((p, i) => (
// <div className="iv-pill" key={i}>
// <span className="iv-pill-icon">{p.icon}</span>
// <span className="iv-pill-val">{isAr ? p.ar : p.en}</span>
// <span className="iv-pill-lbl">{isAr ? p.lAr : p.lEn}</span>
// </div>
// ))}
// </div>
// {/* ───────────────────────────────────────────────
// Join button
// - لو joinable=true → زر مفعّل بيفتح interview.meetingLink (من الـ API)
// - لو joinable=false → شكل مقفول
// ─────────────────────────────────────────────── */}
// {status === 'joinable' ? (
// <a
// href={interview?.meetingLink ?? '#'}
// target="_blank"
// rel="noopener noreferrer"
// className="iv-join-btn"
// >
// <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" width="18" height="18">
// <path d="M15 10l4.553-2.069A1 1 0 0121 8.847v6.306a1 1 0 01-1.447.894L15 14M3 8a2 2 0 012-2h8a2 2 0 012 2v8a2 2 0 01-2 2H5a2 2 0 01-2-2V8z"/>
// </svg>
// {isAr ? 'انضم إلى المقابلة الآن' : 'Join Interview Now'}
// </a>
// ) : (
// <div className="iv-locked-btn">
// <div className="iv-lock-wrap">
// <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" width="18" height="18">
// <rect x="3" y="11" width="18" height="11" rx="2"/>
// <path d="M7 11V7a5 5 0 0110 0v4"/>
// </svg>
// </div>
// <div>
// <div className="iv-locked-title">
// {isAr ? 'سيُفتح الرابط قبل المقابلة بـ 5 دقائق' : 'Link opens 5 minutes before interview'}
// </div>
// <div className="iv-locked-sub">
// {isAr ? 'يُرجى الانتظار حتى يحين الموعد' : 'Please wait until the scheduled time'}
// </div>
// </div>
// </div>
// )}
// <div className="iv-notice">
// <div className="iv-notice-icon">
// <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" width="15" height="15">
// <circle cx="12" cy="12" r="10"/><path d="M12 8v4M12 16h.01"/>
// </svg>
// </div>
// <p>
// {isAr
// ? 'إذا واجهت أي مشكلة في الدخول تواصل معنا عبر support@platform.com'
// : 'If you have any issues joining, contact us at support@platform.com'}
// </p>
// </div>
// </div>
// )}
// {/* ══════════════ COMPLETED ══════════════ */}
// {status === 'completed' && (
// <div className="iv-center">
// <div className="iv-check-wrap">
// <svg viewBox="0 0 64 64" fill="none">
// <circle cx="32" cy="32" r="30" stroke="#16a34a" strokeWidth="2"
// strokeDasharray="190" className="iv-ring" />
// <path d="M20 33l8 8 16-18" stroke="#16a34a" strokeWidth="3"
// strokeLinecap="round" strokeLinejoin="round"
// strokeDasharray="40" className="iv-check" />
// </svg>
// </div>
// <h1 className="iv-h1">
// {isAr ? 'تمت المقابلة بنجاح' : 'Interview Completed'}
// </h1>
// <p className="iv-sub">
// {isAr
// ? 'شكراً لك على وقتك. سيتم مراجعة نتيجة المقابلة وإخطارك قريباً.'
// : "Thank you for your time. We'll review the interview results and notify you soon."}
// </p>
// <button className="iv-primary-btn" onClick={() => navigate('/sheikh/dashboard')}>
// {isAr ? 'الذهاب إلى لوحة التحكم' : 'Go to Dashboard'}
// </button>
// </div>
// )}
// </div>
// </div>
// </>
// );
// }
// // ─── Static data ──────────────────────────────────────────────
// const INFO_PILLS = [
// { icon: '⏱', en: '15–20 min', ar: '١٥–٢٠ دقيقة', lEn: 'Duration', lAr: 'المدة' },
// { icon: '🎥', en: 'Video Call', ar: 'مكالمة فيديو', lEn: 'Format', lAr: 'الشكل' },
// { icon: '🌐', en: 'Ar / En', ar: 'عربي / إنجليزي', lEn: 'Language', lAr: 'اللغة' },
// ];
// // ─── CSS ──────────────────────────────────────────────────────
// const CSS = `
// @import url('https://fonts.googleapis.com/css2?family=Playfair+Display:wght@500;700&family=Noto+Naskh+Arabic:wght@400;500;600;700&family=DM+Sans:wght@300;400;500;600&display=swap');
// :root {
// --green: #16a34a;
// --green-d: #15803d;
// --green-l: #bbf7d0;
// --green-bg: #f0fdf4;
// --green-md: #dcfce7;
// --gborder: #86efac;
// --border: #e2e8f0;
// --text: #1e293b;
// --muted: #64748b;
// --muted-l: #94a3b8;
// --bg: #f8fafc;
// --white: #ffffff;
// }
// * { box-sizing: border-box; margin: 0; padding: 0; }
// .iv-root {
// min-height: 100vh;
// background: var(--bg);
// display: flex;
// align-items: center;
// justify-content: center;
// padding: 48px 16px;
// position: relative;
// overflow: hidden;
// font-family: 'DM Sans', sans-serif;
// color: var(--text);
// }
// [dir="rtl"] .iv-root { font-family: 'Noto Naskh Arabic', 'DM Sans', serif; }
// /* ── Background ── */
// .iv-bg-circles { position: absolute; inset: 0; pointer-events: none; overflow: hidden; }
// .iv-circle { position: absolute; border-radius: 50%; filter: blur(80px); }
// .iv-c1 { width:500px; height:500px; background:rgba(22,163,74,.06); top:-150px; left:-150px; }
// .iv-c2 { width:400px; height:400px; background:rgba(22,163,74,.04); bottom:-100px; right:-100px; }
// .iv-c3 { width:300px; height:300px; background:rgba(186,230,253,.12); top:40%; left:60%; }
// .iv-grid-pattern {
// position: absolute; inset: 0;
// background-image:
// linear-gradient(var(--border) 1px, transparent 1px),
// linear-gradient(90deg, var(--border) 1px, transparent 1px);
// background-size: 40px 40px;
// opacity: .45;
// pointer-events: none;
// }
// /* ── Language ── */
// .iv-lang { position: fixed; top: 20px; right: 20px; z-index: 100; }
// [dir="rtl"] .iv-lang { right: auto; left: 20px; }
// /* ── Card ── */
// .iv-card {
// position: relative;
// z-index: 2;
// width: 100%;
// max-width: 580px;
// background: var(--white);
// border: 1px solid var(--border);
// border-radius: 24px;
// padding: 48px 44px;
// box-shadow: 0 8px 48px rgba(0,0,0,.09);
// animation: iv-rise .6s cubic-bezier(.22,1,.36,1) both;
// }
// @keyframes iv-rise {
// from { opacity:0; transform: translateY(28px) scale(.98); }
// to { opacity:1; transform: none; }
// }
// /* ── Center block ── */
// .iv-center {
// display: flex;
// flex-direction: column;
// align-items: center;
// text-align: center;
// gap: 20px;
// }
// /* ── Badge ── */
// .iv-badge {
// display: inline-block;
// padding: 5px 16px;
// border-radius: 100px;
// font-size: 11px;
// font-weight: 600;
// letter-spacing: .08em;
// text-transform: uppercase;
// background: #f1f5f9;
// color: var(--muted);
// border: 1px solid var(--border);
// }
// [dir="rtl"] .iv-badge { letter-spacing: 0; text-transform: none; font-size: 12px; }
// .iv-badge-green {
// background: var(--green-bg);
// color: var(--green-d);
// border-color: var(--gborder);
// }
// /* ── Ornament ── */
// .iv-ornament {
// display: flex;
// align-items: center;
// gap: 12px;
// width: 100%;
// max-width: 220px;
// }
// .iv-ornament span {
// flex: 1;
// height: 1px;
// background: linear-gradient(90deg, transparent, var(--gborder), transparent);
// }
// .iv-diamond {
// width: 8px; height: 8px;
// background: var(--green);
// border-radius: 2px;
// transform: rotate(45deg);
// flex-shrink: 0;
// opacity: .55;
// }
// /* ── Typography ── */
// .iv-h1 {
// font-family: 'Playfair Display', serif;
// font-size: clamp(24px, 4vw, 32px);
// font-weight: 700;
// color: var(--text);
// line-height: 1.2;
// }
// [dir="rtl"] .iv-h1 {
// font-family: 'Noto Naskh Arabic', serif;
// font-size: clamp(22px, 4vw, 28px);
// }
// .iv-sub {
// font-size: 14px;
// color: var(--muted);
// line-height: 1.75;
// max-width: 400px;
// }
// .iv-muted-sm { font-size: 13px; color: var(--muted-l); margin-top: 8px; }
// .iv-footnote { font-size: 12px; color: var(--muted-l); line-height: 1.6; }
// /* ── Info Pills ── */
// .iv-pills { display: flex; gap: 10px; width: 100%; }
// .iv-pill {
// flex: 1;
// border: 1px solid var(--border);
// border-radius: 14px;
// padding: 14px 8px;
// display: flex;
// flex-direction: column;
// align-items: center;
// gap: 4px;
// background: var(--bg);
// transition: border-color .2s, background .2s;
// }
// .iv-pill:hover { border-color: var(--gborder); background: var(--green-bg); }
// .iv-pill-icon { font-size: 18px; }
// .iv-pill-val { font-size: 12px; font-weight: 600; color: var(--text); text-align: center; }
// .iv-pill-lbl { font-size: 10px; color: var(--muted-l); }
// /* ── Slot card ── */
// .iv-slot-card {
// display: flex;
// align-items: center;
// gap: 16px;
// width: 100%;
// background: var(--green-bg);
// border: 1.5px solid var(--gborder);
// border-radius: 16px;
// padding: 20px 22px;
// text-align: start;
// animation: iv-pop .3s cubic-bezier(.22,1,.36,1) both;
// }
// @keyframes iv-pop {
// from { opacity:0; transform:scale(.96); }
// to { opacity:1; transform:none; }
// }
// .iv-slot-icon {
// width: 44px; height: 44px;
// border-radius: 12px;
// background: var(--green-md);
// border: 1px solid var(--gborder);
// display: flex; align-items: center; justify-content: center;
// color: var(--green-d);
// flex-shrink: 0;
// }
// .iv-slot-icon svg { width: 20px; height: 20px; }
// .iv-slot-info { flex: 1; min-width: 0; }
// .iv-slot-date { font-size: 15px; font-weight: 600; color: var(--text); }
// .iv-slot-time { font-size: 14px; color: var(--green-d); font-weight: 500; margin-top: 2px; }
// .iv-slot-badge {
// display: inline-block;
// margin-top: 6px;
// padding: 2px 10px;
// border-radius: 100px;
// font-size: 11px;
// color: var(--muted);
// background: var(--white);
// border: 1px solid var(--border);
// }
// /* ── Join button (active) ── */
// .iv-join-btn {
// display: flex;
// align-items: center;
// justify-content: center;
// gap: 10px;
// width: 100%;
// padding: 15px 24px;
// border-radius: 14px;
// background: linear-gradient(135deg, var(--green-d), var(--green));
// color: #fff;
// font-size: 15px;
// font-weight: 600;
// text-decoration: none;
// font-family: inherit;
// border: none;
// cursor: pointer;
// transition: all .22s;
// box-shadow: 0 4px 20px rgba(22,163,74,.28);
// animation: iv-glow 2s ease-in-out infinite;
// }
// .iv-join-btn:hover {
// transform: translateY(-2px);
// box-shadow: 0 8px 30px rgba(22,163,74,.4);
// }
// @keyframes iv-glow {
// 0%,100% { box-shadow: 0 4px 20px rgba(22,163,74,.28), 0 0 0 0 rgba(22,163,74,.15); }
// 50% { box-shadow: 0 4px 20px rgba(22,163,74,.28), 0 0 0 8px rgba(22,163,74,0); }
// }
// /* ── Locked button ── */
// .iv-locked-btn {
// display: flex;
// align-items: center;
// gap: 14px;
// width: 100%;
// padding: 16px 20px;
// border-radius: 14px;
// background: var(--bg);
// border: 1.5px dashed var(--border);
// color: var(--muted);
// text-align: start;
// cursor: not-allowed;
// }
// .iv-lock-wrap {
// width: 40px; height: 40px;
// border-radius: 10px;
// background: var(--white);
// border: 1px solid var(--border);
// display: flex; align-items: center; justify-content: center;
// color: var(--muted-l);
// flex-shrink: 0;
// }
// .iv-locked-title { font-size: 13px; font-weight: 600; color: var(--muted); margin-bottom: 2px; }
// .iv-locked-sub { font-size: 12px; color: var(--muted-l); }
// /* ── Primary button ── */
// .iv-primary-btn {
// display: inline-flex;
// align-items: center;
// justify-content: center;
// width: 100%;
// padding: 14px 24px;
// border-radius: 14px;
// background: linear-gradient(135deg, var(--green-d), var(--green));
// color: #fff;
// font-size: 15px;
// font-weight: 600;
// border: none;
// cursor: pointer;
// font-family: inherit;
// transition: all .22s;
// box-shadow: 0 4px 20px rgba(22,163,74,.25);
// }
// .iv-primary-btn:hover { transform: translateY(-2px); box-shadow: 0 8px 28px rgba(22,163,74,.35); }
// /* ── Notice ── */
// .iv-notice {
// display: flex;
// gap: 10px;
// align-items: flex-start;
// width: 100%;
// padding: 14px 16px;
// border-radius: 12px;
// background: var(--bg);
// border: 1px solid var(--border);
// text-align: start;
// }
// .iv-notice-icon { color: var(--muted-l); flex-shrink: 0; margin-top: 2px; }
// .iv-notice p { font-size: 13px; color: var(--muted); line-height: 1.7; }
// /* ── Waiting box ── */
// .iv-waiting-box {
// display: flex;
// flex-direction: column;
// align-items: center;
// gap: 12px;
// padding: 24px 32px;
// width: 100%;
// background: var(--green-bg);
// border: 1px dashed var(--gborder);
// border-radius: 16px;
// }
// .iv-dots { display: flex; gap: 6px; align-items: center; }
// .iv-dot {
// width: 8px; height: 8px;
// border-radius: 50%;
// background: var(--green);
// opacity: .35;
// animation: iv-bounce 1.3s ease-in-out infinite;
// }
// .iv-dot-1 { animation-delay: 0s; }
// .iv-dot-2 { animation-delay: .2s; }
// .iv-dot-3 { animation-delay: .4s; }
// @keyframes iv-bounce {
// 0%,80%,100% { transform: scale(.75); opacity: .35; }
// 40% { transform: scale(1.25); opacity: 1; }
// }
// .iv-waiting-txt { font-size: 13px; color: var(--green-d); font-weight: 500; }
// /* ── Spinner ── */
// .iv-spinner {
// width: 36px; height: 36px;
// border: 3px solid var(--border);
// border-top-color: var(--green);
// border-radius: 50%;
// animation: iv-spin .7s linear infinite;
// }
// @keyframes iv-spin { to { transform: rotate(360deg); } }
// /* ── Check animation ── */
// .iv-check-wrap {
// width: 80px; height: 80px;
// animation: iv-scale-in .5s cubic-bezier(.22,1,.36,1) .1s both;
// }
// @keyframes iv-scale-in {
// from { opacity:0; transform: scale(.4) rotate(-20deg); }
// to { opacity:1; transform: none; }
// }
// .iv-ring {
// animation: iv-draw-ring .6s linear .2s both;
// }
// @keyframes iv-draw-ring {
// from { stroke-dashoffset: 190; }
// to { stroke-dashoffset: 0; }
// }
// .iv-check {
// animation: iv-draw-check .4s ease .7s both;
// }
// @keyframes iv-draw-check {
// from { stroke-dashoffset: 40; }
// to { stroke-dashoffset: 0; }
// }
// @media (max-width: 480px) {
// .iv-card { padding: 28px 20px; }
// .iv-pills { gap: 6px; }
// .iv-pill { padding: 10px 4px; }
// .iv-pill-val { font-size: 11px; }
// .iv-slot-card { padding: 14px 16px; gap: 12px; }
// }
// `;
import { useState, useEffect } from 'react';
import { useNavigate, useLocation } from 'react-router-dom';
import { useLanguage } from '../../lib/languageContext';
import { LanguageSwitcher } from '../LanguageSwitcher';
import { useAuth } from '../../lib/authContext';
import { getApiBaseUrl } from '../../lib/api/config';
// ─── Types ────────────────────────────────────────────────────
type InterviewStatus =
| 'loading' // جاري جلب البيانات
| 'no_interview' // لا يوجد موعد بعد
| 'scheduled' // موعد محدد – الزر معطل
| 'joinable' // حان وقت المقابلة – الزر مفعّل
| 'completed' // انتهت المقابلة
| 'rejected' // تم الرفض
| 'error'; // خطأ
interface InterviewData {
scheduledAt: string; // مثلاً "2026-03-25T14:00:00"
meetingLink: string; // رابط الاجتماع
status: 'PENDING' | 'COMPLETED' | 'CANCELLED';
}
// ─── Date helpers (نفس الدوال السابقة) ────────────────────────
const AR_DAYS = ['الأحد','الاثنين','الثلاثاء','الأربعاء','الخميس','الجمعة','السبت'];
const AR_MONTHS = ['يناير','فبراير','مارس','أبريل','مايو','يونيو','يوليو','أغسطس','سبتمبر','أكتوبر','نوفمبر','ديسمبر'];
function fmtDateAr(d: Date) {
return `${AR_DAYS[d.getDay()]}، ${d.getDate()} ${AR_MONTHS[d.getMonth()]} ${d.getFullYear()}`;
}
function fmtDateEn(d: Date) {
return d.toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' });
}
function fmtTimeAr(d: Date) {
const h = d.getHours();
const m = d.getMinutes().toString().padStart(2, '0');
const h12 = h === 0 ? 12 : h > 12 ? h - 12 : h;
return `${h12}:${m} ${h < 12 ? 'صباحاً' : 'مساءً'}`;
}
function fmtTimeEn(d: Date) {
return d.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true });
}
// حساب المدة المتبقية
function timeUntil(d: Date, isAr: boolean): string {
const diff = d.getTime() - Date.now();
if (diff <= 0) return '';
const days = Math.floor(diff / 86400000);
const hours = Math.floor((diff % 86400000) / 3600000);
const mins = Math.floor((diff % 3600000) / 60000);
if (isAr) {
if (days > 0) return `بعد ${days} يوم${days === 1 ? '' : 'اً'}`;
if (hours > 0) return `بعد ${hours} ساعة`;
return `بعد ${mins} دقيقة`;
}
if (days > 0) return `in ${days} day${days > 1 ? 's' : ''}`;
if (hours > 0) return `in ${hours} hour${hours > 1 ? 's' : ''}`;
return `in ${mins} minute${mins > 1 ? 's' : ''}`;
}
// ─── API calls (مكانها المناسب – يمكن استبدالها بطلبات حقيقية) ─
const BASE_URL = getApiBaseUrl();
/**
* 🔽 هنا يتم جلب بيانات المقابلة من الخادم.
* إذا لم يوجد موعد (404) نعيد null.
* في حالة الخطأ نرمي استثناء.
*/
async function fetchInterview(): Promise<InterviewData | null> {
const token = localStorage.getItem('authToken') ?? '';
const headers = {
'Content-Type': 'application/json',
...(token ? { Authorization: `Bearer ${token}` } : {}),
};
// Try the UNDER_REVIEW endpoint first (sheikh/interview), then fall back to pending
for (const url of [`${BASE_URL}/api/sheikh/interview`, `${BASE_URL}/api/sheikh/pending`]) {
const res = await fetch(url, { headers });
if (res.status === 404 || res.status === 403) continue;
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const text = await res.text();
if (!text.trim()) continue;
const raw = JSON.parse(text);
// /api/sheikh/interview returns { scheduledAt, meetingLink, status }
if (raw.scheduledAt) {
return {
scheduledAt: raw.scheduledAt,
meetingLink: raw.meetingLink ?? '',
status: raw.status ?? 'PENDING',
};
}
// /api/sheikh/pending returns { date, time, message }
if (raw.date && raw.time) {
return {
scheduledAt: `${raw.date}T${raw.time}`,
meetingLink: '',
status: 'PENDING',
};
}
}
return null;
}
// ─── Main Component ───────────────────────────────────────────
export function InterviewSchedule() {
const { language } = useLanguage();
const navigate = useNavigate();
const location = useLocation();
const { user } = useAuth();
const isAr = language === 'ar';
const [status, setStatus] = useState<InterviewStatus>('loading');
const [interview, setInterview] = useState<InterviewData | null>(null);
const [now, setNow] = useState(new Date());
const [error, setError] = useState('');
const [rejectionReason, setRejectionReason] = useState('');
// ── Check if navigated here from session with a decision state ─
useEffect(() => {
const state = location.state as { rejected?: boolean; reason?: string } | null;
if (state?.rejected) {
setRejectionReason(state.reason ?? '');
setStatus('rejected');
}
}, [location.state]);
// ── On mount: check approval status first, then load interview ─
useEffect(() => {
const token = localStorage.getItem('authToken') ?? '';
const checkDecision = async (): Promise<'APPROVED' | 'REJECTED' | 'OTHER'> => {
if (!token) return 'OTHER';
try {
const res = await fetch(`${BASE_URL}/api/auth/verify-profile`, {
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
});
if (!res.ok) return 'OTHER';
const data = await res.json();
const approvalStatus: string = data.sheikhApprovalStatus ?? '';
if (approvalStatus === 'APPROVED') return 'APPROVED';
if (approvalStatus === 'REJECTED') {
setRejectionReason(data.rejectionReason ?? '');
return 'REJECTED';
}
} catch { /* silent */ }
return 'OTHER';
};
const loadInterview = () => {
fetchInterview()
.then(data => {
if (!data) {
setStatus(prev => prev === 'rejected' ? prev : 'no_interview');
return;
}
setInterview(data);
const scheduled = new Date(data.scheduledAt);
if (data.status === 'COMPLETED') { setStatus('completed'); return; }
if (data.status === 'CANCELLED') { setStatus('no_interview'); return; }
if (scheduled.getTime() <= Date.now()) { setStatus('joinable'); return; }
setStatus('scheduled');
})
.catch(e => { setError(e.message); setStatus('error'); });
};
// Run approval check first — only show interview UI if not decided
checkDecision().then(decision => {
if (decision === 'APPROVED') {
navigate('/sheikh/dashboard', { replace: true });
return;
}
if (decision === 'REJECTED') {
setStatus('rejected');
return;
}
// Not decided yet — load the interview schedule
loadInterview();
const timer = setInterval(loadInterview, 30_000);
return () => clearInterval(timer);
});
}, []); // eslint-disable-line react-hooks/exhaustive-deps
// ── Poll verify-profile every 15s for admin decision ───────
useEffect(() => {
if (status === 'rejected') return;
const token = localStorage.getItem('authToken') ?? '';
if (!token) return;
const checkDecision = async () => {
try {
const res = await fetch(`${BASE_URL}/api/auth/verify-profile`, {
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
});
if (!res.ok) return;
const data = await res.json();
const approvalStatus: string = data.sheikhApprovalStatus ?? '';
if (approvalStatus === 'APPROVED') {
navigate('/sheikh/dashboard', { replace: true });
} else if (approvalStatus === 'REJECTED') {
setRejectionReason(data.rejectionReason ?? '');
setStatus('rejected');
}
} catch { /* silent */ }
};
const timer = setInterval(checkDecision, 15_000);
return () => clearInterval(timer);
}, [navigate, status]);
const scheduledDate = interview ? new Date(interview.scheduledAt) : null;
const isJoinable = status === 'joinable';
const remaining = scheduledDate && status === 'scheduled'
? timeUntil(scheduledDate, isAr) : '';
// ── Tick every 30s to update countdown and flip to joinable ─
useEffect(() => {
const timer = setInterval(() => {
setNow(new Date());
if (interview && status === 'scheduled') {
const scheduled = new Date(interview.scheduledAt);
if (scheduled.getTime() - Date.now() <= 5 * 60 * 1000) {
setStatus('joinable');
}
}
}, 30_000);
return () => clearInterval(timer);
}, [interview, status]);
// ─── العرض ─────────────────────────────────────────────────
return (
<>
<style>{CSS}</style>
<div className="iv-root" dir={isAr ? 'rtl' : 'ltr'}>
{/* خلفية */}
<div className="iv-bg-circles" aria-hidden="true">
<div className="iv-circle iv-c1" />
<div className="iv-circle iv-c2" />
<div className="iv-circle iv-c3" />
</div>
<div className="iv-grid-pattern" aria-hidden="true" />
{/* مفتاح اللغة */}
<div className="iv-lang"><LanguageSwitcher /></div>
{/* البطاقة الرئيسية */}
<div className="iv-card">
{/* ── حالة التحميل ── */}
{status === 'loading' && (
<div className="iv-center-block">
<div className="iv-spinner" />
<p className="iv-muted">{isAr ? 'جاري تحميل بيانات المقابلة...' : 'Loading interview details...'}</p>
</div>
)}
{/* ── خطأ ── */}
{status === 'error' && (
<div className="iv-center-block">
<div className="iv-icon-wrap iv-icon-warn">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<circle cx="12" cy="12" r="10"/><path d="M12 8v4M12 16h.01"/>
</svg>
</div>
<h2 className="iv-h2">{isAr ? 'حدث خطأ' : 'Something went wrong'}</h2>
<p className="iv-muted">{error}</p>
<button className="iv-btn-sec" onClick={() => window.location.reload()}>
{isAr ? 'إعادة المحاولة' : 'Try again'}
</button>
</div>
)}
{/* ── لا يوجد موعد بعد ── */}
{status === 'no_interview' && (
<div className="iv-center-block">
<div className="iv-badge">{isAr ? 'المرحلة الأخيرة' : 'Final Step'}</div>
<div className="iv-ornament"><span/><div className="iv-diamond"/><span/></div>
<h1 className="iv-h1">{isAr ? 'مقابلة التحقق' : 'Verification Interview'}</h1>
<p className="iv-sub">
{isAr
? 'أنت على وشك إتمام انضمامك للمنصة. فريقنا سيحدد لك موعد المقابلة وسيصلك إشعار فور تحديده.'
: "You're almost there. Our team will schedule your verification interview and notify you as soon as it's confirmed."}
</p>
{/* نقاط معلومات */}
<div className="iv-pills">
{[
{ icon: '⏱', en: '15–20 min', ar: '١٥–٢٠ دقيقة', lEn: 'Duration', lAr: 'المدة' },
{ icon: '🎥', en: 'Video Call', ar: 'مكالمة فيديو', lEn: 'Format', lAr: 'الشكل' },
{ icon: '🌐', en: 'Ar / En', ar: 'عربي / إنجليزي', lEn: 'Language', lAr: 'اللغة' },
].map((p, i) => (
<div className="iv-pill" key={i}>
<span className="iv-pill-icon">{p.icon}</span>
<span className="iv-pill-val">{isAr ? p.ar : p.en}</span>
<span className="iv-pill-lbl">{isAr ? p.lAr : p.lEn}</span>
</div>
))}
</div>
{/* حالة الانتظار */}
<div className="iv-waiting-box">
<div className="iv-waiting-dot-wrap">
<span className="iv-dot iv-dot-1"/><span className="iv-dot iv-dot-2"/><span className="iv-dot iv-dot-3"/>
</div>
<p className="iv-waiting-txt">
{isAr
? 'في انتظار تحديد الموعد من قِبل الفريق...'
: 'Waiting for the team to schedule your slot...'}
</p>
</div>
<p className="iv-footnote">
{isAr
? 'ستصلك رسالة بريد إلكتروني فور تحديد الموعد'
: "You'll receive an email once your slot is confirmed"}
</p>
</div>
)}
{/* ── موعد محدد (scheduled أو joinable) ── */}
{(status === 'scheduled' || status === 'joinable') && scheduledDate && (
<div className="iv-center-block">
<div className="iv-badge iv-badge-green">
{isAr ? 'تم تحديد موعدك' : 'Interview Scheduled'}
</div>
<div className="iv-ornament"><span/><div className="iv-diamond"/><span/></div>
<h1 className="iv-h1">{isAr ? 'مقابلة التحقق' : 'Verification Interview'}</h1>
<p className="iv-sub">
{isAr
? 'تم تحديد موعد مقابلتك. يُرجى الحضور في الوقت المحدد واستعداد كاميرتك وميكروفونك.'
: 'Your interview has been scheduled. Please be ready on time with your camera and microphone.'}
</p>
{/* بطاقة الموعد */}
<div className="iv-slot-card">
<div className="iv-slot-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
<rect x="3" y="4" width="18" height="18" rx="3"/>
<path d="M16 2v4M8 2v4M3 10h18"/>
</svg>
</div>
<div className="iv-slot-info">
<div className="iv-slot-date">
{isAr ? fmtDateAr(scheduledDate) : fmtDateEn(scheduledDate)}
</div>
<div className="iv-slot-time">
{isAr ? fmtTimeAr(scheduledDate) : fmtTimeEn(scheduledDate)}
</div>
{remaining && (
<div className="iv-slot-remaining">{remaining}</div>
)}
</div>
</div>
{/* نقاط المعلومات */}
<div className="iv-pills">
{[
{ icon: '⏱', en: '15–20 min', ar: '١٥–٢٠ دقيقة', lEn: 'Duration', lAr: 'المدة' },
{ icon: '🎥', en: 'Video Call', ar: 'مكالمة فيديو', lEn: 'Format', lAr: 'الشكل' },
{ icon: '🌐', en: 'Ar / En', ar: 'عربي / إنجليزي', lEn: 'Language', lAr: 'اللغة' },
].map((p, i) => (
<div className="iv-pill" key={i}>
<span className="iv-pill-icon">{p.icon}</span>
<span className="iv-pill-val">{isAr ? p.ar : p.en}</span>
<span className="iv-pill-lbl">{isAr ? p.lAr : p.lEn}</span>
</div>
))}
</div>
{/* زر المقابلة – مفعّل فقط عندما يكون joinable */}
{isJoinable ? (
<button
onClick={() => navigate(`/sheikh/interview-session/${user?.id}`)}
className="iv-btn iv-btn-join"
>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="iv-btn-icon">
<path d="M15 10l4.553-2.069A1 1 0 0121 8.847v6.306a1 1 0 01-1.447.894L15 14M3 8a2 2 0 012-2h8a2 2 0 012 2v8a2 2 0 01-2 2H5a2 2 0 01-2-2V8z"/>
</svg>
{isAr ? 'انضم إلى المقابلة الآن' : 'Join Interview Now'}
</button>
) : (
<div className="iv-btn-disabled">
<div className="iv-lock-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<rect x="3" y="11" width="18" height="11" rx="2"/>
<path d="M7 11V7a5 5 0 0110 0v4"/>
</svg>
</div>
<div>
<div className="iv-btn-disabled-title">
{isAr ? 'سيُفتح الرابط قبل المقابلة بـ 5 دقائق' : 'Link opens 5 minutes before interview'}
</div>
<div className="iv-btn-disabled-sub">
{isAr ? 'يُرجى الانتظار حتى يحين الموعد' : 'Please wait until the scheduled time'}
</div>
</div>
</div>
)}
{/* ملاحظة */}
<div className="iv-notice">
<span className="iv-notice-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<circle cx="12" cy="12" r="10"/><path d="M12 8v4M12 16h.01"/>
</svg>
</span>
<p>
{isAr
? 'إذا واجهت أي مشكلة في الدخول تواصل معنا عبر support@platform.com'
: 'If you have any issues joining, contact us at support@platform.com'}
</p>
</div>
</div>
)}
{/* ── انتهت المقابلة ── */}
{status === 'completed' && (
<div className="iv-center-block">
<div className="iv-check-anim">
<svg viewBox="0 0 64 64" fill="none">
<circle cx="32" cy="32" r="30" stroke="#16a34a" strokeWidth="2" strokeDasharray="190" className="iv-ring"/>
<path d="M20 33l8 8 16-18" stroke="#16a34a" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round" strokeDasharray="40" className="iv-check"/>
</svg>
</div>
<h1 className="iv-h1">{isAr ? 'تمت المقابلة بنجاح' : 'Interview Completed'}</h1>
<p className="iv-sub">
{isAr
? 'شكراً لك على وقتك. سيتم مراجعة نتيجة المقابلة وإخطارك قريباً.'
: "Thank you for your time. We'll review the interview results and notify you soon."}
</p>
<button className="iv-btn" onClick={() => navigate('/sheikh/dashboard')}>
{isAr ? 'الذهاب إلى لوحة التحكم' : 'Go to Dashboard'}
</button>
</div>
)}
{/* ── تم الرفض ── */}
{status === 'rejected' && (
<div className="iv-center-block">
<div className="iv-icon-wrap iv-icon-reject">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<circle cx="12" cy="12" r="10"/>
<path d="M15 9l-6 6M9 9l6 6"/>
</svg>
</div>
<h1 className="iv-h1">
{isAr ? 'لم يتم قبول طلبك' : 'Application Not Approved'}
</h1>
<p className="iv-sub">
{isAr
? 'نأسف لإبلاغك بأن طلبك لم يتم قبوله في الوقت الحالي.'
: "We're sorry to inform you that your application was not approved at this time."}
</p>
{rejectionReason && (
<div className="iv-rejection-box">
<p className="iv-rejection-label">
{isAr ? 'سبب الرفض:' : 'Reason:'}
</p>
<p className="iv-rejection-reason">{rejectionReason}</p>
</div>
)}
<div className="iv-notice">
<span className="iv-notice-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<circle cx="12" cy="12" r="10"/><path d="M12 8v4M12 16h.01"/>
</svg>
</span>
<p>
{isAr
? 'إذا كنت تعتقد أن هذا خطأ أو تريد إعادة التقديم، تواصل معنا عبر support@platform.com'
: 'If you believe this is a mistake or wish to reapply, contact us at support@platform.com'}
</p>
</div>
</div>
)}
</div>
</div>
</>
);
}
// ─── التصميم (نفس السابق بألوان فاتحة) ────────────────────────
const CSS = `
@import url('https://fonts.googleapis.com/css2?family=Playfair+Display:wght@500;700&family=Noto+Naskh+Arabic:wght@400;500;600;700&family=DM+Sans:wght@300;400;500;600&display=swap');
:root {
--green: #16a34a;
--green-d: #15803d;
--green-l: #bbf7d0;
--green-bg: #f0fdf4;
--green-mid: #dcfce7;
--border: #e2e8f0;
--border-g: #86efac;
--text: #1e293b;
--muted: #64748b;
--muted-l: #94a3b8;
--bg: #f8fafc;
--white: #ffffff;
--shadow: 0 4px 24px rgba(0,0,0,.06);
--shadow-lg: 0 8px 48px rgba(0,0,0,.1);
}
* { box-sizing: border-box; margin: 0; padding: 0; }
.iv-root {
min-height: 100vh;
background: var(--bg);
display: flex;
align-items: center;
justify-content: center;
padding: 48px 16px;
position: relative;
overflow: hidden;
font-family: 'DM Sans', sans-serif;
color: var(--text);
}
[dir="rtl"] .iv-root { font-family: 'Noto Naskh Arabic', 'DM Sans', serif; }
/* Background decorations */
.iv-bg-circles { position: absolute; inset: 0; pointer-events: none; overflow: hidden; }
.iv-circle {
position: absolute;
border-radius: 50%;
filter: blur(80px);
}
.iv-c1 { width: 500px; height: 500px; background: rgba(22,163,74,.06); top: -150px; left: -150px; }
.iv-c2 { width: 400px; height: 400px; background: rgba(22,163,74,.04); bottom: -100px; right: -100px; }
.iv-c3 { width: 300px; height: 300px; background: rgba(186,230,253,.15); top: 40%; left: 60%; }
.iv-grid-pattern {
position: absolute;
inset: 0;
background-image:
linear-gradient(var(--border) 1px, transparent 1px),
linear-gradient(90deg, var(--border) 1px, transparent 1px);
background-size: 40px 40px;
opacity: .4;
pointer-events: none;
}
/* Language */
.iv-lang { position: fixed; top: 20px; right: 20px; z-index: 100; }
[dir="rtl"] .iv-lang { right: auto; left: 20px; }
/* Card */
.iv-card {
position: relative;
z-index: 2;
width: 100%;
max-width: 600px;
background: var(--white);
border: 1px solid var(--border);
border-radius: 24px;
padding: 48px 44px;
box-shadow: var(--shadow-lg);
animation: iv-rise .6s cubic-bezier(.22,1,.36,1) both;
}
@keyframes iv-rise {
from { opacity:0; transform: translateY(28px) scale(.98); }
to { opacity:1; transform: none; }
}
/* Center block */
.iv-center-block {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
gap: 18px;
}
/* Badge */
.iv-badge {
display: inline-block;
padding: 5px 16px;
border-radius: 100px;
font-size: 11px;
font-weight: 600;
letter-spacing: .08em;
text-transform: uppercase;
background: var(--green-bg);
color: var(--green-d);
border: 1px solid var(--border-g);
}
[dir="rtl"] .iv-badge { letter-spacing: 0; text-transform: none; font-size: 12px; }
.iv-badge-green { background: var(--green-bg); }
/* Ornament */
.iv-ornament {
display: flex;
align-items: center;
gap: 12px;
width: 100%;
max-width: 240px;
}
.iv-ornament span {
flex: 1;
height: 1px;
background: linear-gradient(90deg, transparent, var(--border-g), transparent);
}
.iv-diamond {
width: 8px; height: 8px;
background: var(--green);
border-radius: 2px;
transform: rotate(45deg);
flex-shrink: 0;
opacity: .6;
}
/* Headings */
.iv-h1 {
font-family: 'Playfair Display', serif;
font-size: clamp(24px, 4vw, 34px);
font-weight: 700;
color: var(--text);
line-height: 1.2;
}
[dir="rtl"] .iv-h1 {
font-family: 'Noto Naskh Arabic', serif;
font-size: clamp(22px, 4vw, 30px);
}
.iv-h2 {
font-size: 20px;
font-weight: 600;
color: var(--text);
}
.iv-sub {
font-size: 14px;
color: var(--muted);
line-height: 1.75;
max-width: 420px;
}
.iv-muted {
font-size: 14px;
color: var(--muted-l);
text-align: center;
}
/* Pills */
.iv-pills {
display: flex;
gap: 10px;
width: 100%;
}
.iv-pill {
flex: 1;
border: 1px solid var(--border);
border-radius: 14px;
padding: 14px 8px;
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
background: var(--bg);
transition: border-color .2s, background .2s;
}
.iv-pill:hover { border-color: var(--border-g); background: var(--green-bg); }
.iv-pill-icon { font-size: 18px; }
.iv-pill-val { font-size: 12px; font-weight: 600; color: var(--text); text-align: center; }
.iv-pill-lbl { font-size: 10px; color: var(--muted-l); }
/* Slot card */
.iv-slot-card {
display: flex;
align-items: center;
gap: 16px;
width: 100%;
background: var(--green-bg);
border: 1.5px solid var(--border-g);
border-radius: 16px;
padding: 20px 24px;
text-align: start;
animation: iv-pop .3s cubic-bezier(.22,1,.36,1) both;
}
@keyframes iv-pop {
from { opacity:0; transform: scale(.96); }
to { opacity:1; transform: none; }
}
.iv-slot-icon {
width: 44px; height: 44px;
border-radius: 12px;
background: var(--green-mid);
border: 1px solid var(--border-g);
display: flex; align-items: center; justify-content: center;
flex-shrink: 0;
color: var(--green-d);
}
.iv-slot-icon svg { width: 20px; height: 20px; }
.iv-slot-info { flex: 1; min-width: 0; }
.iv-slot-date {
font-size: 15px;
font-weight: 600;
color: var(--text);
}
.iv-slot-time {
font-size: 14px;
color: var(--green-d);
margin-top: 2px;
font-weight: 500;
}
.iv-slot-remaining {
font-size: 12px;
color: var(--muted);
margin-top: 4px;
padding: 2px 8px;
background: var(--white);
border-radius: 100px;
display: inline-block;
border: 1px solid var(--border);
}
/* Join button — active */
.iv-btn-join {
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
width: 100%;
padding: 15px 24px;
border-radius: 14px;
background: linear-gradient(135deg, var(--green-d), var(--green));
color: #fff;
font-size: 15px;
font-weight: 600;
text-decoration: none;
font-family: inherit;
cursor: pointer;
border: none;
transition: all .22s;
box-shadow: 0 4px 20px rgba(22,163,74,.3);
animation: iv-pulse-border 2s ease-in-out infinite;
}
.iv-btn-join:hover {
transform: translateY(-2px);
box-shadow: 0 8px 30px rgba(22,163,74,.4);
}
@keyframes iv-pulse-border {
0%, 100% { box-shadow: 0 4px 20px rgba(22,163,74,.3), 0 0 0 0 rgba(22,163,74,.2); }
50% { box-shadow: 0 4px 20px rgba(22,163,74,.3), 0 0 0 8px rgba(22,163,74,0); }
}
.iv-btn-icon { width: 18px; height: 18px; flex-shrink: 0; }
/* Join button — disabled */
.iv-btn-disabled {
display: flex;
align-items: center;
gap: 14px;
width: 100%;
padding: 16px 20px;
border-radius: 14px;
background: var(--bg);
border: 1.5px dashed var(--border);
color: var(--muted);
text-align: start;
cursor: not-allowed;
}
.iv-lock-icon {
width: 40px; height: 40px;
border-radius: 10px;
background: var(--white);
border: 1px solid var(--border);
display: flex; align-items: center; justify-content: center;
flex-shrink: 0;
color: var(--muted-l);
}
.iv-lock-icon svg { width: 18px; height: 18px; }
.iv-btn-disabled-title {
font-size: 13px;
font-weight: 600;
color: var(--muted);
margin-bottom: 2px;
}
.iv-btn-disabled-sub {
font-size: 12px;
color: var(--muted-l);
}
/* General button */
.iv-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 100%;
padding: 14px 24px;
border-radius: 14px;
background: linear-gradient(135deg, var(--green-d), var(--green));
color: #fff;
font-size: 15px;
font-weight: 600;
border: none;
cursor: pointer;
font-family: inherit;
transition: all .22s;
box-shadow: 0 4px 20px rgba(22,163,74,.25);
}
.iv-btn:hover { transform: translateY(-2px); box-shadow: 0 8px 28px rgba(22,163,74,.35); }
.iv-btn-sec {
padding: 10px 24px;
border-radius: 10px;
border: 1.5px solid var(--border);
background: var(--white);
color: var(--text);
font-size: 14px;
font-weight: 500;
cursor: pointer;
font-family: inherit;
transition: all .2s;
}
.iv-btn-sec:hover { border-color: var(--border-g); color: var(--green-d); }
/* Notice */
.iv-notice {
display: flex;
gap: 10px;
align-items: flex-start;
width: 100%;
padding: 14px 16px;
border-radius: 12px;
background: var(--bg);
border: 1px solid var(--border);
text-align: start;
}
.iv-notice-icon { color: var(--muted-l); flex-shrink: 0; margin-top: 2px; }
.iv-notice-icon svg { width: 16px; height: 16px; }
.iv-notice p { font-size: 13px; color: var(--muted); line-height: 1.7; }
/* Rejection state */
.iv-icon-wrap {
width: 72px; height: 72px;
border-radius: 50%;
display: flex; align-items: center; justify-content: center;
flex-shrink: 0;
}
.iv-icon-wrap svg { width: 32px; height: 32px; }
.iv-icon-reject {
background: #fef2f2;
border: 2px solid #fca5a5;
color: #dc2626;
}
.iv-icon-warn {
background: #fffbeb;
border: 2px solid #fcd34d;
color: #d97706;
}
.iv-rejection-box {
width: 100%;
padding: 16px 20px;
border-radius: 14px;
background: #fef2f2;
border: 1.5px solid #fca5a5;
text-align: start;
}
.iv-rejection-label {
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: .06em;
color: #dc2626;
margin-bottom: 6px;
}
[dir="rtl"] .iv-rejection-label { letter-spacing: 0; text-transform: none; font-size: 12px; }
.iv-rejection-reason {
font-size: 14px;
color: #7f1d1d;
line-height: 1.65;
}
/* Waiting state */
.iv-waiting-box {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
padding: 24px 32px;
background: var(--green-bg);
border: 1px dashed var(--border-g);
border-radius: 16px;
width: 100%;
}
.iv-waiting-dot-wrap { display: flex; gap: 6px; align-items: center; }
.iv-dot {
width: 8px; height: 8px;
border-radius: 50%;
background: var(--green);
opacity: .4;
animation: iv-bounce 1.2s ease-in-out infinite;
}
.iv-dot-1 { animation-delay: 0s; }
.iv-dot-2 { animation-delay: .2s; }
.iv-dot-3 { animation-delay: .4s; }
@keyframes iv-bounce {
0%, 80%, 100% { transform: scale(.8); opacity: .4; }
40% { transform: scale(1.2); opacity: 1; }
}
.iv-waiting-txt {
font-size: 13px;
color: var(--green-d);
font-weight: 500;
}
/* Footnote */
.iv-footnote {
font-size: 12px;
color: var(--muted-l);
text-align: center;
line-height: 1.6;
}
/* Spinner */
.iv-spinner {
width: 36px; height: 36px;
border: 3px solid var(--border);
border-top-color: var(--green);
border-radius: 50%;
animation: iv-spin .7s linear infinite;
}
@keyframes iv-spin { to { transform: rotate(360deg); } }
/* Icon wrap */
.iv-icon-wrap {
width: 64px; height: 64px;
border-radius: 50%;
display: flex; align-items: center; justify-content: center;
}
.iv-icon-warn {
background: #fef3c7;
color: #d97706;
}
.iv-icon-warn svg { width: 28px; height: 28px; }
/* Check animation */
.iv-check-anim {
width: 80px; height: 80px;
animation: iv-scale-in .5s cubic-bezier(.22,1,.36,1) .1s both;
}
@keyframes iv-scale-in {
from { opacity:0; transform: scale(.4) rotate(-20deg); }
to { opacity:1; transform: none; }
}
.iv-ring {
animation: iv-draw-ring .6s linear .2s both;
}
@keyframes iv-draw-ring {
from { stroke-dashoffset: 190; }
to { stroke-dashoffset: 0; }
}
.iv-check {
animation: iv-draw-check .4s ease .7s both;
}
@keyframes iv-draw-check {
from { stroke-dashoffset: 40; }
to { stroke-dashoffset: 0; }
}
@media (max-width: 480px) {
.iv-card { padding: 28px 20px; }
.iv-pills { gap: 6px; }
.iv-pill { padding: 10px 4px; }
.iv-pill-val { font-size: 11px; }
.iv-slot-card { padding: 14px 16px; }
}
`;