Introduce SubPopover (#1620)
* Introduce SubPopover * add truncation to sub description popover --------- Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com> Co-authored-by: k00b <k00b@stacker.news>
This commit is contained in:
parent
bd5db1b62e
commit
55d1f2c952
|
@ -27,6 +27,7 @@ import { useRetryCreateItem } from './use-item-submit'
|
|||
import { useToast } from './toast'
|
||||
import { useShowModal } from './modal'
|
||||
import classNames from 'classnames'
|
||||
import SubPopover from './sub-popover'
|
||||
|
||||
export default function ItemInfo ({
|
||||
item, full, commentsText = 'comments',
|
||||
|
@ -134,9 +135,11 @@ export default function ItemInfo ({
|
|||
</>}
|
||||
</span>
|
||||
{item.subName &&
|
||||
<Link href={`/~${item.subName}`}>
|
||||
{' '}<Badge className={styles.newComment} bg={null}>{item.subName}</Badge>
|
||||
</Link>}
|
||||
<SubPopover sub={item.subName}>
|
||||
<Link href={`/~${item.subName}`}>
|
||||
{' '}<Badge className={styles.newComment} bg={null}>{item.subName}</Badge>
|
||||
</Link>
|
||||
</SubPopover>}
|
||||
{sub?.nsfw &&
|
||||
<Badge className={styles.newComment} bg={null}>nsfw</Badge>}
|
||||
{(item.outlawed && !item.mine &&
|
||||
|
|
|
@ -12,6 +12,7 @@ import Badges from './badge'
|
|||
import { MEDIA_URL } from '@/lib/constants'
|
||||
import { abbrNum } from '@/lib/format'
|
||||
import { Badge } from 'react-bootstrap'
|
||||
import SubPopover from './sub-popover'
|
||||
|
||||
export default function ItemJob ({ item, toc, rank, children }) {
|
||||
const isEmail = string().email().isValidSync(item.url)
|
||||
|
@ -62,9 +63,11 @@ export default function ItemJob ({ item, toc, rank, children }) {
|
|||
</Link>
|
||||
</span>
|
||||
{item.subName &&
|
||||
<Link href={`/~${item.subName}`}>
|
||||
{' '}<Badge className={styles.newComment} bg={null}>{item.subName}</Badge>
|
||||
</Link>}
|
||||
<SubPopover sub={item.subName}>
|
||||
<Link href={`/~${item.subName}`}>
|
||||
{' '}<Badge className={styles.newComment} bg={null}>{item.subName}</Badge>
|
||||
</Link>
|
||||
</SubPopover>}
|
||||
{item.status === 'STOPPED' &&
|
||||
<>{' '}<Badge bg='info' className={styles.badge}>stopped</Badge></>}
|
||||
{item.mine && !item.deletedAt &&
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
import { SUB_FULL } from '@/fragments/subs'
|
||||
import errorStyles from '@/styles/error.module.css'
|
||||
import { useLazyQuery } from '@apollo/client'
|
||||
import classNames from 'classnames'
|
||||
import HoverablePopover from './hoverable-popover'
|
||||
import { TerritoryInfo, TerritoryInfoSkeleton } from './territory-header'
|
||||
import { truncateString } from '@/lib/format'
|
||||
|
||||
export default function SubPopover ({ sub, children }) {
|
||||
const [getSub, { loading, data }] = useLazyQuery(
|
||||
SUB_FULL,
|
||||
{
|
||||
variables: { sub },
|
||||
fetchPolicy: 'cache-first'
|
||||
}
|
||||
)
|
||||
|
||||
return (
|
||||
<HoverablePopover
|
||||
onShow={getSub}
|
||||
trigger={children}
|
||||
body={!data || loading
|
||||
? <TerritoryInfoSkeleton />
|
||||
: !data.sub
|
||||
? <h1 className={classNames(errorStyles.status, errorStyles.describe)}>SUB NOT FOUND</h1>
|
||||
: <TerritoryInfo sub={{ ...data.sub, desc: truncateString(data.sub.desc, 280) }} />}
|
||||
/>
|
||||
)
|
||||
}
|
|
@ -31,6 +31,17 @@ export function TerritoryDetails ({ sub, children }) {
|
|||
)
|
||||
}
|
||||
|
||||
export function TerritoryInfoSkeleton ({ children, className }) {
|
||||
return (
|
||||
<div className={`${styles.item} ${styles.skeleton} ${className}`}>
|
||||
<div className={styles.hunk}>
|
||||
<div className={`${styles.name} clouds text-reset`} />
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function TerritoryInfo ({ sub }) {
|
||||
return (
|
||||
<>
|
||||
|
|
|
@ -12,6 +12,7 @@ import { useRouter } from 'next/router'
|
|||
import Link from 'next/link'
|
||||
import { UNKNOWN_LINK_REL } from '@/lib/constants'
|
||||
import isEqual from 'lodash/isEqual'
|
||||
import SubPopover from './sub-popover'
|
||||
import UserPopover from './user-popover'
|
||||
import ItemPopover from './item-popover'
|
||||
import classNames from 'classnames'
|
||||
|
@ -183,8 +184,12 @@ function Mention ({ children, node, href, name, id }) {
|
|||
)
|
||||
}
|
||||
|
||||
function Sub ({ children, node, href, ...props }) {
|
||||
return <Link href={href}>{children}</Link>
|
||||
function Sub ({ children, node, href, name, ...props }) {
|
||||
return (
|
||||
<SubPopover sub={name}>
|
||||
<Link href={href}>{children}</Link>
|
||||
</SubPopover>
|
||||
)
|
||||
}
|
||||
|
||||
function Item ({ children, node, href, id }) {
|
||||
|
|
|
@ -204,3 +204,49 @@ export const toPositive = (x) => {
|
|||
if (typeof x === 'bigint') return toPositiveBigInt(x)
|
||||
return toPositiveNumber(x)
|
||||
}
|
||||
|
||||
/**
|
||||
* Truncates a string intelligently, trying to keep natural breaks
|
||||
* @param {string} str - The string to truncate
|
||||
* @param {number} maxLength - Maximum length of the result
|
||||
* @param {string} [suffix='...'] - String to append when truncated
|
||||
* @returns {string} Truncated string
|
||||
*/
|
||||
export const truncateString = (str, maxLength, suffix = ' ...') => {
|
||||
if (!str || str.length <= maxLength) return str
|
||||
|
||||
const effectiveLength = maxLength - suffix.length
|
||||
|
||||
// Split into paragraphs and accumulate until we exceed the limit
|
||||
const paragraphs = str.split(/\n\n+/)
|
||||
let result = ''
|
||||
for (const paragraph of paragraphs) {
|
||||
if ((result + paragraph).length > effectiveLength) {
|
||||
// If this is the first paragraph and it's too long,
|
||||
// fall back to sentence/word breaking
|
||||
if (!result) {
|
||||
// Try to break at sentence
|
||||
const sentenceBreak = paragraph.slice(0, effectiveLength).match(/[.!?]\s+[A-Z]/g)
|
||||
if (sentenceBreak) {
|
||||
const lastBreak = paragraph.lastIndexOf(sentenceBreak[sentenceBreak.length - 1], effectiveLength)
|
||||
if (lastBreak > effectiveLength / 2) {
|
||||
return paragraph.slice(0, lastBreak + 1) + suffix
|
||||
}
|
||||
}
|
||||
|
||||
// Try to break at word
|
||||
const wordBreak = paragraph.lastIndexOf(' ', effectiveLength)
|
||||
if (wordBreak > 0) {
|
||||
return paragraph.slice(0, wordBreak) + suffix
|
||||
}
|
||||
|
||||
// Fall back to character break
|
||||
return paragraph.slice(0, effectiveLength) + suffix
|
||||
}
|
||||
return result.trim() + suffix
|
||||
}
|
||||
result += (result ? '\n\n' : '') + paragraph
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue