mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-11-28 14:03:48 +03:00
AdminX Announcement Bar (#17950)
refs https://github.com/TryGhost/Product/issues/3807 - Created Announcement Bar components in Admin X.
This commit is contained in:
parent
187f369720
commit
fcb2636d59
@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import Separator from './Separator';
|
||||
|
||||
type HeadingLevel = 1 | 2 | 3 | 4 | 5 | 6;
|
||||
export type HeadingLevel = 1 | 2 | 3 | 4 | 5 | 6;
|
||||
|
||||
interface HeadingBaseProps {
|
||||
level?: HeadingLevel;
|
||||
|
@ -1,13 +1,17 @@
|
||||
import Heading from '../Heading';
|
||||
import clsx from 'clsx';
|
||||
import {Fragment, MouseEvent, useRef} from 'react';
|
||||
|
||||
type SwatchSizes = 'md' | 'lg';
|
||||
|
||||
const ColorSwatch: React.FC<{
|
||||
hex: string;
|
||||
value?: string | null;
|
||||
title: string;
|
||||
size?: SwatchSizes;
|
||||
isSelected: boolean;
|
||||
onSelect: (value: string | null) => void;
|
||||
}> = ({hex, value, title, isSelected, onSelect}) => {
|
||||
}> = ({hex, value, title, size = 'md', isSelected, onSelect}) => {
|
||||
const ref = useRef(null);
|
||||
|
||||
const onSelectHandler = (e: MouseEvent) => {
|
||||
@ -22,11 +26,19 @@ const ColorSwatch: React.FC<{
|
||||
|
||||
const isTransparent = (hex.length === 4 && hex[3] === '0') || (hex.length === 8 && hex.slice(6) === '00');
|
||||
|
||||
let sizeClass = 'h-5 w-5';
|
||||
switch (size) {
|
||||
case 'lg':
|
||||
sizeClass = 'w-6 h-6';
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
ref={ref}
|
||||
className={clsx(
|
||||
`relative flex h-5 w-5 shrink-0 cursor-pointer items-center rounded-full border border-grey-200 dark:border-grey-800`,
|
||||
`relative flex shrink-0 cursor-pointer items-center rounded-full border border-grey-200 dark:border-grey-800`,
|
||||
sizeClass,
|
||||
isSelected && 'outline outline-2 outline-green'
|
||||
)}
|
||||
style={{backgroundColor: hex}}
|
||||
@ -48,33 +60,47 @@ export type SwatchOption = {
|
||||
|
||||
/** Should usually be used via [ColorPickerField](?path=/docs/global-form-color-picker-field--docs) */
|
||||
const ColorIndicator: React.FC<{
|
||||
title?: string;
|
||||
value?: string | null;
|
||||
swatches: SwatchOption[];
|
||||
swatchSize?: SwatchSizes;
|
||||
onSwatchChange: (newValue: string | null) => void;
|
||||
onTogglePicker: () => void;
|
||||
isExpanded: boolean;
|
||||
}> = ({value, swatches, onSwatchChange, onTogglePicker, isExpanded}) => {
|
||||
picker?: boolean;
|
||||
containerClassName?: string;
|
||||
|
||||
}> = ({title, value, swatches, swatchSize = 'md',onSwatchChange, onTogglePicker, isExpanded, picker = true, containerClassName}) => {
|
||||
let selectedSwatch = swatches.find(swatch => swatch.value === value || swatch.hex === value);
|
||||
|
||||
if (isExpanded) {
|
||||
selectedSwatch = undefined;
|
||||
}
|
||||
|
||||
containerClassName = clsx(
|
||||
'flex flex-col gap-3'
|
||||
);
|
||||
|
||||
return (
|
||||
<div className='flex gap-1'>
|
||||
<div className={`flex items-center gap-1`}>
|
||||
{swatches.map(({customContent, ...swatch}) => (
|
||||
customContent ? <Fragment key={swatch.title}>{customContent}</Fragment> : <ColorSwatch key={swatch.title} isSelected={selectedSwatch?.title === swatch.title} onSelect={onSwatchChange} {...swatch} />
|
||||
))}
|
||||
<div className={containerClassName}>
|
||||
{title && <Heading useLabelTag>{title}</Heading>}
|
||||
<div className='flex gap-1'>
|
||||
<div className={`flex items-center gap-1`}>
|
||||
{swatches.map(({customContent, ...swatch}) => (
|
||||
customContent ? <Fragment key={swatch.title}>{customContent}</Fragment> : <ColorSwatch key={swatch.title} isSelected={selectedSwatch?.title === swatch.title} size={swatchSize} onSelect={onSwatchChange} {...swatch} />
|
||||
))}
|
||||
</div>
|
||||
{picker &&
|
||||
<button aria-label="Pick color" className="relative h-6 w-6 cursor-pointer rounded-full border border-grey-200 dark:border-grey-800" type="button" onClick={onTogglePicker}>
|
||||
<div className='absolute inset-0 rounded-full bg-[conic-gradient(hsl(360,100%,50%),hsl(315,100%,50%),hsl(270,100%,50%),hsl(225,100%,50%),hsl(180,100%,50%),hsl(135,100%,50%),hsl(90,100%,50%),hsl(45,100%,50%),hsl(0,100%,50%))]' />
|
||||
{value && !selectedSwatch && (
|
||||
<div className="dark:border-grey-950 absolute inset-[3px] overflow-hidden rounded-full border border-white" style={{backgroundColor: value}}>
|
||||
{value === 'transparent' && <div className="absolute left-[3px] top-[3px] z-10 w-[136%] origin-left rotate-45 border-b border-b-red" />}
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
<button aria-label="Pick color" className="relative h-6 w-6 cursor-pointer rounded-full border border-grey-200 dark:border-grey-800" type="button" onClick={onTogglePicker}>
|
||||
<div className='absolute inset-0 rounded-full bg-[conic-gradient(hsl(360,100%,50%),hsl(315,100%,50%),hsl(270,100%,50%),hsl(225,100%,50%),hsl(180,100%,50%),hsl(135,100%,50%),hsl(90,100%,50%),hsl(45,100%,50%),hsl(0,100%,50%))]' />
|
||||
{value && !selectedSwatch && (
|
||||
<div className="dark:border-grey-950 absolute inset-[3px] overflow-hidden rounded-full border border-white" style={{backgroundColor: value}}>
|
||||
{value === 'transparent' && <div className="absolute left-[3px] top-[3px] z-10 w-[136%] origin-left rotate-45 border-b border-b-red" />}
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -1,6 +1,6 @@
|
||||
import ButtonGroup from '../ButtonGroup';
|
||||
import DesktopChrome from '../chrome/DesktopChrome';
|
||||
import Heading from '../Heading';
|
||||
import Heading, {HeadingLevel} from '../Heading';
|
||||
import MobileChrome from '../chrome/MobileChrome';
|
||||
import Modal, {ModalSize} from './Modal';
|
||||
import NiceModal, {useModal} from '@ebay/nice-modal-react';
|
||||
@ -15,6 +15,7 @@ import {confirmIfDirty} from '../../../utils/modals';
|
||||
export interface PreviewModalProps {
|
||||
testId?: string;
|
||||
title?: string;
|
||||
titleHeadingLevel?: HeadingLevel;
|
||||
size?: ModalSize;
|
||||
sidebar?: boolean | React.ReactNode;
|
||||
preview?: React.ReactNode;
|
||||
@ -48,6 +49,7 @@ export interface PreviewModalProps {
|
||||
export const PreviewModalContent: React.FC<PreviewModalProps> = ({
|
||||
testId,
|
||||
title,
|
||||
titleHeadingLevel = 4,
|
||||
size = 'full',
|
||||
sidebar = '',
|
||||
preview,
|
||||
@ -216,7 +218,7 @@ export const PreviewModalContent: React.FC<PreviewModalProps> = ({
|
||||
<div className='relative flex h-full basis-[400px] flex-col border-l border-grey-100'>
|
||||
{sidebarHeader ? sidebarHeader : (
|
||||
<div className='flex max-h-[74px] items-start justify-between gap-3 px-7 py-5'>
|
||||
<Heading className='mt-1' level={4}>{title}</Heading>
|
||||
<Heading className='mt-1' level={titleHeadingLevel}>{title}</Heading>
|
||||
{sidebarButtons ? sidebarButtons : <ButtonGroup buttons={buttons} /> }
|
||||
</div>
|
||||
)}
|
||||
|
@ -46,6 +46,7 @@ const Sidebar: React.FC = () => {
|
||||
{/* <SettingNavItem navid='theme' title="Theme" onClick={handleSectionClick} /> */}
|
||||
<SettingNavItem navid='design' title="Branding and design" onClick={handleSectionClick} />
|
||||
<SettingNavItem navid='navigation' title="Navigation" onClick={handleSectionClick} />
|
||||
<SettingNavItem navid='announcement-bar' title="Announcement bar" onClick={handleSectionClick} />
|
||||
</SettingNavSection>
|
||||
|
||||
<SettingNavSection title="Membership">
|
||||
|
@ -2,6 +2,7 @@ import AddIntegrationModal from '../settings/advanced/integrations/AddIntegratio
|
||||
import AddNewsletterModal from '../settings/email/newsletters/AddNewsletterModal';
|
||||
import AddRecommendationModal from '../settings/site/recommendations/AddRecommendationModal';
|
||||
import AmpModal from '../settings/advanced/integrations/AmpModal';
|
||||
import AnnouncementBarModal from '../settings/site/AnnouncementBarModal';
|
||||
import ChangeThemeModal from '../settings/site/ThemeModal';
|
||||
import CustomIntegrationModal from '../settings/advanced/integrations/CustomIntegrationModal';
|
||||
import DesignModal from '../settings/site/DesignModal';
|
||||
@ -77,7 +78,8 @@ const modalPaths: {[key: string]: React.FC<NiceModalHocProps & RoutingModalProps
|
||||
'integrations/add': AddIntegrationModal,
|
||||
'integrations/show/:id': CustomIntegrationModal,
|
||||
'recommendations/add': AddRecommendationModal,
|
||||
'recommendations/:id': EditRecommendationModal
|
||||
'recommendations/:id': EditRecommendationModal,
|
||||
'announcement-bar/edit': AnnouncementBarModal
|
||||
};
|
||||
|
||||
function getHashPath(urlPath: string | undefined) {
|
||||
|
@ -0,0 +1,24 @@
|
||||
import Button from '../../../admin-x-ds/global/Button';
|
||||
import React from 'react';
|
||||
import SettingGroup from '../../../admin-x-ds/settings/SettingGroup';
|
||||
import useRouting from '../../../hooks/useRouting';
|
||||
|
||||
const AnnouncementBar: React.FC<{ keywords: string[] }> = ({keywords}) => {
|
||||
const {updateRoute} = useRouting();
|
||||
const openModal = () => {
|
||||
updateRoute('announcement-bar/edit');
|
||||
};
|
||||
|
||||
return (
|
||||
<SettingGroup
|
||||
customButtons={<Button color='green' label='Customize' link onClick={openModal}/>}
|
||||
description="Highlight important updates or offers"
|
||||
keywords={keywords}
|
||||
navid='announcement-bar'
|
||||
testId='announcement-bar'
|
||||
title="Announcement bar"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default AnnouncementBar;
|
@ -0,0 +1,92 @@
|
||||
import AnnouncementBarPreview from './announcementBar/AnnouncementBarPreview';
|
||||
import CheckboxGroup from '../../../admin-x-ds/global/form/CheckboxGroup';
|
||||
import ColorIndicator from '../../../admin-x-ds/global/form/ColorIndicator';
|
||||
import Form from '../../../admin-x-ds/global/form/Form';
|
||||
import HtmlField from '../../../admin-x-ds/global/form/HtmlField';
|
||||
import NiceModal from '@ebay/nice-modal-react';
|
||||
import React from 'react';
|
||||
import useRouting from '../../../hooks/useRouting';
|
||||
import {PreviewModalContent} from '../../../admin-x-ds/global/modal/PreviewModal';
|
||||
import {useGlobalData} from '../../providers/GlobalDataProvider';
|
||||
|
||||
const Sidebar: React.FC = () => {
|
||||
const {config} = useGlobalData();
|
||||
return (
|
||||
<Form>
|
||||
<HtmlField
|
||||
config={config}
|
||||
nodes='MINIMAL_NODES'
|
||||
placeholder='Highlight breaking news, offers or updates'
|
||||
title='Announcement'
|
||||
/>
|
||||
<ColorIndicator
|
||||
isExpanded={false}
|
||||
picker={false}
|
||||
swatches={[
|
||||
{
|
||||
hex: '#08090c',
|
||||
title: 'Dark'
|
||||
},
|
||||
{
|
||||
hex: '#ffffff',
|
||||
title: 'Light'
|
||||
},
|
||||
{
|
||||
hex: '#ffdd00',
|
||||
title: 'Accent'
|
||||
}
|
||||
]}
|
||||
swatchSize='lg'
|
||||
title='Background color'
|
||||
onSwatchChange={() => {}}
|
||||
onTogglePicker={() => {}}
|
||||
/>
|
||||
<CheckboxGroup
|
||||
checkboxes={[
|
||||
{
|
||||
label: 'Logged out visitors',
|
||||
onChange: () => {},
|
||||
value: ''
|
||||
},
|
||||
{
|
||||
label: 'Free members',
|
||||
onChange: () => {},
|
||||
value: ''
|
||||
},
|
||||
{
|
||||
label: 'Paid members',
|
||||
onChange: () => {},
|
||||
value: ''
|
||||
}
|
||||
]}
|
||||
title='Visibility'
|
||||
/>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
const AnnouncementBarModal: React.FC = () => {
|
||||
// const modal = useModal();
|
||||
const {updateRoute} = useRouting();
|
||||
|
||||
const sidebar = <Sidebar />;
|
||||
|
||||
return <PreviewModalContent
|
||||
afterClose={() => {
|
||||
updateRoute('announcement-bar');
|
||||
}}
|
||||
cancelLabel='Close'
|
||||
deviceSelector={false}
|
||||
dirty={false}
|
||||
okLabel='Save'
|
||||
preview={<AnnouncementBarPreview />}
|
||||
previewBgColor='greygradient'
|
||||
sidebar={sidebar}
|
||||
testId='announcement-bar-modal'
|
||||
title='Announcement bar'
|
||||
titleHeadingLevel={5}
|
||||
onOk={() => {}}
|
||||
/>;
|
||||
};
|
||||
|
||||
export default NiceModal.create(AnnouncementBarModal);
|
@ -1,3 +1,4 @@
|
||||
import AnnouncementBar from './AnnouncementBar';
|
||||
import DesignSetting from './DesignSetting';
|
||||
import Navigation from './Navigation';
|
||||
import React from 'react';
|
||||
@ -7,7 +8,8 @@ import SettingSection from '../../../admin-x-ds/settings/SettingSection';
|
||||
const searchKeywords = {
|
||||
theme: ['themes', 'design', 'appearance', 'style'],
|
||||
design: ['design', 'branding', 'logo', 'cover', 'colors', 'fonts', 'background'],
|
||||
navigation: ['navigation', 'menus', 'primary', 'secondary', 'links']
|
||||
navigation: ['navigation', 'menus', 'primary', 'secondary', 'links'],
|
||||
announcementBar: ['announcement', 'bar', 'important', 'banner']
|
||||
};
|
||||
|
||||
const SiteSettings: React.FC = () => {
|
||||
@ -17,6 +19,7 @@ const SiteSettings: React.FC = () => {
|
||||
{/* <Theme keywords={searchKeywords.theme} /> */}
|
||||
<DesignSetting keywords={searchKeywords.design} />
|
||||
<Navigation keywords={searchKeywords.navigation} />
|
||||
<AnnouncementBar keywords={searchKeywords.announcementBar} />
|
||||
</SettingSection>
|
||||
</>
|
||||
);
|
||||
|
@ -0,0 +1,7 @@
|
||||
import React from 'react';
|
||||
|
||||
const AnnouncementBarPreview: React.FC = () => {
|
||||
return (<>Announcement bar preview</>);
|
||||
};
|
||||
|
||||
export default AnnouncementBarPreview;
|
Loading…
Reference in New Issue
Block a user