Added basic theme settings design in AdminX (#17000)

refs https://github.com/TryGhost/Team/issues/3432

- adds basic design structure for theme settings/management in adminX

---------

Co-authored-by: Peter Zimon <peter.zimon@gmail.com>
This commit is contained in:
Rishabh Garg 2023-06-13 14:27:29 +05:30 committed by GitHub
parent 5cc1c43257
commit b697ca2768
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 548 additions and 108 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

View File

@ -58,7 +58,7 @@ const Button: React.FC<IButton> = ({
styles += link ? ' text-white hover:text-white' : ' bg-white text-black';
break;
default:
styles += link ? ' text-black hover:text-grey-800' : ' bg-transparent text-black hover:text-grey-800';
styles += link ? ' text-black hover:text-grey-800' : ' text-black hover:bg-grey-200';
break;
}

View File

@ -13,7 +13,7 @@ export interface ConfirmationModalProps {
onOk?: (modal?: {
remove: () => void;
}) => void;
customFooter?: React.ReactNode;
customFooter?: boolean | React.ReactNode;
}
const ConfirmationModal: React.FC<ConfirmationModalProps> = ({
@ -25,7 +25,7 @@ const ConfirmationModal: React.FC<ConfirmationModalProps> = ({
okColor = 'black',
onCancel,
onOk,
customFooter
customFooter = false
}) => {
const modal = useModal();
const [taskState, setTaskState] = useState<'running' | ''>('');
@ -33,7 +33,7 @@ const ConfirmationModal: React.FC<ConfirmationModalProps> = ({
<Modal
backDropClick={false}
cancelLabel={cancelLabel}
customFooter={customFooter}
footer={customFooter}
okColor={okColor}
okLabel={taskState === 'running' ? okRunningLabel : okLabel}
size={540}

View File

@ -25,6 +25,12 @@ export const Small: Story = {
}
};
export const Large: Story = {
args: {
size: 'lg'
}
};
export const Empty: Story = {
args: {
toolbarLeft: <></>

View File

@ -1,6 +1,6 @@
import React from 'react';
export type DesktopChromeHeaderSize = 'sm' | 'md';
export type DesktopChromeHeaderSize = 'sm' | 'md' | 'lg';
interface DesktopChromeHeaderProps {
size?: DesktopChromeHeaderSize;
@ -17,14 +17,32 @@ const DesktopChromeHeader: React.FC<DesktopChromeHeaderProps> = ({
toolbarRight = '',
toolbarClasses = ''
}) => {
let containerSize = size === 'sm' ? 'min-h-[32px] p-2' : 'min-h-[48px] p-3';
let containerSize;
switch (size) {
case 'sm':
containerSize = 'h-[32px] p-2';
break;
case 'md':
containerSize = 'h-[48px] px-3 py-5';
break;
case 'lg':
containerSize = 'h-[74px] px-3 py-5';
break;
default:
break;
}
const trafficLightSize = size === 'sm' ? 'w-[6px] h-[6px]' : 'w-[10px] h-[10px]';
const trafficLightWidth = size === 'sm' ? 36 : 56;
let trafficLightContainerStyle = size === 'sm' ? 'gap-[5px] ' : 'gap-2 ';
trafficLightContainerStyle += `w-[${trafficLightWidth}px]`;
const trafficLights = (
<div className={`absolute left-4 flex h-full items-center ${trafficLightContainerStyle}`}>
<div className={`absolute left-5 flex h-full items-center ${trafficLightContainerStyle}`}>
<div className={`rounded-full bg-grey-500 ${trafficLightSize}`}></div>
<div className={`rounded-full bg-grey-500 ${trafficLightSize}`}></div>
<div className={`rounded-full bg-grey-500 ${trafficLightSize}`}></div>
@ -34,7 +52,7 @@ const DesktopChromeHeader: React.FC<DesktopChromeHeaderProps> = ({
return (
<header className={`relative flex items-center justify-center bg-grey-50 ${containerSize} ${toolbarClasses}`}>
{toolbarLeft ?
<div className='absolute left-4 flex h-full items-center'>
<div className='absolute left-5 flex h-full items-center'>
{toolbarLeft}
</div>
:
@ -48,7 +66,7 @@ const DesktopChromeHeader: React.FC<DesktopChromeHeaderProps> = ({
}
</div>
{toolbarRight &&
<div className='absolute right-4 flex h-full items-center'>
<div className='absolute right-5 flex h-full items-center'>
{toolbarRight}
</div>
}

View File

@ -21,7 +21,7 @@ export interface ModalProps {
okColor?: string;
cancelLabel?: string;
leftButtonLabel?: string;
customFooter?: React.ReactNode;
footer?: boolean | React.ReactNode;
noPadding?: boolean;
onOk?: () => void;
onCancel?: () => void;
@ -37,7 +37,7 @@ const Modal: React.FC<ModalProps> = ({
title,
okLabel = 'OK',
cancelLabel = 'Cancel',
customFooter,
footer,
leftButtonLabel,
noPadding = false,
onOk,
@ -52,7 +52,7 @@ const Modal: React.FC<ModalProps> = ({
let buttons: IButton[] = [];
if (!customFooter) {
if (!footer) {
if (cancelLabel) {
buttons.push({
key: 'cancel-modal',
@ -135,10 +135,10 @@ const Modal: React.FC<ModalProps> = ({
let contentClasses = clsx(
padding,
size === 'full' && 'h-full'
((size === 'full' || size === 'bleed') && 'grow')
);
if (!customFooter) {
if (!footer) {
contentClasses += ' pb-0 ';
}
@ -165,7 +165,8 @@ const Modal: React.FC<ModalProps> = ({
</div>
);
const footer = (stickyFooter ?
if (footer === undefined) {
footer = (stickyFooter ?
<StickyFooter height={84}>
{footerContent}
</StickyFooter>
@ -174,6 +175,7 @@ const Modal: React.FC<ModalProps> = ({
{footerContent}
</>
);
}
return (
<div className={backdropClasses} id='modal-backdrop' onClick={handleBackdropClick}>
@ -188,9 +190,7 @@ const Modal: React.FC<ModalProps> = ({
{children}
</div>
</div>
{customFooter ? customFooter :
footer
}
{footer}
</section>
</div>
);

View File

@ -4,7 +4,7 @@ import Heading from './Heading';
import NiceModal from '@ebay/nice-modal-react';
import PreviewModal from './PreviewModal';
import PreviewModalContainer from './PreviewModalContainer';
import {SelectOption} from './Select';
import {Tab} from './TabView';
const meta = {
title: 'Global / Modal / Preview Modal',
@ -14,18 +14,24 @@ const meta = {
<NiceModal.Provider>
<PreviewModalContainer {...context.args} />
</NiceModal.Provider>
)]
)],
argTypes: {
sidebar: {control: 'text'},
preview: {control: 'text'},
sidebarButtons: {control: 'text'},
sidebarHeader: {control: 'text'}
}
} satisfies Meta<typeof PreviewModal>;
export default meta;
type Story = StoryObj<typeof PreviewModal>;
const selectOptions: SelectOption[] = [
{value: 'homepage', label: 'Homepage'},
{value: 'post', label: 'Post'},
{value: 'page', label: 'Page'},
{value: 'tag-archive', label: 'Tag archive'},
{value: 'author-archive', label: 'Author archive'}
const previewURLs: Tab[] = [
{id: 'homepage', title: 'Homepage'},
{id: 'post', title: 'Post'},
{id: 'page', title: 'Page'},
{id: 'tag-archive', title: 'Tag archive'},
{id: 'author-archive', title: 'Author archive'}
];
export const Default: Story = {
@ -41,7 +47,10 @@ export const Default: Story = {
Sidebar area
</div>
),
previewToolbarURLs: selectOptions
previewToolbarTabs: previewURLs,
onSelectURL: (id: string) => {
alert(id);
}
}
};
@ -81,3 +90,10 @@ export const CustomSidebarHeader: Story = {
)
}
};
export const FullBleed: Story = {
args: {
...Default.args,
size: 'bleed'
}
};

View File

@ -2,17 +2,18 @@ import ButtonGroup from './ButtonGroup';
import DesktopChromeHeader from './DesktopChromeHeader';
import Heading from './Heading';
import MobileChrome from './MobileChrome';
import Modal from './Modal';
import Modal, {ModalSize} from './Modal';
import NiceModal, {useModal} from '@ebay/nice-modal-react';
import React, {useState} from 'react';
import URLSelect from './URLSelect';
import Select, {SelectOption} from './Select';
import TabView, {Tab} from './TabView';
import {IButton} from './Button';
import {SelectOption} from './Select';
export interface PreviewModalProps {
testId?: string;
title?: string;
sidebar?: React.ReactNode;
size?: ModalSize;
sidebar?: boolean | React.ReactNode;
preview?: React.ReactNode;
cancelLabel?: string;
okLabel?: string;
@ -21,6 +22,8 @@ export interface PreviewModalProps {
previewToolbar?: boolean;
previewToolbarURLs?: SelectOption[];
selectedURL?: string;
previewToolbarTabs?: Tab[];
defaultTab?: string;
sidebarButtons?: React.ReactNode;
sidebarHeader?: React.ReactNode;
sidebarPadding?: boolean;
@ -35,7 +38,8 @@ export interface PreviewModalProps {
export const PreviewModalContent: React.FC<PreviewModalProps> = ({
testId,
title,
sidebar,
size = 'full',
sidebar = '',
preview,
cancelLabel = 'Cancel',
okLabel = 'OK',
@ -43,6 +47,8 @@ export const PreviewModalContent: React.FC<PreviewModalProps> = ({
previewToolbar = true,
previewToolbarURLs,
selectedURL,
previewToolbarTabs,
defaultTab,
buttonsDisabled,
sidebarButtons,
sidebarHeader,
@ -68,11 +74,19 @@ export const PreviewModalContent: React.FC<PreviewModalProps> = ({
}
if (previewToolbar) {
let toolbarCenter = (<></>);
let toolbarLeft = (<></>);
if (previewToolbarURLs) {
toolbarCenter = (
<URLSelect defaultSelectedOption={selectedURL} options={previewToolbarURLs!} onSelect={onSelectURL ? onSelectURL : () => {}} />
toolbarLeft = (
<Select defaultSelectedOption={selectedURL} options={previewToolbarURLs!} onSelect={onSelectURL ? onSelectURL : () => {}} />
);
} else if (previewToolbarTabs) {
toolbarLeft = <TabView
border={false}
defaultSelected={defaultTab}
tabs={previewToolbarTabs}
width='wide'
onTabChange={onSelectURL}
/>;
}
const unSelectedIconColorClass = 'text-grey-500';
@ -103,13 +117,12 @@ export const PreviewModalContent: React.FC<PreviewModalProps> = ({
preview = (
<>
<div className='bg-grey-50 p-2 pl-3'>
<DesktopChromeHeader
toolbarCenter={toolbarCenter}
toolbarLeft={view === 'mobile' ? <></> : ''}
size='lg'
toolbarCenter={<></>}
toolbarLeft={toolbarLeft}
toolbarRight={toolbarRight}
/>
</div>
<div className='flex h-full grow items-center justify-center bg-grey-50 text-sm text-grey-400'>
{preview}
</div>
@ -139,9 +152,9 @@ export const PreviewModalContent: React.FC<PreviewModalProps> = ({
return (
<Modal
customFooter={(<></>)}
footer={false}
noPadding={true}
size='full'
size={size}
testId={testId}
title=''
>
@ -149,19 +162,19 @@ export const PreviewModalContent: React.FC<PreviewModalProps> = ({
<div className='flex grow flex-col'>
{preview}
</div>
<div className='flex h-full basis-[400px] flex-col gap-3 border-l border-grey-100'>
{sidebar &&
<div className='flex h-full basis-[400px] flex-col border-l border-grey-100'>
{sidebarHeader ? sidebarHeader : (
<div className='flex justify-between gap-3 px-7 pt-5'>
<>
<div className='flex max-h-[74px] items-start justify-between gap-3 px-7 py-5'>
<Heading className='mt-1' level={4}>{title}</Heading>
{sidebarButtons ? sidebarButtons : <ButtonGroup buttons={buttons} /> }
</>
</div>
)}
<div className={`grow ${sidebarPadding && 'p-7'} flex flex-col justify-between overflow-y-auto`}>
<div className={`grow ${sidebarPadding && 'p-7 pt-0'} flex flex-col justify-between overflow-y-auto`}>
{sidebar}
</div>
</div>
}
</div>
</Modal>
);

View File

@ -71,7 +71,7 @@ const Select: React.FC<SelectProps> = ({
let selectClasses = '';
if (!unstyled) {
selectClasses = clsx(
'h-10 w-full cursor-pointer appearance-none border-b py-2 outline-none',
'h-10 w-full cursor-pointer appearance-none border-b py-2 pr-5 outline-none',
!clearBg && 'bg-grey-75 px-[10px]',
error ? 'border-red' : 'border-grey-500 hover:border-grey-700 focus:border-black',
(title && !clearBg) && 'mt-2'

View File

@ -29,3 +29,10 @@ export const DefaultSelected: Story = {
defaultSelected: 'tab-2'
}
};
export const NoBorder: Story = {
args: {
tabs: tabs,
border: false
}
};

View File

@ -2,8 +2,12 @@ import React, {useState} from 'react';
import clsx from 'clsx';
export type Tab = {
id: string,
id: string;
title: string;
/**
* Optional, so you can just use the tabs to other views
*/
contents?: React.ReactNode;
}
@ -11,9 +15,17 @@ interface TabViewProps {
tabs: Tab[];
onTabChange?: (id: string) => void;
defaultSelected?: string;
border?:boolean;
width?: 'narrow' | 'normal' | 'wide';
}
const TabView: React.FC<TabViewProps> = ({tabs, onTabChange, defaultSelected}) => {
const TabView: React.FC<TabViewProps> = ({
tabs,
onTabChange,
defaultSelected,
border = true,
width = 'normal'
}) => {
if (tabs.length !== 0 && defaultSelected === undefined) {
defaultSelected = tabs[0].id;
}
@ -30,16 +42,26 @@ const TabView: React.FC<TabViewProps> = ({tabs, onTabChange, defaultSelected}) =
onTabChange?.(newTab);
};
const containerClasses = clsx(
'flex',
width === 'narrow' && 'gap-3',
width === 'normal' && 'gap-5',
width === 'wide' && 'gap-7',
border && 'border-b border-grey-300'
);
return (
<section>
<div className='flex gap-5 border-b border-grey-300' role='tablist'>
<div className={containerClasses} role='tablist'>
{tabs.map(tab => (
<button
key={tab.id}
aria-selected={selectedTab === tab.id}
className={clsx(
'-m-b-px cursor-pointer appearance-none border-b-[3px] py-1 text-sm transition-all after:invisible after:block after:h-px after:overflow-hidden after:font-bold after:text-transparent after:content-[attr(title)]',
selectedTab === tab.id ? 'border-black font-bold' : 'border-transparent hover:border-grey-500'
'-m-b-px cursor-pointer appearance-none py-1 text-sm transition-all after:invisible after:block after:h-px after:overflow-hidden after:font-bold after:text-transparent after:content-[attr(title)]',
border && 'border-b-[3px]',
selectedTab === tab.id && border ? 'border-black' : 'border-transparent hover:border-grey-500',
selectedTab === tab.id && 'font-bold'
)}
id={tab.id}
role='tab'
@ -49,11 +71,17 @@ const TabView: React.FC<TabViewProps> = ({tabs, onTabChange, defaultSelected}) =
>{tab.title}</button>
))}
</div>
{tabs.map(tab => (
{tabs.map((tab) => {
return (
<>
{tab.contents &&
<div key={tab.id} className={`${selectedTab === tab.id ? 'block' : 'hidden'}`} role='tabpanel'>
<div>{tab.contents}</div>
</div>
))}
}
</>
);
})}
</section>
);
};

View File

@ -1,4 +1,5 @@
import BrandSettings, {BrandSettingValues} from './designAndBranding/BrandSettings';
import ChangeThemeModal from './designAndBranding/ChangeThemeModal';
import ConfirmationModal from '../../../admin-x-ds/global/ConfirmationModal';
import NiceModal, {useModal} from '@ebay/nice-modal-react';
import React, {useContext, useEffect, useState} from 'react';
@ -9,7 +10,6 @@ import ThemeSettings from './designAndBranding/ThemeSettings';
import useForm from '../../../hooks/useForm';
import {CustomThemeSetting, Post, Setting, SettingValue, SiteData} from '../../../types/api';
import {PreviewModalContent} from '../../../admin-x-ds/global/PreviewModal';
import {SelectOption} from '../../../admin-x-ds/global/Select';
import {ServicesContext} from '../../providers/ServiceProvider';
import {SettingsContext} from '../../providers/SettingsProvider';
import {getSettingValues} from '../../../utils/helpers';
@ -20,7 +20,15 @@ const Sidebar: React.FC<{
themeSettingSections: Array<{id: string, title: string, settings: CustomThemeSetting[]}>
updateThemeSetting: (updated: CustomThemeSetting) => void
onTabChange: (id: string) => void
}> = ({brandSettings,updateBrandSetting,themeSettingSections,updateThemeSetting,onTabChange}) => {
onChangeTheme: () => void
}> = ({
brandSettings,
updateBrandSetting,
themeSettingSections,
updateThemeSetting,
onTabChange,
onChangeTheme
}) => {
const tabs: Tab[] = [
{
id: 'brand',
@ -40,7 +48,7 @@ const Sidebar: React.FC<{
<TabView tabs={tabs} onTabChange={onTabChange} />
</div>
<StickyFooter>
<button className='flex w-full cursor-pointer flex-col px-7' type='button' onClick={() => {}}>
<button className='m m-3 flex w-full cursor-pointer flex-col rounded p-4 transition-all hover:bg-grey-100' type='button' onClick={onChangeTheme}>
<strong>Change theme</strong>
<span className='text-sm text-grey-600'>Casper</span>
</button>
@ -63,7 +71,7 @@ const DesignModal: React.FC = () => {
const {settings, siteData, saveSettings} = useContext(SettingsContext);
const [themeSettings, setThemeSettings] = useState<Array<CustomThemeSetting>>([]);
const [latestPost, setLatestPost] = useState<Post | null>(null);
const [selectedUrl, setSelectedUrl] = useState(getHomepageUrl(siteData!));
const [selectedPreviewTab, setSelectedPreviewTab] = useState('home');
useEffect(() => {
api.customThemeSettings.browse().then((response) => {
@ -130,28 +138,48 @@ const DesignModal: React.FC = () => {
title: id === 'site-wide' ? 'Site wide' : (id === 'homepage' ? 'Homepage' : 'Post')
}));
const urlOptions: SelectOption[] = [
{value: getHomepageUrl(siteData!), label: 'Homepage'},
latestPost && {value: latestPost.url, label: 'Post'}
].filter((option): option is SelectOption => Boolean(option));
// const urlOptions: SelectOption[] = [
// {value: getHomepageUrl(siteData!), label: 'Homepage'},
// latestPost && {value: latestPost.url, label: 'Post'}
// ].filter((option): option is SelectOption => Boolean(option));
const onSelectURL = (url: string) => {
setSelectedUrl(url);
let previewTabs: Tab[] = [];
if (latestPost) {
previewTabs = [
{id: 'homepage', title: 'Homepage'},
{id: 'post', title: 'Post'}
];
}
const onSelectURL = (id: string) => {
if (previewTabs.length) {
setSelectedPreviewTab(id);
}
};
const onTabChange = (id: string) => {
if (id === 'post' && latestPost) {
setSelectedUrl(latestPost.url);
setSelectedPreviewTab('post');
} else {
setSelectedUrl(getHomepageUrl(siteData!));
setSelectedPreviewTab('home');
}
};
return <PreviewModalContent
buttonsDisabled={saveState === 'saving'}
cancelLabel='Close'
okLabel={saveState === 'saved' ? 'Saved' : (saveState === 'saving' ? 'Saving ...' : 'Save')}
preview={
const showThemeModal = () => {
NiceModal.show(ChangeThemeModal);
};
let selectedTabURL = getHomepageUrl(siteData!);
switch (selectedPreviewTab) {
case 'homepage':
selectedTabURL = getHomepageUrl(siteData!);
break;
case 'post':
selectedTabURL = latestPost!.url;
break;
}
const previewContent =
<ThemePreview
settings={{
description,
@ -161,19 +189,27 @@ const DesignModal: React.FC = () => {
coverImage,
themeSettings
}}
url={selectedUrl}
/>
}
previewToolbarURLs={urlOptions}
selectedURL={selectedUrl}
sidebar={<Sidebar
url={selectedTabURL}
/>;
const sidebarContent =
<Sidebar
brandSettings={{description, accentColor, icon, logo, coverImage}}
themeSettingSections={themeSettingSections}
updateBrandSetting={updateBrandSetting}
updateThemeSetting={updateThemeSetting}
onChangeTheme={showThemeModal}
onTabChange={onTabChange}
/>}
/>;
return <PreviewModalContent
buttonsDisabled={saveState === 'saving'}
defaultTab='homepage'
okLabel={saveState === 'saved' ? 'Saved' : (saveState === 'saving' ? 'Saving...' : 'Save and close')}
preview={previewContent}
previewToolbarTabs={previewTabs}
sidebar={sidebarContent}
sidebarPadding={false}
size='full'
testId='design-modal'
title='Design'
onCancel={() => {
@ -200,7 +236,7 @@ const DesignModal: React.FC = () => {
}}
onOk={async () => {
await handleSave();
// modal.remove();
modal.remove();
}}
onSelectURL={onSelectURL}
/>;

View File

@ -1,12 +1,14 @@
import DesignSetting from './DesignSetting';
import React from 'react';
import SettingSection from '../../../admin-x-ds/settings/SettingSection';
import Theme from './Theme';
const SiteSettings: React.FC = () => {
return (
<>
<SettingSection title="Site">
<DesignSetting />
<Theme />
</SettingSection>
</>
);

View File

@ -0,0 +1,21 @@
import Button from '../../../admin-x-ds/global/Button';
import ChangeThemeModal from './designAndBranding/ChangeThemeModal';
import NiceModal from '@ebay/nice-modal-react';
import React from 'react';
import SettingGroup from '../../../admin-x-ds/settings/SettingGroup';
const Theme: React.FC = () => {
return (
<SettingGroup
customButtons={<Button color='green' label='Manage themes' link onClick={() => {
NiceModal.show(ChangeThemeModal);
}}/>}
description="Change or upload themes"
navid='theme'
testId='theme'
title="Theme"
/>
);
};
export default Theme;

View File

@ -0,0 +1,15 @@
import Heading from '../../../../admin-x-ds/global/Heading';
import React from 'react';
const AdvancedThemeSettings: React.FC = () => {
return (
<div className='p-[8vmin] pt-5'>
<Heading>Installed themes</Heading>
<div className='mt-5'>
List of installed themes
</div>
</div>
);
};
export default AdvancedThemeSettings;

View File

@ -0,0 +1,114 @@
import AdvancedThemeSettings from './AdvancedThemeSettings';
import Button from '../../../../admin-x-ds/global/Button';
import ButtonGroup from '../../../../admin-x-ds/global/ButtonGroup';
import Modal from '../../../../admin-x-ds/global/Modal';
import NewThemePreview from './NewThemePreview';
import NiceModal, {useModal} from '@ebay/nice-modal-react';
import OfficialThemes from './OfficialThemes';
import {useState} from 'react';
const ChangeThemeModal = NiceModal.create(() => {
const [currentTab, setCurrentTab] = useState<'official-themes' | 'advanced'>('official-themes');
const [selectedTheme, setSelectedTheme] = useState('');
const modal = useModal();
const onSelectTheme = (theme: string) => {
setSelectedTheme(theme);
};
let content;
switch (currentTab) {
case 'official-themes':
if (selectedTheme) {
content = <NewThemePreview selectedTheme={selectedTheme} />;
} else {
content = <OfficialThemes onSelectTheme={onSelectTheme} />;
}
break;
case 'advanced':
content = <AdvancedThemeSettings />;
break;
}
let toolBar;
if (selectedTheme) {
toolBar =
<div className='sticky top-0 flex justify-between gap-3 bg-white p-5 px-7'>
<div className='flex w-[33%] items-center gap-2'>
<button
className={`text-sm`}
type="button"
onClick={() => {
setCurrentTab('official-themes');
setSelectedTheme('');
}}>
Official themes
</button>
&rarr;
<span className='text-sm font-bold'>{selectedTheme}</span>
</div>
<div className='flex w-[33%] justify-end gap-8'>
<ButtonGroup
buttons={[
{icon: 'laptop', link: true, size: 'sm'},
{icon: 'mobile', iconColorClass: 'text-grey-500', link: true, size: 'sm'}
]}
/>
<Button color='green' label={`Install ${selectedTheme}`} />
</div>
</div>;
} else {
toolBar =
<div className='sticky top-0 flex justify-between gap-3 bg-white p-5 px-7'>
<div className='flex gap-8'>
<button
className={`text-sm ${currentTab === 'official-themes' && 'font-bold'}`}
type="button"
onClick={() => {
setCurrentTab('official-themes');
setSelectedTheme('');
}}>
Official themes
</button>
<button
className={`text-sm ${currentTab === 'advanced' && 'font-bold'}`}
type="button"
onClick={() => {
setCurrentTab('advanced');
}}>
Installed
</button>
</div>
<ButtonGroup
buttons={[
{label: 'Upload theme', onClick: () => {
alert('Upload');
}},
{label: 'OK', color: 'black', className: 'min-w-[75px]', onClick: () => {
modal.remove();
}}
]}
/>
</div>;
}
return (
<Modal
cancelLabel=''
footer={false}
noPadding={true}
size='full'
title=''
>
<div className='flex h-full justify-between'>
<div className='grow'>
{toolBar}
{content}
</div>
</div>
</Modal>
);
});
export default ChangeThemeModal;

View File

@ -0,0 +1,15 @@
import React from 'react';
const NewThemePreview: React.FC<{
selectedTheme?: string;
}> = ({
selectedTheme
}) => {
return (
<div>
Preview {selectedTheme}
</div>
);
};
export default NewThemePreview;

View File

@ -0,0 +1,149 @@
import Heading from '../../../../admin-x-ds/global/Heading';
import React from 'react';
const OfficialThemes: React.FC<{
onSelectTheme?: (theme: string) => void;
}> = ({
onSelectTheme
}) => {
const officialThemes = [{
name: 'Casper',
category: 'Blog',
previewUrl: 'https://demo.ghost.io/',
ref: 'default',
image: 'assets/images/themes/Casper.png'
}, {
name: 'Headline',
category: 'News',
url: 'https://github.com/TryGhost/Headline',
previewUrl: 'https://headline.ghost.io',
ref: 'TryGhost/Headline',
image: 'assets/images/themes/Headline.png'
}, {
name: 'Edition',
category: 'Newsletter',
url: 'https://github.com/TryGhost/Edition',
previewUrl: 'https://edition.ghost.io/',
ref: 'TryGhost/Edition',
image: 'assets/images/themes/Edition.png'
}, {
name: 'Solo',
category: 'Blog',
url: 'https://github.com/TryGhost/Solo',
previewUrl: 'https://solo.ghost.io',
ref: 'TryGhost/Solo',
image: 'assets/images/themes/Solo.png'
}, {
name: 'Taste',
category: 'Blog',
url: 'https://github.com/TryGhost/Taste',
previewUrl: 'https://taste.ghost.io',
ref: 'TryGhost/Taste',
image: 'assets/images/themes/Taste.png'
}, {
name: 'Episode',
category: 'Podcast',
url: 'https://github.com/TryGhost/Episode',
previewUrl: 'https://episode.ghost.io',
ref: 'TryGhost/Episode',
image: 'assets/images/themes/Episode.png'
}, {
name: 'Digest',
category: 'Newsletter',
url: 'https://github.com/TryGhost/Digest',
previewUrl: 'https://digest.ghost.io/',
ref: 'TryGhost/Digest',
image: 'assets/images/themes/Digest.png'
}, {
name: 'Bulletin',
category: 'Newsletter',
url: 'https://github.com/TryGhost/Bulletin',
previewUrl: 'https://bulletin.ghost.io/',
ref: 'TryGhost/Bulletin',
image: 'assets/images/themes/Bulletin.png'
}, {
name: 'Alto',
category: 'Blog',
url: 'https://github.com/TryGhost/Alto',
previewUrl: 'https://alto.ghost.io',
ref: 'TryGhost/Alto',
image: 'assets/images/themes/Alto.png'
}, {
name: 'Dope',
category: 'Magazine',
url: 'https://github.com/TryGhost/Dope',
previewUrl: 'https://dope.ghost.io',
ref: 'TryGhost/Dope',
image: 'assets/images/themes/Dope.png'
}, {
name: 'Wave',
category: 'Podcast',
url: 'https://github.com/TryGhost/Wave',
previewUrl: 'https://wave.ghost.io',
ref: 'TryGhost/Wave',
image: 'assets/images/themes/Wave.png'
}, {
name: 'Edge',
category: 'Photography',
url: 'https://github.com/TryGhost/Edge',
previewUrl: 'https://edge.ghost.io',
ref: 'TryGhost/Edge',
image: 'assets/images/themes/Edge.png'
}, {
name: 'Dawn',
category: 'Newsletter',
url: 'https://github.com/TryGhost/Dawn',
previewUrl: 'https://dawn.ghost.io/',
ref: 'TryGhost/Dawn',
image: 'assets/images/themes/Dawn.png'
}, {
name: 'Ease',
category: 'Documentation',
url: 'https://github.com/TryGhost/Ease',
previewUrl: 'https://ease.ghost.io',
ref: 'TryGhost/Ease',
image: 'assets/images/themes/Ease.png'
}, {
name: 'Ruby',
category: 'Magazine',
url: 'https://github.com/TryGhost/Ruby',
previewUrl: 'https://ruby.ghost.io',
ref: 'TryGhost/Ruby',
image: 'assets/images/themes/Ruby.png'
}, {
name: 'London',
category: 'Photography',
url: 'https://github.com/TryGhost/London',
previewUrl: 'https://london.ghost.io',
ref: 'TryGhost/London',
image: 'assets/images/themes/London.png'
}, {
name: 'Journal',
category: 'Newsletter',
url: 'https://github.com/TryGhost/Journal',
previewUrl: 'https://journal.ghost.io/',
ref: 'TryGhost/Journal',
image: 'assets/images/themes/Journal.png'
}];
return (
<div className='p-[8vmin] pt-5'>
<Heading>Themes</Heading>
<div className='mt-6 grid grid-cols-3 gap-4'>
{officialThemes.map((theme) => {
return (
<div key={theme.name} className='flex cursor-pointer flex-col gap-3' onClick={() => {
onSelectTheme?.(theme.name);
}}>
{/* <img alt={theme.name} src={theme.image}/> */}
<div className='h-[420px] w-full bg-grey-100'></div>
<span>{theme.name}</span>
</div>
);
})}
</div>
</div>
);
};
export default OfficialThemes;

View File

@ -22,7 +22,7 @@ test.describe('Theme settings', async () => {
await modal.getByLabel('Site description').fill('new description');
await modal.getByRole('button', {name: 'Save'}).click();
await expect(modal.getByRole('button', {name: 'Saved'})).toHaveCount(1);
await expect(modal).not.toBeVisible();
expect(lastApiRequest.body).toEqual({
settings: [
@ -47,7 +47,7 @@ test.describe('Theme settings', async () => {
await modal.getByLabel('Navigation layout').selectOption('Logo in the middle');
await modal.getByRole('button', {name: 'Save'}).click();
await expect(modal.getByRole('button', {name: 'Saved'})).toHaveCount(1);
await expect(modal).not.toBeVisible();
expect(lastApiRequest.body).toMatchObject({
custom_theme_settings: [