mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-11-23 03:42:27 +03:00
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.
This commit is contained in:
parent
9203eea673
commit
19da5c6af4
@ -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",
|
||||
|
@ -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<UnsplashSelectorModalProps> = ({unsplashProviderConfig, onClose, onImageInsert}) => {
|
||||
return (
|
||||
<Portal classNames='admin-x-settings'>
|
||||
<UnsplashSearchModal
|
||||
unsplashProviderConfig={unsplashProviderConfig}
|
||||
onClose={onClose}
|
||||
onImageInsert={onImageInsert}
|
||||
/>
|
||||
</Portal>
|
||||
);
|
||||
};
|
||||
|
||||
export default UnsplashSelector;
|
@ -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:
|
||||
</ImageUpload>
|
||||
{
|
||||
showUnsplash && unsplashConfig && unsplashEnabled && (
|
||||
<UnsplashSearchModal
|
||||
unsplashConf={{
|
||||
defaultHeaders: unsplashConfig
|
||||
}}
|
||||
<UnsplashSelector
|
||||
unsplashProviderConfig={unsplashConfig}
|
||||
onClose={() => {
|
||||
setShowUnsplash(false);
|
||||
}}
|
||||
|
@ -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<UnsplashModalProps> = ({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<HTMLDivElement | null>(null);
|
||||
const [scrollPos, setScrollPos] = useState<number>(0);
|
||||
const [lastScrollPos, setLastScrollPos] = useState<number>(0);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(UnsplashLib.searchIsRunning() || true);
|
||||
const initLoadRef = useRef<boolean>(false);
|
||||
const [searchTerm, setSearchTerm] = useState<string>('');
|
||||
const [zoomedImg, setZoomedImg] = useState<Photo | null>(null);
|
||||
const [dataset, setDataset] = useState<Photo[][] | []>([]);
|
||||
|
||||
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<HTMLInputElement>) => {
|
||||
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 (
|
||||
<Portal classNames='admin-x-settings'>
|
||||
<UnsplashSelector
|
||||
closeModal={onClose}
|
||||
handleSearch={handleSearch}
|
||||
>
|
||||
<UnsplashGallery
|
||||
dataset={dataset}
|
||||
error={null}
|
||||
galleryRef={galleryRef}
|
||||
insertImage={insertImage}
|
||||
isLoading={isLoading}
|
||||
selectImg={selectImg}
|
||||
zoomed={zoomedImg}
|
||||
/>
|
||||
</UnsplashSelector>
|
||||
</Portal>
|
||||
);
|
||||
};
|
||||
|
||||
export default UnsplashSearchModal;
|
@ -1,68 +0,0 @@
|
||||
import MasonryService from './masonry/MasonryService';
|
||||
import {Photo} from './UnsplashTypes';
|
||||
import {PhotoUseCases} from './photo/PhotoUseCase';
|
||||
|
||||
export interface IUnsplashService {
|
||||
loadNew(): Promise<void>;
|
||||
layoutPhotos(): void;
|
||||
getColumns(): Photo[][] | [] | null;
|
||||
updateSearch(term: string): Promise<void>;
|
||||
loadNextPage(): Promise<void>;
|
||||
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();
|
||||
}
|
||||
}
|
@ -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;
|
||||
};
|
@ -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<Photo[]> {
|
||||
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<Photo[] | null> {
|
||||
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<Photo[]> {
|
||||
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;
|
||||
};
|
||||
}
|
||||
}
|
@ -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<Photo[] | {results: Photo[]} | null> {
|
||||
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<Photo[]> {
|
||||
const url = `${this.API_URL}/photos?per_page=30`;
|
||||
const request = await this.makeRequest(url);
|
||||
return request as Photo[];
|
||||
}
|
||||
|
||||
public async fetchNextPage(): Promise<Photo[] | null> {
|
||||
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<Photo[]> {
|
||||
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<void> {
|
||||
if (photo.links.download_location) {
|
||||
await this.makeRequest(photo.links.download_location);
|
||||
}
|
||||
}
|
||||
|
||||
private async checkStatus(response: Response): Promise<Response> {
|
||||
if (response.status >= 200 && response.status < 300) {
|
||||
return response;
|
||||
}
|
||||
|
||||
let errorText = '';
|
||||
let responseTextPromise: Promise<string>; // or Promise<string> 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;
|
||||
}
|
||||
}
|
@ -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'
|
||||
}
|
||||
];
|
@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 122.43 122.41">
|
||||
<path d="M83.86 54.15v34.13H38.57V54.15H0v68.26h122.43V54.15H83.86zM38.57 0h45.3v34.13h-45.3z"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 176 B |
@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" stroke-width="1.5" viewBox="0 0 24 24">
|
||||
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" d="M.75 23.249l22.5-22.5M23.25 23.249L.75.749"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 226 B |
@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path d="M20 5.5l-8 8-8-8m-3.5 13h23" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" fill="none"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 216 B |
@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" stroke-width="1.5" viewBox="0 0 24 24">
|
||||
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" d="M1.472 13.357a9.063 9.063 0 1 0 16.682-7.09 9.063 9.063 0 1 0-16.682 7.09Zm14.749 2.863 7.029 7.03"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 283 B |
@ -1,3 +0,0 @@
|
||||
<svg viewBox="0 0 32 32">
|
||||
<path d="M17.4 29c-.8.8-2 .8-2.8 0L2.3 16.2C-.8 13.1-.8 8 2.3 4.8c3.1-3.1 8.2-3.1 11.3 0L16 7.6l2.3-2.8c3.1-3.1 8.2-3.1 11.3 0 3.1 3.1 3.1 8.2 0 11.4L17.4 29z"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 197 B |
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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<Photo[]> {
|
||||
return await this._provider.fetchPhotos();
|
||||
}
|
||||
|
||||
async searchPhotos(term: string): Promise<Photo[]> {
|
||||
return await this._provider.searchPhotos(term);
|
||||
}
|
||||
|
||||
async triggerDownload(photo: Photo): Promise<void> {
|
||||
this._provider.triggerDownload(photo);
|
||||
}
|
||||
|
||||
async fetchNextPage(): Promise<Photo[] | null> {
|
||||
let request = await this._provider.fetchNextPage();
|
||||
|
||||
if (request) {
|
||||
return request;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
searchIsRunning(): boolean {
|
||||
return this._provider.searchIsRunning();
|
||||
}
|
||||
}
|
@ -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<HTMLAnchorElement> {
|
||||
icon?: ButtonIconType;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
const BUTTON_ICONS: Record<ButtonIconType, React.ComponentType<Partial<React.SVGProps<SVGSVGElement>>>> = {
|
||||
heart: UnsplashHeartIcon,
|
||||
download: DownloadIcon
|
||||
};
|
||||
|
||||
const UnsplashButton: React.FC<UnsplashButtonProps> = ({icon, label, ...props}) => {
|
||||
let Icon = null;
|
||||
if (icon) {
|
||||
Icon = BUTTON_ICONS[icon];
|
||||
}
|
||||
|
||||
return (
|
||||
<a
|
||||
className="flex h-8 shrink-0 cursor-pointer items-center rounded-md bg-white px-3 py-2 font-sans text-sm font-medium leading-6 text-grey-700 opacity-90 transition-all ease-in-out hover:opacity-100"
|
||||
onClick={e => e.stopPropagation()}
|
||||
{...props}
|
||||
>
|
||||
{icon && Icon && <Icon className={`h-4 w-4 ${icon === 'heart' ? 'fill-red' : ''} stroke-[3px] ${label && 'mr-1'}`} />}
|
||||
{label && <span>{label}</span>}
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
||||
export default UnsplashButton;
|
@ -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<HTMLDivElement>;
|
||||
isLoading?: boolean;
|
||||
zoomed?: Photo | null;
|
||||
}
|
||||
|
||||
interface UnsplashGalleryProps extends GalleryLayoutProps {
|
||||
error?: string | null;
|
||||
dataset?: Photo[][] | [];
|
||||
selectImg?: any;
|
||||
insertImage?: any;
|
||||
}
|
||||
|
||||
const UnsplashGalleryLoading: React.FC = () => {
|
||||
return (
|
||||
<div className="absolute inset-y-0 left-0 flex w-full items-center justify-center overflow-hidden pb-[8vh]" data-kg-loader>
|
||||
<div className="relative inline-block h-[50px] w-[50px] animate-spin rounded-full border border-black/10 before:z-10 before:mt-[7px] before:block before:h-[7px] before:w-[7px] before:rounded-full before:bg-grey-800"></div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const MasonryColumn: React.FC<MasonryColumnProps> = (props) => {
|
||||
return (
|
||||
<div className="mr-6 flex grow basis-0 flex-col justify-start last-of-type:mr-0">
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const UnsplashGalleryColumns: React.FC<UnsplashGalleryColumnsProps> = (props) => {
|
||||
if (!props?.columns) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
props?.columns.map((array, index) => (
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
<MasonryColumn key={index}>
|
||||
{
|
||||
array.map((payload: Photo) => (
|
||||
<UnsplashImage
|
||||
key={payload.id}
|
||||
alt={payload.alt_description}
|
||||
height={payload.height}
|
||||
insertImage={props?.insertImage}
|
||||
likes={payload.likes}
|
||||
links={payload.links}
|
||||
payload={payload}
|
||||
selectImg={props?.selectImg}
|
||||
srcUrl={payload.urls.regular}
|
||||
urls={payload.urls}
|
||||
user={payload.user}
|
||||
width={payload.width}
|
||||
zoomed={props?.zoomed || null}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</MasonryColumn>
|
||||
))
|
||||
);
|
||||
};
|
||||
|
||||
const GalleryLayout: React.FC<GalleryLayoutProps> = (props) => {
|
||||
return (
|
||||
<div className="relative h-full overflow-hidden" data-kg-unsplash-gallery>
|
||||
<div ref={props.galleryRef} className={`flex h-full w-full justify-center overflow-auto px-20 ${props?.zoomed ? 'pb-10' : ''}`} data-kg-unsplash-gallery-scrollref>
|
||||
{props.children}
|
||||
{props?.isLoading && <UnsplashGalleryLoading />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const UnsplashGallery: React.FC<UnsplashGalleryProps> = ({zoomed,
|
||||
error,
|
||||
galleryRef,
|
||||
isLoading,
|
||||
dataset,
|
||||
selectImg,
|
||||
insertImage}) => {
|
||||
if (zoomed) {
|
||||
return (
|
||||
<GalleryLayout
|
||||
galleryRef={galleryRef}
|
||||
zoomed={zoomed}>
|
||||
<UnsplashZoomed
|
||||
alt={zoomed.alt_description}
|
||||
height={zoomed.height}
|
||||
insertImage={insertImage}
|
||||
likes={zoomed.likes}
|
||||
links={zoomed.links}
|
||||
payload={zoomed}
|
||||
selectImg={selectImg}
|
||||
srcUrl={zoomed.urls.regular}
|
||||
urls={zoomed.urls}
|
||||
user={zoomed.user}
|
||||
width={zoomed.width}
|
||||
zoomed={zoomed}
|
||||
/>
|
||||
</GalleryLayout>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<GalleryLayout
|
||||
galleryRef={galleryRef}
|
||||
zoomed={zoomed}>
|
||||
<div className="flex h-full flex-col items-center justify-center">
|
||||
<h1 className="mb-4 text-2xl font-bold">Error</h1>
|
||||
<p className="text-lg font-medium">{error}</p>
|
||||
</div>
|
||||
</GalleryLayout>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<GalleryLayout
|
||||
galleryRef={galleryRef}
|
||||
isLoading={isLoading}
|
||||
zoomed={zoomed}>
|
||||
<UnsplashGalleryColumns
|
||||
columns={dataset}
|
||||
insertImage={insertImage}
|
||||
selectImg={selectImg}
|
||||
zoomed={zoomed}
|
||||
/>
|
||||
</GalleryLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default UnsplashGallery;
|
@ -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<UnsplashImageProps> = ({payload, srcUrl, links, likes, user, alt, urls, height, width, zoomed, insertImage, selectImg}) => {
|
||||
const handleClick = (e: MouseEvent<HTMLDivElement>) => {
|
||||
e.stopPropagation();
|
||||
selectImg(zoomed ? null : payload);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`relative mb-6 block bg-grey-100 ${zoomed ? 'h-full w-[max-content] cursor-zoom-out' : 'w-full cursor-zoom-in'}`}
|
||||
data-kg-unsplash-gallery-item
|
||||
onClick={handleClick}>
|
||||
<img
|
||||
alt={alt}
|
||||
className={`${zoomed ? 'h-full w-auto object-contain' : 'block h-auto'}`}
|
||||
height={height}
|
||||
loading='lazy'
|
||||
src={srcUrl}
|
||||
width={width}
|
||||
data-kg-unsplash-gallery-img
|
||||
/>
|
||||
<div className="absolute inset-0 flex flex-col justify-between bg-gradient-to-b from-black/5 via-black/5 to-black/30 p-5 opacity-0 transition-all ease-in-out hover:opacity-100">
|
||||
<div className="flex items-center justify-end gap-3">
|
||||
<UnsplashButton
|
||||
data-kg-button="unsplash-like"
|
||||
href={`${links.html}/?utm_source=ghost&utm_medium=referral&utm_campaign=api-credit`}
|
||||
icon="heart"
|
||||
label={likes.toString()}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
/>
|
||||
<UnsplashButton
|
||||
data-kg-button="unsplash-download"
|
||||
href={`${links.download}/?utm_source=ghost&utm_medium=referral&utm_campaign=api-credit&force=true`}
|
||||
icon="download"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
<img alt="author" className="mr-2 h-8 w-8 rounded-full" src={user.profile_image.medium} />
|
||||
<div className="mr-2 truncate font-sans text-sm font-medium text-white">{user.name}</div>
|
||||
</div>
|
||||
<UnsplashButton label="Insert image" data-kg-unsplash-insert-button onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
insertImage({
|
||||
src: urls.regular.replace(/&w=1080/, '&w=2000'),
|
||||
caption: `<span>Photo by <a href="${user.links.html}">${user.name}</a> / <a href="https://unsplash.com/?utm_source=ghost&utm_medium=referral&utm_campaign=api-credit">Unsplash</a></span>`,
|
||||
height: height,
|
||||
width: width,
|
||||
alt: alt,
|
||||
links: links
|
||||
});
|
||||
}} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default UnsplashImage;
|
@ -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<HTMLInputElement>) => void;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
const UnsplashSelector: FunctionComponent<UnsplashSelectorProps> = ({closeModal, handleSearch, children}) => {
|
||||
return (
|
||||
<>
|
||||
<div className="fixed inset-0 z-40 h-[100vh] bg-black opacity-60"></div>
|
||||
<div className="not-kg-prose fixed inset-8 z-50 overflow-hidden rounded bg-white shadow-xl" data-kg-modal="unsplash">
|
||||
<button className="absolute right-6 top-6 cursor-pointer" type="button">
|
||||
<CloseIcon
|
||||
className="h-4 w-4 stroke-2 text-grey-400"
|
||||
data-kg-modal-close-button
|
||||
onClick={() => closeModal()}
|
||||
/>
|
||||
</button>
|
||||
<div className="flex h-full flex-col">
|
||||
<header className="flex shrink-0 items-center justify-between px-20 py-10">
|
||||
<h1 className="flex items-center gap-2 font-sans text-3xl font-bold text-black">
|
||||
<UnsplashIcon className="mb-1" />
|
||||
Unsplash
|
||||
</h1>
|
||||
<div className="relative w-full max-w-sm">
|
||||
<SearchIcon className="absolute left-4 top-1/2 h-4 w-4 -translate-y-2 text-grey-700" />
|
||||
<input className="h-10 w-full rounded-full border border-solid border-grey-300 pl-10 pr-8 font-sans text-md font-normal text-black focus:border-grey-400 focus-visible:outline-none" placeholder="Search free high-resolution photos" autoFocus data-kg-unsplash-search onChange={handleSearch} />
|
||||
</div>
|
||||
</header>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default UnsplashSelector;
|
@ -1,31 +0,0 @@
|
||||
import UnsplashImage, {UnsplashImageProps} from './UnsplashImage';
|
||||
import {FC} from 'react';
|
||||
import {Photo} from '../UnsplashTypes';
|
||||
|
||||
interface UnsplashZoomedProps extends Omit<UnsplashImageProps, 'zoomed'> {
|
||||
zoomed: Photo | null;
|
||||
selectImg: (photo: Photo | null) => void;
|
||||
}
|
||||
|
||||
const UnsplashZoomed: FC<UnsplashZoomedProps> = ({payload, insertImage, selectImg, zoomed}) => {
|
||||
return (
|
||||
<div className="flex h-full grow basis-0 justify-center" data-kg-unsplash-zoomed onClick={() => selectImg(null)}>
|
||||
<UnsplashImage
|
||||
alt={payload.alt_description}
|
||||
height={payload.height}
|
||||
insertImage={insertImage}
|
||||
likes={payload.likes}
|
||||
links={payload.links}
|
||||
payload={payload}
|
||||
selectImg={selectImg}
|
||||
srcUrl={payload.urls.regular}
|
||||
urls={payload.urls}
|
||||
user={payload.user}
|
||||
width={payload.width}
|
||||
zoomed={zoomed}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default UnsplashZoomed;
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
@ -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);
|
||||
});
|
||||
});
|
@ -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']
|
||||
}
|
||||
}
|
||||
});
|
||||
|
217
yarn.lock
217
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"
|
||||
|
Loading…
Reference in New Issue
Block a user