mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-09-21 14:09:17 +02:00
Add simple wiki pages
This commit is contained in:
5
.changeset/clever-nails-invite.md
Normal file
5
.changeset/clever-nails-invite.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"nostrudel": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Add simple wiki pages
|
@@ -75,6 +75,7 @@
|
|||||||
"react-force-graph-2d": "^1.25.1",
|
"react-force-graph-2d": "^1.25.1",
|
||||||
"react-force-graph-3d": "^1.23.1",
|
"react-force-graph-3d": "^1.23.1",
|
||||||
"react-hook-form": "^7.45.4",
|
"react-hook-form": "^7.45.4",
|
||||||
|
"react-markdown": "^9.0.1",
|
||||||
"react-mosaic-component": "^6.1.0",
|
"react-mosaic-component": "^6.1.0",
|
||||||
"react-photo-album": "^2.3.0",
|
"react-photo-album": "^2.3.0",
|
||||||
"react-qr-barcode-scanner": "^1.0.6",
|
"react-qr-barcode-scanner": "^1.0.6",
|
||||||
@@ -82,6 +83,7 @@
|
|||||||
"react-singleton-hook": "^4.0.1",
|
"react-singleton-hook": "^4.0.1",
|
||||||
"react-use": "^17.4.0",
|
"react-use": "^17.4.0",
|
||||||
"react-virtualized-auto-sizer": "^1.0.20",
|
"react-virtualized-auto-sizer": "^1.0.20",
|
||||||
|
"remark-gfm": "^4.0.0",
|
||||||
"three": "^0.160.0",
|
"three": "^0.160.0",
|
||||||
"three-spritetext": "^1.8.1",
|
"three-spritetext": "^1.8.1",
|
||||||
"three-stdlib": "^2.29.4",
|
"three-stdlib": "^2.29.4",
|
||||||
|
14
src/app.tsx
14
src/app.tsx
@@ -119,6 +119,11 @@ const TorrentsView = lazy(() => import("./views/torrents"));
|
|||||||
const TorrentDetailsView = lazy(() => import("./views/torrents/torrent"));
|
const TorrentDetailsView = lazy(() => import("./views/torrents/torrent"));
|
||||||
const NewTorrentView = lazy(() => import("./views/torrents/new"));
|
const NewTorrentView = lazy(() => import("./views/torrents/new"));
|
||||||
|
|
||||||
|
const WikiHomeView = lazy(() => import("./views/wiki"));
|
||||||
|
const WikiPageView = lazy(() => import("./views/wiki/page"));
|
||||||
|
const WikiTopicView = lazy(() => import("./views/wiki/topic"));
|
||||||
|
const WikiSearchView = lazy(() => import("./views/wiki/search"));
|
||||||
|
|
||||||
const overrideReactTextareaAutocompleteStyles = css`
|
const overrideReactTextareaAutocompleteStyles = css`
|
||||||
.rta__autocomplete {
|
.rta__autocomplete {
|
||||||
z-index: var(--chakra-zIndices-popover);
|
z-index: var(--chakra-zIndices-popover);
|
||||||
@@ -307,6 +312,15 @@ const router = createHashRouter([
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "wiki",
|
||||||
|
children: [
|
||||||
|
{ path: "search", element: <WikiSearchView /> },
|
||||||
|
{ path: "topic/:topic", element: <WikiTopicView /> },
|
||||||
|
{ path: "page/:naddr", element: <WikiPageView /> },
|
||||||
|
{ path: "", element: <WikiHomeView /> },
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "dvm",
|
path: "dvm",
|
||||||
children: [
|
children: [
|
||||||
|
@@ -67,6 +67,7 @@ import Magnet from "./icons/magnet";
|
|||||||
import Recording02 from "./icons/recording-02";
|
import Recording02 from "./icons/recording-02";
|
||||||
import Upload01 from "./icons/upload-01";
|
import Upload01 from "./icons/upload-01";
|
||||||
import Modem02 from "./icons/modem-02";
|
import Modem02 from "./icons/modem-02";
|
||||||
|
import BookOpen01 from "./icons/book-open-01";
|
||||||
|
|
||||||
const defaultProps: IconProps = { boxSize: 4 };
|
const defaultProps: IconProps = { boxSize: 4 };
|
||||||
|
|
||||||
@@ -245,3 +246,5 @@ export const TrackIcon = Recording02;
|
|||||||
|
|
||||||
export const InboxIcon = Download01;
|
export const InboxIcon = Download01;
|
||||||
export const OutboxIcon = Upload01;
|
export const OutboxIcon = Upload01;
|
||||||
|
|
||||||
|
export const WikiIcon = BookOpen01
|
||||||
|
@@ -1,12 +1,10 @@
|
|||||||
import { Box, Button, ButtonProps, Link, Text, useDisclosure } from "@chakra-ui/react";
|
import { Box, Button, ButtonProps, Icon, Link, Text, others, useDisclosure } from "@chakra-ui/react";
|
||||||
import { Link as RouterLink, useLocation } from "react-router-dom";
|
import { Link as RouterLink, useLocation } from "react-router-dom";
|
||||||
import { nip19 } from "nostr-tools";
|
import { nip19 } from "nostr-tools";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DirectMessagesIcon,
|
DirectMessagesIcon,
|
||||||
CommunityIcon,
|
|
||||||
LiveStreamIcon,
|
|
||||||
NotificationsIcon,
|
NotificationsIcon,
|
||||||
ProfileIcon,
|
ProfileIcon,
|
||||||
RelayIcon,
|
RelayIcon,
|
||||||
@@ -15,7 +13,6 @@ import {
|
|||||||
LogoutIcon,
|
LogoutIcon,
|
||||||
NotesIcon,
|
NotesIcon,
|
||||||
LightningIcon,
|
LightningIcon,
|
||||||
ChannelsIcon,
|
|
||||||
} from "../icons";
|
} from "../icons";
|
||||||
import useCurrentAccount from "../../hooks/use-current-account";
|
import useCurrentAccount from "../../hooks/use-current-account";
|
||||||
import accountService from "../../services/account";
|
import accountService from "../../services/account";
|
||||||
@@ -26,7 +23,10 @@ import Package from "../icons/package";
|
|||||||
import Rocket02 from "../icons/rocket-02";
|
import Rocket02 from "../icons/rocket-02";
|
||||||
import { useBreakpointValue } from "../../providers/global/breakpoint-provider";
|
import { useBreakpointValue } from "../../providers/global/breakpoint-provider";
|
||||||
import KeyboardShortcut from "../keyboard-shortcut";
|
import KeyboardShortcut from "../keyboard-shortcut";
|
||||||
import Mail02 from "../icons/mail-02";
|
import useRecentIds from "../../hooks/use-recent-ids";
|
||||||
|
import { allApps } from "../../views/other-stuff/apps";
|
||||||
|
import { App, AppIcon } from "../../views/other-stuff/component/app-card";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
|
||||||
export default function NavItems() {
|
export default function NavItems() {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
@@ -43,8 +43,10 @@ export default function NavItems() {
|
|||||||
variant: "link",
|
variant: "link",
|
||||||
};
|
};
|
||||||
|
|
||||||
let active = "notes";
|
let active = "";
|
||||||
if (location.pathname.startsWith("/notifications")) active = "notifications";
|
if (location.pathname.startsWith("/n/")) active = "notes";
|
||||||
|
else if (location.pathname === "/") active = "notes";
|
||||||
|
else if (location.pathname.startsWith("/notifications")) active = "notifications";
|
||||||
else if (location.pathname.startsWith("/launchpad")) active = "launchpad";
|
else if (location.pathname.startsWith("/launchpad")) active = "launchpad";
|
||||||
else if (location.pathname.startsWith("/dvm")) active = "dvm";
|
else if (location.pathname.startsWith("/dvm")) active = "dvm";
|
||||||
else if (location.pathname.startsWith("/dm")) active = "dm";
|
else if (location.pathname.startsWith("/dm")) active = "dm";
|
||||||
@@ -75,6 +77,25 @@ export default function NavItems() {
|
|||||||
active = "profile";
|
active = "profile";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { recent: recentApps } = useRecentIds("apps");
|
||||||
|
const otherStuff = useMemo(() => {
|
||||||
|
const apps = recentApps.map((id) => allApps.find((app) => app.id === id)).filter(Boolean) as App[];
|
||||||
|
if (apps.length > 3) {
|
||||||
|
apps.length = 3;
|
||||||
|
} else {
|
||||||
|
if (apps.length < 3 && !apps.some((a) => a.id === "streams")) {
|
||||||
|
apps.push(allApps.find((app) => app.id === "streams")!);
|
||||||
|
}
|
||||||
|
if (apps.length < 3 && !apps.some((a) => a.id === "communities")) {
|
||||||
|
apps.push(allApps.find((app) => app.id === "communities")!);
|
||||||
|
}
|
||||||
|
if (apps.length < 3 && !apps.some((a) => a.id === "channels")) {
|
||||||
|
apps.push(allApps.find((app) => app.id === "channels")!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return apps;
|
||||||
|
}, [recentApps]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Button
|
<Button
|
||||||
@@ -162,33 +183,18 @@ export default function NavItems() {
|
|||||||
<Text position="relative" py="2" color="GrayText">
|
<Text position="relative" py="2" color="GrayText">
|
||||||
Other Stuff
|
Other Stuff
|
||||||
</Text>
|
</Text>
|
||||||
<Button
|
{otherStuff.map((app) => (
|
||||||
as={RouterLink}
|
<Button
|
||||||
to="/streams"
|
key={app.id}
|
||||||
leftIcon={<LiveStreamIcon boxSize={6} />}
|
as={RouterLink}
|
||||||
colorScheme={active === "streams" ? "primary" : undefined}
|
to={app.to}
|
||||||
{...buttonProps}
|
leftIcon={<AppIcon size="6" app={app} />}
|
||||||
>
|
colorScheme={typeof app.to === "string" && location.pathname.startsWith(app.to) ? "primary" : undefined}
|
||||||
Streams
|
{...buttonProps}
|
||||||
</Button>
|
>
|
||||||
<Button
|
{app.title}
|
||||||
as={RouterLink}
|
</Button>
|
||||||
to="/communities"
|
))}
|
||||||
leftIcon={<CommunityIcon boxSize={6} />}
|
|
||||||
colorScheme={active === "communities" ? "primary" : undefined}
|
|
||||||
{...buttonProps}
|
|
||||||
>
|
|
||||||
Communities
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
as={RouterLink}
|
|
||||||
to="/channels"
|
|
||||||
leftIcon={<ChannelsIcon boxSize={6} />}
|
|
||||||
colorScheme={active === "channels" ? "primary" : undefined}
|
|
||||||
{...buttonProps}
|
|
||||||
>
|
|
||||||
Channels
|
|
||||||
</Button>
|
|
||||||
<Button
|
<Button
|
||||||
as={RouterLink}
|
as={RouterLink}
|
||||||
to="/other-stuff"
|
to="/other-stuff"
|
||||||
|
13
src/helpers/nostr/wiki.ts
Normal file
13
src/helpers/nostr/wiki.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { NostrEvent } from "nostr-tools";
|
||||||
|
|
||||||
|
export const WIKI_PAGE_KIND = 30818;
|
||||||
|
|
||||||
|
export function getPageTitle(page: NostrEvent) {
|
||||||
|
return page.tags.find((t) => t[0] === "title")?.[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPageTopic(page: NostrEvent) {
|
||||||
|
const d = page.tags.find((t) => t[0] === "d")?.[1];
|
||||||
|
if (!d) throw new Error("Page missing d tag");
|
||||||
|
return d;
|
||||||
|
}
|
@@ -1,8 +1,26 @@
|
|||||||
import { useLocalStorage } from "react-use";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import { useCallback } from "react";
|
|
||||||
|
|
||||||
export default function useRecentIds(key: string, maxLength?: number) {
|
export default function useRecentIds(key: string, maxLength?: number) {
|
||||||
const [recent = [], setRecent] = useLocalStorage<string[]>("recent-" + key, []);
|
const value = localStorage.getItem("recent-" + key);
|
||||||
|
const recent = value ? (JSON.parse(value) as string[]) : [];
|
||||||
|
|
||||||
|
const [_, update] = useState(0);
|
||||||
|
useEffect(() => {
|
||||||
|
const listener = (e: StorageEvent) => {
|
||||||
|
if (e.key === key) update(Math.random());
|
||||||
|
};
|
||||||
|
window.addEventListener("storage", listener);
|
||||||
|
|
||||||
|
return () => window.removeEventListener("storage", listener);
|
||||||
|
}, [key, update]);
|
||||||
|
|
||||||
|
const setRecent = useCallback((recent: string[] | ((recent: string[]) => string[])) => {
|
||||||
|
if (typeof recent === "function") {
|
||||||
|
const value = localStorage.getItem("recent-" + key);
|
||||||
|
const newArr = recent(value ? (JSON.parse(value) as string[]) : []);
|
||||||
|
localStorage.setItem("recent-" + key, JSON.stringify(newArr));
|
||||||
|
} else localStorage.setItem("recent-" + key, JSON.stringify(recent));
|
||||||
|
}, []);
|
||||||
|
|
||||||
const useThing = useCallback(
|
const useThing = useCallback(
|
||||||
(app: string) => {
|
(app: string) => {
|
||||||
|
@@ -13,6 +13,7 @@ import {
|
|||||||
SearchIcon,
|
SearchIcon,
|
||||||
TorrentIcon,
|
TorrentIcon,
|
||||||
TrackIcon,
|
TrackIcon,
|
||||||
|
WikiIcon,
|
||||||
} from "../../components/icons";
|
} from "../../components/icons";
|
||||||
import { App } from "./component/app-card";
|
import { App } from "./component/app-card";
|
||||||
import ShieldOff from "../../components/icons/shield-off";
|
import ShieldOff from "../../components/icons/shield-off";
|
||||||
@@ -22,6 +23,13 @@ import MessageQuestionSquare from "../../components/icons/message-question-squar
|
|||||||
import UploadCloud01 from "../../components/icons/upload-cloud-01";
|
import UploadCloud01 from "../../components/icons/upload-cloud-01";
|
||||||
|
|
||||||
export const internalApps: App[] = [
|
export const internalApps: App[] = [
|
||||||
|
{
|
||||||
|
title: "Streams",
|
||||||
|
description: "Watch live streams",
|
||||||
|
icon: LiveStreamIcon,
|
||||||
|
id: "streams",
|
||||||
|
to: "/streams",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: "Communities",
|
title: "Communities",
|
||||||
description: "Create and manage communities",
|
description: "Create and manage communities",
|
||||||
@@ -29,6 +37,7 @@ export const internalApps: App[] = [
|
|||||||
id: "communities",
|
id: "communities",
|
||||||
to: "/communities",
|
to: "/communities",
|
||||||
},
|
},
|
||||||
|
{ title: "Wiki", description: "Browse wiki pages", icon: WikiIcon, id: "wiki", to: "/wiki" },
|
||||||
{
|
{
|
||||||
title: "Channels",
|
title: "Channels",
|
||||||
description: "Browse and talk in channels",
|
description: "Browse and talk in channels",
|
||||||
|
@@ -1,6 +1,5 @@
|
|||||||
import { ReactNode } from "react";
|
|
||||||
import { Link as RouterLink, To } from "react-router-dom";
|
import { Link as RouterLink, To } from "react-router-dom";
|
||||||
import { Box, Card, ComponentWithAs, Flex, Heading, IconProps, Image, LinkBox, Text } from "@chakra-ui/react";
|
import { Card, ComponentWithAs, Flex, Heading, IconProps, Image, LinkBox, Text } from "@chakra-ui/react";
|
||||||
|
|
||||||
import HoverLinkOverlay from "../../../components/hover-link-overlay";
|
import HoverLinkOverlay from "../../../components/hover-link-overlay";
|
||||||
|
|
||||||
@@ -14,17 +13,19 @@ export type App = {
|
|||||||
to: To;
|
to: To;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function AppCard({ app, onClick }: { app: App; onClick?: () => void }) {
|
export function AppIcon({ app, size }: { app: App; size: string }) {
|
||||||
let icon: ReactNode = null;
|
|
||||||
if (app.icon) {
|
if (app.icon) {
|
||||||
const Icon = app.icon;
|
const Icon = app.icon;
|
||||||
icon = <Icon boxSize={10} />;
|
return <Icon boxSize={size} />;
|
||||||
} else if (app.image) icon = <Image src={app.image} h="10" aspectRatio={1} />;
|
} else if (app.image) return <Image src={app.image} h={size} aspectRatio={1} />;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AppCard({ app, onClick }: { app: App; onClick?: () => void }) {
|
||||||
return (
|
return (
|
||||||
<Flex as={LinkBox} gap="4" alignItems="flex-start">
|
<Flex as={LinkBox} gap="4" alignItems="flex-start">
|
||||||
<Card p="3" borderRadius="lg">
|
<Card p="3" borderRadius="lg">
|
||||||
{icon}
|
<AppIcon app={app} size="10" />
|
||||||
</Card>
|
</Card>
|
||||||
<Flex direction="column" gap="2" py="2">
|
<Flex direction="column" gap="2" py="2">
|
||||||
<Heading size="md">
|
<Heading size="md">
|
||||||
|
33
src/views/wiki/components/markdown.tsx
Normal file
33
src/views/wiki/components/markdown.tsx
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { Image, Link, LinkProps, Text, TextProps } from "@chakra-ui/react";
|
||||||
|
import { NostrEvent } from "nostr-tools";
|
||||||
|
import Markdown, { Components } from "react-markdown";
|
||||||
|
import remarkGfm from "remark-gfm";
|
||||||
|
|
||||||
|
function A({ children, ...props }: LinkProps) {
|
||||||
|
return (
|
||||||
|
<Link color="blue.500" isExternal {...props}>
|
||||||
|
{children}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
function P({ children, ...props }: TextProps) {
|
||||||
|
return (
|
||||||
|
<Text py="2" {...props}>
|
||||||
|
{children}
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const components: Partial<Components> = {
|
||||||
|
a: A,
|
||||||
|
img: Image,
|
||||||
|
p: P,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function MarkdownContent({ event }: { event: NostrEvent }) {
|
||||||
|
return (
|
||||||
|
<Markdown remarkPlugins={[remarkGfm]} components={components}>
|
||||||
|
{event.content}
|
||||||
|
</Markdown>
|
||||||
|
);
|
||||||
|
}
|
27
src/views/wiki/components/wiki-page-result.tsx
Normal file
27
src/views/wiki/components/wiki-page-result.tsx
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { Heading, LinkBox, Text } from "@chakra-ui/react";
|
||||||
|
import { NostrEvent } from "nostr-tools";
|
||||||
|
import { Link as RouterLink } from "react-router-dom";
|
||||||
|
|
||||||
|
import HoverLinkOverlay from "../../../components/hover-link-overlay";
|
||||||
|
import { getSharableEventAddress } from "../../../helpers/nip19";
|
||||||
|
import { getPageTitle } from "../../../helpers/nostr/wiki";
|
||||||
|
import UserLink from "../../../components/user/user-link";
|
||||||
|
import Timestamp from "../../../components/timestamp";
|
||||||
|
|
||||||
|
export default function WikiPageResult({ page }: { page: NostrEvent }) {
|
||||||
|
return (
|
||||||
|
<LinkBox py="2" px="4">
|
||||||
|
<Heading size="md">
|
||||||
|
<HoverLinkOverlay as={RouterLink} to={`/wiki/page/${getSharableEventAddress(page)}`}>
|
||||||
|
{getPageTitle(page)}
|
||||||
|
</HoverLinkOverlay>
|
||||||
|
</Heading>
|
||||||
|
<Text>
|
||||||
|
by <UserLink pubkey={page.pubkey} /> - <Timestamp timestamp={page.created_at} />
|
||||||
|
</Text>
|
||||||
|
<Text color="GrayText" noOfLines={2}>
|
||||||
|
{page.content.split("\n")[0]}
|
||||||
|
</Text>
|
||||||
|
</LinkBox>
|
||||||
|
);
|
||||||
|
}
|
30
src/views/wiki/components/wiki-search-form.tsx
Normal file
30
src/views/wiki/components/wiki-search-form.tsx
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { Button, Flex, FlexProps, Input } from "@chakra-ui/react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
|
export default function WikiSearchForm({ ...props }: Omit<FlexProps, "children">) {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { register, handleSubmit } = useForm({ defaultValues: { search: "" } });
|
||||||
|
|
||||||
|
const onSubmit = handleSubmit((values) => {
|
||||||
|
// navigate(`/wiki/search?q=${encodeURIComponent(values.search)}`);
|
||||||
|
navigate(`/wiki/topic/${values.search}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex gap="2" as="form" maxW="md" {...props} onSubmit={onSubmit}>
|
||||||
|
<Input
|
||||||
|
{...register("search", { required: true })}
|
||||||
|
type="search"
|
||||||
|
name="search"
|
||||||
|
autoComplete="on"
|
||||||
|
w="sm"
|
||||||
|
placeholder="Search Wikifreedia"
|
||||||
|
isRequired
|
||||||
|
/>
|
||||||
|
<Button type="submit" colorScheme="primary">
|
||||||
|
Search
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
}
|
14
src/views/wiki/hooks/use-wiki-topic-timeline.tsx
Normal file
14
src/views/wiki/hooks/use-wiki-topic-timeline.tsx
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { WIKI_PAGE_KIND } from "../../../helpers/nostr/wiki";
|
||||||
|
import { useReadRelays } from "../../../hooks/use-client-relays";
|
||||||
|
import useTimelineLoader from "../../../hooks/use-timeline-loader";
|
||||||
|
|
||||||
|
export default function useWikiTopicTimeline(topic: string) {
|
||||||
|
const relays = useReadRelays(["wss://relay.wikifreedia.xyz/"]);
|
||||||
|
|
||||||
|
return useTimelineLoader(
|
||||||
|
`wiki-${topic.toLocaleLowerCase()}-pages`,
|
||||||
|
relays,
|
||||||
|
[{ kinds: [WIKI_PAGE_KIND], "#d": [topic.toLocaleLowerCase()] }],
|
||||||
|
{ eventFilter: (e) => e.content.length > 0 },
|
||||||
|
);
|
||||||
|
}
|
44
src/views/wiki/index.tsx
Normal file
44
src/views/wiki/index.tsx
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { Box, Flex, Heading, SimpleGrid } from "@chakra-ui/react";
|
||||||
|
import { Link } from "@chakra-ui/react";
|
||||||
|
import { Link as RouterLink } from "react-router-dom";
|
||||||
|
|
||||||
|
import VerticalPageLayout from "../../components/vertical-page-layout";
|
||||||
|
import WikiSearchForm from "./components/wiki-search-form";
|
||||||
|
import { WIKI_PAGE_KIND } from "../../helpers/nostr/wiki";
|
||||||
|
import useTimelineLoader from "../../hooks/use-timeline-loader";
|
||||||
|
import { useReadRelays } from "../../hooks/use-client-relays";
|
||||||
|
import useSubject from "../../hooks/use-subject";
|
||||||
|
import { getWebOfTrust } from "../../services/web-of-trust";
|
||||||
|
import WikiPageResult from "./components/wiki-page-result";
|
||||||
|
import TimelineActionAndStatus from "../../components/timeline-page/timeline-action-and-status";
|
||||||
|
|
||||||
|
export default function WikiHomeView() {
|
||||||
|
const relays = useReadRelays(["wss://relay.wikifreedia.xyz/"]);
|
||||||
|
const timeline = useTimelineLoader(`wiki-recent-pages`, relays, [{ kinds: [WIKI_PAGE_KIND] }]);
|
||||||
|
|
||||||
|
const pages = useSubject(timeline.timeline).filter((p) => p.content.length > 0);
|
||||||
|
const sorted = getWebOfTrust().sortByDistanceAndConnections(pages, (p) => p.pubkey);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<VerticalPageLayout>
|
||||||
|
<Flex mx="auto" mt="10vh" mb="10vh" direction="column" alignItems="center" maxW="full" gap="4">
|
||||||
|
<Heading>
|
||||||
|
<Link as={RouterLink} to="/wiki/topic/wikifreedia">
|
||||||
|
Wikifreedia
|
||||||
|
</Link>
|
||||||
|
</Heading>
|
||||||
|
<WikiSearchForm maxW="full" />
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<Heading size="md" mt="4">
|
||||||
|
Recent Updates:
|
||||||
|
</Heading>
|
||||||
|
<SimpleGrid spacing="2" columns={{ base: 1, lg: 2, xl: 3 }}>
|
||||||
|
{sorted.map((page) => (
|
||||||
|
<WikiPageResult key={page.id} page={page} />
|
||||||
|
))}
|
||||||
|
</SimpleGrid>
|
||||||
|
<TimelineActionAndStatus timeline={timeline} />
|
||||||
|
</VerticalPageLayout>
|
||||||
|
);
|
||||||
|
}
|
61
src/views/wiki/page.tsx
Normal file
61
src/views/wiki/page.tsx
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import { NostrEvent } from "nostr-tools";
|
||||||
|
import { Box, Card, Divider, Flex, Heading, Link, Spinner, Text } from "@chakra-ui/react";
|
||||||
|
import { Link as RouterLink } from "react-router-dom";
|
||||||
|
|
||||||
|
import useParamsAddressPointer from "../../hooks/use-params-address-pointer";
|
||||||
|
import useReplaceableEvent from "../../hooks/use-replaceable-event";
|
||||||
|
import VerticalPageLayout from "../../components/vertical-page-layout";
|
||||||
|
import { getPageTitle, getPageTopic } from "../../helpers/nostr/wiki";
|
||||||
|
import WikiSearchForm from "./components/wiki-search-form";
|
||||||
|
import MarkdownContent from "./components/markdown";
|
||||||
|
import UserLink from "../../components/user/user-link";
|
||||||
|
import { getWebOfTrust } from "../../services/web-of-trust";
|
||||||
|
import useSubject from "../../hooks/use-subject";
|
||||||
|
import useWikiTopicTimeline from "./hooks/use-wiki-topic-timeline";
|
||||||
|
import WikiPageResult from "./components/wiki-page-result";
|
||||||
|
import Timestamp from "../../components/timestamp";
|
||||||
|
|
||||||
|
function WikiPagePage({ page }: { page: NostrEvent }) {
|
||||||
|
const topic = getPageTopic(page);
|
||||||
|
const timeline = useWikiTopicTimeline(topic);
|
||||||
|
|
||||||
|
const pages = useSubject(timeline.timeline).filter((p) => p.pubkey !== page.pubkey);
|
||||||
|
const sorted = getWebOfTrust().sortByDistanceAndConnections(pages, (p) => p.pubkey);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<VerticalPageLayout>
|
||||||
|
<Flex gap="2" wrap="wrap">
|
||||||
|
<Heading mr="4">
|
||||||
|
<Link as={RouterLink} to="/wiki">
|
||||||
|
Wikifreedia
|
||||||
|
</Link>
|
||||||
|
</Heading>
|
||||||
|
<WikiSearchForm w="full" />
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Heading>{getPageTitle(page)}</Heading>
|
||||||
|
<Text>
|
||||||
|
by <UserLink pubkey={page.pubkey} /> - <Timestamp timestamp={page.created_at} />
|
||||||
|
</Text>
|
||||||
|
<Divider my="2" />
|
||||||
|
<MarkdownContent event={page} />
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Heading size="lg" mt="4">
|
||||||
|
Other Versions:
|
||||||
|
</Heading>
|
||||||
|
{sorted.slice(0, 6).map((page) => (
|
||||||
|
<WikiPageResult key={page.id} page={page} />
|
||||||
|
))}
|
||||||
|
</VerticalPageLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function WikiPageView() {
|
||||||
|
const pointer = useParamsAddressPointer("naddr");
|
||||||
|
const event = useReplaceableEvent(pointer, ["wss://relay.wikifreedia.xyz/"]);
|
||||||
|
|
||||||
|
if (!event) return <Spinner />;
|
||||||
|
return <WikiPagePage page={event} />;
|
||||||
|
}
|
54
src/views/wiki/search.tsx
Normal file
54
src/views/wiki/search.tsx
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { Navigate } from "react-router-dom";
|
||||||
|
import { Button, Flex, Heading, Input, Link } from "@chakra-ui/react";
|
||||||
|
import { Link as RouterLink } from "react-router-dom";
|
||||||
|
import { NostrEvent } from "nostr-tools";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
|
||||||
|
import VerticalPageLayout from "../../components/vertical-page-layout";
|
||||||
|
import useRouteSearchValue from "../../hooks/use-route-search-value";
|
||||||
|
import useTimelineLoader from "../../hooks/use-timeline-loader";
|
||||||
|
import { subscribeMany } from "../../helpers/relay";
|
||||||
|
import { SEARCH_RELAYS } from "../../const";
|
||||||
|
|
||||||
|
export default function WikiSearchView() {
|
||||||
|
const { value: query, setValue: setQuery } = useRouteSearchValue("q");
|
||||||
|
if (!query) return <Navigate to="/wiki" />;
|
||||||
|
|
||||||
|
const { register, handleSubmit } = useForm({ defaultValues: { search: query } });
|
||||||
|
const onSubmit = handleSubmit((values) => {
|
||||||
|
setQuery(values.search);
|
||||||
|
});
|
||||||
|
|
||||||
|
const [results, setResults] = useState<NostrEvent[]>([]);
|
||||||
|
|
||||||
|
// useEffect(() => {
|
||||||
|
// const sub = subscribeMany([SEARCH_RELAYS]);
|
||||||
|
// }, [query]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<VerticalPageLayout>
|
||||||
|
<Flex gap="2" wrap="wrap">
|
||||||
|
<Heading mr="4">
|
||||||
|
<Link as={RouterLink} to="/wiki">
|
||||||
|
Wikifreedia
|
||||||
|
</Link>
|
||||||
|
</Heading>
|
||||||
|
<Flex gap="2" as="form" maxW="md" onSubmit={onSubmit} w="full">
|
||||||
|
<Input
|
||||||
|
{...register("search", { required: true })}
|
||||||
|
type="search"
|
||||||
|
name="search"
|
||||||
|
autoComplete="on"
|
||||||
|
w="sm"
|
||||||
|
placeholder="Search Wikifreedia"
|
||||||
|
isRequired
|
||||||
|
/>
|
||||||
|
<Button type="submit" colorScheme="primary">
|
||||||
|
Search
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</VerticalPageLayout>
|
||||||
|
);
|
||||||
|
}
|
28
src/views/wiki/topic.tsx
Normal file
28
src/views/wiki/topic.tsx
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { Heading } from "@chakra-ui/react";
|
||||||
|
import { Navigate, useParams } from "react-router-dom";
|
||||||
|
|
||||||
|
import VerticalPageLayout from "../../components/vertical-page-layout";
|
||||||
|
import useSubject from "../../hooks/use-subject";
|
||||||
|
import { getWebOfTrust } from "../../services/web-of-trust";
|
||||||
|
import WikiPageResult from "./components/wiki-page-result";
|
||||||
|
import useWikiTopicTimeline from "./hooks/use-wiki-topic-timeline";
|
||||||
|
|
||||||
|
export default function WikiTopicView() {
|
||||||
|
const { topic } = useParams();
|
||||||
|
if (!topic) return <Navigate to="/wiki" />;
|
||||||
|
|
||||||
|
const timeline = useWikiTopicTimeline(topic);
|
||||||
|
|
||||||
|
const pages = useSubject(timeline.timeline).filter((p) => p.content.length > 0);
|
||||||
|
const sorted = getWebOfTrust().sortByDistanceAndConnections(pages, (p) => p.pubkey);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<VerticalPageLayout>
|
||||||
|
<Heading>{topic}</Heading>
|
||||||
|
|
||||||
|
{sorted.map((page) => (
|
||||||
|
<WikiPageResult key={page.id} page={page} />
|
||||||
|
))}
|
||||||
|
</VerticalPageLayout>
|
||||||
|
);
|
||||||
|
}
|
Reference in New Issue
Block a user