Genel düzeltmeler Responsive ve Seeder
This commit is contained in:
parent
a70d8650f1
commit
871ee34536
9 changed files with 248 additions and 78 deletions
|
|
@ -16733,8 +16733,8 @@
|
|||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "ListForms.Wizard.MenuInfo",
|
||||
"en": "Menu Information",
|
||||
"tr": "Menü Bilgileri"
|
||||
"en": "Menu",
|
||||
"tr": "Menü"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
|
|
@ -16751,14 +16751,14 @@
|
|||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "ListForms.Wizard.ListFormSettings",
|
||||
"en": "List Form Settings",
|
||||
"tr": "Liste Formu Ayarları"
|
||||
"en": "Settings",
|
||||
"tr": "Ayarlar"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "ListForms.Wizard.ListFormFields",
|
||||
"en": "List Form Fields",
|
||||
"tr": "Liste Formu Alanları"
|
||||
"en": "Fields",
|
||||
"tr": "Alanlar"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
|
|
|
|||
|
|
@ -840,10 +840,10 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep
|
|||
ColSpan = 4,
|
||||
SqlQuery = @"
|
||||
SELECT
|
||||
'Aktif' AS ""Title"",
|
||||
N'Aktif' AS ""Title"",
|
||||
COUNT(""Id"") AS ""Value"",
|
||||
'blue' AS ""Color"",
|
||||
'Aktif Kullanıcılar' AS ""SubTitle"",
|
||||
N'Aktif Kullanıcılar' AS ""SubTitle"",
|
||||
'FaUserCheck' AS ""Icon""
|
||||
FROM ""AbpUsers""
|
||||
WHERE ""IsActive"" = 'true'
|
||||
|
|
@ -851,10 +851,10 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep
|
|||
UNION ALL
|
||||
|
||||
SELECT
|
||||
'Pasif' AS ""Title"",
|
||||
N'Pasif' AS ""Title"",
|
||||
COUNT(""Id"") AS ""Value"",
|
||||
'green' AS ""Color"",
|
||||
'Pasif Kullanıcılar' AS ""SubTitle"",
|
||||
N'Pasif Kullanıcılar' AS ""SubTitle"",
|
||||
'FaUserSlash' AS ""Icon""
|
||||
FROM ""AbpUsers""
|
||||
WHERE ""IsActive"" = 'false'
|
||||
|
|
@ -862,10 +862,10 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep
|
|||
UNION ALL
|
||||
|
||||
SELECT
|
||||
'Doğrulama' AS ""Title"",
|
||||
N'Doğrulama' AS ""Title"",
|
||||
COUNT(""Id"") AS ""Value"",
|
||||
'purple' AS ""Color"",
|
||||
'Yönetici Doğrulaması bekleyenler' AS ""SubTitle"",
|
||||
N'Doğrulama bekleyenler' AS ""SubTitle"",
|
||||
'FaUserClock' AS ""Icon""
|
||||
FROM ""AbpUsers""
|
||||
WHERE ""IsVerified"" = 'false';
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ export default function Widget({
|
|||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-gray-600 uppercase tracking-wide">{title}</p>
|
||||
<p className="text-sm font-semibold text-gray-600">{title}</p>
|
||||
<p className={`${valueClassName} font-bold mt-1 ${colorMap[safeColor].text}`}>{value}</p>
|
||||
<p className="text-sm text-gray-500 mt-1">{subTitle}</p>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -8,13 +8,25 @@ export default function WidgetGroup({ widgetGroups }: { widgetGroups: WidgetGrou
|
|||
|
||||
return (
|
||||
<div>
|
||||
{/* Mobile responsive override: below sm breakpoint each widget spans 6/12 (2 per row) */}
|
||||
<style>{`
|
||||
@media (max-width: 639px) {
|
||||
.widget-item { grid-column: span 6 / span 6 !important; }
|
||||
}
|
||||
`}</style>
|
||||
|
||||
{widgetGroups.map((group, gIdx) => (
|
||||
<div
|
||||
key={gIdx}
|
||||
className={classNames(`grid grid-cols-12 gap-${group.colGap} ${group.className || ''}`)}
|
||||
className={classNames(`grid gap-${group.colGap} ${group.className || ''}`)}
|
||||
style={{ gridTemplateColumns: 'repeat(12, minmax(0, 1fr))' }}
|
||||
>
|
||||
{group.items.map((item: WidgetEditDto, order: number) => (
|
||||
<div key={`${gIdx}-${order}`} className={classNames(`col-span-${group.colSpan}`)}>
|
||||
<div
|
||||
key={`${gIdx}-${order}`}
|
||||
className="widget-item min-w-0"
|
||||
style={{ gridColumn: `span ${group.colSpan} / span ${group.colSpan}` }}
|
||||
>
|
||||
<Widget
|
||||
title={item.title}
|
||||
value={item.value}
|
||||
|
|
@ -36,3 +48,4 @@ export default function WidgetGroup({ widgetGroups }: { widgetGroups: WidgetGrou
|
|||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -452,7 +452,7 @@ function OrgChartNode({
|
|||
data-card=""
|
||||
data-depth={depth}
|
||||
data-users-visible={showUsers ? 'true' : 'false'}
|
||||
className={`relative bg-white border ${borderColor} rounded-xl shadow-sm w-52 hover:shadow-md transition-shadow`}
|
||||
className={`relative bg-white border ${borderColor} rounded-xl shadow-sm w-36 sm:w-44 md:w-52 hover:shadow-md transition-shadow`}
|
||||
style={{ cursor: dragging ? 'grabbing' : 'grab' }}
|
||||
>
|
||||
{/* Header bar */}
|
||||
|
|
@ -522,7 +522,7 @@ function OrgChartNode({
|
|||
|
||||
{/* Children */}
|
||||
{hasChildren && !collapsed && (
|
||||
<div className="mt-10 flex items-start gap-10">
|
||||
<div className="mt-6 sm:mt-8 md:mt-10 flex items-start gap-4 sm:gap-6 md:gap-10">
|
||||
{node.children.map((child, idx) => {
|
||||
const childPosition = positions[child.id] ?? { x: 0, y: 0 }
|
||||
|
||||
|
|
@ -695,7 +695,7 @@ function OrgChartTree({
|
|||
}
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className="relative p-6">
|
||||
<div ref={containerRef} className="relative p-3 sm:p-4 md:p-6">
|
||||
<svg className="pointer-events-none absolute inset-0 w-full h-full overflow-visible">
|
||||
{paths.map((path, index) => (
|
||||
<path
|
||||
|
|
@ -710,7 +710,7 @@ function OrgChartTree({
|
|||
))}
|
||||
</svg>
|
||||
|
||||
<div className="relative z-10 flex gap-8 justify-center flex-wrap items-start">
|
||||
<div className="relative z-10 flex gap-4 sm:gap-6 md:gap-8 justify-center flex-wrap items-start">
|
||||
{nodes.map((root) => (
|
||||
<OrgChartNode
|
||||
key={root.id}
|
||||
|
|
@ -804,73 +804,73 @@ const OrgChart = () => {
|
|||
|
||||
<div className="flex flex-col h-full">
|
||||
{/* Toolbar */}
|
||||
<div className="flex items-center justify-between pb-1 border-b">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2 pb-1 border-b">
|
||||
<div className="flex items-center gap-3 min-w-0">
|
||||
{MenuIcon}
|
||||
<h4 className="text-sm font-medium">
|
||||
<h4 className="text-sm font-medium truncate">
|
||||
{translate('::App.Definitions.OrgChart') || 'Organizasyon Şeması'}
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<div className="flex items-center bg-slate-100 rounded-lg p-1 gap-1">
|
||||
<button
|
||||
onClick={() => setMode('department')}
|
||||
className={`flex items-center gap-2 px-3 py-1.5 rounded-md text-sm font-medium transition-colors ${
|
||||
className={`flex items-center gap-1.5 px-2 py-1.5 rounded-md text-xs sm:text-sm font-medium transition-colors ${
|
||||
mode === 'department'
|
||||
? 'bg-blue-600 text-white shadow-sm'
|
||||
: 'text-slate-600 hover:text-slate-800'
|
||||
}`}
|
||||
>
|
||||
<FaBuilding className="w-3.5 h-3.5" />
|
||||
{translate('::App.Hr.Department') || 'Departman'}
|
||||
<FaBuilding className="w-3.5 h-3.5 flex-shrink-0" />
|
||||
<span className="hidden sm:inline">{translate('::App.Hr.Department') || 'Departman'}</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setMode('jobPosition')}
|
||||
className={`flex items-center gap-2 px-3 py-1.5 rounded-md text-sm font-medium transition-colors ${
|
||||
className={`flex items-center gap-1.5 px-2 py-1.5 rounded-md text-xs sm:text-sm font-medium transition-colors ${
|
||||
mode === 'jobPosition'
|
||||
? 'bg-purple-600 text-white shadow-sm'
|
||||
: 'text-slate-600 hover:text-slate-800'
|
||||
}`}
|
||||
>
|
||||
<FaBriefcase className="w-3.5 h-3.5" />
|
||||
{translate('::App.Hr.JobPosition') || 'Pozisyon'}
|
||||
<FaBriefcase className="w-3.5 h-3.5 flex-shrink-0" />
|
||||
<span className="hidden sm:inline">{translate('::App.Hr.JobPosition') || 'Pozisyon'}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center bg-slate-100 rounded-lg p-1 gap-1">
|
||||
<label className="flex items-center gap-2 px-3 py-1.5 rounded-md text-sm text-slate-600 select-none">
|
||||
<label className="flex items-center gap-2 px-2 py-1.5 rounded-md text-xs sm:text-sm text-slate-600 select-none cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={showUsers}
|
||||
onChange={(e) => setShowUsers(e.target.checked)}
|
||||
className="h-4 w-4 rounded border-slate-300 text-blue-600 focus:ring-blue-500"
|
||||
/>
|
||||
<span>{translate('::App.Definitions.OrgChart.ShowUsers') || 'Kullanıcılar'}</span>
|
||||
<span className="hidden sm:inline">{translate('::App.Definitions.OrgChart.ShowUsers') || 'Kullanıcılar'}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center bg-slate-100 rounded-lg p-1 gap-1">
|
||||
<button
|
||||
onClick={handleZoomOut}
|
||||
className="p-2 rounded-md text-slate-600 hover:text-slate-800 hover:bg-white"
|
||||
className="p-1.5 sm:p-2 rounded-md text-slate-600 hover:text-slate-800 hover:bg-white"
|
||||
title="Zoom Out"
|
||||
>
|
||||
<FaSearchMinus className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
<span className="text-xs font-medium text-slate-600 px-1 min-w-[46px] text-center">
|
||||
<span className="text-xs font-medium text-slate-600 px-1 min-w-[36px] sm:min-w-[46px] text-center">
|
||||
{Math.round(zoom * 100)}%
|
||||
</span>
|
||||
<button
|
||||
onClick={handleZoomIn}
|
||||
className="p-2 rounded-md text-slate-600 hover:text-slate-800 hover:bg-white"
|
||||
className="p-1.5 sm:p-2 rounded-md text-slate-600 hover:text-slate-800 hover:bg-white"
|
||||
title="Zoom In"
|
||||
>
|
||||
<FaSearchPlus className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
<button
|
||||
onClick={handleZoomReset}
|
||||
className="p-2 rounded-md text-slate-600 hover:text-slate-800 hover:bg-white"
|
||||
className="p-1.5 sm:p-2 rounded-md text-slate-600 hover:text-slate-800 hover:bg-white"
|
||||
title="Reset Zoom"
|
||||
>
|
||||
<FaUndo className="w-3.5 h-3.5" />
|
||||
|
|
@ -880,11 +880,11 @@ const OrgChart = () => {
|
|||
<button
|
||||
onClick={handleExportJpg}
|
||||
disabled={exporting || loading}
|
||||
className="flex items-center gap-2 px-3 py-1.5 rounded-lg text-sm font-medium bg-slate-100 text-slate-600 hover:bg-slate-200 disabled:opacity-50 transition-colors"
|
||||
className="flex items-center gap-1.5 px-2 py-1.5 rounded-lg text-xs sm:text-sm font-medium bg-slate-100 text-slate-600 hover:bg-slate-200 disabled:opacity-50 transition-colors"
|
||||
title="JPG olarak indir"
|
||||
>
|
||||
<FaFileImage className="w-3.5 h-3.5" />
|
||||
{exporting ? 'İşleniyor…' : 'Export'}
|
||||
<FaFileImage className="w-3.5 h-3.5 flex-shrink-0" />
|
||||
<span className="hidden sm:inline">{exporting ? 'İşleniyor…' : 'Export'}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -109,21 +109,23 @@ const WizardFileManager = () => {
|
|||
{/* ── Header ─────────────────────────────────────────────── */}
|
||||
<div
|
||||
className={classNames(
|
||||
'flex items-center gap-2 pb-1 border-b',
|
||||
'flex flex-col sm:flex-row sm:items-center gap-2 pb-1 border-b',
|
||||
mode === 'light' ? 'border-gray-200' : 'border-neutral-700',
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
{MenuIcon}
|
||||
<h4 className="text-sm font-medium">
|
||||
<h4 className="text-sm font-medium truncate">
|
||||
{translate('::App.Listforms.WizardManager') || 'Wizard Seed Dosyaları'}
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-1 ml-auto items-center">
|
||||
<div className="flex flex-wrap gap-1 sm:ml-auto items-center">
|
||||
<div className="relative">
|
||||
<FaSearch className="absolute left-2 top-1/2 -translate-y-1/2 text-gray-400 text-xs pointer-events-none" />
|
||||
<Input
|
||||
size="sm"
|
||||
className="pl-6 w-44"
|
||||
className="pl-6 w-36 sm:w-44"
|
||||
placeholder={translate('::App.Platform.Search') || 'Search...'}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
|
|
@ -145,7 +147,7 @@ const WizardFileManager = () => {
|
|||
onClick={() => setShowDbMigrateDialog(true)}
|
||||
title={translate('::App.DbMigrate.StartMessage') || 'Run DB Migration'}
|
||||
>
|
||||
{translate('::ListForms.ListForm.DbMigrate') || 'DB Migrate'}
|
||||
<span className="hidden sm:inline">{translate('::ListForms.ListForm.DbMigrate') || 'DB Migrate'}</span>
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
|
|
@ -154,7 +156,7 @@ const WizardFileManager = () => {
|
|||
className="flex items-center"
|
||||
>
|
||||
<FaPlus className="mr-1" />
|
||||
{translate('::ListForms.Wizard.AddNewRecord') || 'Add New Record'}
|
||||
<span className="hidden sm:inline">{translate('::ListForms.Wizard.AddNewRecord') || 'Add New Record'}</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -181,7 +183,7 @@ const WizardFileManager = () => {
|
|||
return (
|
||||
<div
|
||||
key={f.fileName}
|
||||
className="flex items-center justify-between p-3 rounded-lg border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800"
|
||||
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" />
|
||||
|
|
@ -197,7 +199,7 @@ const WizardFileManager = () => {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 shrink-0 ml-3">
|
||||
<div className="flex items-center gap-2 shrink-0 sm:ml-3">
|
||||
{!f.hasInsertedRecords && (
|
||||
<span
|
||||
title="Bu dosyada izlenen kayıt bilgisi yok. Eski format olabilir."
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import React, { useEffect } from 'react'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||
import { motion } from 'framer-motion'
|
||||
import { FaTimes, FaEye, FaClipboard } from 'react-icons/fa'
|
||||
import { motion, AnimatePresence } from 'framer-motion'
|
||||
import { FaTimes, FaEye, FaClipboard, FaChevronLeft, FaChevronRight } from 'react-icons/fa'
|
||||
import { AnnouncementDto } from '@/proxy/intranet/models'
|
||||
import useLocale from '@/utils/hooks/useLocale'
|
||||
import { currentLocalDate } from '@/utils/dateUtils'
|
||||
|
|
@ -17,11 +17,34 @@ interface AnnouncementModalProps {
|
|||
const AnnouncementModal: React.FC<AnnouncementModalProps> = ({ announcement, onClose }) => {
|
||||
const { translate } = useLocalization()
|
||||
const currentLocale = useLocale()
|
||||
const [lightboxOpen, setLightboxOpen] = useState(false)
|
||||
const [lightboxIndex, setLightboxIndex] = useState(0)
|
||||
|
||||
const openLightbox = (idx: number) => {
|
||||
setLightboxIndex(idx)
|
||||
setLightboxOpen(true)
|
||||
}
|
||||
const closeLightbox = () => setLightboxOpen(false)
|
||||
const prevLightbox = (e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
setLightboxIndex((i) => (i - 1 + images.length) % images.length)
|
||||
}
|
||||
const nextLightbox = (e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
setLightboxIndex((i) => (i + 1) % images.length)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
intranetService.incrementAnnouncementViewCount(announcement.id)
|
||||
}, [announcement.id])
|
||||
|
||||
const images = announcement.imageUrl ? announcement.imageUrl.split('|').filter(Boolean) : []
|
||||
|
||||
const imgSrc = (img: string) =>
|
||||
img.startsWith('data:') || img.startsWith('http://') || img.startsWith('https://') || img.startsWith('/')
|
||||
? img
|
||||
: `data:image/jpeg;base64,${img}`
|
||||
|
||||
const getCategoryColor = (category: string) => {
|
||||
const colors: Record<string, string> = {
|
||||
general: 'bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300',
|
||||
|
|
@ -114,10 +137,8 @@ const AnnouncementModal: React.FC<AnnouncementModalProps> = ({ announcement, onC
|
|||
{/* Content */}
|
||||
<div className="p-6 max-h-[60vh] overflow-y-auto">
|
||||
{/* Images if exist */}
|
||||
{announcement.imageUrl &&
|
||||
{images.length > 0 &&
|
||||
(() => {
|
||||
const images = announcement.imageUrl.split('|').filter(Boolean)
|
||||
if (images.length === 0) return null
|
||||
const getGridClass = (count: number) => {
|
||||
if (count === 1) return ''
|
||||
if (count === 2) return 'grid grid-cols-2 gap-2'
|
||||
|
|
@ -135,16 +156,10 @@ const AnnouncementModal: React.FC<AnnouncementModalProps> = ({ announcement, onC
|
|||
{images.map((img, idx) => (
|
||||
<img
|
||||
key={idx}
|
||||
src={
|
||||
img.startsWith('data:') ||
|
||||
img.startsWith('http://') ||
|
||||
img.startsWith('https://') ||
|
||||
img.startsWith('/')
|
||||
? img
|
||||
: `data:image/jpeg;base64,${img}`
|
||||
}
|
||||
src={imgSrc(img)}
|
||||
alt={`${announcement.title} ${images.length > 1 ? idx + 1 : ''}`.trim()}
|
||||
className={getImgClass(images.length, idx)}
|
||||
className={`${getImgClass(images.length, idx)} cursor-zoom-in hover:opacity-90 transition-opacity`}
|
||||
onClick={() => openLightbox(idx)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
|
@ -236,6 +251,71 @@ const AnnouncementModal: React.FC<AnnouncementModalProps> = ({ announcement, onC
|
|||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* Lightbox */}
|
||||
<AnimatePresence>
|
||||
{lightboxOpen && (
|
||||
<>
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="fixed inset-0 bg-black/95 z-[60]"
|
||||
onClick={closeLightbox}
|
||||
/>
|
||||
<div className="fixed inset-0 z-[70] flex items-center justify-center">
|
||||
<button
|
||||
onClick={closeLightbox}
|
||||
className="absolute top-4 right-4 p-2 text-white hover:text-gray-300 transition-colors z-10"
|
||||
>
|
||||
<FaTimes className="w-8 h-8" />
|
||||
</button>
|
||||
{images.length > 1 && (
|
||||
<>
|
||||
<button
|
||||
onClick={prevLightbox}
|
||||
className="absolute left-4 top-1/2 -translate-y-1/2 p-3 bg-black/50 hover:bg-black/70 text-white rounded-full transition-colors z-10"
|
||||
>
|
||||
<FaChevronLeft className="w-6 h-6" />
|
||||
</button>
|
||||
<button
|
||||
onClick={nextLightbox}
|
||||
className="absolute right-4 top-1/2 -translate-y-1/2 p-3 bg-black/50 hover:bg-black/70 text-white rounded-full transition-colors z-10"
|
||||
>
|
||||
<FaChevronRight className="w-6 h-6" />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
<motion.img
|
||||
key={lightboxIndex}
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
src={imgSrc(images[lightboxIndex])}
|
||||
alt={`${announcement.title} ${lightboxIndex + 1}`}
|
||||
className="max-w-[90vw] max-h-[90vh] object-contain rounded-lg"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
{images.length > 1 && (
|
||||
<div className="absolute bottom-4 left-1/2 -translate-x-1/2 flex gap-2">
|
||||
{images.map((_, idx) => (
|
||||
<button
|
||||
key={idx}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
setLightboxIndex(idx)
|
||||
}}
|
||||
className={`w-2.5 h-2.5 rounded-full transition-all ${
|
||||
idx === lightboxIndex ? 'bg-white' : 'bg-white/40 hover:bg-white/70'
|
||||
}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -65,6 +65,73 @@ const cellTemplateMultiValue = (
|
|||
}
|
||||
}
|
||||
|
||||
// Hover preview overlay — singleton, tüm grid hücreleri tarafından paylaşılır
|
||||
let __imgPreviewEl: HTMLDivElement | null = null
|
||||
|
||||
function getImgPreview(): HTMLDivElement {
|
||||
if (!__imgPreviewEl) {
|
||||
const el = document.createElement('div')
|
||||
el.id = '__cellImgPreview'
|
||||
el.style.cssText = [
|
||||
'position:fixed',
|
||||
'z-index:99999',
|
||||
'display:none',
|
||||
'pointer-events:none',
|
||||
'background:#fff',
|
||||
'border:1px solid #d1d5db',
|
||||
'border-radius:8px',
|
||||
'box-shadow:0 8px 32px rgba(0,0,0,0.22)',
|
||||
'padding:4px',
|
||||
'max-width:320px',
|
||||
'max-height:320px',
|
||||
'overflow:hidden',
|
||||
'transition:opacity 0.15s ease',
|
||||
'opacity:0',
|
||||
].join(';')
|
||||
const img = document.createElement('img')
|
||||
img.style.cssText =
|
||||
'display:block;max-width:312px;max-height:312px;object-fit:contain;border-radius:4px;'
|
||||
el.appendChild(img)
|
||||
document.body.appendChild(el)
|
||||
__imgPreviewEl = el
|
||||
}
|
||||
return __imgPreviewEl
|
||||
}
|
||||
|
||||
function showImgPreview(src: string, e: MouseEvent) {
|
||||
const el = getImgPreview()
|
||||
const imgEl = el.querySelector('img') as HTMLImageElement
|
||||
if (imgEl.src !== src) imgEl.src = src
|
||||
|
||||
const GAP = 12
|
||||
const vw = window.innerWidth
|
||||
const vh = window.innerHeight
|
||||
|
||||
el.style.opacity = '0'
|
||||
el.style.display = 'block'
|
||||
|
||||
const pw = el.offsetWidth || 320
|
||||
const ph = el.offsetHeight || 320
|
||||
let left = e.clientX + GAP
|
||||
let top = e.clientY + GAP
|
||||
|
||||
if (left + pw > vw - 8) left = e.clientX - pw - GAP
|
||||
if (top + ph > vh - 8) top = e.clientY - ph - GAP
|
||||
if (left < 8) left = 8
|
||||
if (top < 8) top = 8
|
||||
|
||||
el.style.left = `${left}px`
|
||||
el.style.top = `${top}px`
|
||||
el.style.opacity = '1'
|
||||
}
|
||||
|
||||
function hideImgPreview() {
|
||||
if (__imgPreviewEl) {
|
||||
__imgPreviewEl.style.opacity = '0'
|
||||
__imgPreviewEl.style.display = 'none'
|
||||
}
|
||||
}
|
||||
|
||||
const cellTemplateImage = (
|
||||
cellElement: HTMLElement,
|
||||
cellInfo: DataGridTypes.ColumnCellTemplateData<any, any>,
|
||||
|
|
@ -74,21 +141,29 @@ const cellTemplateImage = (
|
|||
? cellInfo.value.filter(Boolean)
|
||||
: [cellInfo.value].filter(Boolean)
|
||||
|
||||
const col = cellInfo.column as any
|
||||
const imgOptions = col?.extras?.imageUploadOptions ?? {}
|
||||
const w: number = imgOptions.width ?? 40
|
||||
const h: number = imgOptions.height ?? 40
|
||||
|
||||
const imgs = urls
|
||||
.map(
|
||||
(url) =>
|
||||
`<img src="${url}" alt="" style="width:${w}px;height:${h}px;object-fit:cover;border-radius:4px;border:1px solid #ddd;margin:2px;vertical-align:middle;display:inline-block;" />`,
|
||||
)
|
||||
.join('')
|
||||
//const col = cellInfo.column as any
|
||||
//const imgOptions = col?.extras?.imageUploadOptions ?? {}
|
||||
//const w: number = imgOptions.width ?? 40
|
||||
//const h: number = imgOptions.height ?? 40
|
||||
const w: number = 40
|
||||
const h: number = 40
|
||||
|
||||
cellElement.style.cssText += 'display:flex;flex-wrap:wrap;align-items:center;gap:4px;'
|
||||
cellElement.innerHTML = imgs
|
||||
cellElement.title = urls.join(', ')
|
||||
cellElement.innerHTML = ''
|
||||
//cellElement.title = urls.join(', ')
|
||||
|
||||
urls.forEach((url) => {
|
||||
const img = document.createElement('img')
|
||||
img.src = url
|
||||
img.alt = ''
|
||||
img.style.cssText = `width:${w}px;height:${h}px;object-fit:cover;border-radius:4px;border:1px solid #ddd;margin:2px;vertical-align:middle;display:inline-block;cursor:zoom-in;`
|
||||
|
||||
img.addEventListener('mouseenter', (e) => showImgPreview(url, e as MouseEvent))
|
||||
img.addEventListener('mousemove', (e) => showImgPreview(url, e as MouseEvent))
|
||||
img.addEventListener('mouseleave', hideImgPreview)
|
||||
|
||||
cellElement.appendChild(img)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -211,7 +211,7 @@ export function MenuAddDialog({
|
|||
|
||||
return (
|
||||
<Dialog isOpen={isOpen} onClose={onClose} onRequestClose={onClose} width={680}>
|
||||
<div className="flex flex-col gap-5 p-5">
|
||||
<div className="flex flex-col gap-5 p-1">
|
||||
{/* Header */}
|
||||
<div className="flex items-center gap-2 pb-1 border-b border-gray-100 dark:border-gray-700">
|
||||
<FaPlus className="text-green-500 text-sm" />
|
||||
|
|
|
|||
Loading…
Reference in a new issue