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,131 @@
import {
IAppearanceConfig,
MenuSectionConfig,
SeoSettingsSectionConfig,
} from '@common/admin/appearance/types/appearance-editor-config';
import {message} from '@common/i18n/message';
import {LandingPageSectionGeneral} from '@app/admin/appearance/sections/landing-page-section/landing-page-section-general';
import {LandingPageSectionActionButtons} from '@app/admin/appearance/sections/landing-page-section/landing-page-section-action-buttons';
import {LandingPageSectionPrimaryFeatures} from '@app/admin/appearance/sections/landing-page-section/landing-page-section-primary-features';
import {LandingPageSecondaryFeatures} from '@app/admin/appearance/sections/landing-page-section/landing-page-section-secondary-features';
import {AppearanceEditorBreadcrumbItem} from '@common/admin/appearance/types/appearance-editor-section';
export const AppAppearanceConfig: IAppearanceConfig = {
preview: {
defaultRoute: 'dashboard',
navigationRoutes: ['dashboard'],
},
sections: {
'landing-page': {
label: message('Landing Page'),
position: 1,
previewRoute: '/',
routes: [
{path: 'landing-page', element: <LandingPageSectionGeneral />},
{
path: 'landing-page/action-buttons',
element: <LandingPageSectionActionButtons />,
},
{
path: 'landing-page/primary-features',
element: <LandingPageSectionPrimaryFeatures />,
},
{
path: 'landing-page/secondary-features',
element: <LandingPageSecondaryFeatures />,
},
],
buildBreadcrumb: pathname => {
const parts = pathname.split('/').filter(p => !!p);
const sectionName = parts.pop();
// admin/appearance
const breadcrumb: AppearanceEditorBreadcrumbItem[] = [
{
label: message('Landing page'),
location: 'landing-page',
},
];
if (sectionName === 'action-buttons') {
breadcrumb.push({
label: message('Action buttons'),
location: 'landing-page/action-buttons',
});
}
if (sectionName === 'primary-features') {
breadcrumb.push({
label: message('Primary features'),
location: 'landing-page/primary-features',
});
}
if (sectionName === 'secondary-features') {
breadcrumb.push({
label: message('Secondary features'),
location: 'landing-page/secondary-features',
});
}
return breadcrumb;
},
},
// missing label will get added by deepMerge from default config
// @ts-ignore
menus: {
config: {
positions: [
'sidebar-primary',
'sidebar-secondary',
'mobile-bottom',
'landing-page-navbar',
'landing-page-footer',
],
availableRoutes: [
'/lists',
'/watchlist',
'/admin/channels',
'/admin/comments',
],
} as MenuSectionConfig,
},
// @ts-ignore
'seo-settings': {
config: {
pages: [
{
key: 'title-page',
label: message('Title page'),
},
{
key: 'season-page',
label: message('Season page'),
},
{
key: 'episode-page',
label: message('Episode page'),
},
{
key: 'watch-page',
label: message('Watch page'),
},
{
key: 'person-page',
label: message('Person page'),
},
{
key: 'landing-page',
label: message('Landing page'),
},
{
key: 'news-article-page',
label: message('News article page'),
},
{
key: 'channel-page',
label: message('Channel page'),
},
],
} as SeoSettingsSectionConfig,
},
},
};

View File

@@ -0,0 +1,33 @@
import {MenuItemForm} from '@common/admin/menus/menu-item-form';
import {Accordion, AccordionItem} from '@common/ui/accordion/accordion';
import {Trans} from '@common/i18n/trans';
import {appearanceState} from '@common/admin/appearance/appearance-store';
import {useState} from 'react';
export function LandingPageSectionActionButtons() {
const [expandedValues, setExpandedValues] = useState(['cta1']);
return (
<Accordion
variant="outline"
expandedValues={expandedValues}
onExpandedChange={values => {
setExpandedValues(values as string[]);
if (values.length) {
appearanceState().preview.setHighlight(
`[data-testid="${values[0]}"]`
);
}
}}
>
<AccordionItem value="cta1" label={<Trans message="Header button 1" />}>
<MenuItemForm formPathPrefix="settings.homepage.appearance.actions.cta1" />
</AccordionItem>
<AccordionItem value="ct2" label={<Trans message="Header button 2" />}>
<MenuItemForm formPathPrefix="settings.homepage.appearance.actions.cta2" />
</AccordionItem>
<AccordionItem value="cta3" label={<Trans message="Footer button" />}>
<MenuItemForm formPathPrefix="settings.homepage.appearance.actions.cta3" />
</AccordionItem>
</Accordion>
);
}

