mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-11-27 18:52:14 +03:00
Added homepage/post URL selection in AdminX design settings
refs https://github.com/TryGhost/Team/issues/3354
This commit is contained in:
parent
ba2436e834
commit
345d90bbba
@ -20,6 +20,7 @@ export interface PreviewModalProps {
|
||||
buttonsDisabled?: boolean
|
||||
previewToolbar?: boolean;
|
||||
previewToolbarURLs?: SelectOption[];
|
||||
selectedURL?: string;
|
||||
sidebarButtons?: React.ReactNode;
|
||||
sidebarHeader?: React.ReactNode;
|
||||
sidebarPadding?: boolean;
|
||||
@ -39,8 +40,9 @@ export const PreviewModalContent: React.FC<PreviewModalProps> = ({
|
||||
cancelLabel = 'Cancel',
|
||||
okLabel = 'OK',
|
||||
okColor = 'black',
|
||||
previewToolbarURLs,
|
||||
previewToolbar = true,
|
||||
previewToolbarURLs,
|
||||
selectedURL,
|
||||
buttonsDisabled,
|
||||
sidebarButtons,
|
||||
sidebarHeader,
|
||||
@ -69,7 +71,7 @@ export const PreviewModalContent: React.FC<PreviewModalProps> = ({
|
||||
let toolbarCenter = (<></>);
|
||||
if (previewToolbarURLs) {
|
||||
toolbarCenter = (
|
||||
<URLSelect options={previewToolbarURLs!} onSelect={onSelectURL ? onSelectURL : () => {}} />
|
||||
<URLSelect defaultSelectedOption={selectedURL} options={previewToolbarURLs!} onSelect={onSelectURL ? onSelectURL : () => {}} />
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -9,7 +9,7 @@ export interface SelectOption {
|
||||
label: string;
|
||||
}
|
||||
|
||||
interface SelectProps {
|
||||
export interface SelectProps {
|
||||
title?: string;
|
||||
prompt?: string;
|
||||
options: SelectOption[];
|
||||
|
@ -9,10 +9,11 @@ export type Tab = {
|
||||
|
||||
interface TabViewProps {
|
||||
tabs: Tab[];
|
||||
onTabChange?: (id: string) => void;
|
||||
defaultSelected?: string;
|
||||
}
|
||||
|
||||
const TabView: React.FC<TabViewProps> = ({tabs, defaultSelected}) => {
|
||||
const TabView: React.FC<TabViewProps> = ({tabs, onTabChange, defaultSelected}) => {
|
||||
if (tabs.length !== 0 && defaultSelected === undefined) {
|
||||
defaultSelected = tabs[0].id;
|
||||
}
|
||||
@ -26,6 +27,7 @@ const TabView: React.FC<TabViewProps> = ({tabs, defaultSelected}) => {
|
||||
const handleTabChange = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
const newTab = e.currentTarget.id;
|
||||
setSelectedTab(newTab);
|
||||
onTabChange?.(newTab);
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -1,13 +1,8 @@
|
||||
import React from 'react';
|
||||
import Select, {SelectOption} from './Select';
|
||||
import Select, {SelectProps} from './Select';
|
||||
import clsx from 'clsx';
|
||||
|
||||
interface URLSelectProps {
|
||||
options: SelectOption[];
|
||||
onSelect: (value: string) => void;
|
||||
}
|
||||
|
||||
const URLSelect: React.FC<URLSelectProps> = ({options, onSelect}) => {
|
||||
const URLSelect: React.FC<SelectProps> = (props) => {
|
||||
const selectClasses = clsx(
|
||||
`!h-[unset] w-full appearance-none rounded-full border border-grey-100 bg-white px-3 py-1 text-sm`
|
||||
);
|
||||
@ -20,10 +15,9 @@ const URLSelect: React.FC<URLSelectProps> = ({options, onSelect}) => {
|
||||
return (
|
||||
<Select
|
||||
containerClassName={containerClasses}
|
||||
options={options}
|
||||
selectClassName={selectClasses}
|
||||
unstyled={true}
|
||||
onSelect={onSelect}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -4,10 +4,10 @@ import NiceModal, {useModal} from '@ebay/nice-modal-react';
|
||||
import React, {useContext, useEffect, useState} from 'react';
|
||||
import StickyFooter from '../../../admin-x-ds/global/StickyFooter';
|
||||
import TabView, {Tab} from '../../../admin-x-ds/global/TabView';
|
||||
import ThemePreview from './designAndBranding/ThemePreivew';
|
||||
import ThemePreview from './designAndBranding/ThemePreview';
|
||||
import ThemeSettings from './designAndBranding/ThemeSettings';
|
||||
import useForm from '../../../hooks/useForm';
|
||||
import {CustomThemeSetting, Setting, SettingValue} from '../../../types/api';
|
||||
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';
|
||||
@ -19,7 +19,8 @@ const Sidebar: React.FC<{
|
||||
updateBrandSetting: (key: string, value: SettingValue) => void
|
||||
themeSettingSections: Array<{id: string, title: string, settings: CustomThemeSetting[]}>
|
||||
updateThemeSetting: (updated: CustomThemeSetting) => void
|
||||
}> = ({brandSettings,updateBrandSetting,themeSettingSections,updateThemeSetting}) => {
|
||||
onTabChange: (id: string) => void
|
||||
}> = ({brandSettings,updateBrandSetting,themeSettingSections,updateThemeSetting,onTabChange}) => {
|
||||
const tabs: Tab[] = [
|
||||
{
|
||||
id: 'brand',
|
||||
@ -36,7 +37,7 @@ const Sidebar: React.FC<{
|
||||
return (
|
||||
<>
|
||||
<div className='p-7'>
|
||||
<TabView tabs={tabs} />
|
||||
<TabView tabs={tabs} onTabChange={onTabChange} />
|
||||
</div>
|
||||
<StickyFooter>
|
||||
<button className='flex w-full cursor-pointer flex-col px-7' type='button' onClick={() => {}}>
|
||||
@ -48,11 +49,21 @@ const Sidebar: React.FC<{
|
||||
);
|
||||
};
|
||||
|
||||
function getHomepageUrl(siteData: SiteData): string {
|
||||
const url = new URL(siteData.url);
|
||||
const subdir = url.pathname.endsWith('/') ? url.pathname : `${url.pathname}/`;
|
||||
|
||||
return `${url.origin}${subdir}`;
|
||||
}
|
||||
|
||||
const DesignModal: React.FC = () => {
|
||||
const modal = useModal();
|
||||
|
||||
const {api} = useContext(ServicesContext);
|
||||
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!));
|
||||
|
||||
useEffect(() => {
|
||||
api.customThemeSettings.browse().then((response) => {
|
||||
@ -60,7 +71,11 @@ const DesignModal: React.FC = () => {
|
||||
});
|
||||
}, [api]);
|
||||
|
||||
const {settings, saveSettings} = useContext(SettingsContext);
|
||||
useEffect(() => {
|
||||
api.latestPost.browse().then((response) => {
|
||||
setLatestPost(response.posts[0]);
|
||||
});
|
||||
}, [api]);
|
||||
|
||||
const {
|
||||
formState,
|
||||
@ -116,12 +131,20 @@ const DesignModal: React.FC = () => {
|
||||
}));
|
||||
|
||||
const urlOptions: SelectOption[] = [
|
||||
{value: 'homepage', label: 'Homepage'},
|
||||
{value: 'post', label: 'Post'}
|
||||
];
|
||||
{value: getHomepageUrl(siteData!), label: 'Homepage'},
|
||||
latestPost && {value: latestPost.url, label: 'Post'}
|
||||
].filter((option): option is SelectOption => Boolean(option));
|
||||
|
||||
const onSelectURL = (url: string) => {
|
||||
alert(url);
|
||||
setSelectedUrl(url);
|
||||
};
|
||||
|
||||
const onTabChange = (id: string) => {
|
||||
if (id === 'post' && latestPost) {
|
||||
setSelectedUrl(latestPost.url);
|
||||
} else {
|
||||
setSelectedUrl(getHomepageUrl(siteData!));
|
||||
}
|
||||
};
|
||||
|
||||
return <PreviewModalContent
|
||||
@ -138,14 +161,17 @@ const DesignModal: React.FC = () => {
|
||||
coverImage,
|
||||
themeSettings
|
||||
}}
|
||||
url={selectedUrl}
|
||||
/>
|
||||
}
|
||||
previewToolbarURLs={urlOptions}
|
||||
selectedURL={selectedUrl}
|
||||
sidebar={<Sidebar
|
||||
brandSettings={{description, accentColor, icon, logo, coverImage}}
|
||||
themeSettingSections={themeSettingSections}
|
||||
updateBrandSetting={updateBrandSetting}
|
||||
updateThemeSetting={updateThemeSetting}
|
||||
onTabChange={onTabChange}
|
||||
/>}
|
||||
sidebarPadding={false}
|
||||
testId='design-modal'
|
||||
|
@ -1,5 +1,4 @@
|
||||
import React, {useEffect, useRef} from 'react';
|
||||
import useSettingGroup from '../../../../hooks/useSettingGroup';
|
||||
import {CustomThemeSetting} from '../../../../types/api';
|
||||
|
||||
type BrandSettings = {
|
||||
@ -13,17 +12,7 @@ type BrandSettings = {
|
||||
|
||||
interface ThemePreviewProps {
|
||||
settings: BrandSettings
|
||||
}
|
||||
|
||||
function getApiUrl({siteUrl, path = ''}: {
|
||||
siteUrl: string;
|
||||
path?: string;
|
||||
}): string {
|
||||
const url = new URL(siteUrl);
|
||||
const subdir = url.pathname.endsWith('/') ? url.pathname : `${url.pathname}/`;
|
||||
const fullPath = `${subdir}${path.replace(/^\//, '')}`;
|
||||
|
||||
return `${url.origin}${fullPath}`;
|
||||
url: string
|
||||
}
|
||||
|
||||
function getPreviewData({
|
||||
@ -58,20 +47,16 @@ function getPreviewData({
|
||||
return params.toString();
|
||||
}
|
||||
|
||||
const ThemePreview: React.FC<ThemePreviewProps> = ({settings}) => {
|
||||
const ThemePreview: React.FC<ThemePreviewProps> = ({settings,url}) => {
|
||||
const iframeRef = useRef<HTMLIFrameElement>(null);
|
||||
const {siteData} = useSettingGroup();
|
||||
const apiEndpoint = getApiUrl({
|
||||
siteUrl: siteData?.url || '',
|
||||
path: ''
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!apiEndpoint) {
|
||||
if (!url) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch theme preview HTML
|
||||
fetch(apiEndpoint, {
|
||||
fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'text/html;charset=utf-8',
|
||||
@ -111,7 +96,7 @@ const ThemePreview: React.FC<ThemePreviewProps> = ({settings}) => {
|
||||
.catch(() => {
|
||||
// handle error in fetching data
|
||||
});
|
||||
}, [apiEndpoint, settings]);
|
||||
}, [url, settings]);
|
||||
return (
|
||||
<>
|
||||
<iframe
|
@ -56,6 +56,11 @@ export type SiteData = {
|
||||
version: string;
|
||||
};
|
||||
|
||||
export type Post = {
|
||||
id: string;
|
||||
url: string;
|
||||
};
|
||||
|
||||
type CustomThemeSettingData =
|
||||
{ type: 'text', value: string | null, default: string | null } |
|
||||
{ type: 'color', value: string, default: string } |
|
||||
|
@ -1,4 +1,4 @@
|
||||
import {CustomThemeSetting, Setting, SiteData, User, UserRole} from '../types/api';
|
||||
import {CustomThemeSetting, Post, Setting, SiteData, User, UserRole} from '../types/api';
|
||||
import {getGhostPaths} from './helpers';
|
||||
|
||||
interface Meta {
|
||||
@ -53,6 +53,20 @@ export interface CustomThemeSettingsResponseType {
|
||||
custom_theme_settings: CustomThemeSetting[];
|
||||
}
|
||||
|
||||
export interface PostsResponseType {
|
||||
meta: {
|
||||
pagination: {
|
||||
page: number
|
||||
limit: number
|
||||
pages: number
|
||||
total: number
|
||||
next: number | null
|
||||
prev: number | null
|
||||
}
|
||||
}
|
||||
posts: Post[];
|
||||
}
|
||||
|
||||
export interface SiteResponseType {
|
||||
site: SiteData;
|
||||
}
|
||||
@ -127,6 +141,9 @@ interface API {
|
||||
customThemeSettings: {
|
||||
browse: () => Promise<CustomThemeSettingsResponseType>
|
||||
edit: (newSettings: CustomThemeSetting[]) => Promise<CustomThemeSettingsResponseType>
|
||||
};
|
||||
latestPost: {
|
||||
browse: () => Promise<PostsResponseType>
|
||||
}
|
||||
}
|
||||
|
||||
@ -325,6 +342,13 @@ function setupGhostApi({ghostVersion}: GhostApiOptions): API {
|
||||
const data: CustomThemeSettingsResponseType = await response.json();
|
||||
return data;
|
||||
}
|
||||
},
|
||||
latestPost: {
|
||||
browse: async () => {
|
||||
const response = await fetcher('/posts/?filter=status%3Apublished&order=published_at%20DESC&limit=1&fields=id,url');
|
||||
const data: PostsResponseType = await response.json();
|
||||
return data;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user