ntp&modercontent_front

This commit is contained in:
2026-08-10 23:29:34 +03:00
parent e52bfc8f84
commit 204e6a37ed
13 changed files with 628 additions and 192 deletions
+51 -8
View File
@@ -1,6 +1,6 @@
import React, { useState, useEffect, useRef } from 'react';
import { motion } from 'framer-motion';
const NTP_URL = import.meta.env.VITE_NTP_SERVER || 'http://192.168.139.249:3001/api/time';
interface CurrencyRate {
currency: string;
rate: number;
@@ -30,7 +30,6 @@ const BottomBar: React.FC = () => {
const [isOnline, setIsOnline] = useState(navigator.onLine);
const [isPaused, setIsPaused] = useState(false);
const [currentTime, setCurrentTime] = useState(new Date()); // ✅ Добавлено
const scrollRef = useRef<HTMLDivElement>(null);
const getAllItems = () => {
@@ -58,14 +57,57 @@ const BottomBar: React.FC = () => {
const items = getAllItems();
const doubledItems = [...items, ...items, ...items];
// ✅ Обновление времени каждую секунду
const NTP_URL = 'http://localhost:3005/api/time'; // или твой URL
const useServerTime = (interval: number = 1000) => {
const [time, setTime] = useState(() => new Date());
useEffect(() => {
const interval = setInterval(() => {
setCurrentTime(new Date());
}, 1000);
let timer: NodeJS.Timeout;
let diff = 0;
const fetchTime = async () => {
try {
const res = await fetch(NTP_URL);
if (!res.ok) throw new Error('Server time unavailable');
const data = await res.json();
const serverTime = new Date(data.time);
// Вычисляем разницу между серверным и локальным временем
diff = serverTime.getTime() - Date.now();
// Устанавливаем время
setTime(new Date(Date.now() + diff));
// Запускаем локальный таймер
if (timer) clearInterval(timer);
timer = setInterval(() => {
setTime(new Date(Date.now() + diff));
}, interval);
} catch (err) {
console.warn('Server time unavailable, using local time:', err);
// Если сервер недоступен — используем локальное время
setTime(new Date());
if (timer) clearInterval(timer);
timer = setInterval(() => {
setTime(new Date());
}, interval);
}
};
// Первый запрос
fetchTime();
// Очистка при размонтировании
return () => {
if (timer) clearInterval(timer);
};
}, [interval]);
return time;
};
const currentTime = useServerTime(1000);
return () => clearInterval(interval);
}, []);
useEffect(() => {
const handleOnline = () => setIsOnline(true);
@@ -308,4 +350,5 @@ const BottomBar: React.FC = () => {
);
};
export default BottomBar;
+304 -89
View File
@@ -1,18 +1,79 @@
import React from 'react';
import React, { useState, useEffect } from 'react';
import { motion } from 'framer-motion';
import { FaHandsHelping, FaCalendarCheck, FaMoneyBillWave, FaHome } from 'react-icons/fa';
import { FaHandsHelping, FaCalendarCheck, FaMoneyBillWave, FaHome, FaTimes } from 'react-icons/fa';
import DOMPurify from 'dompurify';
interface TradeUnionProps {
onBack: () => void;
employee: any;
}
// Функция-обертка для рендеринга иконок
const renderIcon = (Icon: any, style?: React.CSSProperties) => {
return React.createElement(Icon, { style });
};
interface Block {
id: number;
tab: string;
content: string;
created_at: string;
}
const sections = [
{ id: 'events', title: 'Мероприятия', description: 'Анонсы и запись на мероприятия', icon: FaCalendarCheck, color: '#e74c3c' },
{ id: 'help', title: 'Материальная помощь', description: 'Размеры и виды материальной помощи', icon: FaMoneyBillWave, color: '#f39c12' },
{ id: 'housing', title: 'Жилищные условия', description: 'Информация об очереди на улучшение жилищных условий', icon: FaHome, color: '#3498db' },
];
const TradeUnion: React.FC<TradeUnionProps> = ({ onBack, employee }) => {
const [selectedSection, setSelectedSection] = useState<string | null>(null);
const [blocksMap, setBlocksMap] = useState<Record<string, Block[]>>({});
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchAllSections = async () => {
try {
setLoading(true);
const results: Record<string, Block[]> = {};
for (const sec of sections) {
const response = await fetch(`http://localhost:3005/moders/content/section/${sec.id}`, {
credentials: 'include',
});
if (response.ok) {
const data = await response.json();
results[sec.id] = Array.isArray(data) ? data : [];
} else {
results[sec.id] = [];
}
}
setBlocksMap(results);
} catch (err) {
console.error('Ошибка загрузки разделов профсоюза:', err);
} finally {
setLoading(false);
}
};
fetchAllSections();
}, []);
const openModal = (sectionId: string) => {
setSelectedSection(sectionId);
};
const closeModal = () => {
setSelectedSection(null);
};
const selectedData = selectedSection ? sections.find(s => s.id === selectedSection) : null;
const selectedBlocks = selectedSection ? blocksMap[selectedSection] || [] : [];
const formatDate = (dateStr: string) => {
const date = new Date(dateStr);
return date.toLocaleString('ru-RU', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
});
};
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
@@ -63,98 +124,252 @@ const TradeUnion: React.FC<TradeUnionProps> = ({ onBack, employee }) => {
alignItems: 'center',
gap: '12px',
}}>
{renderIcon(FaHandsHelping, { color: '#e74c3c', fontSize: '24px' })}
<FaHandsHelping style={{ color: '#e74c3c', fontSize: '24px' }} />
Профсоюз
</h2>
<div style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(280px, 1fr))',
gap: '20px',
}}>
<motion.div
whileHover={{ y: -5 }}
{loading ? (
<div style={{ padding: '40px', textAlign: 'center', color: '#636e72' }}>Загрузка...</div>
) : (
<div style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(280px, 1fr))',
gap: '20px',
}}>
{sections.map((section) => {
const Icon = section.icon;
const blocks = blocksMap[section.id] || [];
const hasContent = blocks.length > 0;
return (
<motion.div
key={section.id}
whileHover={{ y: -5 }}
onClick={() => openModal(section.id)}
style={{
background: 'white',
borderRadius: '16px',
padding: '25px',
boxShadow: '0 4px 20px rgba(0,0,0,0.06)',
cursor: 'pointer',
transition: 'all 0.3s ease',
border: `1px solid ${hasContent ? section.color : '#f1f2f6'}`,
position: 'relative',
}}
>
{hasContent && (
<div style={{
position: 'absolute',
top: '12px',
right: '12px',
background: section.color,
color: 'white',
fontSize: '10px',
padding: '2px 10px',
borderRadius: '12px',
fontWeight: 600,
}}>
{blocks.length} блок(ов)
</div>
)}
<div style={{ fontSize: '32px', color: section.color, marginBottom: '15px' }}>
<Icon />
</div>
<h3 style={{ margin: '0 0 10px 0', color: '#2d3436', fontSize: '18px' }}>
{section.title}
</h3>
<p style={{ color: '#636e72', fontSize: '14px', lineHeight: 1.6, margin: 0 }}>
{section.description}
</p>
{!hasContent && (
<div style={{
marginTop: '12px',
fontSize: '12px',
color: '#b2bec3',
}}>
Информация отсутствует
</div>
)}
</motion.div>
);
})}
</div>
)}
{/* ===== МОДАЛКА ===== */}
{selectedSection && selectedData && (
<div
style={{
background: 'white',
borderRadius: '16px',
padding: '25px',
boxShadow: '0 4px 20px rgba(0,0,0,0.06)',
cursor: 'pointer',
transition: 'all 0.3s ease',
border: '1px solid #f1f2f6',
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
background: 'rgba(0,0,0,0.6)',
backdropFilter: 'blur(6px)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 1000,
padding: '20px',
}}
onClick={(e) => {
if (e.target === e.currentTarget) closeModal();
}}
>
<div style={{ fontSize: '32px', color: '#e74c3c', marginBottom: '15px' }}>
{renderIcon(FaCalendarCheck)}
</div>
<h3 style={{ margin: '0 0 10px 0', color: '#2d3436', fontSize: '18px' }}>
Мероприятия
</h3>
<p style={{ color: '#636e72', fontSize: '14px', lineHeight: 1.6, margin: 0 }}>
Анонсы и запись на мероприятия
</p>
</motion.div>
<div
style={{
background: 'white',
borderRadius: '20px',
width: '70%',
height: '80%',
boxShadow: '0 30px 80px rgba(0,0,0,0.3)',
display: 'flex',
flexDirection: 'column',
overflow: 'hidden',
position: 'relative',
}}
>
{/* Шапка (заголовок + крестик) — фиксирована */}
<div
style={{
padding: '20px 30px 10px 30px',
borderBottom: '1px solid #eee',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
flexShrink: 0,
background: 'white',
zIndex: 2,
}}
>
<h3 style={{
color: selectedData.color,
fontSize: '22px',
margin: 0,
display: 'flex',
alignItems: 'center',
gap: '12px',
}}>
<selectedData.icon style={{ fontSize: '24px' }} />
{selectedData.title}
</h3>
<div style={{ flex: 1, display: 'flex', justifyContent: 'flex-end' }}>
<button
onClick={closeModal}
style={{
background: 'none',
border: 'none',
fontSize: '26px',
cursor: 'pointer',
color: '#999',
padding: '0 8px',
transition: 'color 0.2s',
display: 'flex',
alignItems: 'center',
justifyContent: 'right',
}}
onMouseEnter={(e) => e.currentTarget.style.color = '#333'}
onMouseLeave={(e) => e.currentTarget.style.color = '#999'}
>
<FaTimes />
</button>
</div>
</div>
<motion.div
whileHover={{ y: -5 }}
style={{
background: 'white',
borderRadius: '16px',
padding: '25px',
boxShadow: '0 4px 20px rgba(0,0,0,0.06)',
cursor: 'pointer',
transition: 'all 0.3s ease',
border: '1px solid #f1f2f6',
}}
>
<div style={{ fontSize: '32px', color: '#f39c12', marginBottom: '15px' }}>
{renderIcon(FaMoneyBillWave)}
</div>
<h3 style={{ margin: '0 0 10px 0', color: '#2d3436', fontSize: '18px' }}>
Материальная помощь
</h3>
<p style={{ color: '#636e72', fontSize: '14px', lineHeight: 1.6, margin: 0 }}>
Размеры и виды материальной помощи
</p>
</motion.div>
{/* Тело с прокруткой */}
<div
style={{
flex: 1,
overflowY: 'auto',
padding: '20px 30px 30px 30px',
}}
>
{selectedBlocks.length === 0 ? (
<div style={{
padding: '40px',
textAlign: 'center',
color: '#b2bec3',
fontSize: '16px',
}}>
Информация отсутствует
</div>
) : (
<div style={{
display: 'flex',
flexDirection: 'column',
gap: '16px',
}}>
{selectedBlocks.map((block) => (
<div
key={block.id}
style={{
border: '1px solid #e8e8e8',
borderRadius: '12px',
padding: '18px 20px',
background: '#fafafa',
}}
>
<div
style={{
fontSize: '15px',
lineHeight: 1.7,
color: '#2d3436',
}}
dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(block.content)
}}
/>
<div style={{
marginTop: '14px',
paddingTop: '12px',
borderTop: '1px solid #eee',
fontSize: '12px',
color: '#999',
display: 'flex',
justifyContent: 'space-between',
}}>
<span>🕒 {formatDate(block.created_at)}</span>
<span style={{ fontSize: '11px', color: '#bbb' }}>ID: {block.id}</span>
</div>
</div>
))}
</div>
)}
</div>
<motion.div
whileHover={{ y: -5 }}
style={{
background: 'white',
borderRadius: '16px',
padding: '25px',
boxShadow: '0 4px 20px rgba(0,0,0,0.06)',
cursor: 'pointer',
transition: 'all 0.3s ease',
border: '1px solid #f1f2f6',
}}
>
<div style={{ fontSize: '32px', color: '#3498db', marginBottom: '15px' }}>
{renderIcon(FaHome)}
{/* Нижняя кнопка закрытия */}
<div
style={{
padding: '12px 30px 20px 30px',
borderTop: '1px solid #eee',
display: 'flex',
justifyContent: 'center',
flexShrink: 0,
background: 'white',
}}
>
<button
onClick={closeModal}
style={{
padding: '10px 40px',
background: '#6c757d',
color: 'white',
border: 'none',
borderRadius: '10px',
cursor: 'pointer',
fontSize: '14px',
fontWeight: 500,
transition: 'background 0.2s',
}}
onMouseEnter={(e) => e.currentTarget.style.background = '#5a6268'}
onMouseLeave={(e) => e.currentTarget.style.background = '#6c757d'}
>
Закрыть
</button>
</div>
</div>
<h3 style={{ margin: '0 0 10px 0', color: '#2d3436', fontSize: '18px' }}>
Жилищные условия
</h3>
<p style={{ color: '#636e72', fontSize: '14px', lineHeight: 1.6, margin: 0 }}>
Информация об очереди на улучшение жилищных условий
</p>
</motion.div>
</div>
<div style={{
marginTop: '30px',
padding: '20px',
background: '#f8f9fa',
borderRadius: '12px',
border: '1px solid #f1f2f6',
textAlign: 'center',
color: '#636e72',
}}>
<p style={{ margin: 0 }}>
ℹ️ Раздел в разработке. Функционал будет доступен в ближайшее время.
</p>
</div>
</div>
)}
</motion.div>
);
};