Developer Page Specifications
Listing Page
/:lang/opportunity-radar — page sections, fields, sources, states.
مكتمل
المسار: /:lang/opportunity-radar. يَعرض كل إشارة فرصة مُنَسَّقة مع فلترة، فرز، tabs حالة، بحث، وكتلة اشتراك. وَثِّق كل قسم، كل حقل، كل مصدر بيانات، وكل حالة — المُنَفِّذون ما يجب أن يَحتاجوا قراءة أيّ مستند آخر.
أقسام الصفحة (من الأعلى للأسفل)
- A · قسم Hero
- B · ملخص الرادار / لوحة الإشارات
- C · tabs الحالة (الكل / Open / Closing soon / Rolling / Closed)
- D · بحث + فلاتر
- E · شبكة كروت الفرص (القائمة الأساسية)
- F · كتلة اشتراك Weekly Opportunity Signals
- G · أكمل رحلتك (كروت cross-module)
- H · Footer (مكوّن عام، أبداً ليس لكل صفحة)
A · قسم Hero — الحقول
- Eyebrow label
- سلسلة ثابتة. المصدر: تكوين صفحة CMS (PageConfig.opportunityRadarListing.eyebrow). مثال: "Opportunity Intelligence for Founders". مطلوب: نعم.
- Page title (H1)
- المصدر: PageConfig.opportunityRadarListing.title. مثال: "Opportunity Radar". مطلوب: نعم. يجب أن يَكون الـ <h1> الوحيد على الصفحة.
- وصف الصفحة
- المصدر: PageConfig.opportunityRadarListing.description. يَرسم كفقرة lede تحت H1. مطلوب: نعم.
- ملاحظة دعم قصيرة
- المصدر: PageConfig.opportunityRadarListing.supportingNote. سطر ثانوي اختياري تحت الوصف. مطلوب: لا.
- CTA أساسي (التسمية + الفعل)
- مصدر التسمية: PageConfig.opportunityRadarListing.primaryCtaLabel (افتراضي: "Explore Opportunities"). الفعل: smooth-scroll لـ #opportunities-listing. مطلوب: نعم.
- CTA ثانوي
- التسمية: "Get Weekly Digest". الفعل: smooth-scroll لـ #subscribe-block. مطلوب: نعم.
- نقاط الميزات[]
- المصدر: PageConfig.opportunityRadarListing.featureBullets (مصفوفة { icon, label }). أمثلة: Editorially curated · MENA-wide coverage · Free for founders · Verified by our editorial team. مطلوب: نعم (≥3).
B · ملخص الرادار / لوحة الإشارات
- إجمالي عدد Open
- الاستعلام: SELECT COUNT(*) FROM Opportunity WHERE Status = 'Open' AND IsActive = true. مُخَزَّن جانب-الخادم، يَتحدّث عند كل نشر/إلغاء-نشر.
- عدد Closing soon
- الاستعلام: WHERE Status = 'ClosingSoon' أو (DeadlineDate BETWEEN NOW() AND NOW() + INTERVAL '7 days'). عتبة الـ 7 أيام قابلة للتكوين عبر FeatureFlag.opportunityClosingSoonDays.
- عدد Rolling
- الاستعلام: WHERE IsRolling = true AND IsActive = true.
- عدد Tracked
- الاستعلام: WHERE IsActive = true. إجمالي الفرص النشطة عبر كل الحالات.
- قائمة آخر الإشارات (آخر 5)
- الاستعلام: ORDER BY GREATEST(PublishedAt, LastVerifiedAt) DESC LIMIT 5. كل عنصر: { id, slug, titleLang, typeLabel, countryCode, deadlineLabel, statusBadge, detailsUrl }.
C · tabs الحالة — الحقول + السلوك
- عدد All signals (مجموع Open + ClosingSoon + Rolling + Closed، فقط IsActive = true).
- عدد Open — انظر B أعلاه.
- عدد Closing soon — انظر B أعلاه.
- عدد Rolling — انظر B أعلاه.
- عدد Closed — الاستعلام: WHERE Status = 'Closed' أو DeadlineDate < NOW(). إدخالات Closed تَبقى قابلة للاستعلام لكن مُستثناة من الفرز الافتراضي.
السلوك: نقرة على tab تُطَبِّق فلتر على شبكة الكروت (E). الـ tab النشط يَستخدم صبغة primary لنظام التصميم + وزن غامق. العَدَّادات تُحدَّث تفاعلياً بحسب الفلاتر/البحث. الحالة الفارغة — انظر قسم حالات UI أدناه.
D · بحث + فلاتر
- حقل البحث (q)
- Debounced 300ms. يَبحث عبر: OpportunityLang.Title، OpportunityLang.ShortDescription، Organizer.Name، Country.Name، OpportunityType.Name، OpportunitySector.Name. بحث نصّي كامل جانب-الخادم عبر PostgreSQL tsvector أو ما يُكافئ. الحد الأدنى: 2 حرف.
- فلتر — النوع
- تعدّد-اختيار. المصدر: جدول OpportunityType. القيم: accelerator، incubator، grant، competition، fellowship، soft-landing، residency، vc-program. query param: type=accelerator,grant.
- فلتر — الدولة
- تعدّد-اختيار. المصدر: جدول Country (16 رمز MENA من §9.6). query param: country=eg,sa.
- فلتر — مرحلة الشركة
- تعدّد-اختيار. القيم: idea، pre-seed، seed، series-a، growth، sme، scaleup. يُرآة StartupStage type في §9.6. query param: stage=pre-seed,seed.
- فلتر — القطاع
- تعدّد-اختيار. المصدر: جدول OpportunitySector. taxonomy حرّة مُعَبَّأة تحريرياً: fintech، healthtech، edtech، climatetech، deeptech، agritech، إلخ. query param: sector=fintech,climatetech.
- فلتر — الحالة
- اختيار-واحد (يُرآة status tabs C). query param: status=open|closing-soon|rolling|closed|all.
- منسدلة الفرز
- القيم: most-relevant (افتراضي عند وجود q، وإلا deadline-soonest) · deadline-soonest · newest · recently-verified · closing-soon. query param: sort=deadline-soonest.
E · شبكة كروت الفرص — حقول لكل كرت
- id
- المصدر: Opportunity.Id (UUID). داخلي فقط، لا يُعرض أبداً.
- slug
- المصدر: Opportunity.Slug. kebab-case، ASCII، فريد. يُستخدم في رابط الكرت → /:lang/opportunity-radar/{slug}.
- العنوان
- المصدر: OpportunityLang.Title حيث Lang = المحلّية الحالية. يَعود لـ English لو متغيّر AR مفقود. مطلوب: نعم.
- تسمية النوع
- المصدر: OpportunityType.NameLang. اعرض كـ chip أحرف-كبيرة صغير في صفّ meta للكرت. مطلوب: نعم.
- شارة الحالة
- المصدر: Opportunity.Status. صبغة بالقيمة (انظر صفحة Status Logic). مطلوب: نعم.
- شارة sponsored
- المصدر: Opportunity.IsSponsored. اعرض فقط عند true. معالجة بصرية مميَّزة (صبغة برتقالية، تسمية "SPONSORED").
- شارة verified
- المصدر: Opportunity.IsVerified. اعرض فقط عند true. إيقونة ✓ صغيرة + tooltip "Verified".
- تسمية الموعد
- محتسب من Opportunity.DeadlineDate و Opportunity.IsRolling. المنطق: لو IsRolling → "Rolling applications"؛ وإلا لو days_to(DeadlineDate) ≤ 1 → "Last day to apply"؛ وإلا لو days_to ≤ 7 → "{N} days left"؛ وإلا → سلسلة تاريخ مُهيَّأة بالمحلّية الحالية. مطلوب: نعم.
- تسمية closing-soon
- شَرطي. يُعرض حين الحالة المُحتسَبة === ClosingSoon. يَتراكب pill "Closing soon" بصبغة warning في الزاوية العليا اليمنى للكرت.
- الوصف القصير
- المصدر: OpportunityLang.ShortDescription. حد أقصى 200 حرف، 3-سطر clamp عبر CSS. مطلوب: نعم.
- الدولة
- المصدر: Country.Code (ISO 3166-1 alpha-2) + Country.NameLang. العرض: علم emoji + الاسم. مطلوب: نعم.
- stage[]
- المصدر: جدول join OpportunityStage. العرض: حتى 2 chip؛ لو أكثر، chip overflow "+N". مطلوب: اختياري.
- sector[]
- المصدر: جدول join OpportunitySector. نفس قواعد العرض كـ stage. مطلوب: اختياري.
- اسم المنظِّم
- المصدر: Organizer.Name. العرض: نص صغير تحت العنوان مع logo اختياري (Organizer.LogoUrl). مطلوب: نعم.
- officialUrl
- المصدر: Opportunity.OfficialUrl. مطلوب حين Status ∈ { Open، ClosingSoon، Rolling }. لو مفقود، CTA "Apply via Official Source" يُرسم disabled مع tooltip.
- CTA #1 — Apply
- التسمية: "Apply via Official Source". الفعل: window.open(officialUrl، '_blank'، 'noopener,noreferrer'). يُطلق حدث تحليلات opportunity_apply_click.
- CTA #2 — View Opportunity
- الفعل: routerLink لـ /:lang/opportunity-radar/{slug}. يُطلق opportunity_card_click.
صبغات حالة الكرت
- Open → accent أخضر (color-token: --status-open / --badge-verified-like).
- ClosingSoon → accent برتقالي (--brand-orange).
- Rolling → accent بنفسجي (--primary tint عند opacity 60%).
- Closed → رمادي خفيف (--muted-foreground). opacity الكرت يَنزل لـ 0.6.
F · كتلة اشتراك Weekly Opportunity Signals
- عنوان الكتلة + الوصف
- المصدر: PageConfig.opportunityRadarListing.subscribeBlock.{title,description}.
- حقل البريد
- مطلوب. التحقّق: regex بريد مُمتثل RFC. الإرسال مُعطَّل حتى الصلاحية.
- تفضيل الدولة
- اختياري. نفس خيارات قائمة filter D country. الافتراضي "All MENA".
- تفضيل المرحلة
- اختياري. نفس خيارات قائمة filter D stage. الافتراضي "All stages".
- مربع الموافقة (consentPlatform)
- مطلوب. النص: "I agree to receive weekly opportunity signals from StartupHub.today." بوّابة GDPR / PDPL. الإرسال مُعطَّل حتى التأشير.
- مربع موافقة الشركاء (consentPartners)
- اختياري. النص: "Also share my profile with relevant sponsor partners." يُوَجِّه العميل لـ CRM الراعي لو مُؤشَّر. الافتراضي غير مُؤشَّر.
- زر Subscribe
- POST /api/subscriptions/opportunity-digest. الـ Body: { email, country?, stage?, consentPlatform, consentPartners, source: 'listing' }. عند النجاح: تأكيد inline. عند الخطأ: banner خطأ inline.
G · أكمل رحلتك — كروت cross-module
- 4 كروت ثابتة. المصدر: PageConfig.opportunityRadarListing.crossModuleCards[].
- كل كرت: { title, shortDescription, targetModule, targetRoute, displayOrder, icon }.
- الأهداف القياسية: Build your Founder Profile (/founders/edit) · Read Founder Files (/founder-files) · Apply to Startup Showcase (/showcase) · Track MENA Events (/calendar).
- نقرة كل كرت تُطلق opportunity_cross_module_click مع { target_module, target_route }.
حالات UI
- Loading (الجلب الأوّلي)
- كروت skeleton: نفس تخطيط الكروت الحقيقية، shimmer متحرّك. اعرض 6 skeletons (يطابق الحجم الافتراضي للصفحة).
- Empty (لا نتائج بعد الفلتر)
- ارسم حالة فارغة موثّقة — رسم توضيحي + رسالة محلّية صحيحة تَقتبس الفلاتر النشطة + CTA "Clear filters" + رابط لـ /:lang/opportunity-radar (لا فلاتر).
- Error (فشل API)
- إشعار toast (يمين-أعلى LTR، يسار-أعلى RTL) + CTA retry inline فوق شبكة الكروت. منطقة الكروت تَحتفظ بأيّ بيانات مُحَمَّلة سابقاً.
- Pagination — infinite scroll
- IntersectionObserver على sentinel تحت الشبكة. كل دفعة = 24 كرت. أطلق opportunity_listing_load_more على كل جلب بعد الأول.
