🎉 Complete AI-Enhanced University Portal - Ready for Production
✨ Major Features Added: - AI Chat with conversation memory and university-specific knowledge base - Multi-tenant university support with white-label capabilities - Professional admin interface for knowledge base management - Advanced database schema with Prisma ORM - Comprehensive documentation and guides - Modern Next.js 15 + React 19 architecture - Bilingual support (English/Arabic) - Role-based access control - Real-time chat interface with loading states 🔧 Technical Improvements: - Fixed all linter errors and TypeScript issues - Cleaned up codebase and removed legacy files - Added comprehensive .gitignore - Updated README with detailed setup instructions - Optimized database schema and migrations - Enhanced error handling and user experience 📚 Documentation: - AI Conversation Memory Guide - AI Enhancement Summary - Developer Guide - User Guide - Complete setup and deployment instructions 🚀 Ready for GitHub deployment and production use!
This commit is contained in:
@@ -1,96 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
|
||||
export default function AIConfigPage() {
|
||||
const [apiKey, setApiKey] = useState('');
|
||||
const [ollamaUrl, setOllamaUrl] = useState('http://localhost:11434');
|
||||
const [modelName, setModelName] = useState('command-r7b-arabic');
|
||||
const [saveStatus, setSaveStatus] = useState('');
|
||||
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
setSaveStatus('Saving...');
|
||||
// In a real implementation, we would update the configuration
|
||||
// securely through a protected API endpoint
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
setSaveStatus('Configuration saved successfully!');
|
||||
} catch (error) {
|
||||
console.error('Error saving configuration:', error);
|
||||
setSaveStatus('Error saving configuration');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container mx-auto px-6 py-8">
|
||||
<h1 className="text-3xl font-bold mb-8">AI Assistant Configuration</h1>
|
||||
|
||||
<div className="bg-white p-6 rounded-lg shadow-md mb-6">
|
||||
<h2 className="text-xl font-semibold mb-4">OpenRouter Configuration</h2>
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
API Key
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
value={apiKey}
|
||||
onChange={(e) => setApiKey(e.target.value)}
|
||||
className="w-full p-2 border border-gray-300 rounded focus:ring-blue-500 focus:border-blue-500"
|
||||
placeholder="sk-..."
|
||||
/>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
Your OpenRouter API key is stored securely and never exposed to clients.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white p-6 rounded-lg shadow-md mb-6">
|
||||
<h2 className="text-xl font-semibold mb-4">Ollama Configuration (Fallback)</h2>
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Ollama Server URL
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={ollamaUrl}
|
||||
onChange={(e) => setOllamaUrl(e.target.value)}
|
||||
className="w-full p-2 border border-gray-300 rounded focus:ring-blue-500 focus:border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Model Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={modelName}
|
||||
onChange={(e) => setModelName(e.target.value)}
|
||||
className="w-full p-2 border border-gray-300 rounded focus:ring-blue-500 focus:border-blue-500"
|
||||
/>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
The model must be installed on your Ollama server
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
onClick={handleSave}
|
||||
className="bg-blue-600 text-white px-6 py-2 rounded-lg hover:bg-blue-700 transition-colors"
|
||||
>
|
||||
Save Configuration
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{saveStatus && (
|
||||
<div className={`mt-4 p-3 rounded ${
|
||||
saveStatus.includes('Error')
|
||||
? 'bg-red-100 text-red-800'
|
||||
: 'bg-green-100 text-green-800'
|
||||
}`}>
|
||||
{saveStatus}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,298 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useUniversity } from '@/components/providers/UniversityProvider';
|
||||
import { BranchType } from '@/components/providers/UniversityProvider';
|
||||
|
||||
interface Branch {
|
||||
id: string;
|
||||
name: string;
|
||||
shortName: string | null;
|
||||
slug: string;
|
||||
domain: string | null;
|
||||
subdomain: string | null;
|
||||
branchType: BranchType;
|
||||
status: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export default function BranchesPage() {
|
||||
const { university } = useUniversity();
|
||||
const [branches, setBranches] = useState<Branch[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [showCreateForm, setShowCreateForm] = useState(false);
|
||||
const [formData, setFormData] = useState({
|
||||
name: '',
|
||||
shortName: '',
|
||||
branchSlug: '',
|
||||
branchType: BranchType.CAMPUS,
|
||||
domain: '',
|
||||
subdomain: '',
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (university?.isMultiBranch) {
|
||||
loadBranches();
|
||||
}
|
||||
}, [university]);
|
||||
|
||||
const loadBranches = async () => {
|
||||
try {
|
||||
const response = await fetch(`/api/universities/${university?.slug}/branches`);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setBranches(data.data || []);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load branches:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateBranch = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/universities/${university?.slug}/branches`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(formData),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
setShowCreateForm(false);
|
||||
setFormData({
|
||||
name: '',
|
||||
shortName: '',
|
||||
branchSlug: '',
|
||||
branchType: BranchType.CAMPUS,
|
||||
domain: '',
|
||||
subdomain: '',
|
||||
});
|
||||
loadBranches();
|
||||
} else {
|
||||
const error = await response.json();
|
||||
alert(error.error || 'Failed to create branch');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error creating branch:', error);
|
||||
alert('Failed to create branch');
|
||||
}
|
||||
};
|
||||
|
||||
if (!university?.isMultiBranch) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 py-12">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="text-center">
|
||||
<h1 className="text-3xl font-bold text-gray-900 mb-4">Branch Management</h1>
|
||||
<p className="text-gray-600">
|
||||
This university is not configured for multi-branch management.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 py-12">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="mb-8">
|
||||
<h1 className="text-3xl font-bold text-gray-900 mb-2">Branch Management</h1>
|
||||
<p className="text-gray-600">
|
||||
Manage branches and campuses for {university.name}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-lg shadow">
|
||||
<div className="px-6 py-4 border-b border-gray-200">
|
||||
<div className="flex justify-between items-center">
|
||||
<h2 className="text-xl font-semibold text-gray-900">Branches</h2>
|
||||
<button
|
||||
onClick={() => setShowCreateForm(true)}
|
||||
className="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition-colors"
|
||||
>
|
||||
Add Branch
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="p-6 text-center">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto"></div>
|
||||
<p className="mt-2 text-gray-600">Loading branches...</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Branch
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Type
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Domain
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Status
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Created
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
{branches.map((branch) => (
|
||||
<tr key={branch.id} className="hover:bg-gray-50">
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<div>
|
||||
<div className="text-sm font-medium text-gray-900">{branch.name}</div>
|
||||
{branch.shortName && (
|
||||
<div className="text-sm text-gray-500">{branch.shortName}</div>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<span className="inline-flex px-2 py-1 text-xs font-semibold rounded-full bg-blue-100 text-blue-800">
|
||||
{branch.branchType}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||
{branch.domain || branch.subdomain || 'Not configured'}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<span className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${
|
||||
branch.status === 'ACTIVE'
|
||||
? 'bg-green-100 text-green-800'
|
||||
: 'bg-red-100 text-red-800'
|
||||
}`}>
|
||||
{branch.status}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{new Date(branch.createdAt).toLocaleDateString()}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
||||
<button className="text-blue-600 hover:text-blue-900 mr-3">
|
||||
Edit
|
||||
</button>
|
||||
<button className="text-red-600 hover:text-red-900">
|
||||
Delete
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Create Branch Modal */}
|
||||
{showCreateForm && (
|
||||
<div className="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50">
|
||||
<div className="relative top-20 mx-auto p-5 border w-96 shadow-lg rounded-md bg-white">
|
||||
<div className="mt-3">
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-4">Create New Branch</h3>
|
||||
<form onSubmit={handleCreateBranch}>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700">Name</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={formData.name}
|
||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||
className="mt-1 block w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700">Short Name</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.shortName}
|
||||
onChange={(e) => setFormData({ ...formData, shortName: e.target.value })}
|
||||
className="mt-1 block w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700">Branch Slug</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={formData.branchSlug}
|
||||
onChange={(e) => setFormData({ ...formData, branchSlug: e.target.value })}
|
||||
className="mt-1 block w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700">Branch Type</label>
|
||||
<select
|
||||
value={formData.branchType}
|
||||
onChange={(e) => setFormData({ ...formData, branchType: e.target.value as BranchType })}
|
||||
className="mt-1 block w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
||||
>
|
||||
{Object.values(BranchType).map((type) => (
|
||||
<option key={type} value={type}>{type}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700">Domain</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.domain}
|
||||
onChange={(e) => setFormData({ ...formData, domain: e.target.value })}
|
||||
className="mt-1 block w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700">Subdomain</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.subdomain}
|
||||
onChange={(e) => setFormData({ ...formData, subdomain: e.target.value })}
|
||||
className="mt-1 block w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end space-x-3 mt-6">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowCreateForm(false)}
|
||||
className="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 border border-gray-300 rounded-md hover:bg-gray-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="px-4 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||
>
|
||||
Create Branch
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,328 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import Link from 'next/link';
|
||||
|
||||
interface Content {
|
||||
id: string;
|
||||
contentType: string;
|
||||
title: string;
|
||||
titleAr?: string;
|
||||
content?: string;
|
||||
contentAr?: string;
|
||||
isPublished: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export default function ContentPage() {
|
||||
const [content, setContent] = useState<Content[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [showAddForm, setShowAddForm] = useState(false);
|
||||
const [formData, setFormData] = useState({
|
||||
contentType: 'ABOUT',
|
||||
title: '',
|
||||
titleAr: '',
|
||||
content: '',
|
||||
contentAr: '',
|
||||
isPublished: false,
|
||||
});
|
||||
|
||||
const loadContent = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/content');
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setContent(data.data || []);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading content:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddContent = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
const response = await fetch('/api/content', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(formData),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
setShowAddForm(false);
|
||||
setFormData({
|
||||
contentType: 'ABOUT',
|
||||
title: '',
|
||||
titleAr: '',
|
||||
content: '',
|
||||
contentAr: '',
|
||||
isPublished: false,
|
||||
});
|
||||
loadContent();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error adding content:', error);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadContent();
|
||||
}, []);
|
||||
|
||||
const getContentTypeColor = (type: string) => {
|
||||
switch (type) {
|
||||
case 'ABOUT':
|
||||
return 'bg-blue-100 text-blue-800';
|
||||
case 'PROGRAMS':
|
||||
return 'bg-green-100 text-green-800';
|
||||
case 'ADMISSIONS':
|
||||
return 'bg-purple-100 text-purple-800';
|
||||
case 'RESEARCH':
|
||||
return 'bg-orange-100 text-orange-800';
|
||||
default:
|
||||
return 'bg-gray-100 text-gray-800';
|
||||
}
|
||||
};
|
||||
|
||||
const getContentTypeLabel = (type: string) => {
|
||||
switch (type) {
|
||||
case 'ABOUT':
|
||||
return 'About';
|
||||
case 'PROGRAMS':
|
||||
return 'Programs';
|
||||
case 'ADMISSIONS':
|
||||
return 'Admissions';
|
||||
case 'RESEARCH':
|
||||
return 'Research';
|
||||
default:
|
||||
return type;
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 p-6">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div className="animate-pulse">
|
||||
<div className="h-8 bg-gray-200 rounded w-1/4 mb-6"></div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{[1, 2, 3].map((i) => (
|
||||
<div key={i} className="bg-white rounded-lg shadow p-6">
|
||||
<div className="h-4 bg-gray-200 rounded w-3/4 mb-4"></div>
|
||||
<div className="h-3 bg-gray-200 rounded w-1/2 mb-2"></div>
|
||||
<div className="h-3 bg-gray-200 rounded w-2/3"></div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<div className="max-w-7xl mx-auto px-4 py-8">
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900">Content Management</h1>
|
||||
<p className="text-gray-600 mt-2">Manage university content and pages</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setShowAddForm(true)}
|
||||
className="bg-blue-600 hover:bg-blue-700 text-white px-6 py-3 rounded-lg font-semibold transition-colors"
|
||||
>
|
||||
Add Content
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Add Content Form */}
|
||||
{showAddForm && (
|
||||
<div className="bg-white rounded-lg shadow-md p-6 mb-8">
|
||||
<h2 className="text-xl font-semibold text-gray-900 mb-4">Add New Content</h2>
|
||||
<form onSubmit={handleAddContent} className="space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Content Type
|
||||
</label>
|
||||
<select
|
||||
value={formData.contentType}
|
||||
onChange={(e) => setFormData({ ...formData, contentType: e.target.value })}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
<option value="ABOUT">About</option>
|
||||
<option value="PROGRAMS">Programs</option>
|
||||
<option value="ADMISSIONS">Admissions</option>
|
||||
<option value="RESEARCH">Research</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Published
|
||||
</label>
|
||||
<div className="flex items-center mt-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.isPublished}
|
||||
onChange={(e) => setFormData({ ...formData, isPublished: e.target.checked })}
|
||||
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
|
||||
/>
|
||||
<label className="ml-2 text-sm text-gray-700">Publish immediately</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Title (English)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.title}
|
||||
onChange={(e) => setFormData({ ...formData, title: e.target.value })}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Title (Arabic)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.titleAr}
|
||||
onChange={(e) => setFormData({ ...formData, titleAr: e.target.value })}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
dir="rtl"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Content (English)
|
||||
</label>
|
||||
<textarea
|
||||
value={formData.content}
|
||||
onChange={(e) => setFormData({ ...formData, content: e.target.value })}
|
||||
rows={4}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="Enter content in English..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Content (Arabic)
|
||||
</label>
|
||||
<textarea
|
||||
value={formData.contentAr}
|
||||
onChange={(e) => setFormData({ ...formData, contentAr: e.target.value })}
|
||||
rows={4}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="أدخل المحتوى باللغة العربية..."
|
||||
dir="rtl"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
type="submit"
|
||||
className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md font-medium"
|
||||
>
|
||||
Create Content
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowAddForm(false)}
|
||||
className="bg-gray-300 hover:bg-gray-400 text-gray-700 px-4 py-2 rounded-md font-medium"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Content Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{content.map((item) => (
|
||||
<div key={item.id} className="bg-white rounded-lg shadow-md p-6 hover:shadow-lg transition-shadow">
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-gray-900">{item.title}</h3>
|
||||
{item.titleAr && (
|
||||
<p className="text-sm text-gray-600 mt-1" dir="rtl">{item.titleAr}</p>
|
||||
)}
|
||||
</div>
|
||||
<span className={`px-2 py-1 rounded-full text-xs font-medium ${getContentTypeColor(item.contentType)}`}>
|
||||
{getContentTypeLabel(item.contentType)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 mb-4">
|
||||
<div className="flex items-center text-sm text-gray-600">
|
||||
<span className="font-medium w-20">Status:</span>
|
||||
<span className={`px-2 py-1 rounded-full text-xs font-medium ${
|
||||
item.isPublished ? 'bg-green-100 text-green-800' : 'bg-yellow-100 text-yellow-800'
|
||||
}`}>
|
||||
{item.isPublished ? 'Published' : 'Draft'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-sm text-gray-600">
|
||||
<span className="font-medium">Created:</span> {new Date(item.createdAt).toLocaleDateString()}
|
||||
</div>
|
||||
{item.content && (
|
||||
<div className="text-sm text-gray-600">
|
||||
<span className="font-medium">Preview:</span> {item.content.substring(0, 100)}...
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Link
|
||||
href={`/admin/content/${item.id}`}
|
||||
className="flex-1 bg-blue-50 hover:bg-blue-100 text-blue-700 px-3 py-2 rounded-md text-sm font-medium text-center transition-colors"
|
||||
>
|
||||
Edit
|
||||
</Link>
|
||||
<button
|
||||
className="flex-1 bg-gray-50 hover:bg-gray-100 text-gray-700 px-3 py-2 rounded-md text-sm font-medium transition-colors"
|
||||
>
|
||||
Preview
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{content.length === 0 && (
|
||||
<div className="text-center py-12">
|
||||
<div className="text-gray-400 mb-4">
|
||||
<svg className="mx-auto h-12 w-12" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">No content found</h3>
|
||||
<p className="text-gray-600 mb-4">Get started by creating your first content piece.</p>
|
||||
<button
|
||||
onClick={() => setShowAddForm(true)}
|
||||
className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md font-medium"
|
||||
>
|
||||
Add Content
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,295 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import Link from 'next/link';
|
||||
|
||||
interface Domain {
|
||||
id: string;
|
||||
type: 'SUBDOMAIN' | 'CUSTOM_DOMAIN';
|
||||
domain: string;
|
||||
subdomain?: string;
|
||||
sslStatus: 'PENDING' | 'ACTIVE' | 'EXPIRED' | 'ERROR';
|
||||
sslExpiryDate?: string;
|
||||
dnsStatus: 'PENDING' | 'VERIFIED' | 'ERROR';
|
||||
isActive: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export default function DomainsPage() {
|
||||
const [domains, setDomains] = useState<Domain[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [showAddForm, setShowAddForm] = useState(false);
|
||||
const [formData, setFormData] = useState({
|
||||
type: 'SUBDOMAIN' as 'SUBDOMAIN' | 'CUSTOM_DOMAIN',
|
||||
domain: '',
|
||||
subdomain: '',
|
||||
});
|
||||
|
||||
const loadDomains = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/domains');
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setDomains(data.data || []);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading domains:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddDomain = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
const response = await fetch('/api/domains', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(formData),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
setShowAddForm(false);
|
||||
setFormData({ type: 'SUBDOMAIN', domain: '', subdomain: '' });
|
||||
loadDomains();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error adding domain:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleValidateDomain = async (domainId: string) => {
|
||||
try {
|
||||
const response = await fetch(`/api/domains/${domainId}/validate`, {
|
||||
method: 'POST',
|
||||
});
|
||||
if (response.ok) {
|
||||
loadDomains(); // Refresh the list
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error validating domain:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRenewSSL = async (domainId: string) => {
|
||||
try {
|
||||
const response = await fetch(`/api/domains/${domainId}/renew-ssl`, {
|
||||
method: 'POST',
|
||||
});
|
||||
if (response.ok) {
|
||||
loadDomains(); // Refresh the list
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error renewing SSL:', error);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadDomains();
|
||||
}, []);
|
||||
|
||||
const getSSLStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'ACTIVE':
|
||||
return 'bg-green-100 text-green-800';
|
||||
case 'PENDING':
|
||||
return 'bg-yellow-100 text-yellow-800';
|
||||
case 'EXPIRED':
|
||||
return 'bg-red-100 text-red-800';
|
||||
case 'ERROR':
|
||||
return 'bg-red-100 text-red-800';
|
||||
default:
|
||||
return 'bg-gray-100 text-gray-800';
|
||||
}
|
||||
};
|
||||
|
||||
const getDNSStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'VERIFIED':
|
||||
return 'bg-green-100 text-green-800';
|
||||
case 'PENDING':
|
||||
return 'bg-yellow-100 text-yellow-800';
|
||||
case 'ERROR':
|
||||
return 'bg-red-100 text-red-800';
|
||||
default:
|
||||
return 'bg-gray-100 text-gray-800';
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 p-6">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div className="animate-pulse">
|
||||
<div className="h-8 bg-gray-200 rounded w-1/4 mb-6"></div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{[1, 2, 3].map((i) => (
|
||||
<div key={i} className="bg-white rounded-lg shadow p-6">
|
||||
<div className="h-4 bg-gray-200 rounded w-3/4 mb-4"></div>
|
||||
<div className="h-3 bg-gray-200 rounded w-1/2 mb-2"></div>
|
||||
<div className="h-3 bg-gray-200 rounded w-2/3"></div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<div className="max-w-7xl mx-auto px-4 py-8">
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900">Domain Management</h1>
|
||||
<p className="text-gray-600 mt-2">Manage domain configurations and SSL certificates</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setShowAddForm(true)}
|
||||
className="bg-blue-600 hover:bg-blue-700 text-white px-6 py-3 rounded-lg font-semibold transition-colors"
|
||||
>
|
||||
Add Domain
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Add Domain Form */}
|
||||
{showAddForm && (
|
||||
<div className="bg-white rounded-lg shadow-md p-6 mb-8">
|
||||
<h2 className="text-xl font-semibold text-gray-900 mb-4">Add New Domain</h2>
|
||||
<form onSubmit={handleAddDomain} className="space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Domain Type
|
||||
</label>
|
||||
<select
|
||||
value={formData.type}
|
||||
onChange={(e) => setFormData({ ...formData, type: e.target.value as 'SUBDOMAIN' | 'CUSTOM_DOMAIN' })}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
<option value="SUBDOMAIN">Subdomain</option>
|
||||
<option value="CUSTOM_DOMAIN">Custom Domain</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
{formData.type === 'SUBDOMAIN' ? 'Subdomain' : 'Domain'}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.type === 'SUBDOMAIN' ? formData.subdomain : formData.domain}
|
||||
onChange={(e) => setFormData({
|
||||
...formData,
|
||||
[formData.type === 'SUBDOMAIN' ? 'subdomain' : 'domain']: e.target.value
|
||||
})}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder={formData.type === 'SUBDOMAIN' ? 'university' : 'university.edu'}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
type="submit"
|
||||
className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md font-medium"
|
||||
>
|
||||
Add Domain
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowAddForm(false)}
|
||||
className="bg-gray-300 hover:bg-gray-400 text-gray-700 px-4 py-2 rounded-md font-medium"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Domains Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{domains.map((domain) => (
|
||||
<div key={domain.id} className="bg-white rounded-lg shadow-md p-6 hover:shadow-lg transition-shadow">
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-gray-900">{domain.domain}</h3>
|
||||
<p className="text-sm text-gray-600 capitalize">{domain.type.toLowerCase().replace('_', ' ')}</p>
|
||||
</div>
|
||||
<span className={`px-2 py-1 rounded-full text-xs font-medium ${
|
||||
domain.isActive ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'
|
||||
}`}>
|
||||
{domain.isActive ? 'Active' : 'Inactive'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 mb-4">
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="font-medium text-gray-700">SSL Status:</span>
|
||||
<span className={`px-2 py-1 rounded-full text-xs font-medium ${getSSLStatusColor(domain.sslStatus)}`}>
|
||||
{domain.sslStatus}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="font-medium text-gray-700">DNS Status:</span>
|
||||
<span className={`px-2 py-1 rounded-full text-xs font-medium ${getDNSStatusColor(domain.dnsStatus)}`}>
|
||||
{domain.dnsStatus}
|
||||
</span>
|
||||
</div>
|
||||
{domain.sslExpiryDate && (
|
||||
<div className="text-sm text-gray-600">
|
||||
<span className="font-medium">SSL Expires:</span> {new Date(domain.sslExpiryDate).toLocaleDateString()}
|
||||
</div>
|
||||
)}
|
||||
<div className="text-sm text-gray-600">
|
||||
<span className="font-medium">Added:</span> {new Date(domain.createdAt).toLocaleDateString()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => handleValidateDomain(domain.id)}
|
||||
className="flex-1 bg-blue-50 hover:bg-blue-100 text-blue-700 px-3 py-2 rounded-md text-sm font-medium transition-colors"
|
||||
>
|
||||
Validate
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleRenewSSL(domain.id)}
|
||||
className="flex-1 bg-green-50 hover:bg-green-100 text-green-700 px-3 py-2 rounded-md text-sm font-medium transition-colors"
|
||||
>
|
||||
Renew SSL
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{domains.length === 0 && (
|
||||
<div className="text-center py-12">
|
||||
<div className="text-gray-400 mb-4">
|
||||
<svg className="mx-auto h-12 w-12" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9v-9m0-9v9m0 9c-5 0-9-4-9-9s4-9 9-9" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">No domains found</h3>
|
||||
<p className="text-gray-600 mb-4">Get started by adding your first domain.</p>
|
||||
<button
|
||||
onClick={() => setShowAddForm(true)}
|
||||
className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md font-medium"
|
||||
>
|
||||
Add Domain
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,477 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
interface KnowledgeBaseItem {
|
||||
id: string;
|
||||
category: string;
|
||||
question: string;
|
||||
questionAr?: string;
|
||||
answer: string;
|
||||
answerAr?: string;
|
||||
priority: number;
|
||||
isActive: boolean;
|
||||
}
|
||||
|
||||
export default function KnowledgeBasePage() {
|
||||
const [knowledgeBase, setKnowledgeBase] = useState<KnowledgeBaseItem[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isEditing, setIsEditing] = useState<string | null>(null);
|
||||
const [editingItem, setEditingItem] = useState<Partial<KnowledgeBaseItem>>({});
|
||||
const [showAddForm, setShowAddForm] = useState(false);
|
||||
const [newItem, setNewItem] = useState<Partial<KnowledgeBaseItem>>({
|
||||
category: '',
|
||||
question: '',
|
||||
questionAr: '',
|
||||
answer: '',
|
||||
answerAr: '',
|
||||
priority: 1,
|
||||
isActive: true
|
||||
});
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
fetchKnowledgeBase();
|
||||
}, []);
|
||||
|
||||
const fetchKnowledgeBase = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/knowledge-base');
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setKnowledgeBase(data.knowledgeBase);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching knowledge base:', error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddItem = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/knowledge-base', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(newItem),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
setShowAddForm(false);
|
||||
setNewItem({
|
||||
category: '',
|
||||
question: '',
|
||||
questionAr: '',
|
||||
answer: '',
|
||||
answerAr: '',
|
||||
priority: 1,
|
||||
isActive: true
|
||||
});
|
||||
fetchKnowledgeBase();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error adding knowledge base item:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdateItem = async (id: string) => {
|
||||
try {
|
||||
const response = await fetch(`/api/knowledge-base/${id}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(editingItem),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
setIsEditing(null);
|
||||
setEditingItem({});
|
||||
fetchKnowledgeBase();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error updating knowledge base item:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteItem = async (id: string) => {
|
||||
if (!confirm('Are you sure you want to delete this item?')) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/knowledge-base/${id}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
fetchKnowledgeBase();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error deleting knowledge base item:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggleActive = async (id: string, isActive: boolean) => {
|
||||
try {
|
||||
const response = await fetch(`/api/knowledge-base/${id}`, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ isActive }),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
fetchKnowledgeBase();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error toggling knowledge base item:', error);
|
||||
}
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 p-8">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div className="animate-pulse">
|
||||
<div className="h-8 bg-gray-200 rounded w-1/4 mb-8"></div>
|
||||
<div className="space-y-4">
|
||||
{[...Array(5)].map((_, i) => (
|
||||
<div key={i} className="bg-white p-6 rounded-lg shadow">
|
||||
<div className="h-4 bg-gray-200 rounded w-3/4 mb-2"></div>
|
||||
<div className="h-4 bg-gray-200 rounded w-1/2"></div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 p-8">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<h1 className="text-3xl font-bold text-gray-900 mb-2">
|
||||
AI Knowledge Base Management
|
||||
</h1>
|
||||
<p className="text-gray-600">
|
||||
Customize your university's AI responses by adding frequently asked questions and their answers.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
|
||||
<div className="bg-white p-6 rounded-lg shadow">
|
||||
<div className="text-2xl font-bold text-blue-600">{knowledgeBase.length}</div>
|
||||
<div className="text-gray-600">Total Items</div>
|
||||
</div>
|
||||
<div className="bg-white p-6 rounded-lg shadow">
|
||||
<div className="text-2xl font-bold text-green-600">
|
||||
{knowledgeBase.filter(item => item.isActive).length}
|
||||
</div>
|
||||
<div className="text-gray-600">Active Items</div>
|
||||
</div>
|
||||
<div className="bg-white p-6 rounded-lg shadow">
|
||||
<div className="text-2xl font-bold text-purple-600">
|
||||
{new Set(knowledgeBase.map(item => item.category)).size}
|
||||
</div>
|
||||
<div className="text-gray-600">Categories</div>
|
||||
</div>
|
||||
<div className="bg-white p-6 rounded-lg shadow">
|
||||
<div className="text-2xl font-bold text-orange-600">
|
||||
{knowledgeBase.filter(item => item.questionAr && item.answerAr).length}
|
||||
</div>
|
||||
<div className="text-gray-600">Bilingual Items</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Add New Item Button */}
|
||||
<div className="mb-6">
|
||||
<button
|
||||
onClick={() => setShowAddForm(true)}
|
||||
className="bg-blue-600 text-white px-6 py-3 rounded-lg hover:bg-blue-700 transition-colors"
|
||||
>
|
||||
+ Add New Knowledge Base Item
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Add New Item Form */}
|
||||
{showAddForm && (
|
||||
<div className="bg-white p-6 rounded-lg shadow mb-6">
|
||||
<h3 className="text-lg font-semibold mb-4">Add New Knowledge Base Item</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Category
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={newItem.category || ''}
|
||||
onChange={(e) => setNewItem({ ...newItem, category: e.target.value })}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="e.g., Admissions, Programs, Campus Life"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Priority
|
||||
</label>
|
||||
<select
|
||||
value={newItem.priority || 1}
|
||||
onChange={(e) => setNewItem({ ...newItem, priority: parseInt(e.target.value) })}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
<option value={1}>Low</option>
|
||||
<option value={2}>Medium</option>
|
||||
<option value={3}>High</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="md:col-span-2">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Question (English)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={newItem.question || ''}
|
||||
onChange={(e) => setNewItem({ ...newItem, question: e.target.value })}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="What are the admission requirements?"
|
||||
/>
|
||||
</div>
|
||||
<div className="md:col-span-2">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Question (Arabic)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={newItem.questionAr || ''}
|
||||
onChange={(e) => setNewItem({ ...newItem, questionAr: e.target.value })}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="ما هي متطلبات القبول؟"
|
||||
/>
|
||||
</div>
|
||||
<div className="md:col-span-2">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Answer (English)
|
||||
</label>
|
||||
<textarea
|
||||
value={newItem.answer || ''}
|
||||
onChange={(e) => setNewItem({ ...newItem, answer: e.target.value })}
|
||||
rows={3}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="Detailed answer in English..."
|
||||
/>
|
||||
</div>
|
||||
<div className="md:col-span-2">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Answer (Arabic)
|
||||
</label>
|
||||
<textarea
|
||||
value={newItem.answerAr || ''}
|
||||
onChange={(e) => setNewItem({ ...newItem, answerAr: e.target.value })}
|
||||
rows={3}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="Detailed answer in Arabic..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex space-x-4 mt-6">
|
||||
<button
|
||||
onClick={handleAddItem}
|
||||
className="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 transition-colors"
|
||||
>
|
||||
Add Item
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowAddForm(false)}
|
||||
className="bg-gray-300 text-gray-700 px-4 py-2 rounded-md hover:bg-gray-400 transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Knowledge Base List */}
|
||||
<div className="bg-white rounded-lg shadow">
|
||||
<div className="p-6 border-b border-gray-200">
|
||||
<h2 className="text-xl font-semibold text-gray-900">Knowledge Base Items</h2>
|
||||
</div>
|
||||
<div className="divide-y divide-gray-200">
|
||||
{knowledgeBase.length === 0 ? (
|
||||
<div className="p-6 text-center text-gray-500">
|
||||
No knowledge base items found. Add your first item to get started.
|
||||
</div>
|
||||
) : (
|
||||
knowledgeBase.map((item) => (
|
||||
<div key={item.id} className="p-6">
|
||||
{isEditing === item.id ? (
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Category
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={editingItem.category || item.category}
|
||||
onChange={(e) => setEditingItem({ ...editingItem, category: e.target.value })}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Priority
|
||||
</label>
|
||||
<select
|
||||
value={editingItem.priority || item.priority}
|
||||
onChange={(e) => setEditingItem({ ...editingItem, priority: parseInt(e.target.value) })}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
<option value={1}>Low</option>
|
||||
<option value={2}>Medium</option>
|
||||
<option value={3}>High</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Question (English)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={editingItem.question || item.question}
|
||||
onChange={(e) => setEditingItem({ ...editingItem, question: e.target.value })}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Answer (English)
|
||||
</label>
|
||||
<textarea
|
||||
value={editingItem.answer || item.answer}
|
||||
onChange={(e) => setEditingItem({ ...editingItem, answer: e.target.value })}
|
||||
rows={3}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex space-x-4">
|
||||
<button
|
||||
onClick={() => handleUpdateItem(item.id)}
|
||||
className="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 transition-colors"
|
||||
>
|
||||
Save Changes
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsEditing(null);
|
||||
setEditingItem({});
|
||||
}}
|
||||
className="bg-gray-300 text-gray-700 px-4 py-2 rounded-md hover:bg-gray-400 transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center space-x-3 mb-2">
|
||||
<span className="px-2 py-1 bg-blue-100 text-blue-800 text-xs rounded-full">
|
||||
{item.category}
|
||||
</span>
|
||||
<span className="px-2 py-1 bg-gray-100 text-gray-800 text-xs rounded-full">
|
||||
Priority: {item.priority}
|
||||
</span>
|
||||
<span className={`px-2 py-1 text-xs rounded-full ${
|
||||
item.isActive
|
||||
? 'bg-green-100 text-green-800'
|
||||
: 'bg-red-100 text-red-800'
|
||||
}`}>
|
||||
{item.isActive ? 'Active' : 'Inactive'}
|
||||
</span>
|
||||
</div>
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">
|
||||
{item.question}
|
||||
</h3>
|
||||
<p className="text-gray-600 mb-2">
|
||||
{item.answer}
|
||||
</p>
|
||||
{item.questionAr && item.answerAr && (
|
||||
<div className="mt-4 p-3 bg-gray-50 rounded-md">
|
||||
<h4 className="text-sm font-medium text-gray-700 mb-1">Arabic Version:</h4>
|
||||
<p className="text-sm text-gray-600 mb-1">{item.questionAr}</p>
|
||||
<p className="text-sm text-gray-600">{item.answerAr}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex space-x-2 ml-4">
|
||||
<button
|
||||
onClick={() => handleToggleActive(item.id, !item.isActive)}
|
||||
className={`px-3 py-1 text-xs rounded-md transition-colors ${
|
||||
item.isActive
|
||||
? 'bg-red-100 text-red-700 hover:bg-red-200'
|
||||
: 'bg-green-100 text-green-700 hover:bg-green-200'
|
||||
}`}
|
||||
>
|
||||
{item.isActive ? 'Deactivate' : 'Activate'}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsEditing(item.id);
|
||||
setEditingItem(item);
|
||||
}}
|
||||
className="px-3 py-1 text-xs bg-blue-100 text-blue-700 rounded-md hover:bg-blue-200 transition-colors"
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDeleteItem(item.id)}
|
||||
className="px-3 py-1 text-xs bg-red-100 text-red-700 rounded-md hover:bg-red-200 transition-colors"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* AI Training Tips */}
|
||||
<div className="mt-8 bg-blue-50 p-6 rounded-lg">
|
||||
<h3 className="text-lg font-semibold text-blue-900 mb-4">
|
||||
💡 Tips for Better AI Responses
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm text-blue-800">
|
||||
<div>
|
||||
<h4 className="font-medium mb-2">Question Format:</h4>
|
||||
<ul className="space-y-1">
|
||||
<li>• Use natural, conversational language</li>
|
||||
<li>• Include common variations of questions</li>
|
||||
<li>• Be specific about university policies</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-medium mb-2">Answer Format:</h4>
|
||||
<ul className="space-y-1">
|
||||
<li>• Provide clear, concise responses</li>
|
||||
<li>• Include relevant contact information</li>
|
||||
<li>• Update regularly with current information</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+124
-412
@@ -1,430 +1,142 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { useAuth } from '@/components/providers/MockAuthProvider'
|
||||
import { useLanguage } from '@/components/providers/LanguageProvider'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import {
|
||||
BarChart3,
|
||||
Users,
|
||||
MessageSquare,
|
||||
Star,
|
||||
TrendingUp,
|
||||
Clock,
|
||||
Shield,
|
||||
Heart,
|
||||
LogOut,
|
||||
Languages,
|
||||
ArrowLeft,
|
||||
Eye,
|
||||
CheckCircle,
|
||||
AlertTriangle
|
||||
} from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, BarChart, Bar } from 'recharts'
|
||||
|
||||
interface SurveyStats {
|
||||
totalSurveys: number
|
||||
averageRating: number
|
||||
ratingDistribution: Record<number, number>
|
||||
}
|
||||
import Link from 'next/link';
|
||||
import { BranchSelector } from '@/components/BranchManagement/BranchSelector';
|
||||
|
||||
export default function AdminPage() {
|
||||
const { user, userProfile, logout } = useAuth()
|
||||
const { t, language, setLanguage } = useLanguage()
|
||||
const router = useRouter()
|
||||
const [surveyStats, setSurveyStats] = useState<SurveyStats | null>(null)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
if (!user) {
|
||||
router.push('/')
|
||||
return
|
||||
}
|
||||
|
||||
if (userProfile && userProfile.role !== 'ADMIN') {
|
||||
router.push('/dashboard')
|
||||
return
|
||||
}
|
||||
|
||||
fetchDashboardData()
|
||||
}, [user, userProfile, router])
|
||||
|
||||
const fetchDashboardData = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/survey')
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
setSurveyStats(data.statistics)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching dashboard data:', error)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleLogout = async () => {
|
||||
await logout()
|
||||
}
|
||||
|
||||
const toggleLanguage = () => {
|
||||
setLanguage(language === 'en' ? 'ar' : 'en')
|
||||
}
|
||||
|
||||
// Mock data for charts
|
||||
const responseTimeData = [
|
||||
{ name: 'Mon', time: 1.2 },
|
||||
{ name: 'Tue', time: 0.8 },
|
||||
{ name: 'Wed', time: 1.5 },
|
||||
{ name: 'Thu', time: 1.1 },
|
||||
{ name: 'Fri', time: 0.9 },
|
||||
{ name: 'Sat', time: 1.3 },
|
||||
{ name: 'Sun', time: 1.0 },
|
||||
]
|
||||
|
||||
const satisfactionData = [
|
||||
{ name: 'Week 1', satisfaction: 92 },
|
||||
{ name: 'Week 2', satisfaction: 88 },
|
||||
{ name: 'Week 3', satisfaction: 95 },
|
||||
{ name: 'Week 4', satisfaction: 91 },
|
||||
]
|
||||
|
||||
const ratingData = surveyStats ?
|
||||
Object.entries(surveyStats.ratingDistribution).map(([rating, count]) => ({
|
||||
rating: `${rating} Stars`,
|
||||
count
|
||||
})) : []
|
||||
|
||||
if (!user || !userProfile || isLoading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div>
|
||||
<p className="text-gray-600">Loading admin dashboard...</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (userProfile.role !== 'ADMIN') {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<AlertTriangle className="mx-auto h-12 w-12 text-red-500 mb-4" />
|
||||
<h1 className="text-xl font-bold text-gray-900 mb-2">Access Denied</h1>
|
||||
<p className="text-gray-600">You don't have permission to access this page.</p>
|
||||
<Link href="/dashboard" className="mt-4 inline-block bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700">
|
||||
Go to Dashboard
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
{/* Header */}
|
||||
<header className="bg-white shadow-sm border-b">
|
||||
<div className="container mx-auto px-4 py-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-4">
|
||||
<Link href="/dashboard" className="flex items-center space-x-2 text-blue-600 hover:text-blue-800">
|
||||
<ArrowLeft size={20} />
|
||||
<span>Back to Dashboard</span>
|
||||
</Link>
|
||||
<div className="text-gray-300">|</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<BarChart3 className="text-blue-600" size={24} />
|
||||
<h1 className="text-xl font-bold text-gray-900">
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
{/* Header with Branch Selector */}
|
||||
<div className="bg-white rounded-lg shadow-md p-6 mb-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900 mb-2">
|
||||
Admin Dashboard
|
||||
</h1>
|
||||
<p className="text-gray-600">
|
||||
Manage your university portal and configurations
|
||||
</p>
|
||||
</div>
|
||||
<BranchSelector />
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-4">
|
||||
<button
|
||||
onClick={toggleLanguage}
|
||||
className="flex items-center space-x-2 px-3 py-2 rounded-lg bg-gray-100 hover:bg-gray-200 transition-colors"
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<div className="bg-blue-50 rounded-lg p-6">
|
||||
<h3 className="text-lg font-semibold text-blue-900 mb-2">Universities</h3>
|
||||
<p className="text-blue-700 mb-4">Manage university configurations and settings</p>
|
||||
<Link
|
||||
href="/admin/universities"
|
||||
className="inline-block bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 transition-colors"
|
||||
>
|
||||
<Languages size={16} />
|
||||
<span className="text-sm font-medium">{language.toUpperCase()}</span>
|
||||
</button>
|
||||
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="w-8 h-8 bg-blue-600 rounded-full flex items-center justify-center">
|
||||
<span className="text-white text-sm font-medium">
|
||||
{userProfile.name.charAt(0)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="hidden md:block">
|
||||
<p className="text-sm font-medium text-gray-900">{userProfile.name}</p>
|
||||
<p className="text-xs text-gray-500">{userProfile.role}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="flex items-center space-x-2 px-3 py-2 rounded-lg bg-red-100 hover:bg-red-200 transition-colors text-red-700"
|
||||
Manage Universities
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="bg-green-50 rounded-lg p-6">
|
||||
<h3 className="text-lg font-semibold text-green-900 mb-2">Content</h3>
|
||||
<p className="text-green-700 mb-4">Manage university content and pages</p>
|
||||
<Link
|
||||
href="/admin/content"
|
||||
className="inline-block bg-green-600 text-white px-4 py-2 rounded hover:bg-green-700 transition-colors"
|
||||
>
|
||||
<LogOut size={16} />
|
||||
<span className="text-sm font-medium">{t('logout')}</span>
|
||||
</button>
|
||||
Manage Content
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="bg-purple-50 rounded-lg p-6">
|
||||
<h3 className="text-lg font-semibold text-purple-900 mb-2">Programs</h3>
|
||||
<p className="text-purple-700 mb-4">Manage academic programs and courses</p>
|
||||
<Link
|
||||
href="/admin/programs"
|
||||
className="inline-block bg-purple-600 text-white px-4 py-2 rounded hover:bg-purple-700 transition-colors"
|
||||
>
|
||||
Manage Programs
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="bg-orange-50 rounded-lg p-6">
|
||||
<h3 className="text-lg font-semibold text-orange-900 mb-2">Domains</h3>
|
||||
<p className="text-orange-700 mb-4">Manage domain configurations and SSL</p>
|
||||
<Link
|
||||
href="/admin/domains"
|
||||
className="inline-block bg-orange-600 text-white px-4 py-2 rounded hover:bg-orange-700 transition-colors"
|
||||
>
|
||||
Manage Domains
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="bg-red-50 rounded-lg p-6">
|
||||
<h3 className="text-lg font-semibold text-red-900 mb-2">Deployments</h3>
|
||||
<p className="text-red-700 mb-4">Monitor and manage deployments</p>
|
||||
<Link
|
||||
href="/admin/deployments"
|
||||
className="inline-block bg-red-600 text-white px-4 py-2 rounded hover:bg-red-700 transition-colors"
|
||||
>
|
||||
View Deployments
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="bg-indigo-50 rounded-lg p-6">
|
||||
<h3 className="text-lg font-semibold text-indigo-900 mb-2">Analytics</h3>
|
||||
<p className="text-indigo-700 mb-4">View platform analytics and metrics</p>
|
||||
<Link
|
||||
href="/admin/analytics"
|
||||
className="inline-block bg-indigo-600 text-white px-4 py-2 rounded hover:bg-indigo-700 transition-colors"
|
||||
>
|
||||
View Analytics
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* New Branch Management Card */}
|
||||
<div className="bg-teal-50 rounded-lg p-6">
|
||||
<h3 className="text-lg font-semibold text-teal-900 mb-2">Branch Management</h3>
|
||||
<p className="text-teal-700 mb-4">Manage university branches and campuses</p>
|
||||
<Link
|
||||
href="/admin/branches"
|
||||
className="inline-block bg-teal-600 text-white px-4 py-2 rounded hover:bg-teal-700 transition-colors"
|
||||
>
|
||||
Manage Branches
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Main Content */}
|
||||
<main className="container mx-auto px-4 py-8">
|
||||
{/* KPI Cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
||||
<KPICard
|
||||
title="Active Users"
|
||||
value="12,543"
|
||||
change="+5.2%"
|
||||
trend="up"
|
||||
icon={<Users size={24} />}
|
||||
color="blue"
|
||||
/>
|
||||
<KPICard
|
||||
title="Avg Response Time"
|
||||
value="1.2s"
|
||||
change="-0.3s"
|
||||
trend="down"
|
||||
icon={<Clock size={24} />}
|
||||
color="green"
|
||||
/>
|
||||
<KPICard
|
||||
title="Satisfaction Rate"
|
||||
value={surveyStats ? `${Math.round(surveyStats.averageRating * 20)}%` : '94%'}
|
||||
change="+2.1%"
|
||||
trend="up"
|
||||
icon={<Star size={24} />}
|
||||
color="yellow"
|
||||
/>
|
||||
<KPICard
|
||||
title="Ticket Deflection"
|
||||
value="87%"
|
||||
change="+4.3%"
|
||||
trend="up"
|
||||
icon={<MessageSquare size={24} />}
|
||||
color="purple"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Charts Section */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-8">
|
||||
{/* Response Time Chart */}
|
||||
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">
|
||||
Average Response Time (This Week)
|
||||
</h3>
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<LineChart data={responseTimeData}>
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<XAxis dataKey="name" />
|
||||
<YAxis />
|
||||
<Tooltip />
|
||||
<Line type="monotone" dataKey="time" stroke="#3B82F6" strokeWidth={2} />
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
|
||||
{/* Satisfaction Trend */}
|
||||
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">
|
||||
User Satisfaction Trend
|
||||
</h3>
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<LineChart data={satisfactionData}>
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<XAxis dataKey="name" />
|
||||
<YAxis />
|
||||
<Tooltip />
|
||||
<Line type="monotone" dataKey="satisfaction" stroke="#10B981" strokeWidth={2} />
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Rating Distribution */}
|
||||
{surveyStats && ratingData.length > 0 && (
|
||||
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6 mb-8">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">
|
||||
Rating Distribution ({surveyStats.totalSurveys} responses)
|
||||
</h3>
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<BarChart data={ratingData}>
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<XAxis dataKey="rating" />
|
||||
<YAxis />
|
||||
<Tooltip />
|
||||
<Bar dataKey="count" fill="#F59E0B" />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||
{/* System Health */}
|
||||
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">
|
||||
System Health
|
||||
</h3>
|
||||
<div className="space-y-4">
|
||||
<HealthIndicator
|
||||
label="Chat Service"
|
||||
status="healthy"
|
||||
value="99.8% uptime"
|
||||
/>
|
||||
<HealthIndicator
|
||||
label="Database"
|
||||
status="healthy"
|
||||
value="Response time: 12ms"
|
||||
/>
|
||||
<HealthIndicator
|
||||
label="AI Assistant"
|
||||
status="healthy"
|
||||
value="Processing normally"
|
||||
/>
|
||||
<HealthIndicator
|
||||
label="Accessibility Scanner"
|
||||
status="warning"
|
||||
value="High load detected"
|
||||
/>
|
||||
|
||||
<div className="mt-8 p-6 bg-white rounded-lg shadow-md">
|
||||
<h2 className="text-xl font-semibold text-gray-900 mb-4">Quick Actions</h2>
|
||||
<div className="flex flex-wrap gap-4">
|
||||
<Link
|
||||
href="/api/test"
|
||||
className="bg-gray-600 text-white px-4 py-2 rounded hover:bg-gray-700 transition-colors"
|
||||
>
|
||||
Test API
|
||||
</Link>
|
||||
<Link
|
||||
href="/api/health"
|
||||
className="bg-gray-600 text-white px-4 py-2 rounded hover:bg-gray-700 transition-colors"
|
||||
>
|
||||
Health Check
|
||||
</Link>
|
||||
<Link
|
||||
href="/"
|
||||
className="bg-gray-600 text-white px-4 py-2 rounded hover:bg-gray-700 transition-colors"
|
||||
>
|
||||
Home Page
|
||||
</Link>
|
||||
<Link
|
||||
href="/demo"
|
||||
className="bg-gray-600 text-white px-4 py-2 rounded hover:bg-gray-700 transition-colors"
|
||||
>
|
||||
Demo Page
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Recent Activity */}
|
||||
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">
|
||||
Recent Activity
|
||||
</h3>
|
||||
<div className="space-y-4">
|
||||
<ActivityItem
|
||||
icon={<MessageSquare size={16} />}
|
||||
title="New chat session started"
|
||||
time="2 minutes ago"
|
||||
type="chat"
|
||||
/>
|
||||
<ActivityItem
|
||||
icon={<Star size={16} />}
|
||||
title="Survey response: 5 stars"
|
||||
time="5 minutes ago"
|
||||
type="survey"
|
||||
/>
|
||||
<ActivityItem
|
||||
icon={<Shield size={16} />}
|
||||
title="Accessibility scan completed"
|
||||
time="12 minutes ago"
|
||||
type="accessibility"
|
||||
/>
|
||||
<ActivityItem
|
||||
icon={<Heart size={16} />}
|
||||
title="Mental health escalation"
|
||||
time="25 minutes ago"
|
||||
type="mental-health"
|
||||
/>
|
||||
<ActivityItem
|
||||
icon={<Users size={16} />}
|
||||
title="New user registration"
|
||||
time="1 hour ago"
|
||||
type="user"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 text-sm text-gray-500">
|
||||
<p>White-Label University Portal - Admin Dashboard</p>
|
||||
<p>Server running on: http://localhost:3000</p>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Helper Components
|
||||
const KPICard: React.FC<{
|
||||
title: string
|
||||
value: string
|
||||
change: string
|
||||
trend: 'up' | 'down'
|
||||
icon: React.ReactNode
|
||||
color: 'blue' | 'green' | 'yellow' | 'purple'
|
||||
}> = ({ title, value, change, trend, icon, color }) => {
|
||||
const colorClasses = {
|
||||
blue: 'bg-blue-50 border-blue-200 text-blue-600',
|
||||
green: 'bg-green-50 border-green-200 text-green-600',
|
||||
yellow: 'bg-yellow-50 border-yellow-200 text-yellow-600',
|
||||
purple: 'bg-purple-50 border-purple-200 text-purple-600',
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className={`p-3 rounded-lg ${colorClasses[color]}`}>
|
||||
{icon}
|
||||
</div>
|
||||
<div className={`flex items-center space-x-1 text-sm ${
|
||||
trend === 'up' ? 'text-green-600' : 'text-red-600'
|
||||
}`}>
|
||||
<TrendingUp size={16} className={trend === 'down' ? 'rotate-180' : ''} />
|
||||
<span>{change}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-2xl font-bold text-gray-900">{value}</p>
|
||||
<p className="text-sm text-gray-600">{title}</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const HealthIndicator: React.FC<{
|
||||
label: string
|
||||
status: 'healthy' | 'warning' | 'error'
|
||||
value: string
|
||||
}> = ({ label, status, value }) => {
|
||||
const statusColors = {
|
||||
healthy: 'text-green-600 bg-green-50',
|
||||
warning: 'text-yellow-600 bg-yellow-50',
|
||||
error: 'text-red-600 bg-red-50',
|
||||
}
|
||||
|
||||
const StatusIcon = status === 'healthy' ? CheckCircle : AlertTriangle
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
|
||||
<div className="flex items-center space-x-3">
|
||||
<StatusIcon className={`${statusColors[status].split(' ')[0]}`} size={16} />
|
||||
<span className="font-medium text-gray-900">{label}</span>
|
||||
</div>
|
||||
<span className="text-sm text-gray-600">{value}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const ActivityItem: React.FC<{
|
||||
icon: React.ReactNode
|
||||
title: string
|
||||
time: string
|
||||
type: string
|
||||
}> = ({ icon, title, time, type }) => {
|
||||
const typeColors = {
|
||||
chat: 'text-blue-600 bg-blue-50',
|
||||
survey: 'text-yellow-600 bg-yellow-50',
|
||||
accessibility: 'text-green-600 bg-green-50',
|
||||
'mental-health': 'text-pink-600 bg-pink-50',
|
||||
user: 'text-purple-600 bg-purple-50',
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center space-x-3 p-3 bg-gray-50 rounded-lg">
|
||||
<div className={`p-2 rounded-lg ${typeColors[type as keyof typeof typeColors]}`}>
|
||||
{icon}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-medium text-gray-900">{title}</p>
|
||||
<p className="text-xs text-gray-500">{time}</p>
|
||||
</div>
|
||||
<Eye className="text-gray-400" size={16} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,386 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import Link from 'next/link';
|
||||
|
||||
interface Program {
|
||||
id: string;
|
||||
title: string;
|
||||
titleAr?: string;
|
||||
description?: string;
|
||||
descriptionAr?: string;
|
||||
level: string;
|
||||
duration?: string;
|
||||
fees?: string;
|
||||
entryRequirements?: string;
|
||||
isActive: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export default function ProgramsPage() {
|
||||
const [programs, setPrograms] = useState<Program[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [showAddForm, setShowAddForm] = useState(false);
|
||||
const [formData, setFormData] = useState({
|
||||
title: '',
|
||||
titleAr: '',
|
||||
description: '',
|
||||
descriptionAr: '',
|
||||
level: 'UNDERGRADUATE',
|
||||
duration: '',
|
||||
fees: '',
|
||||
entryRequirements: '',
|
||||
isActive: true,
|
||||
});
|
||||
|
||||
const loadPrograms = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/programs');
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setPrograms(data.data || []);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading programs:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddProgram = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
const response = await fetch('/api/programs', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(formData),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
setShowAddForm(false);
|
||||
setFormData({
|
||||
title: '',
|
||||
titleAr: '',
|
||||
description: '',
|
||||
descriptionAr: '',
|
||||
level: 'UNDERGRADUATE',
|
||||
duration: '',
|
||||
fees: '',
|
||||
entryRequirements: '',
|
||||
isActive: true,
|
||||
});
|
||||
loadPrograms();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error adding program:', error);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadPrograms();
|
||||
}, []);
|
||||
|
||||
const getLevelColor = (level: string) => {
|
||||
switch (level) {
|
||||
case 'UNDERGRADUATE':
|
||||
return 'bg-blue-100 text-blue-800';
|
||||
case 'POSTGRADUATE':
|
||||
return 'bg-green-100 text-green-800';
|
||||
case 'PHD':
|
||||
return 'bg-purple-100 text-purple-800';
|
||||
case 'DIPLOMA':
|
||||
return 'bg-orange-100 text-orange-800';
|
||||
default:
|
||||
return 'bg-gray-100 text-gray-800';
|
||||
}
|
||||
};
|
||||
|
||||
const getLevelLabel = (level: string) => {
|
||||
switch (level) {
|
||||
case 'UNDERGRADUATE':
|
||||
return 'Undergraduate';
|
||||
case 'POSTGRADUATE':
|
||||
return 'Postgraduate';
|
||||
case 'PHD':
|
||||
return 'PhD';
|
||||
case 'DIPLOMA':
|
||||
return 'Diploma';
|
||||
default:
|
||||
return level;
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 p-6">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div className="animate-pulse">
|
||||
<div className="h-8 bg-gray-200 rounded w-1/4 mb-6"></div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{[1, 2, 3].map((i) => (
|
||||
<div key={i} className="bg-white rounded-lg shadow p-6">
|
||||
<div className="h-4 bg-gray-200 rounded w-3/4 mb-4"></div>
|
||||
<div className="h-3 bg-gray-200 rounded w-1/2 mb-2"></div>
|
||||
<div className="h-3 bg-gray-200 rounded w-2/3"></div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<div className="max-w-7xl mx-auto px-4 py-8">
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900">Academic Programs</h1>
|
||||
<p className="text-gray-600 mt-2">Manage university academic programs and courses</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setShowAddForm(true)}
|
||||
className="bg-blue-600 hover:bg-blue-700 text-white px-6 py-3 rounded-lg font-semibold transition-colors"
|
||||
>
|
||||
Add Program
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Add Program Form */}
|
||||
{showAddForm && (
|
||||
<div className="bg-white rounded-lg shadow-md p-6 mb-8">
|
||||
<h2 className="text-xl font-semibold text-gray-900 mb-4">Add New Program</h2>
|
||||
<form onSubmit={handleAddProgram} className="space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Program Level
|
||||
</label>
|
||||
<select
|
||||
value={formData.level}
|
||||
onChange={(e) => setFormData({ ...formData, level: e.target.value })}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
<option value="UNDERGRADUATE">Undergraduate</option>
|
||||
<option value="POSTGRADUATE">Postgraduate</option>
|
||||
<option value="PHD">PhD</option>
|
||||
<option value="DIPLOMA">Diploma</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Active
|
||||
</label>
|
||||
<div className="flex items-center mt-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.isActive}
|
||||
onChange={(e) => setFormData({ ...formData, isActive: e.target.checked })}
|
||||
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
|
||||
/>
|
||||
<label className="ml-2 text-sm text-gray-700">Program is active</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Program Title (English)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.title}
|
||||
onChange={(e) => setFormData({ ...formData, title: e.target.value })}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Program Title (Arabic)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.titleAr}
|
||||
onChange={(e) => setFormData({ ...formData, titleAr: e.target.value })}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
dir="rtl"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Description (English)
|
||||
</label>
|
||||
<textarea
|
||||
value={formData.description}
|
||||
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
|
||||
rows={3}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="Enter program description..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Description (Arabic)
|
||||
</label>
|
||||
<textarea
|
||||
value={formData.descriptionAr}
|
||||
onChange={(e) => setFormData({ ...formData, descriptionAr: e.target.value })}
|
||||
rows={3}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="أدخل وصف البرنامج..."
|
||||
dir="rtl"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Duration
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.duration}
|
||||
onChange={(e) => setFormData({ ...formData, duration: e.target.value })}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="e.g., 4 years"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Fees
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.fees}
|
||||
onChange={(e) => setFormData({ ...formData, fees: e.target.value })}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="e.g., $15,000/year"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Entry Requirements
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.entryRequirements}
|
||||
onChange={(e) => setFormData({ ...formData, entryRequirements: e.target.value })}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="e.g., High school diploma"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
type="submit"
|
||||
className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md font-medium"
|
||||
>
|
||||
Create Program
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowAddForm(false)}
|
||||
className="bg-gray-300 hover:bg-gray-400 text-gray-700 px-4 py-2 rounded-md font-medium"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Programs Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{programs.map((program) => (
|
||||
<div key={program.id} className="bg-white rounded-lg shadow-md p-6 hover:shadow-lg transition-shadow">
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-gray-900">{program.title}</h3>
|
||||
{program.titleAr && (
|
||||
<p className="text-sm text-gray-600 mt-1" dir="rtl">{program.titleAr}</p>
|
||||
)}
|
||||
</div>
|
||||
<span className={`px-2 py-1 rounded-full text-xs font-medium ${getLevelColor(program.level)}`}>
|
||||
{getLevelLabel(program.level)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 mb-4">
|
||||
<div className="flex items-center text-sm text-gray-600">
|
||||
<span className="font-medium w-20">Status:</span>
|
||||
<span className={`px-2 py-1 rounded-full text-xs font-medium ${
|
||||
program.isActive ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
|
||||
}`}>
|
||||
{program.isActive ? 'Active' : 'Inactive'}
|
||||
</span>
|
||||
</div>
|
||||
{program.duration && (
|
||||
<div className="flex items-center text-sm text-gray-600">
|
||||
<span className="font-medium w-20">Duration:</span>
|
||||
<span>{program.duration}</span>
|
||||
</div>
|
||||
)}
|
||||
{program.fees && (
|
||||
<div className="flex items-center text-sm text-gray-600">
|
||||
<span className="font-medium w-20">Fees:</span>
|
||||
<span>{program.fees}</span>
|
||||
</div>
|
||||
)}
|
||||
{program.description && (
|
||||
<div className="text-sm text-gray-600">
|
||||
<span className="font-medium">Description:</span> {program.description.substring(0, 100)}...
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Link
|
||||
href={`/admin/programs/${program.id}`}
|
||||
className="flex-1 bg-blue-50 hover:bg-blue-100 text-blue-700 px-3 py-2 rounded-md text-sm font-medium text-center transition-colors"
|
||||
>
|
||||
Edit
|
||||
</Link>
|
||||
<Link
|
||||
href={`/admin/programs/${program.id}/courses`}
|
||||
className="flex-1 bg-green-50 hover:bg-green-100 text-green-700 px-3 py-2 rounded-md text-sm font-medium text-center transition-colors"
|
||||
>
|
||||
Courses
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{programs.length === 0 && (
|
||||
<div className="text-center py-12">
|
||||
<div className="text-gray-400 mb-4">
|
||||
<svg className="mx-auto h-12 w-12" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.746 0 3.332.477 4.5 1.253v13C19.832 18.477 18.246 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">No programs found</h3>
|
||||
<p className="text-gray-600 mb-4">Get started by creating your first academic program.</p>
|
||||
<button
|
||||
onClick={() => setShowAddForm(true)}
|
||||
className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md font-medium"
|
||||
>
|
||||
Add Program
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,280 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import Link from 'next/link';
|
||||
|
||||
interface University {
|
||||
id: string;
|
||||
slug: string;
|
||||
name: string;
|
||||
shortName: string | null;
|
||||
domain: string | null;
|
||||
subdomain: string | null;
|
||||
status: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export default function UniversitiesPage() {
|
||||
const [universities, setUniversities] = useState<University[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [showAddForm, setShowAddForm] = useState(false);
|
||||
const [formData, setFormData] = useState({
|
||||
name: '',
|
||||
shortName: '',
|
||||
slug: '',
|
||||
domain: '',
|
||||
subdomain: '',
|
||||
});
|
||||
|
||||
const loadUniversities = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/universities');
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setUniversities(data.data || []);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading universities:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddUniversity = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
const response = await fetch('/api/universities', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(formData),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
setShowAddForm(false);
|
||||
setFormData({ name: '', shortName: '', slug: '', domain: '', subdomain: '' });
|
||||
loadUniversities();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error adding university:', error);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadUniversities();
|
||||
}, []);
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'ACTIVE':
|
||||
return 'bg-green-100 text-green-800';
|
||||
case 'SETUP':
|
||||
return 'bg-yellow-100 text-yellow-800';
|
||||
case 'INACTIVE':
|
||||
return 'bg-red-100 text-red-800';
|
||||
default:
|
||||
return 'bg-gray-100 text-gray-800';
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 p-6">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div className="animate-pulse">
|
||||
<div className="h-8 bg-gray-200 rounded w-1/4 mb-6"></div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{[1, 2, 3].map((i) => (
|
||||
<div key={i} className="bg-white rounded-lg shadow p-6">
|
||||
<div className="h-4 bg-gray-200 rounded w-3/4 mb-4"></div>
|
||||
<div className="h-3 bg-gray-200 rounded w-1/2 mb-2"></div>
|
||||
<div className="h-3 bg-gray-200 rounded w-2/3"></div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<div className="max-w-7xl mx-auto px-4 py-8">
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900">Universities</h1>
|
||||
<p className="text-gray-600 mt-2">Manage university configurations and settings</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setShowAddForm(true)}
|
||||
className="bg-blue-600 hover:bg-blue-700 text-white px-6 py-3 rounded-lg font-semibold transition-colors"
|
||||
>
|
||||
Add University
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Add University Form */}
|
||||
{showAddForm && (
|
||||
<div className="bg-white rounded-lg shadow-md p-6 mb-8">
|
||||
<h2 className="text-xl font-semibold text-gray-900 mb-4">Add New University</h2>
|
||||
<form onSubmit={handleAddUniversity} className="space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
University Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.name}
|
||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Short Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.shortName}
|
||||
onChange={(e) => setFormData({ ...formData, shortName: e.target.value })}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Slug
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.slug}
|
||||
onChange={(e) => setFormData({ ...formData, slug: e.target.value })}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Domain
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.domain}
|
||||
onChange={(e) => setFormData({ ...formData, domain: e.target.value })}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="university.edu"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Subdomain
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.subdomain}
|
||||
onChange={(e) => setFormData({ ...formData, subdomain: e.target.value })}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="university"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
type="submit"
|
||||
className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md font-medium"
|
||||
>
|
||||
Create University
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowAddForm(false)}
|
||||
className="bg-gray-300 hover:bg-gray-400 text-gray-700 px-4 py-2 rounded-md font-medium"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Universities Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{universities.map((university) => (
|
||||
<div key={university.id} className="bg-white rounded-lg shadow-md p-6 hover:shadow-lg transition-shadow">
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-gray-900">{university.name}</h3>
|
||||
{university.shortName && (
|
||||
<p className="text-sm text-gray-600">{university.shortName}</p>
|
||||
)}
|
||||
</div>
|
||||
<span className={`px-2 py-1 rounded-full text-xs font-medium ${getStatusColor(university.status)}`}>
|
||||
{university.status}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 mb-4">
|
||||
<div className="flex items-center text-sm text-gray-600">
|
||||
<span className="font-medium w-20">Slug:</span>
|
||||
<span className="font-mono bg-gray-100 px-2 py-1 rounded">{university.slug}</span>
|
||||
</div>
|
||||
{university.domain && (
|
||||
<div className="flex items-center text-sm text-gray-600">
|
||||
<span className="font-medium w-20">Domain:</span>
|
||||
<span>{university.domain}</span>
|
||||
</div>
|
||||
)}
|
||||
{university.subdomain && (
|
||||
<div className="flex items-center text-sm text-gray-600">
|
||||
<span className="font-medium w-20">Subdomain:</span>
|
||||
<span>{university.subdomain}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Link
|
||||
href={`/admin/universities/${university.id}`}
|
||||
className="flex-1 bg-blue-50 hover:bg-blue-100 text-blue-700 px-3 py-2 rounded-md text-sm font-medium text-center transition-colors"
|
||||
>
|
||||
Edit
|
||||
</Link>
|
||||
<Link
|
||||
href={`/admin/universities/${university.id}/settings`}
|
||||
className="flex-1 bg-gray-50 hover:bg-gray-100 text-gray-700 px-3 py-2 rounded-md text-sm font-medium text-center transition-colors"
|
||||
>
|
||||
Settings
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{universities.length === 0 && (
|
||||
<div className="text-center py-12">
|
||||
<div className="text-gray-400 mb-4">
|
||||
<svg className="mx-auto h-12 w-12" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">No universities found</h3>
|
||||
<p className="text-gray-600 mb-4">Get started by creating your first university.</p>
|
||||
<button
|
||||
onClick={() => setShowAddForm(true)}
|
||||
className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md font-medium"
|
||||
>
|
||||
Add University
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user