From 0a9bf8ab82d114f99ddb0dc77b913374511bb5d9 Mon Sep 17 00:00:00 2001 From: Al-Faris Mujahid AlZakwani Date: Thu, 2 Jul 2026 15:24:30 +0400 Subject: [PATCH] Add Contributor Center --- ar/contribute.html | 173 +++++++++ assets/css/contribute.css | 548 +++++++++++++++++++++++++++++ assets/js/auth-ui-db.js | 23 ++ assets/js/contribute.js | 340 ++++++++++++++++++ assets/js/roadmap.js | 3 + contribute.html | 182 ++++++++++ package.json | 3 +- scripts/contributor-center-test.js | 101 ++++++ scripts/seo-assets.js | 20 ++ service-worker.js | 6 + sitemap.xml | 10 + vercel.json | 2 + 12 files changed, 1410 insertions(+), 1 deletion(-) create mode 100644 ar/contribute.html create mode 100644 assets/css/contribute.css create mode 100644 assets/js/contribute.js create mode 100644 contribute.html create mode 100644 scripts/contributor-center-test.js diff --git a/ar/contribute.html b/ar/contribute.html new file mode 100644 index 0000000..d81afd3 --- /dev/null +++ b/ar/contribute.html @@ -0,0 +1,173 @@ + + + + + + NexCore Labs | مركز المساهمين + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+

مركز المساهمين

+

ساعد شيئًا مفيدًا على المضي قدمًا.

+

اختر طريقة عملية للمساهمة—انشر أعمال الطلبة، أو ساهم في تشكيل خارطة الطريق، أو اختبر التجربة، أو حسّن قاعدة الشيفرة مفتوحة المصدر.

+ +
+
+
شاركانشر عملًا مفيدًا
+
ابنِحسّن الأدوات المفتوحة
+
اختبراجعل الوصول أوضح
+
ساهم برأيكوجّه ما سيُطلق لاحقًا
+
+
+
+ +

اختر مسار مساهمة

ابدأ بما يمكنك تقديمه الآن. يقود كل مسار إلى إجراء حقيقي داخل NexCore.

+

نشاط مساهماتك

تظهر هنا المشاريع وأفكار خارطة الطريق التي ترسلها عند تسجيل الدخول.

+

هل لديك تحسين صغير في ذهنك؟

الفكرة المحددة والمشروحة جيدًا مساهمة قيّمة أيضًا.

