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,31 @@
import { ReactNode } from "react";
export const Button: React.FC<{
onClick: React.MouseEventHandler<HTMLButtonElement>;
variant?: "cancel" | "primary" | "secondary" | "danger" | "none";
className?: string;
disabled?: boolean;
children: ReactNode;
}> = ({ onClick, children, className, disabled, variant }) => {
let variantClassNames = {
secondary:
"hover:bg-blue-600 transition-colors duration-100 hover:text-white",
danger:
"bg-red-500 text-white border-green-50 hover:border-red-700 hover:bg-red-600",
primary: "bg-blue-400 text-white hover:bg-blue-600",
cancel: "bg-slate-50 hover:bg-slate-200",
none: "",
}[variant ?? "secondary"];
return (
<button
disabled={disabled}
onClick={(e) => {
e.preventDefault();
onClick(e);
}}
className={`flex inline-flex items-center gap-1 border rounded-lg border disabled:cursor-not-allowed px-2 py-1 transition-colors duration-300 ${variantClassNames} ${className}`}
>
{children}
</button>
);
};

View file

@ -0,0 +1,52 @@
import { RefObject, useRef, useState } from "react";
import { UploadButton } from "./UploadButton";
import { CgFileAdd } from "react-icons/cg";
export const FileInput = ({ className }: { className?: string }) => {
const inputRef = useRef<HTMLInputElement>() as RefObject<HTMLInputElement>;
const [file, setFile] = useState<File | null>(null);
const onFileChange = async () => {
if (!inputRef?.current?.files) {
return;
}
const file = inputRef.current.files[0];
setFile(file);
};
const reset = () => {
if (!inputRef?.current) {
return;
}
inputRef.current.value = "";
setFile(null);
};
const onClick = () => {
if (!inputRef?.current) {
return;
}
inputRef.current.click();
};
return (
<>
<input
type="file"
ref={inputRef}
accept=".torrent"
onChange={onFileChange}
hidden
/>
<UploadButton
onClick={onClick}
data={file}
resetData={reset}
className={className}
>
<CgFileAdd color="blue" />
<div>Upload .torrent File</div>
</UploadButton>
</>
);
};

View file

@ -0,0 +1,30 @@
import { MouseEventHandler } from "react";
export const IconButton: React.FC<{
onClick: () => void;
disabled?: boolean;
className?: string;
color?: string;
children: any;
}> = (props) => {
const { onClick, disabled, color, children, className, ...otherProps } =
props;
const onClickStopPropagation: MouseEventHandler<HTMLAnchorElement> = (e) => {
e.stopPropagation();
if (disabled) {
return;
}
onClick();
};
const colorClassName = color ? `text-${color}` : "";
return (
<a
className={`block p-1 ${colorClassName} ${className}`}
onClick={onClickStopPropagation}
href="#"
{...otherProps}
>
{children}
</a>
);
};

View file

@ -0,0 +1,65 @@
import { useState } from "react";
import { CgLink } from "react-icons/cg";
import { UploadButton } from "./UploadButton";
import { Modal } from "../modal/Modal";
import { Button } from "./Button";
import { ModalBody } from "../modal/ModalBody";
import { ModalFooter } from "../modal/ModalFooter";
import { FormInput } from "../forms/FormInput";
export const MagnetInput = ({ className }: { className?: string }) => {
const [magnet, setMagnet] = useState<string | null>(null);
const [inputValue, setInputValue] = useState("");
const [modalIsOpen, setModalIsOpen] = useState(false);
const clear = () => {
setModalIsOpen(false);
setMagnet(null);
};
return (
<>
<UploadButton
onClick={() => {
setModalIsOpen(true);
}}
data={magnet}
className={className}
resetData={() => setMagnet(null)}
>
<CgLink color="blue" />
<div>Add Torrent from Magnet / URL</div>
</UploadButton>
<Modal isOpen={modalIsOpen} onClose={clear} title="Add torrent">
<ModalBody>
<FormInput
autoFocus
value={inputValue}
name="magnet"
onChange={(e) => setInputValue(e.target.value)}
placeholder="magnet:?xt=urn:btih:..."
help="Enter magnet or HTTP(S) URL to the .torrent"
/>
</ModalBody>
<ModalFooter>
<Button variant="cancel" onClick={clear}>
Cancel
</Button>
<Button
disabled={!inputValue}
variant="primary"
onClick={() => {
setMagnet(inputValue);
setInputValue("");
setModalIsOpen(false);
}}
>
Add
</Button>
</ModalFooter>
</Modal>
</>
);
};

View file

@ -0,0 +1,91 @@
import { useContext, useState } from "react";
import { TorrentStats } from "../../api-types";
import {
AppContext,
APIContext,
RefreshTorrentStatsContext,
} from "../../context";
import { IconButton } from "./IconButton";
import { DeleteTorrentModal } from "../modal/DeleteTorrentModal";
import { FaPause, FaPlay, FaTrash } from "react-icons/fa";
export const TorrentActions: React.FC<{
id: number;
statsResponse: TorrentStats;
}> = ({ id, statsResponse }) => {
let state = statsResponse.state;
let [disabled, setDisabled] = useState<boolean>(false);
let [deleting, setDeleting] = useState<boolean>(false);
let refreshCtx = useContext(RefreshTorrentStatsContext);
const canPause = state == "live";
const canUnpause = state == "paused" || state == "error";
const ctx = useContext(AppContext);
const API = useContext(APIContext);
const unpause = () => {
setDisabled(true);
API.start(id)
.then(
() => {
refreshCtx.refresh();
},
(e) => {
ctx.setCloseableError({
text: `Error starting torrent id=${id}`,
details: e,
});
}
)
.finally(() => setDisabled(false));
};
const pause = () => {
setDisabled(true);
API.pause(id)
.then(
() => {
refreshCtx.refresh();
},
(e) => {
ctx.setCloseableError({
text: `Error pausing torrent id=${id}`,
details: e,
});
}
)
.finally(() => setDisabled(false));
};
const startDeleting = () => {
setDisabled(true);
setDeleting(true);
};
const cancelDeleting = () => {
setDisabled(false);
setDeleting(false);
};
return (
<div className="flex w-full justify-center gap-2">
{canUnpause && (
<IconButton onClick={unpause} disabled={disabled}>
<FaPlay className="hover:text-green-500 transition-colors duration-300" />
</IconButton>
)}
{canPause && (
<IconButton onClick={pause} disabled={disabled}>
<FaPause className="hover:text-yellow-500 transition-colors duration-300" />
</IconButton>
)}
<IconButton onClick={startDeleting} disabled={disabled}>
<FaTrash className="hover:text-red-500 transition-colors duration-500" />
</IconButton>
<DeleteTorrentModal id={id} show={deleting} onHide={cancelDeleting} />
</div>
);
};

View file

@ -0,0 +1,72 @@
import { ReactNode, useContext, useEffect, useState } from "react";
import {
AddTorrentResponse,
ErrorDetails as ApiErrorDetails,
} from "../../api-types";
import { APIContext } from "../../context";
import { ErrorWithLabel } from "../../rqbit-web";
import { FileSelectionModal } from "../modal/FileSelectionModal";
import { Button } from "./Button";
export const UploadButton: React.FC<{
onClick: () => void;
data: string | File | null;
resetData: () => void;
children: ReactNode;
className?: string;
}> = ({ onClick, data, resetData, children, className }) => {
const [loading, setLoading] = useState(false);
const [listTorrentResponse, setListTorrentResponse] =
useState<AddTorrentResponse | null>(null);
const [listTorrentError, setListTorrentError] =
useState<ErrorWithLabel | null>(null);
const API = useContext(APIContext);
// Get the torrent file list if there's data.
useEffect(() => {
if (data === null) {
return;
}
let t = setTimeout(async () => {
setLoading(true);
try {
const response = await API.uploadTorrent(data, { list_only: true });
setListTorrentResponse(response);
} catch (e) {
setListTorrentError({
text: "Error listing torrent files",
details: e as ApiErrorDetails,
});
} finally {
setLoading(false);
}
}, 0);
return () => clearTimeout(t);
}, [data]);
const clear = () => {
resetData();
setListTorrentError(null);
setListTorrentResponse(null);
setLoading(false);
};
return (
<>
<Button onClick={onClick} className={className}>
{children}
</Button>
{data && (
<FileSelectionModal
onHide={clear}
listTorrentError={listTorrentError}
listTorrentResponse={listTorrentResponse}
data={data}
listTorrentLoading={loading}
/>
)}
</>
);
};