mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-05-25 12:34:45 -04:00
feat(explore): API methods for asking backend for explore page notes
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
f645cffcd5
commit
a60c7a6e10
6 changed files with 238 additions and 71 deletions
79
frontend/src/api/explore/index.ts
Normal file
79
frontend/src/api/explore/index.ts
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { NoteEntry } from './types'
|
||||||
|
import { GetApiRequestBuilder } from '../common/api-request-builder/get-api-request-builder'
|
||||||
|
import type { SortMode } from '../../components/explore-page/explore-notes-section/filters/sort-button'
|
||||||
|
import type { NoteType } from '@hedgedoc/commons'
|
||||||
|
import { createURLSearchParams } from './utils'
|
||||||
|
import { mockNotes } from './mock'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches the pinned notes of a user
|
||||||
|
*
|
||||||
|
* @return A list of pinned notes.
|
||||||
|
* @throws {Error} when the api request wasn't successful.
|
||||||
|
*/
|
||||||
|
export const getPinnedNotes = async (): Promise<NoteEntry[]> => {
|
||||||
|
return mockNotes.filter((note) => note.isPinned)
|
||||||
|
const response = await new GetApiRequestBuilder<NoteEntry[]>('explore/pinned').sendRequest()
|
||||||
|
return response.asParsedJsonObject()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches the notes of the logged-in user for the explore page
|
||||||
|
*
|
||||||
|
* @return The notes of the logged-in user.
|
||||||
|
* @throws {Error} when the api request wasn't successful.
|
||||||
|
*/
|
||||||
|
export const getMyNotes = async (
|
||||||
|
sort: SortMode,
|
||||||
|
searchFilter: string | null,
|
||||||
|
typeFilter: NoteType | null
|
||||||
|
): Promise<NoteEntry[]> => {
|
||||||
|
return mockNotes
|
||||||
|
const params = createURLSearchParams(sort, searchFilter, typeFilter)
|
||||||
|
const response = await new GetApiRequestBuilder<NoteEntry[]>(`explore/my?${params}`).sendRequest()
|
||||||
|
return response.asParsedJsonObject()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches the notes shared with the logged-in user for the explore page
|
||||||
|
*
|
||||||
|
* @return The notes shared with the logged-in user.
|
||||||
|
* @throws {Error} when the api request wasn't successful.
|
||||||
|
*/
|
||||||
|
export const getSharedNotes = async (
|
||||||
|
sort: SortMode,
|
||||||
|
searchFilter: string | null,
|
||||||
|
typeFilter: NoteType | null
|
||||||
|
): Promise<NoteEntry[]> => {
|
||||||
|
return mockNotes
|
||||||
|
const params = createURLSearchParams(sort, searchFilter, typeFilter)
|
||||||
|
const response = await new GetApiRequestBuilder<NoteEntry[]>(`explore/shared?${params}`).sendRequest()
|
||||||
|
return response.asParsedJsonObject()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches the public notes of the instance
|
||||||
|
*
|
||||||
|
* @return A list of public notes.
|
||||||
|
* @throws {Error} when the api request wasn't successful.
|
||||||
|
*/
|
||||||
|
export const getPublicNotes = async (
|
||||||
|
sort: SortMode,
|
||||||
|
searchFilter: string | null,
|
||||||
|
typeFilter: NoteType | null
|
||||||
|
): Promise<NoteEntry[]> => {
|
||||||
|
return mockNotes
|
||||||
|
const params = createURLSearchParams(sort, searchFilter, typeFilter)
|
||||||
|
const response = await new GetApiRequestBuilder<NoteEntry[]>(`explore/public?${params}`).sendRequest()
|
||||||
|
return response.asParsedJsonObject()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
60
frontend/src/api/explore/mock.ts
Normal file
60
frontend/src/api/explore/mock.ts
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
// TODO Remove mock entries as soon as the backend is implemented
|
||||||
|
import type { NoteEntry } from './types'
|
||||||
|
import { NoteType } from '@hedgedoc/commons'
|
||||||
|
|
||||||
|
export const mockNotes: NoteEntry[] = [
|
||||||
|
{
|
||||||
|
primaryAddress: 'othermo',
|
||||||
|
title: 'othermo Backend / Fullstack Dev',
|
||||||
|
type: NoteType.DOCUMENT,
|
||||||
|
isPinned: true,
|
||||||
|
lastChangedAt: new Date(2025, 0, 2, 14, 0, 0).toISOString(),
|
||||||
|
tags: ['Arbeit', 'Ausschreibung']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
primaryAddress: 'ev',
|
||||||
|
title: 'HedgeDoc e.V.',
|
||||||
|
type: NoteType.DOCUMENT,
|
||||||
|
isPinned: false,
|
||||||
|
lastChangedAt: new Date(2025, 0, 13, 14, 0, 0).toISOString(),
|
||||||
|
tags: ['HedgeDoc', 'Verein']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
primaryAddress: 'sister-projects',
|
||||||
|
title: 'Sister projects of HedgeDoc for the future',
|
||||||
|
type: NoteType.DOCUMENT,
|
||||||
|
isPinned: false,
|
||||||
|
lastChangedAt: new Date(2025, 0, 13, 14, 0, 0).toISOString(),
|
||||||
|
tags: ['HedgeDoc', 'Funny']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
primaryAddress: 'keynote',
|
||||||
|
title: 'HedgeDoc Keynote',
|
||||||
|
type: NoteType.SLIDE,
|
||||||
|
isPinned: false,
|
||||||
|
lastChangedAt: new Date(2025, 0, 13, 14, 0, 0).toISOString(),
|
||||||
|
tags: []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
primaryAddress: '5',
|
||||||
|
title: 'KIF-Admin KIF 47,5',
|
||||||
|
type: NoteType.DOCUMENT,
|
||||||
|
isPinned: false,
|
||||||
|
lastChangedAt: new Date(2020, 2, 13, 14, 0, 0).toISOString(),
|
||||||
|
tags: ['KIF-Admin', 'KIF 47,5']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
primaryAddress: 'wifionice',
|
||||||
|
title: 'kif.rocks vs WifiOnICE/Bahn WLAN',
|
||||||
|
type: NoteType.DOCUMENT,
|
||||||
|
isPinned: false,
|
||||||
|
lastChangedAt: new Date(2020, 0, 13, 14, 0, 0).toISOString(),
|
||||||
|
tags: ['Privat', 'Blogpost']
|
||||||
|
}
|
||||||
|
]
|
17
frontend/src/api/explore/types.ts
Normal file
17
frontend/src/api/explore/types.ts
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { NoteType } from '@hedgedoc/commons'
|
||||||
|
|
||||||
|
export interface NoteEntry {
|
||||||
|
primaryAddress: string
|
||||||
|
title: string
|
||||||
|
type: NoteType
|
||||||
|
tags: string[]
|
||||||
|
owner: string | null
|
||||||
|
isPinned: boolean
|
||||||
|
lastChangedAt: string
|
||||||
|
}
|
36
frontend/src/api/explore/utils.spec.ts
Normal file
36
frontend/src/api/explore/utils.spec.ts
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { SortMode } from '../../components/explore-page/explore-notes-section/filters/sort-button'
|
||||||
|
import { NoteType } from '@hedgedoc/commons'
|
||||||
|
import { createURLSearchParams } from './utils'
|
||||||
|
|
||||||
|
describe('createURLSearchParams', () => {
|
||||||
|
it('only sort is defined', () => {
|
||||||
|
const sort = SortMode.TITLE_ASC
|
||||||
|
const result = createURLSearchParams(sort, null, null)
|
||||||
|
expect(result).toStrictEqual('sort=title_asc')
|
||||||
|
})
|
||||||
|
it('sort and search are defined', () => {
|
||||||
|
const sort = SortMode.TITLE_ASC
|
||||||
|
const search = 'test'
|
||||||
|
const result = createURLSearchParams(sort, search, null)
|
||||||
|
expect(result).toStrictEqual('sort=title_asc&search=test')
|
||||||
|
})
|
||||||
|
it('sort and type are defined', () => {
|
||||||
|
const sort = SortMode.TITLE_ASC
|
||||||
|
const type = NoteType.DOCUMENT
|
||||||
|
const result = createURLSearchParams(sort, null, type)
|
||||||
|
expect(result).toStrictEqual('sort=title_asc&type=document')
|
||||||
|
})
|
||||||
|
it('everything is defined', () => {
|
||||||
|
const sort = SortMode.TITLE_ASC
|
||||||
|
const search = 'test'
|
||||||
|
const type = NoteType.DOCUMENT
|
||||||
|
const result = createURLSearchParams(sort, search, type)
|
||||||
|
expect(result).toStrictEqual('sort=title_asc&search=test&type=document')
|
||||||
|
})
|
||||||
|
})
|
31
frontend/src/api/explore/utils.ts
Normal file
31
frontend/src/api/explore/utils.ts
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { SortMode } from '../../components/explore-page/explore-notes-section/filters/sort-button'
|
||||||
|
import type { NoteType } from '@hedgedoc/commons'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create the necessary url parameters for the api calls of the explore page.
|
||||||
|
* @param sort
|
||||||
|
* @param searchFilter
|
||||||
|
* @param typeFilter
|
||||||
|
* @return a string representation of the search parameter
|
||||||
|
*/
|
||||||
|
export const createURLSearchParams = (
|
||||||
|
sort: SortMode,
|
||||||
|
searchFilter: string | null,
|
||||||
|
typeFilter: NoteType | null
|
||||||
|
): string => {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
params.set('sort', sort)
|
||||||
|
if (searchFilter) {
|
||||||
|
params.set('search', searchFilter)
|
||||||
|
}
|
||||||
|
if (typeFilter) {
|
||||||
|
params.set('type', typeFilter)
|
||||||
|
}
|
||||||
|
return params.toString()
|
||||||
|
}
|
|
@ -4,75 +4,14 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import React, { Fragment, useCallback, useEffect, useMemo, useRef, useState } 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 { PinnedNoteCard } from './pinned-note-card'
|
||||||
import { Trans, useTranslation } from 'react-i18next'
|
import { Trans, useTranslation } from 'react-i18next'
|
||||||
import { NoteType } from '@hedgedoc/commons'
|
|
||||||
import { Caret } from './caret'
|
import { Caret } from './caret'
|
||||||
import styles from './pinned-notes.module.css'
|
import styles from './pinned-notes.module.css'
|
||||||
|
import type { NoteEntry } from '../../../api/explore/types'
|
||||||
const mockListPinnedNotes: NoteCardProps[] = [
|
import { useAsync } from 'react-use'
|
||||||
{
|
import { getPinnedNotes } from '../../../api/explore'
|
||||||
id: '1',
|
import { AsyncLoadingBoundary } from '../../common/async-loading-boundary/async-loading-boundary'
|
||||||
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 = () => {
|
export const PinnedNotes: React.FC = () => {
|
||||||
useTranslation()
|
useTranslation()
|
||||||
|
@ -80,9 +19,7 @@ export const PinnedNotes: React.FC = () => {
|
||||||
const [enableScrollLeft, setEnableScrollLeft] = useState(false)
|
const [enableScrollLeft, setEnableScrollLeft] = useState(false)
|
||||||
const [enableScrollRight, setEnableScrollRight] = useState(true)
|
const [enableScrollRight, setEnableScrollRight] = useState(true)
|
||||||
|
|
||||||
const pinnedNotes = useMemo(() => {
|
const { value: pinnedNotes, loading, error } = useAsync(getPinnedNotes, [])
|
||||||
return mockListPinnedNotes
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const leftClick = useCallback(() => {
|
const leftClick = useCallback(() => {
|
||||||
if (!scrollboxRef.current) {
|
if (!scrollboxRef.current) {
|
||||||
|
@ -103,6 +40,13 @@ export const PinnedNotes: React.FC = () => {
|
||||||
})
|
})
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
const pinnedNoteCards = useMemo(() => {
|
||||||
|
if (!pinnedNotes) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return pinnedNotes.map((note: NoteEntry) => <PinnedNoteCard key={note.primaryAddress} {...note} />)
|
||||||
|
}, [pinnedNotes])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!scrollboxRef.current) {
|
if (!scrollboxRef.current) {
|
||||||
return
|
return
|
||||||
|
@ -127,9 +71,9 @@ export const PinnedNotes: React.FC = () => {
|
||||||
<div className={'d-flex flex-row gap-2 align-items-center mb-4'}>
|
<div className={'d-flex flex-row gap-2 align-items-center mb-4'}>
|
||||||
<Caret active={enableScrollLeft} left={true} onClick={leftClick} />
|
<Caret active={enableScrollLeft} left={true} onClick={leftClick} />
|
||||||
<div className={styles.scrollbox} ref={scrollboxRef}>
|
<div className={styles.scrollbox} ref={scrollboxRef}>
|
||||||
{pinnedNotes.map((note: NoteCardProps) => (
|
<AsyncLoadingBoundary componentName={'PinnedNotes'} loading={loading} error={error}>
|
||||||
<PinnedNoteCard key={note.id} {...note} />
|
{pinnedNoteCards}
|
||||||
))}
|
</AsyncLoadingBoundary>
|
||||||
</div>
|
</div>
|
||||||
<Caret active={enableScrollRight} left={false} onClick={rightClick} />
|
<Caret active={enableScrollRight} left={false} onClick={rightClick} />
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue