Updated theme preview in AdminX

refs. https://github.com/TryGhost/Team/issues/3432
This commit is contained in:
Peter Zimon 2023-06-14 10:56:58 +02:00
parent e680ddf14a
commit 010be7fd3d
5 changed files with 155 additions and 108 deletions

View File

@ -28,7 +28,7 @@ const PageHeader: React.FC<PageHeaderProps> = ({
children children
}) => { }) => {
const containerClasses = clsx( const containerClasses = clsx(
'h-[74px] p-5 px-7', 'z-50 h-[74px] p-5 px-7',
!children && 'flex items-center justify-between gap-3', !children && 'flex items-center justify-between gap-3',
sticky && 'sticky top-0', sticky && 'sticky top-0',
containerClassName containerClassName

View File

@ -29,6 +29,7 @@ export interface ModalProps {
backDrop?: boolean; backDrop?: boolean;
backDropClick?: boolean; backDropClick?: boolean;
stickyFooter?: boolean; stickyFooter?: boolean;
scrolling?: boolean;
} }
const Modal: React.FC<ModalProps> = ({ const Modal: React.FC<ModalProps> = ({
@ -46,7 +47,8 @@ const Modal: React.FC<ModalProps> = ({
children, children,
backDrop = true, backDrop = true,
backDropClick = true, backDropClick = true,
stickyFooter = false stickyFooter = false,
scrolling = true
}) => { }) => {
const modal = useModal(); const modal = useModal();
@ -75,10 +77,10 @@ const Modal: React.FC<ModalProps> = ({
} }
let modalClasses = clsx( let modalClasses = clsx(
'relative z-50 mx-auto flex max-h-[100%] w-full flex-col justify-between overflow-y-auto overflow-x-hidden rounded bg-white shadow-xl' 'relative z-50 mx-auto flex max-h-[100%] w-full flex-col justify-between overflow-x-hidden rounded bg-white shadow-xl',
// !stickyFooter && ' overflow-hidden' scrolling ? 'overflow-y-auto' : 'overflow-y-hidden'
); );
let backdropClasses = clsx('fixed inset-0 z-40 h-[100vh] w-[100vw] overflow-y-scroll '); let backdropClasses = clsx('fixed inset-0 z-40 h-[100vh] w-[100vw]');
let padding = ''; let padding = '';

View File

@ -1,15 +1,13 @@
import AdvancedThemeSettings from './theme/AdvancedThemeSettings'; import AdvancedThemeSettings from './theme/AdvancedThemeSettings';
import Breadcrumbs from '../../../admin-x-ds/global/Breadcrumbs';
import Button from '../../../admin-x-ds/global/Button'; import Button from '../../../admin-x-ds/global/Button';
import ButtonGroup from '../../../admin-x-ds/global/ButtonGroup';
import FileUpload from '../../../admin-x-ds/global/form/FileUpload'; import FileUpload from '../../../admin-x-ds/global/form/FileUpload';
import Modal from '../../../admin-x-ds/global/modal/Modal'; import Modal from '../../../admin-x-ds/global/modal/Modal';
import NewThemePreview from './theme/ThemePreview';
import NiceModal, {NiceModalHandler, useModal} from '@ebay/nice-modal-react'; import NiceModal, {NiceModalHandler, useModal} from '@ebay/nice-modal-react';
import OfficialThemes from './theme/OfficialThemes'; import OfficialThemes from './theme/OfficialThemes';
import PageHeader from '../../../admin-x-ds/global/layout/PageHeader'; import PageHeader from '../../../admin-x-ds/global/layout/PageHeader';
import React, {useState} from 'react'; import React, {useState} from 'react';
import TabView from '../../../admin-x-ds/global/TabView'; import TabView from '../../../admin-x-ds/global/TabView';
import ThemePreview from './theme/ThemePreview';
import {OfficialTheme} from '../../../models/themes'; import {OfficialTheme} from '../../../models/themes';
import {Theme} from '../../../types/api'; import {Theme} from '../../../types/api';
import {showToast} from '../../../admin-x-ds/global/Toast'; import {showToast} from '../../../admin-x-ds/global/Toast';
@ -23,10 +21,11 @@ interface ThemeToolbarProps {
modal: NiceModalHandler<Record<string, unknown>>; modal: NiceModalHandler<Record<string, unknown>>;
themes: Theme[]; themes: Theme[];
setThemes: (themes: Theme[]) => void; setThemes: (themes: Theme[]) => void;
setPreviewMode: (mode: string) => void;
previewMode: string;
} }
interface ThemeModalContentProps { interface ThemeModalContentProps {
selectedTheme: OfficialTheme|null;
onSelectTheme: (theme: OfficialTheme|null) => void; onSelectTheme: (theme: OfficialTheme|null) => void;
currentTab: string; currentTab: string;
themes: Theme[]; themes: Theme[];
@ -34,112 +33,56 @@ interface ThemeModalContentProps {
} }
const ThemeToolbar: React.FC<ThemeToolbarProps> = ({ const ThemeToolbar: React.FC<ThemeToolbarProps> = ({
selectedTheme,
setCurrentTab, setCurrentTab,
setSelectedTheme,
modal, modal,
themes, themes,
setThemes setThemes
}) => { }) => {
const api = useApi(); const api = useApi();
const left =
<TabView
border={false}
tabs={[
{id: 'official', title: 'Official themes'},
{id: 'installed', title: 'Installed'}
]}
onTabChange={(id: string) => {
setCurrentTab(id);
}} />;
let left, right; const right =
<div className='flex items-center gap-3'>
<FileUpload id='theme-uplaod' onUpload={async (file: File) => {
const data = await api.themes.upload({file});
const uploadedTheme = data.themes[0];
setThemes([...themes, uploadedTheme]);
showToast({
message: `Theme uploaded - ${uploadedTheme.name}`
});
}}>Upload theme</FileUpload>
<Button
className='min-w-[75px]'
color='black'
label='OK'
onClick = {() => {
modal.remove();
}} />
</div>;
if (selectedTheme) { return <PageHeader containerClassName='bg-white' left={left} right={right} />;
const installedTheme = themes.find(theme => theme.name.toLowerCase() === selectedTheme.name.toLowerCase());
left =
<div className='flex items-center gap-2'>
<Breadcrumbs
items={[
{label: 'Official themes', onClick: () => {
setCurrentTab('official');
setSelectedTheme(null);
}},
{label: selectedTheme.name}
]}
/>
</div>;
right =
<div className='flex 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'
disabled={Boolean(installedTheme)}
label={installedTheme?.active ? 'Activated' : (installedTheme ? 'Installed' : `Install ${selectedTheme?.name}`)}
onClick={async () => {
const data = await api.themes.install(selectedTheme.ref);
const newlyInstalledTheme = data.themes[0];
setThemes([
...themes.map(theme => ({...theme, active: false})),
newlyInstalledTheme
]);
showToast({
message: `Theme installed - ${newlyInstalledTheme.name}`
});
setCurrentTab('installed');
}}
/>
</div>;
} else {
left =
<TabView
border={false}
tabs={[
{id: 'official', title: 'Official themes'},
{id: 'installed', title: 'Installed'}
]}
onTabChange={(id: string) => {
setCurrentTab(id);
}} />;
right =
<div className='flex items-center gap-3'>
<FileUpload id='theme-uplaod' onUpload={async (file: File) => {
const data = await api.themes.upload({file});
const uploadedTheme = data.themes[0];
setThemes([...themes, uploadedTheme]);
showToast({
message: `Theme uploaded - ${uploadedTheme.name}`
});
}}>Upload theme</FileUpload>
<Button
className='min-w-[75px]'
color='black'
label='OK'
onClick = {() => {
modal.remove();
}} />
</div>;
}
return <PageHeader containerClassName={selectedTheme! && 'bg-grey-50'} left={left} right={right} />;
}; };
const ThemeModalContent: React.FC<ThemeModalContentProps> = ({ const ThemeModalContent: React.FC<ThemeModalContentProps> = ({
currentTab, currentTab,
selectedTheme,
onSelectTheme, onSelectTheme,
themes, themes,
setThemes setThemes
}) => { }) => {
switch (currentTab) { switch (currentTab) {
case 'official': case 'official':
if (selectedTheme) { return (
return ( <OfficialThemes onSelectTheme={onSelectTheme} />
<NewThemePreview selectedTheme={selectedTheme} /> );
);
} else {
return (
<OfficialThemes onSelectTheme={onSelectTheme} />
);
}
case 'installed': case 'installed':
return ( return (
<AdvancedThemeSettings <AdvancedThemeSettings
@ -154,35 +97,69 @@ const ThemeModalContent: React.FC<ThemeModalContentProps> = ({
const ChangeThemeModal = NiceModal.create(() => { const ChangeThemeModal = NiceModal.create(() => {
const [currentTab, setCurrentTab] = useState('official'); const [currentTab, setCurrentTab] = useState('official');
const [selectedTheme, setSelectedTheme] = useState<OfficialTheme|null>(null); const [selectedTheme, setSelectedTheme] = useState<OfficialTheme|null>(null);
const [previewMode, setPreviewMode] = useState('desktop');
const modal = useModal(); const modal = useModal();
const {themes, setThemes} = useThemes(); const {themes, setThemes} = useThemes();
const api = useApi();
const onSelectTheme = (theme: OfficialTheme|null) => { const onSelectTheme = (theme: OfficialTheme|null) => {
setSelectedTheme(theme); setSelectedTheme(theme);
}; };
let installedTheme;
let onInstall;
if (selectedTheme) {
installedTheme = themes.find(theme => theme.name.toLowerCase() === selectedTheme!.name.toLowerCase());
onInstall = async () => {
const data = await api.themes.install(selectedTheme.ref);
const newlyInstalledTheme = data.themes[0];
setThemes([
...themes.map(theme => ({...theme, active: false})),
newlyInstalledTheme
]);
showToast({
message: `Theme installed - ${newlyInstalledTheme.name}`
});
setCurrentTab('installed');
};
}
return ( return (
<Modal <Modal
cancelLabel='' cancelLabel=''
footer={false} footer={false}
noPadding={true} noPadding={true}
scrolling={currentTab === 'official' ? false : true}
size='full' size='full'
title='' title=''
> >
<div className='flex h-full justify-between'> <div className='flex h-full justify-between'>
<div className='grow'> <div className='grow'>
{selectedTheme &&
<ThemePreview
installButtonLabel={
installedTheme?.active ? 'Activated' : (installedTheme ? 'Installed' : `Install ${selectedTheme?.name}`)
}
selectedTheme={selectedTheme}
themeInstalled={Boolean(installedTheme)}
onBack={() => {
setSelectedTheme(null);
}}
onInstall={onInstall} />
}
<ThemeToolbar <ThemeToolbar
modal={modal} modal={modal}
previewMode={previewMode}
selectedTheme={selectedTheme} selectedTheme={selectedTheme}
setCurrentTab={setCurrentTab} setCurrentTab={setCurrentTab}
setPreviewMode={setPreviewMode}
setSelectedTheme={setSelectedTheme} setSelectedTheme={setSelectedTheme}
setThemes={setThemes} setThemes={setThemes}
themes={themes} themes={themes}
/> />
<ThemeModalContent <ThemeModalContent
currentTab={currentTab} currentTab={currentTab}
selectedTheme={selectedTheme}
setThemes={setThemes} setThemes={setThemes}
themes={themes} themes={themes}
onSelectTheme={onSelectTheme} onSelectTheme={onSelectTheme}

View File

@ -130,7 +130,7 @@ const OfficialThemes: React.FC<{
}]; }];
return ( return (
<div className='p-[8vmin] pt-5'> <div className='h-[calc(100vh-74px-40px)] overflow-y-auto overflow-x-hidden p-[8vmin] pt-5'>
<Heading>Themes</Heading> <Heading>Themes</Heading>
<div className='mt-[6vmin] grid grid-cols-1 gap-[6vmin] sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4'> <div className='mt-[6vmin] grid grid-cols-1 gap-[6vmin] sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4'>
{officialThemes.map((theme) => { {officialThemes.map((theme) => {
@ -139,10 +139,10 @@ const OfficialThemes: React.FC<{
onSelectTheme?.(theme); onSelectTheme?.(theme);
}}> }}>
{/* <img alt={theme.name} src={`${assetRoot}/${theme.image}`}/> */} {/* <img alt={theme.name} src={`${assetRoot}/${theme.image}`}/> */}
<div className='h-[420px] w-full bg-grey-100 shadow-md transition-all duration-500 hover:scale-[1.05]'> <div className='w-full bg-grey-100 shadow-md transition-all duration-500 hover:scale-[1.05]'>
<img <img
alt="Headline Theme" alt="Headline Theme"
className='w-full object-contain' className='h-full w-full object-contain'
src={`${assetRoot}/${theme.image}`} src={`${assetRoot}/${theme.image}`}
/> />
</div> </div>

View File

@ -1,20 +1,88 @@
import React from 'react'; import Breadcrumbs from '../../../../admin-x-ds/global/Breadcrumbs';
import Button from '../../../../admin-x-ds/global/Button';
import ButtonGroup from '../../../../admin-x-ds/global/ButtonGroup';
import MobileChrome from '../../../../admin-x-ds/global/chrome/MobileChrome';
import PageHeader from '../../../../admin-x-ds/global/layout/PageHeader';
import React, {useState} from 'react';
import {OfficialTheme} from '../../../../models/themes'; import {OfficialTheme} from '../../../../models/themes';
const NewThemePreview: React.FC<{ const ThemePreview: React.FC<{
selectedTheme?: OfficialTheme; selectedTheme?: OfficialTheme;
onBack: () => void;
themeInstalled?: boolean;
installButtonLabel?: string;
onInstall?: () => void;
}> = ({ }> = ({
selectedTheme selectedTheme,
onBack,
themeInstalled,
installButtonLabel,
onInstall
}) => { }) => {
const [previewMode, setPreviewMode] = useState('desktop');
if (!selectedTheme) { if (!selectedTheme) {
return null; return null;
} }
const left =
<div className='flex items-center gap-2'>
<Breadcrumbs
items={[
{label: 'Official themes', onClick: onBack},
{label: selectedTheme.name}
]}
/>
</div>;
const right =
<div className='flex justify-end gap-8'>
<ButtonGroup
buttons={[
{
icon: 'laptop',
iconColorClass: (previewMode === 'desktop' ? 'text-black' : 'text-grey-500'),
link: true,
size: 'sm',
onClick: () => {
setPreviewMode('desktop');
}
},
{
icon: 'mobile',
iconColorClass: (previewMode === 'mobile' ? 'text-black' : 'text-grey-500'),
link: true,
size: 'sm',
onClick: () => {
setPreviewMode('mobile');
}
}
]}
/>
<Button
color='green'
disabled={themeInstalled}
label={installButtonLabel}
onClick={onInstall}
/>
</div>;
return ( return (
<div className='flex h-full grow flex-col'> <div className='absolute inset-0 z-[100]'>
<iframe className='h-full w-full' <PageHeader containerClassName='bg-grey-50 z-[100]' left={left} right={right} sticky={false} />
src={selectedTheme?.previewUrl} title='Theme preview' /> <div className='flex h-[calc(100%-74px)] grow flex-col items-center justify-center bg-grey-50'>
{previewMode === 'desktop' ?
<iframe className='h-full w-full'
src={selectedTheme?.previewUrl} title='Theme preview' />
:
<MobileChrome>
<iframe className='h-full w-full'
src={selectedTheme?.previewUrl} title='Theme preview' />
</MobileChrome>
}
</div>
</div> </div>
); );
}; };
export default NewThemePreview; export default ThemePreview;