From 19da5c6af46e2f1b9bc7a9ba8942cd80e5a133f2 Mon Sep 17 00:00:00 2001 From: Ronald Langeveld Date: Tue, 12 Mar 2024 22:40:33 +0200 Subject: [PATCH] Extracted Unsplash Selector from AdminX (#19849) no issue - Adds the unsplash selector as a standalone typescript package inside the Koenig monorepo. - Currently we have 3 versions of the Unsplash Selector. One in Koenig-Lexical, one in AdminX and the original Ember version. - We can now start phasing out the application coupled version of the selector and replace it with the reusable version. - We can now import it via npm to any React application. - This commit removes the Unsplash components from AdminX and imports it instead. This is the second commit for this as the previous commit broke styles due to normalise styles leaking into the Ember app. Disabling preflight (https://github.com/TryGhost/Koenig/pull/1169) in Tailwind fixed it. --- apps/admin-x-settings/package.json | 1 + .../components/selectors/UnsplashSelector.tsx | 24 ++ .../site/designAndBranding/BrandSettings.tsx | 8 +- .../src/unsplash/UnsplashSearchModal.tsx | 192 ---------------- .../src/unsplash/UnsplashService.ts | 68 ------ .../src/unsplash/UnsplashTypes.ts | 80 ------- .../unsplash/api/InMemoryUnsplashProvider.ts | 54 ----- .../src/unsplash/api/UnsplashProvider.ts | 161 ------------- .../src/unsplash/api/unsplashFixtures.ts | 142 ------------ .../unsplash/assets/kg-card-type-unsplash.svg | 3 - .../src/unsplash/assets/kg-close.svg | 3 - .../src/unsplash/assets/kg-download.svg | 3 - .../src/unsplash/assets/kg-search.svg | 3 - .../src/unsplash/assets/kg-unsplash-heart.svg | 3 - .../src/unsplash/masonry/MasonryService.ts | 55 ----- .../src/unsplash/photo/PhotoUseCase.ts | 37 --- .../src/unsplash/ui/UnsplashButton.tsx | 37 --- .../src/unsplash/ui/UnsplashGallery.tsx | 149 ------------ .../src/unsplash/ui/UnsplashImage.tsx | 85 ------- .../src/unsplash/ui/UnsplashSelector.tsx | 42 ---- .../src/unsplash/ui/UnsplashZoomed.tsx | 31 --- .../src/{unsplash => utils}/portal.tsx | 0 .../test/unit/unsplash/Masonry.test.ts | 56 ----- .../unit/unsplash/UnsplashService.test.ts | 53 ----- apps/admin-x-settings/vite.config.mjs | 3 + yarn.lock | 217 +++++++++++++++--- 26 files changed, 216 insertions(+), 1294 deletions(-) create mode 100644 apps/admin-x-settings/src/components/selectors/UnsplashSelector.tsx delete mode 100644 apps/admin-x-settings/src/unsplash/UnsplashSearchModal.tsx delete mode 100644 apps/admin-x-settings/src/unsplash/UnsplashService.ts delete mode 100644 apps/admin-x-settings/src/unsplash/UnsplashTypes.ts delete mode 100644 apps/admin-x-settings/src/unsplash/api/InMemoryUnsplashProvider.ts delete mode 100644 apps/admin-x-settings/src/unsplash/api/UnsplashProvider.ts delete mode 100644 apps/admin-x-settings/src/unsplash/api/unsplashFixtures.ts delete mode 100644 apps/admin-x-settings/src/unsplash/assets/kg-card-type-unsplash.svg delete mode 100644 apps/admin-x-settings/src/unsplash/assets/kg-close.svg delete mode 100644 apps/admin-x-settings/src/unsplash/assets/kg-download.svg delete mode 100644 apps/admin-x-settings/src/unsplash/assets/kg-search.svg delete mode 100644 apps/admin-x-settings/src/unsplash/assets/kg-unsplash-heart.svg delete mode 100644 apps/admin-x-settings/src/unsplash/masonry/MasonryService.ts delete mode 100644 apps/admin-x-settings/src/unsplash/photo/PhotoUseCase.ts delete mode 100644 apps/admin-x-settings/src/unsplash/ui/UnsplashButton.tsx delete mode 100644 apps/admin-x-settings/src/unsplash/ui/UnsplashGallery.tsx delete mode 100644 apps/admin-x-settings/src/unsplash/ui/UnsplashImage.tsx delete mode 100644 apps/admin-x-settings/src/unsplash/ui/UnsplashSelector.tsx delete mode 100644 apps/admin-x-settings/src/unsplash/ui/UnsplashZoomed.tsx rename apps/admin-x-settings/src/{unsplash => utils}/portal.tsx (100%) delete mode 100644 apps/admin-x-settings/test/unit/unsplash/Masonry.test.ts delete mode 100644 apps/admin-x-settings/test/unit/unsplash/UnsplashService.test.ts diff --git a/apps/admin-x-settings/package.json b/apps/admin-x-settings/package.json index 5f0dbef6be..d1a8bc0da8 100644 --- a/apps/admin-x-settings/package.json +++ b/apps/admin-x-settings/package.json @@ -39,6 +39,7 @@ "dependencies": { "@codemirror/lang-html": "^6.4.5", "@tryghost/color-utils": "0.2.0", + "@tryghost/kg-unsplash-selector": "^0.1.11", "@tryghost/limit-service": "^1.2.10", "@tryghost/nql": "0.12.1", "@tryghost/timezone-data": "0.4.1", diff --git a/apps/admin-x-settings/src/components/selectors/UnsplashSelector.tsx b/apps/admin-x-settings/src/components/selectors/UnsplashSelector.tsx new file mode 100644 index 0000000000..0e6665b912 --- /dev/null +++ b/apps/admin-x-settings/src/components/selectors/UnsplashSelector.tsx @@ -0,0 +1,24 @@ +import '@tryghost/kg-unsplash-selector/dist/style.css'; +import Portal from '../../utils/portal'; +import React from 'react'; +import {DefaultHeaderTypes, PhotoType, UnsplashSearchModal} from '@tryghost/kg-unsplash-selector'; + +type UnsplashSelectorModalProps = { + onClose: () => void; + onImageInsert: (image: PhotoType) => void; + unsplashProviderConfig: DefaultHeaderTypes | null; +}; + +const UnsplashSelector : React.FC = ({unsplashProviderConfig, onClose, onImageInsert}) => { + return ( + + + + ); +}; + +export default UnsplashSelector; diff --git a/apps/admin-x-settings/src/components/settings/site/designAndBranding/BrandSettings.tsx b/apps/admin-x-settings/src/components/settings/site/designAndBranding/BrandSettings.tsx index 2be76a8b76..05a9a735f9 100644 --- a/apps/admin-x-settings/src/components/settings/site/designAndBranding/BrandSettings.tsx +++ b/apps/admin-x-settings/src/components/settings/site/designAndBranding/BrandSettings.tsx @@ -1,5 +1,5 @@ import React, {useRef, useState} from 'react'; -import UnsplashSearchModal from '../../../../unsplash/UnsplashSearchModal'; +import UnsplashSelector from '../../../selectors/UnsplashSelector'; import usePinturaEditor from '../../../../hooks/usePinturaEditor'; import {ColorPickerField, Heading, Hint, ImageUpload, SettingGroupContent, TextField, debounce} from '@tryghost/admin-x-design-system'; import {SettingValue, getSettingValues} from '@tryghost/admin-x-framework/api/settings'; @@ -144,10 +144,8 @@ const BrandSettings: React.FC<{ values: BrandSettingValues, updateSetting: (key: { showUnsplash && unsplashConfig && unsplashEnabled && ( - { setShowUnsplash(false); }} diff --git a/apps/admin-x-settings/src/unsplash/UnsplashSearchModal.tsx b/apps/admin-x-settings/src/unsplash/UnsplashSearchModal.tsx deleted file mode 100644 index 86c00216c1..0000000000 --- a/apps/admin-x-settings/src/unsplash/UnsplashSearchModal.tsx +++ /dev/null @@ -1,192 +0,0 @@ -import MasonryService from './masonry/MasonryService'; -import Portal from './portal'; -import React, {useMemo, useRef, useState} from 'react'; -import UnsplashGallery from './ui/UnsplashGallery'; -import UnsplashSelector from './ui/UnsplashSelector'; -import {DefaultHeaderTypes, Photo} from './UnsplashTypes'; -import {PhotoUseCases} from './photo/PhotoUseCase'; -import {UnsplashProvider} from './api/UnsplashProvider'; -import {UnsplashService} from './UnsplashService'; - -interface UnsplashModalProps { - onClose: () => void; - onImageInsert: (image: Photo) => void; - unsplashConf: { - defaultHeaders: DefaultHeaderTypes; - }; - } - -const UnsplashSearchModal : React.FC = ({onClose, onImageInsert, unsplashConf}) => { - const unsplashRepo = useMemo(() => new UnsplashProvider(unsplashConf.defaultHeaders), [unsplashConf.defaultHeaders]); - const photoUseCase = useMemo(() => new PhotoUseCases(unsplashRepo), [unsplashRepo]); - const masonryService = useMemo(() => new MasonryService(3), []); - const UnsplashLib = useMemo(() => new UnsplashService(photoUseCase, masonryService), [photoUseCase, masonryService]); - const galleryRef = useRef(null); - const [scrollPos, setScrollPos] = useState(0); - const [lastScrollPos, setLastScrollPos] = useState(0); - const [isLoading, setIsLoading] = useState(UnsplashLib.searchIsRunning() || true); - const initLoadRef = useRef(false); - const [searchTerm, setSearchTerm] = useState(''); - const [zoomedImg, setZoomedImg] = useState(null); - const [dataset, setDataset] = useState([]); - - React.useEffect(() => { - if (galleryRef.current && zoomedImg === null && lastScrollPos !== 0) { - galleryRef.current.scrollTop = lastScrollPos; - setLastScrollPos(0); - } - }, [zoomedImg, scrollPos, lastScrollPos]); - - React.useEffect(() => { - const handleKeyDown = (e:KeyboardEvent) => { - if (e.key === 'Escape') { - onClose(); - } - }; - window.addEventListener('keydown', handleKeyDown); - return () => { - window.removeEventListener('keydown', handleKeyDown); - }; - }, [onClose]); - - React.useEffect(() => { - const ref = galleryRef.current; - if (!zoomedImg) { - if (ref) { - ref.addEventListener('scroll', () => { - setScrollPos(ref.scrollTop); - }); - } - // unmount - return () => { - if (ref) { - ref.removeEventListener('scroll', () => { - setScrollPos(ref.scrollTop); - }); - } - }; - } - }, [galleryRef, zoomedImg]); - - const loadInitPhotos = React.useCallback(async () => { - if (initLoadRef.current === false || searchTerm.length === 0) { - setDataset([]); - UnsplashLib.clearPhotos(); - await UnsplashLib.loadNew(); - const columns = UnsplashLib.getColumns(); - setDataset(columns || []); - if (galleryRef.current && galleryRef.current.scrollTop !== 0) { - galleryRef.current.scrollTop = 0; - } - setIsLoading(false); - } - }, [UnsplashLib, searchTerm]); - - const handleSearch = async (e: React.ChangeEvent) => { - const query = e.target.value; - if (query.length > 2) { - setZoomedImg(null); - setSearchTerm(query); - } - if (query.length === 0) { - setSearchTerm(''); - initLoadRef.current = false; - await loadInitPhotos(); - } - }; - - const search = React.useCallback(async () => { - if (searchTerm) { - setIsLoading(true); - setDataset([]); - UnsplashLib.clearPhotos(); - await UnsplashLib.updateSearch(searchTerm); - const columns = UnsplashLib.getColumns(); - if (columns) { - setDataset(columns); - } - if (galleryRef.current && galleryRef.current.scrollTop !== 0) { - galleryRef.current.scrollTop = 0; - } - setIsLoading(false); - } - }, [searchTerm, UnsplashLib]); - - React.useEffect(() => { - const timeoutId = setTimeout(async () => { - if (searchTerm.length > 2) { - await search(); - } else { - await loadInitPhotos(); - } - }, 300); - return () => { - initLoadRef.current = true; - clearTimeout(timeoutId); - }; - }, [searchTerm, search, loadInitPhotos]); - - const loadMorePhotos = React.useCallback(async () => { - setIsLoading(true); - await UnsplashLib.loadNextPage(); - const columns = UnsplashLib.getColumns(); - setDataset(columns || []); - setIsLoading(false); - }, [UnsplashLib]); - - React.useEffect(() => { - const ref = galleryRef.current; - if (ref) { - const handleScroll = async () => { - if (zoomedImg === null && ref.scrollTop + ref.clientHeight >= ref.scrollHeight - 1000) { - await loadMorePhotos(); - } - }; - ref.addEventListener('scroll', handleScroll); - return () => { - ref.removeEventListener('scroll', handleScroll); - }; - } - }, [galleryRef, loadMorePhotos, zoomedImg]); - - const selectImg = (payload:Photo) => { - if (payload) { - setZoomedImg(payload); - setLastScrollPos(scrollPos); - } - - if (payload === null) { - setZoomedImg(null); - if (galleryRef.current) { - galleryRef.current.scrollTop = lastScrollPos; - } - } - }; - - async function insertImage(image:Photo) { - if (image.src) { - UnsplashLib.triggerDownload(image); - onImageInsert(image); - } - } - return ( - - - - - - ); -}; - -export default UnsplashSearchModal; diff --git a/apps/admin-x-settings/src/unsplash/UnsplashService.ts b/apps/admin-x-settings/src/unsplash/UnsplashService.ts deleted file mode 100644 index 2bc0941563..0000000000 --- a/apps/admin-x-settings/src/unsplash/UnsplashService.ts +++ /dev/null @@ -1,68 +0,0 @@ -import MasonryService from './masonry/MasonryService'; -import {Photo} from './UnsplashTypes'; -import {PhotoUseCases} from './photo/PhotoUseCase'; - -export interface IUnsplashService { - loadNew(): Promise; - layoutPhotos(): void; - getColumns(): Photo[][] | [] | null; - updateSearch(term: string): Promise; - loadNextPage(): Promise; - clearPhotos(): void; - triggerDownload(photo: Photo): void; - photos: Photo[]; - searchIsRunning(): boolean; -} - -export class UnsplashService implements IUnsplashService { - private photoUseCases: PhotoUseCases; - private masonryService: MasonryService; - public photos: Photo[] = []; - - constructor(photoUseCases: PhotoUseCases, masonryService: MasonryService) { - this.photoUseCases = photoUseCases; - this.masonryService = masonryService; - } - - async loadNew() { - let images = await this.photoUseCases.fetchPhotos(); - this.photos = images; - await this.layoutPhotos(); - } - - async layoutPhotos() { - this.masonryService.reset(); - this.photos.forEach((photo) => { - photo.ratio = photo.height / photo.width; - this.masonryService.addPhotoToColumns(photo); - }); - } - - getColumns() { - return this.masonryService.getColumns(); - } - - async updateSearch(term: string) { - let results = await this.photoUseCases.searchPhotos(term); - this.photos = results; - this.layoutPhotos(); - } - - async loadNextPage() { - const newPhotos = await this.photoUseCases.fetchNextPage() || []; - this.photos = [...this.photos, ...newPhotos]; - this.layoutPhotos(); - } - - clearPhotos() { - this.photos = []; - } - - triggerDownload(photo: Photo) { - this.photoUseCases.triggerDownload(photo); - } - - searchIsRunning() { - return this.photoUseCases.searchIsRunning(); - } -} diff --git a/apps/admin-x-settings/src/unsplash/UnsplashTypes.ts b/apps/admin-x-settings/src/unsplash/UnsplashTypes.ts deleted file mode 100644 index 3c78aa223f..0000000000 --- a/apps/admin-x-settings/src/unsplash/UnsplashTypes.ts +++ /dev/null @@ -1,80 +0,0 @@ -export type URLS = { - raw: string; - full: string; - regular: string; - small: string; - thumb: string; - }; - -export type Links = { - self: string; - html: string; - download: string; - download_location: string; - }; - -export type ProfileImage = { - small: string; - medium: string; - large: string; - }; - -export type User = { - id: string; - updated_at: string; - username: string; - name: string; - first_name: string; - last_name: string; - twitter_username: string; - portfolio_url: string; - bio: string; - location: string; - links: Links; - profile_image: ProfileImage; - instagram_username: string; - total_collections: number; - total_likes: number; - total_photos: number; - accepted_tos: boolean; - for_hire: boolean; - social: { - instagram_username: string; - portfolio_url: string; - twitter_username: string; - paypal_email: null | string; - }; - }; - -export type Photo = { - id: string; - slug: string; - created_at: string; - updated_at: string; - promoted_at: string | null; // Nullable - width: number; - height: number; - color: string; - blur_hash: string; - description: null | string; // Nullable - alt_description: string; - breadcrumbs: []; // You could make this more specific - urls: URLS; - links: Links; - likes: number; - liked_by_user: boolean; - current_user_collections: []; // You could make this more specific - sponsorship: null | []; // Nullable - topic_submissions: []; // You could make this more specific - user: User; - ratio: number; - src? : string; - }; - -export type DefaultHeaderTypes = { - Authorization: string; - 'Accept-Version': string; - 'Content-Type': string; - 'App-Pragma': string; - 'X-Unsplash-Cache': boolean; -}; diff --git a/apps/admin-x-settings/src/unsplash/api/InMemoryUnsplashProvider.ts b/apps/admin-x-settings/src/unsplash/api/InMemoryUnsplashProvider.ts deleted file mode 100644 index c58b530539..0000000000 --- a/apps/admin-x-settings/src/unsplash/api/InMemoryUnsplashProvider.ts +++ /dev/null @@ -1,54 +0,0 @@ -// for testing purposes -import {Photo} from '../UnsplashTypes'; -import {fixturePhotos} from './unsplashFixtures'; - -export class InMemoryUnsplashProvider { - photos: Photo[] = fixturePhotos; - PAGINATION: { [key: string]: string } = {}; - REQUEST_IS_RUNNING: boolean = false; - SEARCH_IS_RUNNING: boolean = false; - LAST_REQUEST_URL: string = ''; - ERROR: string | null = null; - IS_LOADING: boolean = false; - currentPage: number = 1; - - public async fetchPhotos(): Promise { - this.IS_LOADING = true; - - const start = (this.currentPage - 1) * 30; - const end = this.currentPage * 30; - this.currentPage += 1; - - this.IS_LOADING = false; - - return this.photos.slice(start, end); - } - - public async fetchNextPage(): Promise { - if (this.REQUEST_IS_RUNNING || this.SEARCH_IS_RUNNING) { - return null; - } - - const photos = await this.fetchPhotos(); - return photos.length > 0 ? photos : null; - } - - public async searchPhotos(term: string): Promise { - this.SEARCH_IS_RUNNING = true; - const filteredPhotos = this.photos.filter(photo => photo.description?.includes(term) || photo.alt_description?.includes(term) - ); - this.SEARCH_IS_RUNNING = false; - - return filteredPhotos; - } - - searchIsRunning(): boolean { - return this.SEARCH_IS_RUNNING; - } - - triggerDownload(photo: Photo): void { - () => { - photo; - }; - } -} diff --git a/apps/admin-x-settings/src/unsplash/api/UnsplashProvider.ts b/apps/admin-x-settings/src/unsplash/api/UnsplashProvider.ts deleted file mode 100644 index 7050e62943..0000000000 --- a/apps/admin-x-settings/src/unsplash/api/UnsplashProvider.ts +++ /dev/null @@ -1,161 +0,0 @@ -import {DefaultHeaderTypes, Photo} from '../UnsplashTypes'; - -export class UnsplashProvider { - API_URL: string = 'https://api.unsplash.com'; - HEADERS: DefaultHeaderTypes; - ERROR: string | null = null; - PAGINATION: { [key: string]: string } = {}; - REQUEST_IS_RUNNING: boolean = false; - SEARCH_IS_RUNNING: boolean = false; - LAST_REQUEST_URL: string = ''; - IS_LOADING: boolean = false; - - constructor(HEADERS: DefaultHeaderTypes) { - this.HEADERS = HEADERS; - } - - private async makeRequest(url: string): Promise { - if (this.REQUEST_IS_RUNNING) { - return null; - } - - this.LAST_REQUEST_URL = url; - const options = { - method: 'GET', - headers: this.HEADERS as unknown as HeadersInit - }; - - try { - this.REQUEST_IS_RUNNING = true; - this.IS_LOADING = true; - - const response = await fetch(url, options); - const checkedResponse = await this.checkStatus(response); - this.extractPagination(checkedResponse); - - const jsonResponse = await checkedResponse.json(); - - if ('results' in jsonResponse) { - return jsonResponse.results; - } else { - return jsonResponse; - } - } catch (error) { - this.ERROR = error as string; - return null; - } finally { - this.REQUEST_IS_RUNNING = false; - this.IS_LOADING = false; - } - } - - private extractPagination(response: Response): Response { - let linkRegex = new RegExp('<(.*)>; rel="(.*)"'); - - let links = []; - - let pagination : { [key: string]: string } = {}; - - for (let entry of response.headers.entries()) { - if (entry[0] === 'link') { - links.push(entry[1]); - } - } - - if (links) { - links.toString().split(',').forEach((link) => { - if (link){ - let linkParts = linkRegex.exec(link); - if (linkParts) { - pagination[linkParts[2]] = linkParts[1]; - } - } - }); - } - - this.PAGINATION = pagination; - - return response; - } - - public async fetchPhotos(): Promise { - const url = `${this.API_URL}/photos?per_page=30`; - const request = await this.makeRequest(url); - return request as Photo[]; - } - - public async fetchNextPage(): Promise { - if (this.REQUEST_IS_RUNNING) { - return null; - } - - if (this.SEARCH_IS_RUNNING) { - return null; - } - - if (this.PAGINATION.next) { - const url = `${this.PAGINATION.next}`; - const response = await this.makeRequest(url); - if (response) { - return response as Photo[]; - } - } - - return null; - } - - public async searchPhotos(term: string): Promise { - const url = `${this.API_URL}/search/photos?query=${term}&per_page=30`; - - const request = await this.makeRequest(url); - if (request) { - return request as Photo[]; - } - - return []; - } - - public async triggerDownload(photo: Photo): Promise { - if (photo.links.download_location) { - await this.makeRequest(photo.links.download_location); - } - } - - private async checkStatus(response: Response): Promise { - if (response.status >= 200 && response.status < 300) { - return response; - } - - let errorText = ''; - let responseTextPromise: Promise; // or Promise if you know the type - - const contentType = response.headers.get('content-type'); - if (contentType === 'application/json') { - responseTextPromise = response.json().then(json => (json).errors[0]); // or cast to a specific type if you know it - } else if (contentType === 'text/xml') { - responseTextPromise = response.text(); - } else { - throw new Error('Unsupported content type'); - } - - return responseTextPromise.then((responseText: string) => { // you can type responseText based on what you expect - if (response.status === 403 && response.headers.get('x-ratelimit-remaining') === '0') { - // we've hit the rate limit on the API - errorText = 'Unsplash API rate limit reached, please try again later.'; - } - - errorText = errorText || responseText || `Error ${response.status}: Uh-oh! Trouble reaching the Unsplash API`; - - // set error text for display in UI - this.ERROR = errorText; - - // throw error to prevent further processing - let error = new Error(errorText) as Error; // or create a custom Error class - throw error; - }); - } - - searchIsRunning(): boolean { - return this.SEARCH_IS_RUNNING; - } -} diff --git a/apps/admin-x-settings/src/unsplash/api/unsplashFixtures.ts b/apps/admin-x-settings/src/unsplash/api/unsplashFixtures.ts deleted file mode 100644 index 3a56b8ed6b..0000000000 --- a/apps/admin-x-settings/src/unsplash/api/unsplashFixtures.ts +++ /dev/null @@ -1,142 +0,0 @@ -import {Photo} from '../UnsplashTypes'; - -export const fixturePhotos: Photo[] = [ - { - id: '1', - slug: 'photo1', - created_at: '2021-01-01', - updated_at: '2021-01-02', - promoted_at: null, - width: 1080, - height: 720, - color: '#ffffff', - blur_hash: 'abc123', - description: 'A nice photo', - alt_description: 'alt1', - breadcrumbs: [], - urls: { - raw: 'http://example.com/raw1', - full: 'http://example.com/full1', - regular: 'http://example.com/regular1', - small: 'http://example.com/small1', - thumb: 'http://example.com/thumb1' - }, - links: { - self: 'http://example.com/self1', - html: 'http://example.com/html1', - download: 'http://example.com/download1', - download_location: 'http://example.com/download_location1' - }, - likes: 100, - liked_by_user: true, - current_user_collections: [], - sponsorship: null, - topic_submissions: [], - user: { - id: 'user1', - updated_at: '2021-01-01', - username: 'user1', - name: 'User One', - first_name: 'User', - last_name: 'One', - twitter_username: 'user1_twitter', - portfolio_url: 'http://portfolio1.com', - bio: 'Bio1', - location: 'Location1', - links: { - self: 'http://example.com/self1', - html: 'http://example.com/html1', - download: 'http://example.com/download1', - download_location: 'http://example.com/download_location1' - }, - profile_image: { - small: 'http://small1.com', - medium: 'http://medium1.com', - large: 'http://large1.com' - }, - instagram_username: 'insta1', - total_collections: 10, - total_likes: 100, - total_photos: 1000, - accepted_tos: true, - for_hire: false, - social: { - instagram_username: 'insta1', - portfolio_url: 'http://portfolio1.com', - twitter_username: 'user1_twitter', - paypal_email: null - } - }, - ratio: 1.5, - src: 'http://src1.com' - }, - { - id: '2', - slug: 'photo1', - created_at: '2021-01-01', - updated_at: '2021-01-02', - promoted_at: null, - width: 1080, - height: 720, - color: '#ffffff', - blur_hash: 'abc123', - description: 'hello world', - alt_description: 'alt1', - breadcrumbs: [], - urls: { - raw: 'http://example.com/raw1', - full: 'http://example.com/full1', - regular: 'http://example.com/regular1', - small: 'http://example.com/small1', - thumb: 'http://example.com/thumb1' - }, - links: { - self: 'http://example.com/self1', - html: 'http://example.com/html1', - download: 'http://example.com/download1', - download_location: 'http://example.com/download_location1' - }, - likes: 100, - liked_by_user: true, - current_user_collections: [], - sponsorship: null, - topic_submissions: [], - user: { - id: 'user1', - updated_at: '2021-01-01', - username: 'user1', - name: 'User One', - first_name: 'User', - last_name: 'One', - twitter_username: 'user1_twitter', - portfolio_url: 'http://portfolio1.com', - bio: 'Bio1', - location: 'Location1', - links: { - self: 'http://example.com/self1', - html: 'http://example.com/html1', - download: 'http://example.com/download1', - download_location: 'http://example.com/download_location1' - }, - profile_image: { - small: 'http://small1.com', - medium: 'http://medium1.com', - large: 'http://large1.com' - }, - instagram_username: 'insta1', - total_collections: 10, - total_likes: 100, - total_photos: 1000, - accepted_tos: true, - for_hire: false, - social: { - instagram_username: 'insta1', - portfolio_url: 'http://portfolio1.com', - twitter_username: 'user1_twitter', - paypal_email: null - } - }, - ratio: 1.5, - src: 'http://src1.com' - } -]; diff --git a/apps/admin-x-settings/src/unsplash/assets/kg-card-type-unsplash.svg b/apps/admin-x-settings/src/unsplash/assets/kg-card-type-unsplash.svg deleted file mode 100644 index 805b50f3e5..0000000000 --- a/apps/admin-x-settings/src/unsplash/assets/kg-card-type-unsplash.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/apps/admin-x-settings/src/unsplash/assets/kg-close.svg b/apps/admin-x-settings/src/unsplash/assets/kg-close.svg deleted file mode 100644 index 30bce27c3b..0000000000 --- a/apps/admin-x-settings/src/unsplash/assets/kg-close.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/apps/admin-x-settings/src/unsplash/assets/kg-download.svg b/apps/admin-x-settings/src/unsplash/assets/kg-download.svg deleted file mode 100644 index 2d1c72bfa4..0000000000 --- a/apps/admin-x-settings/src/unsplash/assets/kg-download.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/apps/admin-x-settings/src/unsplash/assets/kg-search.svg b/apps/admin-x-settings/src/unsplash/assets/kg-search.svg deleted file mode 100644 index dd56d96d42..0000000000 --- a/apps/admin-x-settings/src/unsplash/assets/kg-search.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/apps/admin-x-settings/src/unsplash/assets/kg-unsplash-heart.svg b/apps/admin-x-settings/src/unsplash/assets/kg-unsplash-heart.svg deleted file mode 100644 index 1b31419ccb..0000000000 --- a/apps/admin-x-settings/src/unsplash/assets/kg-unsplash-heart.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/apps/admin-x-settings/src/unsplash/masonry/MasonryService.ts b/apps/admin-x-settings/src/unsplash/masonry/MasonryService.ts deleted file mode 100644 index 6a624cf983..0000000000 --- a/apps/admin-x-settings/src/unsplash/masonry/MasonryService.ts +++ /dev/null @@ -1,55 +0,0 @@ -import {Photo} from '../UnsplashTypes'; - -export default class MasonryService { - public columnCount: number; - public columns: Photo[][] | [] = []; - public columnHeights: number[] | null; - - constructor(columnCount: number = 3) { - this.columnCount = columnCount; - this.columns = [[]]; - this.columnHeights = null; - } - - reset(): void { - let columns: Photo[][] = []; - let columnHeights: number[] = []; - - for (let i = 0; i < this.columnCount; i += 1) { - columns[i] = []; - columnHeights[i] = 0; - } - - this.columns = columns; - this.columnHeights = columnHeights; - } - - addColumns(): void { - for (let i = 0; i < this.columnCount; i++) { - (this.columns as Photo[][]).push([]); - this.columnHeights!.push(0); - } - } - - addPhotoToColumns(photo: Photo): void { - if (!this.columns) { - this.reset(); - } - let min = Math.min(...this.columnHeights!); - let columnIndex = this.columnHeights!.indexOf(min); - - this.columnHeights![columnIndex] += 300 * photo.ratio; - this.columns![columnIndex].push(photo); - } - - getColumns(): Photo[][] | null { - return this.columns; - } - - changeColumnCount(newColumnCount: number): void { - if (newColumnCount !== this.columnCount) { - this.columnCount = newColumnCount; - this.reset(); - } - } -} diff --git a/apps/admin-x-settings/src/unsplash/photo/PhotoUseCase.ts b/apps/admin-x-settings/src/unsplash/photo/PhotoUseCase.ts deleted file mode 100644 index d46cd8e131..0000000000 --- a/apps/admin-x-settings/src/unsplash/photo/PhotoUseCase.ts +++ /dev/null @@ -1,37 +0,0 @@ -import {InMemoryUnsplashProvider} from '../api/InMemoryUnsplashProvider'; -import {Photo} from '../UnsplashTypes'; -import {UnsplashProvider} from '../api/UnsplashProvider'; - -export class PhotoUseCases { - private _provider: UnsplashProvider | InMemoryUnsplashProvider; // InMemoryUnsplashProvider is for testing purposes - - constructor(provider: UnsplashProvider | InMemoryUnsplashProvider) { - this._provider = provider; - } - - async fetchPhotos(): Promise { - return await this._provider.fetchPhotos(); - } - - async searchPhotos(term: string): Promise { - return await this._provider.searchPhotos(term); - } - - async triggerDownload(photo: Photo): Promise { - this._provider.triggerDownload(photo); - } - - async fetchNextPage(): Promise { - let request = await this._provider.fetchNextPage(); - - if (request) { - return request; - } - - return null; - } - - searchIsRunning(): boolean { - return this._provider.searchIsRunning(); - } -} diff --git a/apps/admin-x-settings/src/unsplash/ui/UnsplashButton.tsx b/apps/admin-x-settings/src/unsplash/ui/UnsplashButton.tsx deleted file mode 100644 index 3fb835663d..0000000000 --- a/apps/admin-x-settings/src/unsplash/ui/UnsplashButton.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import React, {HTMLProps} from 'react'; -import {ReactComponent as DownloadIcon} from '../assets/kg-download.svg'; -import {ReactComponent as UnsplashHeartIcon} from '../assets/kg-unsplash-heart.svg'; - -// Define the available icon types -type ButtonIconType = 'heart' | 'download'; - -// Define the props type -interface UnsplashButtonProps extends HTMLProps { - icon?: ButtonIconType; - label?: string; -} - -const BUTTON_ICONS: Record>>> = { - heart: UnsplashHeartIcon, - download: DownloadIcon -}; - -const UnsplashButton: React.FC = ({icon, label, ...props}) => { - let Icon = null; - if (icon) { - Icon = BUTTON_ICONS[icon]; - } - - return ( - e.stopPropagation()} - {...props} - > - {icon && Icon && } - {label && {label}} - - ); -}; - -export default UnsplashButton; diff --git a/apps/admin-x-settings/src/unsplash/ui/UnsplashGallery.tsx b/apps/admin-x-settings/src/unsplash/ui/UnsplashGallery.tsx deleted file mode 100644 index e162358094..0000000000 --- a/apps/admin-x-settings/src/unsplash/ui/UnsplashGallery.tsx +++ /dev/null @@ -1,149 +0,0 @@ -import React, {ReactNode, RefObject} from 'react'; -import UnsplashImage from './UnsplashImage'; -import UnsplashZoomed from './UnsplashZoomed'; -import {Photo} from '../UnsplashTypes'; - -interface MasonryColumnProps { - children: ReactNode; -} - -interface UnsplashGalleryColumnsProps { - columns?: Photo[][] | []; - insertImage?: any; - selectImg?: any; - zoomed?: Photo | null; -} - -interface GalleryLayoutProps { - children?: ReactNode; - galleryRef: RefObject; - isLoading?: boolean; - zoomed?: Photo | null; -} - -interface UnsplashGalleryProps extends GalleryLayoutProps { - error?: string | null; - dataset?: Photo[][] | []; - selectImg?: any; - insertImage?: any; -} - -const UnsplashGalleryLoading: React.FC = () => { - return ( -
-
-
- ); -}; - -export const MasonryColumn: React.FC = (props) => { - return ( -
- {props.children} -
- ); -}; - -const UnsplashGalleryColumns: React.FC = (props) => { - if (!props?.columns) { - return null; - } - - return ( - props?.columns.map((array, index) => ( - // eslint-disable-next-line react/no-array-index-key - - { - array.map((payload: Photo) => ( - - )) - } - - )) - ); -}; - -const GalleryLayout: React.FC = (props) => { - return ( -
-
- {props.children} - {props?.isLoading && } -
-
- ); -}; - -const UnsplashGallery: React.FC = ({zoomed, - error, - galleryRef, - isLoading, - dataset, - selectImg, - insertImage}) => { - if (zoomed) { - return ( - - - - ); - } - - if (error) { - return ( - -
-

Error

-

{error}

-
-
- ); - } - - return ( - - - - ); -}; - -export default UnsplashGallery; diff --git a/apps/admin-x-settings/src/unsplash/ui/UnsplashImage.tsx b/apps/admin-x-settings/src/unsplash/ui/UnsplashImage.tsx deleted file mode 100644 index 824ba883b0..0000000000 --- a/apps/admin-x-settings/src/unsplash/ui/UnsplashImage.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import UnsplashButton from './UnsplashButton'; -import {FC, MouseEvent} from 'react'; -import {Links, Photo, User} from '../UnsplashTypes'; - -export interface UnsplashImageProps { - payload: Photo; - srcUrl: string; - links: Links; - likes: number; - user: User; - alt: string; - urls: { regular: string }; - height: number; - width: number; - zoomed: Photo | null; - insertImage: (options: { - src: string, - caption: string, - height: number, - width: number, - alt: string, - links: Links - }) => void; - selectImg: (payload: Photo | null) => void; -} - -const UnsplashImage: FC = ({payload, srcUrl, links, likes, user, alt, urls, height, width, zoomed, insertImage, selectImg}) => { - const handleClick = (e: MouseEvent) => { - e.stopPropagation(); - selectImg(zoomed ? null : payload); - }; - - return ( -
- {alt} -
-
- - -
-
-
- author -
{user.name}
-
- { - e.stopPropagation(); - insertImage({ - src: urls.regular.replace(/&w=1080/, '&w=2000'), - caption: `Photo by ${user.name} / Unsplash`, - height: height, - width: width, - alt: alt, - links: links - }); - }} /> -
-
-
- ); -}; - -export default UnsplashImage; diff --git a/apps/admin-x-settings/src/unsplash/ui/UnsplashSelector.tsx b/apps/admin-x-settings/src/unsplash/ui/UnsplashSelector.tsx deleted file mode 100644 index 6873de96dc..0000000000 --- a/apps/admin-x-settings/src/unsplash/ui/UnsplashSelector.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import {ChangeEvent, FunctionComponent, ReactNode} from 'react'; -import {ReactComponent as CloseIcon} from '../assets/kg-close.svg'; -import {ReactComponent as SearchIcon} from '../assets/kg-search.svg'; -import {ReactComponent as UnsplashIcon} from '../assets/kg-card-type-unsplash.svg'; - -interface UnsplashSelectorProps { - closeModal: () => void; - handleSearch: (e: ChangeEvent) => void; - children: ReactNode; -} - -const UnsplashSelector: FunctionComponent = ({closeModal, handleSearch, children}) => { - return ( - <> -
-
- -
-
-

- - Unsplash -

-
- - -
-
- {children} -
-
- - ); -}; - -export default UnsplashSelector; diff --git a/apps/admin-x-settings/src/unsplash/ui/UnsplashZoomed.tsx b/apps/admin-x-settings/src/unsplash/ui/UnsplashZoomed.tsx deleted file mode 100644 index 88860d3b18..0000000000 --- a/apps/admin-x-settings/src/unsplash/ui/UnsplashZoomed.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import UnsplashImage, {UnsplashImageProps} from './UnsplashImage'; -import {FC} from 'react'; -import {Photo} from '../UnsplashTypes'; - -interface UnsplashZoomedProps extends Omit { - zoomed: Photo | null; - selectImg: (photo: Photo | null) => void; -} - -const UnsplashZoomed: FC = ({payload, insertImage, selectImg, zoomed}) => { - return ( -
selectImg(null)}> - -
- ); -}; - -export default UnsplashZoomed; diff --git a/apps/admin-x-settings/src/unsplash/portal.tsx b/apps/admin-x-settings/src/utils/portal.tsx similarity index 100% rename from apps/admin-x-settings/src/unsplash/portal.tsx rename to apps/admin-x-settings/src/utils/portal.tsx diff --git a/apps/admin-x-settings/test/unit/unsplash/Masonry.test.ts b/apps/admin-x-settings/test/unit/unsplash/Masonry.test.ts deleted file mode 100644 index e42ecafe94..0000000000 --- a/apps/admin-x-settings/test/unit/unsplash/Masonry.test.ts +++ /dev/null @@ -1,56 +0,0 @@ -import MasonryService from '../../../src/unsplash/masonry/MasonryService'; -import {Photo} from '../../../src/unsplash/UnsplashTypes'; -import {fixturePhotos} from '../../../src/unsplash/api/unsplashFixtures'; - -describe('MasonryService', () => { - let service: MasonryService; - let mockPhotos: Photo[]; - - beforeEach(() => { - service = new MasonryService(3); - mockPhotos = fixturePhotos; - }); - - it('should initialize with default column count', () => { - expect(service.columnCount).toEqual(3); - }); - - describe('reset', () => { - it('should reset columns and columnHeights', () => { - service.reset(); - expect(service.columns.length).toEqual(3); - expect(service.columnHeights!.length).toEqual(3); - }); - }); - - describe('addPhotoToColumns', () => { - it('should add photo to columns with the minimum height)', () => { - service.reset(); - service.addPhotoToColumns(mockPhotos[0]); - expect(service.columns![0]).toContain(mockPhotos[0]); - }); - }); - - describe('getColumns', () => { - it('should return the columns', () => { - service.reset(); - const columns = service.getColumns(); - expect(columns).toEqual(service.columns); - }); - }); - - describe('changeColumnCount', () => { - it('should change the column count and reset', () => { - service.changeColumnCount(4); - expect(service.columnCount).toEqual(4); - expect(service.columns.length).toEqual(4); - expect(service.columnHeights!.length).toEqual(4); - }); - - it('should not reset if the column count is not changed', () => { - const prevColumns = service.getColumns(); - service.changeColumnCount(3); - expect(service.getColumns()).toEqual(prevColumns); - }); - }); -}); diff --git a/apps/admin-x-settings/test/unit/unsplash/UnsplashService.test.ts b/apps/admin-x-settings/test/unit/unsplash/UnsplashService.test.ts deleted file mode 100644 index 35ae259827..0000000000 --- a/apps/admin-x-settings/test/unit/unsplash/UnsplashService.test.ts +++ /dev/null @@ -1,53 +0,0 @@ -import MasonryService from '../../../src/unsplash/masonry/MasonryService'; -import {IUnsplashService, UnsplashService} from '../../../src/unsplash/UnsplashService'; -import {InMemoryUnsplashProvider} from '../../../src/unsplash/api/InMemoryUnsplashProvider'; -import {PhotoUseCases} from '../../../src/unsplash/photo/PhotoUseCase'; -import {fixturePhotos} from '../../../src/unsplash/api/unsplashFixtures'; - -describe('UnsplashService', () => { - let unsplashService: IUnsplashService; - let UnsplashProvider: InMemoryUnsplashProvider; - let masonryService: MasonryService; - let photoUseCases: PhotoUseCases; - - beforeEach(() => { - UnsplashProvider = new InMemoryUnsplashProvider(); - masonryService = new MasonryService(3); - photoUseCases = new PhotoUseCases(UnsplashProvider); - unsplashService = new UnsplashService(photoUseCases, masonryService); - }); - - it('can load new photos', async function () { - await unsplashService.loadNew(); - const photos = unsplashService.photos; - expect(photos).toEqual(fixturePhotos); - }); - - it('set up new columns of 3', async function () { - await unsplashService.loadNew(); - const columns = unsplashService.getColumns(); - if (columns) { - expect(columns.length).toBe(3); - } - }); - - it('can search for photos', async function () { - await unsplashService.updateSearch('cat'); - const photos = unsplashService.photos; - expect(photos.length).toBe(0); - await unsplashService.updateSearch('photo'); - const photos2 = unsplashService.photos; - expect(photos2.length).toBe(1); - }); - - it('can check if search is running', async function () { - const isRunning = unsplashService.searchIsRunning(); - expect(isRunning).toBe(false); - }); - - it('can load next page', async function () { - await unsplashService.loadNextPage(); - const photos = unsplashService.photos; - expect(photos.length).toBe(2); - }); -}); diff --git a/apps/admin-x-settings/vite.config.mjs b/apps/admin-x-settings/vite.config.mjs index 27795efe4e..79df33d790 100644 --- a/apps/admin-x-settings/vite.config.mjs +++ b/apps/admin-x-settings/vite.config.mjs @@ -20,6 +20,9 @@ export default (function viteConfig() { // @TODO: Remove this when @tryghost/nql is updated mingo: resolve(__dirname, '../../node_modules/mingo/dist/mingo.js') } + }, + optimizeDeps: { + include: ['@tryghost/kg-unsplash-selector'] } } }); diff --git a/yarn.lock b/yarn.lock index 9f324ce122..42cd2a1dab 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2242,6 +2242,14 @@ "@elastic/transport" "^8.3.4" tslib "^2.4.0" +"@elastic/elasticsearch@8.12.2": + version "8.12.2" + resolved "https://registry.yarnpkg.com/@elastic/elasticsearch/-/elasticsearch-8.12.2.tgz#7a241f739a509cc59faee85f79a4c9e9e5ba9128" + integrity sha512-04NvH3LIgcv1Uwguorfw2WwzC9Lhfsqs9f0L6uq6MrCw0lqe/HOQ6E8vJ6EkHAA15iEfbhtxOtenbZVVcE+mAQ== + dependencies: + "@elastic/transport" "^8.4.1" + tslib "^2.4.0" + "@elastic/transport@^8.3.4": version "8.4.0" resolved "https://registry.npmjs.org/@elastic/transport/-/transport-8.4.0.tgz#e1ec05f7a2857162c161e2c97008f9b21301a673" @@ -2254,6 +2262,18 @@ tslib "^2.4.0" undici "^5.22.1" +"@elastic/transport@^8.4.1": + version "8.4.1" + resolved "https://registry.yarnpkg.com/@elastic/transport/-/transport-8.4.1.tgz#f98c5a5e2156bcb3f01170b4aca7e7de4d8b61b8" + integrity sha512-/SXVuVnuU5b4dq8OFY4izG+dmGla185PcoqgK6+AJMpmOeY1QYVNbWtCwvSvoAANN5D/wV+EBU8+x7Vf9EphbA== + dependencies: + debug "^4.3.4" + hpagent "^1.0.0" + ms "^2.1.3" + secure-json-parse "^2.4.0" + tslib "^2.4.0" + undici "^5.22.1" + "@ember-data/adapter@3.24.0": version "3.24.0" resolved "https://registry.yarnpkg.com/@ember-data/adapter/-/adapter-3.24.0.tgz#995c19bc6fb95c94cbb83b8c3c7bc08253346cba" @@ -6947,6 +6967,14 @@ "@tryghost/root-utils" "^0.3.25" debug "^4.3.1" +"@tryghost/debug@^0.1.28": + version "0.1.28" + resolved "https://registry.yarnpkg.com/@tryghost/debug/-/debug-0.1.28.tgz#498ef3450aa654ebb15a47553c2478a33164c0e6" + integrity sha512-iZKKlDDcZZa77GCgZ+o/Vp5oz520SOOpKCnoapgKGkFLRFT/0/D54jw/KY2pHGTFBXrcrE8kqTulgeuMNP+ABA== + dependencies: + "@tryghost/root-utils" "^0.3.26" + debug "^4.3.1" + "@tryghost/elasticsearch@^3.0.15": version "3.0.15" resolved "https://registry.yarnpkg.com/@tryghost/elasticsearch/-/elasticsearch-3.0.15.tgz#d4be60b79155d95de063e17ea90ff0151a0a35d9" @@ -6956,6 +6984,15 @@ "@tryghost/debug" "^0.1.26" split2 "4.2.0" +"@tryghost/elasticsearch@^3.0.16": + version "3.0.17" + resolved "https://registry.yarnpkg.com/@tryghost/elasticsearch/-/elasticsearch-3.0.17.tgz#408e8ba7ce35c9357f6814bf0fd9b88cb56c2ebb" + integrity sha512-4uYnFJQ0QDNleko1J26E0byWnHrEBZzd3S1WVTbCztlC14KQweZxmfou3fc5JmcT/GNiyXd5Pgx+bLMtVi017g== + dependencies: + "@elastic/elasticsearch" "8.12.2" + "@tryghost/debug" "^0.1.28" + split2 "4.2.0" + "@tryghost/email-mock-receiver@0.3.2": version "0.3.2" resolved "https://registry.yarnpkg.com/@tryghost/email-mock-receiver/-/email-mock-receiver-0.3.2.tgz#abd8086935a95a996b6c5c803478a9f81dcae19a" @@ -6977,7 +7014,24 @@ focus-trap "^6.7.2" postcss-preset-env "^7.3.1" -"@tryghost/errors@1.2.26", "@tryghost/errors@1.3.0", "@tryghost/errors@1.3.1", "@tryghost/errors@^1.2.26", "@tryghost/errors@^1.2.27", "@tryghost/errors@^1.2.3": +"@tryghost/errors@1.2.26": + version "1.2.26" + resolved "https://registry.yarnpkg.com/@tryghost/errors/-/errors-1.2.26.tgz#0d0503a51e681998421548fbddbdd7376384c457" + integrity sha512-s/eynvVUiAhHP0HB7CPQs7qH7Pm1quJ2iUMTCuH7HV8LqiGoQFNc21/5R4lRv+2Jt3yf69UPCs/6G+kAgZipNw== + dependencies: + "@stdlib/utils-copy" "^0.0.7" + lodash "^4.17.21" + uuid "^9.0.0" + +"@tryghost/errors@1.3.0": + version "1.3.0" + resolved "https://registry.yarnpkg.com/@tryghost/errors/-/errors-1.3.0.tgz#273beb4c91bd7eb8a44b2e4154e57cc5321ad9dc" + integrity sha512-XI3Gw+6Mbua7FiCdNMY+0bVQ0pP6YDY5UpIAd9YIb1PWtISmiqEA2St1bvBk08Blfks3+lFPigh/YUxjuTKjRw== + dependencies: + "@stdlib/utils-copy" "^0.0.7" + uuid "^9.0.0" + +"@tryghost/errors@1.3.1", "@tryghost/errors@^1.2.26", "@tryghost/errors@^1.2.27", "@tryghost/errors@^1.2.3", "@tryghost/errors@^1.3.1": version "1.3.1" resolved "https://registry.npmjs.org/@tryghost/errors/-/errors-1.3.1.tgz#32a00c5e5293c46e54d03a66da871ac34b2ab35c" integrity sha512-iZqT0vZ3NVZNq9o1HYxW00k1mcUAC+t5OLiI8O29/uQwAfy7NemY+Cabl9mWoIwgvBmw7l0Z8pHTcXMo1c+xMw== @@ -7023,6 +7077,14 @@ "@tryghost/errors" "^1.2.26" "@tryghost/request" "^1.0.0" +"@tryghost/http-stream@^0.1.27": + version "0.1.28" + resolved "https://registry.yarnpkg.com/@tryghost/http-stream/-/http-stream-0.1.28.tgz#ae38ef2e113e23b582d194fc76c5196a54171a11" + integrity sha512-E1TyZE5e121TPjMgJvlSPTcxJl3Xdq78kugmx2deLxADa5W/vuchidZ6yYQQzxCXPciQFUciVYT8B5tggZb+Xw== + dependencies: + "@tryghost/errors" "^1.3.1" + "@tryghost/request" "^1.0.3" + "@tryghost/image-transform@1.2.11": version "1.2.11" resolved "https://registry.yarnpkg.com/@tryghost/image-transform/-/image-transform-1.2.11.tgz#82463d97f8747db6db70165a04e824eed6791fee" @@ -7169,6 +7231,11 @@ dependencies: "@tryghost/kg-clean-basic-html" "^4.0.1" +"@tryghost/kg-unsplash-selector@^0.1.11": + version "0.1.11" + resolved "https://registry.yarnpkg.com/@tryghost/kg-unsplash-selector/-/kg-unsplash-selector-0.1.11.tgz#d5fe973c01217fa5854a528a7b74e6d008b25309" + integrity sha512-5guN8Z8vpF2zCvfziKqFvQN/LQGuV2/ZKz+JiDk6dVBOwHZ/flitMDmnlodCNYT141yQE9x9DCrZAC216koHqw== + "@tryghost/kg-utils@^1.0.24": version "1.0.24" resolved "https://registry.yarnpkg.com/@tryghost/kg-utils/-/kg-utils-1.0.24.tgz#4ef358ef803272cbe257993b9f79ea0a6b432077" @@ -7190,7 +7257,24 @@ lodash "^4.17.21" luxon "^1.26.0" -"@tryghost/logging@2.4.10", "@tryghost/logging@2.4.8", "@tryghost/logging@^2.4.7": +"@tryghost/logging@2.4.10": + version "2.4.10" + resolved "https://registry.yarnpkg.com/@tryghost/logging/-/logging-2.4.10.tgz#2e5b56c53364be330c1e6f2ffa33e3c30b7bac8e" + integrity sha512-l356vLSQmszY14y7ef5YxY4CZ3418NXn5+LvFdlweeTRk0ilWx1mVUoXi8IlVh90rIVbemv+pXi1dusJB6peQA== + dependencies: + "@tryghost/bunyan-rotating-filestream" "^0.0.7" + "@tryghost/elasticsearch" "^3.0.16" + "@tryghost/http-stream" "^0.1.27" + "@tryghost/pretty-stream" "^0.1.21" + "@tryghost/root-utils" "^0.3.25" + bunyan "^1.8.15" + bunyan-loggly "^1.4.2" + fs-extra "^11.0.0" + gelf-stream "^1.1.1" + json-stringify-safe "^5.0.1" + lodash "^4.17.21" + +"@tryghost/logging@2.4.8", "@tryghost/logging@^2.4.7": version "2.4.8" resolved "https://registry.yarnpkg.com/@tryghost/logging/-/logging-2.4.8.tgz#a9e9abdbec823f0c6a009aa2f6847ce454b35475" integrity sha512-/pIeTcw9jpqWJ5/VyUn5sa3rsUxUHortykB4oYd5vKr16KgnrVOuGPCg4JqmdGfz2zrkgKaGd9cAsTNE++0Deg== @@ -7305,6 +7389,15 @@ moment "^2.29.1" prettyjson "^1.2.5" +"@tryghost/pretty-stream@^0.1.21": + version "0.1.22" + resolved "https://registry.yarnpkg.com/@tryghost/pretty-stream/-/pretty-stream-0.1.22.tgz#856a6b8eb3bd17b2fdc5ca668ac27bdd2f55b04b" + integrity sha512-97/JRI7rmdQIG6zwPzux58Kfc/UJfdvKiJgYgH7+CuNQqdl0Zy2+X0wlnRnYjck7tj781/rhGTEGGZg6PHZbaw== + dependencies: + lodash "^4.17.21" + moment "^2.29.1" + prettyjson "^1.2.5" + "@tryghost/promise@0.3.4": version "0.3.4" resolved "https://registry.yarnpkg.com/@tryghost/promise/-/promise-0.3.4.tgz#b5437eb14a3d06e7d32f277e10975ff77061e16e" @@ -7327,6 +7420,18 @@ got "13.0.0" lodash "^4.17.21" +"@tryghost/request@^1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@tryghost/request/-/request-1.0.3.tgz#73f7417bd4eb382133ea1eaa092ff14d070c45aa" + integrity sha512-ruHs3omvxTYGIm87gGJSRx0r64y4mBWV52d0wiwgOeCyyYL2WDYQ1dTgHWZbSjl8YHc2p0lc8gkPPxBZ+9ZnUA== + dependencies: + "@tryghost/errors" "^1.3.1" + "@tryghost/validator" "^0.2.9" + "@tryghost/version" "^0.1.26" + cacheable-lookup "7.0.0" + got "13.0.0" + lodash "^4.17.21" + "@tryghost/root-utils@0.3.24": version "0.3.24" resolved "https://registry.yarnpkg.com/@tryghost/root-utils/-/root-utils-0.3.24.tgz#91653fbadc882fb8510844f163a2231c87f30fab" @@ -7343,6 +7448,14 @@ caller "^1.0.1" find-root "^1.1.0" +"@tryghost/root-utils@^0.3.26": + version "0.3.26" + resolved "https://registry.yarnpkg.com/@tryghost/root-utils/-/root-utils-0.3.26.tgz#26e626706b67dddd13f59812826a806c45722122" + integrity sha512-akbI+mmnU6mlwbVAEy8Ay1MYbMEocKEVP+QHbYCKk3d++2TM2lmSZm6CFIP5+10NeFiP2Em6jSaGXBpaalR9VQ== + dependencies: + caller "^1.0.1" + find-root "^1.1.0" + "@tryghost/server@^0.1.37": version "0.1.37" resolved "https://registry.yarnpkg.com/@tryghost/server/-/server-0.1.37.tgz#04ee5671b19a4a5be05e361e293d47eb9c6c2482" @@ -7382,6 +7495,13 @@ dependencies: lodash.template "^4.5.0" +"@tryghost/tpl@^0.1.28": + version "0.1.28" + resolved "https://registry.yarnpkg.com/@tryghost/tpl/-/tpl-0.1.28.tgz#c1453eedf33da7010b1c556f2e4d92f656351fd9" + integrity sha512-z8DBIDntaJQMHEmp/ZhCpPjc5TXIsu7ZdnOVbAVK2YnzhLcjDl/JPpmt2FXLV3VBo7VM1XBT9nptiYd2kFnZFg== + dependencies: + lodash.template "^4.5.0" + "@tryghost/url-utils@4.4.6", "@tryghost/url-utils@^4.0.0": version "4.4.6" resolved "https://registry.yarnpkg.com/@tryghost/url-utils/-/url-utils-4.4.6.tgz#4938e55fcc11620fd17c64346249d420f6f97129" @@ -7406,6 +7526,17 @@ moment-timezone "^0.5.23" validator "7.2.0" +"@tryghost/validator@^0.2.9": + version "0.2.9" + resolved "https://registry.yarnpkg.com/@tryghost/validator/-/validator-0.2.9.tgz#6b33d884d96e0bca20a750d9dd1ed534a0efbab6" + integrity sha512-7EBFiXUGhU6ReuryAnqh5BM0Fa918NSEN3UR2dqrgk861W/pwofmx8r179jVKBNA0cSYot/DVj5bWlUV25cWvQ== + dependencies: + "@tryghost/errors" "^1.3.1" + "@tryghost/tpl" "^0.1.28" + lodash "^4.17.21" + moment-timezone "^0.5.23" + validator "7.2.0" + "@tryghost/version@0.1.24", "@tryghost/version@^0.1.24": version "0.1.24" resolved "https://registry.yarnpkg.com/@tryghost/version/-/version-0.1.24.tgz#eb8bc345929ba8f67c3f36757bd91c12f701a5f5" @@ -7414,6 +7545,14 @@ "@tryghost/root-utils" "^0.3.24" semver "^7.3.5" +"@tryghost/version@^0.1.26": + version "0.1.26" + resolved "https://registry.yarnpkg.com/@tryghost/version/-/version-0.1.26.tgz#00829961bfef66b0aae01f6b866068df63859236" + integrity sha512-um1GihMBOs+1+p6tPGgIOHGlPeYyj0w+JxRHSIstPyywMmuM+kH3LUrHt3N3hb7zzgWfehM0L81k28MCGTqV5Q== + dependencies: + "@tryghost/root-utils" "^0.3.26" + semver "^7.3.5" + "@tryghost/webhook-mock-receiver@0.2.8": version "0.2.8" resolved "https://registry.yarnpkg.com/@tryghost/webhook-mock-receiver/-/webhook-mock-receiver-0.2.8.tgz#1cb3bb5de667f597f2eaa25aff3e9e572212cafa" @@ -22815,18 +22954,57 @@ module-details-from-path@^1.0.3: resolved "https://registry.yarnpkg.com/module-details-from-path/-/module-details-from-path-1.0.3.tgz#114c949673e2a8a35e9d35788527aa37b679da2b" integrity sha512-ySViT69/76t8VhE1xXHK6Ch4NcDd26gx0MzKXLO+F7NOtnqH68d9zF94nT8ZWSxXh8ELOERsnJO/sWt1xZYw5A== -moment-timezone@0.5.23, moment-timezone@0.5.34, moment-timezone@^0.5.23, moment-timezone@^0.5.31, moment-timezone@^0.5.33: +moment-timezone@0.5.23, moment-timezone@^0.5.23: version "0.5.23" resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.23.tgz#7cbb00db2c14c71b19303cb47b0fb0a6d8651463" integrity sha512-WHFH85DkCfiNMDX5D3X7hpNH3/PUhjTGcD0U1SgfBGZxJ3qUmJh5FdvaFjcClxOvB3rzdfj4oRffbI38jEnC1w== dependencies: moment ">= 2.9.0" -moment@2.24.0, moment@2.27.0, moment@2.29.1, moment@2.29.3, moment@2.29.4, "moment@>= 2.9.0", moment@^2.10.2, moment@^2.18.1, moment@^2.19.3, moment@^2.27.0, moment@^2.29.1: +moment-timezone@0.5.34: + version "0.5.34" + resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.34.tgz#a75938f7476b88f155d3504a9343f7519d9a405c" + integrity sha512-3zAEHh2hKUs3EXLESx/wsgw6IQdusOT8Bxm3D9UrHPQR7zlMmzwybC8zHEM1tQ4LJwP7fcxrWr8tuBg05fFCbg== + dependencies: + moment ">= 2.9.0" + +moment-timezone@^0.5.31, moment-timezone@^0.5.33: + version "0.5.45" + resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.45.tgz#cb685acd56bac10e69d93c536366eb65aa6bcf5c" + integrity sha512-HIWmqA86KcmCAhnMAN0wuDOARV/525R2+lOLotuGFzn4HO+FH+/645z2wx0Dt3iDv6/p61SIvKnDstISainhLQ== + dependencies: + moment "^2.29.4" + +moment@2.24.0, "moment@>= 2.9.0", moment@^2.10.2, moment@^2.18.1, moment@^2.19.3: version "2.24.0" resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b" integrity sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg== +moment@2.27.0: + version "2.27.0" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.27.0.tgz#8bff4e3e26a236220dfe3e36de756b6ebaa0105d" + integrity sha512-al0MUK7cpIcglMv3YF13qSgdAIqxHTO7brRtaz3DlSULbqfazqkc5kEjNrLDOM7fsjshoFIihnU8snrP7zUvhQ== + +moment@2.29.1: + version "2.29.1" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3" + integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ== + +moment@2.29.3: + version "2.29.3" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.3.tgz#edd47411c322413999f7a5940d526de183c031f3" + integrity sha512-c6YRvhEo//6T2Jz/vVtYzqBzwvPT95JBQ+smCytzf7c50oMZRsR/a4w88aD34I+/QVSfnoAnSBFPJHItlOMJVw== + +moment@2.29.4: + version "2.29.4" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.4.tgz#3dbe052889fe7c1b2ed966fcb3a77328964ef108" + integrity sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w== + +moment@^2.27.0, moment@^2.29.1, moment@^2.29.4: + version "2.30.1" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.30.1.tgz#f8c91c07b7a786e30c59926df530b4eac96974ae" + integrity sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how== + moo@^0.5.0, moo@^0.5.1: version "0.5.2" resolved "https://registry.yarnpkg.com/moo/-/moo-0.5.2.tgz#f9fe82473bc7c184b0d32e2215d3f6e67278733c" @@ -28331,7 +28509,7 @@ string-template@~0.2.1: resolved "https://registry.yarnpkg.com/string-template/-/string-template-0.2.1.tgz#42932e598a352d01fc22ec3367d9d84eec6c9add" integrity sha512-Yptehjogou2xm4UJbxJ4CxgZx12HBfeystp0y3x7s4Dj32ltVVG1Gg8YhKjHZkHicuKpZX/ffilA8505VbUbpw== -"string-width-cjs@npm:string-width@^4.2.0": +"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -28349,15 +28527,6 @@ string-width@^1.0.1: is-fullwidth-code-point "^1.0.0" strip-ansi "^3.0.0" -"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - string-width@^2.1.0: version "2.1.1" resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" @@ -28451,7 +28620,7 @@ stringify-entities@^2.0.0: is-decimal "^1.0.2" is-hexadecimal "^1.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -28479,13 +28648,6 @@ strip-ansi@^5.1.0, strip-ansi@^5.2.0: dependencies: ansi-regex "^4.1.0" -strip-ansi@^6.0.0, strip-ansi@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - strip-ansi@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.0.1.tgz#61740a08ce36b61e50e65653f07060d000975fb2" @@ -30975,7 +31137,7 @@ workerpool@^6.0.2, workerpool@^6.0.3, workerpool@^6.1.5, workerpool@^6.4.0: resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.5.1.tgz#060f73b39d0caf97c6db64da004cd01b4c099544" integrity sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -30993,15 +31155,6 @@ wrap-ansi@^6.0.1: string-width "^4.1.0" strip-ansi "^6.0.0" -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrap-ansi@^8.0.1, wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"