Widgets düzenlemeleri
This commit is contained in:
parent
bf5088609e
commit
a2a0d46488
14 changed files with 359 additions and 334 deletions
|
|
@ -352,10 +352,11 @@ const MessengerWidget = () => {
|
||||||
if (!auth.session.signedIn) return null
|
if (!auth.session.signedIn) return null
|
||||||
|
|
||||||
const panelClassName = maximized
|
const panelClassName = maximized
|
||||||
? 'pointer-events-auto fixed inset-2 z-[51] overflow-hidden rounded-xl border border-blue-300 bg-slate-50 shadow-[0_24px_80px_rgba(15,23,42,0.45)] dark:border-blue-800 dark:bg-slate-900 sm:inset-4'
|
? 'pointer-events-auto fixed inset-2 z-[51] overflow-hidden rounded-xl border border-gray-200 bg-white/80 shadow-[0_24px_80px_rgba(15,23,42,0.35)] backdrop-blur-xl dark:border-gray-700 dark:bg-gray-900/85 sm:inset-4'
|
||||||
: 'pointer-events-auto fixed inset-x-2 bottom-20 top-3 z-[51] overflow-hidden rounded-xl border border-blue-300 bg-slate-50 shadow-[0_24px_80px_rgba(15,23,42,0.45)] dark:border-blue-800 dark:bg-slate-900 sm:inset-auto sm:bottom-20 sm:right-5 sm:h-[min(75vh,680px)] sm:w-[min(78vw,800px)]'
|
: 'pointer-events-auto fixed inset-x-2 bottom-20 top-3 z-[51] overflow-hidden rounded-xl border border-gray-200 bg-white/80 shadow-[0_24px_80px_rgba(15,23,42,0.35)] backdrop-blur-xl dark:border-gray-700 dark:bg-gray-900/85 sm:inset-auto sm:bottom-20 sm:right-5 sm:h-[min(75vh,680px)] sm:w-[min(78vw,800px)]'
|
||||||
|
|
||||||
const headerActionClassName = 'text-white hover:bg-white/15 hover:text-white active:bg-white/25'
|
const headerActionClassName =
|
||||||
|
'text-gray-600 hover:bg-gray-900/10 hover:text-gray-900 active:bg-gray-900/15 dark:text-gray-200 dark:hover:bg-white/10 dark:hover:text-white dark:active:bg-white/15'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
@ -372,13 +373,13 @@ const MessengerWidget = () => {
|
||||||
<div className={panelClassName} role="dialog" aria-modal="true">
|
<div className={panelClassName} role="dialog" aria-modal="true">
|
||||||
<div className="flex h-full min-h-0">
|
<div className="flex h-full min-h-0">
|
||||||
<aside
|
<aside
|
||||||
className={`min-h-0 w-full shrink-0 flex-col bg-slate-100 dark:bg-slate-950 sm:flex sm:w-72 ${
|
className={`min-h-0 w-full shrink-0 flex-col bg-gray-50/75 backdrop-blur-xl dark:bg-gray-950/70 sm:flex sm:w-72 ${
|
||||||
selectedIds.length > 0 ? 'hidden' : 'flex'
|
selectedIds.length > 0 ? 'hidden' : 'flex'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={
|
className={
|
||||||
'flex h-16 items-center justify-between border-b border-r-2 border-blue-500/40 bg-blue-700 px-4 text-white dark:bg-blue-950'
|
'flex h-16 items-center justify-between border-b border-r-2 border-gray-200 bg-white/70 px-4 text-gray-900 backdrop-blur-xl dark:border-gray-700 dark:bg-gray-800/70 dark:text-gray-100'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="flex min-w-0 items-center gap-3">
|
<div className="flex min-w-0 items-center gap-3">
|
||||||
|
|
@ -388,12 +389,12 @@ const MessengerWidget = () => {
|
||||||
src={auth.user.avatar || AVATAR_URL(auth.user.id, tenantId)}
|
src={auth.user.avatar || AVATAR_URL(auth.user.id, tenantId)}
|
||||||
/>
|
/>
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<div className="truncate text-sm font-semibold text-white">
|
<div className="truncate text-sm font-semibold text-gray-900 dark:text-gray-100">
|
||||||
{auth.user.name ||
|
{auth.user.name ||
|
||||||
auth.user.userName ||
|
auth.user.userName ||
|
||||||
translate('::MessengerWidget.Title')}
|
translate('::MessengerWidget.Title')}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1 text-xs text-blue-100">
|
<div className="flex items-center gap-1 text-xs text-gray-500 dark:text-gray-300">
|
||||||
<span
|
<span
|
||||||
className={`h-2 w-2 rounded-full ${connected ? 'bg-emerald-500' : 'bg-gray-400'}`}
|
className={`h-2 w-2 rounded-full ${connected ? 'bg-emerald-500' : 'bg-gray-400'}`}
|
||||||
/>
|
/>
|
||||||
|
|
@ -521,7 +522,7 @@ const MessengerWidget = () => {
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={
|
className={
|
||||||
'flex h-16 items-center justify-between border-b border-blue-500/40 bg-blue-700 px-4 text-white dark:bg-blue-950'
|
'flex h-16 items-center justify-between border-b border-gray-200 bg-white/70 px-4 text-gray-900 backdrop-blur-xl dark:border-gray-700 dark:bg-gray-800/70 dark:text-gray-100'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="flex min-w-0 items-center gap-3">
|
<div className="flex min-w-0 items-center gap-3">
|
||||||
|
|
@ -548,14 +549,14 @@ const MessengerWidget = () => {
|
||||||
</Avatar>
|
</Avatar>
|
||||||
)}
|
)}
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<div className="truncate text-sm font-semibold text-white">
|
<div className="truncate text-sm font-semibold text-gray-900 dark:text-gray-100">
|
||||||
{selectedContacts.length === 1
|
{selectedContacts.length === 1
|
||||||
? selectedContacts[0].fullName
|
? selectedContacts[0].fullName
|
||||||
: selectedContacts.length > 1
|
: selectedContacts.length > 1
|
||||||
? `${selectedContacts.length} ${translate('::MessengerWidget.PeopleSelected')}`
|
? `${selectedContacts.length} ${translate('::MessengerWidget.PeopleSelected')}`
|
||||||
: translate('::MessengerWidget.Chat')}
|
: translate('::MessengerWidget.Chat')}
|
||||||
</div>
|
</div>
|
||||||
<div className="truncate text-xs text-blue-100">
|
<div className="truncate text-xs text-gray-500 dark:text-gray-300">
|
||||||
{selectedContacts.length === 1
|
{selectedContacts.length === 1
|
||||||
? `${selectedContacts[0].isOnline ? translate('::MessengerWidget.Online') : translate('::MessengerWidget.Offline')} - ${
|
? `${selectedContacts[0].isOnline ? translate('::MessengerWidget.Online') : translate('::MessengerWidget.Offline')} - ${
|
||||||
selectedContacts[0].email || selectedContacts[0].userName
|
selectedContacts[0].email || selectedContacts[0].userName
|
||||||
|
|
@ -591,7 +592,7 @@ const MessengerWidget = () => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-1 overflow-y-auto bg-slate-50 px-4 py-3 dark:bg-slate-900">
|
<div className="flex-1 overflow-y-auto bg-white/55 px-4 py-3 backdrop-blur-xl dark:bg-gray-900/55">
|
||||||
{visibleMessages.length === 0 && (
|
{visibleMessages.length === 0 && (
|
||||||
<div className="flex h-full items-center justify-center text-sm text-gray-400">
|
<div className="flex h-full items-center justify-center text-sm text-gray-400">
|
||||||
{translate('::MessengerWidget.NoMessagesYet')}
|
{translate('::MessengerWidget.NoMessagesYet')}
|
||||||
|
|
|
||||||
|
|
@ -284,7 +284,7 @@ const FormButtons = (props: {
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="solid"
|
variant="solid"
|
||||||
color="gray-800"
|
color="purple-500"
|
||||||
icon={<FaSistrix />}
|
icon={<FaSistrix />}
|
||||||
title={translate('::App.Reports.Form')}
|
title={translate('::App.Reports.Form')}
|
||||||
onClick={openDynamicGridReport}
|
onClick={openDynamicGridReport}
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||||
import useLocale from '@/utils/hooks/useLocale'
|
import useLocale from '@/utils/hooks/useLocale'
|
||||||
import { currentLocalDate } from '@/utils/dateUtils'
|
import { currentLocalDate } from '@/utils/dateUtils'
|
||||||
import { useStoreState } from '@/store/store'
|
import { useStoreState } from '@/store/store'
|
||||||
import UpcomingEvents from './widgets/UpcomingEvents'
|
import Events from './widgets/Events'
|
||||||
import Button from '@/components/ui/Button'
|
import Button from '@/components/ui/Button'
|
||||||
|
|
||||||
dayjs.extend(relativeTime)
|
dayjs.extend(relativeTime)
|
||||||
|
|
@ -104,6 +104,38 @@ const IntranetDashboard: React.FC = () => {
|
||||||
setSelectedSurvey(null)
|
setSelectedSurvey(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleEventLikeChange = (updatedEvent: EventDto) => {
|
||||||
|
setSelectedEvent((current) => (current ? { ...current, ...updatedEvent } : updatedEvent))
|
||||||
|
setIntranetDashboard((current) =>
|
||||||
|
current
|
||||||
|
? {
|
||||||
|
...current,
|
||||||
|
events: current.events.map((event) =>
|
||||||
|
event.id === updatedEvent.id ? { ...event, ...updatedEvent } : event,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
: current,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAnnouncementLikeChange = (updatedAnnouncement: AnnouncementDto) => {
|
||||||
|
setSelectedAnnouncement((current) =>
|
||||||
|
current ? { ...current, ...updatedAnnouncement } : updatedAnnouncement,
|
||||||
|
)
|
||||||
|
setIntranetDashboard((current) =>
|
||||||
|
current
|
||||||
|
? {
|
||||||
|
...current,
|
||||||
|
announcements: current.announcements.map((announcement) =>
|
||||||
|
announcement.id === updatedAnnouncement.id
|
||||||
|
? { ...announcement, ...updatedAnnouncement }
|
||||||
|
: announcement,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
: current,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Widget metadata (component'lar yerine sadece meta bilgiler)
|
// Widget metadata (component'lar yerine sadece meta bilgiler)
|
||||||
const widgetMetadata = [
|
const widgetMetadata = [
|
||||||
{ id: 'today-birthdays', permission: 'AbpIdentity.Users.Widget', column: 'left' },
|
{ id: 'today-birthdays', permission: 'AbpIdentity.Users.Widget', column: 'left' },
|
||||||
|
|
@ -247,7 +279,7 @@ const IntranetDashboard: React.FC = () => {
|
||||||
switch (widgetId) {
|
switch (widgetId) {
|
||||||
case 'upcoming-events':
|
case 'upcoming-events':
|
||||||
return (
|
return (
|
||||||
<UpcomingEvents
|
<Events
|
||||||
events={intranetDashboard?.events || []}
|
events={intranetDashboard?.events || []}
|
||||||
onEventClick={setSelectedEvent}
|
onEventClick={setSelectedEvent}
|
||||||
/>
|
/>
|
||||||
|
|
@ -640,7 +672,11 @@ const IntranetDashboard: React.FC = () => {
|
||||||
|
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{selectedEvent && (
|
{selectedEvent && (
|
||||||
<EventModal event={selectedEvent} onClose={() => setSelectedEvent(null)} />
|
<EventModal
|
||||||
|
event={selectedEvent}
|
||||||
|
onClose={() => setSelectedEvent(null)}
|
||||||
|
onLikeChange={handleEventLikeChange}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
|
|
||||||
|
|
@ -649,6 +685,7 @@ const IntranetDashboard: React.FC = () => {
|
||||||
<AnnouncementModal
|
<AnnouncementModal
|
||||||
announcement={selectedAnnouncement}
|
announcement={selectedAnnouncement}
|
||||||
onClose={() => setSelectedAnnouncement(null)}
|
onClose={() => setSelectedAnnouncement(null)}
|
||||||
|
onLikeChange={handleAnnouncementLikeChange}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,13 @@ const MediaManager: React.FC<MediaManagerProps> = ({ media, onChange, onClose })
|
||||||
const [activeTab, setActiveTab] = useState<'upload' | 'url'>('upload')
|
const [activeTab, setActiveTab] = useState<'upload' | 'url'>('upload')
|
||||||
const [urlInput, setUrlInput] = useState('')
|
const [urlInput, setUrlInput] = useState('')
|
||||||
const [mediaType, setMediaType] = useState<'image' | 'video'>('image')
|
const [mediaType, setMediaType] = useState<'image' | 'video'>('image')
|
||||||
|
const tabClassName = (tab: 'upload' | 'url') =>
|
||||||
|
classNames(
|
||||||
|
'!h-auto !items-center gap-2 !rounded-none border-b-2 !bg-transparent !px-4 !py-3 font-medium transition-colors hover:!bg-gray-50 active:!bg-transparent focus:!bg-transparent dark:hover:!bg-gray-700/50',
|
||||||
|
activeTab === tab
|
||||||
|
? '!border-blue-600 !text-blue-600 dark:!border-blue-400 dark:!text-blue-300'
|
||||||
|
: '!border-transparent text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-100',
|
||||||
|
)
|
||||||
|
|
||||||
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const files = e.target.files
|
const files = e.target.files
|
||||||
|
|
@ -92,12 +99,7 @@ const MediaManager: React.FC<MediaManagerProps> = ({ media, onChange, onClose })
|
||||||
variant="plain"
|
variant="plain"
|
||||||
shape="none"
|
shape="none"
|
||||||
icon={<FaUpload className="h-5 w-5" />}
|
icon={<FaUpload className="h-5 w-5" />}
|
||||||
className={classNames(
|
className={tabClassName('upload')}
|
||||||
'!h-auto !items-center gap-2 !rounded-none border-b-2 !px-4 !py-3 font-medium transition-colors',
|
|
||||||
activeTab === 'upload'
|
|
||||||
? 'border-blue-600 !text-blue-600'
|
|
||||||
: 'border-transparent text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-200',
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
{translate('::App.Platform.Intranet.SocialWall.MediaManager.SelectFromComputer')}
|
{translate('::App.Platform.Intranet.SocialWall.MediaManager.SelectFromComputer')}
|
||||||
|
|
@ -108,12 +110,7 @@ const MediaManager: React.FC<MediaManagerProps> = ({ media, onChange, onClose })
|
||||||
variant="plain"
|
variant="plain"
|
||||||
shape="none"
|
shape="none"
|
||||||
icon={<FaLink className="h-5 w-5" />}
|
icon={<FaLink className="h-5 w-5" />}
|
||||||
className={classNames(
|
className={tabClassName('url')}
|
||||||
'!h-auto !items-center gap-2 !rounded-none border-b-2 !px-4 !py-3 font-medium transition-colors',
|
|
||||||
activeTab === 'url'
|
|
||||||
? 'border-blue-600 !text-blue-600'
|
|
||||||
: 'border-transparent text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-200',
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
<span>{translate('::App.Platform.Intranet.SocialWall.MediaManager.AddByUrl')}</span>
|
<span>{translate('::App.Platform.Intranet.SocialWall.MediaManager.AddByUrl')}</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
||||||
|
|
@ -321,12 +321,15 @@ const PostItem: React.FC<PostItemProps> = ({ post, onLike, onComment, onDelete,
|
||||||
onClick={() => onLike(post.id)}
|
onClick={() => onLike(post.id)}
|
||||||
variant="plain"
|
variant="plain"
|
||||||
shape="none"
|
shape="none"
|
||||||
icon={post.isLiked ? <FaHeart className="h-5 w-5" /> : <FaRegHeart className="h-5 w-5" />}
|
icon={
|
||||||
|
post.isLiked ? (
|
||||||
|
<FaHeart className="h-5 w-5 text-red-500" />
|
||||||
|
) : (
|
||||||
|
<FaRegHeart className="h-5 w-5" />
|
||||||
|
)
|
||||||
|
}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'!h-auto !items-center gap-2 !rounded-none !px-0 !py-0 transition-colors hover:!bg-transparent active:!bg-transparent focus:!bg-transparent',
|
'!h-auto !items-center gap-2 !rounded-none !border-0 !bg-transparent !px-0 !py-0 text-gray-600 transition-colors hover:!bg-transparent active:!bg-transparent focus:!bg-transparent dark:text-gray-400',
|
||||||
post.isLiked
|
|
||||||
? 'text-red-600 hover:text-red-700'
|
|
||||||
: 'text-gray-600 dark:text-gray-400 hover:text-red-600',
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<span className="text-sm font-medium">{postLikeCount}</span>
|
<span className="text-sm font-medium">{postLikeCount}</span>
|
||||||
|
|
@ -337,7 +340,7 @@ const PostItem: React.FC<PostItemProps> = ({ post, onLike, onComment, onDelete,
|
||||||
variant="plain"
|
variant="plain"
|
||||||
shape="none"
|
shape="none"
|
||||||
icon={<FaRegCommentAlt className="h-5 w-5" />}
|
icon={<FaRegCommentAlt className="h-5 w-5" />}
|
||||||
className="!h-auto !items-center gap-2 !rounded-none !px-0 !py-0 text-gray-600 transition-colors hover:!bg-transparent hover:text-blue-600 active:!bg-transparent focus:!bg-transparent dark:text-gray-400"
|
className="!h-auto !items-center gap-2 !rounded-none !border-0 !bg-transparent !px-0 !py-0 text-gray-600 transition-colors hover:!bg-transparent hover:text-blue-600 active:!bg-transparent focus:!bg-transparent dark:text-gray-400"
|
||||||
>
|
>
|
||||||
<span className="text-sm font-medium">{postComments.length}</span>
|
<span className="text-sm font-medium">{postComments.length}</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,6 @@
|
||||||
import React, { useEffect, useRef, useState } from 'react'
|
import React, { useEffect, useRef, useState } from 'react'
|
||||||
import { createPortal } from 'react-dom'
|
|
||||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||||
import { motion, AnimatePresence } from 'framer-motion'
|
|
||||||
import {
|
import {
|
||||||
FaTimes,
|
|
||||||
FaEye,
|
FaEye,
|
||||||
FaClipboard,
|
FaClipboard,
|
||||||
FaChevronLeft,
|
FaChevronLeft,
|
||||||
|
|
@ -11,7 +8,6 @@ import {
|
||||||
FaExpand,
|
FaExpand,
|
||||||
FaCommentAlt,
|
FaCommentAlt,
|
||||||
FaPaperPlane,
|
FaPaperPlane,
|
||||||
FaHeart,
|
|
||||||
} from 'react-icons/fa'
|
} from 'react-icons/fa'
|
||||||
import { Button, Dialog } from '@/components/ui'
|
import { Button, Dialog } from '@/components/ui'
|
||||||
import { useDialogContext } from '@/components/ui/Dialog/Dialog'
|
import { useDialogContext } from '@/components/ui/Dialog/Dialog'
|
||||||
|
|
@ -23,15 +19,19 @@ import { AVATAR_URL } from '@/constants/app.constant'
|
||||||
import { intranetService } from '@/services/intranet.service'
|
import { intranetService } from '@/services/intranet.service'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||||
|
import ImageLightbox from './ImageLightbox'
|
||||||
|
import LikeButton from './LikeButton'
|
||||||
|
import { categoryColor, imageSrc } from './widgetUtils'
|
||||||
|
|
||||||
dayjs.extend(relativeTime)
|
dayjs.extend(relativeTime)
|
||||||
|
|
||||||
interface AnnouncementModalProps {
|
interface AnnouncementModalProps {
|
||||||
announcement: AnnouncementDto
|
announcement: AnnouncementDto
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
|
onLikeChange?: (announcement: AnnouncementDto) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
function AnnouncementModalContent({ announcement, onClose }: AnnouncementModalProps) {
|
function AnnouncementModalContent({ announcement, onClose, onLikeChange }: AnnouncementModalProps) {
|
||||||
const { translate } = useLocalization()
|
const { translate } = useLocalization()
|
||||||
const currentLocale = useLocale()
|
const currentLocale = useLocale()
|
||||||
const { isMaximized } = useDialogContext()
|
const { isMaximized } = useDialogContext()
|
||||||
|
|
@ -51,14 +51,6 @@ function AnnouncementModalContent({ announcement, onClose }: AnnouncementModalPr
|
||||||
setLightboxOpen(true)
|
setLightboxOpen(true)
|
||||||
}
|
}
|
||||||
const closeLightbox = () => setLightboxOpen(false)
|
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(() => {
|
useEffect(() => {
|
||||||
intranetService.incrementAnnouncementViewCount(announcement.id)
|
intranetService.incrementAnnouncementViewCount(announcement.id)
|
||||||
|
|
@ -107,8 +99,14 @@ function AnnouncementModalContent({ announcement, onClose }: AnnouncementModalPr
|
||||||
try {
|
try {
|
||||||
const res = await intranetService.likeAnnouncement(announcement.id)
|
const res = await intranetService.likeAnnouncement(announcement.id)
|
||||||
if (res.data) {
|
if (res.data) {
|
||||||
setLikes(res.data.likes ?? 0)
|
const updatedAnnouncement = {
|
||||||
setIsLiked(res.data.isLiked ?? false)
|
...announcement,
|
||||||
|
likes: res.data.likes ?? 0,
|
||||||
|
isLiked: res.data.isLiked ?? false,
|
||||||
|
}
|
||||||
|
setLikes(updatedAnnouncement.likes ?? 0)
|
||||||
|
setIsLiked(updatedAnnouncement.isLiked ?? false)
|
||||||
|
onLikeChange?.(updatedAnnouncement)
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
setIsLiked((prev) => !prev)
|
setIsLiked((prev) => !prev)
|
||||||
|
|
@ -127,25 +125,6 @@ function AnnouncementModalContent({ announcement, onClose }: AnnouncementModalPr
|
||||||
const attachments = Array.isArray(announcement.attachments) ? announcement.attachments : []
|
const attachments = Array.isArray(announcement.attachments) ? announcement.attachments : []
|
||||||
const category = announcement.category || 'general'
|
const category = announcement.category || 'general'
|
||||||
|
|
||||||
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',
|
|
||||||
hr: 'bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300',
|
|
||||||
it: 'bg-orange-100 dark:bg-orange-900/30 text-orange-700 dark:text-orange-300',
|
|
||||||
event: 'bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300',
|
|
||||||
urgent: 'bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-300',
|
|
||||||
}
|
|
||||||
return colors[category] || colors.general
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Dialog.Body className="flex flex-col min-h-0">
|
<Dialog.Body className="flex flex-col min-h-0">
|
||||||
|
|
@ -155,7 +134,7 @@ function AnnouncementModalContent({ announcement, onClose }: AnnouncementModalPr
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="flex items-center gap-3 mb-3">
|
<div className="flex items-center gap-3 mb-3">
|
||||||
<span
|
<span
|
||||||
className={`px-3 py-1 text-xs font-medium rounded-full ${getCategoryColor(category)}`}
|
className={`px-3 py-1 text-xs font-medium rounded-full ${categoryColor(category)}`}
|
||||||
>
|
>
|
||||||
{category === 'general' &&
|
{category === 'general' &&
|
||||||
`📢 ${translate('::App.Platform.Intranet.AnnouncementDetailModal.Category.General')}`}
|
`📢 ${translate('::App.Platform.Intranet.AnnouncementDetailModal.Category.General')}`}
|
||||||
|
|
@ -197,21 +176,12 @@ function AnnouncementModalContent({ announcement, onClose }: AnnouncementModalPr
|
||||||
{announcement.viewCount}{' '}
|
{announcement.viewCount}{' '}
|
||||||
{translate('::App.Platform.Intranet.AnnouncementDetailModal.Views')}
|
{translate('::App.Platform.Intranet.AnnouncementDetailModal.Views')}
|
||||||
</span>
|
</span>
|
||||||
<Button
|
<LikeButton
|
||||||
type="button"
|
|
||||||
onClick={handleLike}
|
onClick={handleLike}
|
||||||
disabled={liking}
|
disabled={liking}
|
||||||
variant="plain"
|
isLiked={isLiked}
|
||||||
size="xs"
|
likes={likes}
|
||||||
className={`!h-auto !items-center gap-2 !rounded !border-0 !px-2 !py-1 text-xs font-medium transition-colors ${
|
/>
|
||||||
isLiked
|
|
||||||
? 'bg-red-50 dark:bg-red-900/20 border-red-200 dark:border-red-800 text-red-600 dark:text-red-400 hover:bg-red-100 dark:hover:bg-red-900/40'
|
|
||||||
: 'bg-white dark:bg-gray-700 border-gray-200 dark:border-gray-600 text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-600'
|
|
||||||
} disabled:opacity-50 disabled:cursor-not-allowed`}
|
|
||||||
>
|
|
||||||
<FaHeart className={`w-4 h-4 ${isLiked ? 'text-red-500' : ''}`} />
|
|
||||||
{likes > 0 && likes}
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -241,7 +211,7 @@ function AnnouncementModalContent({ announcement, onClose }: AnnouncementModalPr
|
||||||
<div className="mx-auto border-t border-gray-200 dark:border-gray-700">
|
<div className="mx-auto border-t border-gray-200 dark:border-gray-700">
|
||||||
<div className="mt-3 w-full relative rounded-lg overflow-hidden dark:bg-gray-900 mb-2">
|
<div className="mt-3 w-full relative rounded-lg overflow-hidden dark:bg-gray-900 mb-2">
|
||||||
<img
|
<img
|
||||||
src={imgSrc(images[activePhoto])}
|
src={imageSrc(images[activePhoto])}
|
||||||
alt={`${announcement.title} ${activePhoto + 1}`}
|
alt={`${announcement.title} ${activePhoto + 1}`}
|
||||||
className="w-full h-56 object-contain cursor-zoom-in"
|
className="w-full h-56 object-contain cursor-zoom-in"
|
||||||
onClick={() => openLightbox(activePhoto)}
|
onClick={() => openLightbox(activePhoto)}
|
||||||
|
|
@ -298,7 +268,7 @@ function AnnouncementModalContent({ announcement, onClose }: AnnouncementModalPr
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src={imgSrc(img)}
|
src={imageSrc(img)}
|
||||||
alt={`${announcement.title} thumbnail ${idx + 1}`}
|
alt={`${announcement.title} thumbnail ${idx + 1}`}
|
||||||
className="w-full h-full object-cover"
|
className="w-full h-full object-cover"
|
||||||
/>
|
/>
|
||||||
|
|
@ -416,7 +386,7 @@ function AnnouncementModalContent({ announcement, onClose }: AnnouncementModalPr
|
||||||
'::App.Platform.Intranet.SocialWall.PostItem.CommentPlaceholder',
|
'::App.Platform.Intranet.SocialWall.PostItem.CommentPlaceholder',
|
||||||
)}
|
)}
|
||||||
rows={1}
|
rows={1}
|
||||||
className="flex-1 resize-none px-2 py-1 text-sm rounded-xl border border-gray-200 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 transition-colors"
|
className="flex-1 resize-none px-2 py-1 text-sm rounded-xl bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-400 transition-colors"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|
@ -438,86 +408,31 @@ function AnnouncementModalContent({ announcement, onClose }: AnnouncementModalPr
|
||||||
</Button>
|
</Button>
|
||||||
</Dialog.Footer>
|
</Dialog.Footer>
|
||||||
|
|
||||||
{/* Lightbox */}
|
<ImageLightbox
|
||||||
{createPortal(
|
images={images}
|
||||||
<AnimatePresence>
|
activeIndex={lightboxIndex}
|
||||||
{lightboxOpen && (
|
isOpen={lightboxOpen}
|
||||||
<>
|
altPrefix={announcement.title}
|
||||||
<motion.div
|
getSrc={imageSrc}
|
||||||
initial={{ opacity: 0 }}
|
onClose={closeLightbox}
|
||||||
animate={{ opacity: 1 }}
|
onChangeIndex={setLightboxIndex}
|
||||||
exit={{ opacity: 0 }}
|
/>
|
||||||
className="fixed inset-0 bg-black/95 z-[1100]"
|
|
||||||
onClick={closeLightbox}
|
|
||||||
/>
|
|
||||||
<div className="fixed inset-0 z-[1110] flex items-center justify-center">
|
|
||||||
<Button
|
|
||||||
onClick={closeLightbox}
|
|
||||||
variant="plain"
|
|
||||||
shape="circle"
|
|
||||||
icon={<FaTimes className="h-8 w-8" />}
|
|
||||||
className="absolute top-4 right-4 z-10 !h-12 !w-12 !px-0 text-white transition-colors hover:text-gray-300"
|
|
||||||
/>
|
|
||||||
{images.length > 1 && (
|
|
||||||
<>
|
|
||||||
<Button
|
|
||||||
onClick={prevLightbox}
|
|
||||||
variant="plain"
|
|
||||||
shape="circle"
|
|
||||||
icon={<FaChevronLeft className="h-6 w-6" />}
|
|
||||||
className="absolute left-4 top-1/2 z-10 !h-12 !w-12 -translate-y-1/2 !px-0 bg-black/50 text-white transition-colors hover:!bg-black/70"
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
onClick={nextLightbox}
|
|
||||||
variant="plain"
|
|
||||||
shape="circle"
|
|
||||||
icon={<FaChevronRight className="h-6 w-6" />}
|
|
||||||
className="absolute right-4 top-1/2 z-10 !h-12 !w-12 -translate-y-1/2 !px-0 bg-black/50 text-white transition-colors hover:!bg-black/70"
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<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)
|
|
||||||
}}
|
|
||||||
variant="plain"
|
|
||||||
shape="circle"
|
|
||||||
className={`!h-2.5 !w-2.5 !min-w-2.5 !px-0 transition-all ${
|
|
||||||
idx === lightboxIndex ? 'bg-white' : 'bg-white/40 hover:bg-white/70'
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>,
|
|
||||||
document.body,
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const AnnouncementModal: React.FC<AnnouncementModalProps> = ({ announcement, onClose }) => {
|
const AnnouncementModal: React.FC<AnnouncementModalProps> = ({
|
||||||
|
announcement,
|
||||||
|
onClose,
|
||||||
|
onLikeChange,
|
||||||
|
}) => {
|
||||||
return (
|
return (
|
||||||
<Dialog isOpen onClose={onClose} onRequestClose={onClose} width={768}>
|
<Dialog isOpen onClose={onClose} onRequestClose={onClose} width={768}>
|
||||||
<AnnouncementModalContent announcement={announcement} onClose={onClose} />
|
<AnnouncementModalContent
|
||||||
|
announcement={announcement}
|
||||||
|
onClose={onClose}
|
||||||
|
onLikeChange={onLikeChange}
|
||||||
|
/>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { FaBell, FaClipboardCheck, FaEye } from 'react-icons/fa'
|
import { FaBell, FaClipboardCheck, FaEye, FaHeart } from 'react-icons/fa'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { AnnouncementDto } from '@/proxy/intranet/models'
|
import { AnnouncementDto } from '@/proxy/intranet/models'
|
||||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||||
import { Avatar } from '@/components/ui'
|
import { Avatar } from '@/components/ui'
|
||||||
import { AVATAR_URL } from '@/constants/app.constant'
|
import { AVATAR_URL } from '@/constants/app.constant'
|
||||||
|
import { categoryColor } from './widgetUtils'
|
||||||
|
|
||||||
interface AnnouncementsProps {
|
interface AnnouncementsProps {
|
||||||
announcements: AnnouncementDto[]
|
announcements: AnnouncementDto[]
|
||||||
|
|
@ -16,17 +17,6 @@ const Announcements: React.FC<AnnouncementsProps> = ({ announcements, onAnnounce
|
||||||
const pinnedAnnouncements = safeAnnouncements.filter((a) => a?.isPinned).slice(0, 3)
|
const pinnedAnnouncements = safeAnnouncements.filter((a) => a?.isPinned).slice(0, 3)
|
||||||
const { translate } = useLocalization()
|
const { translate } = useLocalization()
|
||||||
|
|
||||||
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',
|
|
||||||
hr: 'bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300',
|
|
||||||
it: 'bg-orange-100 dark:bg-orange-900/30 text-orange-700 dark:text-orange-300',
|
|
||||||
event: 'bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300',
|
|
||||||
urgent: 'bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-300',
|
|
||||||
}
|
|
||||||
return colors[category] || colors.general
|
|
||||||
}
|
|
||||||
|
|
||||||
const getUserName = (announcement: AnnouncementDto) => {
|
const getUserName = (announcement: AnnouncementDto) => {
|
||||||
const announcementUser = announcement.user
|
const announcementUser = announcement.user
|
||||||
return (
|
return (
|
||||||
|
|
@ -61,7 +51,7 @@ const Announcements: React.FC<AnnouncementsProps> = ({ announcements, onAnnounce
|
||||||
</h3>
|
</h3>
|
||||||
{announcement.category && (
|
{announcement.category && (
|
||||||
<span
|
<span
|
||||||
className={`px-2 py-1 text-center text-xs rounded-full ${getCategoryColor(announcement.category || 'general')}`}
|
className={`px-2 py-1 text-center text-xs rounded-full ${categoryColor(announcement.category)}`}
|
||||||
>
|
>
|
||||||
{(() => {
|
{(() => {
|
||||||
const key = `::App.Platform.Intranet.Widgets.Announcements.Category.${announcement.category.charAt(0).toUpperCase() + announcement.category.slice(1)}`
|
const key = `::App.Platform.Intranet.Widgets.Announcements.Category.${announcement.category.charAt(0).toUpperCase() + announcement.category.slice(1)}`
|
||||||
|
|
@ -88,6 +78,19 @@ const Announcements: React.FC<AnnouncementsProps> = ({ announcements, onAnnounce
|
||||||
<FaEye className="w-3 h-3" />
|
<FaEye className="w-3 h-3" />
|
||||||
{announcement.viewCount ?? 0}
|
{announcement.viewCount ?? 0}
|
||||||
</span>
|
</span>
|
||||||
|
{(announcement.likes ?? 0) > 0 && (
|
||||||
|
<>
|
||||||
|
<span>•</span>
|
||||||
|
<span
|
||||||
|
className={`flex items-center gap-1 ${
|
||||||
|
announcement.isLiked ? 'text-red-500' : ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<FaHeart className="w-3 h-3" />
|
||||||
|
{announcement.likes}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,5 @@
|
||||||
import React, { useState, useEffect, useRef } from 'react'
|
import React, { useState, useEffect, useRef } from 'react'
|
||||||
import { createPortal } from 'react-dom'
|
|
||||||
import { motion, AnimatePresence } from 'framer-motion'
|
|
||||||
import {
|
import {
|
||||||
FaTimes,
|
|
||||||
FaChevronLeft,
|
FaChevronLeft,
|
||||||
FaChevronRight,
|
FaChevronRight,
|
||||||
FaMapMarkerAlt,
|
FaMapMarkerAlt,
|
||||||
|
|
@ -11,7 +8,6 @@ import {
|
||||||
FaExpand,
|
FaExpand,
|
||||||
FaCommentAlt,
|
FaCommentAlt,
|
||||||
FaPaperPlane,
|
FaPaperPlane,
|
||||||
FaHeart,
|
|
||||||
} from 'react-icons/fa'
|
} from 'react-icons/fa'
|
||||||
import { Button, Dialog } from '@/components/ui'
|
import { Button, Dialog } from '@/components/ui'
|
||||||
import { useDialogContext } from '@/components/ui/Dialog/Dialog'
|
import { useDialogContext } from '@/components/ui/Dialog/Dialog'
|
||||||
|
|
@ -24,26 +20,19 @@ import { intranetService } from '@/services/intranet.service'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||||
|
import ImageLightbox from './ImageLightbox'
|
||||||
|
import LikeButton from './LikeButton'
|
||||||
|
import { imageSrc } from './widgetUtils'
|
||||||
|
|
||||||
dayjs.extend(relativeTime)
|
dayjs.extend(relativeTime)
|
||||||
|
|
||||||
interface EventModalProps {
|
interface EventModalProps {
|
||||||
event: EventDto
|
event: EventDto
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
|
onLikeChange?: (event: EventDto) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const imgSrc = (img: string) => {
|
function EventModalContent({ event, onClose, onLikeChange }: EventModalProps) {
|
||||||
if (
|
|
||||||
img.startsWith('data:') ||
|
|
||||||
img.startsWith('http://') ||
|
|
||||||
img.startsWith('https://') ||
|
|
||||||
img.startsWith('/')
|
|
||||||
)
|
|
||||||
return img
|
|
||||||
return `data:image/jpeg;base64,${img}`
|
|
||||||
}
|
|
||||||
|
|
||||||
function EventModalContent({ event, onClose }: EventModalProps) {
|
|
||||||
const currentLocale = useLocale()
|
const currentLocale = useLocale()
|
||||||
const photos = (event.photos || '').split('|').filter(Boolean)
|
const photos = (event.photos || '').split('|').filter(Boolean)
|
||||||
const { translate } = useLocalization()
|
const { translate } = useLocalization()
|
||||||
|
|
@ -108,8 +97,14 @@ function EventModalContent({ event, onClose }: EventModalProps) {
|
||||||
try {
|
try {
|
||||||
const res = await intranetService.likeEvent(event.id)
|
const res = await intranetService.likeEvent(event.id)
|
||||||
if (res.data) {
|
if (res.data) {
|
||||||
setLikes(res.data.likes)
|
const updatedEvent = {
|
||||||
setIsLiked(res.data.isLiked)
|
...event,
|
||||||
|
likes: res.data.likes,
|
||||||
|
isLiked: res.data.isLiked,
|
||||||
|
}
|
||||||
|
setLikes(updatedEvent.likes)
|
||||||
|
setIsLiked(updatedEvent.isLiked)
|
||||||
|
onLikeChange?.(updatedEvent)
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// Revert on error
|
// Revert on error
|
||||||
|
|
@ -127,16 +122,6 @@ function EventModalContent({ event, onClose }: EventModalProps) {
|
||||||
|
|
||||||
const closeLightbox = () => setLightboxOpen(false)
|
const closeLightbox = () => setLightboxOpen(false)
|
||||||
|
|
||||||
const prevLightbox = (e: React.MouseEvent) => {
|
|
||||||
e.stopPropagation()
|
|
||||||
setLightboxIndex((i) => (i - 1 + photos.length) % photos.length)
|
|
||||||
}
|
|
||||||
|
|
||||||
const nextLightbox = (e: React.MouseEvent) => {
|
|
||||||
e.stopPropagation()
|
|
||||||
setLightboxIndex((i) => (i + 1) % photos.length)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Dialog.Body className="flex flex-col min-h-0">
|
<Dialog.Body className="flex flex-col min-h-0">
|
||||||
|
|
@ -165,20 +150,12 @@ function EventModalContent({ event, onClose }: EventModalProps) {
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
<div>
|
<div>
|
||||||
<Button
|
<LikeButton
|
||||||
onClick={handleLike}
|
onClick={handleLike}
|
||||||
disabled={liking}
|
disabled={liking}
|
||||||
variant="plain"
|
isLiked={isLiked}
|
||||||
size="xs"
|
likes={likes}
|
||||||
className={`!h-auto !items-center gap-2 !rounded !border-0 !px-2 !py-1 text-xs font-xs transition-colors ${
|
/>
|
||||||
isLiked
|
|
||||||
? 'bg-red-50 dark:bg-red-900/20 border-red-200 dark:border-red-800 text-red-600 dark:text-red-400 hover:bg-red-100 dark:hover:bg-red-900/40'
|
|
||||||
: 'bg-white dark:bg-gray-700 border-gray-200 dark:border-gray-600 text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-600'
|
|
||||||
} disabled:opacity-50 disabled:cursor-not-allowed`}
|
|
||||||
>
|
|
||||||
<FaHeart className={`w-4 h-4 ${isLiked ? 'text-red-500' : ''}`} />
|
|
||||||
{likes > 0 && likes}
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -206,7 +183,7 @@ function EventModalContent({ event, onClose }: EventModalProps) {
|
||||||
{/* Main photo */}
|
{/* Main photo */}
|
||||||
<div className="relative w-full rounded-lg overflow-hidden dark:bg-gray-900 mb-2 mx-auto">
|
<div className="relative w-full rounded-lg overflow-hidden dark:bg-gray-900 mb-2 mx-auto">
|
||||||
<img
|
<img
|
||||||
src={imgSrc(photos[activePhoto])}
|
src={imageSrc(photos[activePhoto])}
|
||||||
alt={`${event.name} ${activePhoto + 1}`}
|
alt={`${event.name} ${activePhoto + 1}`}
|
||||||
className="w-full h-56 object-contain cursor-zoom-in"
|
className="w-full h-56 object-contain cursor-zoom-in"
|
||||||
onClick={() => openLightbox(activePhoto)}
|
onClick={() => openLightbox(activePhoto)}
|
||||||
|
|
@ -260,7 +237,7 @@ function EventModalContent({ event, onClose }: EventModalProps) {
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src={imgSrc(photo)}
|
src={imageSrc(photo)}
|
||||||
alt={`Thumbnail ${idx + 1}`}
|
alt={`Thumbnail ${idx + 1}`}
|
||||||
className="w-full h-full object-cover"
|
className="w-full h-full object-cover"
|
||||||
/>
|
/>
|
||||||
|
|
@ -336,7 +313,7 @@ function EventModalContent({ event, onClose }: EventModalProps) {
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
placeholder={translate('::App.Intranet.Events.EventAttendance')}
|
placeholder={translate('::App.Intranet.Events.EventAttendance')}
|
||||||
rows={1}
|
rows={1}
|
||||||
className="flex-1 resize-none px-2 py-1 text-sm rounded-xl border border-gray-200 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-green-500 transition-colors"
|
className="flex-1 resize-none px-2 py-1 text-sm rounded-xl bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-400 transition-colors"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
|
|
@ -357,86 +334,23 @@ function EventModalContent({ event, onClose }: EventModalProps) {
|
||||||
</Button>
|
</Button>
|
||||||
</Dialog.Footer>
|
</Dialog.Footer>
|
||||||
|
|
||||||
{/* Lightbox */}
|
<ImageLightbox
|
||||||
{createPortal(
|
images={photos}
|
||||||
<AnimatePresence>
|
activeIndex={lightboxIndex}
|
||||||
{lightboxOpen && (
|
isOpen={lightboxOpen}
|
||||||
<>
|
altPrefix={event.name}
|
||||||
<motion.div
|
getSrc={imageSrc}
|
||||||
initial={{ opacity: 0 }}
|
onClose={closeLightbox}
|
||||||
animate={{ opacity: 1 }}
|
onChangeIndex={setLightboxIndex}
|
||||||
exit={{ opacity: 0 }}
|
/>
|
||||||
className="fixed inset-0 bg-black/95 z-[1100]"
|
|
||||||
onClick={closeLightbox}
|
|
||||||
/>
|
|
||||||
<div className="fixed inset-0 z-[1110] flex items-center justify-center">
|
|
||||||
<Button
|
|
||||||
onClick={closeLightbox}
|
|
||||||
variant="plain"
|
|
||||||
shape="circle"
|
|
||||||
icon={<FaTimes className="h-8 w-8" />}
|
|
||||||
className="absolute top-4 right-4 z-10 !h-12 !w-12 !px-0 text-white transition-colors hover:text-gray-300"
|
|
||||||
/>
|
|
||||||
{photos.length > 1 && (
|
|
||||||
<>
|
|
||||||
<Button
|
|
||||||
onClick={prevLightbox}
|
|
||||||
variant="plain"
|
|
||||||
shape="circle"
|
|
||||||
icon={<FaChevronLeft className="h-6 w-6" />}
|
|
||||||
className="absolute left-4 top-1/2 z-10 !h-12 !w-12 -translate-y-1/2 !px-0 bg-black/50 text-white transition-colors hover:!bg-black/70"
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
onClick={nextLightbox}
|
|
||||||
variant="plain"
|
|
||||||
shape="circle"
|
|
||||||
icon={<FaChevronRight className="h-6 w-6" />}
|
|
||||||
className="absolute right-4 top-1/2 z-10 !h-12 !w-12 -translate-y-1/2 !px-0 bg-black/50 text-white transition-colors hover:!bg-black/70"
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<motion.img
|
|
||||||
key={lightboxIndex}
|
|
||||||
initial={{ opacity: 0, scale: 0.9 }}
|
|
||||||
animate={{ opacity: 1, scale: 1 }}
|
|
||||||
exit={{ opacity: 0 }}
|
|
||||||
src={imgSrc(photos[lightboxIndex])}
|
|
||||||
alt={`${event.name} ${lightboxIndex + 1}`}
|
|
||||||
className="max-w-[90vw] max-h-[90vh] object-contain rounded-lg"
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
/>
|
|
||||||
{photos.length > 1 && (
|
|
||||||
<div className="absolute bottom-4 left-1/2 -translate-x-1/2 flex gap-2">
|
|
||||||
{photos.map((_, idx) => (
|
|
||||||
<Button
|
|
||||||
key={idx}
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation()
|
|
||||||
setLightboxIndex(idx)
|
|
||||||
}}
|
|
||||||
variant="plain"
|
|
||||||
shape="circle"
|
|
||||||
className={`!h-2.5 !w-2.5 !min-w-2.5 !px-0 transition-all ${
|
|
||||||
idx === lightboxIndex ? 'bg-white' : 'bg-white/40 hover:bg-white/70'
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>,
|
|
||||||
document.body,
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const EventModal: React.FC<EventModalProps> = ({ event, onClose }) => {
|
const EventModal: React.FC<EventModalProps> = ({ event, onClose, onLikeChange }) => {
|
||||||
return (
|
return (
|
||||||
<Dialog isOpen onClose={onClose} onRequestClose={onClose} width={768}>
|
<Dialog isOpen onClose={onClose} onRequestClose={onClose} width={768}>
|
||||||
<EventModalContent event={event} onClose={onClose} />
|
<EventModalContent event={event} onClose={onClose} onLikeChange={onLikeChange} />
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,9 @@ import { currentLocalDate } from '@/utils/dateUtils'
|
||||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||||
import { Avatar } from '@/components/ui'
|
import { Avatar } from '@/components/ui'
|
||||||
import { AVATAR_URL } from '@/constants/app.constant'
|
import { AVATAR_URL } from '@/constants/app.constant'
|
||||||
|
import { imageSrc } from './widgetUtils'
|
||||||
|
|
||||||
interface UpcomingEventsProps {
|
interface EventsProps {
|
||||||
events: EventDto[]
|
events: EventDto[]
|
||||||
onEventClick?: (event: EventDto) => void
|
onEventClick?: (event: EventDto) => void
|
||||||
}
|
}
|
||||||
|
|
@ -19,18 +20,7 @@ const getFirstPhoto = (photos?: string): string | null => {
|
||||||
return parts.length > 0 ? parts[0] : null
|
return parts.length > 0 ? parts[0] : null
|
||||||
}
|
}
|
||||||
|
|
||||||
const photoSrc = (img: string) => {
|
const Events: React.FC<EventsProps> = ({ events, onEventClick }) => {
|
||||||
if (
|
|
||||||
img.startsWith('data:') ||
|
|
||||||
img.startsWith('http://') ||
|
|
||||||
img.startsWith('https://') ||
|
|
||||||
img.startsWith('/')
|
|
||||||
)
|
|
||||||
return img
|
|
||||||
return `data:image/jpeg;base64,${img}`
|
|
||||||
}
|
|
||||||
|
|
||||||
const UpcomingEvents: React.FC<UpcomingEventsProps> = ({ events, onEventClick }) => {
|
|
||||||
const currentLocale = useLocale()
|
const currentLocale = useLocale()
|
||||||
const { translate } = useLocalization()
|
const { translate } = useLocalization()
|
||||||
|
|
||||||
|
|
@ -96,7 +86,7 @@ const UpcomingEvents: React.FC<UpcomingEventsProps> = ({ events, onEventClick })
|
||||||
</div>
|
</div>
|
||||||
{firstPhoto && (
|
{firstPhoto && (
|
||||||
<img
|
<img
|
||||||
src={photoSrc(firstPhoto)}
|
src={imageSrc(firstPhoto)}
|
||||||
alt={event.name}
|
alt={event.name}
|
||||||
className="w-14 h-14 rounded-lg object-cover flex-shrink-0"
|
className="w-14 h-14 rounded-lg object-cover flex-shrink-0"
|
||||||
/>
|
/>
|
||||||
|
|
@ -120,5 +110,5 @@ const UpcomingEvents: React.FC<UpcomingEventsProps> = ({ events, onEventClick })
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default UpcomingEvents
|
export default Events
|
||||||
|
|
||||||
104
ui/src/views/intranet/widgets/ImageLightbox.tsx
Normal file
104
ui/src/views/intranet/widgets/ImageLightbox.tsx
Normal file
|
|
@ -0,0 +1,104 @@
|
||||||
|
import { createPortal } from 'react-dom'
|
||||||
|
import { motion, AnimatePresence } from 'framer-motion'
|
||||||
|
import { FaChevronLeft, FaChevronRight, FaTimes } from 'react-icons/fa'
|
||||||
|
import { Button } from '@/components/ui'
|
||||||
|
import type { MouseEvent } from 'react'
|
||||||
|
|
||||||
|
interface ImageLightboxProps {
|
||||||
|
images: string[]
|
||||||
|
activeIndex: number
|
||||||
|
isOpen: boolean
|
||||||
|
altPrefix: string
|
||||||
|
getSrc: (img: string) => string
|
||||||
|
onClose: () => void
|
||||||
|
onChangeIndex: (index: number) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const ImageLightbox = ({
|
||||||
|
images,
|
||||||
|
activeIndex,
|
||||||
|
isOpen,
|
||||||
|
altPrefix,
|
||||||
|
getSrc,
|
||||||
|
onClose,
|
||||||
|
onChangeIndex,
|
||||||
|
}: ImageLightboxProps) => {
|
||||||
|
const changeBy = (delta: number) => (event: MouseEvent) => {
|
||||||
|
event.stopPropagation()
|
||||||
|
onChangeIndex((activeIndex + delta + images.length) % images.length)
|
||||||
|
}
|
||||||
|
|
||||||
|
return createPortal(
|
||||||
|
<AnimatePresence>
|
||||||
|
{isOpen && (
|
||||||
|
<>
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
className="fixed inset-0 bg-black/95 z-[1100]"
|
||||||
|
onClick={onClose}
|
||||||
|
/>
|
||||||
|
<div className="fixed inset-0 z-[1110] flex items-center justify-center">
|
||||||
|
<Button
|
||||||
|
onClick={onClose}
|
||||||
|
variant="plain"
|
||||||
|
shape="circle"
|
||||||
|
icon={<FaTimes className="h-8 w-8" />}
|
||||||
|
className="absolute top-4 right-4 z-10 !h-12 !w-12 !px-0 text-white transition-colors hover:text-gray-300"
|
||||||
|
/>
|
||||||
|
{images.length > 1 && (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
onClick={changeBy(-1)}
|
||||||
|
variant="plain"
|
||||||
|
shape="circle"
|
||||||
|
icon={<FaChevronLeft className="h-6 w-6" />}
|
||||||
|
className="absolute left-4 top-1/2 z-10 !h-12 !w-12 -translate-y-1/2 !px-0 bg-black/50 text-white transition-colors hover:!bg-black/70"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
onClick={changeBy(1)}
|
||||||
|
variant="plain"
|
||||||
|
shape="circle"
|
||||||
|
icon={<FaChevronRight className="h-6 w-6" />}
|
||||||
|
className="absolute right-4 top-1/2 z-10 !h-12 !w-12 -translate-y-1/2 !px-0 bg-black/50 text-white transition-colors hover:!bg-black/70"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<motion.img
|
||||||
|
key={activeIndex}
|
||||||
|
initial={{ opacity: 0, scale: 0.9 }}
|
||||||
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
src={getSrc(images[activeIndex])}
|
||||||
|
alt={`${altPrefix} ${activeIndex + 1}`}
|
||||||
|
className="max-w-[90vw] max-h-[90vh] object-contain rounded-lg"
|
||||||
|
onClick={(event) => event.stopPropagation()}
|
||||||
|
/>
|
||||||
|
{images.length > 1 && (
|
||||||
|
<div className="absolute bottom-4 left-1/2 -translate-x-1/2 flex gap-2">
|
||||||
|
{images.map((_, index) => (
|
||||||
|
<Button
|
||||||
|
key={index}
|
||||||
|
onClick={(event) => {
|
||||||
|
event.stopPropagation()
|
||||||
|
onChangeIndex(index)
|
||||||
|
}}
|
||||||
|
variant="plain"
|
||||||
|
shape="circle"
|
||||||
|
className={`!h-2.5 !w-2.5 !min-w-2.5 !px-0 transition-all ${
|
||||||
|
index === activeIndex ? 'bg-white' : 'bg-white/40 hover:bg-white/70'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>,
|
||||||
|
document.body,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ImageLightbox
|
||||||
25
ui/src/views/intranet/widgets/LikeButton.tsx
Normal file
25
ui/src/views/intranet/widgets/LikeButton.tsx
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { FaHeart } from 'react-icons/fa'
|
||||||
|
import { Button } from '@/components/ui'
|
||||||
|
|
||||||
|
interface LikeButtonProps {
|
||||||
|
likes?: number
|
||||||
|
isLiked?: boolean
|
||||||
|
disabled?: boolean
|
||||||
|
onClick: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const LikeButton = ({ likes = 0, isLiked = false, disabled, onClick }: LikeButtonProps) => (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
onClick={onClick}
|
||||||
|
disabled={disabled}
|
||||||
|
variant="plain"
|
||||||
|
size="xs"
|
||||||
|
className="!inline-flex !h-auto !flex-row !items-center gap-2 whitespace-nowrap !rounded-none !border-0 !bg-transparent !px-0 !py-0 text-xs font-xs text-gray-600 transition-colors hover:!bg-transparent active:!bg-transparent focus:!bg-transparent disabled:cursor-not-allowed disabled:opacity-50 dark:text-gray-300"
|
||||||
|
>
|
||||||
|
<FaHeart className={`h-4 w-4 flex-shrink-0 ${isLiked ? 'text-red-500' : ''}`} />
|
||||||
|
{likes > 0 && <span className="leading-none">{likes}</span>}
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default LikeButton
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import { Button, Dialog } from '@/components/ui'
|
import { Button, Dialog, Rate } from '@/components/ui'
|
||||||
import { useDialogContext } from '@/components/ui/Dialog/Dialog'
|
import { useDialogContext } from '@/components/ui/Dialog/Dialog'
|
||||||
import { SurveyAnswerDto, SurveyDto, SurveyQuestionDto } from '@/proxy/intranet/models'
|
import { SurveyAnswerDto, SurveyDto, SurveyQuestionDto } from '@/proxy/intranet/models'
|
||||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||||
|
|
@ -93,24 +93,14 @@ function SurveyModalContent({ survey, onClose, onSubmit }: SurveyModalProps) {
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
{questionNumber}. {question.questionText} {question.isRequired && '*'}
|
{questionNumber}. {question.questionText} {question.isRequired && '*'}
|
||||||
</label>
|
</label>
|
||||||
<div className="flex gap-2">
|
<Rate
|
||||||
{[1, 2, 3, 4, 5].map((star) => (
|
value={answers[question.id] ?? 0}
|
||||||
<Button
|
onChange={(value) => handleAnswerChange(question.id, value)}
|
||||||
key={star}
|
allowClear={false}
|
||||||
type="button"
|
size={20}
|
||||||
onClick={() => handleAnswerChange(question.id, star)}
|
gap={8}
|
||||||
variant="plain"
|
color="yellow"
|
||||||
shape="circle"
|
/>
|
||||||
className={`!h-8 !w-8 !px-0 text-2xl transition-colors ${
|
|
||||||
(answers[question.id] ?? 0) >= star
|
|
||||||
? 'text-yellow-400'
|
|
||||||
: 'text-gray-300 dark:text-gray-600 hover:text-yellow-300'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
★
|
|
||||||
</Button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
{hasError && (
|
{hasError && (
|
||||||
<p className="text-sm text-red-600 dark:text-red-400">{errors[question.id]}</p>
|
<p className="text-sm text-red-600 dark:text-red-400">{errors[question.id]}</p>
|
||||||
)}
|
)}
|
||||||
|
|
@ -246,7 +236,7 @@ function SurveyModalContent({ survey, onClose, onSubmit }: SurveyModalProps) {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={`py-6 space-y-6 overflow-y-auto ${
|
className={`p-2 space-y-6 overflow-y-auto [scrollbar-color:#6b7280_transparent] [scrollbar-width:thin] ${
|
||||||
isMaximized ? 'flex-1 min-h-0' : 'max-h-[65vh]'
|
isMaximized ? 'flex-1 min-h-0' : 'max-h-[65vh]'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
|
|
@ -277,7 +267,7 @@ function SurveyModalContent({ survey, onClose, onSubmit }: SurveyModalProps) {
|
||||||
<Dialog.Footer className="flex gap-3 pt-4 border-t border-gray-200 dark:border-gray-700 flex-shrink-0">
|
<Dialog.Footer className="flex gap-3 pt-4 border-t border-gray-200 dark:border-gray-700 flex-shrink-0">
|
||||||
<Button
|
<Button
|
||||||
block
|
block
|
||||||
size="sm"
|
size="md"
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
variant="default"
|
variant="default"
|
||||||
|
|
@ -286,7 +276,7 @@ function SurveyModalContent({ survey, onClose, onSubmit }: SurveyModalProps) {
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
block
|
block
|
||||||
size="sm"
|
size="md"
|
||||||
type="submit"
|
type="submit"
|
||||||
variant="solid"
|
variant="solid"
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React from 'react'
|
import React, { useState } from 'react'
|
||||||
import { FaClipboardCheck, FaQuestionCircle, FaUsers, FaClock, FaArrowRight } from 'react-icons/fa'
|
import { FaClipboardCheck, FaQuestionCircle, FaUsers, FaClock, FaArrowRight } from 'react-icons/fa'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { SurveyDto } from '@/proxy/intranet/models'
|
import { SurveyDto } from '@/proxy/intranet/models'
|
||||||
|
|
@ -15,6 +15,7 @@ interface SurveysProps {
|
||||||
const Surveys: React.FC<SurveysProps> = ({ surveys, onTakeSurvey }) => {
|
const Surveys: React.FC<SurveysProps> = ({ surveys, onTakeSurvey }) => {
|
||||||
const currentLocale = useLocale()
|
const currentLocale = useLocale()
|
||||||
const { translate } = useLocalization()
|
const { translate } = useLocalization()
|
||||||
|
const [hoveredSurveyId, setHoveredSurveyId] = useState<string | null>(null)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-gradient-to-br from-white to-gray-50 dark:from-gray-800 dark:to-gray-900 rounded-xl shadow-lg border border-gray-200/50 dark:border-gray-700/50 overflow-hidden">
|
<div className="bg-gradient-to-br from-white to-gray-50 dark:from-gray-800 dark:to-gray-900 rounded-xl shadow-lg border border-gray-200/50 dark:border-gray-700/50 overflow-hidden">
|
||||||
|
|
@ -32,20 +33,31 @@ const Surveys: React.FC<SurveysProps> = ({ surveys, onTakeSurvey }) => {
|
||||||
const daysLeft = dayjs(survey.deadline).diff(dayjs(), 'day')
|
const daysLeft = dayjs(survey.deadline).diff(dayjs(), 'day')
|
||||||
const urgency = daysLeft <= 3 ? 'urgent' : daysLeft <= 7 ? 'warning' : 'normal'
|
const urgency = daysLeft <= 3 ? 'urgent' : daysLeft <= 7 ? 'warning' : 'normal'
|
||||||
const isCompleted = !!survey.myResponse
|
const isCompleted = !!survey.myResponse
|
||||||
|
const isHovered = hoveredSurveyId === survey.id
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={survey.id}
|
key={survey.id}
|
||||||
onClick={() => onTakeSurvey(survey)}
|
onClick={() => onTakeSurvey(survey)}
|
||||||
className={`group relative p-5 rounded-xl border cursor-pointer transition-all duration-300 hover:shadow-lg hover:-translate-y-1 ${
|
onMouseEnter={() => setHoveredSurveyId(survey.id)}
|
||||||
|
onMouseLeave={() => setHoveredSurveyId(null)}
|
||||||
|
className={`relative p-5 rounded-xl border cursor-pointer transition-all duration-300 ${
|
||||||
|
isHovered ? '-translate-y-1 shadow-lg' : ''
|
||||||
|
} ${
|
||||||
isCompleted
|
isCompleted
|
||||||
? 'bg-green-50 dark:bg-green-900/30 border-green-300 dark:border-green-700 hover:border-green-400 dark:hover:border-green-500'
|
? `bg-green-50 dark:bg-green-900/30 border-green-300 dark:border-green-700 ${
|
||||||
: 'bg-white dark:bg-gray-800 border-gray-200 dark:border-gray-700 hover:border-purple-300 dark:hover:border-purple-500'
|
isHovered ? 'border-green-400 dark:border-green-500' : ''
|
||||||
|
}`
|
||||||
|
: `bg-white dark:bg-gray-800 border-gray-200 dark:border-gray-700 ${
|
||||||
|
isHovered ? 'border-purple-300 dark:border-purple-500' : ''
|
||||||
|
}`
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{/* Background gradient on hover */}
|
{/* Background gradient on hover */}
|
||||||
<div
|
<div
|
||||||
className={`absolute inset-0 rounded-xl opacity-0 group-hover:opacity-100 transition-opacity duration-300 pointer-events-none ${
|
className={`absolute inset-0 rounded-xl transition-opacity duration-300 pointer-events-none ${
|
||||||
|
isHovered ? 'opacity-100' : 'opacity-0'
|
||||||
|
} ${
|
||||||
isCompleted
|
isCompleted
|
||||||
? 'bg-gradient-to-r from-green-50 to-emerald-50 dark:from-green-900/20 dark:to-emerald-900/20'
|
? 'bg-gradient-to-r from-green-50 to-emerald-50 dark:from-green-900/20 dark:to-emerald-900/20'
|
||||||
: 'bg-gradient-to-r from-purple-50 to-pink-50 dark:from-purple-900/20 dark:to-pink-900/20'
|
: 'bg-gradient-to-r from-purple-50 to-pink-50 dark:from-purple-900/20 dark:to-pink-900/20'
|
||||||
|
|
@ -72,8 +84,12 @@ const Surveys: React.FC<SurveysProps> = ({ surveys, onTakeSurvey }) => {
|
||||||
<h4
|
<h4
|
||||||
className={`text-base font-semibold transition-colors ${
|
className={`text-base font-semibold transition-colors ${
|
||||||
isCompleted
|
isCompleted
|
||||||
? 'text-green-800 dark:text-green-300 group-hover:text-green-700 dark:group-hover:text-green-200'
|
? isHovered
|
||||||
: 'text-gray-900 dark:text-white group-hover:text-purple-700 dark:group-hover:text-purple-300'
|
? 'text-green-700 dark:text-green-200'
|
||||||
|
: 'text-green-800 dark:text-green-300'
|
||||||
|
: isHovered
|
||||||
|
? 'text-purple-700 dark:text-purple-300'
|
||||||
|
: 'text-gray-900 dark:text-white'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{survey.title}
|
{survey.title}
|
||||||
|
|
@ -167,9 +183,15 @@ const Surveys: React.FC<SurveysProps> = ({ surveys, onTakeSurvey }) => {
|
||||||
<Button
|
<Button
|
||||||
variant="plain"
|
variant="plain"
|
||||||
icon={
|
icon={
|
||||||
<FaArrowRight className="h-3 w-3 transition-transform group-hover:translate-x-1" />
|
<FaArrowRight
|
||||||
|
className={`h-3 w-3 transition-transform ${
|
||||||
|
isHovered ? 'translate-x-1' : ''
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
}
|
}
|
||||||
className={`w-full !h-auto !justify-center gap-2 !rounded-lg !px-4 !py-3 text-sm font-medium text-white shadow-sm transition-all duration-300 hover:shadow-md group-hover:scale-[1.02] focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2 dark:focus:ring-purple-400 ${
|
className={`w-full !h-auto !justify-center gap-2 !rounded-lg !px-4 !py-3 text-sm font-medium text-white shadow-sm transition-all duration-300 hover:shadow-md focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2 dark:focus:ring-purple-400 ${
|
||||||
|
isHovered ? 'scale-[1.02]' : ''
|
||||||
|
} ${
|
||||||
isCompleted
|
isCompleted
|
||||||
? 'bg-gradient-to-r from-green-500 to-emerald-500 hover:from-green-600 hover:to-emerald-600 dark:from-green-600 dark:to-emerald-600 dark:hover:from-green-700 dark:hover:to-emerald-700'
|
? 'bg-gradient-to-r from-green-500 to-emerald-500 hover:from-green-600 hover:to-emerald-600 dark:from-green-600 dark:to-emerald-600 dark:hover:from-green-700 dark:hover:to-emerald-700'
|
||||||
: 'bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-700 hover:to-pink-700 dark:from-purple-700 dark:to-pink-700 dark:hover:from-purple-800 dark:hover:to-pink-800'
|
: 'bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-700 hover:to-pink-700 dark:from-purple-700 dark:to-pink-700 dark:hover:from-purple-800 dark:hover:to-pink-800'
|
||||||
|
|
|
||||||
24
ui/src/views/intranet/widgets/widgetUtils.ts
Normal file
24
ui/src/views/intranet/widgets/widgetUtils.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
export const imageSrc = (img: string) => {
|
||||||
|
if (
|
||||||
|
img.startsWith('data:') ||
|
||||||
|
img.startsWith('http://') ||
|
||||||
|
img.startsWith('https://') ||
|
||||||
|
img.startsWith('/')
|
||||||
|
) {
|
||||||
|
return img
|
||||||
|
}
|
||||||
|
|
||||||
|
return `data:image/jpeg;base64,${img}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export const categoryColor = (category?: string) => {
|
||||||
|
const colors: Record<string, string> = {
|
||||||
|
general: 'bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300',
|
||||||
|
hr: 'bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300',
|
||||||
|
it: 'bg-orange-100 dark:bg-orange-900/30 text-orange-700 dark:text-orange-300',
|
||||||
|
event: 'bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300',
|
||||||
|
urgent: 'bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-300',
|
||||||
|
}
|
||||||
|
|
||||||
|
return colors[category || 'general'] || colors.general
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue