diff --git a/.env b/.env
new file mode 100644
index 0000000..47b147f
--- /dev/null
+++ b/.env
@@ -0,0 +1,4 @@
+VITE_DEFAULT_CLIENT_URL=
+VITE_DEFAULT_CLIENT_USER=
+VITE_DEFAULT_CLIENT_PASS=
+VITE_DEFAULT_TORRENT_ADD=
diff --git a/package.json b/package.json
index f9899e5..2ae33d9 100644
--- a/package.json
+++ b/package.json
@@ -10,18 +10,20 @@
"preview": "vite preview"
},
"dependencies": {
+ "@radix-ui/react-label": "^2.0.2",
+ "@radix-ui/react-progress": "^1.0.3",
+ "@radix-ui/react-scroll-area": "^1.0.4",
"@radix-ui/react-slot": "^1.0.2",
"@tanstack/react-query": "^4.29.7",
"@tanstack/react-table": "^8.9.1",
"class-variance-authority": "^0.6.0",
"clsx": "^1.2.1",
- "immer": "^10.0.2",
"lucide-react": "^0.221.0",
+ "pretty-bytes": "^6.1.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"tailwind-merge": "^1.12.0",
- "tailwindcss-animate": "^1.0.5",
- "zustand": "^4.3.8"
+ "tailwindcss-animate": "^1.0.5"
},
"devDependencies": {
"@types/node": "^20.2.4",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 0da31f5..d0a7f9a 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -1,6 +1,9 @@
lockfileVersion: 5.4
specifiers:
+ '@radix-ui/react-label': ^2.0.2
+ '@radix-ui/react-progress': ^1.0.3
+ '@radix-ui/react-scroll-area': ^1.0.4
'@radix-ui/react-slot': ^1.0.2
'@tanstack/react-query': ^4.29.7
'@tanstack/react-table': ^8.9.1
@@ -16,9 +19,9 @@ specifiers:
eslint: ^8.38.0
eslint-plugin-react-hooks: ^4.6.0
eslint-plugin-react-refresh: ^0.3.4
- immer: ^10.0.2
lucide-react: ^0.221.0
postcss: ^8.4.23
+ pretty-bytes: ^6.1.0
react: ^18.2.0
react-dom: ^18.2.0
tailwind-merge: ^1.12.0
@@ -26,21 +29,22 @@ specifiers:
tailwindcss-animate: ^1.0.5
typescript: ^5.0.2
vite: ^4.3.2
- zustand: ^4.3.8
dependencies:
+ '@radix-ui/react-label': 2.0.2_h73yytdk5ntplvft5jzh7k37ce
+ '@radix-ui/react-progress': 1.0.3_h73yytdk5ntplvft5jzh7k37ce
+ '@radix-ui/react-scroll-area': 1.0.4_h73yytdk5ntplvft5jzh7k37ce
'@radix-ui/react-slot': 1.0.2_bfoz4c5kom3f237nig75ykjhhy
'@tanstack/react-query': 4.29.7_biqbaboplfbrettd7655fr4n2y
'@tanstack/react-table': 8.9.1_biqbaboplfbrettd7655fr4n2y
class-variance-authority: 0.6.0_typescript@5.0.4
clsx: 1.2.1
- immer: 10.0.2
lucide-react: 0.221.0_react@18.2.0
+ pretty-bytes: 6.1.0
react: 18.2.0
react-dom: 18.2.0_react@18.2.0
tailwind-merge: 1.12.0
tailwindcss-animate: 1.0.5_tailwindcss@3.3.2
- zustand: 4.3.8_immer@10.0.2+react@18.2.0
devDependencies:
'@types/node': 20.2.4
@@ -600,6 +604,18 @@ packages:
'@nodelib/fs.scandir': 2.1.5
fastq: 1.15.0
+ /@radix-ui/number/1.0.1:
+ resolution: {integrity: sha512-T5gIdVO2mmPW3NNhjNgEP3cqMXjXL9UbO0BzWcXfvdBs+BohbQxvd/K5hSVKmn9/lbTdsQVKbUcP5WLCwvUbBg==}
+ dependencies:
+ '@babel/runtime': 7.22.0
+ dev: false
+
+ /@radix-ui/primitive/1.0.1:
+ resolution: {integrity: sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==}
+ dependencies:
+ '@babel/runtime': 7.22.0
+ dev: false
+
/@radix-ui/react-compose-refs/1.0.1_bfoz4c5kom3f237nig75ykjhhy:
resolution: {integrity: sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==}
peerDependencies:
@@ -614,6 +630,149 @@ packages:
react: 18.2.0
dev: false
+ /@radix-ui/react-context/1.0.1_bfoz4c5kom3f237nig75ykjhhy:
+ resolution: {integrity: sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ dependencies:
+ '@babel/runtime': 7.22.0
+ '@types/react': 18.2.7
+ react: 18.2.0
+ dev: false
+
+ /@radix-ui/react-direction/1.0.1_bfoz4c5kom3f237nig75ykjhhy:
+ resolution: {integrity: sha512-RXcvnXgyvYvBEOhCBuddKecVkoMiI10Jcm5cTI7abJRAHYfFxeu+FBQs/DvdxSYucxR5mna0dNsL6QFlds5TMA==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ dependencies:
+ '@babel/runtime': 7.22.0
+ '@types/react': 18.2.7
+ react: 18.2.0
+ dev: false
+
+ /@radix-ui/react-label/2.0.2_h73yytdk5ntplvft5jzh7k37ce:
+ resolution: {integrity: sha512-N5ehvlM7qoTLx7nWPodsPYPgMzA5WM8zZChQg8nyFJKnDO5WHdba1vv5/H6IO5LtJMfD2Q3wh1qHFGNtK0w3bQ==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0
+ react-dom: ^16.8 || ^17.0 || ^18.0
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+ dependencies:
+ '@babel/runtime': 7.22.0
+ '@radix-ui/react-primitive': 1.0.3_h73yytdk5ntplvft5jzh7k37ce
+ '@types/react': 18.2.7
+ '@types/react-dom': 18.2.4
+ react: 18.2.0
+ react-dom: 18.2.0_react@18.2.0
+ dev: false
+
+ /@radix-ui/react-presence/1.0.1_h73yytdk5ntplvft5jzh7k37ce:
+ resolution: {integrity: sha512-UXLW4UAbIY5ZjcvzjfRFo5gxva8QirC9hF7wRE4U5gz+TP0DbRk+//qyuAQ1McDxBt1xNMBTaciFGvEmJvAZCg==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0
+ react-dom: ^16.8 || ^17.0 || ^18.0
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+ dependencies:
+ '@babel/runtime': 7.22.0
+ '@radix-ui/react-compose-refs': 1.0.1_bfoz4c5kom3f237nig75ykjhhy
+ '@radix-ui/react-use-layout-effect': 1.0.1_bfoz4c5kom3f237nig75ykjhhy
+ '@types/react': 18.2.7
+ '@types/react-dom': 18.2.4
+ react: 18.2.0
+ react-dom: 18.2.0_react@18.2.0
+ dev: false
+
+ /@radix-ui/react-primitive/1.0.3_h73yytdk5ntplvft5jzh7k37ce:
+ resolution: {integrity: sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0
+ react-dom: ^16.8 || ^17.0 || ^18.0
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+ dependencies:
+ '@babel/runtime': 7.22.0
+ '@radix-ui/react-slot': 1.0.2_bfoz4c5kom3f237nig75ykjhhy
+ '@types/react': 18.2.7
+ '@types/react-dom': 18.2.4
+ react: 18.2.0
+ react-dom: 18.2.0_react@18.2.0
+ dev: false
+
+ /@radix-ui/react-progress/1.0.3_h73yytdk5ntplvft5jzh7k37ce:
+ resolution: {integrity: sha512-5G6Om/tYSxjSeEdrb1VfKkfZfn/1IlPWd731h2RfPuSbIfNUgfqAwbKfJCg/PP6nuUCTrYzalwHSpSinoWoCag==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0
+ react-dom: ^16.8 || ^17.0 || ^18.0
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+ dependencies:
+ '@babel/runtime': 7.22.0
+ '@radix-ui/react-context': 1.0.1_bfoz4c5kom3f237nig75ykjhhy
+ '@radix-ui/react-primitive': 1.0.3_h73yytdk5ntplvft5jzh7k37ce
+ '@types/react': 18.2.7
+ '@types/react-dom': 18.2.4
+ react: 18.2.0
+ react-dom: 18.2.0_react@18.2.0
+ dev: false
+
+ /@radix-ui/react-scroll-area/1.0.4_h73yytdk5ntplvft5jzh7k37ce:
+ resolution: {integrity: sha512-OIClwBkwPG+FKvC4OMTRaa/3cfD069nkKFFL/TQzRzaO42Ce5ivKU9VMKgT7UU6UIkjcQqKBrDOIzWtPGw6e6w==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0
+ react-dom: ^16.8 || ^17.0 || ^18.0
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+ dependencies:
+ '@babel/runtime': 7.22.0
+ '@radix-ui/number': 1.0.1
+ '@radix-ui/primitive': 1.0.1
+ '@radix-ui/react-compose-refs': 1.0.1_bfoz4c5kom3f237nig75ykjhhy
+ '@radix-ui/react-context': 1.0.1_bfoz4c5kom3f237nig75ykjhhy
+ '@radix-ui/react-direction': 1.0.1_bfoz4c5kom3f237nig75ykjhhy
+ '@radix-ui/react-presence': 1.0.1_h73yytdk5ntplvft5jzh7k37ce
+ '@radix-ui/react-primitive': 1.0.3_h73yytdk5ntplvft5jzh7k37ce
+ '@radix-ui/react-use-callback-ref': 1.0.1_bfoz4c5kom3f237nig75ykjhhy
+ '@radix-ui/react-use-layout-effect': 1.0.1_bfoz4c5kom3f237nig75ykjhhy
+ '@types/react': 18.2.7
+ '@types/react-dom': 18.2.4
+ react: 18.2.0
+ react-dom: 18.2.0_react@18.2.0
+ dev: false
+
/@radix-ui/react-slot/1.0.2_bfoz4c5kom3f237nig75ykjhhy:
resolution: {integrity: sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==}
peerDependencies:
@@ -629,6 +788,34 @@ packages:
react: 18.2.0
dev: false
+ /@radix-ui/react-use-callback-ref/1.0.1_bfoz4c5kom3f237nig75ykjhhy:
+ resolution: {integrity: sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ dependencies:
+ '@babel/runtime': 7.22.0
+ '@types/react': 18.2.7
+ react: 18.2.0
+ dev: false
+
+ /@radix-ui/react-use-layout-effect/1.0.1_bfoz4c5kom3f237nig75ykjhhy:
+ resolution: {integrity: sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ dependencies:
+ '@babel/runtime': 7.22.0
+ '@types/react': 18.2.7
+ react: 18.2.0
+ dev: false
+
/@tanstack/query-core/4.29.7:
resolution: {integrity: sha512-GXG4b5hV2Loir+h2G+RXhJdoZhJLnrBWsuLB2r0qBRyhWuXq9w/dWxzvpP89H0UARlH6Mr9DiVj4SMtpkF/aUA==}
dev: false
@@ -683,7 +870,6 @@ packages:
resolution: {integrity: sha512-G2mHoTMTL4yoydITgOGwWdWMVd8sNgyEP85xVmMKAPUBwQWm9wBPQUmvbeF4V3WBY1P7mmL4BkjQ0SqUpf1snw==}
dependencies:
'@types/react': 18.2.7
- dev: true
/@types/react/18.2.7:
resolution: {integrity: sha512-ojrXpSH2XFCmHm7Jy3q44nXDyN54+EYKP2lBhJ2bqfyPj6cIUW/FZW/Csdia34NQgq7KYcAlHi5184m4X88+yw==}
@@ -1441,10 +1627,6 @@ packages:
engines: {node: '>= 4'}
dev: true
- /immer/10.0.2:
- resolution: {integrity: sha512-Rx3CqeqQ19sxUtYV9CU911Vhy8/721wRFnJv3REVGWUmoAcIwzifTsdmJte/MV+0/XpM35LZdQMBGkRIoLPwQA==}
- dev: false
-
/import-fresh/3.3.0:
resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==}
engines: {node: '>=6'}
@@ -1793,6 +1975,11 @@ packages:
engines: {node: '>= 0.8.0'}
dev: true
+ /pretty-bytes/6.1.0:
+ resolution: {integrity: sha512-Rk753HI8f4uivXi4ZCIYdhmG1V+WKzvRMg/X+M42a6t7D07RcmopXJMDNk6N++7Bl75URRGsb40ruvg7Hcp2wQ==}
+ engines: {node: ^14.13.1 || >=16.0.0}
+ dev: false
+
/punycode/2.3.0:
resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==}
engines: {node: '>=6'}
@@ -2154,20 +2341,3 @@ packages:
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
engines: {node: '>=10'}
dev: true
-
- /zustand/4.3.8_immer@10.0.2+react@18.2.0:
- resolution: {integrity: sha512-4h28KCkHg5ii/wcFFJ5Fp+k1J3gJoasaIbppdgZFO4BPJnsNxL0mQXBSFgOgAdCdBj35aDTPvdAJReTMntFPGg==}
- engines: {node: '>=12.7.0'}
- peerDependencies:
- immer: '>=9.0'
- react: '>=16.8'
- peerDependenciesMeta:
- immer:
- optional: true
- react:
- optional: true
- dependencies:
- immer: 10.0.2
- react: 18.2.0
- use-sync-external-store: 1.2.0_react@18.2.0
- dev: false
diff --git a/src/App.tsx b/src/App.tsx
index 329e52f..b8b4c87 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,6 +1,15 @@
-import Home from "./pages/home";
+import { useTransmission } from "@/hooks/use-transmission";
+import Home from "@/pages/home";
+import Login from "@/pages/login";
function App() {
+ const { isLoggedIn } = useTransmission();
+ console.log(import.meta.env.HELLO);
+
+ if (isLoggedIn === false) {
+ return ;
+ }
+
return ;
}
diff --git a/src/assets/login-bg.jpg b/src/assets/login-bg.jpg
new file mode 100644
index 0000000..10ca1c7
Binary files /dev/null and b/src/assets/login-bg.jpg differ
diff --git a/src/components/login/index.tsx b/src/components/login/index.tsx
new file mode 100644
index 0000000..051751c
--- /dev/null
+++ b/src/components/login/index.tsx
@@ -0,0 +1,73 @@
+"use client";
+
+import { Button } from "@/components/ui/button";
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardFooter,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import { useTransmission } from "@/hooks/use-transmission";
+import { env } from "@/lib/env";
+import { useState } from "react";
+
+const defaultUrl = new URL("/transmission/rpc", window.location.origin);
+
+export function LoginCard() {
+ const client = useTransmission();
+
+ const [url, setUrl] = useState(env.VITE_DEFAULT_CLIENT_URL ?? defaultUrl);
+ const [user, setUser] = useState(env.VITE_DEFAULT_CLIENT_USER ?? "");
+ const [password, setPassword] = useState(env.VITE_DEFAULT_CLIENT_PASS ?? "");
+
+ const submitLogin = async () => await client.login({ url, user, password });
+
+ return (
+
+
+ Login into Transmission
+
+ Enter your local or remote credentials.
+
+
+
+
+
+ setUrl(e.target.value)}
+ />
+
+
+
+ setUser(e.target.value)}
+ />
+
+
+
+ setPassword(e.target.value)}
+ />
+
+
+
+
+
+
+ );
+}
diff --git a/src/components/sidebar/index.tsx b/src/components/sidebar/index.tsx
new file mode 100644
index 0000000..5c9fdc2
--- /dev/null
+++ b/src/components/sidebar/index.tsx
@@ -0,0 +1,67 @@
+import {
+ AlignJustify,
+ CheckCircle,
+ FileDown,
+ PauseCircle,
+ PlayCircle,
+} from "lucide-react";
+
+import { cn } from "@/lib/utils";
+import { Button } from "@/components/ui/button";
+import { Card, CardHeader, CardDescription } from "../ui/card";
+import { useTransmission } from "@/hooks/use-transmission";
+
+interface SidebarProps {
+ className: string;
+}
+
+export function Sidebar({ className }: SidebarProps) {
+ const { session } = useTransmission();
+ return (
+
+
+
+
+ Status
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Version: {session.version}
+
+
+
+
+
+ );
+}
diff --git a/src/components/torrents/cells.tsx b/src/components/torrents/cells.tsx
new file mode 100644
index 0000000..5ca0493
--- /dev/null
+++ b/src/components/torrents/cells.tsx
@@ -0,0 +1,23 @@
+import prettyBytes from "pretty-bytes";
+
+const messages = new Map([
+ [0, "Stopped"],
+ [1, "Verifying"],
+ [2, "Verifying"],
+ [3, "Downloading"],
+ [4, "Downloading"],
+ [5, "Seeding"],
+ [6, "Seeding"],
+]);
+
+export function TorrentStatus({ state }: { state: number }) {
+ return {messages.get(state) ?? "Unknown state"};
+}
+
+export function TorrentSize({ sizeInBytes }: { sizeInBytes: number }) {
+ return {prettyBytes(sizeInBytes)};
+}
+
+export function TorrentSpeed({ speedInBytes }: { speedInBytes: number }) {
+ return {prettyBytes(speedInBytes ?? 0)}/s;
+}
diff --git a/src/components/torrents/columns.tsx b/src/components/torrents/columns.tsx
index b2b2f3a..4476799 100644
--- a/src/components/torrents/columns.tsx
+++ b/src/components/torrents/columns.tsx
@@ -1,4 +1,25 @@
import { ColumnDef } from "@tanstack/react-table";
+import {
+ TorrentSize,
+ TorrentSpeed,
+ TorrentStatus,
+} from "@/components/torrents/cells";
+import { Progress } from "@/components/ui/progress";
+
+export const fields = [
+ "id",
+ "name",
+ "sizeWhenDone",
+ "status",
+ "rateDownload (B/s)",
+ "rateUpload (B/s)",
+ "eta",
+ "uploadRatio",
+ "percentDone",
+ "magnetLink",
+ "group",
+ "labels",
+];
export interface Torrent {
id: number;
@@ -9,6 +30,10 @@ export interface Torrent {
"rateUpload (B/s)": number;
eta: number;
uploadRatio: number;
+ percentDone: number;
+ magnetLink: string;
+ group: string;
+ labels: string[];
}
export const columns: ColumnDef[] = [
@@ -19,18 +44,29 @@ export const columns: ColumnDef[] = [
{
accessorKey: "sizeWhenDone",
header: "Size",
+ cell: (props) => ()} />,
+ },
+ {
+ accessorKey: "percentDone",
+ header: "Progress",
+ cell: (props) =>