View File

@@ -0,0 +1,216 @@
import {useFormContext} from 'react-hook-form';
import {
appearanceState,
AppearanceValues,
useAppearanceStore,
} from '@common/admin/appearance/appearance-store';
import {Fragment, ReactNode} from 'react';
import {FormTextField} from '@common/ui/forms/input-field/text-field/text-field';
import {Trans} from '@common/i18n/trans';
import {FormImageSelector} from '@common/ui/images/image-selector';
import {FormSlider} from '@common/ui/forms/slider/slider';
import {DialogTrigger} from '@common/ui/overlays/dialog/dialog-trigger';
import {AppearanceButton} from '@common/admin/appearance/appearance-button';
import {ColorIcon} from '@common/admin/appearance/sections/themes/color-icon';
import {ColorPickerDialog} from '@common/ui/color-picker/color-picker-dialog';
import {Link} from 'react-router-dom';
import {FormSwitch} from '@common/ui/forms/toggle/switch';
import {LandingPageContent} from '@app/landing-page/landing-page-content';
export function LandingPageSectionGeneral() {
return (
<Fragment>
<HeaderSection />
<div className="my-24 border-y py-24">
<AppearanceButton
to="action-buttons"
elementType={Link}
className="mb-20"
>
<Trans message="Action buttons" />
</AppearanceButton>
<AppearanceButton to="primary-features" elementType={Link}>
<Trans message="Primary features" />
</AppearanceButton>
<AppearanceButton to="secondary-features" elementType={Link}>
<Trans message="Secondary features" />
</AppearanceButton>
</div>
<FooterSection />
<PricingSection />
</Fragment>
);
}
function HeaderSection() {
const defaultImage = useAppearanceStore(
s => s.defaults?.settings.homepage?.appearance?.headerImage,
);
return (
<Fragment>
<FormTextField
label={<Trans message="Header title" />}
className="mb-20"
name="settings.homepage.appearance.headerTitle"
onFocus={() => {
appearanceState().preview.setHighlight('[data-testid="headerTitle"]');
}}
/>
<FormTextField
label={<Trans message="Header subtitle" />}
className="mb-30"
inputElementType="textarea"
rows={4}
name="settings.homepage.appearance.headerSubtitle"
onFocus={() => {
appearanceState().preview.setHighlight(
'[data-testid="headerSubtitle"]',
);
}}
/>
<FormImageSelector
name="settings.homepage.appearance.headerImage"
className="mb-30"
label={<Trans message="Header image" />}
defaultValue={defaultImage}
diskPrefix="homepage"
/>
<FormSwitch
className="mb-24"
name="settings.homepage.appearance.blurHeaderImage"
>
<Trans message="Blur header image" />
</FormSwitch>
<FormSlider
name="settings.homepage.appearance.headerImageOpacity"
label={<Trans message="Header image opacity" />}
minValue={0}
step={0.1}
maxValue={1}
formatOptions={{style: 'percent'}}
/>
<div className="mb-20 text-xs text-muted">
<Trans message="In order for overlay colors to appear, header image opacity will need to be less then 100%" />
</div>
<ColorPickerTrigger
formKey="settings.homepage.appearance.headerOverlayColor1"
label={<Trans message="Header overlay color 1" />}
/>
<ColorPickerTrigger
formKey="settings.homepage.appearance.headerOverlayColor2"
label={<Trans message="Header overlay color 2" />}
/>
</Fragment>
);
}
function FooterSection() {
const defaultImage = useAppearanceStore(
s =>
(s.defaults?.settings.homepage?.appearance as LandingPageContent)
?.footerImage,
);
return (
<Fragment>
<FormSwitch className="mb-24" name="settings.homepage.trending">
<Trans message="Show trending titles" />
</FormSwitch>
<FormTextField
label={<Trans message="Footer title" />}
className="mb-20"
name="settings.homepage.appearance.footerTitle"
onFocus={() => {
appearanceState().preview.setHighlight('[data-testid="footerTitle"]');
}}
/>
<FormTextField
label={<Trans message="Footer subtitle" />}
className="mb-20"
name="settings.homepage.appearance.footerSubtitle"
onFocus={() => {
appearanceState().preview.setHighlight(
'[data-testid="footerSubtitle"]',
);
}}
/>
<FormImageSelector
name="settings.homepage.appearance.footerImage"
className="mb-30"
label={<Trans message="Footer background image" />}
defaultValue={defaultImage}
diskPrefix="homepage"
/>
</Fragment>
);
}
function PricingSection() {
return (
<div className="mt-24 border-t pt-24">
<FormTextField
label={<Trans message="Pricing title" />}
className="mb-20"
name="settings.homepage.appearance.pricingTitle"
onFocus={() => {
appearanceState().preview.setHighlight(
'[data-testid="pricingTitle"]',
);
}}
/>
<FormTextField
label={<Trans message="Pricing subtitle" />}
className="mb-20"
name="settings.homepage.appearance.pricingSubtitle"
onFocus={() => {
appearanceState().preview.setHighlight(
'[data-testid="pricingSubtitle"]',
);
}}
/>
<FormSwitch className="mb-24" name="settings.homepage.pricing">
<Trans message="Show pricing table" />
</FormSwitch>
</div>
);
}
interface ColorPickerTriggerProps {
formKey: string;
label: ReactNode;
}
function ColorPickerTrigger({label, formKey}: ColorPickerTriggerProps) {
const key = formKey as 'settings.homepage.appearance.headerOverlayColor1';
const {watch, setValue} = useFormContext<AppearanceValues>();
const formValue = watch(key);
const setColor = (value: string | null) => {
setValue(formKey as any, value, {
shouldDirty: true,
});
};
return (
<DialogTrigger
value={formValue}
onValueChange={newValue => setColor(newValue)}
type="popover"
onClose={value => setColor(value)}
>
<AppearanceButton
className="capitalize"
startIcon={
<ColorIcon
viewBox="0 0 48 48"
className="icon-lg"
style={{fill: formValue}}
/>
}
>
{label}
</AppearanceButton>
<ColorPickerDialog />
</DialogTrigger>
);
}

