diff --git a/index.html b/index.html index 846fefc..95725ed 100644 --- a/index.html +++ b/index.html @@ -11,6 +11,7 @@ DTAN.XYZ + diff --git a/public/fonts/outfit/outfit.css b/public/fonts/outfit/outfit.css new file mode 100644 index 0000000..9e4a910 --- /dev/null +++ b/public/fonts/outfit/outfit.css @@ -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; +} diff --git a/public/fonts/outfit/outfit_400_latin-ext.woff2 b/public/fonts/outfit/outfit_400_latin-ext.woff2 new file mode 100644 index 0000000..9428a7d Binary files /dev/null and b/public/fonts/outfit/outfit_400_latin-ext.woff2 differ diff --git a/public/fonts/outfit/outfit_400_latin.woff2 b/public/fonts/outfit/outfit_400_latin.woff2 new file mode 100644 index 0000000..b4b889e Binary files /dev/null and b/public/fonts/outfit/outfit_400_latin.woff2 differ diff --git a/public/fonts/outfit/outfit_500_latin-ext.woff2 b/public/fonts/outfit/outfit_500_latin-ext.woff2 new file mode 100644 index 0000000..9428a7d Binary files /dev/null and b/public/fonts/outfit/outfit_500_latin-ext.woff2 differ diff --git a/public/fonts/outfit/outfit_500_latin.woff2 b/public/fonts/outfit/outfit_500_latin.woff2 new file mode 100644 index 0000000..b4b889e Binary files /dev/null and b/public/fonts/outfit/outfit_500_latin.woff2 differ diff --git a/public/fonts/outfit/outfit_600_latin-ext.woff2 b/public/fonts/outfit/outfit_600_latin-ext.woff2 new file mode 100644 index 0000000..9428a7d Binary files /dev/null and b/public/fonts/outfit/outfit_600_latin-ext.woff2 differ diff --git a/public/fonts/outfit/outfit_600_latin.woff2 b/public/fonts/outfit/outfit_600_latin.woff2 new file mode 100644 index 0000000..b4b889e Binary files /dev/null and b/public/fonts/outfit/outfit_600_latin.woff2 differ diff --git a/public/fonts/outfit/outfit_700_latin-ext.woff2 b/public/fonts/outfit/outfit_700_latin-ext.woff2 new file mode 100644 index 0000000..9428a7d Binary files /dev/null and b/public/fonts/outfit/outfit_700_latin-ext.woff2 differ diff --git a/public/fonts/outfit/outfit_700_latin.woff2 b/public/fonts/outfit/outfit_700_latin.woff2 new file mode 100644 index 0000000..b4b889e Binary files /dev/null and b/public/fonts/outfit/outfit_700_latin.woff2 differ diff --git a/src/element/button.tsx b/src/element/button.tsx index 68540d1..0d9d7b7 100644 --- a/src/element/button.tsx +++ b/src/element/button.tsx @@ -3,6 +3,8 @@ import { HTMLProps, forwardRef, useState } from "react"; type ButtonProps = Omit, "onClick"> & { onClick?: (e: React.MouseEvent) => Promise | void; + type: "primary" | "secondary" | "danger"; + small?: boolean; }; export const Button = forwardRef((props, ref) => { @@ -19,18 +21,28 @@ export const Button = forwardRef((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 ( ); }); diff --git a/src/element/comments.tsx b/src/element/comments.tsx index f75706b..f71bd08 100644 --- a/src/element/comments.tsx +++ b/src/element/comments.tsx @@ -20,9 +20,9 @@ export function Comments({ link }: { link: NostrLink }) { {comments.data ?.sort((a, b) => (a.created_at > b.created_at ? -1 : 1)) .map((a) => ( -
+
- {new Date(a.created_at * 1000).toLocaleString()} + {new Date(a.created_at * 1000).toLocaleString()}
@@ -50,10 +50,21 @@ function WriteComment({ link }: { link: NostrLink }) { } return ( -
-

Write a Comment

- - +
+
+ +
+
+ +
+
+ +
); } diff --git a/src/element/profile-image.tsx b/src/element/profile-image.tsx index e6f38ab..320030f 100644 --- a/src/element/profile-image.tsx +++ b/src/element/profile-image.tsx @@ -26,7 +26,7 @@ export function ProfileImage({ pubkey, size, withName, children, ...props }: Pro >
{withName === true && <>{profile?.name}} diff --git a/src/element/rich-text-content.tsx b/src/element/rich-text-content.tsx new file mode 100644 index 0000000..db77a99 --- /dev/null +++ b/src/element/rich-text-content.tsx @@ -0,0 +1,73 @@ +import React from 'react'; + +interface RichTextContentProps { + text: string; +} + +// Helper function to check if a string is an image URL +const isImageUrl = (string: string): boolean => /\.(jpeg|jpg|gif|png)$/.test(string); + +// Helper function to check if a string is a web URL +const isWebUrl = (string: string): boolean => { + const urlPattern = new RegExp('^(https?:\\/\\/)' + // protocol + '((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|' + // domain name + '((\\d{1,3}\\.){3}\\d{1,3}))' + // OR IP (v4) address + '(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*' + // port and path + '(\\?[;&a-z\\d%_.~+=-]*)?' + // query string + '(\\#[-a-z\\d_]*)?$', 'i'); // fragment locator + return !!urlPattern.test(string); +}; + +// Function to split the text into segments +const getSegments = (text: string): { type: 'text' | 'image' | 'link', content: string }[] => { + const words = text.match(/(\S+|\s+)/g) || []; + const segments: { type: 'text' | 'image' | 'link', content: string }[] = []; + let currentTextSegment = ''; + + words.forEach(word => { + const trimmedWord = word.trim(); + if (isImageUrl(trimmedWord)) { + if (currentTextSegment) { + segments.push({ type: 'text', content: currentTextSegment }); + currentTextSegment = ''; + } + segments.push({ type: 'image', content: trimmedWord }); + } else if (isWebUrl(trimmedWord)) { + if (currentTextSegment) { + segments.push({ type: 'text', content: currentTextSegment }); + currentTextSegment = ''; + } + segments.push({ type: 'link', content: trimmedWord }); + } else { + currentTextSegment += word; + } + }); + + if (currentTextSegment) { + segments.push({ type: 'text', content: currentTextSegment }); + } + + return segments; +}; + +const RichTextContent: React.FC = ({ text }) => { + const segments = getSegments(text); + + return ( +
+ {segments.map((segment, index) => { + switch(segment.type) { + case 'image': + return ; + case 'link': + return {segment.content}; + default: + return {segment.content}; + } + })} +
+ ); +}; + +export default RichTextContent; + diff --git a/src/element/search.tsx b/src/element/search.tsx index d2460d3..188716e 100644 --- a/src/element/search.tsx +++ b/src/element/search.tsx @@ -12,19 +12,17 @@ export function Search(params: { term?: string; tags?: Array }) { }, [params]); return ( -
- setTerm(e.target.value)} - onKeyDown={(e) => { - if (e.key == "Enter") { - navigate(`/search/${encodeURIComponent(term)}${tags.length > 0 ? `?tags=${tags.join(",")}` : ""}`); - } - }} - /> -
+ setTerm(e.target.value)} + onKeyDown={(e) => { + if (e.key == "Enter") { + navigate(`/search/${encodeURIComponent(term)}${tags.length > 0 ? `?tags=${tags.join(",")}` : ""}`); + } + }} + /> ); } diff --git a/src/element/torrent-list.css b/src/element/torrent-list.css index bc21b33..8559597 100644 --- a/src/element/torrent-list.css +++ b/src/element/torrent-list.css @@ -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; } diff --git a/src/element/torrent-list.tsx b/src/element/torrent-list.tsx index 7f4b68a..21e9959 100644 --- a/src/element/torrent-list.tsx +++ b/src/element/torrent-list.tsx @@ -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 }) { return ( - +
- - + + - + @@ -27,45 +28,59 @@ export function TorrentList({ items }: { items: Array }) { ); } -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) => ( + + )); +} + +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 ( - - + - - + - - + diff --git a/src/element/trending.tsx b/src/element/trending.tsx index 613f233..239a8d3 100644 --- a/src/element/trending.tsx +++ b/src/element/trending.tsx @@ -15,7 +15,7 @@ export function LatestTorrents({ author }: { author?: string }) { return ( <> -

Latest Torrents

+

Latest Torrents

); diff --git a/src/index.css b/src/index.css index 970d306..8c00a1c 100644 --- a/src/index.css +++ b/src/index.css @@ -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; +} \ No newline at end of file diff --git a/src/page/home.tsx b/src/page/home.tsx index 96ddcf7..5e3d000 100644 --- a/src/page/home.tsx +++ b/src/page/home.tsx @@ -1,10 +1,8 @@ -import { Search } from "../element/search"; import { LatestTorrents } from "../element/trending"; export function HomePage() { return ( -
- +
); diff --git a/src/page/layout.tsx b/src/page/layout.tsx index a36fabb..41bafe7 100644 --- a/src/page/layout.tsx +++ b/src/page/layout.tsx @@ -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 (
-
- +
+

dtan.xyz

- {login ? : } +
+ {login ? : }
-
+
@@ -33,10 +35,10 @@ export function Layout() { function LoggedInHeader({ login }: { login: LoginSession }) { return ( -
+
- +
); diff --git a/src/page/new.css b/src/page/new.css new file mode 100644 index 0000000..fc630bf --- /dev/null +++ b/src/page/new.css @@ -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; +} diff --git a/src/page/new.tsx b/src/page/new.tsx index c194c6f..329a4c1 100644 --- a/src/page/new.tsx +++ b/src/page/new.tsx @@ -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 { }); } +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({ name: "", desc: "", btih: "", - tags: [] as Array, - 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): ReactNode { return ( <> -
+
+
{a?.name}
+ + {a.sub_category?.map((b) => renderCategories(b, [...tags, b.tag]))} ); @@ -132,57 +152,69 @@ export function NewPage() { return ( <> -

New

-
- - +

New Torrent

+
+ + {/**/}
-

Torrent Info

-
-
-
- + +
+
+ setObj((o) => ({ ...o, name: e.target.value }))} /> - + setObj((o) => ({ ...o, btih: e.target.value }))} /> - -
+ +
{Categories.map((a) => (
-
{a.name}
+
{a.name}
{renderCategories(a, [a.tag])}
))}
- +
-

Files

+
+ {obj.files.map((a, i) => ( -
+
setObj((o) => ({ @@ -198,6 +230,7 @@ export function NewPage() { />
- + ); diff --git a/src/page/profile.tsx b/src/page/profile.tsx index 3939e5b..7b873b1 100644 --- a/src/page/profile.tsx +++ b/src/page/profile.tsx @@ -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 ( -
+
@@ -21,18 +21,29 @@ export function ProfilePage() { export function ProfileSection({ pubkey }: { pubkey: string }) { const profile = useUserProfile(pubkey); + return ( -
- -
+
+ +

{profile?.name}

- {profile?.website && ( - - {new URL(profile.website).hostname} - - )} +
); } + +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 ( + + {hostname} + + ); +} diff --git a/src/page/search.tsx b/src/page/search.tsx index b65403a..151cc0c 100644 --- a/src/page/search.tsx +++ b/src/page/search.tsx @@ -26,9 +26,9 @@ export function SearchPage() { const data = useRequestBuilder(NoteCollection, rb); return ( -
+
-

Search Results:

+

Search Results

); diff --git a/src/page/torrent.tsx b/src/page/torrent.tsx index 7a02324..b18e2ad 100644 --- a/src/page/torrent.tsx +++ b/src/page/torrent.tsx @@ -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 RichTextContent from "../element/rich-text-content"; 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,70 @@ export function TorrentDetail({ item }: { item: TaggedNostrEvent }) { } return ( -
-
+
+
{name}
-
-
Size: {FormatBytes(size)}
-
Uploaded: {new Date(item.created_at * 1000).toLocaleDateString()}
-
- Tags:{" "} -
- {tags.map((a) => ( -
#{a}
- ))} +
+
+
+
Size: {FormatBytes(size)}
+
Uploaded: {new Date(item.created_at * 1000).toLocaleDateString()}
+
+ Tags:{" "} +
+ {tags.map((a) => ( +
+ #{a} +
+ ))} +
+
+
+
+ + Get this torrent + + {item.pubkey == login?.publicKey && ( + + )}
-
- - Get this torrent - -
-

Description

-
{item.content}
-

Files

-
- {files.map((a) => ( -
- {a[1]} - {FormatBytes(Number(a[2]))} -
- ))} -
- {item.pubkey == login?.publicKey && ( - + {item.content && ( + <> +

Description

+
+            
+          
+ )} -

Comments

+

Files

+
+
Category
Category Name Uploaded SizeFromFrom
- {item.tags - .filter((a) => a[0] === "t") - .slice(0, 3) - .map((a, i, arr) => ( - <> - b[1]) - .join(","), - )}`} - > - {a[1]} - - {arr.length !== i + 1 && " > "} - - ))} + <> + {tag[1]} + {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 ( +
+ + {name} {new Date(item.created_at * 1000).toLocaleDateString()}{new Date(item.created_at * 1000).toLocaleDateString()} {FormatBytes(size)} + {FormatBytes(size)}
+ + + + + {sortedFiles.map((a) => ( + + + + + ))} +
+ Filename + + Size +
{a[1]}{FormatBytes(Number(a[2]))}
+
+

Comments

);