Share - WIP

This commit is contained in:
Philipinho
2025-04-09 13:26:50 +01:00
parent a9f370660b
commit 18e8c4cbaf
19 changed files with 514 additions and 6 deletions

View File

@ -26,6 +26,7 @@ import { useTranslation } from "react-i18next";
import Security from "@/ee/security/pages/security.tsx";
import License from "@/ee/licence/pages/license.tsx";
import { useRedirectToCloudSelect } from "@/ee/hooks/use-redirect-to-cloud-select.tsx";
import SharedPage from '@/pages/share/shared-page.tsx';
export default function App() {
const { t } = useTranslation();
@ -51,6 +52,8 @@ export default function App() {
</>
)}
<Route path={"/share/:shareId/:pageId"} element={<SharedPage />} />
<Route path={"/p/:pageSlug"} element={<PageRedirect />} />
<Route element={<Layout />}>

View File

@ -139,7 +139,7 @@ export default function DrawioView(props: NodeViewProps) {
)}
/>
{selected && (
{selected && editor.isEditable && (
<ActionIcon
onClick={handleOpen}
variant="default"

View File

@ -170,7 +170,7 @@ export default function ExcalidrawView(props: NodeViewProps) {
)}
/>
{selected && (
{selected && editor.isEditable && (
<ActionIcon
onClick={handleOpen}
variant="default"

View File

@ -0,0 +1,52 @@
import "@/features/editor/styles/index.css";
import React, { useMemo } from "react";
import { EditorProvider } from "@tiptap/react";
import { mainExtensions } from "@/features/editor/extensions/extensions";
import { Document } from "@tiptap/extension-document";
import { Heading } from "@tiptap/extension-heading";
import { Text } from "@tiptap/extension-text";
import { Placeholder } from "@tiptap/extension-placeholder";
interface PageEditorProps {
title: string;
content: any;
}
export default function ReadonlyPageEditor({
title,
content,
}: PageEditorProps) {
const extensions = useMemo(() => {
return [...mainExtensions];
}, []);
const titleExtensions = [
Document.extend({
content: "heading",
}),
Heading,
Text,
Placeholder.configure({
placeholder: "Untitled",
showOnlyWhenEditable: false,
}),
];
return (
<>
<EditorProvider
editable={false}
immediatelyRender={true}
extensions={titleExtensions}
content={title}
></EditorProvider>
<EditorProvider
editable={false}
immediatelyRender={true}
extensions={extensions}
content={content}
></EditorProvider>
</>
);
}

View File

@ -1,6 +1,6 @@
import slugify from "@sindresorhus/slugify";
const buildPageSlug = (pageSlugId: string, pageTitle?: string): string => {
export const buildPageSlug = (pageSlugId: string, pageTitle?: string): string => {
const titleSlug = slugify(pageTitle?.substring(0, 70) || "untitled", {
customReplacements: [
["♥", ""],
@ -8,7 +8,7 @@ const buildPageSlug = (pageSlugId: string, pageTitle?: string): string => {
],
});
return `p/${titleSlug}-${pageSlugId}`;
return `${titleSlug}-${pageSlugId}`;
};
export const buildPageUrl = (
@ -17,7 +17,7 @@ export const buildPageUrl = (
pageTitle?: string,
): string => {
if (spaceName === undefined) {
return `/${buildPageSlug(pageSlugId, pageTitle)}`;
return `/p/${buildPageSlug(pageSlugId, pageTitle)}`;
}
return `/s/${spaceName}/${buildPageSlug(pageSlugId, pageTitle)}`;
return `/s/${spaceName}/p/${buildPageSlug(pageSlugId, pageTitle)}`;
};

View File

@ -0,0 +1,65 @@
import {
useMutation,
useQuery,
UseQueryResult,
} from "@tanstack/react-query";
import { notifications } from "@mantine/notifications";
import { validate as isValidUuid } from "uuid";
import { useTranslation } from "react-i18next";
import {
ICreateShare,
IShareInput,
} from "@/features/share/types/share.types.ts";
import {
createShare,
deleteShare,
getShare,
updateShare,
} from "@/features/share/services/share-service.ts";
import { IPage } from "@/features/page/types/page.types.ts";
export function useShareQuery(
shareInput: Partial<IShareInput>,
): UseQueryResult<IPage, Error> {
const query = useQuery({
queryKey: ["shares", shareInput],
queryFn: () => getShare(shareInput),
enabled: !!shareInput.shareId,
staleTime: 5 * 60 * 1000,
});
return query;
}
export function useCreateShareMutation() {
const { t } = useTranslation();
return useMutation<any, Error, ICreateShare>({
mutationFn: (data) => createShare(data),
onSuccess: (data) => {},
onError: (error) => {
notifications.show({ message: t("Failed to share page"), color: "red" });
},
});
}
export function useUpdateShareMutation() {
return useMutation<any, Error, Partial<IShareInput>>({
mutationFn: (data) => updateShare(data),
});
}
export function useDeleteShareMutation() {
const { t } = useTranslation();
return useMutation({
mutationFn: (shareId: string) => deleteShare(shareId),
onSuccess: () => {
notifications.show({ message: t("Share deleted successfully") });
},
onError: (error) => {
notifications.show({
message: t("Failed to delete share"),
color: "red",
});
},
});
}

View File

@ -0,0 +1,36 @@
import api from "@/lib/api-client";
import {
IExportPageParams,
IMovePage,
IMovePageToSpace,
IPage,
IPageInput,
SidebarPagesParams,
} from "@/features/page/types/page.types";
import { IAttachment, IPagination } from "@/lib/types.ts";
import { saveAs } from "file-saver";
import {
ICreateShare,
IShareInput,
} from "@/features/share/types/share.types.ts";
export async function createShare(data: ICreateShare): Promise<any> {
const req = await api.post<any>("/shares/create", data);
return req.data;
}
export async function getShare(
shareInput: Partial<IShareInput>,
): Promise<IPage> {
const req = await api.post<IPage>("/shares/info", shareInput);
return req.data;
}
export async function updateShare(data: Partial<IShareInput>): Promise<any> {
const req = await api.post<IPage>("/shares/update", data);
return req.data;
}
export async function deleteShare(shareId: string): Promise<void> {
await api.post("/shares/delete", { shareId });
}

View File

@ -0,0 +1,12 @@
export interface ICreateShare {
slugId: string;
pageId: string;
}
export interface IShareInput {
shareId: string;
pageId?: string;
}

View File

@ -0,0 +1,56 @@
import { useNavigate, useParams } from "react-router-dom";
import { Helmet } from "react-helmet-async";
import { useTranslation } from "react-i18next";
import { useShareQuery } from "@/features/share/queries/share-query.ts";
import { Container } from "@mantine/core";
import React, { useEffect } from "react";
import ReadonlyPageEditor from "@/features/editor/readonly-page-editor.tsx";
import { buildPageSlug } from "@/features/page/page.utils.ts";
export default function SharedPage() {
const { t } = useTranslation();
const { shareId } = useParams();
const {
data: page,
isLoading,
isError,
error,
} = useShareQuery({ shareId: shareId });
const navigate = useNavigate();
useEffect(() => {
if (!page) return;
const pageSlug = buildPageSlug(page.slugId, page.title);
const shareSlug = `/share/${shareId}/${pageSlug}`;
navigate(shareSlug, { replace: true });
}, [page]);
if (isLoading) {
return <></>;
}
if (isError || !page) {
if ([401, 403, 404].includes(error?.["status"])) {
return <div>{t("Page not found")}</div>;
}
return <div>{t("Error fetching page data.")}</div>;
}
return (
page && (
<div>
<Helmet>
<title>{`${page?.icon || ""} ${page?.title || t("untitled")}`}</title>
</Helmet>
<Container size={900} pt={50}>
<ReadonlyPageEditor
key={page.id}
title={page.title}
content={page.content}
/>
</Container>
</div>
)
);
}