View File

@@ -0,0 +1,117 @@
import {Accordion, AccordionItem} from '@common/ui/accordion/accordion';
import {Trans} from '@common/i18n/trans';
import {
appearanceState,
useAppearanceStore,
} from '@common/admin/appearance/appearance-store';
import {useFieldArray} from 'react-hook-form';
import {FormTextField} from '@common/ui/forms/input-field/text-field/text-field';
import {FormImageSelector} from '@common/ui/images/image-selector';
import {Button} from '@common/ui/buttons/button';
import {AddIcon} from '@common/icons/material/Add';
import {useState} from 'react';
export function LandingPageSectionPrimaryFeatures() {
const {fields, remove, append} = useFieldArray({
name: 'settings.homepage.appearance.primaryFeatures',
});
const [expandedValues, setExpandedValues] = useState([0]);
return (
<div>
<Accordion
variant="outline"
expandedValues={expandedValues}
onExpandedChange={values => {
setExpandedValues(values as number[]);
if (values.length) {
appearanceState().preview.setHighlight(
`[data-testid="primary-root-${values[0]}"]`
);
}
}}
>
{fields.map((field, index) => {
return (
<AccordionItem
key={field.id}
value={index}
label={<Trans message={`Primary feature ${index + 1}`} />}
>
<FeatureForm index={index} />
<div className="text-right">
<Button
size="xs"
variant="outline"
color="danger"
onClick={() => {
remove(index);
}}
>
<Trans message="Remove" />
</Button>
</div>
</AccordionItem>
);
})}
</Accordion>
<div className="mt-20 text-right">
<Button
size="xs"
variant="outline"
color="primary"
startIcon={<AddIcon />}
onClick={() => {
append({});
setExpandedValues([fields.length]);
}}
>
<Trans message="Add feature" />
</Button>
</div>
</div>
);
}
interface FeatureFormProps {
index: number;
}
function FeatureForm({index}: FeatureFormProps) {
const defaultImage = useAppearanceStore(
s =>
s.defaults?.settings.homepage?.appearance?.primaryFeatures?.[index]?.image
);
return (
<>
<FormImageSelector
name={`settings.homepage.appearance.primaryFeatures.${index}.image`}
className="mb-30"
label={<Trans message="Image" />}
defaultValue={defaultImage}
diskPrefix="homepage"
/>
<FormTextField
name={`settings.homepage.appearance.primaryFeatures.${index}.title`}
label={<Trans message="Title" />}
className="mb-20"
onFocus={() => {
appearanceState().preview.setHighlight(
`[data-testid="primary-title-${index}"]`
);
}}
/>
<FormTextField
name={`settings.homepage.appearance.primaryFeatures.${index}.subtitle`}
label={<Trans message="Subtitle" />}
className="mb-20"
inputElementType="textarea"
rows={4}
onFocus={() => {
appearanceState().preview.setHighlight(
`[data-testid="primary-subtitle-${index}"]`
);
}}
/>
</>
);
}

