mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-05-31 07:08:40 -04:00
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:
parent
6bc051165f
commit
485ac5a7a6
6 changed files with 135 additions and 24 deletions
|
@ -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);
|
||||||
|
}
|
32
frontend/src/components/explore-page/pinned-notes/caret.tsx
Normal file
32
frontend/src/components/explore-page/pinned-notes/caret.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,20 +1,29 @@
|
||||||
/*
|
/*!
|
||||||
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
|
width: 25rem;
|
||||||
min-width: 25rem;
|
min-width: 25rem;
|
||||||
height: 8rem;
|
height: 8rem;
|
||||||
position: relative;
|
position: relative;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
scroll-snap-align: start;
|
||||||
|
scroll-snap-stop: always;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card:hover {
|
.card:hover {
|
||||||
background-color: var(--bs-card-border-color);
|
background-color: var(--bs-card-border-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cardBody {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
.bookmark {
|
.bookmark {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: -2px;
|
top: -2px;
|
|
@ -7,7 +7,7 @@ import React, { type MouseEvent, useMemo } from 'react'
|
||||||
import { Badge, Card } from 'react-bootstrap'
|
import { Badge, Card } from 'react-bootstrap'
|
||||||
import { DateTime } from 'luxon'
|
import { DateTime } from 'luxon'
|
||||||
import { BookmarkStarFill as IconPinned } from 'react-bootstrap-icons'
|
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 { useCallback } from 'react'
|
||||||
import { NoteTypeIcon } from '../../common/note-type-icon/note-type-icon'
|
import { NoteTypeIcon } from '../../common/note-type-icon/note-type-icon'
|
||||||
import type { NoteType } from '@hedgedoc/commons'
|
import type { NoteType } from '@hedgedoc/commons'
|
||||||
|
@ -60,23 +60,21 @@ export const PinnedNoteCard: React.FC<NoteCardProps> = ({ title, id, lastVisited
|
||||||
}, [tags, onClickTag, labelTag])
|
}, [tags, onClickTag, labelTag])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li className={'d-block'}>
|
<Card className={`${styles.card}`} as={Link} href={`/n/${primaryAddress}`}>
|
||||||
<Card className={`${styles.card}`} as={Link} href={`/n/${primaryAddress}`}>
|
<Card.Body className={`${styles.cardBody}`}>
|
||||||
<Card.Body>
|
<div onClick={onClickUnpin} title={labelUnpinNote}>
|
||||||
<div onClick={onClickUnpin} title={labelUnpinNote}>
|
<UiIcon icon={IconPinned} size={1.5} className={`${styles.bookmark}`} />
|
||||||
<UiIcon icon={IconPinned} size={1.5} className={`${styles.bookmark}`} />
|
<div className={`${styles.star}`} />
|
||||||
<div className={`${styles.star}`} />
|
</div>
|
||||||
</div>
|
<Card.Title className={`${styles.title}`}>
|
||||||
<Card.Title className={`${styles.title}`}>
|
<NoteTypeIcon noteType={type} />
|
||||||
<NoteTypeIcon noteType={type} />
|
<span className={`${styles.titleText}`} title={title}>
|
||||||
<span className={`${styles.titleText}`} title={title}>
|
{title}
|
||||||
{title}
|
</span>
|
||||||
</span>
|
</Card.Title>
|
||||||
</Card.Title>
|
<Card.Subtitle className='mb-2 text-muted'>{lastVisitedString}</Card.Subtitle>
|
||||||
<Card.Subtitle className='mb-2 text-muted'>{lastVisitedString}</Card.Subtitle>
|
<div>{tagsChips}</div>
|
||||||
{tagsChips}
|
</Card.Body>
|
||||||
</Card.Body>
|
</Card>
|
||||||
</Card>
|
|
||||||
</li>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
|
@ -3,11 +3,13 @@
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* 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 type { NoteCardProps } from './pinned-note-card'
|
||||||
import { PinnedNoteCard } from './pinned-note-card'
|
import { PinnedNoteCard } from './pinned-note-card'
|
||||||
import { Trans, useTranslation } from 'react-i18next'
|
import { Trans, useTranslation } from 'react-i18next'
|
||||||
import { NoteType } from '@hedgedoc/commons'
|
import { NoteType } from '@hedgedoc/commons'
|
||||||
|
import { Caret } from './caret'
|
||||||
|
import styles from './pinned-notes.module.css'
|
||||||
|
|
||||||
const mockListPinnedNotes: NoteCardProps[] = [
|
const mockListPinnedNotes: NoteCardProps[] = [
|
||||||
{
|
{
|
||||||
|
@ -74,9 +76,47 @@ const mockListPinnedNotes: NoteCardProps[] = [
|
||||||
|
|
||||||
export const PinnedNotes: React.FC = () => {
|
export const PinnedNotes: React.FC = () => {
|
||||||
useTranslation()
|
useTranslation()
|
||||||
|
const scrollboxRef = useRef<HTMLDivElement>(null)
|
||||||
|
const [enableScrollLeft, setEnableScrollLeft] = useState(false)
|
||||||
|
const [enableScrollRight, setEnableScrollRight] = useState(true)
|
||||||
|
|
||||||
const cards = useMemo(() => {
|
const pinnedNotes = useMemo(() => {
|
||||||
return mockListPinnedNotes.map((note: NoteCardProps) => <PinnedNoteCard key={note.id} {...note} />)
|
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 (
|
return (
|
||||||
|
@ -84,7 +124,15 @@ export const PinnedNotes: React.FC = () => {
|
||||||
<h2 className={'mb-2'}>
|
<h2 className={'mb-2'}>
|
||||||
<Trans i18nKey={'explore.pinnedNotes.title'} />
|
<Trans i18nKey={'explore.pinnedNotes.title'} />
|
||||||
</h2>
|
</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>
|
</Fragment>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue