Wizard ve Component Manager List ve Card görünümleri

This commit is contained in:
Sedat ÖZTÜRK 2026-08-20 18:20:49 +03:00
parent 4c09de9ad0
commit 90a66562e7
6 changed files with 610 additions and 325 deletions

View file

@ -10854,6 +10854,48 @@
"tr": "Görüntüle", "tr": "Görüntüle",
"en": "View" "en": "View"
}, },
{
"resourceName": "Platform",
"key": "App.Platform.CardView",
"tr": "Kart Görünümü",
"en": "Card View"
},
{
"resourceName": "Platform",
"key": "App.Platform.ListView",
"tr": "Liste Görünümü",
"en": "List View"
},
{
"resourceName": "Platform",
"key": "App.Platform.Name",
"tr": "Ad",
"en": "Name"
},
{
"resourceName": "Platform",
"key": "App.Platform.Description",
"tr": "Açıklama",
"en": "Description"
},
{
"resourceName": "Platform",
"key": "App.Platform.Actions",
"tr": "İşlemler",
"en": "Actions"
},
{
"resourceName": "Platform",
"key": "App.Platform.Dependencies",
"tr": "Bağımlılıklar",
"en": "Dependencies"
},
{
"resourceName": "Platform",
"key": "App.Platform.RoutePath",
"tr": "Rota",
"en": "Route Path"
},
{ {
"resourceName": "Platform", "resourceName": "Platform",
"key": "App.Platform.Manage2", "key": "App.Platform.Manage2",

View file

@ -18,6 +18,8 @@ import {
FaExternalLinkAlt, FaExternalLinkAlt,
FaFileExport, FaFileExport,
FaFileImport, FaFileImport,
FaTh,
FaList,
} from 'react-icons/fa' } from 'react-icons/fa'
import DbMigrateButton from '@/components/shared/DbMigrateButton' import DbMigrateButton from '@/components/shared/DbMigrateButton'
import { import {
@ -28,7 +30,8 @@ import {
} from '@/services/wizard.service' } from '@/services/wizard.service'
import { useCurrentMenuIcon } from '@/utils/hooks/useCurrentMenuIcon' import { useCurrentMenuIcon } from '@/utils/hooks/useCurrentMenuIcon'
import { useLocalization } from '@/utils/hooks/useLocalization' import { useLocalization } from '@/utils/hooks/useLocalization'
import { useStoreState } from '@/store/store' import { useStoreActions, useStoreState } from '@/store/store'
import type { ListViewLayoutType } from '../edit/types'
import { ROUTES_ENUM } from '@/routes/route.constant' import { ROUTES_ENUM } from '@/routes/route.constant'
import { WizardComponentKindEnum, WizardFileInfoDto } from '@/proxy/admin/wizard/models' import { WizardComponentKindEnum, WizardFileInfoDto } from '@/proxy/admin/wizard/models'
import { useNavigationIcons } from '@/proxy/menus/navigation-icon.config' import { useNavigationIcons } from '@/proxy/menus/navigation-icon.config'
@ -36,6 +39,9 @@ import { usePermission } from '@/utils/hooks/usePermission'
import { WIZARD_PERMISSION } from '@/constants/permission.constant' import { WIZARD_PERMISSION } from '@/constants/permission.constant'
import WizardImportDialog from './WizardImportDialog' import WizardImportDialog from './WizardImportDialog'
/** Layout tercihi diger listelerle ayni yerde (admin.lists.states) saklanir. */
const VIEW_STATE_CODE = 'developerkit-wizardmanager'
interface ConfirmState { interface ConfirmState {
fileName: string fileName: string
wizardName: string wizardName: string
@ -57,6 +63,13 @@ const WizardFileManager = () => {
const [importFile, setImportFile] = useState<File | null>(null) const [importFile, setImportFile] = useState<File | null>(null)
const importInputRef = useRef<HTMLInputElement>(null) const importInputRef = useRef<HTMLInputElement>(null)
const { states } = useStoreState((state) => state.admin.lists)
const { setStates } = useStoreActions((a) => a.admin.lists)
const viewMode: ListViewLayoutType =
states.find((s) => s.listFormCode === VIEW_STATE_CODE)?.layout === 'grid' ? 'grid' : 'card'
const setViewMode = (layout: ListViewLayoutType) =>
setStates({ listFormCode: VIEW_STATE_CODE, layout })
// Butonlar yalnizca gizlenir; asil kontrol ListFormWizardAppService'te yapilir. // Butonlar yalnizca gizlenir; asil kontrol ListFormWizardAppService'te yapilir.
const { checkPermission } = usePermission() const { checkPermission } = usePermission()
const canCreate = checkPermission(WIZARD_PERMISSION.CREATE) const canCreate = checkPermission(WIZARD_PERMISSION.CREATE)
@ -152,6 +165,72 @@ const WizardFileManager = () => {
} }
} }
const renderActions = (f: WizardFileInfoDto, className: string) => (
<div className={className}>
{!f.hasInsertedRecords && (
<span
title={translate('::App.ListForm.WizardFileNoTrackedRecords')}
className="text-yellow-500 text-xs flex items-center gap-1"
>
<FaExclamationTriangle />
</span>
)}
{canUpdate && (
<Button
type="button"
variant="solid"
color="blue-600"
title={translate('::App.Platform.Edit')}
onClick={() =>
navigate(ROUTES_ENUM.protected.saas.listFormManagement.wizard, {
state: { editFileName: f.fileName },
})
}
>
<FaEdit className="w-4 h-4" />
</Button>
)}
{/* Menünün açtığı adres yeni sekmede açılır; adres yoksa buton anlamsızdır. */}
{f.menuUrl && (
<Button
type="button"
variant="solid"
color="gray-600"
title={translate('::App.Platform.OpenUrl')}
onClick={() => window.open(f.menuUrl, '_blank', 'noopener,noreferrer')}
>
<FaExternalLinkAlt className="w-4 h-4" />
</Button>
)}
{canExport && (
<Button
type="button"
variant="solid"
color="green-600"
title={translate('::App.ListForm.WizardFileExport')}
loading={exportingFile === f.fileName}
onClick={() => handleExport(f)}
>
<FaFileExport className="w-4 h-4" />
</Button>
)}
{canDelete && (
<Button
type="button"
variant="solid"
color="red-600"
title={translate('::App.Platform.Delete')}
loading={deletingFile === f.fileName}
onClick={() =>
setConfirm({ fileName: f.fileName, wizardName: f.wizardName || f.fileName })
}
>
<FaTrash className="w-4 h-4" />
</Button>
)}
</div>
)
return ( return (
<Container> <Container>
{/* ── Header ─────────────────────────────────────────────── */} {/* ── Header ─────────────────────────────────────────────── */}
@ -227,6 +306,24 @@ const WizardFileManager = () => {
</span> </span>
</Button> </Button>
)} )}
<div className="flex items-center gap-1">
<Button
size="sm"
type="button"
icon={<FaTh />}
variant={viewMode === 'card' ? 'solid' : 'default'}
title={translate('::App.Platform.CardView')}
onClick={() => setViewMode('card')}
/>
<Button
size="sm"
type="button"
icon={<FaList />}
variant={viewMode === 'grid' ? 'solid' : 'default'}
title={translate('::App.Platform.ListView')}
onClick={() => setViewMode('grid')}
/>
</div>
</div> </div>
</div> </div>
@ -246,138 +343,149 @@ const WizardFileManager = () => {
</p> </p>
)} )}
<div className="space-y-2"> {viewMode === 'card' ? (
{filteredFiles.map((f) => { <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-3">
const Icon = navigationIcon[f.menuIcon] ?? FaDatabase {filteredFiles.map((f) => {
const isCustom = f.componentKind === WizardComponentKindEnum.Custom const Icon = navigationIcon[f.menuIcon] ?? FaDatabase
return ( const isCustom = f.componentKind === WizardComponentKindEnum.Custom
<div return (
key={f.fileName} <div
className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2 p-3 rounded-lg border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800" key={f.fileName}
> className="flex flex-col gap-2 p-3 rounded-lg border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800"
<div className="flex items-center gap-3 min-w-0"> >
<Icon className="text-indigo-400 shrink-0 text-3xl" /> <div className="flex items-start gap-2 min-w-0">
<div className="min-w-0 space-y-1"> <Icon className="text-indigo-400 shrink-0 text-2xl" />
{/* 1. satır: wizard adı */} <div className="min-w-0 flex-1 space-y-1">
<div className="font-medium text-sm text-gray-800 dark:text-gray-200 truncate"> {/* 1. satır: wizard adı */}
{f.wizardName || f.fileName} <div
</div> className="font-medium text-sm text-gray-800 dark:text-gray-200 truncate"
title={f.wizardName || f.fileName}
{/* 2. satır: dosya adı ve üretilen kod */}
<div className="flex flex-wrap items-center gap-x-2 text-xs text-gray-400">
<span className="truncate font-mono">{f.fileName}</span>
<span className="text-gray-300 dark:text-gray-600"></span>
<span className="truncate">
{isCustom ? f.customComponentName : f.listFormCode}
</span>
</div>
{/* 3. satır: yol ayrımı (Custom / List), layout ve menü adresi */}
<div className="flex flex-wrap items-center gap-1.5 text-xs">
<span
className={classNames(
'inline-flex items-center gap-1 rounded-full px-2 py-0.5 font-medium',
isCustom
? 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-300'
: 'bg-indigo-100 text-indigo-700 dark:bg-indigo-900/30 dark:text-indigo-300',
)}
> >
{isCustom ? <FaCube /> : <FaListUl />} {f.wizardName || f.fileName}
{translate( </div>
isCustom
? '::App.WizardStepComponent.CustomComponent'
: '::App.WizardStepComponent.ListComponent',
)}
</span>
{/* Custom yolunda layout kavramı yoktur. */} {/* 2. satır: dosya adı ve üretilen kod */}
{!isCustom && f.defaultLayout && ( <div className="text-xs text-gray-400 truncate font-mono" title={f.fileName}>
<span className="inline-flex items-center rounded-full bg-gray-200 px-2 py-0.5 font-medium text-gray-600 dark:bg-gray-700 dark:text-gray-300"> {f.fileName}
{f.defaultLayout} </div>
</span> <div className="text-xs text-gray-400 truncate">
)} {isCustom ? f.customComponentName : f.listFormCode}
</div>
{f.menuUrl && (
<span className="inline-flex min-w-0 items-center gap-1 rounded-full bg-gray-200 px-2 py-0.5 font-mono text-gray-600 dark:bg-gray-700 dark:text-gray-300">
<FaLink className="shrink-0" />
<span className="truncate">{f.menuUrl}</span>
</span>
)}
</div> </div>
</div> </div>
</div>
<div className="flex items-center gap-2 shrink-0 sm:ml-3"> {/* 3. satır: yol ayrımı (Custom / List), layout ve menü adresi */}
{!f.hasInsertedRecords && ( <div className="flex flex-wrap items-center gap-1.5 text-xs">
<span <span
title={translate('::App.ListForm.WizardFileNoTrackedRecords')} className={classNames(
className="text-yellow-500 text-xs flex items-center gap-1" 'inline-flex items-center gap-1 rounded-full px-2 py-0.5 font-medium',
isCustom
? 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-300'
: 'bg-indigo-100 text-indigo-700 dark:bg-indigo-900/30 dark:text-indigo-300',
)}
> >
<FaExclamationTriangle /> {isCustom ? <FaCube /> : <FaListUl />}
{translate(
isCustom
? '::App.WizardStepComponent.CustomComponent'
: '::App.WizardStepComponent.ListComponent',
)}
</span> </span>
)}
{/* Menünün açtığı adres yeni sekmede açılır; adres yoksa buton anlamsızdır. */} {/* Custom yolunda layout kavramı yoktur. */}
{f.menuUrl && ( {!isCustom && f.defaultLayout && (
<Button <span className="inline-flex items-center rounded-full bg-gray-200 px-2 py-0.5 font-medium text-gray-600 dark:bg-gray-700 dark:text-gray-300">
size="sm" {f.defaultLayout}
variant="plain" </span>
className="text-emerald-500 hover:bg-emerald-50 dark:hover:bg-emerald-900/20" )}
type="button"
title={translate('::App.Platform.OpenUrl')} {f.menuUrl && (
onClick={() => window.open(f.menuUrl, '_blank', 'noopener,noreferrer')} <span
> className="inline-flex min-w-0 max-w-full items-center gap-1 rounded-full bg-gray-200 px-2 py-0.5 font-mono text-gray-600 dark:bg-gray-700 dark:text-gray-300"
<FaExternalLinkAlt /> title={f.menuUrl}
</Button> >
)} <FaLink className="shrink-0" />
{canExport && ( <span className="truncate">{f.menuUrl}</span>
<Button </span>
size="sm" )}
variant="plain" </div>
className="text-sky-500 hover:bg-sky-50 dark:hover:bg-sky-900/20"
type="button" {renderActions(
title={translate('::App.ListForm.WizardFileExport')} f,
loading={exportingFile === f.fileName} 'flex items-center justify-end gap-1 pt-1 border-t border-gray-200 dark:border-gray-700',
onClick={() => handleExport(f)}
>
<FaFileExport />
</Button>
)}
{canUpdate && (
<Button
size="sm"
variant="plain"
className="text-indigo-500 hover:bg-indigo-50 dark:hover:bg-indigo-900/20"
type="button"
title={translate('::App.Platform.Edit')}
onClick={() =>
navigate(ROUTES_ENUM.protected.saas.listFormManagement.wizard, {
state: { editFileName: f.fileName },
})
}
>
<FaEdit />
</Button>
)}
{canDelete && (
<Button
size="sm"
variant="plain"
className="text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20"
type="button"
title={translate('::App.Platform.Delete')}
loading={deletingFile === f.fileName}
onClick={() =>
setConfirm({ fileName: f.fileName, wizardName: f.wizardName || f.fileName })
}
>
<FaTrash />
</Button>
)} )}
</div> </div>
</div> )
) })}
})} </div>
</div> ) : (
<div className="space-y-2">
{filteredFiles.map((f) => {
const Icon = navigationIcon[f.menuIcon] ?? FaDatabase
const isCustom = f.componentKind === WizardComponentKindEnum.Custom
return (
<div
key={f.fileName}
className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2 p-3 rounded-lg border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800"
>
<div className="flex items-center gap-3 min-w-0">
<Icon className="text-indigo-400 shrink-0 text-3xl" />
<div className="min-w-0 space-y-1">
{/* 1. satır: wizard adı */}
<div className="font-medium text-sm text-gray-800 dark:text-gray-200 truncate">
{f.wizardName || f.fileName}
</div>
{/* 2. satır: dosya adı ve üretilen kod */}
<div className="flex flex-wrap items-center gap-x-2 text-xs text-gray-400">
<span className="truncate font-mono">{f.fileName}</span>
<span className="text-gray-300 dark:text-gray-600"></span>
<span className="truncate">
{isCustom ? f.customComponentName : f.listFormCode}
</span>
</div>
{/* 3. satır: yol ayrımı (Custom / List), layout ve menü adresi */}
<div className="flex flex-wrap items-center gap-1.5 text-xs">
<span
className={classNames(
'inline-flex items-center gap-1 rounded-full px-2 py-0.5 font-medium',
isCustom
? 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-300'
: 'bg-indigo-100 text-indigo-700 dark:bg-indigo-900/30 dark:text-indigo-300',
)}
>
{isCustom ? <FaCube /> : <FaListUl />}
{translate(
isCustom
? '::App.WizardStepComponent.CustomComponent'
: '::App.WizardStepComponent.ListComponent',
)}
</span>
{/* Custom yolunda layout kavramı yoktur. */}
{!isCustom && f.defaultLayout && (
<span className="inline-flex items-center rounded-full bg-gray-200 px-2 py-0.5 font-medium text-gray-600 dark:bg-gray-700 dark:text-gray-300">
{f.defaultLayout}
</span>
)}
{f.menuUrl && (
<span className="inline-flex min-w-0 items-center gap-1 rounded-full bg-gray-200 px-2 py-0.5 font-mono text-gray-600 dark:bg-gray-700 dark:text-gray-300">
<FaLink className="shrink-0" />
<span className="truncate">{f.menuUrl}</span>
</span>
)}
</div>
</div>
</div>
{renderActions(f, 'flex items-center gap-1 shrink-0 sm:ml-3')}
</div>
)
})}
</div>
)}
</div> </div>
{importFile && ( {importFile && (

View file

@ -12,6 +12,8 @@ import {
FaExternalLinkAlt, FaExternalLinkAlt,
FaCog, FaCog,
FaSyncAlt, FaSyncAlt,
FaTh,
FaList,
} from 'react-icons/fa' } from 'react-icons/fa'
import Widget from '@/components/common/Widget' import Widget from '@/components/common/Widget'
import { ROUTES_ENUM } from '@/routes/route.constant' import { ROUTES_ENUM } from '@/routes/route.constant'
@ -26,6 +28,12 @@ import { usePermission } from '@/utils/hooks/usePermission'
import { COMPONENT_PERMISSION } from '@/constants/permission.constant' import { COMPONENT_PERMISSION } from '@/constants/permission.constant'
import Input from '@/components/ui/Input' import Input from '@/components/ui/Input'
import Select from '@/components/ui/Select' import Select from '@/components/ui/Select'
import type { CustomComponent } from '@/proxy/developerKit/models'
import { useStoreActions, useStoreState } from '@/store/store'
import type { ListViewLayoutType } from '../admin/listForm/edit/types'
/** Layout tercihi diger listelerle ayni yerde (admin.lists.states) saklanir. */
const VIEW_STATE_CODE = 'developerkit-components'
const ComponentManager: React.FC = () => { const ComponentManager: React.FC = () => {
const { const {
@ -40,6 +48,12 @@ const ComponentManager: React.FC = () => {
const [filterActive, setFilterActive] = useState<'all' | 'active' | 'inactive'>('all') const [filterActive, setFilterActive] = useState<'all' | 'active' | 'inactive'>('all')
const [isRefreshing, setIsRefreshing] = useState(false) const [isRefreshing, setIsRefreshing] = useState(false)
const [editorComponentId, setEditorComponentId] = useState<string | null | undefined>(undefined) const [editorComponentId, setEditorComponentId] = useState<string | null | undefined>(undefined)
const { states } = useStoreState((state) => state.admin.lists)
const { setStates } = useStoreActions((a) => a.admin.lists)
const viewMode: ListViewLayoutType =
states.find((s) => s.listFormCode === VIEW_STATE_CODE)?.layout === 'grid' ? 'grid' : 'card'
const setViewMode = (layout: ListViewLayoutType) =>
setStates({ listFormCode: VIEW_STATE_CODE, layout })
// Calculate statistics // Calculate statistics
const totalComponents = components?.length || 0 const totalComponents = components?.length || 0
@ -104,6 +118,85 @@ const ComponentManager: React.FC = () => {
} }
} }
const renderStatusToggle = (component: CustomComponent) => (
<Button
type="button"
variant="plain"
shape="circle"
disabled={!canUpdate}
className="!inline-flex !h-auto items-center gap-1 rounded !px-2 py-1 text-xs font-medium transition-colors"
onClick={() => handleToggleActive(component.id, !component.isActive)}
>
{component.isActive ? (
<>
<FaEye className="w-4 h-4" />
{translate('::App.Listform.ListformField.IsActive')}
</>
) : (
<>
<FaEyeSlash className="w-4 h-4" />
{translate('::App.Platform.Passive')}
</>
)}
</Button>
)
const renderActions = (component: CustomComponent) => (
<div className="flex items-center gap-1">
{canUpdate && (
<Button
type="button"
variant="solid"
color="blue-600"
title={translate('::App.Platform.Edit')}
onClick={() =>
window.open(
ROUTES_ENUM.protected.saas.developerKit.componentsEdit.replace(':id', component.id),
'_blank',
)
}
>
<FaRegEdit className="w-4 h-4" />
</Button>
)}
<Button
type="button"
variant="solid"
color="gray-600"
disabled={!component.isActive || !component.routePath?.trim()}
title={translate('::App.Platform.OpenUrl')}
onClick={() => {
const routePath = component.routePath.startsWith('/')
? component.routePath
: `/${component.routePath}`
window.open(routePath, '_blank')
}}
>
<FaExternalLinkAlt className="w-4 h-4" />
</Button>
<Button
type="button"
variant="solid"
color="green-600"
title={translate('::App.Platform.View')}
onClick={() => setEditorComponentId(component.id)}
>
<FaCog className="w-4 h-4" />
</Button>
{canDelete && (
<Button
type="button"
variant="solid"
color="red-600"
title={translate('::App.Platform.Delete')}
onClick={() => handleDelete(component.id)}
>
<FaTrashAlt className="w-4 h-4" />
</Button>
)}
</div>
)
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<PageTitle title={translate('::' + 'App.DeveloperKit.Components')} /> <PageTitle title={translate('::' + 'App.DeveloperKit.Components')} />
@ -181,6 +274,22 @@ const ComponentManager: React.FC = () => {
</Button> </Button>
</div> </div>
)} )}
<div className="flex items-center gap-1">
<Button
type="button"
icon={<FaTh />}
variant={viewMode === 'card' ? 'solid' : 'default'}
title={translate('::App.Platform.CardView')}
onClick={() => setViewMode('card')}
/>
<Button
type="button"
icon={<FaList />}
variant={viewMode === 'grid' ? 'solid' : 'default'}
title={translate('::App.Platform.ListView')}
onClick={() => setViewMode('grid')}
/>
</div>
</div> </div>
{/* Components List */} {/* Components List */}
@ -191,148 +300,158 @@ const ComponentManager: React.FC = () => {
</div> </div>
</div> </div>
) : filteredComponents?.length > 0 ? ( ) : filteredComponents?.length > 0 ? (
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-4 gap-6"> viewMode === 'card' ? (
{filteredComponents.map((component) => ( <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-4 gap-4">
<div {filteredComponents.map((component) => (
key={component.id} <div
className="bg-white dark:bg-gray-900 rounded-lg border border-slate-200 dark:border-gray-700 shadow-sm hover:shadow-md transition-shadow" key={component.id}
> className="bg-white dark:bg-gray-900 rounded-lg border border-slate-200 dark:border-gray-700 shadow-sm hover:shadow-md transition-shadow"
<div className="p-6"> >
<div className="flex items-start justify-between"> <div className="p-4">
{/* Sol taraf */} <div className="flex items-start justify-between">
<div className="flex-1"> {/* Sol taraf */}
<div className="flex items-center gap-2 mb-1"> <div className="flex-1 min-w-0">
<h3 className="text-lg font-semibold text-slate-900 dark:text-gray-100"> <div className="flex items-center gap-2 mb-1">
{component.name} <h3
</h3> className="text-base font-semibold text-slate-900 dark:text-gray-100 truncate"
<div title={component.name}
className={`w-2 h-2 rounded-full ${ >
component.isActive ? 'bg-green-500' : 'bg-slate-300 dark:bg-gray-700' {component.name}
}`} </h3>
/> <div
</div> className={`w-2 h-2 shrink-0 rounded-full ${
component.isActive ? 'bg-green-500' : 'bg-slate-300 dark:bg-gray-700'
}`}
/>
</div>
<p className="text-slate-600 dark:text-gray-300 text-sm mb-2"> <p className="text-slate-600 dark:text-gray-300 text-sm mb-2 truncate">
{parseComponentDependencies(component.dependencies).join(', ') || {parseComponentDependencies(component.dependencies).join(', ') ||
translate('::App.DeveloperKitComponent.NoDependencies')} translate('::App.DeveloperKitComponent.NoDependencies')}
</p>
<p className="text-slate-600 dark:text-gray-300 text-sm mb-2">
{component.routePath}
</p>
{component.description && (
<p className="text-slate-600 dark:text-gray-300 text-sm mb-2">
{component.description}
</p> </p>
)}
{componentErrors[component.name] && (
<p <p
className="text-xs text-red-700 dark:text-red-300 bg-red-50 dark:bg-red-950 border border-red-200 dark:border-red-800 rounded px-2 py-1 mb-2 break-words" className="text-slate-600 dark:text-gray-300 text-sm mb-2 truncate"
title={componentErrors[component.name]} title={component.routePath}
> >
{componentErrors[component.name]} {component.routePath}
</p> </p>
)}
</div>
{/* Sağ taraf */} {component.description && (
{component.lastModificationTime && ( <p className="text-slate-600 dark:text-gray-300 text-sm mb-2 line-clamp-2">
<div className="flex items-center gap-1 text-xs text-slate-500 dark:text-gray-400 ml-4 whitespace-nowrap"> {component.description}
<FaCalendarAlt className="w-3 h-3" /> </p>
<span>
{new Date(component.lastModificationTime).toLocaleDateString() ?? ''}
</span>
</div>
)}
</div>
{/* Actions */}
<div className="flex items-center justify-between pt-2 border-t border-slate-100 dark:border-gray-700">
<div className="flex items-center gap-2">
<Button
type="button"
variant="plain"
shape="circle"
disabled={!canUpdate}
className={`!inline-flex !h-auto items-center gap-1 rounded !px-2 py-1 text-xs font-medium transition-colors`}
onClick={() => handleToggleActive(component.id, !component.isActive)}
>
{component.isActive ? (
<>
<FaEye className="w-4 h-4" />
{translate('::App.Listform.ListformField.IsActive')}
</>
) : (
<>
<FaEyeSlash className="w-4 h-4" />
{translate('::App.Platform.Passive')}
</>
)} )}
</Button>
{componentErrors[component.name] && (
<p
className="text-xs text-red-700 dark:text-red-300 bg-red-50 dark:bg-red-950 border border-red-200 dark:border-red-800 rounded px-2 py-1 mb-2 break-words"
title={componentErrors[component.name]}
>
{componentErrors[component.name]}
</p>
)}
</div>
{/* Sag taraf */}
{component.lastModificationTime && (
<div className="flex items-center gap-1 text-xs text-slate-500 dark:text-gray-400 ml-2 whitespace-nowrap">
<FaCalendarAlt className="w-3 h-3" />
<span>
{new Date(component.lastModificationTime).toLocaleDateString() ?? ''}
</span>
</div>
)}
</div> </div>
<div className="flex items-center gap-1">
{canUpdate && ( {/* Actions */}
<Button <div className="flex items-center justify-between gap-1 pt-2 border-t border-slate-100 dark:border-gray-700">
type="button" {renderStatusToggle(component)}
variant="solid" {renderActions(component)}
color="blue-600"
title={translate('::App.Platform.Edit')}
onClick={() =>
window.open(
ROUTES_ENUM.protected.saas.developerKit.componentsEdit.replace(
':id',
component.id,
),
'_blank',
)
}
>
<FaRegEdit className="w-4 h-4" />
</Button>
)}
<Button
type="button"
variant="solid"
color="gray-600"
disabled={!component.isActive || !component.routePath?.trim()}
title={translate('::App.Platform.OpenUrl')}
onClick={() => {
const routePath = component.routePath.startsWith('/')
? component.routePath
: `/${component.routePath}`
window.open(routePath, '_blank')
}}
>
<FaExternalLinkAlt className="w-4 h-4" />
</Button>
<Button
type="button"
variant="solid"
color="green-600"
title={translate('::App.Platform.View')}
onClick={() => setEditorComponentId(component.id)}
>
<FaCog className="w-4 h-4" />
</Button>
{canDelete && (
<Button
type="button"
variant="solid"
color="red-600"
title={translate('::App.Platform.Delete')}
onClick={() => handleDelete(component.id)}
>
<FaTrashAlt className="w-4 h-4" />
</Button>
)}
</div> </div>
</div> </div>
</div> </div>
</div> ))}
))} </div>
</div> ) : (
<div className="bg-white dark:bg-gray-900 rounded-lg border border-slate-200 dark:border-gray-700 shadow-sm overflow-x-auto">
<table className="w-full text-sm">
<thead className="bg-slate-50 dark:bg-gray-800 text-slate-600 dark:text-gray-300">
<tr>
<th className="text-left font-medium px-4 py-2">
{translate('::App.Platform.Name')}
</th>
<th className="text-left font-medium px-4 py-2">
{translate('::App.Platform.Dependencies')}
</th>
<th className="text-left font-medium px-4 py-2">
{translate('::App.Platform.RoutePath')}
</th>
<th className="text-left font-medium px-4 py-2">
{translate('::App.Platform.Description')}
</th>
<th className="text-left font-medium px-4 py-2 whitespace-nowrap">
{translate('::App.Platform.Modified')}
</th>
<th className="text-left font-medium px-4 py-2">
{translate('::App.Platform.Status')}
</th>
<th className="text-right font-medium px-4 py-2">
{translate('::App.Platform.Actions')}
</th>
</tr>
</thead>
<tbody>
{filteredComponents.map((component) => (
<tr
key={component.id}
className="border-t border-slate-100 dark:border-gray-700 hover:bg-slate-50 dark:hover:bg-gray-800"
>
<td className="px-4 py-2">
<div className="flex items-center gap-2">
<div
className={`w-2 h-2 shrink-0 rounded-full ${
component.isActive ? 'bg-green-500' : 'bg-slate-300 dark:bg-gray-700'
}`}
/>
<span className="font-medium text-slate-900 dark:text-gray-100">
{component.name}
</span>
</div>
{componentErrors[component.name] && (
<p
className="mt-1 text-xs text-red-700 dark:text-red-300 break-words"
title={componentErrors[component.name]}
>
{componentErrors[component.name]}
</p>
)}
</td>
<td className="px-4 py-2 text-slate-600 dark:text-gray-300">
{parseComponentDependencies(component.dependencies).join(', ') ||
translate('::App.DeveloperKitComponent.NoDependencies')}
</td>
<td className="px-4 py-2 text-slate-600 dark:text-gray-300">
{component.routePath}
</td>
<td className="px-4 py-2 text-slate-600 dark:text-gray-300">
{component.description}
</td>
<td className="px-4 py-2 text-slate-500 dark:text-gray-400 whitespace-nowrap">
{component.lastModificationTime
? new Date(component.lastModificationTime).toLocaleDateString()
: ''}
</td>
<td className="px-4 py-2">{renderStatusToggle(component)}</td>
<td className="px-4 py-2">
<div className="flex justify-end">{renderActions(component)}</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
)
) : ( ) : (
<div className="text-center py-12"> <div className="text-center py-12">
<div className="mx-auto"> <div className="mx-auto">

View file

@ -16,7 +16,7 @@ import { useLocalization } from '@/utils/hooks/useLocalization'
import { Formik, Form, Field, FieldProps } from 'formik' import { Formik, Form, Field, FieldProps } from 'formik'
import * as Yup from 'yup' import * as Yup from 'yup'
import { FormContainer, FormItem } from '@/components/ui/Form' import { FormContainer, FormItem } from '@/components/ui/Form'
import { Input, Checkbox, Button, Select } from '@/components/ui' import { Input, Checkbox, Button, Select, Dialog } from '@/components/ui'
import { ConfirmDialog } from '@/components/shared' import { ConfirmDialog } from '@/components/shared'
import { CategoryIconOption, resolveCategoryIconOptions } from './categoryIcons' import { CategoryIconOption, resolveCategoryIconOptions } from './categoryIcons'
@ -69,6 +69,11 @@ export function CategoryManagement({
setShowCreateForm(true) setShowCreateForm(true)
} }
const handleCloseForm = () => {
setShowCreateForm(false)
setEditingCategory(null)
}
const handleToggleActive = async (category: ForumCategory) => { const handleToggleActive = async (category: ForumCategory) => {
try { try {
await onUpdateCategoryActiveState(category.id) await onUpdateCategoryActiveState(category.id)
@ -87,16 +92,24 @@ export function CategoryManagement({
return ( return (
<div className="space-y-3"> <div className="space-y-3">
{/* Create/Edit Form */} {/* Create/Edit Modal */}
{showCreateForm && ( <Dialog
<div className="bg-white dark:bg-gray-900 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 p-6"> isOpen={showCreateForm}
<h3 className="mb-4 text-lg font-semibold text-gray-900 dark:text-gray-100"> width={720}
onClose={handleCloseForm}
onRequestClose={handleCloseForm}
>
<div className="pb-4 border-b border-gray-200 dark:border-gray-700">
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100">
{editingCategory {editingCategory
? translate('::App.ForumCategoryManagement.EditCategory') ? translate('::App.ForumCategoryManagement.EditCategory')
: translate('::App.ForumCategoryManagement.AddCategory')} : translate('::App.ForumCategoryManagement.AddCategory')}
</h3> </h3>
</div>
<div className="pt-4 max-h-[70vh] overflow-y-auto">
<Formik <Formik
enableReinitialize
initialValues={{ initialValues={{
name: editingCategory?.name ?? '', name: editingCategory?.name ?? '',
slug: editingCategory?.slug ?? '', slug: editingCategory?.slug ?? '',
@ -221,14 +234,7 @@ export function CategoryManagement({
</div> </div>
<div className="mt-4 flex justify-end space-x-3"> <div className="mt-4 flex justify-end space-x-3">
<Button <Button variant="plain" type="button" onClick={handleCloseForm}>
variant="plain"
type="button"
onClick={() => {
setShowCreateForm(false)
setEditingCategory(null)
}}
>
{translate('::App.Platform.Cancel')} {translate('::App.Platform.Cancel')}
</Button> </Button>
<Button variant="solid" type="submit" loading={isSubmitting}> <Button variant="solid" type="submit" loading={isSubmitting}>
@ -242,7 +248,7 @@ export function CategoryManagement({
)} )}
</Formik> </Formik>
</div> </div>
)} </Dialog>
{/* Categories List */} {/* Categories List */}
<div className="bg-white dark:bg-gray-900 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden"> <div className="bg-white dark:bg-gray-900 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden">
@ -321,7 +327,7 @@ export function CategoryManagement({
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<Button <Button
size="xs" size="sm"
className={`p-1 rounded-lg transition-colors ${ className={`p-1 rounded-lg transition-colors ${
category.isActive category.isActive
? 'text-green-600 dark:text-green-400 hover:bg-green-100 dark:hover:bg-green-900' ? 'text-green-600 dark:text-green-400 hover:bg-green-100 dark:hover:bg-green-900'
@ -335,14 +341,14 @@ export function CategoryManagement({
onClick={() => handleToggleActive(category)} onClick={() => handleToggleActive(category)}
> >
{category.isActive ? ( {category.isActive ? (
<FaEye className="w-3 h-3" /> <FaEye className="w-4 h-4" />
) : ( ) : (
<FaEyeSlash className="w-3 h-3" /> <FaEyeSlash className="w-4 h-4" />
)} )}
</Button> </Button>
<Button <Button
size="xs" size="sm"
className={`p-1 rounded-lg transition-colors ${ className={`p-1 rounded-lg transition-colors ${
category.isLocked category.isLocked
? 'text-yellow-600 dark:text-yellow-300 hover:bg-yellow-100 dark:hover:bg-yellow-900' ? 'text-yellow-600 dark:text-yellow-300 hover:bg-yellow-100 dark:hover:bg-yellow-900'
@ -356,28 +362,28 @@ export function CategoryManagement({
onClick={() => handleToggleLocked(category)} onClick={() => handleToggleLocked(category)}
> >
{category.isLocked ? ( {category.isLocked ? (
<FaLock className="w-3 h-3" /> <FaLock className="w-4 h-4" />
) : ( ) : (
<FaUnlock className="w-3 h-3" /> <FaUnlock className="w-4 h-4" />
)} )}
</Button> </Button>
<Button <Button
size="xs" size="sm"
className="p-1 text-blue-600 dark:text-blue-400 hover:bg-blue-100 dark:hover:bg-blue-900 rounded-lg transition-colors" className="p-1 text-blue-600 dark:text-blue-400 hover:bg-blue-100 dark:hover:bg-blue-900 rounded-lg transition-colors"
title={translate('::App.ForumCategoryManagement.EditCategory')} title={translate('::App.ForumCategoryManagement.EditCategory')}
onClick={() => handleEdit(category)} onClick={() => handleEdit(category)}
> >
<FaEdit className="w-3 h-3" /> <FaEdit className="w-4 h-4" />
</Button> </Button>
<Button <Button
size="xs" size="sm"
className="p-1 text-red-600 dark:text-red-400 hover:bg-red-100 dark:hover:bg-red-900 rounded-lg transition-colors" className="p-1 text-red-600 dark:text-red-400 hover:bg-red-100 dark:hover:bg-red-900 rounded-lg transition-colors"
title={translate('::App.ForumCategoryManagement.DeleteCategory')} title={translate('::App.ForumCategoryManagement.DeleteCategory')}
onClick={() => confirmDeleteCategory(category)} onClick={() => confirmDeleteCategory(category)}
> >
<FaTrash className="w-3 h-3" /> <FaTrash className="w-4 h-4" />
</Button> </Button>
</div> </div>
</div> </div>

View file

@ -15,7 +15,7 @@ import { useStoreState } from '@/store/store'
import { useLocalization } from '@/utils/hooks/useLocalization' import { useLocalization } from '@/utils/hooks/useLocalization'
import { Formik, Form, Field, FieldProps } from 'formik' import { Formik, Form, Field, FieldProps } from 'formik'
import * as Yup from 'yup' import * as Yup from 'yup'
import { FormContainer, FormItem, Button } from '@/components/ui' import { FormContainer, FormItem, Button, Dialog } from '@/components/ui'
import { ConfirmDialog } from '@/components/shared' import { ConfirmDialog } from '@/components/shared'
import { formatForumDate, sanitizeForumHtml, stripHtml } from '../forum/utils' import { formatForumDate, sanitizeForumHtml, stripHtml } from '../forum/utils'
import { import {
@ -72,6 +72,11 @@ export function PostManagement({
setShowCreateForm(true) setShowCreateForm(true)
} }
const handleCloseForm = () => {
setShowCreateForm(false)
setEditingPost(null)
}
const handleToggleAcceptedAnswer = async (post: ForumPost) => { const handleToggleAcceptedAnswer = async (post: ForumPost) => {
try { try {
if (post.isAcceptedAnswer) { if (post.isAcceptedAnswer) {
@ -89,15 +94,22 @@ export function PostManagement({
return ( return (
<div className="space-y-3"> <div className="space-y-3">
{/* Create/Edit Form */} {/* Create/Edit Modal */}
{showCreateForm && ( <Dialog
<div className="bg-white dark:bg-gray-900 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 p-6"> isOpen={showCreateForm}
<h3 className="mb-4 text-lg font-semibold text-gray-900 dark:text-gray-100"> width={900}
onClose={handleCloseForm}
onRequestClose={handleCloseForm}
>
<div className="pb-4 border-b border-gray-200 dark:border-gray-700">
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100">
{editingPost {editingPost
? translate('::App.ForumPostManagement.EditPost') ? translate('::App.ForumPostManagement.EditPost')
: translate('::App.ForumPostManagement.AddPost')} : translate('::App.ForumPostManagement.AddPost')}
</h3> </h3>
</div>
<div className="pt-4 max-h-[70vh] overflow-y-auto">
<Formik <Formik
enableReinitialize enableReinitialize
initialValues={{ initialValues={{
@ -234,14 +246,7 @@ export function PostManagement({
</FormItem> </FormItem>
<div className="flex justify-end space-x-3 pt-2"> <div className="flex justify-end space-x-3 pt-2">
<Button <Button variant="plain" type="button" onClick={handleCloseForm}>
variant="plain"
type="button"
onClick={() => {
setShowCreateForm(false)
setEditingPost(null)
}}
>
{translate('::App.Platform.Cancel')} {translate('::App.Platform.Cancel')}
</Button> </Button>
<Button <Button
@ -260,7 +265,7 @@ export function PostManagement({
)} )}
</Formik> </Formik>
</div> </div>
)} </Dialog>
{/* Posts List */} {/* Posts List */}
<div className="bg-white dark:bg-gray-900 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden"> <div className="bg-white dark:bg-gray-900 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden">
@ -304,7 +309,7 @@ export function PostManagement({
<h4 className="text-sm font-semibold text-gray-900 dark:text-gray-100">{post.authorName}</h4> <h4 className="text-sm font-semibold text-gray-900 dark:text-gray-100">{post.authorName}</h4>
{post.isAcceptedAnswer && ( {post.isAcceptedAnswer && (
<div className="flex items-center space-x-1 bg-emerald-100 dark:bg-emerald-900 text-emerald-700 dark:text-emerald-300 px-2 py-1 rounded-full text-xs"> <div className="flex items-center space-x-1 bg-emerald-100 dark:bg-emerald-900 text-emerald-700 dark:text-emerald-300 px-2 py-1 rounded-full text-xs">
<FaCheckCircle className="w-3 h-3" /> <FaCheckCircle className="w-4 h-4" />
<span>{translate('::App.ForumPostManagement.AcceptedAnswer')}</span> <span>{translate('::App.ForumPostManagement.AcceptedAnswer')}</span>
</div> </div>
)} )}
@ -336,7 +341,7 @@ export function PostManagement({
<div className="flex items-center space-x-2 ml-4"> <div className="flex items-center space-x-2 ml-4">
<Button <Button
size="xs" size="sm"
className={`p-1 rounded-lg transition-colors ${ className={`p-1 rounded-lg transition-colors ${
post.isAcceptedAnswer post.isAcceptedAnswer
? 'text-emerald-600 dark:text-emerald-400 hover:bg-emerald-100 dark:hover:bg-emerald-900' ? 'text-emerald-600 dark:text-emerald-400 hover:bg-emerald-100 dark:hover:bg-emerald-900'
@ -350,28 +355,28 @@ export function PostManagement({
onClick={() => handleToggleAcceptedAnswer(post)} onClick={() => handleToggleAcceptedAnswer(post)}
> >
{post.isAcceptedAnswer ? ( {post.isAcceptedAnswer ? (
<FaCheckCircle className="w-3 h-3" /> <FaCheckCircle className="w-4 h-4" />
) : ( ) : (
<FaCircle className="w-3 h-3" /> <FaCircle className="w-4 h-4" />
)} )}
</Button> </Button>
<Button <Button
size="xs" size="sm"
className="p-1 text-blue-600 dark:text-blue-400 hover:bg-blue-100 dark:hover:bg-blue-900 rounded-lg transition-colors" className="p-1 text-blue-600 dark:text-blue-400 hover:bg-blue-100 dark:hover:bg-blue-900 rounded-lg transition-colors"
title={translate('::App.ForumPostManagement.EditPost')} title={translate('::App.ForumPostManagement.EditPost')}
onClick={() => handleEdit(post)} onClick={() => handleEdit(post)}
> >
<FaEdit className="w-3 h-3" /> <FaEdit className="w-4 h-4" />
</Button> </Button>
<Button <Button
size="xs" size="sm"
className="p-1 text-red-600 dark:text-red-400 hover:bg-red-100 dark:hover:bg-red-900 rounded-lg transition-colors" className="p-1 text-red-600 dark:text-red-400 hover:bg-red-100 dark:hover:bg-red-900 rounded-lg transition-colors"
title={translate('::App.ForumPostManagement.DeletePost')} title={translate('::App.ForumPostManagement.DeletePost')}
onClick={() => confirmDeletePost(post)} onClick={() => confirmDeletePost(post)}
> >
<FaTrashAlt className="w-3 h-3" /> <FaTrashAlt className="w-4 h-4" />
</Button> </Button>
</div> </div>
</div> </div>

View file

@ -17,7 +17,7 @@ import { useStoreState } from '@/store/store'
import { useLocalization } from '@/utils/hooks/useLocalization' import { useLocalization } from '@/utils/hooks/useLocalization'
import { Formik, Form, Field, FieldProps } from 'formik' import { Formik, Form, Field, FieldProps } from 'formik'
import * as Yup from 'yup' import * as Yup from 'yup'
import { FormContainer, FormItem, Button, Select } from '@/components/ui' import { FormContainer, FormItem, Button, Select, Dialog } from '@/components/ui'
import { ConfirmDialog } from '@/components/shared' import { ConfirmDialog } from '@/components/shared'
import { formatForumDate } from '../forum/utils' import { formatForumDate } from '../forum/utils'
@ -84,6 +84,11 @@ export function TopicManagement({
setShowCreateForm(true) setShowCreateForm(true)
} }
const handleCloseForm = () => {
setShowCreateForm(false)
setEditingTopic(null)
}
const handlePin = async (topic: ForumTopic) => { const handlePin = async (topic: ForumTopic) => {
try { try {
if (topic.isPinned) { if (topic.isPinned) {
@ -141,15 +146,22 @@ export function TopicManagement({
return ( return (
<div className="space-y-3"> <div className="space-y-3">
{/* Create/Edit Form */} {/* Create/Edit Modal */}
{showCreateForm && ( <Dialog
<div className="bg-white dark:bg-gray-900 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 p-6"> isOpen={showCreateForm}
<h3 className="mb-4 text-lg font-semibold text-gray-900 dark:text-gray-100"> width={720}
onClose={handleCloseForm}
onRequestClose={handleCloseForm}
>
<div className="pb-4 border-b border-gray-200 dark:border-gray-700">
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100">
{editingTopic {editingTopic
? translate('::App.ForumTopicManagement.EditTopic') ? translate('::App.ForumTopicManagement.EditTopic')
: translate('::App.ForumTopicManagement.AddTopic')} : translate('::App.ForumTopicManagement.AddTopic')}
</h3> </h3>
</div>
<div className="pt-4 max-h-[70vh] overflow-y-auto">
<Formik <Formik
enableReinitialize enableReinitialize
initialValues={ initialValues={
@ -260,14 +272,7 @@ export function TopicManagement({
</FormItem> </FormItem>
<div className="flex justify-end space-x-3 pt-2"> <div className="flex justify-end space-x-3 pt-2">
<Button <Button variant="plain" type="button" onClick={handleCloseForm}>
variant="plain"
type="button"
onClick={() => {
setShowCreateForm(false)
setEditingTopic(null)
}}
>
{translate('::App.Platform.Cancel')} {translate('::App.Platform.Cancel')}
</Button> </Button>
<Button variant="solid" type="submit" loading={isSubmitting}> <Button variant="solid" type="submit" loading={isSubmitting}>
@ -279,7 +284,7 @@ export function TopicManagement({
)} )}
</Formik> </Formik>
</div> </div>
)} </Dialog>
{/* Topics List */} {/* Topics List */}
<div className="bg-white dark:bg-gray-900 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden"> <div className="bg-white dark:bg-gray-900 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden">
@ -366,7 +371,7 @@ export function TopicManagement({
<div className="flex items-center space-x-2 ml-4"> <div className="flex items-center space-x-2 ml-4">
<Button <Button
size="xs" size="sm"
className={`p-1 rounded-lg transition-colors ${ className={`p-1 rounded-lg transition-colors ${
topic.isPinned topic.isPinned
? 'text-orange-600 dark:text-orange-400 hover:bg-orange-100 dark:hover:bg-orange-900' ? 'text-orange-600 dark:text-orange-400 hover:bg-orange-100 dark:hover:bg-orange-900'
@ -379,11 +384,11 @@ export function TopicManagement({
} }
onClick={() => handlePin(topic)} onClick={() => handlePin(topic)}
> >
<FaThumbtack className="h-3 w-3" /> <FaThumbtack className="h-4 w-4" />
</Button> </Button>
<Button <Button
size="xs" size="sm"
className={`p-1 rounded-lg transition-colors ${ className={`p-1 rounded-lg transition-colors ${
topic.isLocked topic.isLocked
? 'text-yellow-600 dark:text-yellow-300 hover:bg-yellow-100 dark:hover:bg-yellow-900' ? 'text-yellow-600 dark:text-yellow-300 hover:bg-yellow-100 dark:hover:bg-yellow-900'
@ -397,14 +402,14 @@ export function TopicManagement({
onClick={() => handleLock(topic)} onClick={() => handleLock(topic)}
> >
{topic.isLocked ? ( {topic.isLocked ? (
<FaLock className="w-3 h-3" /> <FaLock className="w-4 h-4" />
) : ( ) : (
<FaUnlock className="w-3 h-3" /> <FaUnlock className="w-4 h-4" />
)} )}
</Button> </Button>
<Button <Button
size="xs" size="sm"
className={`p-1 rounded-lg transition-colors ${ className={`p-1 rounded-lg transition-colors ${
topic.isSolved topic.isSolved
? 'text-emerald-600 dark:text-emerald-400 hover:bg-emerald-100 dark:hover:bg-emerald-900' ? 'text-emerald-600 dark:text-emerald-400 hover:bg-emerald-100 dark:hover:bg-emerald-900'
@ -418,28 +423,28 @@ export function TopicManagement({
onClick={() => handleSolved(topic)} onClick={() => handleSolved(topic)}
> >
{topic.isSolved ? ( {topic.isSolved ? (
<FaCheckCircle className="w-3 h-3" /> <FaCheckCircle className="w-4 h-4" />
) : ( ) : (
<FaCircle className="w-3 h-3" /> <FaCircle className="w-4 h-4" />
)} )}
</Button> </Button>
<Button <Button
size="xs" size="sm"
className="p-1 text-blue-600 dark:text-blue-400 hover:bg-blue-100 dark:hover:bg-blue-900 rounded-lg transition-colors" className="p-1 text-blue-600 dark:text-blue-400 hover:bg-blue-100 dark:hover:bg-blue-900 rounded-lg transition-colors"
title={translate('::App.ForumTopicManagement.EditTopic')} title={translate('::App.ForumTopicManagement.EditTopic')}
onClick={() => handleEdit(topic)} onClick={() => handleEdit(topic)}
> >
<FaEdit className="w-3 h-3" /> <FaEdit className="w-4 h-4" />
</Button> </Button>
<Button <Button
size="xs" size="sm"
className="p-1 text-red-600 dark:text-red-400 hover:bg-red-100 dark:hover:bg-red-900 rounded-lg transition-colors" className="p-1 text-red-600 dark:text-red-400 hover:bg-red-100 dark:hover:bg-red-900 rounded-lg transition-colors"
title={translate('::App.ForumTopicManagement.DeleteTopic')} title={translate('::App.ForumTopicManagement.DeleteTopic')}
onClick={() => confirmDeleteTopic(topic)} onClick={() => confirmDeleteTopic(topic)}
> >
<FaTrashAlt className="w-3 h-3" /> <FaTrashAlt className="w-4 h-4" />
</Button> </Button>
</div> </div>
</div> </div>