noteJS-react/src/components/notes/PublicNoteItem.tsx

182 lines
6.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState, useEffect, useRef } from "react";
import { Icon } from "@iconify/react";
import { Note } from "../../types/note";
import {
parseMarkdown,
makeTagsClickable,
} from "../../utils/markdown";
import { parseSQLiteUtc, formatLocalDateTime } from "../../utils/dateFormat";
import { useMarkdown } from "../../hooks/useMarkdown";
interface PublicNoteItemProps {
note: Note;
onImageClick: (imageUrl: string) => void;
}
export const PublicNoteItem: React.FC<PublicNoteItemProps> = ({
note,
onImageClick,
}) => {
useMarkdown(); // Инициализируем обработчики спойлеров и внешних ссылок
const textNoteRef = useRef<HTMLDivElement>(null);
const [isLongNote, setIsLongNote] = useState(false);
const [isExpanded, setIsExpanded] = useState(false);
// Форматируем дату для отображения
const formatDate = () => {
const createdDate = parseSQLiteUtc(note.created_at);
return formatLocalDateTime(createdDate);
};
// Форматируем содержимое заметки
const formatContent = () => {
let content = note.content;
// Делаем теги кликабельными (для публичной страницы они не будут работать, но стиль сохраним)
content = makeTagsClickable(content);
// Парсим markdown с флагом read-only (чтобы чекбоксы были disabled)
const htmlContent = parseMarkdown(content, true);
return htmlContent;
};
const toggleExpand = () => {
setIsExpanded(!isExpanded);
};
// Определяем длинные заметки для сворачивания
useEffect(() => {
const checkNoteLength = () => {
if (!textNoteRef.current) return;
const scrollHeight = textNoteRef.current.scrollHeight;
// Считаем заметку длинной, если она больше 300px
const isLong = scrollHeight > 300;
setIsLongNote(isLong);
};
const timer = setTimeout(checkNoteLength, 100);
return () => clearTimeout(timer);
}, [note.content]);
// Получаем иконку файла по расширению
const getFileIcon = (filename: string): string => {
const ext = filename.split(".").pop()?.toLowerCase();
const iconMap: { [key: string]: string } = {
pdf: "mdi:file-pdf-box",
doc: "mdi:file-word-box",
docx: "mdi:file-word-box",
xls: "mdi:file-excel-box",
xlsx: "mdi:file-excel-box",
txt: "mdi:file-document-outline",
zip: "mdi:folder-zip",
rar: "mdi:folder-zip",
"7z": "mdi:folder-zip",
};
return iconMap[ext || ""] || "mdi:file";
};
// Форматируем размер файла
const formatFileSize = (bytes: number): string => {
if (bytes < 1024) return bytes + " B";
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + " KB";
return (bytes / (1024 * 1024)).toFixed(1) + " MB";
};
return (
<div
className={`container ${note.is_pinned === 1 ? "note-pinned" : ""}`}
data-note-id={note.id}
>
<div className="date">
<span className="date-text">
{formatDate()}
{note.is_pinned === 1 && (
<span className="pin-indicator">
<Icon icon="mdi:pin" />
Закреплено
</span>
)}
</span>
</div>
<div
ref={textNoteRef}
className={`textNote ${isLongNote && !isExpanded ? "collapsed" : ""}`}
data-original-content={note.content}
dangerouslySetInnerHTML={{ __html: formatContent() }}
onClick={(e) => {
// Обработка клика по тегам (для публичной страницы не работает, но стиль сохраняем)
const target = e.target as HTMLElement;
if (target.classList.contains("tag-in-note")) {
// Для публичной страницы теги не кликабельны
e.preventDefault();
}
}}
/>
{isLongNote && (
<button
className="show-more-btn"
onClick={toggleExpand}
type="button"
>
<Icon
icon={isExpanded ? "mdi:chevron-up" : "mdi:chevron-down"}
/>
<span>{isExpanded ? "Скрыть" : "Раскрыть"}</span>
</button>
)}
{note.images && note.images.length > 0 && (
<div className="note-images-container">
{note.images.map((image) => {
// Используем публичный endpoint для изображений
const imageUrl = `/api/public/notes/${note.id}/images/${image.id}`;
return (
<div key={image.id} className="note-image-item">
<img
src={imageUrl}
alt={image.original_name}
className="note-image lazy"
data-src={imageUrl}
data-image-id={image.id}
loading="lazy"
onClick={() => onImageClick(imageUrl)}
/>
</div>
);
})}
</div>
)}
{note.files && note.files.length > 0 && (
<div className="note-files-container">
{note.files.map((file) => {
// Используем публичный endpoint для файлов
const fileUrl = `/api/public/notes/${note.id}/files/${file.id}`;
return (
<div key={file.id} className="note-file-item">
<a
href={fileUrl}
download={file.original_name}
className="note-file-link"
data-file-id={file.id}
>
<Icon
icon={getFileIcon(file.original_name)}
className="file-icon"
/>
<div className="file-info">
<div className="file-name">{file.original_name}</div>
<div className="file-size">
{formatFileSize(file.file_size)}
</div>
</div>
</a>
</div>
);
})}
</div>
)}
</div>
);
};