1
0
mirror of https://github.com/chinchang/web-maker.git synced 2025-07-24 23:41:14 +02:00
Files
php-web-maker/src/components/SavedItemPane.jsx
2024-04-28 22:39:10 +05:30

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>
);
}