Public Access
feat(import): bookmarklet for store pages
This commit is contained in:
1 parent
c6d76b4cb5
commit
f01ad03f6e
8 files changed
+411
-22
No files matched your search
@@ -1,6 +1,7 @@
|
||||
import {useState} from 'react';
|
||||
import {useTranslation} from 'react-i18next';
|
||||
import {Loader2, Search} from 'lucide-react';
|
||||
import {Copy, Loader2, Search} from 'lucide-react';
|
||||
import {toast} from 'sonner';
|
||||
|
||||
import {Button} from '@/components/ui/button';
|
||||
import {Card} from '@/components/ui/card';
|
||||
@@ -10,14 +11,22 @@ import useStartImport from '../../hooks/useStartImport.js';
|
||||
import {readImportFile} from '../../utils/importFormats.js';
|
||||
import {parseTextList} from '../../utils/imports.js';
|
||||
import {MAX_IMPORT_ROWS} from '../../utils/limits.js';
|
||||
import {bookmarkletHref, consoleScript} from '../../utils/storeList.js';
|
||||
|
||||
export default function ImportInput({onCreated}) {
|
||||
const STORE_SITES = 'PlayStation, Xbox, Nintendo eShop, Epic Games, GOG';
|
||||
// React 19 blocks javascript: URLs in href. One function, so React sets it only when the link mounts.
|
||||
const setBookmarklet = (link) => link?.setAttribute('href', bookmarkletHref(location.origin));
|
||||
|
||||
/**
|
||||
* @param storeRows the games the bookmarklet read on a store page, or null
|
||||
*/
|
||||
export default function ImportInput({storeRows, onCreated}) {
|
||||
const {t} = useTranslation();
|
||||
const [source, setSource] = useState('TEXT');
|
||||
const [source, setSource] = useState(storeRows ? 'BOOKMARKLET' : 'TEXT');
|
||||
const [text, setText] = useState('');
|
||||
const [csv, setCsv] = useState(null);
|
||||
const [busy, setBusy] = useState(false);
|
||||
const rows = source === 'TEXT' ? parseTextList(text) : csv?.rows ?? [];
|
||||
const rows = source === 'TEXT' ? parseTextList(text) : (source === 'CSV' ? csv?.rows : storeRows) ?? [];
|
||||
const tooMany = rows.length > MAX_IMPORT_ROWS;
|
||||
|
||||
const readFile = async (file) => setCsv(file ? readImportFile(await file.text()) : null);
|
||||
@@ -35,12 +44,14 @@ export default function ImportInput({onCreated}) {
|
||||
<Card className="gap-4 p-5">
|
||||
<p className="text-sm text-muted-foreground">{t('IMPORT.HINT')}</p>
|
||||
<div role="group" aria-label={t('IMPORT.SOURCE')} className="flex gap-2">
|
||||
{['TEXT', 'CSV'].map((value) => (
|
||||
{['TEXT', 'CSV', 'BOOKMARKLET'].map((value) => (
|
||||
<Button key={value} size="sm" variant={source === value ? 'default' : 'outline'} aria-pressed={source === value}
|
||||
onClick={() => setSource(value)}>{t(`IMPORT.SOURCE_${value}`)}</Button>
|
||||
))}
|
||||
</div>
|
||||
{source === 'TEXT' ? (
|
||||
{source === 'BOOKMARKLET' ? (
|
||||
<StoreInput rows={storeRows}/>
|
||||
) : source === 'TEXT' ? (
|
||||
<Textarea rows={10} value={text} aria-label={t('IMPORT.TEXT_LABEL')} placeholder={t('IMPORT.TEXT_PLACEHOLDER')}
|
||||
onChange={(e) => setText(e.target.value)}/>
|
||||
) : (
|
||||
@@ -52,15 +63,7 @@ export default function ImportInput({onCreated}) {
|
||||
{csv?.rows && csv.format !== 'CSV' && (
|
||||
<p className="text-sm">{t('IMPORT.FORMAT_READ', {format: t(`IMPORT.SOURCE_${csv.format}`)})}</p>
|
||||
)}
|
||||
{csv?.rows?.length > 0 && (
|
||||
<ul className="text-sm text-muted-foreground">
|
||||
{csv.rows.slice(0, 5).map((row, index) => (
|
||||
<li key={index} className="truncate">
|
||||
{[row.title, row.platform, row.status].filter(Boolean).join(' · ')}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
<RowPreview rows={csv?.rows}/>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
@@ -74,3 +77,43 @@ export default function ImportInput({onCreated}) {
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
const RowPreview = ({rows}) => rows?.length > 0 && (
|
||||
<ul className="text-sm text-muted-foreground">
|
||||
{rows.slice(0, 5).map((row, index) => (
|
||||
<li key={index} className="truncate">{[row.title, row.platform, row.status].filter(Boolean).join(' · ')}</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
|
||||
function StoreInput({rows}) {
|
||||
const {t} = useTranslation();
|
||||
|
||||
const copyScript = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(consoleScript(location.origin));
|
||||
toast.success(t('IMPORT.STORE_COPIED'));
|
||||
} catch {
|
||||
toast.error(t('IMPORT.STORE_COPY_FAILED'));
|
||||
}
|
||||
};
|
||||
|
||||
if (rows) {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm">{t('IMPORT.STORE_READ', {count: rows.length})}</p>
|
||||
<RowPreview rows={rows}/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="space-y-3 text-sm">
|
||||
<p>{t('IMPORT.STORE_HINT', {sites: STORE_SITES})}</p>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<a ref={setBookmarklet} onClick={(e) => e.preventDefault()} className="rounded-md border border-dashed px-3 py-1.5 font-medium">{t('IMPORT.STORE_LINK')}</a>
|
||||
<Button size="sm" variant="outline" onClick={copyScript}><Copy/>{t('IMPORT.STORE_COPY')}</Button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">{t('IMPORT.STORE_NOTE')}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user