View File

@@ -0,0 +1,126 @@
import {Accordion, AccordionItem} from '@common/ui/accordion/accordion';
import {Trans} from '@common/i18n/trans';
import {appearanceState} from '@common/admin/appearance/appearance-store';
import {useFieldArray} from 'react-hook-form';
import {FormTextField} from '@common/ui/forms/input-field/text-field/text-field';
import {FormImageSelector} from '@common/ui/images/image-selector';
import {Button} from '@common/ui/buttons/button';
import {AddIcon} from '@common/icons/material/Add';
import {useState} from 'react';
export function LandingPageSecondaryFeatures() {
const {fields, remove, append} = useFieldArray({
name: 'settings.homepage.appearance.secondaryFeatures',
});
const [expandedValues, setExpandedValues] = useState([0]);
return (
<div>
<Accordion
variant="outline"
expandedValues={expandedValues}
onExpandedChange={values => {
setExpandedValues(values as number[]);
if (values.length) {
appearanceState().preview.setHighlight(
`[data-testid="secondary-root-${values[0]}"]`
);
}
}}
>
{fields.map((field, index) => {
return (
<AccordionItem
key={field.id}
value={index}
label={<Trans message={`Secondary feature ${index + 1}`} />}
>
<FeatureForm index={index} />
<div className="text-right">
<Button
size="xs"
variant="outline"
color="danger"
onClick={() => {
remove(index);
}}
>
<Trans message="Remove" />
</Button>
</div>
</AccordionItem>
);
})}
</Accordion>
<div className="mt-20 text-right">
<Button
size="xs"
variant="outline"
color="primary"
startIcon={<AddIcon />}
onClick={() => {
append({});
setExpandedValues([fields.length]);
}}
>
<Trans message="Add feature" />
</Button>
</div>
</div>
);
}
interface FeatureFormProps {
index: number;
}
function FeatureForm({index}: FeatureFormProps) {
return (
<>
<FormImageSelector
name={`settings.homepage.appearance.secondaryFeatures.${index}.image`}
className="mb-30"
label={<Trans message="Image" />}
defaultValue={getDefaultImage(index)}
diskPrefix="homepage"
/>
<FormTextField
name={`settings.homepage.appearance.secondaryFeatures.${index}.title`}
label={<Trans message="Title" />}
className="mb-20"
onFocus={() => {
appearanceState().preview.setHighlight(
`[data-testid="secondary-title-${index}"]`
);
}}
/>
<FormTextField
name={`settings.homepage.appearance.secondaryFeatures.${index}.subtitle`}
label={<Trans message="Subtitle" />}
className="mb-20"
inputElementType="textarea"
rows={4}
onFocus={() => {
appearanceState().preview.setHighlight(
`[data-testid="secondary-subtitle-${index}"]`
);
}}
/>
<FormTextField
name={`settings.homepage.appearance.secondaryFeatures.${index}.description`}
label={<Trans message="Description" />}
className="mb-20"
inputElementType="textarea"
rows={4}
onFocus={() => {
appearanceState().preview.setHighlight(
`[data-testid="secondary-description-${index}"]`
);
}}
/>
</>
);
}
function getDefaultImage(index: number): string | undefined {
return appearanceState().defaults?.settings.homepage?.appearance
.secondaryFeatures[index]?.image;
}