شارك الفكرة
+
+ +
NexCore Labs
+ + + + + + + + + diff --git a/assets/css/contribute.css b/assets/css/contribute.css new file mode 100644 index 0000000..cfa43ce --- /dev/null +++ b/assets/css/contribute.css @@ -0,0 +1,548 @@ +.contribute-hero { + min-height: 0; + padding: 132px 0 54px; +} + +.contribute-shell { + width: min(1180px, calc(100% - 48px)); + margin: 0 auto; +} + +.contribute-hero-grid { + display: grid; + grid-template-columns: minmax(0, 1.12fr) minmax(320px, .88fr); + align-items: center; + gap: clamp(32px, 6vw, 76px); + padding-top: 58px; +} + +.contribute-eyebrow { + display: inline-flex; + align-items: center; + gap: 8px; + margin: 0 0 14px; + color: #6ee7f3; + font-size: .76rem; + font-weight: 800; + letter-spacing: .12em; + text-transform: uppercase; +} + +.contribute-title { + max-width: 720px; + margin: 0 0 20px; + font-size: clamp(2.8rem, 5vw, 5rem); + line-height: .98; + letter-spacing: -.055em; +} + +.contribute-title em { + color: #6ee7f3; + font-style: normal; +} + +.contribute-lead { + max-width: 60ch; + margin: 0; + color: var(--muted); + font-size: 1rem; + line-height: 1.7; +} + +.contribute-actions { + display: flex; + flex-wrap: wrap; + gap: 12px; + margin-top: 26px; +} + +.contribute-actions .btn { + justify-content: center; +} + +.contribute-summary { + position: relative; + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 10px; + padding: 16px; + overflow: hidden; + border: 2px solid rgba(255, 255, 255, .035); + border-radius: 20px; + background: linear-gradient(135deg, rgba(110, 231, 243, .08), rgba(10, 11, 28, .58) 58%, rgba(172, 217, 229, .04)); + box-shadow: var(--card-shadow); + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); +} + +.contribute-summary::before { + position: absolute; + top: -90px; + right: -70px; + width: 230px; + height: 230px; + border-radius: 50%; + background: radial-gradient(circle, rgba(110, 231, 243, .2), transparent 68%); + content: ""; + pointer-events: none; +} + +.contribute-summary-item { + position: relative; + z-index: 1; + min-height: 112px; + padding: 18px; + border: 1px solid rgba(255, 255, 255, .06); + border-radius: 14px; + background: rgba(255, 255, 255, .025); +} + +.contribute-summary-item i { + color: #6ee7f3; + font-size: 1rem; +} + +.contribute-summary-item strong { + display: block; + margin-top: 14px; + color: #eaf6fb; + font-size: 1.25rem; + line-height: 1.1; +} + +.contribute-summary-item span { + display: block; + margin-top: 6px; + color: var(--muted); + font-size: .75rem; +} + +.contribute-section { + padding: 52px 0; +} + +.contribute-section-head { + display: flex; + align-items: end; + justify-content: space-between; + gap: 24px; + margin-bottom: 24px; +} + +.contribute-section-head .section-lead { + margin-bottom: 0; +} + +.contribute-filters { + display: flex; + max-width: 100%; + gap: 8px; + overflow-x: auto; + padding: 4px; + scrollbar-width: thin; +} + +.contribute-filter { + flex: 0 0 auto; + padding: 8px 14px; + border: 1px solid rgba(255, 255, 255, .1); + border-radius: 999px; + background: rgba(255, 255, 255, .015); + color: var(--muted); + font: inherit; + font-size: .82rem; + cursor: pointer; +} + +.contribute-filter:hover, +.contribute-filter[aria-pressed="true"] { + border-color: rgba(110, 231, 243, .55); + background: rgba(110, 231, 243, .1); + color: #dffbff; +} + +.contribute-filter:focus-visible, +.contribute-card:focus-visible, +.activity-link:focus-visible { + outline: 2px solid #6ee7f3; + outline-offset: 3px; +} + +.contribute-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 16px; +} + +.contribute-card { + display: flex; + min-width: 0; + flex-direction: column; + padding: 24px; + border: 1px solid rgba(255, 255, 255, .055); + border-radius: 16px; + background: linear-gradient(180deg, rgba(255, 255, 255, .038), rgba(255, 255, 255, .012)); + box-shadow: 0 10px 30px rgba(2, 6, 23, .48); +} + +.contribute-card:hover { + border-color: rgba(110, 231, 243, .22); + box-shadow: 0 18px 48px rgba(2, 6, 23, .68), 0 0 24px rgba(110, 231, 243, .07); + transform: translateY(-4px); +} + +.contribute-card-top { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 16px; +} + +.contribute-card-icon { + display: inline-flex; + width: 42px; + height: 42px; + align-items: center; + justify-content: center; + flex: 0 0 auto; + border: 1px solid rgba(110, 231, 243, .18); + border-radius: 12px; + background: rgba(110, 231, 243, .08); + color: #6ee7f3; +} + +.contribute-card-kind { + padding: 5px 9px; + border-radius: 999px; + background: rgba(255, 255, 255, .05); + color: var(--muted); + font-size: .68rem; + font-weight: 700; + letter-spacing: .06em; + text-transform: uppercase; +} + +.contribute-card h3 { + margin: 20px 0 8px; + color: #eaf6fb; + font-size: 1.15rem; +} + +.contribute-card p { + margin: 0 0 20px; + color: var(--muted); + font-size: .9rem; + line-height: 1.65; +} + +.contribute-card-meta { + display: flex; + flex-wrap: wrap; + gap: 7px; + margin-bottom: 22px; +} + +.contribute-card-meta span { + padding: 5px 8px; + border: 1px solid rgba(255, 255, 255, .06); + border-radius: 8px; + color: #b8c4cf; + font-size: .7rem; +} + +.contribute-card .btn { + width: fit-content; + margin-top: auto; + font-size: .78rem; +} + +.contribute-empty { + grid-column: 1 / -1; + padding: 36px; + border: 1px dashed rgba(110, 231, 243, .2); + border-radius: 16px; + color: var(--muted); + text-align: center; +} + +.activity-panel { + display: grid; + grid-template-columns: minmax(250px, .72fr) minmax(0, 1.28fr); + gap: 16px; + padding: 18px; + border: 1px solid rgba(255, 255, 255, .055); + border-radius: 18px; + background: linear-gradient(135deg, rgba(110, 231, 243, .055), rgba(10, 11, 28, .5) 52%, rgba(255, 255, 255, .018)); + box-shadow: var(--card-shadow); +} + +.activity-overview, +.activity-feed { + min-width: 0; + padding: 22px; + border-radius: 14px; + background: rgba(10, 11, 28, .48); +} + +.activity-kicker { + display: inline-flex; + align-items: center; + gap: 7px; + color: #6ee7f3; + font-size: .72rem; + font-weight: 800; + letter-spacing: .09em; + text-transform: uppercase; +} + +.activity-overview h3 { + margin: 12px 0 8px; + color: #eaf6fb; + font-size: 1.35rem; +} + +.activity-overview p, +.activity-feed p { + color: var(--muted); + font-size: .86rem; + line-height: 1.6; +} + +.activity-stats { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 8px; + margin-top: 20px; +} + +.activity-stat { + padding: 12px 8px; + border: 1px solid rgba(255, 255, 255, .055); + border-radius: 10px; + text-align: center; +} + +.activity-stat strong { + display: block; + color: #eaf6fb; + font-size: 1.15rem; +} + +.activity-stat span { + color: var(--muted); + font-size: .66rem; +} + +.activity-feed-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + margin-bottom: 12px; +} + +.activity-feed-head h3 { + margin: 0; + color: #eaf6fb; + font-size: 1rem; +} + +.activity-list { + display: grid; + gap: 8px; +} + +.activity-item { + display: grid; + grid-template-columns: auto minmax(0, 1fr) auto; + align-items: center; + gap: 12px; + padding: 12px; + border: 1px solid rgba(255, 255, 255, .05); + border-radius: 11px; + background: rgba(255, 255, 255, .018); +} + +.activity-item-icon { + display: inline-flex; + width: 32px; + height: 32px; + align-items: center; + justify-content: center; + border-radius: 9px; + background: rgba(110, 231, 243, .08); + color: #6ee7f3; +} + +.activity-item-title { + overflow: hidden; + color: #eaf6fb; + font-size: .84rem; + font-weight: 700; + text-overflow: ellipsis; + white-space: nowrap; +} + +.activity-item-meta { + color: var(--muted); + font-size: .68rem; +} + +.activity-status { + padding: 5px 8px; + border-radius: 999px; + background: rgba(110, 231, 243, .09); + color: #bffcff; + font-size: .65rem; + white-space: nowrap; +} + +.activity-status--completed, +.activity-status--published, +.activity-status--approved { + background: rgba(34, 197, 94, .12); + color: #86efac; +} + +.activity-status--pending, +.activity-status--planned, +.activity-status--draft { + background: rgba(245, 166, 35, .12); + color: #ffd27a; +} + +.activity-link { + color: #6ee7f3; + font-size: .75rem; + font-weight: 700; +} + +.activity-message { + display: flex; + min-height: 190px; + align-items: center; + justify-content: center; + flex-direction: column; + gap: 10px; + color: var(--muted); + text-align: center; +} + +.activity-message i { + color: #6ee7f3; + font-size: 1.5rem; +} + +.contribute-cta { + display: flex; + align-items: center; + justify-content: space-between; + gap: 28px; + margin: 38px 0 72px; + padding: 30px; + border: 1px solid rgba(110, 231, 243, .14); + border-radius: 18px; + background: linear-gradient(135deg, rgba(110, 231, 243, .08), rgba(10, 11, 28, .5) 54%, rgba(172, 217, 229, .04)); + box-shadow: var(--card-shadow); +} + +.contribute-cta h2 { + margin: 0 0 6px; + color: #eaf6fb; + font-size: 1.35rem; +} + +.contribute-cta p { + margin: 0; + color: var(--muted); +} + +html[dir="rtl"] .contribute-eyebrow, +html[dir="rtl"] .contribute-card-kind, +html[dir="rtl"] .activity-kicker { + letter-spacing: 0; +} + +@media (max-width: 900px) { + .contribute-hero-grid, + .activity-panel { + grid-template-columns: 1fr; + } + + .contribute-summary { + width: min(560px, 100%); + } + + .contribute-section-head { + align-items: flex-start; + flex-direction: column; + } +} + +@media (max-width: 680px) { + .contribute-shell { + width: min(100% - 28px, 1180px); + } + + .contribute-hero { + padding: 104px 0 28px; + } + + .contribute-hero-grid { + gap: 28px; + padding-top: 42px; + text-align: center; + } + + .contribute-eyebrow, + .contribute-actions { + justify-content: center; + } + + .contribute-grid { + grid-template-columns: 1fr; + } + + .contribute-card, + .activity-overview, + .activity-feed { + padding: 18px; + } + + .activity-panel { + padding: 10px; + } + + .activity-item { + grid-template-columns: auto minmax(0, 1fr); + } + + .activity-status { + grid-column: 2; + width: fit-content; + } + + .contribute-cta { + align-items: stretch; + flex-direction: column; + text-align: center; + } + + .contribute-cta .btn { + justify-content: center; + } +} + +@media (max-width: 420px) { + .contribute-summary { + grid-template-columns: 1fr; + } + + .contribute-summary-item { + min-height: 94px; + } + + .activity-stats { + grid-template-columns: 1fr; + } +} diff --git a/assets/js/auth-ui-db.js b/assets/js/auth-ui-db.js index c47ca32..490ed52 100644 --- a/assets/js/auth-ui-db.js +++ b/assets/js/auth-ui-db.js @@ -80,6 +80,28 @@ hubLink.insertAdjacentElement('afterend', link); } + function ensureContributeNavigation() { + const menu = document.getElementById('myDropdown'); + if (!menu || menu.querySelector('[data-contribute-nav]')) return; + + const initiativesLink = menu.querySelector('[data-initiatives-nav]'); + const hubLink = [...menu.querySelectorAll('a')].find((link) => + /(^|\/)hub(?:\.html)?(?:#|$)/.test(link.getAttribute('href') || '') + ); + const anchor = initiativesLink || hubLink; + if (!anchor) return; + + const link = document.createElement('a'); + link.href = isArabicPage ? '/ar/contribute' : '/contribute'; + link.dataset.contributeNav = 'true'; + link.title = isArabicPage ? 'مركز المساهمين' : 'Contributor Center'; + link.innerHTML = ` ${isArabicPage ? 'مركز المساهمين' : 'Contributor Center'} ${isArabicPage ? 'جديد' : 'New'}`; + if (window.location.pathname.replace(/\/$/, '') === link.getAttribute('href')) { + link.setAttribute('aria-current', 'page'); + } + anchor.insertAdjacentElement('afterend', link); + } + /** * Fetch approved emails from database */ @@ -397,6 +419,7 @@ // Initialize function init() { ensureInitiativesNavigation(); + ensureContributeNavigation(); // Ensure nav elements exist ensureNavElements(); diff --git a/assets/js/contribute.js b/assets/js/contribute.js new file mode 100644 index 0000000..a6b638c --- /dev/null +++ b/assets/js/contribute.js @@ -0,0 +1,340 @@ +(function () { + "use strict"; + + const isArabic = document.documentElement.lang.toLowerCase().startsWith("ar"); + const locale = isArabic ? "ar" : "en"; + const prefix = isArabic ? "../" : ""; + const cleanPrefix = isArabic ? "/ar" : ""; + const text = (en, ar) => (isArabic ? ar : en); + const byId = (id) => document.getElementById(id); + + const paths = Object.freeze([ + { + id: "share-project", + category: "share", + icon: "fa-solid fa-diagram-project", + kind: { en: "Share", ar: "شارك" }, + title: { en: "Publish your project", ar: "انشر مشروعك" }, + description: { + en: "Create a project profile, explain what you built, and make useful student work easier for the SQU community to discover.", + ar: "أنشئ ملفًا لمشروعك، واشرح ما بنيته، واجعل أعمال الطلبة المفيدة أسهل اكتشافًا لمجتمع جامعة السلطان قابوس." + }, + meta: [{ en: "Project owners", ar: "أصحاب المشاريع" }, { en: "15–25 min", ar: "15–25 دقيقة" }], + href: `${cleanPrefix}/dashboard`, + cta: { en: "Create a project", ar: "أنشئ مشروعًا" } + }, + { + id: "shape-roadmap", + category: "shape", + icon: "fa-solid fa-lightbulb", + kind: { en: "Shape", ar: "ساهم برأيك" }, + title: { en: "Suggest an improvement", ar: "اقترح تحسينًا" }, + description: { + en: "Share a focused feature idea, vote on community requests, and follow what moves from planned to shipped.", + ar: "شارك فكرة ميزة محددة، وصوّت على طلبات المجتمع، وتابع ما ينتقل من التخطيط إلى الإطلاق." + }, + meta: [{ en: "Everyone", ar: "للجميع" }, { en: "5 min", ar: "5 دقائق" }], + href: `${cleanPrefix}/roadmap?action=suggest`, + cta: { en: "Suggest an idea", ar: "اقترح فكرة" } + }, + { + id: "explore-initiatives", + category: "build", + icon: "fa-solid fa-wand-magic-sparkles", + kind: { en: "Build", ar: "ابنِ" }, + title: { en: "Find an initiative to support", ar: "اعثر على مبادرة لدعمها" }, + description: { + en: "Understand what NexCore Labs is launching, building, and exploring before offering the skill or perspective that fits.", + ar: "تعرّف على ما تطلقه NexCore Labs وما تبنيه وتستكشفه قبل تقديم المهارة أو المنظور المناسب." + }, + meta: [{ en: "Collaborators", ar: "المتعاونون" }, { en: "Explore first", ar: "ابدأ بالاستكشاف" }], + href: `${cleanPrefix}/initiatives`, + cta: { en: "Explore initiatives", ar: "استكشف المبادرات" } + }, + { + id: "improve-code", + category: "build", + icon: "fa-brands fa-github", + kind: { en: "Build", ar: "ابنِ" }, + title: { en: "Improve the NexCore codebase", ar: "حسّن قاعدة شيفرة NexCore" }, + description: { + en: "Review the open-source repository, report a reproducible issue, or propose a focused code improvement through GitHub.", + ar: "راجع المستودع مفتوح المصدر، أو أبلغ عن مشكلة قابلة للتكرار، أو اقترح تحسينًا محددًا للشيفرة عبر GitHub." + }, + meta: [{ en: "Developers", ar: "المطورون" }, { en: "Open source", ar: "مفتوح المصدر" }], + href: "https://github.com/NexCoreLabs/NexCore", + external: true, + cta: { en: "Open GitHub", ar: "افتح GitHub" } + }, + { + id: "test-experience", + category: "test", + icon: "fa-solid fa-universal-access", + kind: { en: "Test", ar: "اختبر" }, + title: { en: "Test the community experience", ar: "اختبر تجربة المجتمع" }, + description: { + en: "Use NexCore in English or Arabic, then turn accessibility, clarity, or mobile issues into a focused roadmap suggestion.", + ar: "استخدم NexCore بالعربية أو الإنجليزية، ثم حوّل مشكلات الوصول أو الوضوح أو الهاتف إلى اقتراح محدد في خارطة الطريق." + }, + meta: [{ en: "Testers", ar: "المختبرون" }, { en: "EN + AR", ar: "عربي + إنجليزي" }], + href: `${cleanPrefix}/roadmap?action=suggest`, + cta: { en: "Report an improvement", ar: "أبلغ عن تحسين" } + }, + { + id: "support-community", + category: "share", + icon: "fa-solid fa-people-group", + kind: { en: "Share", ar: "شارك" }, + title: { en: "Help useful work get noticed", ar: "ساعد الأعمال المفيدة على الظهور" }, + description: { + en: "Browse published projects, share the ones that solve real problems, and help more students find practical work from their peers.", + ar: "تصفّح المشاريع المنشورة، وشارك ما يحل مشكلات حقيقية، وساعد مزيدًا من الطلبة على اكتشاف أعمال عملية من زملائهم." + }, + meta: [{ en: "Community", ar: "المجتمع" }, { en: "Any time", ar: "في أي وقت" }], + href: `${cleanPrefix}/hub#projects`, + cta: { en: "Browse projects", ar: "تصفّح المشاريع" } + } + ]); + + const copy = { + filters: { + all: text("All paths", "كل المسارات"), + build: text("Build", "البناء"), + test: text("Test", "الاختبار"), + share: text("Share", "المشاركة"), + shape: text("Shape", "إبداء الرأي") + }, + signInTitle: text("Your contribution story starts here", "تبدأ قصة مساهمتك من هنا"), + signInBody: text("Sign in to see your submitted projects and roadmap ideas together in one place.", "سجّل الدخول لترى مشاريعك وأفكار خارطة الطريق التي أرسلتها معًا في مكان واحد."), + signIn: text("Sign in", "تسجيل الدخول"), + activityTitle: text("Your contribution activity", "نشاط مساهماتك"), + activityBody: text("A live view of the work and ideas you have shared with NexCore.", "عرض مباشر للأعمال والأفكار التي شاركتها مع NexCore."), + projects: text("Projects", "المشاريع"), + ideas: text("Ideas", "الأفكار"), + shipped: text("Shipped", "المُطلقة"), + recent: text("Recent submissions", "أحدث المشاركات"), + viewAll: text("Open dashboard", "افتح لوحة التحكم"), + emptyTitle: text("No submissions yet", "لا توجد مشاركات بعد"), + emptyBody: text("Choose one contribution path above and your activity will appear here.", "اختر أحد مسارات المساهمة أعلاه وسيظهر نشاطك هنا."), + loading: text("Loading your activity…", "جارٍ تحميل نشاطك…"), + unavailable: text("Your activity could not be loaded right now.", "تعذر تحميل نشاطك الآن."), + project: text("Project", "مشروع"), + idea: text("Roadmap idea", "فكرة خارطة طريق"), + filtersLabel: text("Filter contribution paths", "تصفية مسارات المساهمة") + }; + + const state = { activeFilter: "all" }; + + function renderFilters() { + const container = byId("contributeFilters"); + if (!container) return; + container.setAttribute("aria-label", copy.filtersLabel); + container.replaceChildren(...Object.entries(copy.filters).map(([value, label]) => { + const button = document.createElement("button"); + button.type = "button"; + button.className = "contribute-filter"; + button.dataset.filter = value; + button.setAttribute("aria-pressed", String(value === state.activeFilter)); + button.textContent = label; + return button; + })); + } + + function createPathCard(path) { + const article = document.createElement("article"); + article.className = "contribute-card"; + article.dataset.category = path.category; + article.dataset.pathId = path.id; + + const top = document.createElement("div"); + top.className = "contribute-card-top"; + const icon = document.createElement("span"); + icon.className = "contribute-card-icon"; + icon.setAttribute("aria-hidden", "true"); + icon.innerHTML = ``; + const kind = document.createElement("span"); + kind.className = "contribute-card-kind"; + kind.textContent = path.kind[locale]; + top.append(icon, kind); + + const title = document.createElement("h3"); + title.textContent = path.title[locale]; + const description = document.createElement("p"); + description.textContent = path.description[locale]; + const meta = document.createElement("div"); + meta.className = "contribute-card-meta"; + path.meta.forEach((item) => { + const tag = document.createElement("span"); + tag.textContent = item[locale]; + meta.append(tag); + }); + const link = document.createElement("a"); + link.className = "btn ghost"; + link.href = path.href; + if (path.external) { + link.target = "_blank"; + link.rel = "noopener"; + } + link.innerHTML = ` ${path.cta[locale]}`; + article.append(top, title, description, meta, link); + return article; + } + + function renderPaths() { + const grid = byId("contributeGrid"); + if (!grid) return; + const visible = paths.filter((path) => state.activeFilter === "all" || path.category === state.activeFilter); + grid.replaceChildren(...visible.map(createPathCard)); + } + + function bindFilters() { + byId("contributeFilters")?.addEventListener("click", (event) => { + const button = event.target.closest("[data-filter]"); + if (!button || button.dataset.filter === state.activeFilter) return; + state.activeFilter = button.dataset.filter; + renderFilters(); + renderPaths(); + }); + } + + function safeStatus(value, fallback) { + const status = String(value || fallback || "").trim().toLowerCase().replace(/[^a-z_\-]/g, ""); + return status || "draft"; + } + + function statusLabel(status) { + const labels = { + draft: text("Draft", "مسودة"), + pending: text("Pending", "قيد المراجعة"), + approved: text("Approved", "معتمد"), + published: text("Published", "منشور"), + planned: text("Planned", "مخطط"), + in_progress: text("In progress", "قيد التنفيذ"), + completed: text("Shipped", "تم الإطلاق") + }; + return labels[status] || status.replace(/[_-]/g, " "); + } + + function renderSignedOut() { + const panel = byId("activityPanel"); + if (!panel) return; + panel.innerHTML = ` +
+ ${text("Member view", "عرض العضو")} +

${copy.signInTitle}

+

${copy.signInBody}

+ ${copy.signIn} +
+

${copy.signInBody}

`; + } + + function renderLoading() { + const panel = byId("activityPanel"); + if (!panel) return; + panel.innerHTML = `

${copy.loading}

`; + } + + function createActivityItem(item) { + const row = document.createElement("div"); + row.className = "activity-item"; + const icon = document.createElement("span"); + icon.className = "activity-item-icon"; + icon.innerHTML = ``; + const body = document.createElement("div"); + const title = document.createElement("div"); + title.className = "activity-item-title"; + title.textContent = item.title; + const meta = document.createElement("div"); + meta.className = "activity-item-meta"; + meta.textContent = `${item.type === "project" ? copy.project : copy.idea} · ${new Intl.DateTimeFormat(locale, { dateStyle: "medium" }).format(new Date(item.date))}`; + body.append(title, meta); + const status = document.createElement("span"); + status.className = `activity-status activity-status--${item.status}`; + status.textContent = statusLabel(item.status); + row.append(icon, body, status); + return row; + } + + function renderActivity(user, projects, ideas) { + const panel = byId("activityPanel"); + if (!panel) return; + const shipped = ideas.filter((idea) => idea.status === "completed").length; + const name = String(user.user_metadata?.full_name || user.user_metadata?.name || user.email?.split("@")[0] || text("Member", "عضو")); + const items = [ + ...projects.map((project) => ({ + type: "project", + title: project.name || text("Untitled project", "مشروع بلا عنوان"), + status: safeStatus(project.published ? "published" : project.moderation_status, "draft"), + date: project.updated_at || project.created_at || new Date().toISOString() + })), + ...ideas.map((idea) => ({ + type: "idea", + title: idea.title || text("Untitled idea", "فكرة بلا عنوان"), + status: safeStatus(idea.status, "planned"), + date: idea.created_at || new Date().toISOString() + })) + ].sort((a, b) => new Date(b.date) - new Date(a.date)).slice(0, 6); + + panel.innerHTML = ` +
+ ${text("Member view", "عرض العضو")} +

${copy.activityTitle}

+

${name}, ${copy.activityBody.charAt(0).toLocaleLowerCase(locale)}${copy.activityBody.slice(1)}

+
+
${projects.length}${copy.projects}
+
${ideas.length}${copy.ideas}
+
${shipped}${copy.shipped}
+
+
+
+

${copy.recent}

${copy.viewAll}
+
+
`; + + const list = byId("activityList"); + if (!items.length) { + list.innerHTML = `
${copy.emptyTitle}

${copy.emptyBody}

`; + return; + } + list.replaceChildren(...items.map(createActivityItem)); + } + + async function loadActivity() { + const db = window.supabaseClient; + if (!db) { + renderSignedOut(); + return; + } + renderLoading(); + try { + const { data: { session } } = await db.auth.getSession(); + if (!session?.user) { + renderSignedOut(); + return; + } + const [projectsResult, ideasResult] = await Promise.all([ + db.from("projects").select("id,name,slug,published,moderation_status,created_at,updated_at").eq("owner_user_id", session.user.id), + db.from("features").select("id,title,status,votes_count,created_at").eq("created_by", session.user.id) + ]); + if (projectsResult.error || ideasResult.error) throw projectsResult.error || ideasResult.error; + renderActivity(session.user, projectsResult.data || [], ideasResult.data || []); + } catch (error) { + console.warn("Contributor activity unavailable:", error?.message || error); + const panel = byId("activityPanel"); + if (panel) panel.innerHTML = `

${copy.unavailable}

`; + } + } + + function init() { + const year = byId("year"); + if (year) year.textContent = new Date().getFullYear(); + renderFilters(); + renderPaths(); + bindFilters(); + loadActivity(); + } + + window.ContributorCenter = Object.freeze({ paths, safeStatus, statusLabel }); + if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", init); + else init(); +}()); diff --git a/assets/js/roadmap.js b/assets/js/roadmap.js index 1f93c1c..15277eb 100644 --- a/assets/js/roadmap.js +++ b/assets/js/roadmap.js @@ -124,6 +124,9 @@ async function init() { // Load data loadAnonVotes(); await loadFeatures(); + if (new URLSearchParams(window.location.search).get('action') === 'suggest') { + openSuggestModal(); + } } // ── REVEAL OBSERVER ──────────────────────────────────────── diff --git a/contribute.html b/contribute.html new file mode 100644 index 0000000..40a2194 --- /dev/null +++ b/contribute.html @@ -0,0 +1,182 @@ + + + + + + NexCore Labs | Contributor Center + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+

Contributor Center

+

Make something useful move forward.

+

Choose a practical way to contribute—publish student work, shape the roadmap, test the experience, or improve the open-source codebase.

+ +
+
+
SharePublish useful work
+
BuildImprove open tools
+
TestMake access clearer
+
ShapeGuide what ships next
+
+
+
+ +
+
+

Choose a contribution path

Start with what you can offer now. Each path leads to a real NexCore workflow.

+
+
+
+ +
+

Your contribution activity

Projects and roadmap ideas you submit appear here when you sign in.

+
+ +

Have a small improvement in mind?

A focused, well-explained idea is a valuable contribution too.

Share the idea
+
+ +
NexCore Labs
+ + + + + + + + + diff --git a/package.json b/package.json index 215cc1a..c61f3ce 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "lib": "lib" }, "scripts": { - "test": "npm run release:test && npm run pricing:test && npm run branding:test && npm run seo:test && npm run auth-nav:test && npm run anchor-nav:test && npm run initiatives:test && npm run initiatives-admin:test && npm run roadmap:test && npm run ai-api:test && npm run homepage-metrics:test && npm run counter-coverage:test && npm run account-ux:test && npm run team-id-cards:test", + "test": "npm run release:test && npm run pricing:test && npm run branding:test && npm run seo:test && npm run auth-nav:test && npm run anchor-nav:test && npm run initiatives:test && npm run initiatives-admin:test && npm run roadmap:test && npm run contributor-center:test && npm run ai-api:test && npm run homepage-metrics:test && npm run counter-coverage:test && npm run account-ux:test && npm run team-id-cards:test", "release": "node scripts/sync-release-version.js", "release:test": "node scripts/release-version-test.js", "security:audit": "npm audit --omit=dev --audit-level=high", @@ -19,6 +19,7 @@ "initiatives:test": "node scripts/initiatives-test.js", "initiatives-admin:test": "node scripts/initiatives-admin-test.js", "roadmap:test": "node scripts/roadmap-shared-test.js", + "contributor-center:test": "node scripts/contributor-center-test.js", "ai-api:test": "node scripts/ai-api-test.js", "homepage-metrics:test": "node scripts/homepage-metrics-test.js", "counter-coverage:test": "node scripts/counter-coverage-test.js", diff --git a/scripts/contributor-center-test.js b/scripts/contributor-center-test.js new file mode 100644 index 0000000..ac6014a --- /dev/null +++ b/scripts/contributor-center-test.js @@ -0,0 +1,101 @@ +"use strict"; + +const assert = require("assert"); +const fs = require("fs"); +const path = require("path"); +const vm = require("vm"); + +const root = path.resolve(__dirname, ".."); +const read = (file) => fs.readFileSync(path.join(root, file), "utf8"); + +const pages = { + "contribute.html": { + title: "NexCore Labs | Contributor Center", + canonical: "https://nexcorelabs.vercel.app/contribute", + alternate: "https://nexcorelabs.vercel.app/ar/contribute", + css: "assets/css/contribute.css", + js: "assets/js/contribute.js", + label: "Contributor Center", + }, + "ar/contribute.html": { + title: "NexCore Labs | مركز المساهمين", + canonical: "https://nexcorelabs.vercel.app/ar/contribute", + alternate: "https://nexcorelabs.vercel.app/contribute", + css: "../assets/css/contribute.css", + js: "../assets/js/contribute.js", + label: "مركز المساهمين", + }, +}; + +for (const [file, expected] of Object.entries(pages)) { + const html = read(file); + assert(html.includes(`${expected.title}`), `${file} must use the localized title`); + assert(html.includes(``), `${file} must use a clean canonical URL`); + assert(html.includes(expected.alternate), `${file} must link to its alternate locale`); + assert(html.includes(`href="${expected.css}"`), `${file} must load shared Contributor Center styles`); + assert(html.includes(`src="${expected.js}"`), `${file} must load shared Contributor Center behavior`); + assert(html.includes('id="contributeFilters"'), `${file} must include path filters`); + assert(html.includes('id="contributeGrid"'), `${file} must include contribution paths`); + assert(html.includes('id="activityPanel"'), `${file} must include member activity`); + assert(html.includes('data-contribute-nav'), `${file} must identify Contributor Center navigation`); + assert(html.includes('aria-current="page"'), `${file} must mark the active navigation item`); + assert(html.includes(expected.label), `${file} must include the localized Contributor Center label`); + assert(html.includes("roadmap.html?action=suggest"), `${file} must deep-link to idea submission`); + assert(!html.includes("fa-sparkles"), `${file} must only use supported Font Awesome Free icons`); +} + +const contributorJs = read("assets/js/contribute.js"); +for (const contract of [ + 'db.from("projects")', + '.eq("owner_user_id", session.user.id)', + 'db.from("features")', + '.eq("created_by", session.user.id)', + 'window.ContributorCenter', +]) { + assert(contributorJs.includes(contract), `Contributor activity must retain ${contract}`); +} + +const windowStub = { ContributorCenter: null }; +const documentStub = { + readyState: "loading", + documentElement: { lang: "en" }, + addEventListener() {}, + getElementById() { return null; }, +}; +vm.runInNewContext(contributorJs, { + window: windowStub, + document: documentStub, + console, + Intl, + Date, +}); +assert.strictEqual(windowStub.ContributorCenter.paths.length, 6, "Contributor Center must expose six honest contribution paths"); +assert.strictEqual(windowStub.ContributorCenter.safeStatus("In Progress"), "inprogress", "Activity status classes must be sanitized"); +assert.strictEqual(windowStub.ContributorCenter.safeStatus("", "draft"), "draft", "Missing activity status must fall back safely"); + +const authUi = read("assets/js/auth-ui-db.js"); +assert(authUi.includes("function ensureContributeNavigation()"), "Shared navigation must inject Contributor Center where static markup is older"); +assert(authUi.includes("data.contributeNav") || authUi.includes("dataset.contributeNav"), "Injected Contributor Center links must carry a stable data attribute"); +assert(authUi.includes("fa-handshake-angle"), "Shared navigation must use the supported Contributor Center icon"); +assert(authUi.includes("ensureContributeNavigation();"), "Contributor Center navigation injection must run during auth UI initialization"); + +const roadmap = read("assets/js/roadmap.js"); +assert(roadmap.includes("get('action') === 'suggest'"), "Roadmap must recognize Contributor Center idea deep-links"); +assert(roadmap.includes("openSuggestModal();"), "Roadmap idea deep-links must open the real submission workflow"); + +const routeConfig = JSON.parse(read("vercel.json")); +for (const [source, destination] of [["/contribute", "/contribute.html"], ["/ar/contribute", "/ar/contribute.html"]]) { + assert(routeConfig.rewrites.some((rewrite) => rewrite.source === source && rewrite.destination === destination), `Vercel must rewrite ${source}`); +} + +const serviceWorker = read("service-worker.js"); +for (const url of ["/contribute", "/contribute.html", "/ar/contribute", "/ar/contribute.html", "/assets/css/contribute.css", "/assets/js/contribute.js"]) { + assert(serviceWorker.includes(`'${url}'`), `Service worker must precache ${url}`); +} + +const css = read("assets/css/contribute.css"); +assert(css.includes('html[dir="rtl"]'), "Contributor Center styles must include explicit RTL details"); +assert(css.includes("@media (max-width: 680px)"), "Contributor Center must include a mobile layout"); +assert(css.includes("var(--card-shadow)"), "Contributor Center must reuse NexCore design tokens"); + +console.log("Contributor Center tests passed."); diff --git a/scripts/seo-assets.js b/scripts/seo-assets.js index 8426730..87c73aa 100644 --- a/scripts/seo-assets.js +++ b/scripts/seo-assets.js @@ -154,6 +154,26 @@ const PAGE_PAIRS = [ breadcrumb: "المبادرات", }, }, + { + key: "contribute", + priority: "0.82", + en: { + file: "contribute.html", + path: "/contribute", + title: "NexCore Labs | Contributor Center", + description: + "Find practical ways to contribute to NexCore Labs, share projects, suggest improvements, and follow your community activity.", + breadcrumb: "Contributor Center", + }, + ar: { + file: "ar/contribute.html", + path: "/ar/contribute", + title: "NexCore Labs | مركز المساهمين", + description: + "اكتشف طرقًا عملية للمساهمة في NexCore Labs، وشارك المشاريع، واقترح التحسينات، وتابع نشاطك المجتمعي.", + breadcrumb: "مركز المساهمين", + }, + }, { key: "how-to-use", priority: "0.80", diff --git a/service-worker.js b/service-worker.js index f9dfca9..e1f2036 100644 --- a/service-worker.js +++ b/service-worker.js @@ -18,6 +18,10 @@ const PRECACHE_URLS = [ '/initiatives.html', '/ar/initiatives', '/ar/initiatives.html', + '/contribute', + '/contribute.html', + '/ar/contribute', + '/ar/contribute.html', '/how-to-use', '/how-to-use.html', '/ar/how-to-use', @@ -107,6 +111,7 @@ const PRECACHE_URLS = [ '/assets/css/roadmap.css', '/assets/css/initiatives.css', '/assets/css/initiatives-admin.css', + '/assets/css/contribute.css', '/assets/js/script.js', '/assets/js/team-id-cards.js', '/assets/js/ai-chat.js', @@ -120,6 +125,7 @@ const PRECACHE_URLS = [ '/assets/js/roadmap.js', '/assets/js/initiatives.js', '/assets/js/initiatives-admin.js', + '/assets/js/contribute.js', '/assets/js/supabase-client.js', '/assets/js/count-up.js', '/assets/js/github-contributions.js', diff --git a/sitemap.xml b/sitemap.xml index a4c25ad..cf1ff30 100644 --- a/sitemap.xml +++ b/sitemap.xml @@ -30,6 +30,16 @@ 2026-06-23 0.82 + + https://nexcorelabs.vercel.app/contribute + 2026-07-02 + 0.82 + + + https://nexcorelabs.vercel.app/ar/contribute + 2026-07-02 + 0.82 + https://nexcorelabs.vercel.app/how-to-use 2026-06-16 diff --git a/vercel.json b/vercel.json index 8b22adf..0a88ef0 100644 --- a/vercel.json +++ b/vercel.json @@ -15,6 +15,7 @@ { "source": "/how-to-use", "destination": "/how-to-use.html" }, { "source": "/hub", "destination": "/hub.html" }, { "source": "/initiatives", "destination": "/initiatives.html" }, + { "source": "/contribute", "destination": "/contribute.html" }, { "source": "/offline", "destination": "/offline.html" }, { "source": "/privacy-policy", "destination": "/privacy-policy.html" }, { "source": "/project", "destination": "/project.html" }, @@ -39,6 +40,7 @@ { "source": "/ar/how-to-use", "destination": "/ar/how-to-use.html" }, { "source": "/ar/hub", "destination": "/ar/hub.html" }, { "source": "/ar/initiatives", "destination": "/ar/initiatives.html" }, + { "source": "/ar/contribute", "destination": "/ar/contribute.html" }, { "source": "/ar/offline", "destination": "/ar/offline.html" }, { "source": "/ar/order-confirmation", "destination": "/ar/order-confirmation.html" }, { "source": "/ar/pricing", "destination": "/ar/pricing.html" },