Colocated AdminX types and helpers with API requests (#17629)

refs https://github.com/TryGhost/Product/issues/3349
This commit is contained in:
Jono M 2023-08-08 21:34:07 +01:00 committed by GitHub
parent 116d11b6ab
commit d8259fb4fe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
75 changed files with 577 additions and 583 deletions

View File

@ -6,9 +6,8 @@ import RoutingProvider from './components/providers/RoutingProvider';
import Settings from './components/Settings';
import Sidebar from './components/Sidebar';
import {GlobalDirtyStateProvider} from './hooks/useGlobalDirtyState';
import {OfficialTheme} from './models/themes';
import {OfficialTheme, ServicesProvider} from './components/providers/ServiceProvider';
import {QueryClient, QueryClientProvider} from '@tanstack/react-query';
import {ServicesProvider} from './components/providers/ServiceProvider';
import {Toaster} from 'react-hot-toast';
interface AppProps {

View File

@ -0,0 +1,30 @@
import {createQuery} from '../utils/apiRequests';
export type JSONValue = string|number|boolean|null|Date|JSONObject|JSONArray;
export interface JSONObject { [key: string]: JSONValue }
export interface JSONArray extends Array<string|number|boolean|Date|JSONObject|JSONValue> {}
export type Config = {
version: string;
environment: string;
editor: {
url: string
version: string
};
labs: Record<string, boolean>;
stripeDirect: boolean;
// Config is relatively fluid, so we only type used properties above and still support arbitrary property access when needed
[key: string]: JSONValue;
};
export interface ConfigResponseType {
config: Config;
}
const dataType = 'ConfigResponseType';
export const useBrowseConfig = createQuery<ConfigResponseType>({
dataType,
path: '/config/'
});

View File

@ -0,0 +1,44 @@
import {Setting} from './settings';
import {createMutation, createQuery} from '../utils/apiRequests';
type CustomThemeSettingData =
{ type: 'text', value: string | null, default: string | null } |
{ type: 'color', value: string, default: string } |
{ type: 'image', value: string | null } |
{ type: 'boolean', value: boolean, default: boolean } |
{
type: 'select',
value: string
default: string
options: string[]
};
export type CustomThemeSetting = CustomThemeSettingData & {
id: string
key: string
description?: string
// homepage and post are the only two groups we handle, but technically theme authors can put other things in package.json
group?: 'homepage' | 'post' | string
}
export interface CustomThemeSettingsResponseType {
custom_theme_settings: CustomThemeSetting[];
}
const dataType = 'CustomThemeSettingsResponseType';
export const useBrowseCustomThemeSettings = createQuery<CustomThemeSettingsResponseType>({
dataType,
path: '/custom_theme_settings/'
});
export const useEditCustomThemeSettings = createMutation<CustomThemeSettingsResponseType, Setting[]>({
method: 'PUT',
path: () => '/custom_theme_settings/',
body: settings => ({custom_theme_settings: settings}),
updateQueries: {
dataType,
update: newData => newData
}
});

View File

@ -1,4 +1,4 @@
import {createMutation} from '../apiRequests';
import {createMutation} from '../utils/apiRequests';
export interface ImagesResponseType {
images: {

View File

@ -1,4 +1,4 @@
import {Meta, createMutation, createQuery} from '../apiRequests';
import {Meta, createMutation, createQuery} from '../utils/apiRequests';
export interface UserInvite {
created_at: string;

View File

@ -1,5 +1,12 @@
import {Label} from '../../types/api';
import {Meta, createQuery} from '../apiRequests';
import {Meta, createQuery} from '../utils/apiRequests';
export type Label = {
id: string;
name: string;
slug: string;
created_at: string;
updated_at: string;
}
export interface LabelsResponseType {
meta?: Meta

View File

@ -1,5 +1,8 @@
import {Member} from '../../types/api';
import {Meta, createQuery} from '../apiRequests';
import {Meta, createQuery} from '../utils/apiRequests';
export type Member = {
id: string;
};
export interface MembersResponseType {
meta?: Meta

View File

@ -1,5 +1,43 @@
import {Meta, createMutation, createQuery} from '../apiRequests';
import {Newsletter} from '../../types/api';
import {Meta, createMutation, createQuery} from '../utils/apiRequests';
export type Newsletter = {
id: string;
uuid: string;
name: string;
description: string | null;
feedback_enabled: boolean;
slug: string;
sender_name: string | null;
sender_email: string | null;
sender_reply_to: string;
status: string;
visibility: string;
subscribe_on_signup: boolean;
sort_order: number;
header_image: string | null;
show_header_icon: boolean;
show_header_title: boolean;
title_font_category: string;
title_alignment: string;
show_feature_image: boolean;
body_font_category: string;
footer_content: string | null;
show_badge: boolean;
show_header_name: boolean;
show_post_title_section: boolean;
show_comment_cta: boolean;
show_subscription_details: boolean;
show_latest_posts: boolean;
background_color: string;
border_color: string | null;
title_color: string | null;
created_at: string;
updated_at: string;
count?: {
posts?: number;
active_members?: number;
}
}
export interface NewslettersResponseType {
meta?: Meta

View File

@ -0,0 +1,35 @@
import {Meta, createQuery} from '../utils/apiRequests';
export type Offer = {
id: string;
name: string;
code: string;
display_title: string;
display_description: string;
type: string;
cadence: string;
amount: number;
duration: string;
duration_in_months: number | null;
currency_restriction: boolean;
currency: string | null;
status: string;
redemption_count: number;
tier: {
id: string;
name: string;
}
}
export interface OffersResponseType {
meta?: Meta
offers: Offer[]
}
const dataType = 'OffersResponseType';
export const useBrowseOffers = createQuery<OffersResponseType>({
dataType,
path: '/offers/',
defaultSearchParams: {limit: 'all'}
});

View File

@ -1,5 +1,9 @@
import {Meta, createQuery} from '../apiRequests';
import {Post} from '../../types/api';
import {Meta, createQuery} from '../utils/apiRequests';
export type Post = {
id: string;
url: string;
};
export interface PostsResponseType {
meta?: Meta

View File

@ -0,0 +1,24 @@
import {Meta, createQuery} from '../utils/apiRequests';
export type UserRoleType = 'Owner' | 'Administrator' | 'Editor' | 'Author' | 'Contributor';
export type UserRole = {
id: string;
name: UserRoleType;
description: string;
created_at: string;
updated_at: string;
};
export interface RolesResponseType {
meta?: Meta;
roles: UserRole[];
}
const dataType = 'RolesResponseType';
export const useBrowseRoles = createQuery<RolesResponseType>({
dataType,
path: '/roles/',
defaultSearchParams: {limit: 'all'}
});

View File

@ -0,0 +1,85 @@
import {Config} from './config';
import {Meta, createMutation, createQuery} from '../utils/apiRequests';
// Types
export type SettingValue = string | boolean | null;
export type Setting = {
key: string;
value: SettingValue;
}
export type SettingsResponseMeta = Meta & { sent_email_verification?: boolean }
export interface SettingsResponseType {
meta?: SettingsResponseMeta;
settings: Setting[];
}
// Requests
const dataType = 'SettingsResponseType';
export const useBrowseSettings = createQuery<SettingsResponseType>({
dataType,
path: '/settings/',
defaultSearchParams: {
group: 'site,theme,private,members,portal,newsletter,email,amp,labs,slack,unsplash,views,firstpromoter,editor,comments,analytics,announcement,pintura'
}
});
export const useEditSettings = createMutation<SettingsResponseType, Setting[]>({
method: 'PUT',
path: () => '/settings/',
body: settings => ({settings: settings.map(({key, value}) => ({key, value}))}),
updateQueries: {
dataType,
update: newData => ({
...newData,
settings: newData.settings
})
}
});
export const useDeleteStripeSettings = createMutation<unknown, null>({
method: 'DELETE',
path: () => '/settings/stripe/connect/',
invalidateQueries: {dataType}
});
// Helpers
export function humanizeSettingKey(key: string) {
const allCaps = ['API', 'CTA', 'RSS'];
return key
.replace(/^[a-z]/, char => char.toUpperCase())
.replace(/_/g, ' ')
.replace(new RegExp(`\\b(${allCaps.join('|')})\\b`, 'ig'), match => match.toUpperCase());
}
export function getSettingValues<ValueType = SettingValue>(settings: Setting[] | null, keys: string[]): Array<ValueType | undefined> {
return keys.map(key => settings?.find(setting => setting.key === key)?.value) as ValueType[];
}
export function getSettingValue(settings: Setting[] | null | undefined, key: string): SettingValue {
if (!settings) {
return '';
}
const setting = settings.find(d => d.key === key);
return setting?.value || null;
}
export function checkStripeEnabled(settings: Setting[], config: Config) {
const hasSetting = (key: string) => settings.some(setting => setting.key === key && setting.value);
const hasDirectKeys = hasSetting('stripe_secret_key') && hasSetting('stripe_publishable_key');
const hasConnectKeys = hasSetting('stripe_connect_secret_key') && hasSetting('stripe_connect_publishable_key');
if (config.stripeDirect) {
return hasDirectKeys;
}
return hasConnectKeys || hasDirectKeys;
}

View File

@ -0,0 +1,49 @@
import {createQuery} from '../utils/apiRequests';
// Types
export type SiteData = {
title: string;
description: string;
logo: string;
icon: string;
accent_color: string;
url: string;
locale: string;
version: string;
};
export interface SiteResponseType {
site: SiteData;
}
// Requests
const dataType = 'SiteResponseType';
export const useBrowseSite = createQuery<SiteResponseType>({
dataType,
path: '/site/'
});
// Helpers
export function getHomepageUrl(siteData: SiteData): string {
const url = new URL(siteData.url);
const subdir = url.pathname.endsWith('/') ? url.pathname : `${url.pathname}/`;
return `${url.origin}${subdir}`;
}
export function getEmailDomain(siteData: SiteData): string {
const domain = new URL(siteData.url).hostname || '';
if (domain.startsWith('www.')) {
return domain.replace(/^(www)\.(?=[^/]*\..{2,5})/, '');
}
return domain;
}
export function fullEmailAddress(value: 'noreply' | string, siteData: SiteData) {
const emailDomain = getEmailDomain(siteData);
return value === 'noreply' ? `noreply@${emailDomain}` : value;
}

View File

@ -1,5 +1,35 @@
import {InstalledTheme, Theme} from '../../types/api';
import {createMutation, createQuery} from '../apiRequests';
import {createMutation, createQuery} from '../utils/apiRequests';
// Types
export type Theme = {
active: boolean;
name: string;
package: {
name?: string;
description?: string;
version?: string;
};
templates?: string[];
}
export type InstalledTheme = Theme & {
errors?: ThemeProblem<'error'>[];
warnings?: ThemeProblem<'warning'>[];
}
export type ThemeProblem<Level extends string = 'error' | 'warning'> = {
code: string
details: string
failures: Array<{
ref: string
message?: string
rule?: string
}>
fatal: boolean
level: Level
rule: string
}
export interface ThemesResponseType {
themes: Theme[];
@ -9,6 +39,8 @@ export interface ThemesInstallResponseType {
themes: InstalledTheme[];
}
// Requests
const dataType = 'ThemesResponseType';
export const useBrowseThemes = createQuery<ThemesResponseType>({
@ -85,3 +117,17 @@ export const useUploadTheme = createMutation<ThemesInstallResponseType, {file: F
})
}
});
// Helpers
export function isActiveTheme(theme: Theme): boolean {
return theme.active;
}
export function isDefaultTheme(theme: Theme): boolean {
return theme.name === 'casper';
}
export function isDeletableTheme(theme: Theme): boolean {
return !isDefaultTheme(theme) && !isActiveTheme(theme);
}

View File

@ -1,11 +1,32 @@
import {Meta, createMutation, createQuery} from '../apiRequests';
import {Tier} from '../../types/api';
import {Meta, createMutation, createQuery} from '../utils/apiRequests';
// Types
export type Tier = {
id: string;
name: string;
description: string | null;
slug: string;
active: boolean,
type: string;
welcome_page_url: string | null;
created_at: string;
updated_at: string;
visibility: string;
benefits: string[];
currency?: string;
monthly_price?: number;
yearly_price?: number;
trial_days: number;
}
export interface TiersResponseType {
meta?: Meta
tiers: Tier[]
}
// Requests
const dataType = 'TiersResponseType';
export const useBrowseTiers = createQuery<TiersResponseType>({
@ -39,3 +60,23 @@ export const useEditTier = createMutation<TiersResponseType, Tier>({
})
}
});
// Helpers
export function getPaidActiveTiers(tiers: Tier[]) {
return tiers.filter((tier) => {
return tier.type === 'paid' && tier.active;
});
}
export function getActiveTiers(tiers: Tier[]) {
return tiers.filter((tier) => {
return tier.active;
});
}
export function getArchivedTiers(tiers: Tier[]) {
return tiers.filter((tier) => {
return !tier.active;
});
}

View File

@ -1,5 +1,37 @@
import {Meta, createMutation, createQuery} from '../apiRequests';
import {User} from '../../types/api';
import {Meta, createMutation, createQuery} from '../utils/apiRequests';
import {UserRole} from './roles';
// Types
export type User = {
id: string;
name: string;
slug: string;
email: string;
profile_image: string;
cover_image: string|null;
bio: string;
website: string;
location: string;
facebook: string;
twitter: string;
accessibility: string|null;
status: string;
meta_title: string|null;
meta_description: string|null;
tour: string|null;
last_seen: string|null;
created_at: string;
updated_at: string;
comment_notifications: boolean;
free_member_signup_notification: boolean;
paid_subscription_canceled_notification: boolean;
paid_subscription_started_notification: boolean;
mention_notifications: boolean;
milestone_notifications: boolean;
roles: UserRole[];
url: string;
}
export interface UsersResponseType {
meta?: Meta;
@ -25,6 +57,8 @@ export interface DeleteUserResponse {
}
}
// Requests
const dataType = 'UsersResponseType';
const updateUsers = (newData: UsersResponseType, currentData: unknown) => ({
@ -95,3 +129,13 @@ export const useMakeOwner = createMutation<UsersResponseType, string>({
update: updateUsers
}
});
// Helpers
export function isOwnerUser(user: User) {
return user.roles.some(role => role.name === 'Owner');
}
export function isAdminUser(user: User) {
return user.roles.some(role => role.name === 'Administrator');
}

View File

@ -4,7 +4,7 @@ import SettingNavItem from '../admin-x-ds/settings/SettingNavItem';
import SettingNavSection from '../admin-x-ds/settings/SettingNavSection';
import TextField from '../admin-x-ds/global/form/TextField';
import useRouting from '../hooks/useRouting';
import {getSettingValues} from '../utils/helpers';
import {getSettingValues} from '../api/settings';
import {useGlobalData} from './providers/GlobalDataProvider';
import {useSearch} from './providers/ServiceProvider';

View File

@ -1,9 +1,8 @@
import {Config, Setting, SiteData, User} from '../../types/api';
import {Config, useBrowseConfig} from '../../api/config';
import {ReactNode, createContext, useContext} from 'react';
import {useBrowseConfig} from '../../utils/api/config';
import {useBrowseSettings} from '../../utils/api/settings';
import {useBrowseSite} from '../../utils/api/site';
import {useCurrentUser} from '../../utils/api/users';
import {Setting, useBrowseSettings} from '../../api/settings';
import {SiteData, useBrowseSite} from '../../api/site';
import {User, useCurrentUser} from '../../api/users';
interface GlobalData {
settings: Setting[]

View File

@ -1,6 +1,14 @@
import React, {createContext, useContext} from 'react';
import useSearchService, {SearchService} from '../../utils/search';
import {OfficialTheme} from '../../models/themes';
export type OfficialTheme = {
name: string;
category: string;
previewUrl: string;
ref: string;
image: string;
url?: string;
};
interface ServicesContextProps {
ghostVersion: string

View File

@ -7,7 +7,7 @@ import SettingGroup from '../../../admin-x-ds/settings/SettingGroup';
import TabView from '../../../admin-x-ds/global/TabView';
import useSettingGroup from '../../../hooks/useSettingGroup';
import {ReactCodeMirrorRef} from '@uiw/react-codemirror';
import {getSettingValues} from '../../../utils/helpers';
import {getSettingValues} from '../../../api/settings';
import {html} from '@codemirror/lang-html';
const CodeInjection: React.FC<{ keywords: string[] }> = ({keywords}) => {

View File

@ -5,10 +5,11 @@ import SettingGroup from '../../../admin-x-ds/settings/SettingGroup';
import SettingGroupContent from '../../../admin-x-ds/settings/SettingGroupContent';
import useSettingGroup from '../../../hooks/useSettingGroup';
import {GroupBase, MultiValue} from 'react-select';
import {getOptionLabel, getSettingValues} from '../../../utils/helpers';
import {useBrowseLabels} from '../../../utils/api/labels';
import {useBrowseOffers} from '../../../utils/api/offers';
import {useBrowseTiers} from '../../../utils/api/tiers';
import {getOptionLabel} from '../../../utils/helpers';
import {getSettingValues} from '../../../api/settings';
import {useBrowseLabels} from '../../../api/labels';
import {useBrowseOffers} from '../../../api/offers';
import {useBrowseTiers} from '../../../api/tiers';
type RefipientValueArgs = {
defaultEmailRecipients: string;

View File

@ -4,7 +4,7 @@ import MailGun from './Mailgun';
import Newsletters from './Newsletters';
import React from 'react';
import SettingSection from '../../../admin-x-ds/settings/SettingSection';
import {getSettingValues} from '../../../utils/helpers';
import {getSettingValues} from '../../../api/settings';
import {useGlobalData} from '../../providers/GlobalDataProvider';
const searchKeywords = {

View File

@ -3,9 +3,7 @@ import React from 'react';
import SettingGroup from '../../../admin-x-ds/settings/SettingGroup';
import SettingGroupContent from '../../../admin-x-ds/settings/SettingGroupContent';
import Toggle from '../../../admin-x-ds/global/form/Toggle';
import {Setting} from '../../../types/api';
import {getSettingValues} from '../../../utils/helpers';
import {useEditSettings} from '../../../utils/api/settings';
import {Setting, getSettingValues, useEditSettings} from '../../../api/settings';
import {useGlobalData} from '../../providers/GlobalDataProvider';
const EnableNewsletters: React.FC<{ keywords: string[] }> = ({keywords}) => {

View File

@ -6,7 +6,7 @@ import SettingGroup from '../../../admin-x-ds/settings/SettingGroup';
import SettingGroupContent from '../../../admin-x-ds/settings/SettingGroupContent';
import TextField from '../../../admin-x-ds/global/form/TextField';
import useSettingGroup from '../../../hooks/useSettingGroup';
import {getSettingValues} from '../../../utils/helpers';
import {getSettingValues} from '../../../api/settings';
const MAILGUN_REGIONS = [
{label: '🇺🇸 US', value: 'https://api.mailgun.net/v3'},

View File

@ -4,7 +4,7 @@ import React, {useState} from 'react';
import SettingGroup from '../../../admin-x-ds/settings/SettingGroup';
import TabView from '../../../admin-x-ds/global/TabView';
import useRouting from '../../../hooks/useRouting';
import {useBrowseNewsletters} from '../../../utils/api/newsletters';
import {useBrowseNewsletters} from '../../../api/newsletters';
const Newsletters: React.FC<{ keywords: string[] }> = ({keywords}) => {
const {updateRoute} = useRouting();

View File

@ -10,8 +10,8 @@ import useForm from '../../../../hooks/useForm';
import useRouting from '../../../../hooks/useRouting';
import {showToast} from '../../../../admin-x-ds/global/Toast';
import {toast} from 'react-hot-toast';
import {useAddNewsletter} from '../../../../utils/api/newsletters';
import {useBrowseMembers} from '../../../../utils/api/members';
import {useAddNewsletter} from '../../../../api/newsletters';
import {useBrowseMembers} from '../../../../api/members';
interface AddNewsletterModalProps {}

View File

@ -18,13 +18,13 @@ import Toggle from '../../../../admin-x-ds/global/form/Toggle';
import ToggleGroup from '../../../../admin-x-ds/global/form/ToggleGroup';
import useForm from '../../../../hooks/useForm';
import validator from 'validator';
import {Newsletter} from '../../../../types/api';
import {Newsletter, useEditNewsletter} from '../../../../api/newsletters';
import {PreviewModalContent} from '../../../../admin-x-ds/global/modal/PreviewModal';
import {fullEmailAddress, getSettingValues} from '../../../../utils/helpers';
import {getImageUrl, useUploadImage} from '../../../../utils/api/images';
import {fullEmailAddress} from '../../../../api/site';
import {getImageUrl, useUploadImage} from '../../../../api/images';
import {getSettingValues} from '../../../../api/settings';
import {showToast} from '../../../../admin-x-ds/global/Toast';
import {toast} from 'react-hot-toast';
import {useEditNewsletter} from '../../../../utils/api/newsletters';
import {useGlobalData} from '../../../providers/GlobalDataProvider';
interface NewsletterDetailModalProps {

View File

@ -6,8 +6,9 @@ import LatestPosts3 from '../../../../assets/images/latest-posts-3.png';
import React from 'react';
import clsx from 'clsx';
import {ReactComponent as GhostOrb} from '../../../../admin-x-ds/assets/images/ghost-orb.svg';
import {Newsletter} from '../../../../types/api';
import {fullEmailAddress, getSettingValues} from '../../../../utils/helpers';
import {Newsletter} from '../../../../api/newsletters';
import {fullEmailAddress} from '../../../../api/site';
import {getSettingValues} from '../../../../api/settings';
import {useGlobalData} from '../../../providers/GlobalDataProvider';
const NewsletterPreview: React.FC<{newsletter: Newsletter}> = ({newsletter}) => {

View File

@ -7,8 +7,7 @@ import React from 'react';
import Table from '../../../../admin-x-ds/global/Table';
import TableCell from '../../../../admin-x-ds/global/TableCell';
import TableRow from '../../../../admin-x-ds/global/TableRow';
import {Newsletter} from '../../../../types/api';
import {useEditNewsletter} from '../../../../utils/api/newsletters';
import {Newsletter, useEditNewsletter} from '../../../../api/newsletters';
interface NewslettersListProps {
newsletters: Newsletter[]

View File

@ -5,8 +5,8 @@ import SettingGroupContent from '../../../admin-x-ds/settings/SettingGroupConten
import TextField from '../../../admin-x-ds/global/form/TextField';
import useSettingGroup from '../../../hooks/useSettingGroup';
import {ReactComponent as FacebookLogo} from '../../../admin-x-ds/assets/images/facebook-logo.svg';
import {getImageUrl, useUploadImage} from '../../../utils/api/images';
import {getSettingValues} from '../../../utils/helpers';
import {getImageUrl, useUploadImage} from '../../../api/images';
import {getSettingValues} from '../../../api/settings';
const Facebook: React.FC<{ keywords: string[] }> = ({keywords}) => {
const {

View File

@ -5,8 +5,8 @@ import TextField from '../../../admin-x-ds/global/form/TextField';
import useRouting from '../../../hooks/useRouting';
import validator from 'validator';
import {showToast} from '../../../admin-x-ds/global/Toast';
import {useAddInvite} from '../../../utils/api/invites';
import {useBrowseRoles} from '../../../utils/api/roles';
import {useAddInvite} from '../../../api/invites';
import {useBrowseRoles} from '../../../api/roles';
import {useEffect, useRef, useState} from 'react';
type RoleType = 'administrator' | 'editor' | 'author' | 'contributor';

View File

@ -6,7 +6,7 @@ import SettingGroupContent from '../../../admin-x-ds/settings/SettingGroupConten
import TextField from '../../../admin-x-ds/global/form/TextField';
import Toggle from '../../../admin-x-ds/global/form/Toggle';
import useSettingGroup from '../../../hooks/useSettingGroup';
import {getSettingValues} from '../../../utils/helpers';
import {getSettingValues} from '../../../api/settings';
const LockSite: React.FC<{ keywords: string[] }> = ({keywords}) => {
const {

View File

@ -6,7 +6,7 @@ import TextField from '../../../admin-x-ds/global/form/TextField';
import useSettingGroup from '../../../hooks/useSettingGroup';
import {ReactComponent as GoogleLogo} from '../../../admin-x-ds/assets/images/google-logo.svg';
import {ReactComponent as MagnifyingGlass} from '../../../admin-x-ds/assets/icons/magnifying-glass.svg';
import {getSettingValues} from '../../../utils/helpers';
import {getSettingValues} from '../../../api/settings';
interface SearchEnginePreviewProps {
title: string;

View File

@ -3,7 +3,7 @@ import SettingGroup from '../../../admin-x-ds/settings/SettingGroup';
import SettingGroupContent from '../../../admin-x-ds/settings/SettingGroupContent';
import TextField from '../../../admin-x-ds/global/form/TextField';
import useSettingGroup from '../../../hooks/useSettingGroup';
import {getSettingValues} from '../../../utils/helpers';
import {getSettingValues} from '../../../api/settings';
const PublicationLanguage: React.FC<{ keywords: string[] }> = ({keywords}) => {
const {

View File

@ -4,7 +4,7 @@ import SettingGroupContent from '../../../admin-x-ds/settings/SettingGroupConten
import TextField from '../../../admin-x-ds/global/form/TextField';
import useSettingGroup from '../../../hooks/useSettingGroup';
import validator from 'validator';
import {getSettingValues} from '../../../utils/helpers';
import {getSettingValues} from '../../../api/settings';
function validateFacebookUrl(newUrl: string) {
const errMessage = 'The URL must be in a format like https://www.facebook.com/yourPage';

View File

@ -4,7 +4,8 @@ import SettingGroup from '../../../admin-x-ds/settings/SettingGroup';
import SettingGroupContent from '../../../admin-x-ds/settings/SettingGroupContent';
import timezoneData from '@tryghost/timezone-data';
import useSettingGroup from '../../../hooks/useSettingGroup';
import {getLocalTime, getSettingValues} from '../../../utils/helpers';
import {getLocalTime} from '../../../utils/helpers';
import {getSettingValues} from '../../../api/settings';
interface TimezoneDataDropdownOption {
name: string;

View File

@ -3,7 +3,7 @@ import SettingGroup from '../../../admin-x-ds/settings/SettingGroup';
import SettingGroupContent from '../../../admin-x-ds/settings/SettingGroupContent';
import TextField from '../../../admin-x-ds/global/form/TextField';
import useSettingGroup from '../../../hooks/useSettingGroup';
import {getSettingValues} from '../../../utils/helpers';
import {getSettingValues} from '../../../api/settings';
const TitleAndDescription: React.FC<{ keywords: string[] }> = ({keywords}) => {
const {

View File

@ -5,8 +5,8 @@ import SettingGroupContent from '../../../admin-x-ds/settings/SettingGroupConten
import TextField from '../../../admin-x-ds/global/form/TextField';
import useSettingGroup from '../../../hooks/useSettingGroup';
import {ReactComponent as TwitterLogo} from '../../../admin-x-ds/assets/images/twitter-logo.svg';
import {getImageUrl, useUploadImage} from '../../../utils/api/images';
import {getSettingValues} from '../../../utils/helpers';
import {getImageUrl, useUploadImage} from '../../../api/images';
import {getSettingValues} from '../../../api/settings';
const Twitter: React.FC<{ keywords: string[] }> = ({keywords}) => {
const {

View File

@ -14,13 +14,11 @@ import TextField from '../../../admin-x-ds/global/form/TextField';
import Toggle from '../../../admin-x-ds/global/form/Toggle';
import useStaffUsers from '../../../hooks/useStaffUsers';
import validator from 'validator';
import {User} from '../../../types/api';
import {getImageUrl, useUploadImage} from '../../../utils/api/images';
import {isAdminUser, isOwnerUser} from '../../../utils/helpers';
import {User, isAdminUser, isOwnerUser, useDeleteUser, useEditUser, useMakeOwner, useUpdatePassword} from '../../../api/users';
import {getImageUrl, useUploadImage} from '../../../api/images';
import {showToast} from '../../../admin-x-ds/global/Toast';
import {toast} from 'react-hot-toast';
import {useBrowseRoles} from '../../../utils/api/roles';
import {useDeleteUser, useEditUser, useMakeOwner, useUpdatePassword} from '../../../utils/api/users';
import {useBrowseRoles} from '../../../api/roles';
interface CustomHeadingProps {
children?: React.ReactNode;

View File

@ -10,8 +10,8 @@ import TabView from '../../../admin-x-ds/global/TabView';
import UserDetailModal from './UserDetailModal';
import useRouting from '../../../hooks/useRouting';
import useStaffUsers from '../../../hooks/useStaffUsers';
import {User} from '../../../types/api';
import {UserInvite, useAddInvite, useDeleteInvite} from '../../../utils/api/invites';
import {User} from '../../../api/users';
import {UserInvite, useAddInvite, useDeleteInvite} from '../../../api/invites';
import {generateAvatarColor, getInitials} from '../../../utils/helpers';
import {showToast} from '../../../admin-x-ds/global/Toast';

View File

@ -5,8 +5,9 @@ import SettingGroup from '../../../admin-x-ds/settings/SettingGroup';
import SettingGroupContent from '../../../admin-x-ds/settings/SettingGroupContent';
import useSettingGroup from '../../../hooks/useSettingGroup';
import {GroupBase, MultiValue} from 'react-select';
import {getOptionLabel, getSettingValues} from '../../../utils/helpers';
import {useBrowseTiers} from '../../../utils/api/tiers';
import {getOptionLabel} from '../../../utils/helpers';
import {getSettingValues} from '../../../api/settings';
import {useBrowseTiers} from '../../../api/tiers';
const MEMBERS_SIGNUP_ACCESS_OPTIONS = [
{value: 'all', label: 'Anyone can sign up'},

View File

@ -4,7 +4,7 @@ import SettingGroup from '../../../admin-x-ds/settings/SettingGroup';
import SettingGroupContent from '../../../admin-x-ds/settings/SettingGroupContent';
import Toggle from '../../../admin-x-ds/global/form/Toggle';
import useSettingGroup from '../../../hooks/useSettingGroup';
import {getSettingValues} from '../../../utils/helpers';
import {getSettingValues} from '../../../api/settings';
const Analytics: React.FC<{ keywords: string[] }> = ({keywords}) => {
const {

View File

@ -4,9 +4,8 @@ import StripeButton from '../../../admin-x-ds/settings/StripeButton';
import TabView from '../../../admin-x-ds/global/TabView';
import TiersList from './tiers/TiersList';
import useRouting from '../../../hooks/useRouting';
import {Tier} from '../../../types/api';
import {checkStripeEnabled, getActiveTiers, getArchivedTiers} from '../../../utils/helpers';
import {useBrowseTiers} from '../../../utils/api/tiers';
import {Tier, getActiveTiers, getArchivedTiers, useBrowseTiers} from '../../../api/tiers';
import {checkStripeEnabled} from '../../../api/settings';
import {useGlobalData} from '../../providers/GlobalDataProvider';
const Tiers: React.FC<{ keywords: string[] }> = ({keywords}) => {

View File

@ -1,8 +1,8 @@
import Form from '../../../../admin-x-ds/global/form/Form';
import React, {FocusEventHandler, useState} from 'react';
import TextField from '../../../../admin-x-ds/global/form/TextField';
import {Setting, SettingValue} from '../../../../types/api';
import {fullEmailAddress, getEmailDomain, getSettingValues} from '../../../../utils/helpers';
import {Setting, SettingValue, getSettingValues} from '../../../../api/settings';
import {fullEmailAddress, getEmailDomain} from '../../../../api/site';
import {useGlobalData} from '../../../providers/GlobalDataProvider';
const AccountPage: React.FC<{

View File

@ -1,21 +1,19 @@
import Form from '../../../../admin-x-ds/global/form/Form';
import Heading from '../../../../admin-x-ds/global/Heading';
import Icon from '../../../../admin-x-ds/global/Icon';
import ImageUpload from '../../../../admin-x-ds/global/form/ImageUpload';
import React, {useState} from 'react';
import Select from '../../../../admin-x-ds/global/form/Select';
import TextField from '../../../../admin-x-ds/global/form/TextField';
import Toggle from '../../../../admin-x-ds/global/form/Toggle';
import {Setting, SettingValue} from '../../../../types/api';
import {getSettingValues} from '../../../../utils/helpers';
import Heading from '../../../../admin-x-ds/global/Heading';
import Icon from '../../../../admin-x-ds/global/Icon';
import ImageUpload from '../../../../admin-x-ds/global/form/ImageUpload';
import clsx from 'clsx';
import {ReactComponent as PortalIcon1} from '../../../../assets/icons/portal-icon-1.svg';
import {ReactComponent as PortalIcon2} from '../../../../assets/icons/portal-icon-2.svg';
import {ReactComponent as PortalIcon3} from '../../../../assets/icons/portal-icon-3.svg';
import {ReactComponent as PortalIcon4} from '../../../../assets/icons/portal-icon-4.svg';
import {ReactComponent as PortalIcon5} from '../../../../assets/icons/portal-icon-5.svg';
import {getImageUrl, useUploadImage} from '../../../../utils/api/images';
import {Setting, SettingValue, getSettingValues} from '../../../../api/settings';
import {getImageUrl, useUploadImage} from '../../../../api/images';
const defaultButtonIcons = [
{

View File

@ -1,7 +1,8 @@
import React, {useEffect, useRef, useState} from 'react';
import useSettingGroup from '../../../../hooks/useSettingGroup';
import {Setting, SiteData, Tier} from '../../../../types/api';
import {getSettingValue} from '../../../../utils/helpers';
import {Setting, getSettingValue} from '../../../../api/settings';
import {SiteData} from '../../../../api/site';
import {Tier} from '../../../../api/tiers';
type PortalFrameProps = {
settings: Setting[];

View File

@ -5,8 +5,8 @@ import ModalPage from '../../../../admin-x-ds/global/modal/ModalPage';
import React, {useEffect, useState} from 'react';
import Select from '../../../../admin-x-ds/global/form/Select';
import TextField from '../../../../admin-x-ds/global/form/TextField';
import {getHomepageUrl, getPaidActiveTiers} from '../../../../utils/helpers';
import {useBrowseTiers} from '../../../../utils/api/tiers';
import {getHomepageUrl} from '../../../../api/site';
import {getPaidActiveTiers, useBrowseTiers} from '../../../../api/tiers';
import {useGlobalData} from '../../../providers/GlobalDataProvider';
interface PortalLinkPrefs {

View File

@ -9,10 +9,9 @@ import TabView, {Tab} from '../../../../admin-x-ds/global/TabView';
import useForm, {Dirtyable} from '../../../../hooks/useForm';
import useRouting from '../../../../hooks/useRouting';
import {PreviewModalContent} from '../../../../admin-x-ds/global/modal/PreviewModal';
import {Setting, SettingValue, Tier} from '../../../../types/api';
import {fullEmailAddress, getPaidActiveTiers} from '../../../../utils/helpers';
import {useBrowseTiers, useEditTier} from '../../../../utils/api/tiers';
import {useEditSettings} from '../../../../utils/api/settings';
import {Setting, SettingValue, useEditSettings} from '../../../../api/settings';
import {Tier, getPaidActiveTiers, useBrowseTiers, useEditTier} from '../../../../api/tiers';
import {fullEmailAddress} from '../../../../api/site';
import {useGlobalData} from '../../../providers/GlobalDataProvider';
const Sidebar: React.FC<{

View File

@ -1,7 +1,8 @@
import PortalFrame from './PortalFrame';
import PortalLinks from './PortalLinks';
import React from 'react';
import {Setting, Tier} from '../../../../types/api';
import {Setting} from '../../../../api/settings';
import {Tier} from '../../../../api/tiers';
interface PortalPreviewProps {
selectedTab: string;
@ -39,4 +40,4 @@ const PortalPreview: React.FC<PortalPreviewProps> = ({
return tabContents;
};
export default PortalPreview;
export default PortalPreview;

View File

@ -4,8 +4,8 @@ import HtmlField from '../../../../admin-x-ds/global/form/HtmlField';
import React, {useEffect, useMemo} from 'react';
import Toggle from '../../../../admin-x-ds/global/form/Toggle';
import {CheckboxProps} from '../../../../admin-x-ds/global/form/Checkbox';
import {Setting, SettingValue, Tier} from '../../../../types/api';
import {checkStripeEnabled, getSettingValues} from '../../../../utils/helpers';
import {Setting, SettingValue, checkStripeEnabled, getSettingValues} from '../../../../api/settings';
import {Tier} from '../../../../api/tiers';
import {useGlobalData} from '../../../providers/GlobalDataProvider';
const SignupOptions: React.FC<{

View File

@ -17,12 +17,12 @@ import useRouting from '../../../../hooks/useRouting';
import useSettingGroup from '../../../../hooks/useSettingGroup';
import {ApiError} from '../../../../utils/apiRequests';
import {ReactComponent as StripeVerified} from '../../../../assets/images/stripe-verified.svg';
import {checkStripeEnabled, getGhostPaths, getSettingValue, getSettingValues} from '../../../../utils/helpers';
import {checkStripeEnabled, getSettingValue, getSettingValues, useDeleteStripeSettings, useEditSettings} from '../../../../api/settings';
import {getGhostPaths} from '../../../../utils/helpers';
import {showToast} from '../../../../admin-x-ds/global/Toast';
import {toast} from 'react-hot-toast';
import {useBrowseMembers} from '../../../../utils/api/members';
import {useBrowseTiers, useEditTier} from '../../../../utils/api/tiers';
import {useDeleteStripeSettings, useEditSettings} from '../../../../utils/api/settings';
import {useBrowseMembers} from '../../../../api/members';
import {useBrowseTiers, useEditTier} from '../../../../api/tiers';
import {useGlobalData} from '../../../providers/GlobalDataProvider';
const RETRY_PRODUCT_SAVE_POLL_LENGTH = 1000;

View File

@ -14,12 +14,11 @@ import useForm from '../../../../hooks/useForm';
import useRouting from '../../../../hooks/useRouting';
import useSettingGroup from '../../../../hooks/useSettingGroup';
import useSortableIndexedList from '../../../../hooks/useSortableIndexedList';
import {Tier} from '../../../../types/api';
import {Tier, useAddTier, useEditTier} from '../../../../api/tiers';
import {currencies, currencyFromDecimal, currencyGroups, currencyToDecimal, getSymbol} from '../../../../utils/currency';
import {getSettingValues} from '../../../../utils/helpers';
import {getSettingValues} from '../../../../api/settings';
import {showToast} from '../../../../admin-x-ds/global/Toast';
import {toast} from 'react-hot-toast';
import {useAddTier, useEditTier} from '../../../../utils/api/tiers';
interface TierDetailModalProps {
tier?: Tier

View File

@ -3,8 +3,8 @@ import Heading from '../../../../admin-x-ds/global/Heading';
import Icon from '../../../../admin-x-ds/global/Icon';
import React, {useState} from 'react';
import useSettingGroup from '../../../../hooks/useSettingGroup';
import {Tier} from '../../../../types/api';
import {getSettingValues} from '../../../../utils/helpers';
import {Tier} from '../../../../api/tiers';
import {getSettingValues} from '../../../../api/settings';
import {getSymbol} from '../../../../utils/currency';
import {numberWithCommas} from '../../../../utils/helpers';

View File

@ -5,10 +5,9 @@ import NoValueLabel from '../../../../admin-x-ds/global/NoValueLabel';
import React from 'react';
import TierDetailModal from './TierDetailModal';
import useRouting from '../../../../hooks/useRouting';
import {Tier} from '../../../../types/api';
import {Tier, useEditTier} from '../../../../api/tiers';
import {currencyToDecimal, getSymbol} from '../../../../utils/currency';
import {numberWithCommas} from '../../../../utils/helpers';
import {useEditTier} from '../../../../utils/api/tiers';
interface TiersListProps {
tab?: 'active-tiers' | 'archive-tiers' | 'free-tier';

View File

@ -10,12 +10,11 @@ import ThemePreview from './designAndBranding/ThemePreview';
import ThemeSettings from './designAndBranding/ThemeSettings';
import useForm from '../../../hooks/useForm';
import useRouting from '../../../hooks/useRouting';
import {CustomThemeSetting, Setting, SettingValue} from '../../../types/api';
import {CustomThemeSetting, useBrowseCustomThemeSettings, useEditCustomThemeSettings} from '../../../api/customThemeSettings';
import {PreviewModalContent} from '../../../admin-x-ds/global/modal/PreviewModal';
import {getHomepageUrl, getSettingValues} from '../../../utils/helpers';
import {useBrowseCustomThemeSettings, useEditCustomThemeSettings} from '../../../utils/api/customThemeSettings';
import {useBrowsePosts} from '../../../utils/api/posts';
import {useEditSettings} from '../../../utils/api/settings';
import {Setting, SettingValue, getSettingValues, useEditSettings} from '../../../api/settings';
import {getHomepageUrl} from '../../../api/site';
import {useBrowsePosts} from '../../../api/posts';
import {useGlobalData} from '../../providers/GlobalDataProvider';
const Sidebar: React.FC<{

View File

@ -5,7 +5,7 @@ import TabView from '../../../admin-x-ds/global/TabView';
import useNavigationEditor, {NavigationItem} from '../../../hooks/site/useNavigationEditor';
import useRouting from '../../../hooks/useRouting';
import useSettingGroup from '../../../hooks/useSettingGroup';
import {getSettingValues} from '../../../utils/helpers';
import {getSettingValues} from '../../../api/settings';
import {useState} from 'react';
const NavigationModal = NiceModal.create(() => {

View File

@ -12,9 +12,8 @@ import TabView from '../../../admin-x-ds/global/TabView';
import ThemeInstalledModal from './theme/ThemeInstalledModal';
import ThemePreview from './theme/ThemePreview';
import useRouting from '../../../hooks/useRouting';
import {OfficialTheme} from '../../../models/themes';
import {Theme} from '../../../types/api';
import {useBrowseThemes, useInstallTheme, useUploadTheme} from '../../../utils/api/themes';
import {OfficialTheme} from '../../providers/ServiceProvider';
import {Theme, useBrowseThemes, useInstallTheme, useUploadTheme} from '../../../api/themes';
interface ThemeToolbarProps {
selectedTheme: OfficialTheme|null;

View File

@ -4,8 +4,8 @@ import ImageUpload from '../../../../admin-x-ds/global/form/ImageUpload';
import React from 'react';
import SettingGroupContent from '../../../../admin-x-ds/settings/SettingGroupContent';
import TextField from '../../../../admin-x-ds/global/form/TextField';
import {SettingValue} from '../../../../types/api';
import {getImageUrl, useUploadImage} from '../../../../utils/api/images';
import {SettingValue} from '../../../../api/settings';
import {getImageUrl, useUploadImage} from '../../../../api/images';
export interface BrandSettingValues {
description: string

View File

@ -1,5 +1,5 @@
import React, {useEffect, useRef} from 'react';
import {CustomThemeSetting} from '../../../../types/api';
import {CustomThemeSetting} from '../../../../api/customThemeSettings';
type BrandSettings = {
description: string;

View File

@ -6,9 +6,9 @@ import Select from '../../../../admin-x-ds/global/form/Select';
import SettingGroupContent from '../../../../admin-x-ds/settings/SettingGroupContent';
import TextField from '../../../../admin-x-ds/global/form/TextField';
import Toggle from '../../../../admin-x-ds/global/form/Toggle';
import {CustomThemeSetting} from '../../../../types/api';
import {getImageUrl, useUploadImage} from '../../../../utils/api/images';
import {humanizeSettingKey} from '../../../../utils/helpers';
import {CustomThemeSetting} from '../../../../api/customThemeSettings';
import {getImageUrl, useUploadImage} from '../../../../api/images';
import {humanizeSettingKey} from '../../../../api/settings';
const ThemeSetting: React.FC<{
setting: CustomThemeSetting,

View File

@ -6,10 +6,8 @@ import Menu from '../../../../admin-x-ds/global/Menu';
import ModalPage from '../../../../admin-x-ds/global/modal/ModalPage';
import NiceModal from '@ebay/nice-modal-react';
import React from 'react';
import {Theme} from '../../../../types/api';
import {Theme, isActiveTheme, isDefaultTheme, isDeletableTheme, useActivateTheme, useDeleteTheme} from '../../../../api/themes';
import {downloadFile, getGhostPaths} from '../../../../utils/helpers';
import {isActiveTheme, isDefaultTheme, isDeletableTheme} from '../../../../models/themes';
import {useActivateTheme, useDeleteTheme} from '../../../../utils/api/themes';
interface ThemeActionProps {
theme: Theme;

View File

@ -1,9 +1,8 @@
import Heading from '../../../../admin-x-ds/global/Heading';
import ModalPage from '../../../../admin-x-ds/global/modal/ModalPage';
import React from 'react';
import {OfficialTheme} from '../../../../models/themes';
import {OfficialTheme, useOfficialThemes} from '../../../providers/ServiceProvider';
import {getGhostPaths} from '../../../../utils/helpers';
import {useOfficialThemes} from '../../../providers/ServiceProvider';
const OfficialThemes: React.FC<{
onSelectTheme?: (theme: OfficialTheme) => void;

View File

@ -5,9 +5,8 @@ import ListItem from '../../../../admin-x-ds/global/ListItem';
import NiceModal from '@ebay/nice-modal-react';
import React, {ReactNode, useState} from 'react';
import {ConfirmationModalContent} from '../../../../admin-x-ds/global/modal/ConfirmationModal';
import {InstalledTheme, ThemeProblem} from '../../../../types/api';
import {InstalledTheme, ThemeProblem, useActivateTheme} from '../../../../api/themes';
import {showToast} from '../../../../admin-x-ds/global/Toast';
import {useActivateTheme} from '../../../../utils/api/themes';
const ThemeProblemView = ({problem}:{problem: ThemeProblem}) => {
const [isExpanded, setExpanded] = useState(false);

View File

@ -7,8 +7,8 @@ import MobileChrome from '../../../../admin-x-ds/global/chrome/MobileChrome';
import NiceModal from '@ebay/nice-modal-react';
import PageHeader from '../../../../admin-x-ds/global/layout/PageHeader';
import React, {useState} from 'react';
import {OfficialTheme} from '../../../../models/themes';
import {Theme} from '../../../../types/api';
import {OfficialTheme} from '../../../providers/ServiceProvider';
import {Theme} from '../../../../api/themes';
const ThemePreview: React.FC<{
selectedTheme?: OfficialTheme;

View File

@ -1,8 +1,8 @@
import React, {useEffect, useRef, useState} from 'react';
import useForm, {SaveState} from './useForm';
import useGlobalDirtyState from './useGlobalDirtyState';
import {Setting, SettingValue, SiteData} from '../types/api';
import {useEditSettings} from '../utils/api/settings';
import {Setting, SettingValue, useEditSettings} from '../api/settings';
import {SiteData} from '../api/site';
import {useGlobalData} from '../components/providers/GlobalDataProvider';
interface LocalSetting extends Setting {

View File

@ -1,7 +1,6 @@
import {User} from '../types/api';
import {UserInvite, useBrowseInvites} from '../utils/api/invites';
import {useBrowseRoles} from '../utils/api/roles';
import {useBrowseUsers} from '../utils/api/users';
import {User, useBrowseUsers} from '../api/users';
import {UserInvite, useBrowseInvites} from '../api/invites';
import {useBrowseRoles} from '../api/roles';
import {useGlobalData} from '../components/providers/GlobalDataProvider';
export type UsersHook = {

View File

@ -1,31 +0,0 @@
export type Theme = {
active: boolean;
name: string;
package: {
name?: string;
description?: string;
version?: string;
};
templates?: string[];
}
export type OfficialTheme = {
name: string;
category: string;
previewUrl: string;
ref: string;
image: string;
url?: string;
};
export function isActiveTheme(theme: Theme): boolean {
return theme.active;
}
export function isDefaultTheme(theme: Theme): boolean {
return theme.name === 'casper';
}
export function isDeletableTheme(theme: Theme): boolean {
return !isDefaultTheme(theme) && !isActiveTheme(theme);
}

View File

@ -1,219 +0,0 @@
export type JSONValue = string|number|boolean|null|Date|JSONObject|JSONArray;
export interface JSONObject { [key: string]: JSONValue }
export interface JSONArray extends Array<string|number|boolean|Date|JSONObject|JSONValue> {}
export type SettingValue = string | boolean | null;
export type Setting = {
key: string;
value: SettingValue;
}
export type Config = {
version: string;
environment: string;
editor: {
url: string
version: string
};
labs: Record<string, boolean>;
stripeDirect: boolean;
// Config is relatively fluid, so we only type used properties above and still support arbitrary property access when needed
[key: string]: JSONValue;
};
export type User = {
id: string;
name: string;
slug: string;
email: string;
profile_image: string;
cover_image: string|null;
bio: string;
website: string;
location: string;
facebook: string;
twitter: string;
accessibility: string|null;
status: string;
meta_title: string|null;
meta_description: string|null;
tour: string|null;
last_seen: string|null;
created_at: string;
updated_at: string;
comment_notifications: boolean;
free_member_signup_notification: boolean;
paid_subscription_canceled_notification: boolean;
paid_subscription_started_notification: boolean;
mention_notifications: boolean;
milestone_notifications: boolean;
roles: UserRole[];
url: string;
}
export type UserRoleType = 'Owner' | 'Administrator' | 'Editor' | 'Author' | 'Contributor';
export type UserRole = {
id: string;
name: UserRoleType;
description: string;
created_at: string;
updated_at: string;
};
export type SiteData = {
title: string;
description: string;
logo: string;
icon: string;
accent_color: string;
url: string;
locale: string;
version: string;
};
export type Post = {
id: string;
url: string;
};
export type Member = {
id: string;
};
export type Tier = {
id: string;
name: string;
description: string | null;
slug: string;
active: boolean,
type: string;
welcome_page_url: string | null;
created_at: string;
updated_at: string;
visibility: string;
benefits: string[];
currency?: string;
monthly_price?: number;
yearly_price?: number;
trial_days: number;
}
export type Label = {
id: string;
name: string;
slug: string;
created_at: string;
updated_at: string;
}
export type Offer = {
id: string;
name: string;
code: string;
display_title: string;
display_description: string;
type: string;
cadence: string;
amount: number;
duration: string;
duration_in_months: number | null;
currency_restriction: boolean;
currency: string | null;
status: string;
redemption_count: number;
tier: {
id: string;
name: string;
}
}
type CustomThemeSettingData =
{ type: 'text', value: string | null, default: string | null } |
{ type: 'color', value: string, default: string } |
{ type: 'image', value: string | null } |
{ type: 'boolean', value: boolean, default: boolean } |
{
type: 'select',
value: string
default: string
options: string[]
};
export type CustomThemeSetting = CustomThemeSettingData & {
id: string
key: string
description?: string
// homepage and post are the only two groups we handle, but technically theme authors can put other things in package.json
group?: 'homepage' | 'post' | string
}
export type Theme = {
active: boolean;
name: string;
package: {
name?: string;
description?: string;
version?: string;
};
templates?: string[];
}
export type InstalledTheme = Theme & {
errors?: ThemeProblem<'error'>[];
warnings?: ThemeProblem<'warning'>[];
}
export type ThemeProblem<Level extends string = 'error' | 'warning'> = {
code: string
details: string
failures: Array<{
ref: string
message?: string
rule?: string
}>
fatal: boolean
level: Level
rule: string
}
export type Newsletter = {
id: string;
uuid: string;
name: string;
description: string | null;
feedback_enabled: boolean;
slug: string;
sender_name: string | null;
sender_email: string | null;
sender_reply_to: string;
status: string;
visibility: string;
subscribe_on_signup: boolean;
sort_order: number;
header_image: string | null;
show_header_icon: boolean;
show_header_title: boolean;
title_font_category: string;
title_alignment: string;
show_feature_image: boolean;
body_font_category: string;
footer_content: string | null;
show_badge: boolean;
show_header_name: boolean;
show_post_title_section: boolean;
show_comment_cta: boolean;
show_subscription_details: boolean;
show_latest_posts: boolean;
background_color: string;
border_color: string | null;
title_color: string | null;
created_at: string;
updated_at: string;
count?: {
posts?: number;
active_members?: number;
}
}

View File

@ -1,13 +0,0 @@
import {Config} from '../../types/api';
import {createQuery} from '../apiRequests';
export interface ConfigResponseType {
config: Config;
}
const dataType = 'ConfigResponseType';
export const useBrowseConfig = createQuery<ConfigResponseType>({
dataType,
path: '/config/'
});

View File

@ -1,24 +0,0 @@
import {CustomThemeSetting, Setting} from '../../types/api';
import {createMutation, createQuery} from '../apiRequests';
export interface CustomThemeSettingsResponseType {
custom_theme_settings: CustomThemeSetting[];
}
const dataType = 'CustomThemeSettingsResponseType';
export const useBrowseCustomThemeSettings = createQuery<CustomThemeSettingsResponseType>({
dataType,
path: '/custom_theme_settings/'
});
export const useEditCustomThemeSettings = createMutation<CustomThemeSettingsResponseType, Setting[]>({
method: 'PUT',
path: () => '/custom_theme_settings/',
body: settings => ({custom_theme_settings: settings}),
updateQueries: {
dataType,
update: newData => newData
}
});

View File

@ -1,15 +0,0 @@
import {Meta, createQuery} from '../apiRequests';
import {Offer} from '../../types/api';
export interface OffersResponseType {
meta?: Meta
offers: Offer[]
}
const dataType = 'OffersResponseType';
export const useBrowseOffers = createQuery<OffersResponseType>({
dataType,
path: '/offers/',
defaultSearchParams: {limit: 'all'}
});

View File

@ -1,15 +0,0 @@
import {Meta, createQuery} from '../apiRequests';
import {UserRole} from '../../types/api';
export interface RolesResponseType {
meta?: Meta;
roles: UserRole[];
}
const dataType = 'RolesResponseType';
export const useBrowseRoles = createQuery<RolesResponseType>({
dataType,
path: '/roles/',
defaultSearchParams: {limit: 'all'}
});

View File

@ -1,38 +0,0 @@
import {Meta, createMutation, createQuery} from '../apiRequests';
import {Setting} from '../../types/api';
export type SettingsResponseMeta = Meta & { sent_email_verification?: boolean }
export interface SettingsResponseType {
meta?: SettingsResponseMeta;
settings: Setting[];
}
const dataType = 'SettingsResponseType';
export const useBrowseSettings = createQuery<SettingsResponseType>({
dataType,
path: '/settings/',
defaultSearchParams: {
group: 'site,theme,private,members,portal,newsletter,email,amp,labs,slack,unsplash,views,firstpromoter,editor,comments,analytics,announcement,pintura'
}
});
export const useEditSettings = createMutation<SettingsResponseType, Setting[]>({
method: 'PUT',
path: () => '/settings/',
body: settings => ({settings: settings.map(({key, value}) => ({key, value}))}),
updateQueries: {
dataType,
update: newData => ({
...newData,
settings: newData.settings
})
}
});
export const useDeleteStripeSettings = createMutation<unknown, null>({
method: 'DELETE',
path: () => '/settings/stripe/connect/',
invalidateQueries: {dataType}
});

View File

@ -1,13 +0,0 @@
import {SiteData} from '../../types/api';
import {createQuery} from '../apiRequests';
export interface SiteResponseType {
site: SiteData;
}
const dataType = 'SiteResponseType';
export const useBrowseSite = createQuery<SiteResponseType>({
dataType,
path: '/site/'
});

View File

@ -1,19 +1,9 @@
import {Config, Setting, SettingValue, SiteData, Tier, User} from '../types/api';
export interface IGhostPaths {
adminRoot: string;
assetRoot: string;
apiRoot: string;
}
export function getSettingValue(settings: Setting[] | null | undefined, key: string): SettingValue {
if (!settings) {
return '';
}
const setting = settings.find(d => d.key === key);
return setting?.value || null;
}
export function getGhostPaths(): IGhostPaths {
let path = window.location.pathname;
let subdir = path.substr(0, path.search('/ghost/'));
@ -59,27 +49,6 @@ export function generateAvatarColor(name: string) {
return 'hsl(' + h + ', ' + s + '%, ' + l + '%)';
}
export function humanizeSettingKey(key: string) {
const allCaps = ['API', 'CTA', 'RSS'];
return key
.replace(/^[a-z]/, char => char.toUpperCase())
.replace(/_/g, ' ')
.replace(new RegExp(`\\b(${allCaps.join('|')})\\b`, 'ig'), match => match.toUpperCase());
}
export function getSettingValues<ValueType = SettingValue>(settings: Setting[] | null, keys: string[]): Array<ValueType | undefined> {
return keys.map(key => settings?.find(setting => setting.key === key)?.value) as ValueType[];
}
export function isOwnerUser(user: User) {
return user.roles.some(role => role.name === 'Owner');
}
export function isAdminUser(user: User) {
return user.roles.some(role => role.name === 'Administrator');
}
export function downloadFile(url: string) {
let iframe = document.getElementById('iframeDownload');
@ -93,57 +62,6 @@ export function downloadFile(url: string) {
iframe.setAttribute('src', url);
}
export function getHomepageUrl(siteData: SiteData): string {
const url = new URL(siteData.url);
const subdir = url.pathname.endsWith('/') ? url.pathname : `${url.pathname}/`;
return `${url.origin}${subdir}`;
}
export function getEmailDomain(siteData: SiteData): string {
const domain = new URL(siteData.url).hostname || '';
if (domain.startsWith('www.')) {
return domain.replace(/^(www)\.(?=[^/]*\..{2,5})/, '');
}
return domain;
}
export function fullEmailAddress(value: 'noreply' | string, siteData: SiteData) {
const emailDomain = getEmailDomain(siteData);
return value === 'noreply' ? `noreply@${emailDomain}` : value;
}
export function checkStripeEnabled(settings: Setting[], config: Config) {
const hasSetting = (key: string) => settings.some(setting => setting.key === key && setting.value);
const hasDirectKeys = hasSetting('stripe_secret_key') && hasSetting('stripe_publishable_key');
const hasConnectKeys = hasSetting('stripe_connect_secret_key') && hasSetting('stripe_connect_publishable_key');
if (config.stripeDirect) {
return hasDirectKeys;
}
return hasConnectKeys || hasDirectKeys;
}
export function getPaidActiveTiers(tiers: Tier[]) {
return tiers.filter((tier) => {
return tier.type === 'paid' && tier.active;
});
}
export function getActiveTiers(tiers: Tier[]) {
return tiers.filter((tier) => {
return tier.active;
});
}
export function getArchivedTiers(tiers: Tier[]) {
return tiers.filter((tier) => {
return !tier.active;
});
}
export function numberWithCommas(x: number) {
return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
}