feat: Added router

This commit is contained in:
florian 2024-03-27 10:45:46 +01:00
parent 3a8ba07903
commit e66b41560d
11 changed files with 287 additions and 270 deletions

View File

@ -23,7 +23,8 @@
"dayjs": "^1.11.10",
"nostr-tools": "^2.3.2",
"react": "^18.2.0",
"react-dom": "^18.2.0"
"react-dom": "^18.2.0",
"react-router-dom": "^6.22.3"
},
"devDependencies": {
"@tanstack/eslint-plugin-query": "^5.28.6",

View File

@ -1,7 +1,8 @@
import { Outlet } from 'react-router-dom';
import { useNDK } from '../../ndk';
import './Layout.css';
export const Layout = ({ children }: { children: React.ReactElement }) => {
export const Layout = () => {
const { user } = useNDK();
return (
@ -12,7 +13,7 @@ export const Layout = ({ children }: { children: React.ReactElement }) => {
<img src={user?.profile?.image} />
</div>
</div>
<div className="content">{children}</div>
<div className="content">{<Outlet />}</div>
<div className="footer">
made with 💜 by{' '}
<a href="https://njump.me/npub1klr0dy2ul2dx9llk58czvpx73rprcmrvd5dc7ck8esg8f8es06qs427gxc">florian</a>

View File

@ -3,7 +3,7 @@
}
.server {
@apply bg-neutral-800 text-neutral-300 rounded-lg p-4 gap-4 cursor-pointer hover:bg-neutral-700 flex flex-row items-center;
@apply bg-neutral-800 text-neutral-300 rounded-lg p-4 gap-4 flex flex-row items-center;
}
.server.selected {

View File

@ -27,7 +27,10 @@ export const ServerList = ({ servers, selectedServer, setSelectedServer, onTrans
<div className="server-list">
{servers.map((server, sx) => (
<div
className={`server ${selectedServer == server.name ? 'selected' : ''}`}
className={
`server ${selectedServer == server.name ? 'selected' : ''} ` +
`${setSelectedServer ? ' hover:bg-neutral-700 cursor-pointer' : ''} `
}
key={sx}
onClick={() => setSelectedServer && setSelectedServer(server.name)}
>

View File

@ -1,54 +0,0 @@
.message {
@apply text-3xl text-center text-white p-12;
}
.message svg {
@apply w-8 inline;
}
.action-button {
@apply bg-white text-lg text-black hover:bg-pink-600 hover:text-white inline-block rounded-lg p-2 pr-4 pl-3;
}
.action-button svg {
@apply w-5 inline align-text-bottom;
}
.error-log {
@apply bg-neutral-800 p-4 text-neutral-300 rounded-lg;
}
.error-log svg {
@apply w-5 inline align-text-bottom mr-1;
}
.error-log div {
@apply p-1 hover:bg-neutral-700 rounded-md grid pr-4;
grid-template-columns: 2em auto 6em 10em 7em 1em;
}
.error-log div span {
@apply overflow-ellipsis overflow-hidden text-nowrap;
}
.blob-list svg {
@apply w-5 inline align-text-bottom mr-1;
}
.blob-list a {
@apply text-pink-500 hover:text-white;
}
.blob-list {
}
.blob-list .blob {
}
.blob-list .blob a {
@apply cursor-pointer;
}

View File

@ -1,171 +0,0 @@
import { ArrowDownOnSquareIcon, ArrowUpOnSquareIcon, CheckBadgeIcon, DocumentIcon } from '@heroicons/react/24/outline';
import { ServerList } from '../ServerList/ServerList';
import { useServerInfo } from '../../utils/useServerInfo';
import { useMemo, useState } from 'react';
import { BlobDescriptor, BlossomClient } from 'blossom-client-sdk';
import { useNDK } from '../../ndk';
import { useQueryClient } from '@tanstack/react-query';
import { formatFileSize } from '../../utils';
import BlobList from '../BlobList/BlobList';
import './Transfer.css';
type TransferStatus = {
[key: string]: {
sha256: string;
status: 'pending' | 'done' | 'error';
message?: string;
size: number;
};
};
export const Transfer = ({ transferSource, onCancel }: { transferSource: string; onCancel?: () => void }) => {
const serverInfo = useServerInfo();
const [transferTarget, setTransferTarget] = useState<string | undefined>();
const { signEventTemplate } = useNDK();
const queryClient = useQueryClient();
const [started, setStarted] = useState(false);
const [transferLog, setTransferLog] = useState<TransferStatus>({});
const closeTransferMode = () => {
queryClient.invalidateQueries({ queryKey: ['blobs', transferTarget] });
setTransferTarget(undefined);
setTransferLog({});
setStarted(false);
onCancel && onCancel();
};
const transferJobs = useMemo(() => {
if (transferSource && transferTarget) {
const sourceBlobs = serverInfo[transferSource].blobs;
const targetBlobs = serverInfo[transferTarget].blobs;
return sourceBlobs?.filter(src => targetBlobs?.find(tgt => tgt.sha256 == src.sha256) == undefined);
}
return [];
}, [serverInfo, transferSource, transferTarget]);
// https://github.com/sindresorhus/p-limit
//
const performTransfer = async (sourceServer: string, targetServer: string, blobs: BlobDescriptor[]) => {
setTransferLog({});
setStarted(true);
for (const b of blobs) {
try {
// BlossomClient.getGetAuth()
setTransferLog(ts => ({ ...ts, [b.sha256]: { sha256: b.sha256, status: 'pending', size: b.size } }));
const data = await BlossomClient.getBlob(serverInfo[sourceServer].url, b.sha256);
const file = new File([data], b.sha256, { type: b.type, lastModified: b.created });
const uploadAuth = await BlossomClient.getUploadAuth(file, signEventTemplate, 'Upload Blob');
await BlossomClient.uploadBlob(serverInfo[targetServer].url, file, uploadAuth);
setTransferLog(ts => ({ ...ts, [b.sha256]: { sha256: b.sha256, status: 'done', size: b.size } }));
} catch (e) {
setTransferLog(ts => ({
...ts,
[b.sha256]: { sha256: b.sha256, status: 'error', message: (e as Error).message, size: blobs.length },
}));
console.warn(e);
}
}
// if (Object.values(transferLog).filter(b => b.status == 'error').length == 0) {
// closeTransferMode();
// }
};
const transferStatus = useMemo(() => {
const stats = Object.values(transferLog).reduce(
(acc, t) => {
if (t.status === 'done') {
acc.done += 1;
} else if (t.status === 'error') {
acc.error += 1;
} else {
acc.pending += 1;
}
acc.size += t.size;
return acc;
},
{ pending: 0, done: 0, error: 0, size: 0 }
);
return { ...stats, fullSize: transferJobs?.reduce((acc, b) => acc + b.size, 0) || 0 };
}, [transferLog, transferJobs]);
return (
<>
<h2>
<ArrowUpOnSquareIcon /> Transfer Source
</h2>
<ServerList
servers={Object.values(serverInfo).filter(s => s.name == transferSource)}
onCancel={() => closeTransferMode()}
></ServerList>
<h2>
<ArrowDownOnSquareIcon /> Transfer Target
</h2>
<ServerList
servers={Object.values(serverInfo)
.filter(s => s.name != transferSource)
.sort()}
selectedServer={transferTarget}
setSelectedServer={setTransferTarget}
></ServerList>
{transferTarget && transferJobs && transferJobs.length > 0 ? (
<>
<div className="message">
{transferJobs.length} object{transferJobs.length > 1 ? 's' : ''} to transfer{' '}
{!started && (
<button
className="action-button"
onClick={() => performTransfer(transferSource, transferTarget, transferJobs)}
>
<ArrowUpOnSquareIcon />
Start
</button>
)}
</div>
<div>
<div className="w-full bg-gray-200 rounded-lg dark:bg-neutral-800">
<div
className="bg-pink-600 text-sm font-medium text-pink-100 text-center p-1 leading-none rounded-lg"
style={{ width: `${Math.floor((transferStatus.size * 100) / transferStatus.fullSize)}%` }}
>
{Math.floor((transferStatus.size * 100) / transferStatus.fullSize)}&nbsp;%
</div>
</div>
{
<div className="message">
{formatFileSize(transferStatus.size)} / {formatFileSize(transferStatus.fullSize)} transfered.
</div>
}
<div className="error-log">
{Object.values(transferLog)
.filter(b => b.status == 'error')
.map(t => (
<div>
<span>
<DocumentIcon />
</span>
<span>{t.sha256}</span>
<span>{formatFileSize(t.size)}</span>
<span>{t.status && `${t.status}`}</span>
<span>{t.message}</span>
</div>
))}
</div>
</div>
{!started && <BlobList blobs={transferJobs}></BlobList>}
</>
) : (
<div className="message">
{transferTarget ? (
<>
<CheckBadgeIcon /> no missing objects to transfer
</>
) : (
<>choose a transfer target above</>
)}
</div>
)}
</>
);
};

View File

@ -1,18 +1,30 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App.tsx';
import './index.css';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { NDKContextProvider } from './ndk.tsx';
import { Route, RouterProvider, createBrowserRouter, createRoutesFromElements } from 'react-router-dom';
import { Layout } from './components/Layout/Layout.tsx';
import Home from './pages/Home.tsx';
import { Transfer } from './pages/Transfer.tsx';
const queryClient = new QueryClient();
const router = createBrowserRouter(
createRoutesFromElements(
<Route element={<Layout />}>
<Route path="/" element={<Home />} />
<Route path="/transfer/:source" element={<Transfer />} />
</Route>
)
);
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<QueryClientProvider client={queryClient}>
<NDKContextProvider>
<App />
<RouterProvider router={router} />
</NDKContextProvider>
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>

View File

@ -1,19 +1,19 @@
import { useEffect, useMemo, useState } from 'react';
import './App.css';
import './Home.css';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { BlobDescriptor, BlossomClient } from 'blossom-client-sdk';
import { useNDK } from './ndk';
import BlobList from './components/BlobList/BlobList';
import { useServerInfo } from './utils/useServerInfo';
import { ServerList } from './components/ServerList/ServerList';
import { Layout } from './components/Layout/Layout';
import { Transfer } from './components/Transfer/Transfer';
import { useNDK } from '../ndk';
import BlobList from '../components/BlobList/BlobList';
import { useServerInfo } from '../utils/useServerInfo';
import { ServerList } from '../components/ServerList/ServerList';
import { useNavigate } from 'react-router-dom';
/* BOUQUET Blob Organizer Update Quality Use Enhancement Tool */
// TODOs
/*
- multi threaded sync
- Add server and pulbish list event
- upload to single/multi servers
- upload exif data removal
- upload image resize
@ -21,11 +21,11 @@ import { Transfer } from './components/Transfer/Transfer';
- thumbnail gallery
- check blobs (download & sha256 sum check), maybe limit max size
*/
function App() {
function Home() {
const { loginWithExtension, signEventTemplate } = useNDK();
const [selectedServer, setSelectedServer] = useState<string | undefined>();
const [transferSource, setTransferSource] = useState<string | undefined>();
const serverInfo = useServerInfo();
const navigate = useNavigate();
useEffect(() => {
loginWithExtension();
@ -79,38 +79,32 @@ function App() {
);
return (
<Layout>
{transferSource ? (
<Transfer transferSource={transferSource} onCancel={() => setTransferSource(undefined)} />
) : (
<>
<h2>Servers</h2>
<ServerList
servers={Object.values(serverInfo).sort()}
selectedServer={selectedServer}
setSelectedServer={setSelectedServer}
onTransfer={server => setTransferSource(server)}
></ServerList>
<>
<h2>Servers</h2>
<ServerList
servers={Object.values(serverInfo).sort()}
selectedServer={selectedServer}
setSelectedServer={setSelectedServer}
onTransfer={() => navigate('/transfer/' + selectedServer)}
></ServerList>
{selectedServer && serverInfo[selectedServer] && selectedServerBlobs && (
<>
<h2>Your objects on {serverInfo[selectedServer].name}</h2>
<BlobList
blobs={selectedServerBlobs}
onDelete={blob =>
deleteBlob.mutate({
serverName: serverInfo[selectedServer].name,
serverUrl: serverInfo[selectedServer].url,
hash: blob.sha256,
})
}
></BlobList>
</>
)}
{selectedServer && serverInfo[selectedServer] && selectedServerBlobs && (
<>
<h2>Your objects on {serverInfo[selectedServer].name}</h2>
<BlobList
blobs={selectedServerBlobs}
onDelete={blob =>
deleteBlob.mutate({
serverName: serverInfo[selectedServer].name,
serverUrl: serverInfo[selectedServer].url,
hash: blob.sha256,
})
}
></BlobList>
</>
)}
</Layout>
</>
);
}
export default App;
export default Home;

49
src/pages/Transfer.css Normal file
View File

@ -0,0 +1,49 @@
.message {
@apply text-3xl text-center text-white p-12;
}
.message svg {
@apply w-8 inline;
}
.action-button {
@apply bg-white text-lg text-black hover:bg-pink-600 hover:text-white inline-block rounded-lg p-2 pr-4 pl-3;
}
.action-button svg {
@apply w-5 inline align-text-bottom;
}
.error-log {
@apply bg-neutral-800 p-4 text-neutral-300 rounded-lg;
}
.error-log svg {
@apply w-5 inline align-text-bottom mr-1;
}
.error-log div {
@apply p-1 hover:bg-neutral-700 rounded-md grid pr-4;
grid-template-columns: 2em auto 6em 2em 15em;
}
.error-log div span {
@apply overflow-ellipsis overflow-hidden text-nowrap;
}
.blob-list svg {
@apply w-5 inline align-text-bottom mr-1;
}
.blob-list a {
@apply text-pink-500 hover:text-white;
}
.blob-list {
}
.blob-list .blob {
}
.blob-list .blob a {
@apply cursor-pointer;
}

182
src/pages/Transfer.tsx Normal file
View File

@ -0,0 +1,182 @@
import {
ArrowDownOnSquareIcon,
ArrowUpOnSquareIcon,
CheckBadgeIcon,
DocumentIcon,
ExclamationTriangleIcon,
} from '@heroicons/react/24/outline';
import { ServerList } from '../components/ServerList/ServerList';
import { useServerInfo } from '../utils/useServerInfo';
import { useMemo, useState } from 'react';
import { BlobDescriptor, BlossomClient } from 'blossom-client-sdk';
import { useNDK } from '../ndk';
import { useQueryClient } from '@tanstack/react-query';
import { formatFileSize } from '../utils';
import BlobList from '../components/BlobList/BlobList';
import './Transfer.css';
import { useNavigate, useParams } from 'react-router-dom';
type TransferStatus = {
[key: string]: {
sha256: string;
status: 'pending' | 'done' | 'error';
message?: string;
size: number;
};
};
export const Transfer = () => {
const { source: transferSource } = useParams();
const navigate = useNavigate();
const serverInfo = useServerInfo();
const [transferTarget, setTransferTarget] = useState<string | undefined>();
const { signEventTemplate } = useNDK();
const queryClient = useQueryClient();
const [started, setStarted] = useState(false);
const [transferLog, setTransferLog] = useState<TransferStatus>({});
const closeTransferMode = () => {
queryClient.invalidateQueries({ queryKey: ['blobs', transferTarget] });
setTransferTarget(undefined);
setTransferLog({});
setStarted(false);
navigate('/');
};
const transferJobs = useMemo(() => {
if (transferSource && transferTarget) {
const sourceBlobs = serverInfo[transferSource].blobs;
const targetBlobs = serverInfo[transferTarget].blobs;
return sourceBlobs?.filter(src => targetBlobs?.find(tgt => tgt.sha256 == src.sha256) == undefined);
}
return [];
}, [serverInfo, transferSource, transferTarget]);
// https://github.com/sindresorhus/p-limit
//
const performTransfer = async (sourceServer: string, targetServer: string, blobs: BlobDescriptor[]) => {
setTransferLog({});
setStarted(true);
for (const b of blobs) {
try {
// BlossomClient.getGetAuth()
setTransferLog(ts => ({ ...ts, [b.sha256]: { sha256: b.sha256, status: 'pending', size: b.size } }));
const data = await BlossomClient.getBlob(serverInfo[sourceServer].url, b.sha256);
const file = new File([data], b.sha256, { type: b.type, lastModified: b.created });
const uploadAuth = await BlossomClient.getUploadAuth(file, signEventTemplate, 'Upload Blob');
await BlossomClient.uploadBlob(serverInfo[targetServer].url, file, uploadAuth);
setTransferLog(ts => ({ ...ts, [b.sha256]: { sha256: b.sha256, status: 'done', size: b.size } }));
} catch (e) {
setTransferLog(ts => ({
...ts,
[b.sha256]: { sha256: b.sha256, status: 'error', message: (e as Error).message, size: blobs.length },
}));
console.warn(e);
}
}
// if (Object.values(transferLog).filter(b => b.status == 'error').length == 0) {
// closeTransferMode();
// }
};
const transferStatus = useMemo(() => {
const stats = Object.values(transferLog).reduce(
(acc, t) => {
if (t.status === 'done') {
acc.done += 1;
} else if (t.status === 'error') {
acc.error += 1;
} else {
acc.pending += 1;
}
acc.size += t.status == 'done' ? t.size : 0;
return acc;
},
{ pending: 0, done: 0, error: 0, size: 0 }
);
return { ...stats, fullSize: transferJobs?.reduce((acc, b) => acc + b.size, 0) || 0 };
}, [transferLog, transferJobs]);
return (
transferSource && (
<>
<h2>
<ArrowUpOnSquareIcon /> Transfer Source
</h2>
<ServerList
servers={Object.values(serverInfo).filter(s => s.name == transferSource)}
onCancel={() => closeTransferMode()}
></ServerList>
<h2>
<ArrowDownOnSquareIcon /> Transfer Target
</h2>
<ServerList
servers={Object.values(serverInfo)
.filter(s => s.name != transferSource)
.sort()}
selectedServer={transferTarget}
setSelectedServer={setTransferTarget}
></ServerList>
{transferTarget && transferJobs && transferJobs.length > 0 ? (
<>
<div className="message">
{transferJobs.length} object{transferJobs.length > 1 ? 's' : ''} to transfer{' '}
{!started && (
<button
className="action-button"
onClick={() => performTransfer(transferSource, transferTarget, transferJobs)}
>
<ArrowUpOnSquareIcon />
Start
</button>
)}
</div>
<div>
<div className="w-full bg-gray-200 rounded-lg dark:bg-neutral-800">
<div
className="bg-pink-600 text-sm font-medium text-pink-100 text-center p-1 leading-none rounded-lg"
style={{ width: `${Math.floor((transferStatus.size * 100) / transferStatus.fullSize)}%` }}
>
{Math.floor((transferStatus.size * 100) / transferStatus.fullSize)}&nbsp;%
</div>
</div>
{
<div className="message">
{formatFileSize(transferStatus.size)} / {formatFileSize(transferStatus.fullSize)} transferred
</div>
}
<div className="error-log">
{Object.values(transferLog)
.filter(b => b.status == 'error')
.map(t => (
<div>
<span>
<DocumentIcon />
</span>
<span>{t.sha256}</span>
<span>{formatFileSize(t.size)}</span>
<span>{t.status && (t.status == 'error' ? <ExclamationTriangleIcon /> : '')}</span>
<span>{t.message}</span>
</div>
))}
</div>
</div>
{!started && <BlobList blobs={transferJobs}></BlobList>}
</>
) : (
<div className="message">
{transferTarget ? (
<>
<CheckBadgeIcon /> no missing objects to transfer
</>
) : (
<>choose a transfer target above</>
)}
</div>
)}
</>
)
);
};