mirror of
https://github.com/pezkuwichain/pwap.git
synced 2026-04-21 23:47:56 +00:00
feat: add University, Certificates (Perwerde), Research education pages
- UniversityPage (/education/university): static course catalog, 6 courses, level badges, enroll redirects to Perwerde - CertificatesPage (/education/certificates): blockchain-connected Perwerde platform, 3 tabs (Kurs/Tev li/Qediya), real api.query.perwerde + api.tx.perwerde.enroll, score tracker - ResearchPage (/education/research): 5 academic papers, expandable abstracts, status badges, submit CTA - Wire routes in App.tsx, remove comingSoon from all 3 in MobileHomeLayout - Add university.*, certificates.*, research.* i18n keys to en.ts
This commit is contained in:
@@ -72,6 +72,9 @@ const PollsPage = lazy(() => import('@/pages/governance/PollsPage'));
|
||||
const WhatsKURDPage = lazy(() => import('@/pages/social/WhatsKURDPage'));
|
||||
const KurdMediaPage = lazy(() => import('@/pages/social/KurdMediaPage'));
|
||||
const HelpPage = lazy(() => import('@/pages/HelpPage'));
|
||||
const UniversityPage = lazy(() => import('@/pages/education/UniversityPage'));
|
||||
const CertificatesPage = lazy(() => import('@/pages/education/CertificatesPage'));
|
||||
const ResearchPage = lazy(() => import('@/pages/education/ResearchPage'));
|
||||
|
||||
// Network pages
|
||||
const Mainnet = lazy(() => import('@/pages/networks/Mainnet'));
|
||||
@@ -242,6 +245,9 @@ function App() {
|
||||
<Route path="/social/whatskurd" element={<WhatsKURDPage />} />
|
||||
<Route path="/social/kurdmedia" element={<KurdMediaPage />} />
|
||||
<Route path="/help" element={<HelpPage />} />
|
||||
<Route path="/education/university" element={<UniversityPage />} />
|
||||
<Route path="/education/certificates" element={<CertificatesPage />} />
|
||||
<Route path="/education/research" element={<ResearchPage />} />
|
||||
<Route path="/presale" element={<Presale />} />
|
||||
<Route path="/launchpad" element={<PresaleList />} />
|
||||
<Route path="/launchpad/:id" element={<PresaleDetail />} />
|
||||
|
||||
@@ -96,10 +96,10 @@ const APP_SECTIONS: AppSection[] = [
|
||||
emoji: '📚',
|
||||
borderColor: 'border-l-yellow-500',
|
||||
apps: [
|
||||
{ title: 'mobile.app.university', icon: '🎓', route: '/education', comingSoon: true },
|
||||
{ title: 'mobile.app.university', icon: '🎓', route: '/education/university' },
|
||||
{ title: 'mobile.app.perwerde', icon: '📖', route: '/education', requiresAuth: true },
|
||||
{ title: 'mobile.app.certificates', icon: '🏆', route: '/education', comingSoon: true },
|
||||
{ title: 'mobile.app.research', icon: '🔬', route: '/education', comingSoon: true },
|
||||
{ title: 'mobile.app.certificates', icon: '🏆', route: '/education/certificates' },
|
||||
{ title: 'mobile.app.research', icon: '🔬', route: '/education/research' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -3990,4 +3990,53 @@ export default {
|
||||
'help.feature.community': 'Community Contact',
|
||||
'help.whatskurd.title': 'WhatsKURD Messaging',
|
||||
'help.whatskurd.desc': 'Contact us via the blockchain messaging system',
|
||||
|
||||
// University page
|
||||
'university.title': 'Zanîngeha Dijîtal',
|
||||
'university.subtitle': 'Kurdistan Digital University',
|
||||
'university.breadcrumb': 'Education',
|
||||
'university.stats.courses': 'Kurs / Courses',
|
||||
'university.stats.students': 'Xwendekar / Students',
|
||||
'university.reward': '🎁 Xelat / Reward:',
|
||||
'university.enroll': 'Tomar bibe / Enroll',
|
||||
|
||||
// Certificates / Perwerde page
|
||||
'certificates.title': 'Perwerde',
|
||||
'certificates.subtitle': 'Digital Education Platform',
|
||||
'certificates.breadcrumb': 'Education',
|
||||
'certificates.tab.courses': 'Kurs',
|
||||
'certificates.tab.enrolled': 'Tev li',
|
||||
'certificates.tab.completed': 'Qediya',
|
||||
'certificates.stats.points': 'Puan / Points',
|
||||
'certificates.stats.done': 'Qediyayî / Done',
|
||||
'certificates.stats.active': 'Aktîv / Active',
|
||||
'certificates.noWallet.title': 'Perwerde',
|
||||
'certificates.noWallet.desc': 'Ji kerema xwe wallet ve girêbidin da ku bikaribin kursan bibînin û tev li wan bibin.',
|
||||
'certificates.noWallet.en': 'Please connect your wallet to view and enroll in courses.',
|
||||
'certificates.loading': 'Tê barkirin... / Loading...',
|
||||
'certificates.empty.courses': 'Kursek tune / No courses available',
|
||||
'certificates.empty.enrolled': 'Tu tev li kursekê nebûyî / Not enrolled in any course',
|
||||
'certificates.empty.completed': 'Kursek neqediyaye / No completed courses',
|
||||
'certificates.course.status': 'Rewş / Status',
|
||||
'certificates.course.enrollment': 'Têketin / Enrolled',
|
||||
'certificates.course.points': 'Puan / Points',
|
||||
'certificates.course.openContent': 'Naveroka Kursê Veke / Open Course Content',
|
||||
'certificates.enroll': 'Tev li Kursê / Enroll',
|
||||
'certificates.completedBanner': 'Te ev kurs qedand! / You completed this course!',
|
||||
'certificates.enrolledBanner': 'Tu tev li vê kursê yî / You are enrolled',
|
||||
|
||||
// Research page
|
||||
'research.title': 'Navenda Lêkolînê',
|
||||
'research.subtitle': 'Research Center',
|
||||
'research.breadcrumb': 'Education',
|
||||
'research.paperCount': '{{count}} lêkolîn / papers',
|
||||
'research.about.title': 'Der barê Navenda Lêkolînê / About',
|
||||
'research.about.ku': 'Navenda Lêkolînê gotarên zanistî yên li ser aboriya dijîtal a Kurdistanê, teknolojiya blockchain, û mijarên pêwendîdar berhev dike.',
|
||||
'research.about.en': "The Research Center curates academic papers on Kurdistan's digital economy, blockchain technology, and related topics.",
|
||||
'research.papers.title': 'Lêkolîn / Papers',
|
||||
'research.more': 'Bêtir / More',
|
||||
'research.less': 'Kêmtir / Less',
|
||||
'research.submit.title': 'Lêkolîna xwe bişînin / Submit your research',
|
||||
'research.submit.desc': 'Hûn dikarin gotarên xwe yên zanistî ji bo vekolînê bişînin. Submit your academic papers for peer review.',
|
||||
'research.submit.btn': 'Bişîne / Submit',
|
||||
}
|
||||
|
||||
@@ -0,0 +1,341 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { usePezkuwi } from '@/contexts/PezkuwiContext';
|
||||
|
||||
interface Course {
|
||||
id: number;
|
||||
owner: string;
|
||||
name: string;
|
||||
description: string;
|
||||
contentLink: string;
|
||||
status: 'Active' | 'Archived';
|
||||
}
|
||||
|
||||
interface Enrollment {
|
||||
courseId: number;
|
||||
enrolledAt: number;
|
||||
completedAt: number | null;
|
||||
pointsEarned: number;
|
||||
}
|
||||
|
||||
type Tab = 'courses' | 'enrolled' | 'completed';
|
||||
|
||||
interface CourseChainData {
|
||||
id: number; owner: string;
|
||||
name: number[] | string; description: number[] | string;
|
||||
contentLink: number[] | string;
|
||||
status: 'Active' | 'Archived';
|
||||
}
|
||||
interface EnrollmentChainData {
|
||||
student: string; courseId: number; enrolledAt: number;
|
||||
completedAt: number | null; pointsEarned: number;
|
||||
}
|
||||
|
||||
function decodeText(data: number[] | string): string {
|
||||
if (typeof data === 'string') return data;
|
||||
try { return new TextDecoder().decode(new Uint8Array(data)); } catch { return ''; }
|
||||
}
|
||||
|
||||
export default function CertificatesPage() {
|
||||
const navigate = useNavigate();
|
||||
const { t } = useTranslation();
|
||||
const { api, isApiReady, selectedAccount, getKeyPair } = usePezkuwi();
|
||||
|
||||
const [tab, setTab] = useState<Tab>('courses');
|
||||
const [courses, setCourses] = useState<Course[]>([]);
|
||||
const [enrollments, setEnrollments] = useState<Enrollment[]>([]);
|
||||
const [score, setScore] = useState(0);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [enrolling, setEnrolling] = useState<number | null>(null);
|
||||
const [expandedCourse, setExpandedCourse] = useState<Course | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const fetchCourses = useCallback(async () => {
|
||||
if (!api || !isApiReady) return;
|
||||
try {
|
||||
const entries = await api.query.perwerde.courses.entries();
|
||||
const list: Course[] = [];
|
||||
for (const [, value] of entries) {
|
||||
if (!value.isEmpty) {
|
||||
const d = value.toJSON() as unknown as CourseChainData;
|
||||
list.push({ id: d.id, owner: d.owner, name: decodeText(d.name), description: decodeText(d.description), contentLink: decodeText(d.contentLink), status: d.status });
|
||||
}
|
||||
}
|
||||
list.sort((a, b) => b.id - a.id);
|
||||
setCourses(list);
|
||||
} catch (e) { console.error('fetchCourses', e); }
|
||||
}, [api, isApiReady]);
|
||||
|
||||
const fetchEnrollments = useCallback(async () => {
|
||||
if (!api || !isApiReady || !selectedAccount) return;
|
||||
try {
|
||||
const ids = (await api.query.perwerde.studentCourses(selectedAccount.address)).toJSON() as number[];
|
||||
const list: Enrollment[] = [];
|
||||
let pts = 0;
|
||||
for (const cid of ids) {
|
||||
const e = await api.query.perwerde.enrollments([selectedAccount.address, cid]);
|
||||
if (!e.isEmpty) {
|
||||
const d = e.toJSON() as unknown as EnrollmentChainData;
|
||||
list.push({ courseId: d.courseId, enrolledAt: d.enrolledAt, completedAt: d.completedAt, pointsEarned: d.pointsEarned });
|
||||
if (d.completedAt) pts += d.pointsEarned;
|
||||
}
|
||||
}
|
||||
setEnrollments(list);
|
||||
setScore(pts);
|
||||
} catch (e) { console.error('fetchEnrollments', e); }
|
||||
}, [api, isApiReady, selectedAccount]);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true);
|
||||
await Promise.all([fetchCourses(), fetchEnrollments()]);
|
||||
setLoading(false);
|
||||
}, [fetchCourses, fetchEnrollments]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isApiReady) load();
|
||||
}, [isApiReady, selectedAccount, load]);
|
||||
|
||||
const isEnrolled = (id: number) => enrollments.some(e => e.courseId === id);
|
||||
const isCompleted = (id: number) => enrollments.find(e => e.courseId === id)?.completedAt != null;
|
||||
const getEnrollment = (id: number) => enrollments.find(e => e.courseId === id);
|
||||
|
||||
const handleEnroll = async (courseId: number) => {
|
||||
if (!api || !selectedAccount) return;
|
||||
setEnrolling(courseId);
|
||||
setError(null);
|
||||
try {
|
||||
const kp = await getKeyPair(selectedAccount.address);
|
||||
if (!kp) throw new Error('Could not get signer');
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
api.tx.perwerde.enroll(courseId).signAndSend(kp, ({ status, dispatchError }) => {
|
||||
if (dispatchError) {
|
||||
const msg = dispatchError.isModule
|
||||
? (() => { const d = api.registry.findMetaError(dispatchError.asModule); return `${d.section}.${d.name}`; })()
|
||||
: dispatchError.toString();
|
||||
reject(new Error(msg)); return;
|
||||
}
|
||||
if (status.isInBlock || status.isFinalized) resolve();
|
||||
});
|
||||
});
|
||||
setExpandedCourse(null);
|
||||
await load();
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : 'Unknown error');
|
||||
} finally {
|
||||
setEnrolling(null);
|
||||
}
|
||||
};
|
||||
|
||||
const filteredCourses = courses.filter(c => {
|
||||
if (tab === 'courses') return c.status === 'Active';
|
||||
if (tab === 'enrolled') return isEnrolled(c.id) && !isCompleted(c.id);
|
||||
return isCompleted(c.id);
|
||||
});
|
||||
|
||||
const enrolledCount = enrollments.filter(e => !e.completedAt).length;
|
||||
const completedCount = enrollments.filter(e => e.completedAt != null).length;
|
||||
|
||||
const TABS: { key: Tab; label: string; count: number }[] = [
|
||||
{ key: 'courses', label: t('certificates.tab.courses', 'Kurs'), count: courses.filter(c => c.status === 'Active').length },
|
||||
{ key: 'enrolled', label: t('certificates.tab.enrolled', 'Tev li'), count: enrolledCount },
|
||||
{ key: 'completed', label: t('certificates.tab.completed', 'Qediya'), count: completedCount },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-950 text-white flex flex-col">
|
||||
{/* Header */}
|
||||
<div className="bg-yellow-600 px-4 pt-4 pb-3">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<button onClick={() => navigate(-1)} className="text-white/80 hover:text-white text-xl leading-none">←</button>
|
||||
<span className="text-sm text-white/70">{t('certificates.breadcrumb', 'Education')}</span>
|
||||
</div>
|
||||
<div className="text-center mb-3">
|
||||
<span className="text-4xl block mb-1">🏆</span>
|
||||
<h1 className="text-xl font-bold">{t('certificates.title', 'Perwerde')}</h1>
|
||||
<p className="text-white/70 text-xs mt-0.5">{t('certificates.subtitle', 'Digital Education Platform')}</p>
|
||||
</div>
|
||||
{/* Score bar */}
|
||||
{selectedAccount && (
|
||||
<div className="flex justify-around bg-white/15 rounded-xl py-2.5 mx-2">
|
||||
<div className="text-center">
|
||||
<p className="text-lg font-bold text-yellow-300">{score}</p>
|
||||
<p className="text-[10px] text-white/70">{t('certificates.stats.points', 'Puan / Points')}</p>
|
||||
</div>
|
||||
<div className="w-px bg-white/30" />
|
||||
<div className="text-center">
|
||||
<p className="text-lg font-bold">{completedCount}</p>
|
||||
<p className="text-[10px] text-white/70">{t('certificates.stats.done', 'Qediyayî / Done')}</p>
|
||||
</div>
|
||||
<div className="w-px bg-white/30" />
|
||||
<div className="text-center">
|
||||
<p className="text-lg font-bold">{enrolledCount}</p>
|
||||
<p className="text-[10px] text-white/70">{t('certificates.stats.active', 'Aktîv / Active')}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* No wallet */}
|
||||
{!selectedAccount && (
|
||||
<div className="flex-1 flex flex-col items-center justify-center px-8 py-12 text-center">
|
||||
<span className="text-6xl mb-4">📚</span>
|
||||
<p className="font-bold text-lg mb-2">{t('certificates.noWallet.title', 'Perwerde')}</p>
|
||||
<p className="text-sm text-gray-400 mb-1">{t('certificates.noWallet.desc', 'Ji kerema xwe wallet ve girêbidin da ku bikaribin kursan bibînin û tev li wan bibin.')}</p>
|
||||
<p className="text-xs text-gray-500">{t('certificates.noWallet.en', 'Please connect your wallet to view and enroll in courses.')}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedAccount && (
|
||||
<>
|
||||
{/* Tabs */}
|
||||
<div className="flex gap-2 px-4 pt-3 pb-1">
|
||||
{TABS.map(tb => (
|
||||
<button
|
||||
key={tb.key}
|
||||
onClick={() => setTab(tb.key)}
|
||||
className={`flex-1 flex items-center justify-center gap-1.5 py-2 px-3 rounded-xl text-xs font-semibold transition-colors ${
|
||||
tab === tb.key ? 'bg-yellow-500 text-gray-900' : 'bg-gray-800 text-gray-400'
|
||||
}`}
|
||||
>
|
||||
{tb.label}
|
||||
{tb.count > 0 && (
|
||||
<span className={`rounded-full px-1.5 py-0.5 text-[10px] font-bold ${tab === tb.key ? 'bg-gray-900 text-yellow-400' : 'bg-gray-600 text-white'}`}>
|
||||
{tb.count}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Error */}
|
||||
{error && (
|
||||
<div className="mx-4 mt-2 bg-red-900/30 border border-red-700 rounded-xl p-3 text-xs text-red-300">
|
||||
⚠️ {error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Course list */}
|
||||
<div className="flex-1 overflow-y-auto px-4 py-3 space-y-3 max-w-lg mx-auto w-full">
|
||||
{loading && (
|
||||
<div className="text-center py-12 text-gray-500">
|
||||
<div className="text-3xl mb-3 animate-spin">⏳</div>
|
||||
<p className="text-sm">{t('certificates.loading', 'Tê barkirin... / Loading...')}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!loading && filteredCourses.length === 0 && (
|
||||
<div className="text-center py-12 text-gray-500">
|
||||
<p className="text-4xl mb-3">{tab === 'completed' ? '🎓' : tab === 'enrolled' ? '📋' : '📭'}</p>
|
||||
<p className="text-sm">
|
||||
{tab === 'courses' ? t('certificates.empty.courses', 'Kursek tune / No courses available')
|
||||
: tab === 'enrolled' ? t('certificates.empty.enrolled', 'Tu tev li kursekê nebûyî / Not enrolled in any course')
|
||||
: t('certificates.empty.completed', 'Kursek neqediyaye / No completed courses')}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!loading && filteredCourses.map(course => {
|
||||
const enrolled = isEnrolled(course.id);
|
||||
const completed = isCompleted(course.id);
|
||||
const enroll = getEnrollment(course.id);
|
||||
const isExpanded = expandedCourse?.id === course.id;
|
||||
|
||||
return (
|
||||
<div key={course.id} className="bg-gray-900 rounded-2xl p-4">
|
||||
<div
|
||||
className="flex items-center gap-3 cursor-pointer"
|
||||
onClick={() => setExpandedCourse(isExpanded ? null : course)}
|
||||
>
|
||||
<div className={`w-12 h-12 rounded-xl flex items-center justify-center text-2xl flex-shrink-0 ${
|
||||
completed ? 'bg-green-900/50' : enrolled ? 'bg-blue-900/50' : 'bg-gray-800'
|
||||
}`}>
|
||||
{completed ? '✅' : enrolled ? '📖' : '📚'}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="font-semibold text-sm text-white truncate">{course.name}</p>
|
||||
<p className="text-[11px] text-gray-500">Kurs #{course.id}</p>
|
||||
</div>
|
||||
{completed && enroll && (
|
||||
<span className="text-[11px] font-bold text-white bg-green-700 px-2 py-0.5 rounded-full flex-shrink-0">
|
||||
+{enroll.pointsEarned}
|
||||
</span>
|
||||
)}
|
||||
<span className="text-gray-600 text-sm">{isExpanded ? '▲' : '▼'}</span>
|
||||
</div>
|
||||
|
||||
{isExpanded && (
|
||||
<div className="mt-3 pt-3 border-t border-gray-700 space-y-3">
|
||||
{course.description && (
|
||||
<p className="text-xs text-gray-300 leading-relaxed">{course.description}</p>
|
||||
)}
|
||||
|
||||
<div className="space-y-1.5 text-xs">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-500">{t('certificates.course.status', 'Rewş / Status')}</span>
|
||||
<span className={course.status === 'Active' ? 'text-green-400' : 'text-gray-400'}>
|
||||
{course.status === 'Active' ? 'Aktîv' : 'Arşîv'}
|
||||
</span>
|
||||
</div>
|
||||
{enrolled && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-500">{t('certificates.course.enrollment', 'Têketin / Enrolled')}</span>
|
||||
<span className={completed ? 'text-green-400' : 'text-blue-400'}>
|
||||
{completed ? 'Qediya ✅' : 'Aktîv 📖'}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{completed && enroll && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-500">{t('certificates.course.points', 'Puan / Points')}</span>
|
||||
<span className="text-yellow-400 font-bold">+{enroll.pointsEarned}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{course.contentLink && (
|
||||
<a
|
||||
href={course.contentLink.startsWith('http') ? course.contentLink : `https://ipfs.io/ipfs/${course.contentLink}`}
|
||||
target="_blank" rel="noopener noreferrer"
|
||||
className="flex items-center justify-center gap-2 bg-blue-900/30 border border-blue-700/40 rounded-xl py-2.5 text-xs text-blue-300 font-semibold"
|
||||
>
|
||||
📄 {t('certificates.course.openContent', 'Naveroka Kursê Veke / Open Course Content')}
|
||||
</a>
|
||||
)}
|
||||
|
||||
{!enrolled && course.status === 'Active' && (
|
||||
<button
|
||||
className="w-full bg-yellow-600 hover:bg-yellow-500 text-white rounded-xl py-2.5 text-sm font-semibold flex items-center justify-center gap-2 transition-colors disabled:opacity-50"
|
||||
disabled={enrolling === course.id}
|
||||
onClick={() => handleEnroll(course.id)}
|
||||
>
|
||||
{enrolling === course.id ? (
|
||||
<span className="animate-spin">⏳</span>
|
||||
) : (
|
||||
<>📝 {t('certificates.enroll', 'Tev li Kursê / Enroll')}</>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
{completed && (
|
||||
<div className="flex items-center justify-center gap-2 bg-green-900/30 border border-green-700/40 rounded-xl py-2.5 text-xs text-green-300 font-semibold">
|
||||
🎓 {t('certificates.completedBanner', 'Te ev kurs qedand! / You completed this course!')}
|
||||
</div>
|
||||
)}
|
||||
{enrolled && !completed && (
|
||||
<div className="flex items-center justify-center gap-2 bg-blue-900/30 border border-blue-700/40 rounded-xl py-2.5 text-xs text-blue-300 font-semibold">
|
||||
📖 {t('certificates.enrolledBanner', 'Tu tev li vê kursê yî / You are enrolled')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="h-10" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
import { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface Paper {
|
||||
id: string;
|
||||
emoji: string;
|
||||
titleKu: string;
|
||||
title: string;
|
||||
authors: string;
|
||||
abstract: string;
|
||||
category: string;
|
||||
date: string;
|
||||
citations: number;
|
||||
status: 'published' | 'peer-review' | 'draft';
|
||||
}
|
||||
|
||||
const PAPERS: Paper[] = [
|
||||
{
|
||||
id: '1', emoji: '🔗',
|
||||
titleKu: 'Blockchain ji bo Aboriya Dijîtal a Kurdistanê',
|
||||
title: 'Blockchain for Kurdistan Digital Economy',
|
||||
authors: 'Dr. A. Kurdo, M. Ehmed',
|
||||
abstract: 'This paper explores the potential of blockchain technology in building a decentralized digital economy for Kurdistan. We propose the Pezkuwi consensus mechanism and analyze its throughput, security, and decentralization trade-offs.',
|
||||
category: 'Blockchain', date: '2026-02-15', citations: 42, status: 'published',
|
||||
},
|
||||
{
|
||||
id: '2', emoji: '💱',
|
||||
titleKu: 'Tokenomiya HEZ: Modela Aborî ya Nenavendî',
|
||||
title: 'HEZ Tokenomics: A Decentralized Economic Model',
|
||||
authors: 'Prof. R. Xan, S. Demirtash',
|
||||
abstract: 'An analysis of the HEZ token economic model including supply dynamics, staking incentives, governance utility, and long-term sustainability. We model inflation, deflation, and equilibrium scenarios.',
|
||||
category: 'Economics', date: '2026-01-20', citations: 31, status: 'published',
|
||||
},
|
||||
{
|
||||
id: '3', emoji: '🏘️',
|
||||
titleKu: 'Bereketli: Aboriya Taxê ya Dijîtal',
|
||||
title: 'Bereketli: Digital Neighborhood Economy',
|
||||
authors: 'K. Zana, B. Shêx',
|
||||
abstract: 'We present Bereketli, a peer-to-peer neighborhood economy platform built on blockchain. The system enables local trade, micro-lending, and community resource sharing with minimal trust assumptions.',
|
||||
category: 'DeFi / Social', date: '2025-11-30', citations: 18, status: 'published',
|
||||
},
|
||||
{
|
||||
id: '4', emoji: '🗣️',
|
||||
titleKu: 'NLP ji bo Zimanê Kurdî: Rewş û Derfet',
|
||||
title: 'NLP for Kurdish Language: Status and Opportunities',
|
||||
authors: 'Dr. J. Bakir, D. Ehmed',
|
||||
abstract: 'A comprehensive survey of Natural Language Processing research for the Kurdish language. We identify key gaps in tokenization, machine translation, and sentiment analysis, and propose a community-driven dataset initiative.',
|
||||
category: 'AI / Language', date: '2026-03-05', citations: 8, status: 'peer-review',
|
||||
},
|
||||
{
|
||||
id: '5', emoji: '🔐',
|
||||
titleKu: 'Nasnameya Dijîtal a Nenavendî li ser Pezkuwi',
|
||||
title: 'Decentralized Digital Identity on Pezkuwi',
|
||||
authors: 'M. Baran, A. Kurdo',
|
||||
abstract: 'We design a self-sovereign identity (SSI) framework for the Pezkuwi network. Citizens control their own credentials using zero-knowledge proofs while maintaining compliance with governance requirements.',
|
||||
category: 'Identity / Privacy', date: '2026-03-20', citations: 3, status: 'peer-review',
|
||||
},
|
||||
];
|
||||
|
||||
const STATUS_META = {
|
||||
'published': { label: 'Weşandî / Published', color: '#16a34a', bg: '#16a34a22' },
|
||||
'peer-review': { label: 'Peer Review', color: '#b45309', bg: '#f59e0b33' },
|
||||
'draft': { label: 'Draft', color: '#6b7280', bg: '#6b728022' },
|
||||
};
|
||||
|
||||
export default function ResearchPage() {
|
||||
const navigate = useNavigate();
|
||||
const { t } = useTranslation();
|
||||
const [expandedId, setExpandedId] = useState<string | null>(null);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-950 text-white">
|
||||
{/* Header */}
|
||||
<div className="bg-green-700 px-4 pt-4 pb-5">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<button onClick={() => navigate(-1)} className="text-white/80 hover:text-white text-xl leading-none">←</button>
|
||||
<span className="text-sm text-white/70">{t('research.breadcrumb', 'Education')}</span>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<span className="text-5xl block mb-2">🔬</span>
|
||||
<h1 className="text-2xl font-bold">{t('research.title', 'Navenda Lêkolînê')}</h1>
|
||||
<p className="text-white/70 text-sm mt-0.5">{t('research.subtitle', 'Research Center')}</p>
|
||||
<p className="text-yellow-400 text-xs mt-2 font-semibold">
|
||||
{t('research.paperCount', '{{count}} lêkolîn / papers', { count: PAPERS.length })}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="px-4 py-4 space-y-3 max-w-lg mx-auto">
|
||||
|
||||
{/* About card */}
|
||||
<div className="bg-gray-900 rounded-2xl p-4">
|
||||
<p className="text-sm font-bold text-white mb-2">{t('research.about.title', 'Der barê Navenda Lêkolînê / About')}</p>
|
||||
<p className="text-xs text-gray-300 leading-relaxed mb-1">
|
||||
{t('research.about.ku', 'Navenda Lêkolînê gotarên zanistî yên li ser aboriya dijîtal a Kurdistanê, teknolojiya blockchain, û mijarên pêwendîdar berhev dike.')}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 leading-relaxed">
|
||||
{t('research.about.en', 'The Research Center curates academic papers on Kurdistan\'s digital economy, blockchain technology, and related topics.')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Papers */}
|
||||
<p className="text-sm font-bold text-white px-1">{t('research.papers.title', 'Lêkolîn / Papers')}</p>
|
||||
|
||||
{PAPERS.map(paper => {
|
||||
const meta = STATUS_META[paper.status];
|
||||
const isOpen = expandedId === paper.id;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={paper.id}
|
||||
className="w-full text-left bg-gray-900 rounded-2xl p-4"
|
||||
onClick={() => setExpandedId(isOpen ? null : paper.id)}
|
||||
>
|
||||
{/* Header row */}
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-2xl">{paper.emoji}</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className="text-[10px] font-semibold px-2 py-0.5 rounded-full"
|
||||
style={{ color: meta.color, backgroundColor: meta.bg }}
|
||||
>
|
||||
{meta.label}
|
||||
</span>
|
||||
<span className="text-[11px] text-gray-500">📖 {paper.citations}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="font-bold text-white text-sm leading-snug">{paper.titleKu}</p>
|
||||
<p className="text-xs text-gray-400 mt-0.5 mb-1">{paper.title}</p>
|
||||
<p className="text-xs text-blue-400 mb-2">{paper.authors}</p>
|
||||
|
||||
<div className="flex gap-4 mb-2">
|
||||
<span className="text-[11px] text-gray-500">📁 {paper.category}</span>
|
||||
<span className="text-[11px] text-gray-500">📅 {paper.date}</span>
|
||||
</div>
|
||||
|
||||
{isOpen && (
|
||||
<div className="mt-3 pt-3 border-t border-gray-700">
|
||||
<p className="text-xs font-semibold text-green-400 mb-1">Abstract</p>
|
||||
<p className="text-xs text-gray-300 leading-relaxed">{paper.abstract}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<p className="text-xs text-green-500 text-center mt-2">
|
||||
{isOpen ? '▲ ' + t('research.less', 'Kêmtir / Less') : '▼ ' + t('research.more', 'Bêtir / More')}
|
||||
</p>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Submit CTA */}
|
||||
<div className="bg-green-900/20 border border-green-700/40 rounded-2xl p-4 text-center">
|
||||
<p className="font-bold text-green-300 text-sm mb-1">
|
||||
{t('research.submit.title', 'Lêkolîna xwe bişînin / Submit your research')}
|
||||
</p>
|
||||
<p className="text-xs text-gray-400 leading-relaxed mb-3">
|
||||
{t('research.submit.desc', 'Hûn dikarin gotarên xwe yên zanistî ji bo vekolînê bişînin. Submit your academic papers for peer review.')}
|
||||
</p>
|
||||
<button className="bg-green-700 hover:bg-green-600 text-white text-sm font-semibold px-5 py-2 rounded-xl transition-colors">
|
||||
📝 {t('research.submit.btn', 'Bişîne / Submit')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div className="h-10" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface Course {
|
||||
id: string;
|
||||
emoji: string;
|
||||
titleKu: string;
|
||||
title: string;
|
||||
instructor: string;
|
||||
duration: string;
|
||||
level: 'Destpêk' | 'Navîn' | 'Pêşketî';
|
||||
levelEn: 'Beginner' | 'Intermediate' | 'Advanced';
|
||||
enrolled: number;
|
||||
lessons: number;
|
||||
description: string;
|
||||
reward: string;
|
||||
}
|
||||
|
||||
const COURSES: Course[] = [
|
||||
{
|
||||
id: '1', emoji: '🗣️',
|
||||
titleKu: 'Zimanê Kurdî - Kurmancî', title: 'Kurdish Language - Kurmanji',
|
||||
instructor: 'Prof. Berfîn Shêx', duration: '12 hefte / weeks',
|
||||
level: 'Destpêk', levelEn: 'Beginner', enrolled: 456, lessons: 48,
|
||||
description: 'Learn Kurmanji Kurdish from scratch. Covers alphabet, grammar, conversation, reading, and writing.',
|
||||
reward: 'NFT Certificate + 50 HEZ',
|
||||
},
|
||||
{
|
||||
id: '2', emoji: '🔗',
|
||||
titleKu: 'Blockchain 101', title: 'Blockchain 101',
|
||||
instructor: 'Dr. Azad Kurdo', duration: '8 hefte / weeks',
|
||||
level: 'Destpêk', levelEn: 'Beginner', enrolled: 312, lessons: 32,
|
||||
description: 'Introduction to blockchain technology. Learn about consensus, cryptography, smart contracts, and decentralized applications.',
|
||||
reward: 'NFT Certificate + 30 HEZ',
|
||||
},
|
||||
{
|
||||
id: '3', emoji: '💻',
|
||||
titleKu: 'Pêşvebirina Smart Contract', title: 'Smart Contract Development',
|
||||
instructor: 'Jîn Bakir', duration: '10 hefte / weeks',
|
||||
level: 'Navîn', levelEn: 'Intermediate', enrolled: 187, lessons: 40,
|
||||
description: 'Build smart contracts for the Pezkuwi network. Covers Solidity/Ink!, testing, deployment, and security best practices.',
|
||||
reward: 'NFT Certificate + 100 HEZ',
|
||||
},
|
||||
{
|
||||
id: '4', emoji: '📊',
|
||||
titleKu: 'Aboriya Dijîtal û DeFi', title: 'Digital Economics & DeFi',
|
||||
instructor: 'Prof. Serhat Demirtash', duration: '6 hefte / weeks',
|
||||
level: 'Navîn', levelEn: 'Intermediate', enrolled: 234, lessons: 24,
|
||||
description: 'Understand decentralized finance, tokenomics, yield farming, liquidity pools, and the Bereketli neighborhood economy model.',
|
||||
reward: 'NFT Certificate + 40 HEZ',
|
||||
},
|
||||
{
|
||||
id: '5', emoji: '🛡️',
|
||||
titleKu: 'Ewlehiya Sîber û Blockchain', title: 'Cyber Security & Blockchain',
|
||||
instructor: 'Kawa Zana', duration: '8 hefte / weeks',
|
||||
level: 'Pêşketî', levelEn: 'Advanced', enrolled: 98, lessons: 32,
|
||||
description: 'Advanced security topics including audit techniques, common vulnerabilities, formal verification, and incident response.',
|
||||
reward: 'NFT Certificate + 150 HEZ',
|
||||
},
|
||||
{
|
||||
id: '6', emoji: '🏛️',
|
||||
titleKu: 'Dîroka Kurdistanê - Dijîtal', title: 'History of Kurdistan - Digital',
|
||||
instructor: 'Prof. Dilovan Ehmed', duration: '10 hefte / weeks',
|
||||
level: 'Destpêk', levelEn: 'Beginner', enrolled: 567, lessons: 40,
|
||||
description: 'A comprehensive digital course on Kurdish history, culture, and the journey toward digital sovereignty and self-governance.',
|
||||
reward: 'NFT Certificate + 25 HEZ',
|
||||
},
|
||||
];
|
||||
|
||||
const LEVEL_COLORS: Record<Course['level'], string> = {
|
||||
'Destpêk': '#16a34a',
|
||||
'Navîn': '#2563eb',
|
||||
'Pêşketî': '#7c3aed',
|
||||
};
|
||||
|
||||
export default function UniversityPage() {
|
||||
const navigate = useNavigate();
|
||||
const { t } = useTranslation();
|
||||
const totalStudents = COURSES.reduce((s, c) => s + c.enrolled, 0);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-950 text-white">
|
||||
{/* Header */}
|
||||
<div className="bg-green-700 px-4 pt-4 pb-5">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<button onClick={() => navigate(-1)} className="text-white/80 hover:text-white text-xl leading-none">←</button>
|
||||
<span className="text-sm text-white/70">{t('university.breadcrumb', 'Education')}</span>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<span className="text-5xl block mb-2">🎓</span>
|
||||
<h1 className="text-2xl font-bold">{t('university.title', 'Zanîngeha Dijîtal')}</h1>
|
||||
<p className="text-white/70 text-sm mt-0.5">{t('university.subtitle', 'Kurdistan Digital University')}</p>
|
||||
</div>
|
||||
<div className="flex justify-center gap-8 mt-4 bg-white/15 rounded-xl py-3 mx-4">
|
||||
<div className="text-center">
|
||||
<p className="text-xl font-bold">{COURSES.length}</p>
|
||||
<p className="text-[10px] text-white/70">{t('university.stats.courses', 'Kurs / Courses')}</p>
|
||||
</div>
|
||||
<div className="w-px bg-white/30" />
|
||||
<div className="text-center">
|
||||
<p className="text-xl font-bold">{totalStudents.toLocaleString()}</p>
|
||||
<p className="text-[10px] text-white/70">{t('university.stats.students', 'Xwendekar / Students')}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="px-4 py-4 space-y-3 max-w-lg mx-auto">
|
||||
{COURSES.map(course => {
|
||||
const color = LEVEL_COLORS[course.level];
|
||||
return (
|
||||
<div key={course.id} className="bg-gray-900 rounded-2xl p-4">
|
||||
{/* Top row */}
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<span className="text-3xl">{course.emoji}</span>
|
||||
<span
|
||||
className="text-[11px] font-semibold px-2.5 py-1 rounded-full"
|
||||
style={{ color, backgroundColor: color + '22' }}
|
||||
>
|
||||
{course.level} / {course.levelEn}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p className="font-bold text-white text-sm">{course.titleKu}</p>
|
||||
<p className="text-xs text-gray-400 mb-1">{course.title}</p>
|
||||
<p className="text-xs text-blue-400 mb-3">👨🏫 {course.instructor}</p>
|
||||
<p className="text-xs text-gray-300 leading-relaxed mb-3">{course.description}</p>
|
||||
|
||||
{/* Meta */}
|
||||
<div className="flex justify-around bg-gray-800 rounded-xl py-2.5 mb-3">
|
||||
<div className="text-center">
|
||||
<p className="text-base">📚</p>
|
||||
<p className="text-[11px] text-gray-400">{course.lessons} ders</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<p className="text-base">⏱️</p>
|
||||
<p className="text-[11px] text-gray-400">{course.duration}</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<p className="text-base">👥</p>
|
||||
<p className="text-[11px] text-gray-400">{course.enrolled}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Reward */}
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<span className="text-xs text-gray-400">{t('university.reward', '🎁 Xelat / Reward:')}</span>
|
||||
<span className="text-xs font-semibold text-yellow-400">{course.reward}</span>
|
||||
</div>
|
||||
|
||||
{/* Enroll button */}
|
||||
<button
|
||||
className="w-full py-2.5 rounded-xl text-sm font-semibold text-white transition-opacity active:opacity-80"
|
||||
style={{ backgroundColor: color }}
|
||||
onClick={() => navigate('/education/certificates')}
|
||||
>
|
||||
{t('university.enroll', 'Tomar bibe / Enroll')}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="h-10" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user