Genel düzenlemeler

This commit is contained in:
Sedat ÖZTÜRK 2025-09-15 12:11:50 +03:00
parent b009b520e1
commit 795ad3d00c
9 changed files with 5372 additions and 583 deletions

View file

@ -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');
})); }));

File diff suppressed because it is too large Load diff

View file

@ -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>
) )

View file

@ -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> </>
) )
} }

View file

@ -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'

View file

@ -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 ı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 ı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 ı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 ı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>

View file

@ -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>
) )

View file

@ -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)}

View file

@ -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
} }