feat(explore): add pinned notes carousel

Co-authored-by: Erik Michelson <github@erik.michelson.eu>
Signed-off-by: Erik Michelson <github@erik.michelson.eu>
Signed-off-by: Philip Molares <philip.molares@udo.edu>
This commit is contained in:
Philip Molares 2025-02-05 21:27:48 +01:00 committed by Erik Michelson
parent 6bc051165f
commit 485ac5a7a6
No known key found for this signature in database
GPG key ID: DB99ADDDC5C0AF82
6 changed files with 135 additions and 24 deletions

View file

@ -0,0 +1,10 @@
/*!
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
.active:hover {
cursor: pointer;
color: var(--bs-emphasis-color);
}

View file

@ -0,0 +1,32 @@
/*
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import React, { useMemo } from 'react'
import {
CaretLeft as IconCaretLeftEmpty,
CaretLeftFill as IconCaretLeft,
CaretRight as IconCaretRightEmpty,
CaretRightFill as IconCaretRight
} from 'react-bootstrap-icons'
import styles from './caret.module.scss'
import { UiIcon } from '../../common/icons/ui-icon'
interface CaretProps {
left: boolean
active: boolean
onClick?: () => void
}
export const Caret: React.FC<CaretProps> = ({ active, left, onClick }) => {
const activeIcon = useMemo(() => (left ? IconCaretLeft : IconCaretRight), [left])
const inactiveIcon = useMemo(() => (left ? IconCaretLeftEmpty : IconCaretRightEmpty), [left])
return (
<div onClick={active ? onClick : undefined} className={`${active ? styles.active : undefined}`}>
<UiIcon icon={active ? activeIcon : inactiveIcon} size={2} />
</div>
)
}

View file

@ -1,20 +1,29 @@
/*
/*!
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
.card {
width: 25rem;
min-width: 25rem;
height: 8rem;
position: relative;
text-decoration: none;
scroll-snap-align: start;
scroll-snap-stop: always;
}
.card:hover {
background-color: var(--bs-card-border-color);
}
.cardBody {
display: flex;
flex-direction: column;
justify-content: space-between;
}
.bookmark {
position: absolute;
top: -2px;

View file

@ -7,7 +7,7 @@ import React, { type MouseEvent, useMemo } from 'react'
import { Badge, Card } from 'react-bootstrap'
import { DateTime } from 'luxon'
import { BookmarkStarFill as IconPinned } from 'react-bootstrap-icons'
import styles from './pinned-note-card.module.css'
import styles from './pinned-note-card.module.scss'
import { useCallback } from 'react'
import { NoteTypeIcon } from '../../common/note-type-icon/note-type-icon'
import type { NoteType } from '@hedgedoc/commons'
@ -60,9 +60,8 @@ export const PinnedNoteCard: React.FC<NoteCardProps> = ({ title, id, lastVisited
}, [tags, onClickTag, labelTag])
return (
<li className={'d-block'}>
<Card className={`${styles.card}`} as={Link} href={`/n/${primaryAddress}`}>
<Card.Body>
<Card.Body className={`${styles.cardBody}`}>
<div onClick={onClickUnpin} title={labelUnpinNote}>
<UiIcon icon={IconPinned} size={1.5} className={`${styles.bookmark}`} />
<div className={`${styles.star}`} />
@ -74,9 +73,8 @@ export const PinnedNoteCard: React.FC<NoteCardProps> = ({ title, id, lastVisited
</span>
</Card.Title>
<Card.Subtitle className='mb-2 text-muted'>{lastVisitedString}</Card.Subtitle>
{tagsChips}
<div>{tagsChips}</div>
</Card.Body>
</Card>
</li>
)
}

View file

@ -0,0 +1,14 @@
/*
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
.scrollbox {
display: flex;
flex-flow: row;
overflow-x: scroll;
gap: 0.5rem;
scroll-snap-type: x mandatory;
scrollbar-width: none;
}

View file

@ -3,11 +3,13 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import React, { Fragment, useMemo } from 'react'
import React, { Fragment, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import type { NoteCardProps } from './pinned-note-card'
import { PinnedNoteCard } from './pinned-note-card'
import { Trans, useTranslation } from 'react-i18next'
import { NoteType } from '@hedgedoc/commons'
import { Caret } from './caret'
import styles from './pinned-notes.module.css'
const mockListPinnedNotes: NoteCardProps[] = [
{
@ -74,9 +76,47 @@ const mockListPinnedNotes: NoteCardProps[] = [
export const PinnedNotes: React.FC = () => {
useTranslation()
const scrollboxRef = useRef<HTMLDivElement>(null)
const [enableScrollLeft, setEnableScrollLeft] = useState(false)
const [enableScrollRight, setEnableScrollRight] = useState(true)
const cards = useMemo(() => {
return mockListPinnedNotes.map((note: NoteCardProps) => <PinnedNoteCard key={note.id} {...note} />)
const pinnedNotes = useMemo(() => {
return mockListPinnedNotes
}, [])
const leftClick = useCallback(() => {
if (!scrollboxRef.current) {
return
}
scrollboxRef.current.scrollBy({
left: -400,
behavior: 'smooth'
})
}, [])
const rightClick = useCallback(() => {
if (!scrollboxRef.current) {
return
}
scrollboxRef.current.scrollBy({
left: 400,
behavior: 'smooth'
})
}, [])
useEffect(() => {
if (!scrollboxRef.current) {
return
}
const scrollbox = scrollboxRef.current
const scrollHandler = () => {
setEnableScrollLeft(scrollbox.scrollLeft > 0)
setEnableScrollRight(Math.ceil(scrollbox.scrollLeft + scrollbox.clientWidth) < scrollbox.scrollWidth)
}
scrollbox.addEventListener('scroll', scrollHandler)
scrollHandler()
return () => {
scrollbox.removeEventListener('scroll', scrollHandler)
}
}, [])
return (
@ -84,7 +124,15 @@ export const PinnedNotes: React.FC = () => {
<h2 className={'mb-2'}>
<Trans i18nKey={'explore.pinnedNotes.title'} />
</h2>
<ul className={'d-block mx-2 py-2 mb-5 d-flex gap-2 w-100 overflow-x-auto'}>{cards}</ul>
<div className={'d-flex flex-row gap-2 align-items-center mb-4'}>
<Caret active={enableScrollLeft} left={true} onClick={leftClick} />
<div className={styles.scrollbox} ref={scrollboxRef}>
{pinnedNotes.map((note: NoteCardProps) => (
<PinnedNoteCard key={note.id} {...note} />
))}
</div>
<Caret active={enableScrollRight} left={false} onClick={rightClick} />
</div>
</Fragment>
)
}