mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-24 19:33:02 +03:00
Updated theme preview in AdminX
refs. https://github.com/TryGhost/Team/issues/3432
This commit is contained in:
parent
e680ddf14a
commit
010be7fd3d
@ -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
|
||||||
|
@ -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 = '';
|
||||||
|
|
||||||
|
@ -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}
|
||||||
|
@ -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>
|
||||||
|
@ -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;
|
Loading…
Reference in New Issue
Block a user