Anton Tranelis 9e6bcf1846
fix(source): update tailwind and daisyui (#196)
* removed daisy from config

* removed tw-elements artefact

* removed comments from tailwind config

* removed safelist

* migrated to tailwind4 and daisyui5

* deleted tailwind.config.js which is not eeded anymore

* 3.0.79

* version number

* fixed broken layouts

* more fixing

* more layout fixing

* tested theming

* small fixes

* adapt snapshots to changes

* package.json: add unit test update script

* more ui refactoring & theme controller

* ui improvements

* package-lock.json

* fix linting

* fixed tabs

* fix linting

* fixed typing

---------

Co-authored-by: mahula <lenzmath@posteo.de>
2025-04-25 16:03:42 +02:00

248 lines
7.0 KiB
TypeScript

/* eslint-disable no-console */
/* eslint-disable @typescript-eslint/no-unnecessary-condition */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/restrict-plus-operands */
/* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-call */
import { memo } from 'react'
import Markdown from 'react-markdown'
import remarkBreaks from 'remark-breaks'
import { useAddFilterTag } from '#components/Map/hooks/useFilter'
import { useTags } from '#components/Map/hooks/useTags'
import { decodeTag } from '#utils/FormatTags'
import { hashTagRegex } from '#utils/HashTagRegex'
import { fixUrls, mailRegex } from '#utils/ReplaceURLs'
import type { Item } from '#types/Item'
import type { Tag } from '#types/Tag'
/**
* @category Map
*/
export const TextView = ({
item,
itemId,
text,
truncate = false,
rawText,
itemTextField,
}: {
item?: Item
itemId?: string
text?: string
truncate?: boolean
rawText?: string
itemTextField?: string
}) => {
if (item) {
text = item.text
itemId = item.id
}
const tags = useTags()
const addFilterTag = useAddFilterTag()
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const itemTextFieldDummy = itemTextField
let innerText = ''
let replacedText = ''
if (rawText) {
innerText = replacedText = rawText
} else if (text) {
innerText = text
}
if (innerText && truncate)
innerText = truncateText(removeMarkdownKeepLinksAndParagraphs(innerText), 100)
if (innerText) replacedText = fixUrls(innerText)
if (replacedText) {
replacedText = replacedText.replace(/(?<!\]?\()https?:\/\/[^\s)]+(?!\))/g, (url) => {
let shortUrl = url
if (url.match('^https://')) {
shortUrl = url.split('https://')[1]
}
if (url.match('^http://')) {
shortUrl = url.split('http://')[1]
}
return `[${shortUrl}](${url})`
})
}
if (replacedText) {
replacedText = replacedText.replace(mailRegex, (url) => {
return `[${url}](mailto:${url})`
})
}
if (replacedText) {
replacedText = replacedText.replace(hashTagRegex, (match) => {
return `[${match}](${match})`
})
}
const CustomH1 = ({ children }) => <h1 className='tw:text-xl tw:font-bold'>{children}</h1>
const CustomH2 = ({ children }) => <h2 className='tw:text-lg tw:font-bold'>{children}</h2>
const CustomH3 = ({ children }) => <h3 className='tw:text-base tw:font-bold'>{children}</h3>
const CustomH4 = ({ children }) => <h4 className='tw:text-base tw:font-bold'>{children}</h4>
const CustomH5 = ({ children }) => <h5 className='tw:text-sm tw:font-bold'>{children}</h5>
const CustomH6 = ({ children }) => <h6 className='tw:text-sm tw:font-bold'>{children}</h6>
const CustomParagraph = ({ children }) => <p className='tw:my-2!'>{children}</p>
const CustomUnorderdList = ({ children }) => (
<ul className='tw:list-disc tw:list-inside'>{children}</ul>
)
const CustomOrderdList = ({ children }) => (
<ol className='tw:list-decimal tw:list-inside'>{children}</ol>
)
const CustomHorizontalRow = ({ children }) => <hr className='tw:border-current'>{children}</hr>
// eslint-disable-next-line react/prop-types
const CustomImage = ({ alt, src, title }) => (
<img className='tw:max-w-full tw:rounded tw:shadow' src={src} alt={alt} title={title} />
)
const CustomExternalLink = ({ href, children }) => (
<a className='tw:font-bold tw:underline' href={href} target='_blank' rel='noreferrer'>
{' '}
{children}
</a>
)
const CustomHashTagLink = ({
children,
tag,
itemId,
}: {
children: string
tag: Tag
itemId?: string
}) => {
return (
<a
style={{ color: tag ? tag.color : '#faa', fontWeight: 'bold', cursor: 'pointer' }}
key={tag ? tag.name + itemId : itemId}
onClick={(e) => {
e.stopPropagation()
addFilterTag(tag)
}}
>
{decodeTag(children)}
</a>
)
}
// eslint-disable-next-line react/display-name
const MemoizedVideoEmbed = memo(({ url }: { url: string }) => (
<iframe
className='tw:w-full'
src={url}
allow='fullscreen; picture-in-picture'
allowFullScreen
/>
))
return (
<Markdown
className={'tw:text-map tw:leading-map tw:text-sm'}
remarkPlugins={[remarkBreaks]}
components={{
p: CustomParagraph,
a: ({ href, children }: { href: string; children: string }) => {
const isYouTubeVideo = href?.startsWith('https://www.youtube.com/watch?v=')
const isRumbleVideo = href?.startsWith('https://rumble.com/embed/')
if (isYouTubeVideo) {
const videoId = href?.split('v=')[1].split('&')[0]
const youtubeEmbedUrl = `https://www.youtube-nocookie.com/embed/${videoId}`
return <MemoizedVideoEmbed url={youtubeEmbedUrl}></MemoizedVideoEmbed>
}
if (isRumbleVideo) {
return <MemoizedVideoEmbed url={href}></MemoizedVideoEmbed>
}
if (href?.startsWith('#')) {
console.log(href.slice(1).toLowerCase())
console.log(tags)
const tag = tags.find(
(t) => t.name.toLowerCase() === decodeURI(href).slice(1).toLowerCase(),
)
if (tag)
return (
<CustomHashTagLink tag={tag} itemId={itemId}>
{children}
</CustomHashTagLink>
)
else return children
} else {
return <CustomExternalLink href={href}>{children}</CustomExternalLink>
}
},
ul: CustomUnorderdList,
ol: CustomOrderdList,
img: CustomImage,
hr: CustomHorizontalRow,
h1: CustomH1,
h2: CustomH2,
h3: CustomH3,
h4: CustomH4,
h5: CustomH5,
h6: CustomH6,
}}
>
{replacedText}
</Markdown>
)
}
function removeMarkdownKeepLinksAndParagraphs(text) {
// Remove Markdown syntax using regular expressions but keep links and paragraphs
return text
.replace(/!\[.*?\]\(.*?\)/g, '') // Remove images
.replace(/(`{1,3})(.*?)\1/g, '$2') // Remove inline code
.replace(/(\*{1,2}|_{1,2})(.*?)\1/g, '$2') // Remove bold and italic
.replace(/(#+)\s+(.*)/g, '$2') // Remove headers
.replace(/>\s+(.*)/g, '$1') // Remove blockquotes
.replace(/^\s*\n/gm, '\n') // Preserve empty lines
.replace(/(\r\n|\n|\r)/gm, '\n') // Preserve line breaks
}
function truncateText(text, limit) {
if (text.length <= limit) {
return text
}
let truncated = ''
let length = 0
// Split the text by paragraphs
const paragraphs = text.split('\n')
for (const paragraph of paragraphs) {
if (length + paragraph.length > limit) {
truncated += paragraph.slice(0, limit - length) + '...'
break
} else {
truncated += paragraph + '\n'
length += paragraph.length
}
}
return truncated.trim()
}