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:
Erik Michelson 2025-02-19 23:06:03 +01:00
parent f645cffcd5
commit a60c7a6e10
No known key found for this signature in database
GPG key ID: DB99ADDDC5C0AF82
6 changed files with 238 additions and 71 deletions
frontend/src
api/explore
components/explore-page/pinned-notes

View 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()
}
/**
*
*/

View 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']
}
]

View 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
}

View 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')
})
})

View 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()
}

View file

@ -4,75 +4,14 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
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[] = [
{
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'
}
]
import type { NoteEntry } from '../../../api/explore/types'
import { useAsync } from 'react-use'
import { getPinnedNotes } from '../../../api/explore'
import { AsyncLoadingBoundary } from '../../common/async-loading-boundary/async-loading-boundary'
export const PinnedNotes: React.FC = () => {
useTranslation()
@ -80,9 +19,7 @@ export const PinnedNotes: React.FC = () => {
const [enableScrollLeft, setEnableScrollLeft] = useState(false)
const [enableScrollRight, setEnableScrollRight] = useState(true)
const pinnedNotes = useMemo(() => {
return mockListPinnedNotes
}, [])
const { value: pinnedNotes, loading, error } = useAsync(getPinnedNotes, [])
const leftClick = useCallback(() => {
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(() => {
if (!scrollboxRef.current) {
return
@ -127,9 +71,9 @@ export const PinnedNotes: React.FC = () => {
<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} />
))}
<AsyncLoadingBoundary componentName={'PinnedNotes'} loading={loading} error={error}>
{pinnedNoteCards}
</AsyncLoadingBoundary>
</div>
<Caret active={enableScrollRight} left={false} onClick={rightClick} />
</div>