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:
Kieran 2023-12-19 22:11:28 +00:00
commit 01610ab846
26 changed files with 399 additions and 172 deletions

View File

@ -11,6 +11,7 @@
<meta name="description" content="Torrents on Nostr" />
<link rel="icon" href="/logo_32.png" />
<title>DTAN.XYZ</title>
<link href="/fonts/outfit/outfit.css" rel="stylesheet" />
</head>
<body>

View 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;
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -3,6 +3,8 @@ import { HTMLProps, forwardRef, useState } from "react";
type ButtonProps = Omit<HTMLProps<HTMLButtonElement>, "onClick"> & {
onClick?: (e: React.MouseEvent<HTMLButtonElement>) => Promise<void> | void;
type: "primary" | "secondary" | "danger";
small?: boolean;
};
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 (
<button
{...props}
type="button"
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,
)}
ref={ref}
onClick={clicking}
>
{spinning ? "Loading.." : props.children}
{spinning ? "Loading..." : props.children}
</button>
);
});

View File

@ -19,10 +19,10 @@ export function Comments({ link }: { link: NostrLink }) {
<WriteComment link={link} />
{comments.data
?.sort((a, b) => (a.created_at > b.created_at ? -1 : 1))
.map((a) => (
<div className="flex flex-col gap-2 rounded p-2 bg-slate-900">
.map((a, i) => (
<div key={i} className="flex flex-col gap-2 rounded-lg p-4 bg-neutral-900">
<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>
<Text content={a.content} tags={a.tags} />
</div>
@ -50,10 +50,21 @@ function WriteComment({ link }: { link: NostrLink }) {
}
return (
<div className="rounded p-2 bg-slate-900">
<h3>Write a Comment</h3>
<textarea className="w-full" value={msg} onChange={(e) => setMsg(e.target.value)}></textarea>
<Button onClick={sendComment}>Send</Button>
<div className="rounded-lg p-4 bg-neutral-900 flex flex-row gap-4">
<div className="flex-shrink">
<ProfileImage pubkey={login.publicKey} />
</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>
);
}

View File

@ -26,7 +26,7 @@ export function ProfileImage({ pubkey, size, withName, children, ...props }: Pro
>
<div
{...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}
></div>
{withName === true && <>{profile?.name}</>}

View File

@ -12,19 +12,17 @@ export function Search(params: { term?: string; tags?: Array<string> }) {
}, [params]);
return (
<div>
<input
type="text"
placeholder="Search.."
className="p-3 rounded w-full"
value={term}
onChange={(e) => setTerm(e.target.value)}
onKeyDown={(e) => {
if (e.key == "Enter") {
navigate(`/search/${encodeURIComponent(term)}${tags.length > 0 ? `?tags=${tags.join(",")}` : ""}`);
}
}}
/>
</div>
<input
type="text"
placeholder="Search..."
className="px-4 py-3 bg-neutral-800 rounded-full w-full focus-visible:outline-none"
value={term}
onChange={(e) => setTerm(e.target.value)}
onKeyDown={(e) => {
if (e.key == "Enter") {
navigate(`/search/${encodeURIComponent(term)}${tags.length > 0 ? `?tags=${tags.join(",")}` : ""}`);
}
}}
/>
);
}

View File

@ -3,29 +3,34 @@ import { useMemo } from "react";
import { Mention } from "./mention";
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]);
function renderFrag(f: ParsedFragment) {
function renderFrag(f: ParsedFragment, index: number) {
switch (f.type) {
case "media":
return <img key={index} src={f.content} style={{ maxHeight: "50vh" }} />;
case "mention":
case "link": {
const link = tryParseNostrLink(f.content);
if (link) {
return <Mention link={link} />;
const nostrLink = tryParseNostrLink(f.content);
if (nostrLink) {
return <Mention key={index} link={nostrLink} />;
} else {
return (
<Link to={f.content} target="_blank">
<Link key={index} to={f.content} target="_blank" className="text-indigo-300" rel="noopener noreferrer">
{f.content}
</Link>
);
}
}
default: {
return <span>{f.content}</span>;
return <span key={index}>{f.content}</span>;
}
}
}
return <div className="text">{frags.map(renderFrag)}</div>;
if (wrap) {
return <div className="text">{frags.map(renderFrag)}</div>;
}
return frags.map(renderFrag);
}

View File

@ -1,11 +1,12 @@
.torrent-list {
width: 100%;
border-collapse: collapse;
font-size: 14px;
font-weight: 400;
}
.torrent-list td,
.torrent-list th {
border: 1px solid #333;
padding: 0px 5px;
font-size: 14px;
border-bottom: 1px solid #222;
padding: 0px 6px;
}

View File

@ -4,18 +4,19 @@ import { FormatBytes } from "../const";
import { Link } from "react-router-dom";
import { MagnetLink } from "./magnet";
import { Mention } from "./mention";
import { useMemo } from "react";
export function TorrentList({ items }: { items: Array<TaggedNostrEvent> }) {
return (
<table className="torrent-list">
<table className="torrent-list mb-8">
<thead>
<tr className="bg-slate-600">
<th>Category</th>
<tr className="h-8">
<th className="rounded-tl-lg">Category</th>
<th>Name</th>
<th>Uploaded</th>
<th></th>
<th>Size</th>
<th>From</th>
<th className="rounded-tr-lg">From</th>
</tr>
</thead>
<tbody>
@ -27,45 +28,59 @@ export function TorrentList({ items }: { items: Array<TaggedNostrEvent> }) {
);
}
function TorrentTableEntry({ item }: { item: TaggedNostrEvent }) {
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);
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 (
<tr className="hover:bg-slate-800">
<td>
{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 && " > "}
</>
))}
<>
<Link to={`/search/?tags=${tagUrl}`}>{tag[1]}</Link>
{tags.length !== startIndex + 1 && " > "}
</>
);
}
function TorrentTableEntry({ item }: { item: TaggedNostrEvent }) {
const { name, size } = useMemo(() => {
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);
return { name, size };
}, [item]);
return (
<tr className="hover:bg-indigo-800">
<td className="text-indigo-300">
<TagList tags={item.tags} />
</td>
<td>
<td className="break-words">
<Link to={`/e/${NostrLink.fromEvent(item).encode()}`} state={item}>
{name}
</Link>
</td>
<td>{new Date(item.created_at * 1000).toLocaleDateString()}</td>
<td className="text-neutral-300">{new Date(item.created_at * 1000).toLocaleDateString()}</td>
<td>
<MagnetLink item={item} />
</td>
<td>{FormatBytes(size)}</td>
<td>
<td className="whitespace-nowrap text-right text-neutral-300">{FormatBytes(size)}</td>
<td className="text-indigo-300 whitespace-nowrap break-words text-ellipsis">
<Mention link={new NostrLink(NostrPrefix.PublicKey, item.pubkey)} />
</td>
</tr>

View File

@ -15,7 +15,7 @@ export function LatestTorrents({ author }: { author?: string }) {
return (
<>
<h3>Latest Torrents</h3>
<h2>Latest Torrents</h2>
<TorrentList items={latest.data ?? []} />
</>
);

View File

@ -4,10 +4,16 @@
html,
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;
color: white;
font-size: 16px;
font-family: Arial, Helvetica, sans-serif;
}
h1 {
@ -20,19 +26,15 @@ h3 {
font-size: 21px;
}
input[type="text"],
input[type="number"],
textarea {
color: black;
padding: 4px;
border-radius: 4px;
}
a:not([href="/"], :has(button)) {
text-decoration: dotted;
text-decoration-line: underline;
text-decoration-line: none;
}
.text {
white-space-collapse: preserve-breaks;
}
.file-list {
font-size: 15px;
font-weight: 400;
}

View File

@ -1,10 +1,8 @@
import { Search } from "../element/search";
import { LatestTorrents } from "../element/trending";
export function HomePage() {
return (
<div className="flex flex-col gap-2">
<Search />
<div className="flex flex-col gap-4">
<LatestTorrents />
</div>
);

View File

@ -2,6 +2,7 @@ import { Link, Outlet } from "react-router-dom";
import { Button } from "../element/button";
import { LoginSession, LoginState, useLogin } from "../login";
import { ProfileImage } from "../element/profile-image";
import { Search } from "../element/search";
export function Layout() {
const login = useLogin();
@ -17,14 +18,15 @@ export function Layout() {
return (
<div className="container mx-auto">
<header className="flex justify-between items-center p-1">
<Link to={"/"} className="flex gap-1 items-center">
<header className="flex justify-between items-center pt-4 pb-6">
<Link to={"/"} className="flex gap-2 items-center">
<img src="/logo_256.jpg" className="rounded-full" height={40} width={40} />
<h1 className="font-bold uppercase">dtan.xyz</h1>
</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>
<div className="p-1">
<div>
<Outlet />
</div>
</div>
@ -33,10 +35,10 @@ export function Layout() {
function LoggedInHeader({ login }: { login: LoginSession }) {
return (
<div className="flex items-center gap-3">
<div className="flex items-center gap-2">
<ProfileImage pubkey={login.publicKey} />
<Link to="/new">
<Button>+ Create</Button>
<Button type="primary">+ Create</Button>
</Link>
</div>
);

32
src/page/new.css Normal file
View 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;
}

View File

@ -1,3 +1,4 @@
import "./new.css";
import { ReactNode, useState } from "react";
import { Categories, Category, TorrentKind } from "../const";
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() {
const login = useLogin();
const navigate = useNavigate();
const [obj, setObj] = useState({
const [obj, setObj] = useState<TorrentEntry>({
name: "",
desc: "",
btih: "",
tags: [] as Array<string>,
files: [] as Array<{
name: string;
size: number;
}>,
tags: [],
files: [],
});
async function loadTorrent() {
@ -110,7 +129,7 @@ export function NewPage() {
function renderCategories(a: Category, tags: Array<string>): ReactNode {
return (
<>
<div className="flex gap-1 bg-slate-500 p-1 rounded">
<label className="category">
<input
type="radio"
value={tags.join(",")}
@ -123,8 +142,9 @@ export function NewPage() {
}))
}
/>
<label>{a?.name}</label>
</div>
<div data-checked={obj.tags.join(",") === tags.join(",")}>{a?.name}</div>
</label>
{a.sub_category?.map((b) => renderCategories(b, [...tags, b.tag]))}
</>
);
@ -132,57 +152,69 @@ export function NewPage() {
return (
<>
<h1>New</h1>
<div className="flex gap-1">
<Button onClick={loadTorrent}>Import from Torrent</Button>
<Button>Import from Magnet</Button>
<h2>New Torrent</h2>
<div className="flex gap-4 my-4">
<Button onClick={loadTorrent} type="primary">
Import from Torrent
</Button>
{/*<Button>Import from Magnet</Button>*/}
</div>
<h2>Torrent Info</h2>
<form className="flex flex-col gap-2">
<div className="flex gap-2">
<div className="flex-1 flex flex-col gap-1">
<label>Title</label>
<form className="flex flex-col gap-2 bg-neutral-900 rounded-2xl p-6 mb-8">
<div className="flex gap-4">
<div className="flex-1 flex flex-col gap-2">
<label className="text-indigo-300">
Title <span className="text-red-500">*</span>
</label>
<input
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}
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
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}
onChange={(e) => setObj((o) => ({ ...o, btih: e.target.value }))}
/>
<label>Category</label>
<div className="flex flex-col gap-1">
<label className=" text-indigo-300 mt-2">
Category <span className="text-red-500">*</span>
</label>
<div className="flex flex-col gap-2">
{Categories.map((a) => (
<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>
))}
</div>
</div>
<div className="flex-1 flex flex-col gap-1">
<label>Description</label>
<label className="text-indigo-300">Description</label>
<textarea
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}
onChange={(e) => setObj((o) => ({ ...o, desc: e.target.value }))}
></textarea>
</div>
</div>
<h2>Files</h2>
<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) => (
<div className="flex gap-1">
<div className="flex gap-2">
<input
type="text"
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"
onChange={(e) =>
setObj((o) => ({
@ -198,6 +230,7 @@ export function NewPage() {
/>
<input
type="number"
className="px-3 py-1 bg-neutral-800 rounded-xl focus-visible:outline-none"
value={a.size}
min={0}
placeholder="69000"
@ -214,6 +247,8 @@ export function NewPage() {
}
/>
<Button
small
type="secondary"
onClick={() =>
setObj((o) => ({
...o,
@ -227,6 +262,7 @@ export function NewPage() {
))}
</div>
<Button
type="secondary"
onClick={() =>
setObj((o) => ({
...o,
@ -236,7 +272,9 @@ export function NewPage() {
>
Add File
</Button>
<Button onClick={publish}>Publish</Button>
<Button className="mt-4" type="primary" disabled={!entryIsValid(obj)} onClick={publish}>
Publish
</Button>
</form>
</>
);

View File

@ -1,7 +1,7 @@
import { useUserProfile } from "@snort/system-react";
import { Link, useParams } from "react-router-dom";
import { ProfileImage } from "../element/profile-image";
import { parseNostrLink } from "@snort/system";
import { MetadataCache, parseNostrLink } from "@snort/system";
import { LatestTorrents } from "../element/trending";
import { Text } from "../element/text";
@ -12,7 +12,7 @@ export function ProfilePage() {
if (!link) return;
return (
<div className="flex flex-col gap-2">
<div className="flex flex-col gap-4">
<ProfileSection pubkey={link.id} />
<LatestTorrents author={link.id} />
</div>
@ -21,18 +21,29 @@ export function ProfilePage() {
export function ProfileSection({ pubkey }: { pubkey: string }) {
const profile = useUserProfile(pubkey);
return (
<div className="flex items-center gap-3">
<ProfileImage pubkey={pubkey} size={240} />
<div className="flex flex-col gap-2">
<div className="flex items-center gap-4 mb-4">
<ProfileImage pubkey={pubkey} size={200} />
<div className="flex flex-col gap-4">
<h2>{profile?.name}</h2>
<Text content={profile?.about ?? ""} tags={[]} />
{profile?.website && (
<Link to={profile.website} target="_blank">
{new URL(profile.website).hostname}
</Link>
)}
<WebSiteLink profile={profile} />
</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>
);
}

View File

@ -3,7 +3,6 @@ import { useLocation, useParams } from "react-router-dom";
import { TorrentKind } from "../const";
import { useRequestBuilder } from "@snort/system-react";
import { TorrentList } from "../element/torrent-list";
import { Search } from "../element/search";
export function SearchPage() {
const params = useParams();
@ -26,9 +25,8 @@ export function SearchPage() {
const data = useRequestBuilder(NoteCollection, rb);
return (
<div className="flex flex-col gap-2">
<Search term={term} tags={tags} />
<h2>Search Results:</h2>
<div className="flex flex-col gap-4">
<h2>Search Results</h2>
<TorrentList items={data.data ?? []} />
</div>
);

View File

@ -1,13 +1,15 @@
import { unwrap } from "@snort/shared";
import { NostrLink, NoteCollection, RequestBuilder, TaggedNostrEvent, parseNostrLink } from "@snort/system";
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 { ProfileImage } from "../element/profile-image";
import { MagnetLink } from "../element/magnet";
import { useLogin } from "../login";
import { Button } from "../element/button";
import { Comments } from "../element/comments";
import { useMemo } from "react";
import { Text } from "../element/text";
export function TorrentPage() {
const location = useLocation();
@ -31,11 +33,11 @@ export function TorrentDetail({ item }: { item: TaggedNostrEvent }) {
const navigate = useNavigate();
const link = NostrLink.fromEvent(item);
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 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]);
async function deleteTorrent() {
@ -47,45 +49,74 @@ export function TorrentDetail({ item }: { item: TaggedNostrEvent }) {
}
return (
<div className="flex flex-col gap-2">
<div className="flex gap-2 items-center text-xl">
<div className="flex flex-col gap-4 pb-8">
<div className="flex gap-4 items-center text-xl">
<ProfileImage pubkey={item.pubkey} />
{name}
</div>
<div className="flex flex-col gap-1 bg-slate-700 p-2 rounded">
<div>Size: {FormatBytes(size)}</div>
<div>Uploaded: {new Date(item.created_at * 1000).toLocaleDateString()}</div>
<div className="flex items-center gap-2">
Tags:{" "}
<div className="flex gap-1">
{tags.map((a) => (
<div className="rounded p-1 bg-slate-400">#{a}</div>
))}
<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>Uploaded: {new Date(item.created_at * 1000).toLocaleDateString()}</div>
<div className="flex items-center gap-2">
Tags:{" "}
<div className="flex gap-2">
{tags.map((a, i) => (
<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 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
</MagnetLink>
{item.pubkey == login?.publicKey && (
<Button type="danger" onClick={deleteTorrent}>
Delete
</Button>
)}
</div>
</div>
<div>
<MagnetLink item={item} className="flex gap-1 items-center">
Get this torrent
</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 && (
<Button className="bg-red-600 hover:bg-red-800" onClick={deleteTorrent}>
Delete
</Button>
{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>Comments</h3>
<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} />
</div>
);