Rewrite all styles to tailwind CSS from Bootstrap by @arccik (#58)

* add tailwindcss

* add header component with logo and add torrent buttons

* remove bootstrap from few files replace it with tailwindcss classes, add card which diplay all nessesarry information about torrent and current state

* Add modal component and reorganize components folder

* add useModal hook to render modal though react portal, remove UrlPromptModal and replace it with useModal.

* add taliwindcss to Desctop app

* removed bootstrap from deleteTorrentModal replace it with useModal

* replacing bootstrap with useModal

* saving

* Saving

* Header and cards now look good

* Modals still broken...

* still doesnt work

* Finally it scrolls

* Continuing to fix bugs

* Continuing to fix bugs

* Aler

* Getting better

* Desktop doesnt work with tailwind somehow

* Desktop now works with tailwind

* Styles fully work

* (De)select all buttons

* fix alert styles

* Animate progress bar

* Progress bar + error colors

* Fix error message

* Torrent status icon (#56)

* add statusIcon component to display icon of the torrent status

* change props name and remove isDownloading variable

* Tweak styles for icon

* Tweak styles

* Update styles

---------

Co-authored-by: Artur Lozovski <arccik@gmail.com>
This commit is contained in:
Igor Katson 2023-12-14 10:37:29 +00:00 committed by GitHub
parent 911bf3a0d5
commit 50fc7f2f01
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
62 changed files with 7454 additions and 1776 deletions

View file

@ -0,0 +1,87 @@
import { useContext, useState } from "react";
import { AppContext, APIContext } from "../../context";
import { ErrorWithLabel } from "../../rqbit-web";
import { ErrorComponent } from "../ErrorComponent";
import { Spinner } from "../Spinner";
import { Modal } from "./Modal";
import { ModalBody } from "./ModalBody";
import { ModalFooter } from "./ModalFooter";
import { Button } from "../buttons/Button";
export const DeleteTorrentModal: React.FC<{
id: number;
show: boolean;
onHide: () => void;
}> = ({ id, show, onHide }) => {
if (!show) {
return null;
}
const [deleteFiles, setDeleteFiles] = useState(false);
const [error, setError] = useState<ErrorWithLabel | null>(null);
const [deleting, setDeleting] = useState(false);
const ctx = useContext(AppContext);
const API = useContext(APIContext);
const close = () => {
setDeleteFiles(false);
setError(null);
setDeleting(false);
onHide();
};
const deleteTorrent = () => {
setDeleting(true);
const call = deleteFiles ? API.delete : API.forget;
call(id)
.then(() => {
ctx.refreshTorrents();
close();
})
.catch((e) => {
setError({
text: `Error deleting torrent id=${id}`,
details: e,
});
setDeleting(false);
});
};
return (
<Modal isOpen={show} onClose={onHide} title="Delete torrent">
<ModalBody>
<p className="text-gray-700">
Are you sure you want to delete the torrent?
</p>
<div className="mt-4 flex items-center">
<input
type="checkbox"
id="deleteFiles"
className="form-checkbox h-4 w-4 text-blue-500"
onChange={() => setDeleteFiles(!deleteFiles)}
checked={deleteFiles}
placeholder="Also delete files"
/>
<label htmlFor="deleteFiles" className="ml-2 text-gray-700">
Also delete files
</label>
</div>
{error && <ErrorComponent error={error} />}
</ModalBody>
<ModalFooter>
{deleting && <Spinner />}
<Button variant="cancel" onClick={close}>
Cancel
</Button>
<Button variant="danger" onClick={deleteTorrent} disabled={deleting}>
Delete Torrent
</Button>
</ModalFooter>
</Modal>
);
};

View file

@ -0,0 +1,174 @@
import { useCallback, useContext, useEffect, useState } from "react";
import { AddTorrentResponse, AddTorrentOptions } from "../../api-types";
import { AppContext, APIContext } from "../../context";
import { ErrorComponent } from "../ErrorComponent";
import { formatBytes } from "../../helper/formatBytes";
import { ErrorWithLabel } from "../../rqbit-web";
import { Spinner } from "../Spinner";
import { Modal } from "./Modal";
import { ModalBody } from "./ModalBody";
import { ModalFooter } from "./ModalFooter";
import { Button } from "../buttons/Button";
import { FormCheckbox } from "../forms/FormCheckbox";
import { Fieldset } from "../forms/Fieldset";
import { FormInput } from "../forms/FormInput";
import { Form } from "../forms/Form";
export const FileSelectionModal = (props: {
onHide: () => void;
listTorrentResponse: AddTorrentResponse | null;
listTorrentError: ErrorWithLabel | null;
listTorrentLoading: boolean;
data: string | File;
}) => {
let {
onHide,
listTorrentResponse,
listTorrentError,
listTorrentLoading,
data,
} = props;
const [selectedFiles, setSelectedFiles] = useState<number[]>([]);
const [uploading, setUploading] = useState(false);
const [uploadError, setUploadError] = useState<ErrorWithLabel | null>(null);
const [unpopularTorrent, setUnpopularTorrent] = useState(false);
const [outputFolder, setOutputFolder] = useState<string>("");
const ctx = useContext(AppContext);
const API = useContext(APIContext);
// const [Modal, , , closeModal] = useModal({ fullScreen: true });
const selectAll = () => {
setSelectedFiles(
listTorrentResponse
? listTorrentResponse.details.files.map((_, id) => id)
: []
);
};
useEffect(() => {
console.log(listTorrentResponse);
selectAll();
setOutputFolder(listTorrentResponse?.output_folder || "");
}, [listTorrentResponse]);
const clear = () => {
onHide();
setSelectedFiles([]);
setUploadError(null);
setUploading(false);
};
const handleToggleFile = (toggledId: number) => {
if (selectedFiles.includes(toggledId)) {
setSelectedFiles(selectedFiles.filter((i) => i !== toggledId));
} else {
setSelectedFiles([...selectedFiles, toggledId]);
}
};
const handleUpload = async () => {
if (!listTorrentResponse) {
return;
}
setUploading(true);
let initialPeers = listTorrentResponse.seen_peers
? listTorrentResponse.seen_peers.slice(0, 32)
: null;
let opts: AddTorrentOptions = {
overwrite: true,
only_files: selectedFiles,
initial_peers: initialPeers,
output_folder: outputFolder,
};
if (unpopularTorrent) {
opts.peer_opts = {
connect_timeout: 20,
read_write_timeout: 60,
};
}
API.uploadTorrent(data, opts)
.then(
() => {
onHide();
ctx.refreshTorrents();
},
(e) => {
setUploadError({ text: "Error starting torrent", details: e });
}
)
.finally(() => setUploading(false));
};
const getBody = () => {
if (listTorrentLoading) {
return <Spinner />;
} else if (listTorrentError) {
return <ErrorComponent error={listTorrentError}></ErrorComponent>;
} else if (listTorrentResponse) {
return (
<Form>
<Fieldset className="mb-4" label="Pick the files to download">
<div className="mb-3 flex gap-2">
<Button onClick={selectAll} className="text-sm">
Select all
</Button>
<Button onClick={() => setSelectedFiles([])} className="text-sm">
Deselect all
</Button>
</div>
{listTorrentResponse.details.files.map((file, index) => (
<FormCheckbox
key={index}
label={`${file.name} (${formatBytes(file.length)})`}
checked={selectedFiles.includes(index)}
onChange={() => handleToggleFile(index)}
name={`check-${index}`}
/>
))}
</Fieldset>
<Fieldset label="Options">
<FormInput
label="Output folder"
name="output_folder"
inputType="text"
value={outputFolder}
onChange={(e) => setOutputFolder(e.target.value)}
/>
<FormCheckbox
label="Increase timeouts"
checked={unpopularTorrent}
onChange={() => setUnpopularTorrent(!unpopularTorrent)}
help="This might be useful for unpopular torrents with few peers. It will slow down fast torrents though."
name="increase_timeouts"
/>
</Fieldset>
</Form>
);
}
};
return (
<Modal isOpen={true} onClose={clear} title="Add Torrent">
<ModalBody>
{getBody()}
<ErrorComponent error={uploadError} />
</ModalBody>
<ModalFooter>
{uploading && <Spinner />}
<Button onClick={clear} variant="cancel">
Cancel
</Button>
<Button
onClick={handleUpload}
variant="primary"
disabled={
listTorrentLoading || uploading || selectedFiles.length == 0
}
>
OK
</Button>
</ModalFooter>
</Modal>
);
};

View file

@ -0,0 +1,42 @@
import { useContext } from "react";
import { APIContext } from "../../context";
import { ErrorComponent } from "../ErrorComponent";
import { LogStream } from "../LogStream";
import { Modal } from "./Modal";
import { ModalFooter } from "./ModalFooter";
import { ModalBody } from "./ModalBody";
import { Button } from "../buttons/Button";
interface Props {
show: boolean;
onClose: () => void;
}
export const LogStreamModal: React.FC<Props> = ({ show, onClose }) => {
const api = useContext(APIContext);
let logsUrl = api.getStreamLogsUrl();
return (
<Modal
isOpen={show}
onClose={onClose}
title="rqbit server logs"
className="max-w-7xl"
>
<ModalBody>
{logsUrl ? (
<LogStream url={logsUrl} />
) : (
<ErrorComponent
error={{ text: "HTTP API not available to stream logs" }}
></ErrorComponent>
)}
</ModalBody>
<ModalFooter>
<Button variant="primary" onClick={onClose}>
Close
</Button>
</ModalFooter>
</Modal>
);
};

View file

@ -0,0 +1,59 @@
// Modal.tsx
import React, { type ReactNode } from "react";
import RestartModal from "@restart/ui/Modal";
import { BsX } from "react-icons/bs";
interface ModalProps {
isOpen: boolean;
onClose?: () => void;
title: string;
children: ReactNode;
className?: string;
}
const ModalHeader: React.FC<{
onClose?: () => void;
title: string;
}> = ({ onClose, title }) => {
return (
<div className="flex p-3 justify-between items-center border-b">
<h2 className="text-xl font-semibold">{title}</h2>
{onClose && (
<button
className="text-gray-500 hover:text-gray-700"
onClick={onClose}
aria-label="Close modal"
>
<BsX className="w-5 h-5" />
</button>
)}
</div>
);
};
export const Modal: React.FC<ModalProps> = ({
isOpen,
onClose,
title,
children,
className,
}) => {
const renderBackdrop = () => {
return <div className="fixed inset-0 bg-black/30 z-[300]"></div>;
};
return (
<RestartModal
show={isOpen}
onHide={onClose}
renderBackdrop={renderBackdrop}
className={`fixed z-[301] top-0 left-0 w-full h-full block overflow-x-hidden overflow-y-auto`}
>
<div
className={`bg-white shadow-lg my-8 mx-auto max-w-2xl rounded ${className}`}
>
<ModalHeader onClose={onClose} title={title} />
{children}
</div>
</RestartModal>
);
};

View file

@ -0,0 +1,5 @@
import { ReactNode } from "react";
export const ModalBody = ({ children }: { children: ReactNode }) => {
return <div className="p-3 border-b">{children}</div>;
};

View file

@ -0,0 +1,13 @@
import { ReactNode } from "react";
export const ModalFooter = ({
children,
className,
}: {
children: ReactNode;
className?: string;
}) => {
return (
<div className={`p-3 flex justify-end gap-2 ${className}`}>{children}</div>
);
};