feat: Added upload dialog

This commit is contained in:
florian 2024-03-27 17:38:24 +01:00
parent 5bc7831222
commit f7cb216c70
9 changed files with 146 additions and 12 deletions

View File

@ -3,7 +3,7 @@
}
.content {
@apply flex flex-col self-center sm:w-10/12 lg:w-4/6 w-full;
@apply flex flex-col self-center sm:w-10/12 lg:w-4/6 w-full min-h-[80vh]
}
.title {
@ -14,10 +14,22 @@
@apply w-10;
}
.title a.action {
@apply flex flex-col text-sm items-center opacity-50 hover:opacity-100 cursor-pointer px-2;
}
.title a.logo {
@apply flex flex-row flex-grow items-center gap-2 cursor-pointer;
}
.title span {
@apply flex-grow;
}
.title svg {
@apply w-8;
}
.avatar {
@apply flex-shrink;
}

View File

@ -1,14 +1,23 @@
import { Outlet } from 'react-router-dom';
import { Outlet, useNavigate } from 'react-router-dom';
import { useNDK } from '../../ndk';
import './Layout.css';
import { ArrowUpOnSquareIcon } from '@heroicons/react/24/outline';
export const Layout = () => {
const { user } = useNDK();
const navigate = useNavigate();
return (
<div className="main">
<div className="title">
<a className="logo" onClick={() => navigate('/')}>
<img src="/bouquet.png" /> <span>bouquet</span>
</a>
<div>
<a className='action' onClick={() => navigate('/upload')}>
<ArrowUpOnSquareIcon />
</a>
</div>
<div className="avatar">
<img src={user?.profile?.image} />
</div>

View File

@ -5,22 +5,23 @@ import {
ClockIcon,
CubeIcon,
DocumentDuplicateIcon,
ExclamationCircleIcon,
ExclamationTriangleIcon,
ServerIcon,
ShieldExclamationIcon,
XMarkIcon,
} from '@heroicons/react/24/outline';
import { Server } from '../../utils/useServers';
import { Server as ServerType } from '../../utils/useServers';
import { ServerInfo } from '../../utils/useServerInfo';
import { formatDate, formatFileSize } from '../../utils';
type ServerProps = {
server: Server;
server: ServerType;
serverInfo: ServerInfo;
selectedServer?: string | undefined;
setSelectedServer?: React.Dispatch<React.SetStateAction<string | undefined>>;
onTransfer?: (server: string) => void;
onCancel?: () => void;
onCheck?: (server: string) => void;
blobsOnlyOnThisServer: number;
};
@ -31,6 +32,7 @@ const Server = ({
serverInfo,
onTransfer,
onCancel,
onCheck,
blobsOnlyOnThisServer,
}: ServerProps) => {
return (
@ -75,11 +77,20 @@ const Server = ({
</div>
{((selectedServer == server.name && onTransfer) || onCancel) && (
<div className="server-actions">
{selectedServer == server.name && onTransfer && (
{selectedServer == server.name && (
<>
{onCheck && (
<a onClick={() => onCheck(server.name)}>
<ShieldExclamationIcon /> Check
</a>
)}
{onTransfer && (
<a onClick={() => onTransfer(server.name)}>
<ArrowUpOnSquareStackIcon /> Transfer
</a>
)}
</>
)}
{onCancel && (
<a onClick={() => onCancel()}>
<XMarkIcon />

View File

@ -32,8 +32,12 @@
@apply w-10;
}
.server-actions {
@apply flex flex-row gap-4;
}
.server-actions a {
@apply cursor-pointer text-center flex flex-col items-center hover:text-white opacity-80 hover:opacity-100;
@apply cursor-pointer text-center flex flex-col items-center hover:text-white opacity-80 hover:opacity-100 gap-1;
}
@keyframes spin {

View File

@ -9,9 +9,10 @@ type ServerListProps = {
setSelectedServer?: React.Dispatch<React.SetStateAction<string | undefined>>;
onTransfer?: (server: string) => void;
onCancel?: () => void;
onCheck?: (server: string) => void;
};
export const ServerList = ({ servers, selectedServer, setSelectedServer, onTransfer, onCancel }: ServerListProps) => {
export const ServerList = ({ servers, selectedServer, setSelectedServer, onTransfer, onCancel, onCheck }: ServerListProps) => {
const { serverInfo, distribution } = useServerInfo();
const blobsWithOnlyOneOccurance = Object.values(distribution)
.filter(d => d.servers.length == 1)
@ -28,6 +29,7 @@ export const ServerList = ({ servers, selectedServer, setSelectedServer, onTrans
setSelectedServer={setSelectedServer}
onTransfer={onTransfer}
onCancel={onCancel}
/* onCheck={onCheck} */
blobsOnlyOnThisServer={blobsWithOnlyOneOccurance.filter(b => b.server == server.name).length}
></Server>
))}

View File

@ -8,6 +8,8 @@ import { Route, RouterProvider, createBrowserRouter, createRoutesFromElements }
import { Layout } from './components/Layout/Layout.tsx';
import Home from './pages/Home.tsx';
import { Transfer } from './pages/Transfer.tsx';
import Upload from './pages/Upload.tsx';
import Check from './pages/Check.tsx';
const queryClient = new QueryClient();
@ -16,6 +18,8 @@ const router = createBrowserRouter(
<Route element={<Layout />}>
<Route path="/" element={<Home />} />
<Route path="/transfer/:source" element={<Transfer />} />
<Route path="/upload" element={<Upload/>} />
<Route path="/check/:source" element={<Check/>} />
</Route>
)
);

24
src/pages/Check.tsx Normal file
View File

@ -0,0 +1,24 @@
import { useNavigate, useParams } from 'react-router-dom';
import { ServerList } from '../components/ServerList/ServerList';
import { useServerInfo } from '../utils/useServerInfo';
function Check() {
const { serverInfo } = useServerInfo();
const navigate = useNavigate();
const { source } = useParams();
return (
<>
<h2>Check integrity</h2>
<p className="py-4 text-neutral-400">
Downloads all objects from the server and checks the integrity of the content.
</p>
<ServerList
servers={Object.values(serverInfo).filter(s => s.name == source)}
onCancel={() => navigate('/')}
></ServerList>
</>
);
}
export default Check;

View File

@ -64,6 +64,7 @@ function Home() {
selectedServer={selectedServer}
setSelectedServer={setSelectedServer}
onTransfer={() => navigate('/transfer/' + selectedServer)}
onCheck={() => navigate('/check/' + selectedServer)}
></ServerList>
{selectedServer && serverInfo[selectedServer] && selectedServerBlobs && (

67
src/pages/Upload.tsx Normal file
View File

@ -0,0 +1,67 @@
import { useEffect, useRef, useState } from 'react';
import { useServers } from '../utils/useServers';
import { BlossomClient } from 'blossom-client-sdk';
import { useNDK } from '../ndk';
import { useServerInfo } from '../utils/useServerInfo';
import { useQueryClient } from '@tanstack/react-query';
function Upload() {
const servers = useServers();
const { signEventTemplate } = useNDK();
const { serverInfo } = useServerInfo();
const inputRef = useRef<HTMLInputElement>(null);
const queryClient = useQueryClient();
const [uploadTarget, setUploadTarget] = useState<{ [key: string]: boolean }>({});
const upload = async () => {
if (inputRef.current && inputRef.current.files) {
for (const server of servers) {
if (!uploadTarget[server.name]) {
continue;
}
const serverUrl = serverInfo[server.name].url;
for (const file of inputRef.current.files) {
const uploadAuth = await BlossomClient.getUploadAuth(file, signEventTemplate, 'Upload Blob');
const newBlob = await BlossomClient.uploadBlob(serverUrl, file, uploadAuth);
console.log(newBlob);
}
queryClient.invalidateQueries({ queryKey: ['blobs', server.name] });
}
}
};
useEffect(() => {
setUploadTarget(servers.reduce((acc, s) => ({ ...acc, [s.name]: true }), {}));
}, [servers]);
return (
<>
<h2>Upload</h2>
<div className=" bg-neutral-800 rounded-xl p-4 text-neutral-400 gap-4 flex flex-col">
<input className=" cursor-pointer" type="file" ref={inputRef} multiple />
{servers.map(s => (
<div className="cursor-pointer flex flex-row gap-2 " key={s.name}>
<input
className="w-5 accent-pink-700 hover:accent-pink-600"
id={s.name}
type="checkbox"
checked={uploadTarget[s.name] || false}
onChange={e => setUploadTarget(ut => ({ ...ut, [s.name]: e.target.checked }))}
/>
<label htmlFor={s.name} className="cursor-pointer">
{s.name}
</label>
</div>
))}
<button className="p-2 px-4 bg-neutral-600 hover:bg-pink-700 text-white rounded-lg w-2/6" onClick={() => upload()}>
Upload
</button>
</div>
</>
);
}
export default Upload;