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

View File

@@ -0,0 +1,17 @@
import {BootstrapData} from './bootstrap-data';
import {createContext, useContext} from 'react';
export interface BoostrapDataContextValue<T = BootstrapData> {
data: T;
setBootstrapData: (data: string | T) => void;
mergeBootstrapData: (data: Partial<T>) => void;
invalidateBootstrapData: () => void;
}
export const BoostrapDataContext = createContext<BoostrapDataContextValue>(
null!
);
export function useBootstrapData() {
return useContext(BoostrapDataContext);
}

View File

@@ -0,0 +1,33 @@
import React, {useMemo} from 'react';
import {
invalidateBootstrapData,
mergeBootstrapData,
setBootstrapData,
useBackendBootstrapData,
} from './use-backend-bootstrap-data';
import {
BoostrapDataContext,
BoostrapDataContextValue,
} from './bootstrap-data-context';
interface BootstrapDataProviderProps {
children: any;
}
export function BootstrapDataProvider({children}: BootstrapDataProviderProps) {
const {data} = useBackendBootstrapData();
const value: BoostrapDataContextValue = useMemo(() => {
return {
data: data,
setBootstrapData: setBootstrapData,
mergeBootstrapData: mergeBootstrapData,
invalidateBootstrapData: invalidateBootstrapData,
};
}, [data]);
return (
<BoostrapDataContext.Provider value={value}>
{children}
</BoostrapDataContext.Provider>
);
}

View File

@@ -0,0 +1,20 @@
import {CssTheme} from '../../ui/themes/css-theme';
import {Settings} from '../settings/settings';
import {User} from '../../auth/user';
import {Role} from '../../auth/role';
import {Localization} from '../../i18n/localization';
import {MetaTag} from '../../seo/meta-tag';
export interface BootstrapData {
themes: {all: CssTheme[]; selectedThemeId?: number | string | null};
sentry_release: string;
is_mobile_device: boolean;
csrf_token: string;
settings: Settings;
user: User | null;
guest_role: Role | null;
i18n: Localization;
default_meta_tags: MetaTag[];
show_cookie_notice: boolean;
rendered_ssr?: boolean;
}

View File

@@ -0,0 +1,57 @@
import {apiClient, queryClient} from '../../http/query-client';
import {BootstrapData} from './bootstrap-data';
import {keepPreviousData, useQuery} from '@tanstack/react-query';
const queryKey = ['bootstrapData'];
export function getBootstrapData(): BootstrapData {
return queryClient.getQueryData(queryKey)!;
}
export function invalidateBootstrapData() {
queryClient.invalidateQueries({queryKey});
}
export function setBootstrapData(data: string | BootstrapData) {
queryClient.setQueryData<BootstrapData>(
queryKey,
typeof data === 'string' ? decodeBootstrapData(data) : data,
);
}
export function mergeBootstrapData(partialData: Partial<BootstrapData>) {
setBootstrapData({
...getBootstrapData(),
...partialData,
});
}
// set bootstrap data that was provided with initial request from backend
const initialBootstrapData = (
typeof window !== 'undefined' && window.bootstrapData
? decodeBootstrapData(window.bootstrapData)
: undefined
) as BootstrapData;
// make sure initial data is available right away when accessing it via "getBootstrapData()"
queryClient.setQueryData(queryKey, initialBootstrapData);
export function useBackendBootstrapData() {
return useQuery({
queryKey: queryKey,
queryFn: () => fetchBootstrapData(),
staleTime: Infinity,
placeholderData: keepPreviousData,
initialData: initialBootstrapData,
});
}
const fetchBootstrapData = async (): Promise<BootstrapData> => {
return apiClient.get('bootstrap-data').then(response => {
return decodeBootstrapData(response.data.data);
});
};
function decodeBootstrapData(data: string | BootstrapData): BootstrapData {
return typeof data === 'string' ? JSON.parse(data) : data;
}

View File

