first commit
Some checks failed
Build / run (push) Has been cancelled

This commit is contained in:
maher
2025-10-29 11:42:25 +01:00
commit 703f50a09d
4595 changed files with 385164 additions and 0 deletions

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,58 @@
import React, {Suspense} from 'react';
import {FormProvider, useForm} from 'react-hook-form';
import {FileUploadProvider} from '@common/uploads/uploader/file-upload-provider';
import {ArticleEditorTitle} from '@common/article-editor/article-editor-title';
import {ArticleEditorStickyHeader} from '@common/article-editor/article-editor-sticky-header';
import {useNavigate} from '@common/utils/hooks/use-navigate';
import {
CreateCustomPagePayload,
useCreateCustomPage,
} from '@common/admin/custom-pages/requests/use-create-custom-page';
import {FullPageLoader} from '@common/ui/progress/full-page-loader';
const ArticleBodyEditor = React.lazy(
() => import('@common/article-editor/article-body-editor'),
);
export function CreateCustomPage() {
const navigate = useNavigate();
const createPage = useCreateCustomPage();
const form = useForm<CreateCustomPagePayload>();
const handleSave = (editorContent: string) => {
createPage.mutate(
{
...form.getValues(),
body: editorContent,
},
{
onSuccess: () => navigate('../', {relative: 'path'}),
},
);
};
return (
<Suspense fallback={<FullPageLoader />}>
<ArticleBodyEditor>
{(content, editor) => (
<FileUploadProvider>
<FormProvider {...form}>
<ArticleEditorStickyHeader
editor={editor}
isLoading={createPage.isPending}
onSave={handleSave}
backLink="../"
/>
<div className="mx-20">
<div className="prose dark:prose-invert mx-auto flex-auto">
<ArticleEditorTitle />
{content}
</div>
</div>
</FormProvider>
</FileUploadProvider>
)}
</ArticleBodyEditor>
</Suspense>
);
}

View File

@@ -0,0 +1,45 @@
import React, {useContext, useMemo} from 'react';
import {Link} from 'react-router-dom';
import {DataTablePage} from '../../datatable/page/data-table-page';
import {Trans} from '../../i18n/trans';
import {DataTableEmptyStateMessage} from '../../datatable/page/data-table-emty-state-message';
import articlesSvg from './articles.svg';
import {DataTableAddItemButton} from '../../datatable/data-table-add-item-button';
import {CustomPageDatatableFilters} from './custom-page-datatable-filters';
import {DeleteSelectedItemsAction} from '../../datatable/page/delete-selected-items-action';
import {CustomPageDatatableColumns} from '@common/admin/custom-pages/custom-page-datatable-columns';
import {SiteConfigContext} from '@common/core/settings/site-config-context';
export function CustomPageDatablePage() {
const config = useContext(SiteConfigContext);
const filters = useMemo(() => {
return CustomPageDatatableFilters(config);
}, [config]);
return (
<DataTablePage
endpoint="custom-pages"
title={<Trans message="Custom pages" />}
filters={filters}
columns={CustomPageDatatableColumns}
queryParams={{with: 'user'}}
actions={<Actions />}
selectedActions={<DeleteSelectedItemsAction />}
emptyStateMessage={
<DataTableEmptyStateMessage
image={articlesSvg}
title={<Trans message="No pages have been created yet" />}
filteringTitle={<Trans message="No matching pages" />}
/>
}
/>
);
}
function Actions() {
return (
<DataTableAddItemButton elementType={Link} to="new">
<Trans message="New page" />
</DataTableAddItemButton>
);
}

View File

@@ -0,0 +1,70 @@
import {ColumnConfig} from '@common/datatable/column-config';
import {CustomPage} from '@common/admin/custom-pages/custom-page';
import {Trans} from '@common/i18n/trans';
import {Link} from 'react-router-dom';
import {LinkStyle} from '@common/ui/buttons/external-link';
import {NameWithAvatar} from '@common/datatable/column-templates/name-with-avatar';
import {FormattedDate} from '@common/i18n/formatted-date';
import React from 'react';
import {IconButton} from '@common/ui/buttons/icon-button';
import {EditIcon} from '@common/icons/material/Edit';
export const CustomPageDatatableColumns: ColumnConfig<CustomPage>[] = [
{
key: 'slug',
allowsSorting: true,
width: 'flex-2 min-w-200',
visibleInMode: 'all',
header: () => <Trans message="Slug" />,
body: page => (
<Link target="_blank" to={`/pages/${page.slug}`} className={LinkStyle}>
{page.slug}
</Link>
),
},
{
key: 'user_id',
allowsSorting: true,
width: 'flex-2 min-w-140',
header: () => <Trans message="Owner" />,
body: page =>
page.user && (
<NameWithAvatar
image={page.user.avatar}
label={page.user.display_name}
description={page.user.email}
/>
),
},
{
key: 'type',
maxWidth: 'max-w-100',
header: () => <Trans message="Type" />,
body: page => <Trans message={page.type} />,
},
{
key: 'updated_at',
allowsSorting: true,
width: 'w-100',
header: () => <Trans message="Last updated" />,
body: page => <FormattedDate date={page.updated_at} />,
},
{
key: 'actions',
header: () => <Trans message="Actions" />,
hideHeader: true,
align: 'end',
width: 'w-84 flex-shrink-0',
visibleInMode: 'all',
body: page => (
<IconButton
size="md"
className="text-muted"
elementType={Link}
to={`${page.id}/edit`}
>
<EditIcon />
</IconButton>
),
},
];

View File

@@ -0,0 +1,58 @@
import {
BackendFilter,
FilterControlType,
FilterOperator,
} from '../../datatable/filters/backend-filter';
import {message} from '../../i18n/message';
import {USER_MODEL} from '../../auth/user';
import {SiteConfigContextValue} from '@common/core/settings/site-config-context';
import {
createdAtFilter,
updatedAtFilter,
} from '@common/datatable/filters/timestamp-filters';
export const CustomPageDatatableFilters = (
config: SiteConfigContextValue
): BackendFilter[] => {
const dynamicFilters: BackendFilter[] =
config.customPages.types.length > 1
? [
{
control: {
type: FilterControlType.Select,
defaultValue: 'default',
options: config.customPages.types.map(type => ({
value: type.type,
label: type.label,
key: type.type,
})),
},
key: 'type',
label: message('Type'),
description: message('Type of the page'),
defaultOperator: FilterOperator.eq,
},
]
: [];
return [
{
key: 'user_id',
label: message('User'),
description: message('User page was created by'),
defaultOperator: FilterOperator.eq,
control: {
type: FilterControlType.SelectModel,
model: USER_MODEL,
},
},
...dynamicFilters,
createdAtFilter({
description: message('Date page was created'),
}),
updatedAtFilter({
description: message('Date page was last updated'),
}),
];
};

View File

@@ -0,0 +1,16 @@
import {User} from '../../auth/user';
export interface CustomPage {
id: number;
title?: string;
body: string;
slug: string;
type: string;
user?: User;
user_id?: number;
hide_nav: boolean;
created_at?: string;
updated_at?: string;
meta?: Record<string, unknown>;
model_type: 'customPage';
}

View File

@@ -0,0 +1,84 @@
import {useCustomPage} from '@common/custom-page/use-custom-page';
import React, {Fragment, Suspense} from 'react';
import {PageMetaTags} from '@common/http/page-meta-tags';
import {PageStatus} from '@common/http/page-status';
import {CustomPage} from '@common/admin/custom-pages/custom-page';
import {FormProvider, useForm} from 'react-hook-form';
import {useUpdateCustomPage} from '@common/admin/custom-pages/requests/use-update-custom-page';
import {FileUploadProvider} from '@common/uploads/uploader/file-upload-provider';
import {ArticleEditorTitle} from '@common/article-editor/article-editor-title';
import {ArticleEditorStickyHeader} from '@common/article-editor/article-editor-sticky-header';
import {useNavigate} from '@common/utils/hooks/use-navigate';
import {CreateCustomPagePayload} from '@common/admin/custom-pages/requests/use-create-custom-page';
import {FullPageLoader} from '@common/ui/progress/full-page-loader';
const ArticleBodyEditor = React.lazy(
() => import('@common/article-editor/article-body-editor'),
);
export function EditCustomPage() {
const query = useCustomPage();
return query.data ? (
<Fragment>
<PageMetaTags query={query} />
<PageContent page={query.data.page} />
</Fragment>
) : (
<div className="relative w-full h-full">
<PageStatus query={query} />
</div>
);
}
interface PageContentProps {
page: CustomPage;
}
function PageContent({page}: PageContentProps) {
const navigate = useNavigate();
const crupdatePage = useUpdateCustomPage();
const form = useForm<CreateCustomPagePayload>({
defaultValues: {
title: page.title,
slug: page.slug,
body: page.body,
},
});
const handleSave = (editorContent: string) => {
crupdatePage.mutate(
{
...form.getValues(),
body: editorContent,
},
{
onSuccess: () => navigate('../..', {relative: 'path'}),
},
);
};
return (
<Suspense fallback={<FullPageLoader />}>
<ArticleBodyEditor initialContent={page.body}>
{(content, editor) => (
<FileUploadProvider>
<FormProvider {...form}>
<ArticleEditorStickyHeader
editor={editor}
backLink="../.."
isLoading={crupdatePage.isPending}
onSave={handleSave}
/>
<div className="mx-20">
<div className="prose dark:prose-invert mx-auto flex-auto">
<ArticleEditorTitle />
{content}
</div>
</div>
</FormProvider>
</FileUploadProvider>
)}
</ArticleBodyEditor>
</Suspense>
);
}

View File

@@ -0,0 +1,42 @@
import {useMutation} from '@tanstack/react-query';
import {BackendResponse} from '@common/http/backend-response/backend-response';
import {CustomPage} from '../custom-page';
import {apiClient, queryClient} from '@common/http/query-client';
import {showHttpErrorToast} from '@common/utils/http/show-http-error-toast';
import {DatatableDataQueryKey} from '@common/datatable/requests/paginated-resources';
import {toast} from '@common/ui/toast/toast';
import {message} from '@common/i18n/message';
interface Response extends BackendResponse {
page: CustomPage;
}
export interface CreateCustomPagePayload {
title?: string;
body?: string;
slug?: string;
hide_nav?: boolean;
}
export function useCreateCustomPage(endpoint?: string) {
const finalEndpoint = endpoint || 'custom-pages';
return useMutation({
mutationFn: (payload: CreateCustomPagePayload) =>
createPage(payload, finalEndpoint),
onError: err => showHttpErrorToast(err),
onSuccess: async () => {
await queryClient.invalidateQueries({queryKey: ['custom-pages']});
await queryClient.invalidateQueries({
queryKey: DatatableDataQueryKey(finalEndpoint),
});
toast(message('Page created'));
},
});
}
function createPage(
payload: CreateCustomPagePayload,
endpoint: string,
): Promise<Response> {
return apiClient.post(`${endpoint}`, payload).then(r => r.data);
}

View File

@@ -0,0 +1,38 @@
import {useMutation} from '@tanstack/react-query';
import {BackendResponse} from '@common/http/backend-response/backend-response';
import {CustomPage} from '../custom-page';
import {apiClient, queryClient} from '@common/http/query-client';
import {showHttpErrorToast} from '@common/utils/http/show-http-error-toast';
import {DatatableDataQueryKey} from '@common/datatable/requests/paginated-resources';
import {toast} from '@common/ui/toast/toast';
import {message} from '@common/i18n/message';
import {useParams} from 'react-router-dom';
import {CreateCustomPagePayload} from '@common/admin/custom-pages/requests/use-create-custom-page';
interface Response extends BackendResponse {
page: CustomPage;
}
export function useUpdateCustomPage(endpoint?: string) {
const {pageId} = useParams();
const finalEndpoint = `${endpoint || 'custom-pages'}/${pageId}`;
return useMutation({
mutationFn: (payload: CreateCustomPagePayload) =>
updatePage(payload, finalEndpoint),
onError: err => showHttpErrorToast(err),
onSuccess: async () => {
await queryClient.invalidateQueries({queryKey: ['custom-pages']});
await queryClient.invalidateQueries({
queryKey: DatatableDataQueryKey(finalEndpoint),
});
toast(message('Page updated'));
},
});
}
function updatePage(
payload: CreateCustomPagePayload,
endpoint: string,
): Promise<Response> {
return apiClient.put(`${endpoint}`, payload).then(r => r.data);
}