131
resources/client/admin/appearance/app-appearance-config.tsx
Executable file
131
resources/client/admin/appearance/app-appearance-config.tsx
Executable 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,
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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}"]`
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user