diff --git a/frontend/locales/en.json b/frontend/locales/en.json index 1b40a936f..b3cdfebfd 100644 --- a/frontend/locales/en.json +++ b/frontend/locales/en.json @@ -3,13 +3,26 @@ "slogan": "Ideas grow better together", "icon": "HedgeDoc logo with text" }, - "notificationTest": { - "title": "Test", - "content": "It works!" - }, "navigation": { "newNote": "New Note" }, + "explore": { + "title": "Explore notes", + "welcome": { + "user": "Welcome, {{userName}}!", + "guest": "Welcome, guest!" + }, + "pinnedNotes": { + "title": "Pinned notes", + "unpin": "Click to unpin this note", + "empty": "You don't have any pinned notes yet." + }, + "modes": { + "my": "My notes", + "shared": "Shared with me", + "public": "Public notes" + } + }, "renderer": { "highlightCode": { "copyCode": "Copy code to clipboard" diff --git a/frontend/src/app/(editor)/@appBar/explore/my/page.tsx b/frontend/src/app/(editor)/@appBar/explore/my/page.tsx new file mode 100644 index 000000000..b17c65ee4 --- /dev/null +++ b/frontend/src/app/(editor)/@appBar/explore/my/page.tsx @@ -0,0 +1,18 @@ +'use client' +/* + * SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ +import { BaseAppBar } from '../../../../../components/layout/app-bar/base-app-bar' +import React from 'react' +import { Trans, useTranslation } from 'react-i18next' + +export default function AppBar() { + useTranslation() + return ( + + + + ) +} diff --git a/frontend/src/app/(editor)/@appBar/explore/public/page.tsx b/frontend/src/app/(editor)/@appBar/explore/public/page.tsx new file mode 100644 index 000000000..a6944bd81 --- /dev/null +++ b/frontend/src/app/(editor)/@appBar/explore/public/page.tsx @@ -0,0 +1,18 @@ +'use client' +/* + * SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ +import { BaseAppBar } from '../../../../../components/layout/app-bar/base-app-bar' +import React from 'react' +import { Trans, useTranslation } from 'react-i18next' + +export default function AppBar() { + useTranslation() + return ( + + + + ) +} diff --git a/frontend/src/app/(editor)/@appBar/explore/shared/page.tsx b/frontend/src/app/(editor)/@appBar/explore/shared/page.tsx new file mode 100644 index 000000000..8b4edb827 --- /dev/null +++ b/frontend/src/app/(editor)/@appBar/explore/shared/page.tsx @@ -0,0 +1,18 @@ +'use client' +/* + * SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ +import { BaseAppBar } from '../../../../../components/layout/app-bar/base-app-bar' +import React from 'react' +import { Trans, useTranslation } from 'react-i18next' + +export default function AppBar() { + useTranslation() + return ( + + + + ) +} diff --git a/frontend/src/app/(editor)/explore/layout.tsx b/frontend/src/app/(editor)/explore/layout.tsx new file mode 100644 index 000000000..d71fa6ee9 --- /dev/null +++ b/frontend/src/app/(editor)/explore/layout.tsx @@ -0,0 +1,31 @@ +'use client' + +/* + * SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ +import type { PropsWithChildren } from 'react' +import React from 'react' +import { Container } from 'react-bootstrap' +import { Welcome } from '../../../components/explore-page/welcome' +import { ModeSelection } from '../../../components/explore-page/mode-selection/mode-selection' +import { PinnedNotes } from '../../../components/explore-page/pinned-notes/pinned-notes' + +/** + * Layout for the login page with the intro content on the left and children on the right. + * @param children The content to show on the right + */ + +export type ExploreLayoutProps = PropsWithChildren + +export default function ExploreLayout({ children }: ExploreLayoutProps) { + return ( + + + + + {children} + + ) +} diff --git a/frontend/src/app/(editor)/explore/my/page.tsx b/frontend/src/app/(editor)/explore/my/page.tsx new file mode 100644 index 000000000..25702af1c --- /dev/null +++ b/frontend/src/app/(editor)/explore/my/page.tsx @@ -0,0 +1,14 @@ +/* + * SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import type { NextPage } from 'next' +import { LandingLayout } from '../../../../components/landing-layout/landing-layout' + +const ExploreMyNotesPage: NextPage = () => { + return Own Notes +} + +export default ExploreMyNotesPage diff --git a/frontend/src/app/(editor)/explore/public/page.tsx b/frontend/src/app/(editor)/explore/public/page.tsx new file mode 100644 index 000000000..06af805a2 --- /dev/null +++ b/frontend/src/app/(editor)/explore/public/page.tsx @@ -0,0 +1,14 @@ +/* + * SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import type { NextPage } from 'next' +import { LandingLayout } from '../../../../components/landing-layout/landing-layout' + +const ExplorePublicPage: NextPage = () => { + return Public Notes +} + +export default ExplorePublicPage diff --git a/frontend/src/app/(editor)/explore/shared/page.tsx b/frontend/src/app/(editor)/explore/shared/page.tsx new file mode 100644 index 000000000..996f91e18 --- /dev/null +++ b/frontend/src/app/(editor)/explore/shared/page.tsx @@ -0,0 +1,14 @@ +/* + * SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import type { NextPage } from 'next' +import { LandingLayout } from '../../../../components/landing-layout/landing-layout' + +const ExploreSharedPage: NextPage = () => { + return Shared Notes +} + +export default ExploreSharedPage diff --git a/frontend/src/app/(editor)/login/page.tsx b/frontend/src/app/(editor)/login/page.tsx index b06967984..dfd134d30 100644 --- a/frontend/src/app/(editor)/login/page.tsx +++ b/frontend/src/app/(editor)/login/page.tsx @@ -8,7 +8,7 @@ import type { NextPage } from 'next' import React from 'react' -import { RedirectToParamOrHistory } from '../../../components/login-page/redirect-to-param-or-history' +import { RedirectToParamOrExplore } from '../../../components/login-page/redirect-to-param-or-explore' import { LocalLoginCard } from '../../../components/login-page/local-login/local-login-card' import { LdapLoginCards } from '../../../components/login-page/ldap/ldap-login-cards' import { OneClickLoginCard } from '../../../components/login-page/one-click/one-click-login-card' @@ -20,7 +20,7 @@ const LoginPage: NextPage = () => { const userLoggedIn = useIsLoggedIn() if (userLoggedIn) { - return + return } return ( diff --git a/frontend/src/app/(editor)/new-user/page.tsx b/frontend/src/app/(editor)/new-user/page.tsx index 0042bc0ef..ee3d7ebfb 100644 --- a/frontend/src/app/(editor)/new-user/page.tsx +++ b/frontend/src/app/(editor)/new-user/page.tsx @@ -8,7 +8,7 @@ import type { NextPage } from 'next' import { useTranslation } from 'react-i18next' import React from 'react' import { useIsLoggedIn } from '../../../hooks/common/use-is-logged-in' -import { RedirectToParamOrHistory } from '../../../components/login-page/redirect-to-param-or-history' +import { RedirectToParamOrExplore } from '../../../components/login-page/redirect-to-param-or-explore' import { NewUserCard } from '../../../components/login-page/new-user/new-user-card' import { LoginLayout } from '../../../components/layout/login-layout' @@ -20,7 +20,7 @@ const NewUserPage: NextPage = () => { const userLoggedIn = useIsLoggedIn() if (userLoggedIn) { - return + return } return ( diff --git a/frontend/src/components/common/note-type-icon/note-type-icon.tsx b/frontend/src/components/common/note-type-icon/note-type-icon.tsx new file mode 100644 index 000000000..3cca3596c --- /dev/null +++ b/frontend/src/components/common/note-type-icon/note-type-icon.tsx @@ -0,0 +1,31 @@ +/* + * SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ +import React, { useMemo } from 'react' +import { + FileEarmarkSlidesFill as IconFileEarmarkSlidesFill, + FileEarmarkTextFill as IconFileEarmarkTextFill +} from 'react-bootstrap-icons' +import { NoteType } from '@hedgedoc/commons' +import { UiIcon } from '../icons/ui-icon' +import type { UiIconProps } from '../icons/ui-icon' + +interface NoteTypeIconProps extends Omit { + noteType: NoteType +} + +export const NoteTypeIcon: React.FC = ({ noteType, ...props }) => { + const icon = useMemo(() => { + switch (noteType) { + case NoteType.DOCUMENT: + default: + return IconFileEarmarkTextFill + case NoteType.SLIDE: + return IconFileEarmarkSlidesFill + } + }, [noteType]) + + return +} diff --git a/frontend/src/components/explore-page/mode-selection/mode-link.module.css b/frontend/src/components/explore-page/mode-selection/mode-link.module.css new file mode 100644 index 000000000..3c9d7bdab --- /dev/null +++ b/frontend/src/components/explore-page/mode-selection/mode-link.module.css @@ -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; +} diff --git a/frontend/src/components/explore-page/mode-selection/mode-link.tsx b/frontend/src/components/explore-page/mode-selection/mode-link.tsx new file mode 100644 index 000000000..a14dd21f3 --- /dev/null +++ b/frontend/src/components/explore-page/mode-selection/mode-link.tsx @@ -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 = ({ mode }) => { + useTranslation() + const path = usePathname() + const isActive = useMemo(() => path === `/explore/${mode}`, [path, mode]) + + return isActive ? ( + + + + ) : ( + + + + ) +} diff --git a/frontend/src/components/explore-page/mode-selection/mode-selection.tsx b/frontend/src/components/explore-page/mode-selection/mode-selection.tsx new file mode 100644 index 000000000..6de7f6eb5 --- /dev/null +++ b/frontend/src/components/explore-page/mode-selection/mode-selection.tsx @@ -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 ( +

+ + {userLoggedIn && } + +

+ ) +} diff --git a/frontend/src/components/explore-page/mode-selection/mode.ts b/frontend/src/components/explore-page/mode-selection/mode.ts new file mode 100644 index 000000000..954b94308 --- /dev/null +++ b/frontend/src/components/explore-page/mode-selection/mode.ts @@ -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' +} diff --git a/frontend/src/components/explore-page/pinned-notes/pinned-note-card.module.css b/frontend/src/components/explore-page/pinned-notes/pinned-note-card.module.css new file mode 100644 index 000000000..49f91b968 --- /dev/null +++ b/frontend/src/components/explore-page/pinned-notes/pinned-note-card.module.css @@ -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; +} diff --git a/frontend/src/components/explore-page/pinned-notes/pinned-note-card.tsx b/frontend/src/components/explore-page/pinned-notes/pinned-note-card.tsx new file mode 100644 index 000000000..8b01b699e --- /dev/null +++ b/frontend/src/components/explore-page/pinned-notes/pinned-note-card.tsx @@ -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 = ({ 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) => { + event.preventDefault() + alert(`UnFav ${id}`) + }, + [id] + ) + + const onClickTag = useCallback( + (tag: string) => { + return (event: MouseEvent) => { + event.preventDefault() + router.push(`?search=tag:${tag}`) + } + }, + [router] + ) + + const tagsChips = useMemo(() => { + return tags.map((tag) => ( + + {tag} + + )) + }, [tags, onClickTag, labelTag]) + + return ( +
  • + + +
    + +
    +
    + + + + {title} + + + {lastVisitedString} + {tagsChips} + + +
  • + ) +} diff --git a/frontend/src/components/explore-page/pinned-notes/pinned-notes.tsx b/frontend/src/components/explore-page/pinned-notes/pinned-notes.tsx new file mode 100644 index 000000000..e1750c1bb --- /dev/null +++ b/frontend/src/components/explore-page/pinned-notes/pinned-notes.tsx @@ -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) => ) + }, []) + + return ( + +

    + +

    +
      {cards}
    +
    + ) +} diff --git a/frontend/src/components/explore-page/welcome.tsx b/frontend/src/components/explore-page/welcome.tsx new file mode 100644 index 000000000..5f262257f --- /dev/null +++ b/frontend/src/components/explore-page/welcome.tsx @@ -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 ( +

    + {userName !== undefined ? ( + + ) : ( + + )} +

    + ) +} diff --git a/frontend/src/components/layout/app-bar/app-bar-elements/branding-element.tsx b/frontend/src/components/layout/app-bar/app-bar-elements/branding-element.tsx index f4743ffec..ed059d9ad 100644 --- a/frontend/src/components/layout/app-bar/app-bar-elements/branding-element.tsx +++ b/frontend/src/components/layout/app-bar/app-bar-elements/branding-element.tsx @@ -20,7 +20,7 @@ export const BrandingElement: React.FC = () => { return ( - + { +export const ExploreButton: React.FC = () => { useTranslation() return ( - + ) diff --git a/frontend/src/components/login-page/guest/guest-card.tsx b/frontend/src/components/login-page/guest/guest-card.tsx index 2ca30d3bc..de88f220b 100644 --- a/frontend/src/components/login-page/guest/guest-card.tsx +++ b/frontend/src/components/login-page/guest/guest-card.tsx @@ -7,7 +7,7 @@ import React from 'react' import { Card } from 'react-bootstrap' import { NewNoteButton } from '../../common/new-note-button/new-note-button' -import { HistoryButton } from '../../layout/app-bar/app-bar-elements/help-dropdown/history-button' +import { ExploreButton } from './explore-button' import { useFrontendConfig } from '../../common/frontend-config-context/use-frontend-config' import { Trans, useTranslation } from 'react-i18next' import { GuestAccessLevel } from '../../../api/config/types' @@ -32,7 +32,7 @@ export const GuestCard: React.FC = () => {
    - +
    {guestAccessLevel !== GuestAccessLevel.CREATE && (
    diff --git a/frontend/src/components/login-page/redirect-to-param-or-history.tsx b/frontend/src/components/login-page/redirect-to-param-or-explore.tsx similarity index 91% rename from frontend/src/components/login-page/redirect-to-param-or-history.tsx rename to frontend/src/components/login-page/redirect-to-param-or-explore.tsx index 74a8527f6..805e65473 100644 --- a/frontend/src/components/login-page/redirect-to-param-or-history.tsx +++ b/frontend/src/components/login-page/redirect-to-param-or-explore.tsx @@ -12,7 +12,7 @@ import { useGetPostLoginRedirectUrl } from './utils/use-get-post-login-redirect- * Redirects the browser to the relative URL that is provided via "redirectBackTo" URL parameter. * If no parameter has been provided or if the URL is not relative, then "/history" will be used. */ -export const RedirectToParamOrHistory: React.FC = () => { +export const RedirectToParamOrExplore: React.FC = () => { const redirectUrl = useGetPostLoginRedirectUrl() return } diff --git a/frontend/src/components/login-page/utils/use-get-post-login-redirect-url.ts b/frontend/src/components/login-page/utils/use-get-post-login-redirect-url.ts index 3a766edf1..9d5cd8e79 100644 --- a/frontend/src/components/login-page/utils/use-get-post-login-redirect-url.ts +++ b/frontend/src/components/login-page/utils/use-get-post-login-redirect-url.ts @@ -5,7 +5,7 @@ */ import { useSingleStringUrlParameter } from '../../../hooks/common/use-single-string-url-parameter' -const defaultFallback = '/history' +const defaultFallback = '/explore/my' /** * Returns the URL that the user should be redirected to after logging in.