@@ -0,0 +1,32 @@
import React, {StrictMode} from 'react';
import {QueryClientProvider} from '@tanstack/react-query';
import {domAnimation, LazyMotion} from 'framer-motion';
import {queryClient} from '../http/query-client';
import {SiteConfigContext} from './settings/site-config-context';
import {SiteConfig} from '@app/site-config';
import deepMerge from 'deepmerge';
import {BaseSiteConfig} from './settings/base-site-config';
import {ThemeProvider} from './theme-provider';
import {BootstrapDataProvider} from './bootstrap-data/bootstrap-data-provider';
interface ProvidersProps {
children: any;
}
const mergedConfig = deepMerge(BaseSiteConfig, SiteConfig);
export function CommonProvider({children}: ProvidersProps) {
return (
<StrictMode>
<QueryClientProvider client={queryClient}>
<LazyMotion features={domAnimation}>
<SiteConfigContext.Provider value={mergedConfig}>
<BootstrapDataProvider>
<ThemeProvider>{children}</ThemeProvider>
</BootstrapDataProvider>
</SiteConfigContext.Provider>
</LazyMotion>
</QueryClientProvider>
</StrictMode>
);
}

View File

@@ -0,0 +1,14 @@
export let rootEl = (
typeof document !== 'undefined'
? document.getElementById('root') ?? document.body
: undefined
) as HTMLElement;
export let themeEl = (
typeof document !== 'undefined' ? document.documentElement : undefined
) as HTMLElement;
export function setRootEl(el: HTMLElement) {
rootEl = el;
themeEl = el;
}

View File

@@ -0,0 +1,36 @@
import {SiteConfigContextValue} from './site-config-context';
import {WorkspaceInviteNotificationRenderer} from '../../workspace/notifications/workspace-invite-notification-renderer';
import {message} from '../../i18n/message';
const workspaceInviteNotif =
'Common\\Workspaces\\Notifications\\WorkspaceInvitation';
export const BaseSiteConfig: SiteConfigContextValue = {
auth: {
redirectUri: '/',
adminRedirectUri: '/admin',
},
tags: {
types: [{name: 'custom'}],
},
customPages: {
types: [{type: 'default', label: message('Default')}],
},
notifications: {
renderMap: {
[workspaceInviteNotif]: WorkspaceInviteNotificationRenderer,
},
},
admin: {
ads: [],
},
demo: {
loginPageDefaults: 'singleAccount',
},
homepage: {
options: [
{label: message('Login page'), value: 'loginPage'},
{label: message('Registration page'), value: 'registerPage'},
],
},
};

View File

@@ -0,0 +1,146 @@
import {IconTree} from '../../icons/create-svg-icon';
export type RecaptchaAction = 'contact' | 'register' | 'link_creation';
export interface Settings {
version: string;
branding: {
logo_light: string;
logo_dark: string;
logo_light_mobile: string;
logo_dark_mobile: string;
site_name: string;
site_description: string;
favicon: string;
};
menus: MenuConfig[];
base_url: string;
asset_url?: string;
html_base_uri: string;
cookie_notice: {
enable: boolean;
position: 'top' | 'bottom';
button?: MenuItemConfig;
};
logging: {
sentry_public?: string;
};
themes?: {
default_id?: number | string | null;
user_change: boolean;
};
custom_domains?: {
default_host?: string;
allow_select?: boolean;
allow_all_option?: boolean;
};
dates: {
format: string;
default_timezone: string;
};
i18n: {
enable: boolean;
default_localization: string;
};
api?: {
integrated: boolean;
};
billing: {
integrated: boolean;
enable: boolean;
accepted_cards?: string | string[];
paypal_test_mode: boolean;
stripe_public_key?: string;
invoice: {
address?: string;
notes?: string;
};
paypal: {
public_key: string;
enable: boolean;
};
stripe: {
enable: boolean;
};
};
notifications: {
integrated: boolean;
};
notif: {
subs: {
integrated: boolean;
};
};
site: {
hide_docs_button: boolean;
has_mobile_app: boolean;
demo: boolean;
};
registration: {
disable: boolean;
policies?: MenuItemConfig[];
};
social: {
envato: {
enable: boolean;
};
google: {
enable: boolean;
};
twitter: {
enable: boolean;
};
facebook: {
enable: boolean;
};
compact_buttons: boolean;
};
workspaces: {
integrated: boolean;
};
uploads: {
chunk_size: number;
max_size: number;
available_space: number;
allowed_extensions?: string[];
blocked_extensions?: string[];
public_driver: string;
uploads_driver: string;
s3_direct_upload: boolean;
disable_tus: boolean;
};
require_email_confirmation: boolean;
single_device_login: boolean;
mail: {
contact_page_address: string;
handler: string;
};
recaptcha?: {
enable?: Record<RecaptchaAction, boolean>;
site_key: string;
};
analytics?: {
tracking_code?: string;
gchart_api_key?: string;
};
}
export interface MenuConfig {
id: string;
name: string;
positions: string[];
items: MenuItemConfig[];
}
export interface MenuItemConfig {
id: string;
type: 'route' | 'link';
order: number;
label: string;
action: string;
target?: '_blank' | '_self';
roles?: number[];
permissions?: string[];
settings?: Record<string, any>;
icon?: IconTree[] | null;
}

