Genel düzenlemeler
This commit is contained in:
parent
b009b520e1
commit
795ad3d00c
9 changed files with 5372 additions and 583 deletions
|
|
@ -67,7 +67,7 @@ if (!self.define) {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
define(['./workbox-54d0af47'], (function (workbox) { 'use strict';
|
define(['./workbox-a959eb95'], (function (workbox) { 'use strict';
|
||||||
|
|
||||||
self.skipWaiting();
|
self.skipWaiting();
|
||||||
workbox.clientsClaim();
|
workbox.clientsClaim();
|
||||||
|
|
@ -81,13 +81,29 @@ define(['./workbox-54d0af47'], (function (workbox) { 'use strict';
|
||||||
"url": "registerSW.js",
|
"url": "registerSW.js",
|
||||||
"revision": "3ca0b8505b4bec776b69afdba2768812"
|
"revision": "3ca0b8505b4bec776b69afdba2768812"
|
||||||
}, {
|
}, {
|
||||||
"url": "index.html",
|
"url": "/index.html",
|
||||||
"revision": "0.h9qpsacvko"
|
"revision": "0.g281p2a917g"
|
||||||
}], {});
|
}], {});
|
||||||
workbox.cleanupOutdatedCaches();
|
workbox.cleanupOutdatedCaches();
|
||||||
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {
|
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("/index.html"), {
|
||||||
allowlist: [/^\/$/],
|
allowlist: [/^\/$/],
|
||||||
denylist: [/^\/api\//]
|
denylist: [/^\/api\//]
|
||||||
}));
|
}));
|
||||||
|
workbox.registerRoute(/\.(?:js|css|html|json)$/, new workbox.NetworkFirst({
|
||||||
|
"cacheName": "static-resources",
|
||||||
|
plugins: [new workbox.ExpirationPlugin({
|
||||||
|
maxEntries: 50,
|
||||||
|
maxAgeSeconds: 86400
|
||||||
|
}), new workbox.CacheableResponsePlugin({
|
||||||
|
statuses: [0, 200]
|
||||||
|
})]
|
||||||
|
}), 'GET');
|
||||||
|
workbox.registerRoute(/\.(?:png|jpg|jpeg|svg|gif|webp|ico)$/, new workbox.CacheFirst({
|
||||||
|
"cacheName": "images",
|
||||||
|
plugins: [new workbox.ExpirationPlugin({
|
||||||
|
maxEntries: 100,
|
||||||
|
maxAgeSeconds: 2592000
|
||||||
|
})]
|
||||||
|
}), 'GET');
|
||||||
|
|
||||||
}));
|
}));
|
||||||
|
|
|
||||||
4784
ui/dev-dist/workbox-a959eb95.js
Normal file
4784
ui/dev-dist/workbox-a959eb95.js
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -49,11 +49,9 @@ const DeveloperLayout: React.FC<DeveloperLayoutProps> = ({ children }) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<div className="mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
<div className="flex flex-col lg:flex-row gap-8">
|
||||||
<div className="flex flex-col lg:flex-row gap-8">
|
{/* Main Content */}
|
||||||
{/* Main Content */}
|
<div className="flex-1">{children}</div>
|
||||||
<div className="flex-1">{children}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -137,7 +137,7 @@ export const Dashboard: React.FC = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
<>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="flex items-center justify-center min-h-96">
|
<div className="flex items-center justify-center min-h-96">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
|
|
@ -236,7 +236,9 @@ export const Dashboard: React.FC = () => {
|
||||||
<div className="bg-white rounded-xl shadow-md p-6 border border-gray-200">
|
<div className="bg-white rounded-xl shadow-md p-6 border border-gray-200">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-medium text-gray-600">{translate('::App.Reports.Dashboard.ActiveCategories')}</p>
|
<p className="text-sm font-medium text-gray-600">
|
||||||
|
{translate('::App.Reports.Dashboard.ActiveCategories')}
|
||||||
|
</p>
|
||||||
<p className="text-2xl font-bold text-gray-900">{categories.length}</p>
|
<p className="text-2xl font-bold text-gray-900">{categories.length}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-emerald-100 p-3 rounded-full">
|
<div className="bg-emerald-100 p-3 rounded-full">
|
||||||
|
|
@ -313,6 +315,6 @@ export const Dashboard: React.FC = () => {
|
||||||
template={generatingTemplate}
|
template={generatingTemplate}
|
||||||
onGenerate={handleReportGeneration}
|
onGenerate={handleReportGeneration}
|
||||||
/>
|
/>
|
||||||
</div>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,53 +1,54 @@
|
||||||
export interface ForumCategory {
|
export interface ForumCategory {
|
||||||
id: string;
|
id: string
|
||||||
name: string;
|
name: string
|
||||||
slug: string;
|
slug: string
|
||||||
description: string;
|
description: string
|
||||||
icon: string;
|
icon: string
|
||||||
displayOrder: number;
|
displayOrder: number
|
||||||
isActive: boolean;
|
isActive: boolean
|
||||||
isLocked: boolean;
|
isLocked: boolean
|
||||||
topicCount: number;
|
topicCount: number
|
||||||
postCount: number;
|
postCount: number
|
||||||
lastPostId?: string;
|
lastPostId?: string
|
||||||
lastPostDate?: string;
|
lastPostDate?: string
|
||||||
lastPostUserId?: string;
|
lastPostUserId?: string
|
||||||
creationTime: string;
|
creationTime: string
|
||||||
tenantId?: string;
|
tenantId?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ForumTopic {
|
export interface ForumTopic {
|
||||||
id: string;
|
id: string
|
||||||
title: string;
|
title: string
|
||||||
content: string;
|
content: string
|
||||||
categoryId: string;
|
categoryId: string
|
||||||
authorId: string;
|
authorId: string
|
||||||
authorName: string;
|
authorName: string
|
||||||
viewCount: number;
|
viewCount: number
|
||||||
replyCount: number;
|
replyCount: number
|
||||||
likeCount: number;
|
likeCount: number
|
||||||
isPinned: boolean;
|
isPinned: boolean
|
||||||
isLocked: boolean;
|
isLocked: boolean
|
||||||
isSolved: boolean;
|
isSolved: boolean
|
||||||
lastPostId?: string;
|
lastPostId?: string
|
||||||
lastPostDate?: string;
|
lastPostDate?: string
|
||||||
lastPostUserId?: string;
|
lastPostUserId?: string
|
||||||
lastPostUserName?: string;
|
lastPostUserName?: string
|
||||||
creationTime: string;
|
creationTime: string
|
||||||
tenantId?: string;
|
tenantId?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ForumPost {
|
export interface ForumPost {
|
||||||
id: string;
|
id: string
|
||||||
topicId: string;
|
topicId: string
|
||||||
content: string;
|
content: string
|
||||||
authorId: string;
|
authorId: string
|
||||||
authorName: string;
|
authorName: string
|
||||||
likeCount: number;
|
likeCount: number
|
||||||
isAcceptedAnswer: boolean;
|
isAcceptedAnswer: boolean
|
||||||
parentPostId?: string;
|
parentPostId?: string
|
||||||
creationTime: string;
|
creationTime: string
|
||||||
tenantId?: string;
|
tenantId?: string
|
||||||
|
children: ForumPost[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ViewMode = 'forum' | 'admin';
|
export type ViewMode = 'forum' | 'admin'
|
||||||
|
|
|
||||||
|
|
@ -262,292 +262,285 @@ const ClassList: React.FC = () => {
|
||||||
defaultTitle="Sözsoft Kurs Platform"
|
defaultTitle="Sözsoft Kurs Platform"
|
||||||
></Helmet>
|
></Helmet>
|
||||||
<Container>
|
<Container>
|
||||||
{/* Main Content */}
|
{/* Stats Cards */}
|
||||||
<div className="mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-5 gap-4 sm:gap-6 mb-6 sm:mb-8">
|
||||||
{/* Stats Cards */}
|
<motion.div
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-5 gap-4 sm:gap-6 mb-6 sm:mb-8">
|
initial={{ opacity: 0, y: 20 }}
|
||||||
<motion.div
|
animate={{ opacity: 1, y: 0 }}
|
||||||
initial={{ opacity: 0, y: 20 }}
|
className="bg-white rounded-lg shadow-md p-4 sm:p-6"
|
||||||
animate={{ opacity: 1, y: 0 }}
|
>
|
||||||
className="bg-white rounded-lg shadow-md p-4 sm:p-6"
|
<div className="flex items-center">
|
||||||
>
|
<div className="p-2 sm:p-3 bg-blue-100 rounded-full">
|
||||||
<div className="flex items-center">
|
<FaCalendarAlt className="text-blue-600" size={20} />
|
||||||
<div className="p-2 sm:p-3 bg-blue-100 rounded-full">
|
|
||||||
<FaCalendarAlt className="text-blue-600" size={20} />
|
|
||||||
</div>
|
|
||||||
<div className="ml-3 sm:ml-4">
|
|
||||||
<p className="text-xs sm:text-sm font-medium text-gray-600">Toplam Sınıf</p>
|
|
||||||
<p className="text-xl sm:text-2xl font-bold text-gray-900">
|
|
||||||
{widgets().totalCount}{' '}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
<div className="ml-3 sm:ml-4">
|
||||||
|
<p className="text-xs sm:text-sm font-medium text-gray-600">Toplam Sınıf</p>
|
||||||
{/* Aktif Sınıf */}
|
<p className="text-xl sm:text-2xl font-bold text-gray-900">
|
||||||
<motion.div
|
{widgets().totalCount}{' '}
|
||||||
initial={{ opacity: 0, y: 20 }}
|
</p>
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ delay: 0.1 }}
|
|
||||||
className="bg-white rounded-lg shadow-md p-4 sm:p-6"
|
|
||||||
>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<div className="p-2 sm:p-3 bg-green-100 rounded-full">
|
|
||||||
<FaPlay className="text-green-600" size={20} />
|
|
||||||
</div>
|
|
||||||
<div className="ml-3 sm:ml-4">
|
|
||||||
<p className="text-xs sm:text-sm font-medium text-gray-600">Aktif Sınıf</p>
|
|
||||||
<p className="text-xl sm:text-2xl font-bold text-gray-900">
|
|
||||||
{widgets().activeCount}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
{/* Katılıma Açık */}
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ delay: 0.15 }}
|
|
||||||
className="bg-white rounded-lg shadow-md p-4 sm:p-6"
|
|
||||||
>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<div className="p-2 sm:p-3 bg-blue-100 rounded-full">
|
|
||||||
<FaDoorOpen className="text-blue-600" size={20} />
|
|
||||||
</div>
|
|
||||||
<div className="ml-3 sm:ml-4">
|
|
||||||
<p className="text-xs sm:text-sm font-medium text-gray-600">Katılıma Açık</p>
|
|
||||||
<p className="text-xl sm:text-2xl font-bold text-gray-900">
|
|
||||||
{widgets().openCount}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
{/* Pasif Sınıf */}
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ delay: 0.2 }}
|
|
||||||
className="bg-white rounded-lg shadow-md p-4 sm:p-6"
|
|
||||||
>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<div className="p-2 sm:p-3 bg-gray-100 rounded-full">
|
|
||||||
<FaHourglassEnd className="text-gray-600" size={20} />
|
|
||||||
</div>
|
|
||||||
<div className="ml-3 sm:ml-4">
|
|
||||||
<p className="text-xs sm:text-sm font-medium text-gray-600">Pasif Sınıf</p>
|
|
||||||
<p className="text-xl sm:text-2xl font-bold text-gray-900">
|
|
||||||
{widgets().passiveCount}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
{/* Toplam Katılımcı */}
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ delay: 0.2 }}
|
|
||||||
className="bg-white rounded-lg shadow-md p-4 sm:p-6 sm:col-span-2 lg:col-span-1"
|
|
||||||
>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<div className="p-2 sm:p-3 bg-purple-100 rounded-full">
|
|
||||||
<FaUsers className="text-purple-600" size={20} />
|
|
||||||
</div>
|
|
||||||
<div className="ml-3 sm:ml-4">
|
|
||||||
<p className="text-xs sm:text-sm font-medium text-gray-600">Toplam Katılımcı</p>
|
|
||||||
<p className="text-xl sm:text-2xl font-bold text-gray-900">
|
|
||||||
{classList.reduce((sum, c) => sum + c.participantCount, 0)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Filter Bar */}
|
|
||||||
<div className="bg-white rounded-lg border border-slate-200 p-6 mb-6 shadow-sm">
|
|
||||||
<div className="flex flex-col lg:flex-row gap-4">
|
|
||||||
<div className="flex-1 relative">
|
|
||||||
<FaSearch className="absolute left-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-slate-400" />
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
||||||
placeholder="Search class"
|
|
||||||
value={searchTerm}
|
|
||||||
onChange={(e) => setSearchTerm(e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<FaFilter className="w-5 h-5 text-slate-500" />
|
|
||||||
<select
|
|
||||||
className="ml-2 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
||||||
value={statusFilter}
|
|
||||||
onChange={(e) => setStatusFilter(e.target.value)}
|
|
||||||
style={{ minWidth: 120 }}
|
|
||||||
>
|
|
||||||
<option value="">All Status</option>
|
|
||||||
<option value="Active">Aktif</option>
|
|
||||||
<option value="Open">Katılıma Açık</option>
|
|
||||||
<option value="Passive">Pasif</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</motion.div>
|
||||||
|
|
||||||
{/* Scheduled Classes */}
|
{/* Aktif Sınıf */}
|
||||||
<div className="bg-white rounded-lg shadow-md">
|
<motion.div
|
||||||
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4 p-4 sm:px-6 border-b border-gray-200">
|
initial={{ opacity: 0, y: 20 }}
|
||||||
<h2 className="text-lg sm:text-xl font-semibold text-gray-900">Programlı Sınıflar</h2>
|
animate={{ opacity: 1, y: 0 }}
|
||||||
{user.role === 'teacher' && (
|
transition={{ delay: 0.1 }}
|
||||||
<button
|
className="bg-white rounded-lg shadow-md p-4 sm:p-6"
|
||||||
onClick={() => setShowCreateModal(true)}
|
>
|
||||||
className="flex items-center justify-center space-x-2 bg-blue-600 text-white px-3 sm:px-6 py-2 rounded-lg hover:bg-blue-700 transition-colors sm:w-auto"
|
<div className="flex items-center">
|
||||||
>
|
<div className="p-2 sm:p-3 bg-green-100 rounded-full">
|
||||||
<FaPlus size={15} />
|
<FaPlay className="text-green-600" size={20} />
|
||||||
<span className="hidden sm:inline">Yeni Sınıf Oluştur</span>
|
</div>
|
||||||
<span className="sm:hidden">Yeni Sınıf</span>
|
<div className="ml-3 sm:ml-4">
|
||||||
</button>
|
<p className="text-xs sm:text-sm font-medium text-gray-600">Aktif Sınıf</p>
|
||||||
)}
|
<p className="text-xl sm:text-2xl font-bold text-gray-900">
|
||||||
|
{widgets().activeCount}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-4 sm:p-6">
|
</motion.div>
|
||||||
{classList.length === 0 ? (
|
|
||||||
<div className="text-center py-12">
|
{/* Katılıma Açık */}
|
||||||
<FaCalendarAlt size={48} className="mx-auto text-gray-400 mb-4" />
|
<motion.div
|
||||||
<p className="text-gray-500">Henüz programlanmış sınıf bulunmamaktadır.</p>
|
initial={{ opacity: 0, y: 20 }}
|
||||||
</div>
|
animate={{ opacity: 1, y: 0 }}
|
||||||
) : (
|
transition={{ delay: 0.15 }}
|
||||||
<div className="grid gap-4 sm:gap-6">
|
className="bg-white rounded-lg shadow-md p-4 sm:p-6"
|
||||||
{classList.map((classSession, index) => {
|
>
|
||||||
const { status, className, showButtons, title, classes, event } =
|
<div className="flex items-center">
|
||||||
getClassProps(classSession)
|
<div className="p-2 sm:p-3 bg-blue-100 rounded-full">
|
||||||
return (
|
<FaDoorOpen className="text-blue-600" size={20} />
|
||||||
<motion.div
|
</div>
|
||||||
key={classSession.id}
|
<div className="ml-3 sm:ml-4">
|
||||||
initial={{ opacity: 0, x: -20 }}
|
<p className="text-xs sm:text-sm font-medium text-gray-600">Katılıma Açık</p>
|
||||||
animate={{ opacity: 1, x: 0 }}
|
<p className="text-xl sm:text-2xl font-bold text-gray-900">{widgets().openCount}</p>
|
||||||
transition={{ delay: index * 0.1 }}
|
</div>
|
||||||
className="border border-gray-200 rounded-lg p-4 sm:p-6 hover:shadow-md transition-shadow"
|
</div>
|
||||||
>
|
</motion.div>
|
||||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between">
|
|
||||||
<div className="flex items-center space-x-3">
|
{/* Pasif Sınıf */}
|
||||||
<h3 className="text-base sm:text-lg font-semibold text-gray-900 break-words">
|
<motion.div
|
||||||
{classSession.name}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
</h3>
|
animate={{ opacity: 1, y: 0 }}
|
||||||
<span
|
transition={{ delay: 0.2 }}
|
||||||
className={`px-2 py-1 rounded-full text-xs font-medium ${className}`}
|
className="bg-white rounded-lg shadow-md p-4 sm:p-6"
|
||||||
|
>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="p-2 sm:p-3 bg-gray-100 rounded-full">
|
||||||
|
<FaHourglassEnd className="text-gray-600" size={20} />
|
||||||
|
</div>
|
||||||
|
<div className="ml-3 sm:ml-4">
|
||||||
|
<p className="text-xs sm:text-sm font-medium text-gray-600">Pasif Sınıf</p>
|
||||||
|
<p className="text-xl sm:text-2xl font-bold text-gray-900">
|
||||||
|
{widgets().passiveCount}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Toplam Katılımcı */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: 0.2 }}
|
||||||
|
className="bg-white rounded-lg shadow-md p-4 sm:p-6 sm:col-span-2 lg:col-span-1"
|
||||||
|
>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="p-2 sm:p-3 bg-purple-100 rounded-full">
|
||||||
|
<FaUsers className="text-purple-600" size={20} />
|
||||||
|
</div>
|
||||||
|
<div className="ml-3 sm:ml-4">
|
||||||
|
<p className="text-xs sm:text-sm font-medium text-gray-600">Toplam Katılımcı</p>
|
||||||
|
<p className="text-xl sm:text-2xl font-bold text-gray-900">
|
||||||
|
{classList.reduce((sum, c) => sum + c.participantCount, 0)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Filter Bar */}
|
||||||
|
<div className="bg-white rounded-lg border border-slate-200 p-6 mb-6 shadow-sm">
|
||||||
|
<div className="flex flex-col lg:flex-row gap-4">
|
||||||
|
<div className="flex-1 relative">
|
||||||
|
<FaSearch className="absolute left-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-slate-400" />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
|
placeholder="Search class"
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<FaFilter className="w-5 h-5 text-slate-500" />
|
||||||
|
<select
|
||||||
|
className="ml-2 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
|
value={statusFilter}
|
||||||
|
onChange={(e) => setStatusFilter(e.target.value)}
|
||||||
|
style={{ minWidth: 120 }}
|
||||||
|
>
|
||||||
|
<option value="">All Status</option>
|
||||||
|
<option value="Active">Aktif</option>
|
||||||
|
<option value="Open">Katılıma Açık</option>
|
||||||
|
<option value="Passive">Pasif</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Scheduled Classes */}
|
||||||
|
<div className="bg-white rounded-lg shadow-md">
|
||||||
|
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4 p-4 sm:px-6 border-b border-gray-200">
|
||||||
|
<h2 className="text-lg sm:text-xl font-semibold text-gray-900">Programlı Sınıflar</h2>
|
||||||
|
{user.role === 'teacher' && (
|
||||||
|
<button
|
||||||
|
onClick={() => setShowCreateModal(true)}
|
||||||
|
className="flex items-center justify-center space-x-2 bg-blue-600 text-white px-3 sm:px-6 py-2 rounded-lg hover:bg-blue-700 transition-colors sm:w-auto"
|
||||||
|
>
|
||||||
|
<FaPlus size={15} />
|
||||||
|
<span className="hidden sm:inline">Yeni Sınıf Oluştur</span>
|
||||||
|
<span className="sm:hidden">Yeni Sınıf</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="p-4 sm:p-6">
|
||||||
|
{classList.length === 0 ? (
|
||||||
|
<div className="text-center py-12">
|
||||||
|
<FaCalendarAlt size={48} className="mx-auto text-gray-400 mb-4" />
|
||||||
|
<p className="text-gray-500">Henüz programlanmış sınıf bulunmamaktadır.</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="grid gap-4 sm:gap-6">
|
||||||
|
{classList.map((classSession, index) => {
|
||||||
|
const { status, className, showButtons, title, classes, event } =
|
||||||
|
getClassProps(classSession)
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
key={classSession.id}
|
||||||
|
initial={{ opacity: 0, x: -20 }}
|
||||||
|
animate={{ opacity: 1, x: 0 }}
|
||||||
|
transition={{ delay: index * 0.1 }}
|
||||||
|
className="border border-gray-200 rounded-lg p-4 sm:p-6 hover:shadow-md transition-shadow"
|
||||||
|
>
|
||||||
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between">
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
<h3 className="text-base sm:text-lg font-semibold text-gray-900 break-words">
|
||||||
|
{classSession.name}
|
||||||
|
</h3>
|
||||||
|
<span
|
||||||
|
className={`px-2 py-1 rounded-full text-xs font-medium ${className}`}
|
||||||
|
>
|
||||||
|
{status}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Sağ kısım: buton */}
|
||||||
|
{showButtons && (
|
||||||
|
<div className="flex space-x-2">
|
||||||
|
{user.role === 'teacher' && classSession.teacherId === user.id && (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
onClick={() => handlePlanningClass(classSession)}
|
||||||
|
disabled={classSession.actualStartTime ? true : false}
|
||||||
|
className="flex px-3 sm:px-4 py-2 rounded-lg bg-yellow-600 text-white
|
||||||
|
hover:bg-yellow-700
|
||||||
|
disabled:bg-gray-400 disabled:cursor-not-allowed disabled:hover:bg-gray-400"
|
||||||
|
title="Sınıfı Planla"
|
||||||
|
>
|
||||||
|
<FaUsers size={14} />
|
||||||
|
Planlama
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => openEditModal(classSession)}
|
||||||
|
disabled={classSession.actualStartTime ? true : false}
|
||||||
|
className="flex px-3 sm:px-4 py-2 rounded-lg bg-blue-600 text-white
|
||||||
|
hover:bg-blue-700
|
||||||
|
disabled:bg-gray-400 disabled:cursor-not-allowed disabled:hover:bg-gray-400"
|
||||||
|
title="Sınıfı Düzenle"
|
||||||
|
>
|
||||||
|
<FaEdit size={14} />
|
||||||
|
Düzenle
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => openDeleteModal(classSession)}
|
||||||
|
disabled={classSession.actualStartTime ? true : false}
|
||||||
|
className="flex px-3 sm:px-4 py-2 rounded-lg bg-red-600 text-white
|
||||||
|
hover:bg-red-700
|
||||||
|
disabled:bg-gray-400 disabled:cursor-not-allowed disabled:hover:bg-gray-400"
|
||||||
|
title="Sınıfı Sil"
|
||||||
|
>
|
||||||
|
<FaTrash size={14} />
|
||||||
|
Sil
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={event}
|
||||||
|
disabled={status === 'Katılıma Açık' ? true : false}
|
||||||
|
className={`px-3 sm:px-4 py-2 rounded-lg transition-colors ${
|
||||||
|
classes
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
{status}
|
{title}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-4">
|
||||||
|
<p className="text-gray-600 text-sm sm:text-base">{classSession.subject}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-4">
|
||||||
|
<sub className="text-gray-500 mb-3 text-xs sm:text-sm">
|
||||||
|
{classSession.description}
|
||||||
|
</sub>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-4">
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 md:gap-3 w-full text-xs sm:text-sm text-gray-600">
|
||||||
|
<div className="col-span-1 flex items-center gap-2 p-1 rounded-lg">
|
||||||
|
<FaCalendarAlt size={14} className="text-gray-500" />
|
||||||
|
<span className="truncate">
|
||||||
|
{showDbDateAsIs(classSession.scheduledStartTime)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Sağ kısım: buton */}
|
<div className="col-span-1 flex items-center gap-2 p-1 rounded-lg">
|
||||||
{showButtons && (
|
<FaClock size={14} className="text-gray-500" />
|
||||||
<div className="flex space-x-2">
|
<span>{classSession.duration} dakika</span>
|
||||||
{user.role === 'teacher' && classSession.teacherId === user.id && (
|
</div>
|
||||||
<>
|
|
||||||
<button
|
|
||||||
onClick={() => handlePlanningClass(classSession)}
|
|
||||||
disabled={classSession.actualStartTime ? true : false}
|
|
||||||
className="flex px-3 sm:px-4 py-2 rounded-lg bg-yellow-600 text-white
|
|
||||||
hover:bg-yellow-700
|
|
||||||
disabled:bg-gray-400 disabled:cursor-not-allowed disabled:hover:bg-gray-400"
|
|
||||||
title="Sınıfı Planla"
|
|
||||||
>
|
|
||||||
<FaUsers size={14} />
|
|
||||||
Planlama
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
<div className="col-span-1 flex items-center gap-2 p-1 rounded-lg">
|
||||||
onClick={() => openEditModal(classSession)}
|
{classSession.scheduledEndTime && (
|
||||||
disabled={classSession.actualStartTime ? true : false}
|
<>
|
||||||
className="flex px-3 sm:px-4 py-2 rounded-lg bg-blue-600 text-white
|
<FaEye size={14} className="text-gray-500" />
|
||||||
hover:bg-blue-700
|
<span className="truncate">
|
||||||
disabled:bg-gray-400 disabled:cursor-not-allowed disabled:hover:bg-gray-400"
|
{showDbDateAsIs(classSession.scheduledEndTime!)}
|
||||||
title="Sınıfı Düzenle"
|
</span>
|
||||||
>
|
</>
|
||||||
<FaEdit size={14} />
|
)}
|
||||||
Düzenle
|
</div>
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
<div className="col-span-1 flex items-center gap-2 p-1 rounded-lg">
|
||||||
onClick={() => openDeleteModal(classSession)}
|
<FaUsers size={14} className="text-gray-500" />
|
||||||
disabled={classSession.actualStartTime ? true : false}
|
<span>
|
||||||
className="flex px-3 sm:px-4 py-2 rounded-lg bg-red-600 text-white
|
{classSession.participantCount}/{classSession.maxParticipants}
|
||||||
hover:bg-red-700
|
</span>
|
||||||
disabled:bg-gray-400 disabled:cursor-not-allowed disabled:hover:bg-gray-400"
|
|
||||||
title="Sınıfı Sil"
|
|
||||||
>
|
|
||||||
<FaTrash size={14} />
|
|
||||||
Sil
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={event}
|
|
||||||
disabled={status === 'Katılıma Açık' ? true : false}
|
|
||||||
className={`px-3 sm:px-4 py-2 rounded-lg transition-colors ${
|
|
||||||
classes
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{title}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-4">
|
|
||||||
<p className="text-gray-600 text-sm sm:text-base">
|
|
||||||
{classSession.subject}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-4">
|
|
||||||
<sub className="text-gray-500 mb-3 text-xs sm:text-sm">
|
|
||||||
{classSession.description}
|
|
||||||
</sub>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-4">
|
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 md:gap-3 w-full text-xs sm:text-sm text-gray-600">
|
|
||||||
<div className="col-span-1 flex items-center gap-2 p-1 rounded-lg">
|
|
||||||
<FaCalendarAlt size={14} className="text-gray-500" />
|
|
||||||
<span className="truncate">
|
|
||||||
{showDbDateAsIs(classSession.scheduledStartTime)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="col-span-1 flex items-center gap-2 p-1 rounded-lg">
|
|
||||||
<FaClock size={14} className="text-gray-500" />
|
|
||||||
<span>{classSession.duration} dakika</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="col-span-1 flex items-center gap-2 p-1 rounded-lg">
|
|
||||||
{classSession.scheduledEndTime && (
|
|
||||||
<>
|
|
||||||
<FaEye size={14} className="text-gray-500" />
|
|
||||||
<span className="truncate">
|
|
||||||
{showDbDateAsIs(classSession.scheduledEndTime!)}
|
|
||||||
</span>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="col-span-1 flex items-center gap-2 p-1 rounded-lg">
|
|
||||||
<FaUsers size={14} className="text-gray-500" />
|
|
||||||
<span>
|
|
||||||
{classSession.participantCount}/{classSession.maxParticipants}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</div>
|
||||||
)
|
</motion.div>
|
||||||
})}
|
)
|
||||||
</div>
|
})}
|
||||||
)}
|
</div>
|
||||||
</div>
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -101,79 +101,77 @@ export function AdminView({
|
||||||
]
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
<div className="flex flex-col lg:flex-row gap-8">
|
||||||
<div className="flex flex-col lg:flex-row gap-8">
|
{/* Sidebar Navigation */}
|
||||||
{/* Sidebar Navigation */}
|
<div className="lg:w-64 flex-shrink-0 p-4 bg-gray-50">
|
||||||
<div className="lg:w-64 flex-shrink-0 p-4 bg-gray-50">
|
<nav className="space-y-2">
|
||||||
<nav className="space-y-2">
|
{navigationItems.map((item) => {
|
||||||
{navigationItems.map((item) => {
|
const Icon = item.icon
|
||||||
const Icon = item.icon
|
return (
|
||||||
return (
|
<button
|
||||||
<button
|
key={item.id}
|
||||||
key={item.id}
|
onClick={() => setActiveSection(item.id)}
|
||||||
onClick={() => setActiveSection(item.id)}
|
className={`w-full flex items-center space-x-3 px-4 py-3 rounded-lg text-left transition-colors ${
|
||||||
className={`w-full flex items-center space-x-3 px-4 py-3 rounded-lg text-left transition-colors ${
|
activeSection === item.id
|
||||||
activeSection === item.id
|
? 'bg-blue-100 text-blue-700'
|
||||||
? 'bg-blue-100 text-blue-700'
|
: 'text-gray-600 hover:bg-gray-100 hover:text-gray-900'
|
||||||
: 'text-gray-600 hover:bg-gray-100 hover:text-gray-900'
|
}`}
|
||||||
}`}
|
>
|
||||||
>
|
<Icon className="w-5 h-5" />
|
||||||
<Icon className="w-5 h-5" />
|
<span className="font-medium">{item.label}</span>
|
||||||
<span className="font-medium">{item.label}</span>
|
</button>
|
||||||
</button>
|
)
|
||||||
)
|
})}
|
||||||
})}
|
</nav>
|
||||||
</nav>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Main Content */}
|
{/* Main Content */}
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
{activeSection === 'stats' && (
|
{activeSection === 'stats' && (
|
||||||
<AdminStats categories={categories} topics={topics} posts={posts} />
|
<AdminStats categories={categories} topics={topics} posts={posts} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{activeSection === 'categories' && (
|
{activeSection === 'categories' && (
|
||||||
<CategoryManagement
|
<CategoryManagement
|
||||||
categories={categories}
|
categories={categories}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
onCreateCategory={onCreateCategory}
|
onCreateCategory={onCreateCategory}
|
||||||
onUpdateCategory={onUpdateCategory}
|
onUpdateCategory={onUpdateCategory}
|
||||||
onDeleteCategory={onDeleteCategory}
|
onDeleteCategory={onDeleteCategory}
|
||||||
onUpdateCategoryLockState={onUpdateCategoryLockState}
|
onUpdateCategoryLockState={onUpdateCategoryLockState}
|
||||||
onUpdateCategoryActiveState={onUpdateCategoryActiveState}
|
onUpdateCategoryActiveState={onUpdateCategoryActiveState}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{activeSection === 'topics' && (
|
{activeSection === 'topics' && (
|
||||||
<TopicManagement
|
<TopicManagement
|
||||||
topics={topics}
|
topics={topics}
|
||||||
categories={categories}
|
categories={categories}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
onCreateTopic={onCreateTopic}
|
onCreateTopic={onCreateTopic}
|
||||||
onUpdateTopic={onUpdateTopic}
|
onUpdateTopic={onUpdateTopic}
|
||||||
onDeleteTopic={onDeleteTopic}
|
onDeleteTopic={onDeleteTopic}
|
||||||
onPinTopic={onPinTopic}
|
onPinTopic={onPinTopic}
|
||||||
onUnpinTopic={onUnpinTopic}
|
onUnpinTopic={onUnpinTopic}
|
||||||
onLockTopic={onLockTopic}
|
onLockTopic={onLockTopic}
|
||||||
onUnlockTopic={onUnlockTopic}
|
onUnlockTopic={onUnlockTopic}
|
||||||
onMarkTopicAsSolved={onMarkTopicAsSolved}
|
onMarkTopicAsSolved={onMarkTopicAsSolved}
|
||||||
onMarkTopicAsUnsolved={onMarkTopicAsUnsolved}
|
onMarkTopicAsUnsolved={onMarkTopicAsUnsolved}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{activeSection === 'posts' && (
|
{activeSection === 'posts' && (
|
||||||
<PostManagement
|
<PostManagement
|
||||||
posts={posts}
|
posts={posts}
|
||||||
topics={topics}
|
topics={topics}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
onCreatePost={onCreatePost}
|
onCreatePost={onCreatePost}
|
||||||
onUpdatePost={onUpdatePost}
|
onUpdatePost={onUpdatePost}
|
||||||
onDeletePost={onDeletePost}
|
onDeletePost={onDeletePost}
|
||||||
onMarkPostAsAcceptedAnswer={onMarkPostAsAcceptedAnswer}
|
onMarkPostAsAcceptedAnswer={onMarkPostAsAcceptedAnswer}
|
||||||
onUnmarkPostAsAcceptedAnswer={onUnmarkPostAsAcceptedAnswer}
|
onUnmarkPostAsAcceptedAnswer={onUnmarkPostAsAcceptedAnswer}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import { FaArrowLeft, FaPlus, FaSpinner, FaSearch } from 'react-icons/fa';
|
import { FaArrowLeft, FaPlus, FaSpinner, FaSearch } from 'react-icons/fa'
|
||||||
import { CreateTopicModal } from './CreateTopicModal'
|
import { CreateTopicModal } from './CreateTopicModal'
|
||||||
import { CreatePostModal } from './CreatePostModal'
|
import { CreatePostModal } from './CreatePostModal'
|
||||||
import { ForumCategory, ForumPost, ForumTopic } from '@/proxy/forum/forum'
|
import { ForumCategory, ForumPost, ForumTopic } from '@/proxy/forum/forum'
|
||||||
|
|
@ -238,7 +238,7 @@ export function ForumView({
|
||||||
|
|
||||||
const threadedPosts = buildPostTree(filteredPosts)
|
const threadedPosts = buildPostTree(filteredPosts)
|
||||||
|
|
||||||
function renderPosts(posts: (ForumPost & { children: ForumPost[] })[]) {
|
function renderPosts(posts: ForumPost[]) {
|
||||||
return posts.map((post) => (
|
return posts.map((post) => (
|
||||||
<div key={post.id}>
|
<div key={post.id}>
|
||||||
<ForumPostCard
|
<ForumPostCard
|
||||||
|
|
@ -301,191 +301,189 @@ export function ForumView({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
{/* Breadcrumb + Actions + Search Row */}
|
||||||
{/* Breadcrumb + Actions + Search Row */}
|
<div className="flex items-center justify-between mb-4">
|
||||||
<div className="flex items-center justify-between mb-4">
|
{/* Left Side: Breadcrumb */}
|
||||||
{/* Left Side: Breadcrumb */}
|
<div className="flex items-center space-x-2">
|
||||||
<div className="flex items-center space-x-2">
|
{viewState !== 'categories' && (
|
||||||
{viewState !== 'categories' && (
|
|
||||||
<button
|
|
||||||
onClick={handleBack}
|
|
||||||
className="flex items-center space-x-1 text-blue-600 hover:text-blue-700 transition-colors"
|
|
||||||
>
|
|
||||||
<FaArrowLeft className="w-4 h-4" />
|
|
||||||
<span>Back</span>
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
<nav className="flex items-center space-x-2 text-sm text-gray-500">
|
|
||||||
<button
|
|
||||||
onClick={() => handleBreadcrumbClick('forum')}
|
|
||||||
className={`transition-colors ${
|
|
||||||
viewState === 'categories'
|
|
||||||
? 'text-gray-900 font-medium cursor-default'
|
|
||||||
: 'hover:text-blue-600 cursor-pointer'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
Forum
|
|
||||||
</button>
|
|
||||||
{selectedCategory && (
|
|
||||||
<>
|
|
||||||
<span>/</span>
|
|
||||||
<button
|
|
||||||
onClick={() => handleBreadcrumbClick('category')}
|
|
||||||
className={`transition-colors ${
|
|
||||||
viewState === 'topics'
|
|
||||||
? 'text-gray-900 font-medium cursor-default'
|
|
||||||
: 'hover:text-blue-600 cursor-pointer'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{selectedCategory.name}
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{selectedTopic && (
|
|
||||||
<>
|
|
||||||
<span>/</span>
|
|
||||||
<span className="text-gray-900 font-medium">{selectedTopic.title}</span>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Right Side: Actions + Search */}
|
|
||||||
<div className="flex items-center space-x-2 ml-auto">
|
|
||||||
{viewState === 'topics' && selectedCategory && !selectedCategory.isLocked && (
|
|
||||||
<button
|
|
||||||
onClick={() => setShowCreateTopic(true)}
|
|
||||||
className="flex items-center space-x-2 bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition-colors"
|
|
||||||
>
|
|
||||||
<FaPlus className="w-4 h-4" />
|
|
||||||
<span>{translate('::App.Forum.TopicManagement.NewTopic')}</span>
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
{viewState === 'posts' && selectedTopic && !selectedTopic.isLocked && (
|
|
||||||
<button
|
|
||||||
onClick={() => setShowCreatePost(true)}
|
|
||||||
className="flex items-center space-x-2 bg-emerald-600 text-white px-4 py-2 rounded-lg hover:bg-emerald-700 transition-colors"
|
|
||||||
>
|
|
||||||
<FaPlus className="w-4 h-4" />
|
|
||||||
<span>{translate('::App.Forum.PostManagement.NewPost')}</span>
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Search */}
|
|
||||||
<button
|
<button
|
||||||
onClick={() => setIsSearchModalOpen(true)}
|
onClick={handleBack}
|
||||||
className="hidden md:flex items-center space-x-2 px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors"
|
className="flex items-center space-x-1 text-blue-600 hover:text-blue-700 transition-colors"
|
||||||
>
|
>
|
||||||
<FaSearch className="w-4 h-4 text-gray-400" />
|
<FaArrowLeft className="w-4 h-4" />
|
||||||
<span className="text-gray-500">{translate('::App.Forum.TopicManagement.Searchtopics')}</span>
|
<span>Back</span>
|
||||||
<kbd className="hidden sm:inline-block px-2 py-1 text-xs font-semibold text-gray-500 bg-gray-100 border border-gray-200 rounded">
|
|
||||||
⌘K
|
|
||||||
</kbd>
|
|
||||||
</button>
|
</button>
|
||||||
|
)}
|
||||||
|
<nav className="flex items-center space-x-2 text-sm text-gray-500">
|
||||||
<button
|
<button
|
||||||
onClick={() => setIsSearchModalOpen(true)}
|
onClick={() => handleBreadcrumbClick('forum')}
|
||||||
className="md:hidden p-2 text-gray-400 hover:text-gray-600 transition-colors"
|
className={`transition-colors ${
|
||||||
|
viewState === 'categories'
|
||||||
|
? 'text-gray-900 font-medium cursor-default'
|
||||||
|
: 'hover:text-blue-600 cursor-pointer'
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
<FaSearch className="w-5 h-5" />
|
Forum
|
||||||
</button>
|
</button>
|
||||||
</div>
|
{selectedCategory && (
|
||||||
|
<>
|
||||||
|
<span>/</span>
|
||||||
|
<button
|
||||||
|
onClick={() => handleBreadcrumbClick('category')}
|
||||||
|
className={`transition-colors ${
|
||||||
|
viewState === 'topics'
|
||||||
|
? 'text-gray-900 font-medium cursor-default'
|
||||||
|
: 'hover:text-blue-600 cursor-pointer'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{selectedCategory.name}
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{selectedTopic && (
|
||||||
|
<>
|
||||||
|
<span>/</span>
|
||||||
|
<span className="text-gray-900 font-medium">{selectedTopic.title}</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Categories View */}
|
{/* Right Side: Actions + Search */}
|
||||||
{viewState === 'categories' && (
|
<div className="flex items-center space-x-2 ml-auto">
|
||||||
<div className="space-y-6">
|
{viewState === 'topics' && selectedCategory && !selectedCategory.isLocked && (
|
||||||
<div>
|
<button
|
||||||
<div className="space-y-4">
|
onClick={() => setShowCreateTopic(true)}
|
||||||
{categories
|
className="flex items-center space-x-2 bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition-colors"
|
||||||
.filter((cat) => cat.isActive)
|
>
|
||||||
.sort((a, b) => a.displayOrder - b.displayOrder)
|
<FaPlus className="w-4 h-4" />
|
||||||
.map((category) => (
|
<span>{translate('::App.Forum.TopicManagement.NewTopic')}</span>
|
||||||
<ForumCategoryCard
|
</button>
|
||||||
key={category.id}
|
)}
|
||||||
category={category}
|
{viewState === 'posts' && selectedTopic && !selectedTopic.isLocked && (
|
||||||
onClick={() => handleCategoryClick(category)}
|
<button
|
||||||
/>
|
onClick={() => setShowCreatePost(true)}
|
||||||
))}
|
className="flex items-center space-x-2 bg-emerald-600 text-white px-4 py-2 rounded-lg hover:bg-emerald-700 transition-colors"
|
||||||
</div>
|
>
|
||||||
</div>
|
<FaPlus className="w-4 h-4" />
|
||||||
</div>
|
<span>{translate('::App.Forum.PostManagement.NewPost')}</span>
|
||||||
)}
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Topics View */}
|
{/* Search */}
|
||||||
{viewState === 'topics' && selectedCategory && (
|
<button
|
||||||
<div className="space-y-6">
|
onClick={() => setIsSearchModalOpen(true)}
|
||||||
<div>
|
className="hidden md:flex items-center space-x-2 px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors"
|
||||||
<h2 className="text-2xl font-bold text-gray-900 mb-6">{selectedCategory.name}</h2>
|
>
|
||||||
<div className="space-y-4">
|
<FaSearch className="w-4 h-4 text-gray-400" />
|
||||||
{filteredTopics
|
<span className="text-gray-500">
|
||||||
.sort((a, b) => {
|
{translate('::App.Forum.TopicManagement.Searchtopics')}
|
||||||
if (a.isPinned && !b.isPinned) return -1
|
</span>
|
||||||
if (!a.isPinned && b.isPinned) return 1
|
<kbd className="hidden sm:inline-block px-2 py-1 text-xs font-semibold text-gray-500 bg-gray-100 border border-gray-200 rounded">
|
||||||
return new Date(b.creationTime).getTime() - new Date(a.creationTime).getTime()
|
⌘K
|
||||||
})
|
</kbd>
|
||||||
.map((topic) => (
|
</button>
|
||||||
<ForumTopicCard
|
|
||||||
key={topic.id}
|
|
||||||
topic={topic}
|
|
||||||
onClick={() => handleTopicClick(topic)}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Posts View */}
|
<button
|
||||||
{viewState === 'posts' && selectedTopic && (
|
onClick={() => setIsSearchModalOpen(true)}
|
||||||
<div className="space-y-6">
|
className="md:hidden p-2 text-gray-400 hover:text-gray-600 transition-colors"
|
||||||
<div>
|
>
|
||||||
<h2 className="text-2xl font-bold text-gray-900 mb-6">{selectedTopic.title}</h2>
|
<FaSearch className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
{/* Topic Ana İçeriği */}
|
</div>
|
||||||
<ForumPostCard
|
|
||||||
post={{
|
|
||||||
id: selectedTopic.id,
|
|
||||||
topicId: selectedTopic.id,
|
|
||||||
content: selectedTopic.content,
|
|
||||||
authorId: selectedTopic.authorId,
|
|
||||||
authorName: selectedTopic.authorName,
|
|
||||||
likeCount: postLikeCounts[selectedTopic.id] ?? selectedTopic.likeCount,
|
|
||||||
isAcceptedAnswer: false,
|
|
||||||
parentPostId: undefined,
|
|
||||||
creationTime: selectedTopic.creationTime,
|
|
||||||
tenantId: selectedTopic.tenantId,
|
|
||||||
}}
|
|
||||||
onLike={handleLike}
|
|
||||||
onReply={handleReply}
|
|
||||||
isFirst={true}
|
|
||||||
isLiked={likedPosts.has(selectedTopic.id)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Hiyerarşik Postlar */}
|
|
||||||
<div className="mt-4 space-y-4">{renderPosts(threadedPosts)}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Create Topic Modal */}
|
|
||||||
{showCreateTopic && (
|
|
||||||
<CreateTopicModal
|
|
||||||
onClose={() => setShowCreateTopic(false)}
|
|
||||||
onSubmit={handleCreateTopic}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Create Post Modal */}
|
|
||||||
{showCreatePost && (
|
|
||||||
<CreatePostModal
|
|
||||||
onClose={() => setShowCreatePost(false)}
|
|
||||||
onSubmit={handleCreatePost}
|
|
||||||
parentPostId={replyToPostId!}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Categories View */}
|
||||||
|
{viewState === 'categories' && (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div>
|
||||||
|
<div className="space-y-4">
|
||||||
|
{categories
|
||||||
|
.filter((cat) => cat.isActive)
|
||||||
|
.sort((a, b) => a.displayOrder - b.displayOrder)
|
||||||
|
.map((category) => (
|
||||||
|
<ForumCategoryCard
|
||||||
|
key={category.id}
|
||||||
|
category={category}
|
||||||
|
onClick={() => handleCategoryClick(category)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Topics View */}
|
||||||
|
{viewState === 'topics' && selectedCategory && (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-2xl font-bold text-gray-900 mb-6">{selectedCategory.name}</h2>
|
||||||
|
<div className="space-y-4">
|
||||||
|
{filteredTopics
|
||||||
|
.sort((a, b) => {
|
||||||
|
if (a.isPinned && !b.isPinned) return -1
|
||||||
|
if (!a.isPinned && b.isPinned) return 1
|
||||||
|
return new Date(b.creationTime).getTime() - new Date(a.creationTime).getTime()
|
||||||
|
})
|
||||||
|
.map((topic) => (
|
||||||
|
<ForumTopicCard
|
||||||
|
key={topic.id}
|
||||||
|
topic={topic}
|
||||||
|
onClick={() => handleTopicClick(topic)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Posts View */}
|
||||||
|
{viewState === 'posts' && selectedTopic && (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-2xl font-bold text-gray-900 mb-6">{selectedTopic.title}</h2>
|
||||||
|
|
||||||
|
{/* Topic Ana İçeriği */}
|
||||||
|
<ForumPostCard
|
||||||
|
post={{
|
||||||
|
id: selectedTopic.id,
|
||||||
|
topicId: selectedTopic.id,
|
||||||
|
content: selectedTopic.content,
|
||||||
|
authorId: selectedTopic.authorId,
|
||||||
|
authorName: selectedTopic.authorName,
|
||||||
|
likeCount: postLikeCounts[selectedTopic.id] ?? selectedTopic.likeCount,
|
||||||
|
isAcceptedAnswer: false,
|
||||||
|
parentPostId: undefined,
|
||||||
|
creationTime: selectedTopic.creationTime,
|
||||||
|
tenantId: selectedTopic.tenantId,
|
||||||
|
children: [],
|
||||||
|
}}
|
||||||
|
onLike={handleLike}
|
||||||
|
onReply={handleReply}
|
||||||
|
isFirst={true}
|
||||||
|
isLiked={likedPosts.has(selectedTopic.id)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Hiyerarşik Postlar */}
|
||||||
|
<div className="mt-4 space-y-4">{renderPosts(threadedPosts)}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Create Topic Modal */}
|
||||||
|
{showCreateTopic && (
|
||||||
|
<CreateTopicModal onClose={() => setShowCreateTopic(false)} onSubmit={handleCreateTopic} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Create Post Modal */}
|
||||||
|
{showCreatePost && (
|
||||||
|
<CreatePostModal
|
||||||
|
onClose={() => setShowCreatePost(false)}
|
||||||
|
onSubmit={handleCreatePost}
|
||||||
|
parentPostId={replyToPostId!}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<SearchModal
|
<SearchModal
|
||||||
isOpen={isSearchModalOpen}
|
isOpen={isSearchModalOpen}
|
||||||
onClose={() => setIsSearchModalOpen(false)}
|
onClose={() => setIsSearchModalOpen(false)}
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,23 @@
|
||||||
import { ForumPost } from '@/proxy/forum/forum'
|
import { ForumPost } from '@/proxy/forum/forum'
|
||||||
|
|
||||||
export function buildPostTree(posts: ForumPost[]): (ForumPost & { children: ForumPost[] })[] {
|
export function buildPostTree(posts: ForumPost[]): ForumPost[] {
|
||||||
const postMap = new Map<string, ForumPost & { children: ForumPost[] }>();
|
const postMap = new Map<string, ForumPost>()
|
||||||
|
|
||||||
// 1. Her post için children array'i eklenmiş yeni bir nesne oluştur
|
// 1. Her post için children array'i eklenmiş yeni bir nesne oluştur
|
||||||
posts.forEach((post) => {
|
posts.forEach((post) => {
|
||||||
postMap.set(post.id, { ...post, children: [] });
|
postMap.set(post.id, { ...post, children: [] })
|
||||||
});
|
})
|
||||||
|
|
||||||
const roots: (ForumPost & { children: ForumPost[] })[] = [];
|
const roots: ForumPost[] = []
|
||||||
|
|
||||||
// 2. Her post'un parent'ı varsa ilgili parent'ın children listesine ekle
|
// 2. Her post'un parent'ı varsa ilgili parent'ın children listesine ekle
|
||||||
postMap.forEach((post) => {
|
postMap.forEach((post) => {
|
||||||
if (post.parentPostId && postMap.has(post.parentPostId)) {
|
if (post.parentPostId && postMap.has(post.parentPostId)) {
|
||||||
postMap.get(post.parentPostId)!.children.push(post);
|
postMap.get(post.parentPostId)!.children.push(post)
|
||||||
} else {
|
} else {
|
||||||
roots.push(post);
|
roots.push(post)
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
return roots;
|
return roots
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue