mirror of
https://git.v0l.io/Kieran/dtan.git
synced 2024-12-12 23:16:21 +00:00
Merge pull request 'Design update and some fixes' (#1) from florian/dtan:feat/ui-design-update into main
Reviewed-on: https://git.v0l.io/Kieran/dtan/pulls/1 Reviewed-by: Kieran <kieran@noreply.localhost>
This commit is contained in:
commit
01610ab846
@ -11,6 +11,7 @@
|
|||||||
<meta name="description" content="Torrents on Nostr" />
|
<meta name="description" content="Torrents on Nostr" />
|
||||||
<link rel="icon" href="/logo_32.png" />
|
<link rel="icon" href="/logo_32.png" />
|
||||||
<title>DTAN.XYZ</title>
|
<title>DTAN.XYZ</title>
|
||||||
|
<link href="/fonts/outfit/outfit.css" rel="stylesheet" />
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
72
public/fonts/outfit/outfit.css
Normal file
72
public/fonts/outfit/outfit.css
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
/* latin-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Outfit';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(outfit_400_latin-ext.woff2) format('woff2');
|
||||||
|
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||||
|
}
|
||||||
|
/* latin */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Outfit';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(outfit_400_latin.woff2) format('woff2');
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
||||||
|
/* latin-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Outfit';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 500;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(outfit_500_latin-ext.woff2) format('woff2');
|
||||||
|
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||||
|
}
|
||||||
|
/* latin */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Outfit';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 500;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(outfit_500_latin.woff2) format('woff2');
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
||||||
|
/* latin-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Outfit';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 600;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(outfit_600_latin-ext.woff2) format('woff2');
|
||||||
|
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||||
|
}
|
||||||
|
/* latin */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Outfit';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 600;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(outfit_600_latin.woff2) format('woff2');
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
||||||
|
/* latin-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Outfit';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(outfit_700_latin-ext.woff2) format('woff2');
|
||||||
|
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||||
|
}
|
||||||
|
/* latin */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Outfit';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(outfit_700_latin.woff2) format('woff2');
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
BIN
public/fonts/outfit/outfit_400_latin-ext.woff2
Normal file
BIN
public/fonts/outfit/outfit_400_latin-ext.woff2
Normal file
Binary file not shown.
BIN
public/fonts/outfit/outfit_400_latin.woff2
Normal file
BIN
public/fonts/outfit/outfit_400_latin.woff2
Normal file
Binary file not shown.
BIN
public/fonts/outfit/outfit_500_latin-ext.woff2
Normal file
BIN
public/fonts/outfit/outfit_500_latin-ext.woff2
Normal file
Binary file not shown.
BIN
public/fonts/outfit/outfit_500_latin.woff2
Normal file
BIN
public/fonts/outfit/outfit_500_latin.woff2
Normal file
Binary file not shown.
BIN
public/fonts/outfit/outfit_600_latin-ext.woff2
Normal file
BIN
public/fonts/outfit/outfit_600_latin-ext.woff2
Normal file
Binary file not shown.
BIN
public/fonts/outfit/outfit_600_latin.woff2
Normal file
BIN
public/fonts/outfit/outfit_600_latin.woff2
Normal file
Binary file not shown.
BIN
public/fonts/outfit/outfit_700_latin-ext.woff2
Normal file
BIN
public/fonts/outfit/outfit_700_latin-ext.woff2
Normal file
Binary file not shown.
BIN
public/fonts/outfit/outfit_700_latin.woff2
Normal file
BIN
public/fonts/outfit/outfit_700_latin.woff2
Normal file
Binary file not shown.
@ -3,6 +3,8 @@ import { HTMLProps, forwardRef, useState } from "react";
|
|||||||
|
|
||||||
type ButtonProps = Omit<HTMLProps<HTMLButtonElement>, "onClick"> & {
|
type ButtonProps = Omit<HTMLProps<HTMLButtonElement>, "onClick"> & {
|
||||||
onClick?: (e: React.MouseEvent<HTMLButtonElement>) => Promise<void> | void;
|
onClick?: (e: React.MouseEvent<HTMLButtonElement>) => Promise<void> | void;
|
||||||
|
type: "primary" | "secondary" | "danger";
|
||||||
|
small?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Button = forwardRef<HTMLButtonElement, ButtonProps>((props, ref) => {
|
export const Button = forwardRef<HTMLButtonElement, ButtonProps>((props, ref) => {
|
||||||
@ -19,18 +21,28 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>((props, ref) =>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const colorScheme =
|
||||||
|
props.disabled ? "bg-neutral-900 text-neutral-600 border border-solid border-neutral-700" :
|
||||||
|
props.type == "danger"
|
||||||
|
? "bg-red-900 hover:bg-red-600"
|
||||||
|
: props.type == "primary"
|
||||||
|
? "bg-indigo-800 hover:bg-indigo-700"
|
||||||
|
: "bg-neutral-800 hover:bg-neutral-700";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
{...props}
|
{...props}
|
||||||
type="button"
|
type="button"
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"p-2 rounded flex gap-1 items-center justify-center bg-slate-800 hover:bg-slate-600",
|
props.small ? "px-3 py-1 rounded-2xl" : "px-4 py-3 rounded-full ",
|
||||||
|
"flex gap-1 items-center justify-center whitespace-nowrap",
|
||||||
|
colorScheme,
|
||||||
props.className,
|
props.className,
|
||||||
)}
|
)}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
onClick={clicking}
|
onClick={clicking}
|
||||||
>
|
>
|
||||||
{spinning ? "Loading.." : props.children}
|
{spinning ? "Loading..." : props.children}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -19,10 +19,10 @@ export function Comments({ link }: { link: NostrLink }) {
|
|||||||
<WriteComment link={link} />
|
<WriteComment link={link} />
|
||||||
{comments.data
|
{comments.data
|
||||||
?.sort((a, b) => (a.created_at > b.created_at ? -1 : 1))
|
?.sort((a, b) => (a.created_at > b.created_at ? -1 : 1))
|
||||||
.map((a) => (
|
.map((a, i) => (
|
||||||
<div className="flex flex-col gap-2 rounded p-2 bg-slate-900">
|
<div key={i} className="flex flex-col gap-2 rounded-lg p-4 bg-neutral-900">
|
||||||
<ProfileImage pubkey={a.pubkey} withName={true}>
|
<ProfileImage pubkey={a.pubkey} withName={true}>
|
||||||
<span className="text-slate-400 text-sm">{new Date(a.created_at * 1000).toLocaleString()}</span>
|
<span className="text-neutral-400 text-sm">{new Date(a.created_at * 1000).toLocaleString()}</span>
|
||||||
</ProfileImage>
|
</ProfileImage>
|
||||||
<Text content={a.content} tags={a.tags} />
|
<Text content={a.content} tags={a.tags} />
|
||||||
</div>
|
</div>
|
||||||
@ -50,10 +50,21 @@ function WriteComment({ link }: { link: NostrLink }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="rounded p-2 bg-slate-900">
|
<div className="rounded-lg p-4 bg-neutral-900 flex flex-row gap-4">
|
||||||
<h3>Write a Comment</h3>
|
<div className="flex-shrink">
|
||||||
<textarea className="w-full" value={msg} onChange={(e) => setMsg(e.target.value)}></textarea>
|
<ProfileImage pubkey={login.publicKey} />
|
||||||
<Button onClick={sendComment}>Send</Button>
|
</div>
|
||||||
|
<div className="flex-grow">
|
||||||
|
<textarea
|
||||||
|
className="px-4 py-2 rounded-xl bg-neutral-800 focus-visible:outline-none w-full"
|
||||||
|
placeholder="Write a comment..."
|
||||||
|
value={msg}
|
||||||
|
onChange={(e) => setMsg(e.target.value)}
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Button type="primary" onClick={sendComment}>Send</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -26,7 +26,7 @@ export function ProfileImage({ pubkey, size, withName, children, ...props }: Pro
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
{...props}
|
{...props}
|
||||||
className="rounded-full aspect-square w-12 bg-slate-800 border border-slate-200 bg-cover bg-center"
|
className="rounded-full aspect-square w-12 bg-neutral-800 border border-neutral-500 bg-cover bg-center"
|
||||||
style={v}
|
style={v}
|
||||||
></div>
|
></div>
|
||||||
{withName === true && <>{profile?.name}</>}
|
{withName === true && <>{profile?.name}</>}
|
||||||
|
@ -12,11 +12,10 @@ export function Search(params: { term?: string; tags?: Array<string> }) {
|
|||||||
}, [params]);
|
}, [params]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Search.."
|
placeholder="Search..."
|
||||||
className="p-3 rounded w-full"
|
className="px-4 py-3 bg-neutral-800 rounded-full w-full focus-visible:outline-none"
|
||||||
value={term}
|
value={term}
|
||||||
onChange={(e) => setTerm(e.target.value)}
|
onChange={(e) => setTerm(e.target.value)}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
@ -25,6 +24,5 @@ export function Search(params: { term?: string; tags?: Array<string> }) {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -3,29 +3,34 @@ import { useMemo } from "react";
|
|||||||
import { Mention } from "./mention";
|
import { Mention } from "./mention";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
export function Text({ content, tags }: { content: string; tags: Array<Array<string>> }) {
|
export function Text({ content, tags, wrap = true }: { content: string; tags: Array<Array<string>>; wrap?: boolean }) {
|
||||||
const frags = useMemo(() => transformText(content, tags), [content, tags]);
|
const frags = useMemo(() => transformText(content, tags), [content, tags]);
|
||||||
|
|
||||||
function renderFrag(f: ParsedFragment) {
|
function renderFrag(f: ParsedFragment, index: number) {
|
||||||
switch (f.type) {
|
switch (f.type) {
|
||||||
|
case "media":
|
||||||
|
return <img key={index} src={f.content} style={{ maxHeight: "50vh" }} />;
|
||||||
case "mention":
|
case "mention":
|
||||||
case "link": {
|
case "link": {
|
||||||
const link = tryParseNostrLink(f.content);
|
const nostrLink = tryParseNostrLink(f.content);
|
||||||
if (link) {
|
if (nostrLink) {
|
||||||
return <Mention link={link} />;
|
return <Mention key={index} link={nostrLink} />;
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<Link to={f.content} target="_blank">
|
<Link key={index} to={f.content} target="_blank" className="text-indigo-300" rel="noopener noreferrer">
|
||||||
{f.content}
|
{f.content}
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
return <span>{f.content}</span>;
|
return <span key={index}>{f.content}</span>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (wrap) {
|
||||||
return <div className="text">{frags.map(renderFrag)}</div>;
|
return <div className="text">{frags.map(renderFrag)}</div>;
|
||||||
}
|
}
|
||||||
|
return frags.map(renderFrag);
|
||||||
|
}
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
.torrent-list {
|
.torrent-list {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 400;
|
||||||
}
|
}
|
||||||
|
|
||||||
.torrent-list td,
|
.torrent-list td,
|
||||||
.torrent-list th {
|
.torrent-list th {
|
||||||
border: 1px solid #333;
|
border-bottom: 1px solid #222;
|
||||||
padding: 0px 5px;
|
padding: 0px 6px;
|
||||||
font-size: 14px;
|
|
||||||
}
|
}
|
||||||
|
@ -4,18 +4,19 @@ import { FormatBytes } from "../const";
|
|||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { MagnetLink } from "./magnet";
|
import { MagnetLink } from "./magnet";
|
||||||
import { Mention } from "./mention";
|
import { Mention } from "./mention";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
|
||||||
export function TorrentList({ items }: { items: Array<TaggedNostrEvent> }) {
|
export function TorrentList({ items }: { items: Array<TaggedNostrEvent> }) {
|
||||||
return (
|
return (
|
||||||
<table className="torrent-list">
|
<table className="torrent-list mb-8">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="bg-slate-600">
|
<tr className="h-8">
|
||||||
<th>Category</th>
|
<th className="rounded-tl-lg">Category</th>
|
||||||
<th>Name</th>
|
<th>Name</th>
|
||||||
<th>Uploaded</th>
|
<th>Uploaded</th>
|
||||||
<th></th>
|
<th></th>
|
||||||
<th>Size</th>
|
<th>Size</th>
|
||||||
<th>From</th>
|
<th className="rounded-tr-lg">From</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@ -27,45 +28,59 @@ export function TorrentList({ items }: { items: Array<TaggedNostrEvent> }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function TagList({ tags }: { tags: string[][] }) {
|
||||||
|
return tags
|
||||||
|
.filter((a) => a[0] === "t")
|
||||||
|
.slice(0, 3)
|
||||||
|
.map((current, index, allTags) => (
|
||||||
|
<TagListEntry key={current[1]} tags={allTags} startIndex={index} tag={current} />
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
function TagListEntry({ tags, startIndex, tag }: { tags: string[][]; startIndex: number; tag: string[] }) {
|
||||||
|
const tagUrl = useMemo(() => {
|
||||||
|
return encodeURIComponent(
|
||||||
|
tags
|
||||||
|
.slice(0, startIndex + 1)
|
||||||
|
.map((b) => b[1])
|
||||||
|
.join(","),
|
||||||
|
);
|
||||||
|
}, [tags, startIndex]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Link to={`/search/?tags=${tagUrl}`}>{tag[1]}</Link>
|
||||||
|
{tags.length !== startIndex + 1 && " > "}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function TorrentTableEntry({ item }: { item: TaggedNostrEvent }) {
|
function TorrentTableEntry({ item }: { item: TaggedNostrEvent }) {
|
||||||
|
const { name, size } = useMemo(() => {
|
||||||
const name = item.tags.find((a) => a[0] === "title")?.at(1);
|
const name = item.tags.find((a) => a[0] === "title")?.at(1);
|
||||||
const size = item.tags
|
const size = item.tags
|
||||||
.filter((a) => a[0] === "file")
|
.filter((a) => a[0] === "file")
|
||||||
.map((a) => Number(a[2]))
|
.map((a) => Number(a[2]))
|
||||||
.reduce((acc, v) => (acc += v), 0);
|
.reduce((acc, v) => (acc += v), 0);
|
||||||
|
return { name, size };
|
||||||
|
}, [item]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr className="hover:bg-slate-800">
|
<tr className="hover:bg-indigo-800">
|
||||||
<td>
|
<td className="text-indigo-300">
|
||||||
{item.tags
|
<TagList tags={item.tags} />
|
||||||
.filter((a) => a[0] === "t")
|
|
||||||
.slice(0, 3)
|
|
||||||
.map((a, i, arr) => (
|
|
||||||
<>
|
|
||||||
<Link
|
|
||||||
to={`/search/?tags=${encodeURIComponent(
|
|
||||||
arr
|
|
||||||
.slice(0, i + 1)
|
|
||||||
.map((b) => b[1])
|
|
||||||
.join(","),
|
|
||||||
)}`}
|
|
||||||
>
|
|
||||||
{a[1]}
|
|
||||||
</Link>
|
|
||||||
{arr.length !== i + 1 && " > "}
|
|
||||||
</>
|
|
||||||
))}
|
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td className="break-words">
|
||||||
<Link to={`/e/${NostrLink.fromEvent(item).encode()}`} state={item}>
|
<Link to={`/e/${NostrLink.fromEvent(item).encode()}`} state={item}>
|
||||||
{name}
|
{name}
|
||||||
</Link>
|
</Link>
|
||||||
</td>
|
</td>
|
||||||
<td>{new Date(item.created_at * 1000).toLocaleDateString()}</td>
|
<td className="text-neutral-300">{new Date(item.created_at * 1000).toLocaleDateString()}</td>
|
||||||
<td>
|
<td>
|
||||||
<MagnetLink item={item} />
|
<MagnetLink item={item} />
|
||||||
</td>
|
</td>
|
||||||
<td>{FormatBytes(size)}</td>
|
<td className="whitespace-nowrap text-right text-neutral-300">{FormatBytes(size)}</td>
|
||||||
<td>
|
<td className="text-indigo-300 whitespace-nowrap break-words text-ellipsis">
|
||||||
<Mention link={new NostrLink(NostrPrefix.PublicKey, item.pubkey)} />
|
<Mention link={new NostrLink(NostrPrefix.PublicKey, item.pubkey)} />
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
@ -15,7 +15,7 @@ export function LatestTorrents({ author }: { author?: string }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h3>Latest Torrents</h3>
|
<h2>Latest Torrents</h2>
|
||||||
<TorrentList items={latest.data ?? []} />
|
<TorrentList items={latest.data ?? []} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -4,10 +4,16 @@
|
|||||||
|
|
||||||
html,
|
html,
|
||||||
body {
|
body {
|
||||||
|
font-family: 'Outfit', Arial, Helvetica, sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
font-size: 16px;
|
||||||
|
color: #adadad;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 24px;
|
||||||
background-color: black;
|
background-color: black;
|
||||||
color: white;
|
color: white;
|
||||||
font-size: 16px;
|
|
||||||
font-family: Arial, Helvetica, sans-serif;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
@ -20,19 +26,15 @@ h3 {
|
|||||||
font-size: 21px;
|
font-size: 21px;
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="text"],
|
|
||||||
input[type="number"],
|
|
||||||
textarea {
|
|
||||||
color: black;
|
|
||||||
padding: 4px;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
a:not([href="/"], :has(button)) {
|
a:not([href="/"], :has(button)) {
|
||||||
text-decoration: dotted;
|
text-decoration-line: none;
|
||||||
text-decoration-line: underline;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.text {
|
.text {
|
||||||
white-space-collapse: preserve-breaks;
|
white-space-collapse: preserve-breaks;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.file-list {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
@ -1,10 +1,8 @@
|
|||||||
import { Search } from "../element/search";
|
|
||||||
import { LatestTorrents } from "../element/trending";
|
import { LatestTorrents } from "../element/trending";
|
||||||
|
|
||||||
export function HomePage() {
|
export function HomePage() {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-4">
|
||||||
<Search />
|
|
||||||
<LatestTorrents />
|
<LatestTorrents />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -2,6 +2,7 @@ import { Link, Outlet } from "react-router-dom";
|
|||||||
import { Button } from "../element/button";
|
import { Button } from "../element/button";
|
||||||
import { LoginSession, LoginState, useLogin } from "../login";
|
import { LoginSession, LoginState, useLogin } from "../login";
|
||||||
import { ProfileImage } from "../element/profile-image";
|
import { ProfileImage } from "../element/profile-image";
|
||||||
|
import { Search } from "../element/search";
|
||||||
|
|
||||||
export function Layout() {
|
export function Layout() {
|
||||||
const login = useLogin();
|
const login = useLogin();
|
||||||
@ -17,14 +18,15 @@ export function Layout() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto">
|
<div className="container mx-auto">
|
||||||
<header className="flex justify-between items-center p-1">
|
<header className="flex justify-between items-center pt-4 pb-6">
|
||||||
<Link to={"/"} className="flex gap-1 items-center">
|
<Link to={"/"} className="flex gap-2 items-center">
|
||||||
<img src="/logo_256.jpg" className="rounded-full" height={40} width={40} />
|
<img src="/logo_256.jpg" className="rounded-full" height={40} width={40} />
|
||||||
<h1 className="font-bold uppercase">dtan.xyz</h1>
|
<h1 className="font-bold uppercase">dtan.xyz</h1>
|
||||||
</Link>
|
</Link>
|
||||||
{login ? <LoggedInHeader login={login} /> : <Button onClick={DoLogin}>Login</Button>}
|
<div className="w-1/2"><Search /></div>
|
||||||
|
{login ? <LoggedInHeader login={login} /> : <Button type="primary" onClick={DoLogin}>Login</Button>}
|
||||||
</header>
|
</header>
|
||||||
<div className="p-1">
|
<div>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -33,10 +35,10 @@ export function Layout() {
|
|||||||
|
|
||||||
function LoggedInHeader({ login }: { login: LoginSession }) {
|
function LoggedInHeader({ login }: { login: LoginSession }) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-2">
|
||||||
<ProfileImage pubkey={login.publicKey} />
|
<ProfileImage pubkey={login.publicKey} />
|
||||||
<Link to="/new">
|
<Link to="/new">
|
||||||
<Button>+ Create</Button>
|
<Button type="primary">+ Create</Button>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
32
src/page/new.css
Normal file
32
src/page/new.css
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
label.category input {
|
||||||
|
border: 0px;
|
||||||
|
clip: rect(0px, 0px, 0px, 0px);
|
||||||
|
height: 1px;
|
||||||
|
width: 1px;
|
||||||
|
margin: -1px;
|
||||||
|
padding: 0px;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
position: absolute;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
label.category div {
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
margin: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
label.category div:hover {
|
||||||
|
border: 1px solid white;
|
||||||
|
outline: none;
|
||||||
|
margin: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
label.category div[data-checked="true"] {
|
||||||
|
background-color: #3730a3;
|
||||||
|
border: 1px solid white;
|
||||||
|
margin: 0px;
|
||||||
|
}
|
@ -1,3 +1,4 @@
|
|||||||
|
import "./new.css";
|
||||||
import { ReactNode, useState } from "react";
|
import { ReactNode, useState } from "react";
|
||||||
import { Categories, Category, TorrentKind } from "../const";
|
import { Categories, Category, TorrentKind } from "../const";
|
||||||
import { Button } from "../element/button";
|
import { Button } from "../element/button";
|
||||||
@ -42,19 +43,37 @@ async function openFile(): Promise<File | undefined> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TorrentEntry = {
|
||||||
|
name: string;
|
||||||
|
desc: string;
|
||||||
|
btih: string;
|
||||||
|
tags: string[];
|
||||||
|
files: Array<{
|
||||||
|
name: string;
|
||||||
|
size: number;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
|
||||||
|
function entryIsValid(entry: TorrentEntry) {
|
||||||
|
return (
|
||||||
|
entry.name &&
|
||||||
|
entry.btih &&
|
||||||
|
entry.files.length > 0 &&
|
||||||
|
entry.tags.length > 0 &&
|
||||||
|
entry.files.every((f) => f.name.length > 0)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function NewPage() {
|
export function NewPage() {
|
||||||
const login = useLogin();
|
const login = useLogin();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const [obj, setObj] = useState({
|
const [obj, setObj] = useState<TorrentEntry>({
|
||||||
name: "",
|
name: "",
|
||||||
desc: "",
|
desc: "",
|
||||||
btih: "",
|
btih: "",
|
||||||
tags: [] as Array<string>,
|
tags: [],
|
||||||
files: [] as Array<{
|
files: [],
|
||||||
name: string;
|
|
||||||
size: number;
|
|
||||||
}>,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
async function loadTorrent() {
|
async function loadTorrent() {
|
||||||
@ -110,7 +129,7 @@ export function NewPage() {
|
|||||||
function renderCategories(a: Category, tags: Array<string>): ReactNode {
|
function renderCategories(a: Category, tags: Array<string>): ReactNode {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex gap-1 bg-slate-500 p-1 rounded">
|
<label className="category">
|
||||||
<input
|
<input
|
||||||
type="radio"
|
type="radio"
|
||||||
value={tags.join(",")}
|
value={tags.join(",")}
|
||||||
@ -123,8 +142,9 @@ export function NewPage() {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<label>{a?.name}</label>
|
<div data-checked={obj.tags.join(",") === tags.join(",")}>{a?.name}</div>
|
||||||
</div>
|
</label>
|
||||||
|
|
||||||
{a.sub_category?.map((b) => renderCategories(b, [...tags, b.tag]))}
|
{a.sub_category?.map((b) => renderCategories(b, [...tags, b.tag]))}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@ -132,57 +152,69 @@ export function NewPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h1>New</h1>
|
<h2>New Torrent</h2>
|
||||||
<div className="flex gap-1">
|
<div className="flex gap-4 my-4">
|
||||||
<Button onClick={loadTorrent}>Import from Torrent</Button>
|
<Button onClick={loadTorrent} type="primary">
|
||||||
<Button>Import from Magnet</Button>
|
Import from Torrent
|
||||||
|
</Button>
|
||||||
|
{/*<Button>Import from Magnet</Button>*/}
|
||||||
</div>
|
</div>
|
||||||
<h2>Torrent Info</h2>
|
<form className="flex flex-col gap-2 bg-neutral-900 rounded-2xl p-6 mb-8">
|
||||||
<form className="flex flex-col gap-2">
|
<div className="flex gap-4">
|
||||||
<div className="flex gap-2">
|
<div className="flex-1 flex flex-col gap-2">
|
||||||
<div className="flex-1 flex flex-col gap-1">
|
<label className="text-indigo-300">
|
||||||
<label>Title</label>
|
Title <span className="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="raw noods"
|
className="px-4 py-2 rounded-xl bg-neutral-800 focus-visible:outline-none"
|
||||||
|
placeholder="Title of the torrent..."
|
||||||
value={obj.name}
|
value={obj.name}
|
||||||
onChange={(e) => setObj((o) => ({ ...o, name: e.target.value }))}
|
onChange={(e) => setObj((o) => ({ ...o, name: e.target.value }))}
|
||||||
/>
|
/>
|
||||||
<label>Info Hash</label>
|
<label className=" text-indigo-300 mt-2 ">
|
||||||
|
Info Hash <span className="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="hex"
|
className="px-4 py-2 rounded-xl bg-neutral-800 focus-visible:outline-none"
|
||||||
|
placeholder="Hash in hex format..."
|
||||||
value={obj.btih}
|
value={obj.btih}
|
||||||
onChange={(e) => setObj((o) => ({ ...o, btih: e.target.value }))}
|
onChange={(e) => setObj((o) => ({ ...o, btih: e.target.value }))}
|
||||||
/>
|
/>
|
||||||
<label>Category</label>
|
<label className=" text-indigo-300 mt-2">
|
||||||
<div className="flex flex-col gap-1">
|
Category <span className="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
{Categories.map((a) => (
|
{Categories.map((a) => (
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<div className="font-bold bg-slate-800 p-1">{a.name}</div>
|
<div className="font-bold">{a.name}</div>
|
||||||
<div className="flex gap-1 flex-wrap">{renderCategories(a, [a.tag])}</div>
|
<div className="flex gap-1 flex-wrap">{renderCategories(a, [a.tag])}</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 flex flex-col gap-1">
|
<div className="flex-1 flex flex-col gap-1">
|
||||||
<label>Description</label>
|
<label className="text-indigo-300">Description</label>
|
||||||
<textarea
|
<textarea
|
||||||
rows={30}
|
rows={30}
|
||||||
className="font-mono text-xs"
|
className="p-4 rounded-xl bg-neutral-800 focus-visible:outline-none font-mono text-sm"
|
||||||
value={obj.desc}
|
value={obj.desc}
|
||||||
onChange={(e) => setObj((o) => ({ ...o, desc: e.target.value }))}
|
onChange={(e) => setObj((o) => ({ ...o, desc: e.target.value }))}
|
||||||
></textarea>
|
></textarea>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<h2>Files</h2>
|
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
|
<label className="text-indigo-300">
|
||||||
|
Files <span className="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
{obj.files.map((a, i) => (
|
{obj.files.map((a, i) => (
|
||||||
<div className="flex gap-1">
|
<div className="flex gap-2">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={a.name}
|
value={a.name}
|
||||||
className="flex-1"
|
className="flex-1 px-3 py-1 bg-neutral-800 rounded-xl focus-visible:outline-none"
|
||||||
placeholder="collection1/IMG_00001.jpg"
|
placeholder="collection1/IMG_00001.jpg"
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setObj((o) => ({
|
setObj((o) => ({
|
||||||
@ -198,6 +230,7 @@ export function NewPage() {
|
|||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
|
className="px-3 py-1 bg-neutral-800 rounded-xl focus-visible:outline-none"
|
||||||
value={a.size}
|
value={a.size}
|
||||||
min={0}
|
min={0}
|
||||||
placeholder="69000"
|
placeholder="69000"
|
||||||
@ -214,6 +247,8 @@ export function NewPage() {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
|
small
|
||||||
|
type="secondary"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setObj((o) => ({
|
setObj((o) => ({
|
||||||
...o,
|
...o,
|
||||||
@ -227,6 +262,7 @@ export function NewPage() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
|
type="secondary"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setObj((o) => ({
|
setObj((o) => ({
|
||||||
...o,
|
...o,
|
||||||
@ -236,7 +272,9 @@ export function NewPage() {
|
|||||||
>
|
>
|
||||||
Add File
|
Add File
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={publish}>Publish</Button>
|
<Button className="mt-4" type="primary" disabled={!entryIsValid(obj)} onClick={publish}>
|
||||||
|
Publish
|
||||||
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { useUserProfile } from "@snort/system-react";
|
import { useUserProfile } from "@snort/system-react";
|
||||||
import { Link, useParams } from "react-router-dom";
|
import { Link, useParams } from "react-router-dom";
|
||||||
import { ProfileImage } from "../element/profile-image";
|
import { ProfileImage } from "../element/profile-image";
|
||||||
import { parseNostrLink } from "@snort/system";
|
import { MetadataCache, parseNostrLink } from "@snort/system";
|
||||||
import { LatestTorrents } from "../element/trending";
|
import { LatestTorrents } from "../element/trending";
|
||||||
import { Text } from "../element/text";
|
import { Text } from "../element/text";
|
||||||
|
|
||||||
@ -12,7 +12,7 @@ export function ProfilePage() {
|
|||||||
|
|
||||||
if (!link) return;
|
if (!link) return;
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-4">
|
||||||
<ProfileSection pubkey={link.id} />
|
<ProfileSection pubkey={link.id} />
|
||||||
<LatestTorrents author={link.id} />
|
<LatestTorrents author={link.id} />
|
||||||
</div>
|
</div>
|
||||||
@ -21,18 +21,29 @@ export function ProfilePage() {
|
|||||||
|
|
||||||
export function ProfileSection({ pubkey }: { pubkey: string }) {
|
export function ProfileSection({ pubkey }: { pubkey: string }) {
|
||||||
const profile = useUserProfile(pubkey);
|
const profile = useUserProfile(pubkey);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-4 mb-4">
|
||||||
<ProfileImage pubkey={pubkey} size={240} />
|
<ProfileImage pubkey={pubkey} size={200} />
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-4">
|
||||||
<h2>{profile?.name}</h2>
|
<h2>{profile?.name}</h2>
|
||||||
<Text content={profile?.about ?? ""} tags={[]} />
|
<Text content={profile?.about ?? ""} tags={[]} />
|
||||||
{profile?.website && (
|
<WebSiteLink profile={profile} />
|
||||||
<Link to={profile.website} target="_blank">
|
|
||||||
{new URL(profile.website).hostname}
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function WebSiteLink({ profile }: { profile?: MetadataCache }) {
|
||||||
|
const website = profile?.website;
|
||||||
|
if (!website) return;
|
||||||
|
|
||||||
|
const hostname = website.startsWith("http") ? new URL(website).hostname : website;
|
||||||
|
const url = website.startsWith("http") ? website : `https://${website}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link to={url} target="_blank">
|
||||||
|
{hostname}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@ -3,7 +3,6 @@ import { useLocation, useParams } from "react-router-dom";
|
|||||||
import { TorrentKind } from "../const";
|
import { TorrentKind } from "../const";
|
||||||
import { useRequestBuilder } from "@snort/system-react";
|
import { useRequestBuilder } from "@snort/system-react";
|
||||||
import { TorrentList } from "../element/torrent-list";
|
import { TorrentList } from "../element/torrent-list";
|
||||||
import { Search } from "../element/search";
|
|
||||||
|
|
||||||
export function SearchPage() {
|
export function SearchPage() {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
@ -26,9 +25,8 @@ export function SearchPage() {
|
|||||||
const data = useRequestBuilder(NoteCollection, rb);
|
const data = useRequestBuilder(NoteCollection, rb);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-4">
|
||||||
<Search term={term} tags={tags} />
|
<h2>Search Results</h2>
|
||||||
<h2>Search Results:</h2>
|
|
||||||
<TorrentList items={data.data ?? []} />
|
<TorrentList items={data.data ?? []} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -1,13 +1,15 @@
|
|||||||
import { unwrap } from "@snort/shared";
|
import { unwrap } from "@snort/shared";
|
||||||
import { NostrLink, NoteCollection, RequestBuilder, TaggedNostrEvent, parseNostrLink } from "@snort/system";
|
import { NostrLink, NoteCollection, RequestBuilder, TaggedNostrEvent, parseNostrLink } from "@snort/system";
|
||||||
import { useRequestBuilder } from "@snort/system-react";
|
import { useRequestBuilder } from "@snort/system-react";
|
||||||
import { useLocation, useNavigate, useParams } from "react-router-dom";
|
import { Link, useLocation, useNavigate, useParams } from "react-router-dom";
|
||||||
import { FormatBytes, TorrentKind } from "../const";
|
import { FormatBytes, TorrentKind } from "../const";
|
||||||
import { ProfileImage } from "../element/profile-image";
|
import { ProfileImage } from "../element/profile-image";
|
||||||
import { MagnetLink } from "../element/magnet";
|
import { MagnetLink } from "../element/magnet";
|
||||||
import { useLogin } from "../login";
|
import { useLogin } from "../login";
|
||||||
import { Button } from "../element/button";
|
import { Button } from "../element/button";
|
||||||
import { Comments } from "../element/comments";
|
import { Comments } from "../element/comments";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
import { Text } from "../element/text";
|
||||||
|
|
||||||
export function TorrentPage() {
|
export function TorrentPage() {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
@ -31,11 +33,11 @@ export function TorrentDetail({ item }: { item: TaggedNostrEvent }) {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const link = NostrLink.fromEvent(item);
|
const link = NostrLink.fromEvent(item);
|
||||||
const name = item.tags.find((a) => a[0] === "title")?.at(1);
|
const name = item.tags.find((a) => a[0] === "title")?.at(1);
|
||||||
const size = item.tags
|
|
||||||
.filter((a) => a[0] === "file")
|
|
||||||
.map((a) => Number(a[2]))
|
|
||||||
.reduce((acc, v) => (acc += v), 0);
|
|
||||||
const files = item.tags.filter((a) => a[0] === "file");
|
const files = item.tags.filter((a) => a[0] === "file");
|
||||||
|
const size = useMemo(() => files.map((a) => Number(a[2])).reduce((acc, v) => (acc += v), 0), [files]);
|
||||||
|
const sortedFiles = useMemo(() => files.sort((a, b) => (a[1] < b[1] ? -1 : 1)), [files]);
|
||||||
|
|
||||||
const tags = item.tags.filter((a) => a[0] === "t").map((a) => a[1]);
|
const tags = item.tags.filter((a) => a[0] === "t").map((a) => a[1]);
|
||||||
|
|
||||||
async function deleteTorrent() {
|
async function deleteTorrent() {
|
||||||
@ -47,45 +49,74 @@ export function TorrentDetail({ item }: { item: TaggedNostrEvent }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-4 pb-8">
|
||||||
<div className="flex gap-2 items-center text-xl">
|
<div className="flex gap-4 items-center text-xl">
|
||||||
<ProfileImage pubkey={item.pubkey} />
|
<ProfileImage pubkey={item.pubkey} />
|
||||||
{name}
|
{name}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1 bg-slate-700 p-2 rounded">
|
<div className=" bg-neutral-900 p-4 rounded-lg">
|
||||||
|
<div className="flex flex-row">
|
||||||
|
<div className="flex flex-col gap-2 flex-grow">
|
||||||
<div>Size: {FormatBytes(size)}</div>
|
<div>Size: {FormatBytes(size)}</div>
|
||||||
<div>Uploaded: {new Date(item.created_at * 1000).toLocaleDateString()}</div>
|
<div>Uploaded: {new Date(item.created_at * 1000).toLocaleDateString()}</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
Tags:{" "}
|
Tags:{" "}
|
||||||
<div className="flex gap-1">
|
<div className="flex gap-2">
|
||||||
{tags.map((a) => (
|
{tags.map((a, i) => (
|
||||||
<div className="rounded p-1 bg-slate-400">#{a}</div>
|
<div key={i} className="rounded-2xl py-1 px-4 bg-indigo-800 hover:bg-indigo-700">
|
||||||
|
<Link to={`/search/?tags=${a}`}>#{a}</Link>
|
||||||
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
</div>
|
||||||
<MagnetLink item={item} className="flex gap-1 items-center">
|
<div className="flex flex-col gap-2">
|
||||||
|
<MagnetLink
|
||||||
|
item={item}
|
||||||
|
className="flex gap-1 items-center px-4 py-3 rounded-full justify-center bg-indigo-800 hover:bg-indigo-700"
|
||||||
|
>
|
||||||
Get this torrent
|
Get this torrent
|
||||||
</MagnetLink>
|
</MagnetLink>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<h3>Description</h3>
|
|
||||||
<pre className="font-mono text-xs bg-slate-700 p-2 rounded overflow-y-auto">{item.content}</pre>
|
|
||||||
<h3>Files</h3>
|
|
||||||
<div className="flex flex-col gap-1 bg-slate-700 p-2 rounded">
|
|
||||||
{files.map((a) => (
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
{a[1]}
|
|
||||||
<small className="text-slate-500 font-semibold">{FormatBytes(Number(a[2]))}</small>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
{item.pubkey == login?.publicKey && (
|
{item.pubkey == login?.publicKey && (
|
||||||
<Button className="bg-red-600 hover:bg-red-800" onClick={deleteTorrent}>
|
<Button type="danger" onClick={deleteTorrent}>
|
||||||
Delete
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<h3>Comments</h3>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{item.content && (
|
||||||
|
<>
|
||||||
|
<h3 className="mt-2">Description</h3>
|
||||||
|
<pre className="font-mono text-sm bg-neutral-900 p-4 rounded-lg overflow-y-auto">
|
||||||
|
<Text content={item.content} tags={item.tags} wrap={false}></Text>
|
||||||
|
</pre>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<h3 className="mt-2">Files</h3>
|
||||||
|
<div className="file-list flex flex-col gap-1 bg-neutral-900 p-4 rounded-lg">
|
||||||
|
<table className="w-max">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
<b>Filename</b>
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
<b>Size</b>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{sortedFiles.map((a, i) => (
|
||||||
|
<tr key={i}>
|
||||||
|
<td className="pr-4">{a[1]}</td>
|
||||||
|
<td className="text-neutral-500 font-semibold text-right text-sm">{FormatBytes(Number(a[2]))}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<h3 className="mt-2">Comments</h3>
|
||||||
<Comments link={link} />
|
<Comments link={link} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
Loading…
Reference in New Issue
Block a user