View File

@@ -0,0 +1,72 @@
import React, {ComponentType} from 'react';
import type {NotificationListItemProps} from '../../notifications/notification-list';
import {MessageDescriptor} from '../../i18n/message-descriptor';
import {User} from '@common/auth/user';
import {SvgIconProps} from '@common/icons/svg-icon';
export interface AdConfig {
slot: string;
description: MessageDescriptor;
image: string;
}
export interface TagType {
name: string;
system?: boolean;
}
export interface CustomPageType {
type: string;
label: MessageDescriptor;
}
export interface HomepageOption {
label: MessageDescriptor;
value: string;
}
export interface SiteConfigContextValue {
auth: {
redirectUri: string;
// redirect uri to use when homepage is set to login page, to avoid infinite loop
secondaryRedirectUri?: string;
adminRedirectUri: string;
getUserProfileLink?: (user: User) => string;
registerFields?: ComponentType;
accountSettingsPanels?: {
icon: ComponentType<SvgIconProps>;
label: MessageDescriptor;
id: string;
component: ComponentType<{user: User}>;
}[];
};
notifications: {
renderMap?: Record<string, ComponentType<NotificationListItemProps>>;
};
tags: {
types: TagType[];
};
customPages: {
types: CustomPageType[];
};
settings?: {
showIncomingMailMethod?: boolean;
showRecaptchaLinkSwitch?: boolean;
};
admin: {
ads: AdConfig[];
channelsDocsLink?: string;
};
demo: {
loginPageDefaults: 'singleAccount' | 'randomAccount';
email?: string;
password?: string;
};
homepage: {
options: HomepageOption[];
};
}
export const SiteConfigContext = React.createContext<SiteConfigContextValue>(
null!,
);

View File

@@ -0,0 +1,9 @@
import {Settings} from './settings';
import {useBootstrapData} from '../bootstrap-data/bootstrap-data-context';
export function useSettings(): Settings {
const {
data: {settings},
} = useBootstrapData();
return settings;
}

View File

@@ -0,0 +1,69 @@
import React, {useMemo} from 'react';
import {applyThemeToDom} from '../ui/themes/utils/apply-theme-to-dom';
import {
ThemeId,
ThemeSelectorContext,
ThemeSelectorContextValue,
} from '../ui/themes/theme-selector-context';
import {CssTheme} from '../ui/themes/css-theme';
import {useSettings} from './settings/use-settings';
import {useBootstrapData} from './bootstrap-data/bootstrap-data-context';
import {useCookie} from '@common/utils/hooks/use-cookie';
const STORAGE_KEY = 'be-active-theme';
interface ThemeProviderProps {
children: any;
}
export function ThemeProvider({children}: ThemeProviderProps) {
const {themes} = useSettings();
const canChangeTheme = themes?.user_change;
const {data} = useBootstrapData();
const allThemes = useMemo(() => data.themes.all || [], [data.themes.all]);
const initialThemeId = data.themes.selectedThemeId || undefined;
const [selectedThemeId, setSelectedThemeId] = useCookie(
STORAGE_KEY,
`${initialThemeId}`,
);
let selectedTheme = canChangeTheme
? allThemes.find(t => t.id == selectedThemeId)
: allThemes.find(t => t.id == themes?.default_id);
if (!selectedTheme) {
selectedTheme = allThemes[0];
}
const contextValue: ThemeSelectorContextValue = useMemo(() => {
return {
allThemes,
selectedTheme: selectedTheme!,
selectTheme: (id: ThemeId) => {
if (!canChangeTheme) return;
const theme = findTheme(allThemes, id);
if (theme) {
setSelectedThemeId(`${theme.id}`);
applyThemeToDom(theme);
}
},
};
}, [allThemes, selectedTheme, setSelectedThemeId, canChangeTheme]);
return (
<ThemeSelectorContext.Provider value={contextValue}>
{children}
</ThemeSelectorContext.Provider>
);
}
function findTheme(themes: CssTheme[], id: ThemeId) {
return themes.find(t => {
if (id === 'light') {
return t.default_light === true;
}
if (id === 'dark') {
return t.default_dark === true;
}
return t.id === id;
});
}