mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-06-08 02:15:02 -04:00
feat(explore): add common explore page layout
Co-authored-by: Philip Molares <philip.molares@udo.edu> Signed-off-by: Philip Molares <philip.molares@udo.edu> Signed-off-by: Erik Michelson <github@erik.michelson.eu>
This commit is contained in:
parent
055d2b6c5b
commit
869366a744
24 changed files with 525 additions and 18 deletions
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
.link {
|
||||
color: var(--bs-secondary-color);
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
.active {
|
||||
color: var(--bs-heading-color);
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
cursor: text;
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import { useMemo } from 'react'
|
||||
import React from 'react'
|
||||
import Link from 'next/link'
|
||||
import { usePathname } from 'next/navigation'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import styles from './mode-link.module.css'
|
||||
import type { Mode } from './mode'
|
||||
|
||||
export interface ModeLinkProps {
|
||||
mode: Mode
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a link to switch to another mode of the explore page
|
||||
|
||||
* @param mode The target mode to link to
|
||||
*/
|
||||
export const ModeLink: React.FC<ModeLinkProps> = ({ mode }) => {
|
||||
useTranslation()
|
||||
const path = usePathname()
|
||||
const isActive = useMemo(() => path === `/explore/${mode}`, [path, mode])
|
||||
|
||||
return isActive ? (
|
||||
<span className={`${styles.link} ${styles.active}`}>
|
||||
<Trans i18nKey={`explore.modes.${mode}`} />
|
||||
</span>
|
||||
) : (
|
||||
<Link href={`/explore/${mode}`} className={styles.link}>
|
||||
<Trans i18nKey={`explore.modes.${mode}`} />
|
||||
</Link>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import React from 'react'
|
||||
import { ModeLink } from './mode-link'
|
||||
import { useIsLoggedIn } from '../../../hooks/common/use-is-logged-in'
|
||||
import { Mode } from './mode'
|
||||
|
||||
/**
|
||||
* Renders the switcher for the different modes of the explore page.
|
||||
* Since notes can't be shared with anonymous guests, the shared mode is only shown to logged-in users.
|
||||
*/
|
||||
export const ModeSelection: React.FC = () => {
|
||||
const userLoggedIn = useIsLoggedIn()
|
||||
|
||||
return (
|
||||
<h2 className={'mb-3'}>
|
||||
<ModeLink mode={Mode.MY_NOTES} />
|
||||
{userLoggedIn && <ModeLink mode={Mode.SHARED_WITH_ME} />}
|
||||
<ModeLink mode={Mode.PUBLIC} />
|
||||
</h2>
|
||||
)
|
||||
}
|
10
frontend/src/components/explore-page/mode-selection/mode.ts
Normal file
10
frontend/src/components/explore-page/mode-selection/mode.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
export enum Mode {
|
||||
MY_NOTES = 'my',
|
||||
SHARED_WITH_ME = 'shared',
|
||||
PUBLIC = 'public'
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
.card {
|
||||
min-width: 25rem;
|
||||
height: 8rem;
|
||||
position: relative;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
background-color: var(--bs-card-border-color);
|
||||
}
|
||||
|
||||
.bookmark {
|
||||
position: absolute;
|
||||
top: -2px;
|
||||
right: 1rem;
|
||||
padding: 0;
|
||||
color: #c40c0c;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.star {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 1.5rem;
|
||||
padding: 0;
|
||||
width: 11px;
|
||||
height: 16px;
|
||||
background-color: #f8df33;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.title {
|
||||
width: 22rem;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.titleText {
|
||||
padding-top: 2rem;
|
||||
padding-left: 0.5rem;
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
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 { useCallback } from 'react'
|
||||
import { NoteTypeIcon } from '../../common/note-type-icon/note-type-icon'
|
||||
import type { NoteType } from '@hedgedoc/commons'
|
||||
import { UiIcon } from '../../common/icons/ui-icon'
|
||||
import Link from 'next/link'
|
||||
import { useTranslatedText } from '../../../hooks/common/use-translated-text'
|
||||
import { useRouter } from 'next/navigation'
|
||||
|
||||
export interface NoteCardProps {
|
||||
title: string
|
||||
id: string
|
||||
type: NoteType
|
||||
lastVisited: string
|
||||
created: string
|
||||
pinned: boolean
|
||||
tags: string[]
|
||||
primaryAddress: string
|
||||
}
|
||||
|
||||
export const PinnedNoteCard: React.FC<NoteCardProps> = ({ title, id, lastVisited, type, primaryAddress, tags }) => {
|
||||
const router = useRouter()
|
||||
const labelTag = useTranslatedText('explore.filters.byTag')
|
||||
const labelUnpinNote = useTranslatedText('explore.pinnedNotes.unpin')
|
||||
const lastVisitedString = useMemo(() => DateTime.fromISO(lastVisited).toRelative(), [lastVisited])
|
||||
// const createdString = DateTime.fromISO(created).toFormat('DDDD T')
|
||||
const onClickUnpin = useCallback(
|
||||
(event: MouseEvent<HTMLDivElement>) => {
|
||||
event.preventDefault()
|
||||
alert(`UnFav ${id}`)
|
||||
},
|
||||
[id]
|
||||
)
|
||||
|
||||
const onClickTag = useCallback(
|
||||
(tag: string) => {
|
||||
return (event: MouseEvent<HTMLDivElement>) => {
|
||||
event.preventDefault()
|
||||
router.push(`?search=tag:${tag}`)
|
||||
}
|
||||
},
|
||||
[router]
|
||||
)
|
||||
|
||||
const tagsChips = useMemo(() => {
|
||||
return tags.map((tag) => (
|
||||
<Badge key={tag} bg={'secondary'} pill={true} className={'me-1'} onClick={onClickTag(tag)} title={labelTag}>
|
||||
{tag}
|
||||
</Badge>
|
||||
))
|
||||
}, [tags, onClickTag, labelTag])
|
||||
|
||||
return (
|
||||
<li className={'d-block'}>
|
||||
<Card className={`${styles.card}`} as={Link} href={`/n/${primaryAddress}`}>
|
||||
<Card.Body>
|
||||
<div onClick={onClickUnpin} title={labelUnpinNote}>
|
||||
<UiIcon icon={IconPinned} size={1.5} className={`${styles.bookmark}`} />
|
||||
<div className={`${styles.star}`} />
|
||||
</div>
|
||||
<Card.Title className={`${styles.title}`}>
|
||||
<NoteTypeIcon noteType={type} />
|
||||
<span className={`${styles.titleText}`} title={title}>
|
||||
{title}
|
||||
</span>
|
||||
</Card.Title>
|
||||
<Card.Subtitle className='mb-2 text-muted'>{lastVisitedString}</Card.Subtitle>
|
||||
{tagsChips}
|
||||
</Card.Body>
|
||||
</Card>
|
||||
</li>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,90 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import React, { Fragment, useMemo } 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'
|
||||
|
||||
const mockListPinnedNotes: NoteCardProps[] = [
|
||||
{
|
||||
id: '1',
|
||||
title: 'othermo Backend / Fullstack Dev',
|
||||
type: NoteType.DOCUMENT,
|
||||
pinned: true,
|
||||
lastVisited: new Date(2025, 0, 2, 14, 0, 0).toISOString(),
|
||||
created: new Date(2025, 0, 1, 12, 0, 0).toISOString(),
|
||||
tags: ['Arbeit', 'Ausschreibung'],
|
||||
primaryAddress: 'othermo'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
title: 'HedgeDoc e.V.',
|
||||
type: NoteType.DOCUMENT,
|
||||
pinned: false,
|
||||
lastVisited: new Date(2025, 0, 13, 14, 0, 0).toISOString(),
|
||||
created: new Date(2025, 0, 12, 12, 0, 0).toISOString(),
|
||||
tags: ['HedgeDoc', 'Verein'],
|
||||
primaryAddress: 'ev'
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
title: 'Sister projects of HedgeDoc for the future',
|
||||
type: NoteType.DOCUMENT,
|
||||
pinned: false,
|
||||
lastVisited: new Date(2025, 0, 13, 14, 0, 0).toISOString(),
|
||||
created: new Date(2025, 0, 12, 12, 0, 0).toISOString(),
|
||||
tags: ['HedgeDoc', 'Funny'],
|
||||
primaryAddress: 'sister-projects'
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
title: 'HedgeDoc Keynote',
|
||||
type: NoteType.SLIDE,
|
||||
pinned: false,
|
||||
lastVisited: new Date(2025, 0, 13, 14, 0, 0).toISOString(),
|
||||
created: new Date(2025, 0, 12, 12, 0, 0).toISOString(),
|
||||
tags: [],
|
||||
primaryAddress: 'keynote'
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
title: 'KIF-Admin KIF 47,5',
|
||||
type: NoteType.DOCUMENT,
|
||||
pinned: false,
|
||||
lastVisited: new Date(2020, 2, 13, 14, 0, 0).toISOString(),
|
||||
created: new Date(2019, 10, 12, 12, 0, 0).toISOString(),
|
||||
tags: ['KIF-Admin', 'KIF 47,5'],
|
||||
primaryAddress: '5'
|
||||
},
|
||||
{
|
||||
id: '6',
|
||||
title: 'kif.rocks vs WifiOnICE/Bahn WLAN',
|
||||
type: NoteType.DOCUMENT,
|
||||
pinned: false,
|
||||
lastVisited: new Date(2020, 0, 13, 14, 0, 0).toISOString(),
|
||||
created: new Date(2020, 0, 12, 12, 0, 0).toISOString(),
|
||||
tags: ['Privat', 'Blogpost'],
|
||||
primaryAddress: 'wifionice'
|
||||
}
|
||||
]
|
||||
|
||||
export const PinnedNotes: React.FC = () => {
|
||||
useTranslation()
|
||||
|
||||
const cards = useMemo(() => {
|
||||
return mockListPinnedNotes.map((note: NoteCardProps) => <PinnedNoteCard key={note.id} {...note} />)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<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>
|
||||
</Fragment>
|
||||
)
|
||||
}
|
26
frontend/src/components/explore-page/welcome.tsx
Normal file
26
frontend/src/components/explore-page/welcome.tsx
Normal file
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import React from 'react'
|
||||
import { useApplicationState } from '../../hooks/common/use-application-state'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
|
||||
/**
|
||||
* Renders the welcome message for the explore page.
|
||||
*/
|
||||
export const Welcome: React.FC = () => {
|
||||
useTranslation()
|
||||
const userName = useApplicationState((state) => state.user?.displayName)
|
||||
|
||||
return (
|
||||
<h1 className={'my-4'}>
|
||||
{userName !== undefined ? (
|
||||
<Trans i18nKey={'explore.welcome.user'} values={{ userName }} />
|
||||
) : (
|
||||
<Trans i18nKey={'explore.welcome.guest'} />
|
||||
)}
|
||||
</h1>
|
||||
)
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue