mirror of
https://github.com/chinchang/web-maker.git
synced 2025-07-24 23:41:14 +02:00
251 lines
6.5 KiB
JavaScript
251 lines
6.5 KiB
JavaScript
import { h } from 'preact';
|
|
import { useState, useEffect, useRef } from 'preact/hooks';
|
|
import { log } from '../utils';
|
|
import { trackEvent } from '../analytics';
|
|
import { ItemTile } from './ItemTile';
|
|
import { Trans, t } from '@lingui/macro';
|
|
import { I18n } from '@lingui/react';
|
|
|
|
export default function SavedItemPane({
|
|
itemsMap,
|
|
isOpen,
|
|
closeHandler,
|
|
onItemSelect,
|
|
onItemRemove,
|
|
onItemFork,
|
|
onExport,
|
|
mergeImportedItems
|
|
}) {
|
|
const [items, setItems] = useState([]);
|
|
const [filteredItems, setFilteredItems] = useState([]);
|
|
const searchInputRef = useRef();
|
|
|
|
useEffect(() => {
|
|
if (!itemsMap) return;
|
|
const newItems = Object.values(itemsMap);
|
|
newItems.sort(function (a, b) {
|
|
return b.updatedOn - a.updatedOn;
|
|
});
|
|
setItems(newItems);
|
|
setFilteredItems(newItems);
|
|
}, [itemsMap]);
|
|
|
|
useEffect(() => {
|
|
// Opening
|
|
if (isOpen) {
|
|
searchInputRef.current.value = '';
|
|
searchInputRef.current.focus();
|
|
}
|
|
// Closing
|
|
if (!isOpen) {
|
|
setFilteredItems([]);
|
|
}
|
|
}, [isOpen]);
|
|
|
|
function onCloseIntent() {
|
|
closeHandler();
|
|
}
|
|
function itemClickHandler(item) {
|
|
onItemSelect(item);
|
|
}
|
|
function itemRemoveBtnClickHandler(item, e) {
|
|
e.stopPropagation();
|
|
onItemRemove(item);
|
|
}
|
|
function itemForkBtnClickHandler(item, e) {
|
|
e.stopPropagation();
|
|
onItemFork(item);
|
|
}
|
|
function keyDownHandler(event) {
|
|
if (!isOpen) {
|
|
return;
|
|
}
|
|
|
|
const isCtrlOrMetaPressed = event.ctrlKey || event.metaKey;
|
|
const isForkKeyPressed = isCtrlOrMetaPressed && event.keyCode === 70;
|
|
const isDownKeyPressed = event.keyCode === 40;
|
|
const isUpKeyPressed = event.keyCode === 38;
|
|
const isEnterKeyPressed = event.keyCode === 13;
|
|
|
|
const selectedItemElement = $('.js-saved-item-tile.selected');
|
|
const havePaneItems = $all('.js-saved-item-tile').length !== 0;
|
|
|
|
if ((isDownKeyPressed || isUpKeyPressed) && havePaneItems) {
|
|
const method = isDownKeyPressed ? 'nextUntil' : 'previousUntil';
|
|
|
|
if (selectedItemElement) {
|
|
selectedItemElement.classList.remove('selected');
|
|
selectedItemElement[method](
|
|
'.js-saved-item-tile:not(.hide)'
|
|
).classList.add('selected');
|
|
} else {
|
|
$('.js-saved-item-tile:not(.hide)').classList.add('selected');
|
|
}
|
|
$('.js-saved-item-tile.selected').scrollIntoView(false);
|
|
}
|
|
|
|
if (isEnterKeyPressed && selectedItemElement) {
|
|
const item = itemsMap[selectedItemElement.dataset.itemId];
|
|
onItemSelect(item);
|
|
trackEvent('ui', 'openItemKeyboardShortcut');
|
|
}
|
|
|
|
// Fork shortcut inside saved creations panel with Ctrl/⌘ + F
|
|
if (isForkKeyPressed) {
|
|
event.preventDefault();
|
|
const item = itemsMap[selectedItemElement.dataset.itemId];
|
|
itemForkBtnClickHandler(item);
|
|
trackEvent('ui', 'forkKeyboardShortcut');
|
|
}
|
|
}
|
|
|
|
function importFileChangeHandler(e) {
|
|
var file = e.target.files[0];
|
|
|
|
var reader = new FileReader();
|
|
reader.addEventListener('load', progressEvent => {
|
|
var items;
|
|
try {
|
|
items = JSON.parse(progressEvent.target.result);
|
|
log(items);
|
|
mergeImportedItems(items);
|
|
} catch (exception) {
|
|
log(exception);
|
|
alert(
|
|
i18n._(
|
|
t`'Oops! Selected file is corrupted. Please select a file that was generated by clicking the "Export" button.`
|
|
)
|
|
);
|
|
}
|
|
});
|
|
|
|
reader.readAsText(file, 'utf-8');
|
|
}
|
|
|
|
function importBtnClickHandler(e) {
|
|
var input = document.createElement('input');
|
|
input.type = 'file';
|
|
input.style.display = 'none';
|
|
input.accept = 'accept="application/json';
|
|
document.body.appendChild(input);
|
|
input.addEventListener('change', importFileChangeHandler);
|
|
input.click();
|
|
trackEvent('ui', 'importBtnClicked');
|
|
e.preventDefault();
|
|
}
|
|
|
|
function searchInputHandler(e) {
|
|
const text = e.target.value.toLowerCase().trim();
|
|
if (!text) {
|
|
setFilteredItems(items);
|
|
} else {
|
|
setFilteredItems(
|
|
items.filter(item => item.title.toLowerCase().indexOf(text) !== -1)
|
|
);
|
|
}
|
|
trackEvent('ui', 'searchInputType');
|
|
}
|
|
|
|
return (
|
|
<I18n>
|
|
{({ i18n }) => (
|
|
<div
|
|
id="js-saved-items-pane"
|
|
class={`saved-items-pane ${isOpen ? 'is-open' : ''}`}
|
|
onKeyDown={keyDownHandler}
|
|
aria-hidden={isOpen}
|
|
>
|
|
<button
|
|
onClick={onCloseIntent}
|
|
class="btn dialog__close-btn saved-items-pane__close-btn"
|
|
id="js-saved-items-pane-close-btn"
|
|
aria-label={i18n._(t`Close saved creations pane`)}
|
|
>
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
strokeWidth="3.5"
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
>
|
|
<line x1="18" y1="6" x2="6" y2="18"></line>
|
|
<line x1="6" y1="6" x2="18" y2="18"></line>
|
|
</svg>
|
|
</button>
|
|
<div
|
|
class="flex flex-v-center"
|
|
style="justify-content: space-between;"
|
|
>
|
|
<h3>
|
|
<Trans>My Library</Trans> ({filteredItems.length})
|
|
</h3>
|
|
|
|
<div>
|
|
<button
|
|
onClick={onExport}
|
|
class="btn--dark hint--bottom-left hint--rounded hint--medium"
|
|
aria-label={i18n._(
|
|
t`Export all your creations into a single importable file.`
|
|
)}
|
|
>
|
|
<Trans>Export</Trans>
|
|
</button>
|
|
<button
|
|
onClick={importBtnClickHandler}
|
|
class="btn--dark hint--bottom-left hint--rounded hint--medium"
|
|
aria-label={i18n._(
|
|
t`Import your creations. Only the file that you export through the 'Export' button can be imported.`
|
|
)}
|
|
>
|
|
<Trans>Import</Trans>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<form autoComplete="off" onSubmit={e => e.preventDefault()}>
|
|
<input
|
|
type="search"
|
|
id="searchInput"
|
|
ref={searchInputRef}
|
|
class="search-input"
|
|
onInput={searchInputHandler}
|
|
placeholder={i18n._(t`Search your creations here...`)}
|
|
/>
|
|
</form>
|
|
|
|
<div id="js-saved-items-wrap" class="saved-items-pane__container">
|
|
{!filteredItems.length && items.length ? (
|
|
<div class="mt-1">
|
|
<Trans>No match found.</Trans>
|
|
</div>
|
|
) : null}
|
|
{filteredItems.map(item => (
|
|
<ItemTile
|
|
item={item}
|
|
onClick={() => itemClickHandler(item)}
|
|
onForkBtnClick={e => itemForkBtnClickHandler(item, e)}
|
|
onRemoveBtnClick={e => itemRemoveBtnClickHandler(item, e)}
|
|
onToggleVisibilityBtnClick={e =>
|
|
itemVisibilityToggleHandler(item, e)
|
|
}
|
|
/>
|
|
))}
|
|
{!items.length ? (
|
|
<div class="tac">
|
|
<h2 class="opacity--30">
|
|
<Trans>Nothing saved here.</Trans>
|
|
</h2>
|
|
<img
|
|
style="max-width: 80%; opacity:0.4"
|
|
src="./assets/empty.svg"
|
|
/>
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</I18n>
|
|
);
|
|
}
|