mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-24 03:14:03 +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
|
||||
}) => {
|
||||
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',
|
||||
sticky && 'sticky top-0',
|
||||
containerClassName
|
||||
|
@ -29,6 +29,7 @@ export interface ModalProps {
|
||||
backDrop?: boolean;
|
||||
backDropClick?: boolean;
|
||||
stickyFooter?: boolean;
|
||||
scrolling?: boolean;
|
||||
}
|
||||
|
||||
const Modal: React.FC<ModalProps> = ({
|
||||
@ -46,7 +47,8 @@ const Modal: React.FC<ModalProps> = ({
|
||||
children,
|
||||
backDrop = true,
|
||||
backDropClick = true,
|
||||
stickyFooter = false
|
||||
stickyFooter = false,
|
||||
scrolling = true
|
||||
}) => {
|
||||
const modal = useModal();
|
||||
|
||||
@ -75,10 +77,10 @@ const Modal: React.FC<ModalProps> = ({
|
||||
}
|
||||
|
||||
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'
|
||||
// !stickyFooter && ' overflow-hidden'
|
||||
'relative z-50 mx-auto flex max-h-[100%] w-full flex-col justify-between overflow-x-hidden rounded bg-white shadow-xl',
|
||||
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 = '';
|
||||
|
||||
|
@ -1,15 +1,13 @@
|
||||
import AdvancedThemeSettings from './theme/AdvancedThemeSettings';
|
||||
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 FileUpload from '../../../admin-x-ds/global/form/FileUpload';
|
||||
import Modal from '../../../admin-x-ds/global/modal/Modal';
|
||||
import NewThemePreview from './theme/ThemePreview';
|
||||
import NiceModal, {NiceModalHandler, useModal} from '@ebay/nice-modal-react';
|
||||
import OfficialThemes from './theme/OfficialThemes';
|
||||
import PageHeader from '../../../admin-x-ds/global/layout/PageHeader';
|
||||
import React, {useState} from 'react';
|
||||
import TabView from '../../../admin-x-ds/global/TabView';
|
||||
import ThemePreview from './theme/ThemePreview';
|
||||
import {OfficialTheme} from '../../../models/themes';
|
||||
import {Theme} from '../../../types/api';
|
||||
import {showToast} from '../../../admin-x-ds/global/Toast';
|
||||
@ -23,10 +21,11 @@ interface ThemeToolbarProps {
|
||||
modal: NiceModalHandler<Record<string, unknown>>;
|
||||
themes: Theme[];
|
||||
setThemes: (themes: Theme[]) => void;
|
||||
setPreviewMode: (mode: string) => void;
|
||||
previewMode: string;
|
||||
}
|
||||
|
||||
interface ThemeModalContentProps {
|
||||
selectedTheme: OfficialTheme|null;
|
||||
onSelectTheme: (theme: OfficialTheme|null) => void;
|
||||
currentTab: string;
|
||||
themes: Theme[];
|
||||
@ -34,112 +33,56 @@ interface ThemeModalContentProps {
|
||||
}
|
||||
|
||||
const ThemeToolbar: React.FC<ThemeToolbarProps> = ({
|
||||
selectedTheme,
|
||||
setCurrentTab,
|
||||
setSelectedTheme,
|
||||
modal,
|
||||
themes,
|
||||
setThemes
|
||||
}) => {
|
||||
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) {
|
||||
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} />;
|
||||
return <PageHeader containerClassName='bg-white' left={left} right={right} />;
|
||||
};
|
||||
|
||||
const ThemeModalContent: React.FC<ThemeModalContentProps> = ({
|
||||
currentTab,
|
||||
selectedTheme,
|
||||
onSelectTheme,
|
||||
themes,
|
||||
setThemes
|
||||
}) => {
|
||||
switch (currentTab) {
|
||||
case 'official':
|
||||
if (selectedTheme) {
|
||||
return (
|
||||
<NewThemePreview selectedTheme={selectedTheme} />
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<OfficialThemes onSelectTheme={onSelectTheme} />
|
||||
);
|
||||
}
|
||||
return (
|
||||
<OfficialThemes onSelectTheme={onSelectTheme} />
|
||||
);
|
||||
case 'installed':
|
||||
return (
|
||||
<AdvancedThemeSettings
|
||||
@ -154,35 +97,69 @@ const ThemeModalContent: React.FC<ThemeModalContentProps> = ({
|
||||
const ChangeThemeModal = NiceModal.create(() => {
|
||||
const [currentTab, setCurrentTab] = useState('official');
|
||||
const [selectedTheme, setSelectedTheme] = useState<OfficialTheme|null>(null);
|
||||
const [previewMode, setPreviewMode] = useState('desktop');
|
||||
|
||||
const modal = useModal();
|
||||
const {themes, setThemes} = useThemes();
|
||||
const api = useApi();
|
||||
|
||||
const onSelectTheme = (theme: OfficialTheme|null) => {
|
||||
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 (
|
||||
<Modal
|
||||
cancelLabel=''
|
||||
footer={false}
|
||||
noPadding={true}
|
||||
scrolling={currentTab === 'official' ? false : true}
|
||||
size='full'
|
||||
title=''
|
||||
>
|
||||
<div className='flex h-full justify-between'>
|
||||
<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
|
||||
modal={modal}
|
||||
previewMode={previewMode}
|
||||
selectedTheme={selectedTheme}
|
||||
setCurrentTab={setCurrentTab}
|
||||
setPreviewMode={setPreviewMode}
|
||||
setSelectedTheme={setSelectedTheme}
|
||||
setThemes={setThemes}
|
||||
themes={themes}
|
||||
/>
|
||||
<ThemeModalContent
|
||||
currentTab={currentTab}
|
||||
selectedTheme={selectedTheme}
|
||||
setThemes={setThemes}
|
||||
themes={themes}
|
||||
onSelectTheme={onSelectTheme}
|
||||
|
@ -130,7 +130,7 @@ const OfficialThemes: React.FC<{
|
||||
}];
|
||||
|
||||
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>
|
||||
<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) => {
|
||||
@ -139,10 +139,10 @@ const OfficialThemes: React.FC<{
|
||||
onSelectTheme?.(theme);
|
||||
}}>
|
||||
{/* <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
|
||||
alt="Headline Theme"
|
||||
className='w-full object-contain'
|
||||
className='h-full w-full object-contain'
|
||||
src={`${assetRoot}/${theme.image}`}
|
||||
/>
|
||||
</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';
|
||||
|
||||
const NewThemePreview: React.FC<{
|
||||
const ThemePreview: React.FC<{
|
||||
selectedTheme?: OfficialTheme;
|
||||
onBack: () => void;
|
||||
themeInstalled?: boolean;
|
||||
installButtonLabel?: string;
|
||||
onInstall?: () => void;
|
||||
}> = ({
|
||||
selectedTheme
|
||||
selectedTheme,
|
||||
onBack,
|
||||
themeInstalled,
|
||||
installButtonLabel,
|
||||
onInstall
|
||||
}) => {
|
||||
const [previewMode, setPreviewMode] = useState('desktop');
|
||||
|
||||
if (!selectedTheme) {
|
||||
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 (
|
||||
<div className='flex h-full grow flex-col'>
|
||||
<iframe className='h-full w-full'
|
||||
src={selectedTheme?.previewUrl} title='Theme preview' />
|
||||
<div className='absolute inset-0 z-[100]'>
|
||||
<PageHeader containerClassName='bg-grey-50 z-[100]' left={left} right={right} sticky={false} />
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
||||
export default NewThemePreview;
|
||||
export default ThemePreview;
|
Loading…
Reference in New Issue
Block a user