diff --git a/apps/admin-x-settings/package.json b/apps/admin-x-settings/package.json index 5f0dbef6be..1914d70e21 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.8", "@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..2c030ce6f5 --- /dev/null +++ b/apps/admin-x-settings/src/components/selectors/UnsplashSelector.tsx @@ -0,0 +1,24 @@ +import '@tryghost/kg-unsplash-selector/dist/style.css'; // required to load the unsplash styles +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..0753ad2672 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7169,6 +7169,11 @@ dependencies: "@tryghost/kg-clean-basic-html" "^4.0.1" +"@tryghost/kg-unsplash-selector@^0.1.8": + version "0.1.8" + resolved "https://registry.yarnpkg.com/@tryghost/kg-unsplash-selector/-/kg-unsplash-selector-0.1.8.tgz#ef82ca1e2017f4d822d3e6358cb4ccf80a1ea269" + integrity sha512-ymyf4gwAASOyyvyw3ANP3/YnDB7jp4jgS5CdT/hM8BIg3xMZ6808blV+sOA21fgv72Jwaxs6pYAsHRQvIPXu9g== + "@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" @@ -28331,7 +28336,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 +28354,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 +28447,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 +28475,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 +30964,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 +30982,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"