mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-09-30 14:05:28 +02:00
UI improvements
This commit is contained in:
50
src/app.jsx
50
src/app.jsx
@@ -1,23 +1,55 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { ChakraProvider } from "@chakra-ui/react";
|
import { Spinner } from "@chakra-ui/react";
|
||||||
import { HashRouter, Route, Routes } from "react-router-dom";
|
import { Route, Routes } from "react-router-dom";
|
||||||
import { HomeView } from "./views/home";
|
import { HomeView } from "./views/home";
|
||||||
import { UserView } from "./views/user";
|
import { UserView } from "./views/user";
|
||||||
import { ErrorBoundary } from "./components/error-boundary";
|
import { ErrorBoundary } from "./components/error-boundary";
|
||||||
import { SettingsView } from "./views/settings";
|
import { SettingsView } from "./views/settings";
|
||||||
|
import { GlobalView } from "./views/global";
|
||||||
|
import { useRelays } from "./providers/relay-provider";
|
||||||
|
import { Page } from "./components/page";
|
||||||
|
|
||||||
export const App = () => {
|
export const App = () => {
|
||||||
|
const { loading } = useRelays();
|
||||||
|
|
||||||
|
if (loading) return <Spinner size="xl" />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ChakraProvider>
|
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<HashRouter>
|
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/user/:pubkey" element={<UserView />} />
|
<Route
|
||||||
<Route path="/settings" element={<SettingsView />} />
|
path="/user/:pubkey"
|
||||||
<Route path="/" element={<HomeView />} />
|
element={
|
||||||
|
<Page>
|
||||||
|
<UserView />
|
||||||
|
</Page>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/settings"
|
||||||
|
element={
|
||||||
|
<Page>
|
||||||
|
<SettingsView />
|
||||||
|
</Page>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/global"
|
||||||
|
element={
|
||||||
|
<Page>
|
||||||
|
<GlobalView />
|
||||||
|
</Page>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/"
|
||||||
|
element={
|
||||||
|
<Page>
|
||||||
|
<HomeView />
|
||||||
|
</Page>
|
||||||
|
}
|
||||||
|
/>
|
||||||
</Routes>
|
</Routes>
|
||||||
</HashRouter>
|
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</ChakraProvider>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
23
src/components/page.jsx
Normal file
23
src/components/page.jsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Box, Button, Container, HStack, VStack } from "@chakra-ui/react";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { ErrorBoundary } from "./error-boundary";
|
||||||
|
|
||||||
|
export const Page = ({ children }) => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container size="lg" padding={4}>
|
||||||
|
<HStack alignItems="flex-start" spacing={4} overflow="hidden">
|
||||||
|
<VStack style={{ width: "20rem" }} alignItems="stretch" flexShrink={0}>
|
||||||
|
<Button onClick={() => navigate("/")}>Home</Button>
|
||||||
|
<Button onClick={() => navigate("/global")}>Global</Button>
|
||||||
|
<Button onClick={() => navigate("/settings")}>Settings</Button>
|
||||||
|
</VStack>
|
||||||
|
<Box flexGrow={1} overflow="auto">
|
||||||
|
<ErrorBoundary>{children}</ErrorBoundary>
|
||||||
|
</Box>
|
||||||
|
</HStack>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
};
|
30
src/components/post-modal.jsx
Normal file
30
src/components/post-modal.jsx
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import React from "react";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Modal,
|
||||||
|
ModalBody,
|
||||||
|
ModalCloseButton,
|
||||||
|
ModalContent,
|
||||||
|
ModalFooter,
|
||||||
|
ModalHeader,
|
||||||
|
ModalOverlay,
|
||||||
|
} from "@chakra-ui/react";
|
||||||
|
import ReactMarkdown from "react-markdown";
|
||||||
|
|
||||||
|
export const PostModal = ({ event, onClose, isOpen }) => (
|
||||||
|
<Modal isOpen={isOpen} onClose={onClose} size="6xl">
|
||||||
|
<ModalOverlay />
|
||||||
|
<ModalContent>
|
||||||
|
<ModalHeader>{event.pubkey}</ModalHeader>
|
||||||
|
<ModalCloseButton />
|
||||||
|
<ModalBody>
|
||||||
|
<ReactMarkdown>{event.content}</ReactMarkdown>
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button colorScheme="blue" onClick={onClose}>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
42
src/components/post.jsx
Normal file
42
src/components/post.jsx
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import React from "react";
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
CardBody,
|
||||||
|
Heading,
|
||||||
|
useDisclosure,
|
||||||
|
VStack,
|
||||||
|
} from "@chakra-ui/react";
|
||||||
|
import ReactMarkdown from "react-markdown";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import { PostModal } from "./post-modal";
|
||||||
|
|
||||||
|
export const Post = ({ event }) => {
|
||||||
|
const { isOpen, onClose, onOpen } = useDisclosure();
|
||||||
|
|
||||||
|
const isLong = event.content.length > 800;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardBody>
|
||||||
|
<VStack alignItems="flex-start" justifyContent="stretch">
|
||||||
|
<Heading size="sm">
|
||||||
|
<Link to={`/user/${event.pubkey}`}>{event.pubkey}</Link>
|
||||||
|
</Heading>
|
||||||
|
<Box maxHeight="10rem" overflow="hidden" width="100%">
|
||||||
|
<ReactMarkdown>{event.content}</ReactMarkdown>
|
||||||
|
</Box>
|
||||||
|
{isLong && (
|
||||||
|
<>
|
||||||
|
<Button size="sm" variant="link" onClick={onOpen}>
|
||||||
|
Read More
|
||||||
|
</Button>
|
||||||
|
<PostModal event={event} isOpen={isOpen} onClose={onClose} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</VStack>
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
@@ -1,6 +1,11 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { createRoot } from "react-dom/client";
|
import { createRoot } from "react-dom/client";
|
||||||
import { App } from "./app";
|
import { App } from "./app";
|
||||||
|
import { Providers } from "./providers";
|
||||||
|
|
||||||
const root = createRoot(document.getElementById("root"));
|
const root = createRoot(document.getElementById("root"));
|
||||||
root.render(<App />);
|
root.render(
|
||||||
|
<Providers>
|
||||||
|
<App />
|
||||||
|
</Providers>
|
||||||
|
);
|
||||||
|
13
src/providers/index.jsx
Normal file
13
src/providers/index.jsx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { ChakraProvider } from "@chakra-ui/react";
|
||||||
|
import { RelaysProvider } from "./relay-provider";
|
||||||
|
import { HashRouter } from "react-router-dom";
|
||||||
|
import theme from "../theme";
|
||||||
|
|
||||||
|
export const Providers = ({ children }) => (
|
||||||
|
<ChakraProvider theme={theme}>
|
||||||
|
<HashRouter>
|
||||||
|
<RelaysProvider>{children}</RelaysProvider>
|
||||||
|
</HashRouter>
|
||||||
|
</ChakraProvider>
|
||||||
|
);
|
52
src/providers/relay-provider.jsx
Normal file
52
src/providers/relay-provider.jsx
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import React, {
|
||||||
|
createContext,
|
||||||
|
useCallback,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
|
import settingsService from "../services/settings";
|
||||||
|
|
||||||
|
const relaysContext = createContext({
|
||||||
|
relays: [],
|
||||||
|
loading: true,
|
||||||
|
overwriteRelays: () => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
export function useRelays() {
|
||||||
|
return useContext(relaysContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RelaysProvider = ({ children }) => {
|
||||||
|
const [relays, setRelays] = useState([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
const update = useCallback(async () => {
|
||||||
|
setRelays(await settingsService.getRelays());
|
||||||
|
setLoading(false);
|
||||||
|
}, [setRelays]);
|
||||||
|
|
||||||
|
const overwriteRelays = useCallback(
|
||||||
|
async (urls) => {
|
||||||
|
if (urls) await settingsService.setRelays(urls);
|
||||||
|
await update();
|
||||||
|
},
|
||||||
|
[update]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
update();
|
||||||
|
}, [update]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<relaysContext.Provider
|
||||||
|
value={{
|
||||||
|
relays,
|
||||||
|
loading,
|
||||||
|
overwriteRelays,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</relaysContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
0
src/providers/styles.jsx
Normal file
0
src/providers/styles.jsx
Normal file
17
src/theme/container.js
Normal file
17
src/theme/container.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { defineStyle, defineStyleConfig } from "@chakra-ui/react";
|
||||||
|
|
||||||
|
// define custom sizes
|
||||||
|
const sizes = {
|
||||||
|
sm: defineStyle({
|
||||||
|
maxW: "10rem",
|
||||||
|
}),
|
||||||
|
md: defineStyle({
|
||||||
|
maxW: "50rem",
|
||||||
|
}),
|
||||||
|
lg: defineStyle({
|
||||||
|
maxW: "100rem",
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
// export the component theme
|
||||||
|
export const containerTheme = defineStyleConfig({ sizes });
|
10
src/theme/index.js
Normal file
10
src/theme/index.js
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { extendTheme } from "@chakra-ui/react";
|
||||||
|
import { containerTheme } from "./container";
|
||||||
|
|
||||||
|
const theme = extendTheme({
|
||||||
|
components: {
|
||||||
|
Container: containerTheme,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default theme;
|
39
src/views/global/index.jsx
Normal file
39
src/views/global/index.jsx
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { SkeletonText } from "@chakra-ui/react";
|
||||||
|
import { useSubscription } from "../../helpers/use-subscription";
|
||||||
|
import { useSignal } from "../../hooks/use-signal";
|
||||||
|
import { useRelays } from "../../providers/relay-provider";
|
||||||
|
import { Post } from "../../components/post";
|
||||||
|
|
||||||
|
export const GlobalView = () => {
|
||||||
|
const { relays } = useRelays();
|
||||||
|
const [events, setEvents] = useState({});
|
||||||
|
|
||||||
|
const sub = useSubscription(relays, { kinds: [1], limit: 10 }, []);
|
||||||
|
|
||||||
|
useSignal(
|
||||||
|
sub?.onEvent,
|
||||||
|
(event) => {
|
||||||
|
if (event.kind === 1) {
|
||||||
|
setEvents((dir) => ({ [event.id]: event, ...dir }));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[setEvents]
|
||||||
|
);
|
||||||
|
|
||||||
|
const timeline = Object.values(events).sort(
|
||||||
|
(a, b) => a.created_at - b.created_at
|
||||||
|
);
|
||||||
|
|
||||||
|
if (timeline.length === 0) {
|
||||||
|
return <SkeletonText />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{timeline.map((event) => (
|
||||||
|
<Post key={event.id} event={event} />
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@@ -1,11 +1,9 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { VStack, Heading } from "@chakra-ui/react";
|
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
export const HomeView = () => {
|
export const HomeView = () => {
|
||||||
return (
|
return (
|
||||||
<VStack>
|
<>
|
||||||
<Heading>personal nostr client</Heading>
|
|
||||||
<span>
|
<span>
|
||||||
There are many benefits to a joint design and development system. Not
|
There are many benefits to a joint design and development system. Not
|
||||||
only does it bring benefits to the design team, but it also brings
|
only does it bring benefits to the design team, but it also brings
|
||||||
@@ -13,13 +11,12 @@ export const HomeView = () => {
|
|||||||
consistent look and feel, not just in our design specs, but in
|
consistent look and feel, not just in our design specs, but in
|
||||||
production.
|
production.
|
||||||
</span>
|
</span>
|
||||||
<Link to="/settings">Settings</Link>
|
|
||||||
<Link to="/user/266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5">
|
<Link to="/user/266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5">
|
||||||
self
|
self
|
||||||
</Link>
|
</Link>
|
||||||
<Link to="/user/6b0d4c8d9dc59e110d380b0429a02891f1341a0fa2ba1b1cf83a3db4d47e3964">
|
<Link to="/user/6b0d4c8d9dc59e110d380b0429a02891f1341a0fa2ba1b1cf83a3db4d47e3964">
|
||||||
gigi
|
gigi
|
||||||
</Link>
|
</Link>
|
||||||
</VStack>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@@ -6,30 +6,28 @@ import {
|
|||||||
Stack,
|
Stack,
|
||||||
Textarea,
|
Textarea,
|
||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useState } from "react";
|
||||||
import * as settings from "../services/settings";
|
import { useRelays } from "../providers/relay-provider";
|
||||||
|
|
||||||
export const SettingsView = () => {
|
export const SettingsView = () => {
|
||||||
const [relayUrls, setRelayUrls] = useState("");
|
const { relays, overwriteRelays } = useRelays();
|
||||||
|
const [relayUrls, setRelayUrls] = useState(relays.join("\n"));
|
||||||
|
|
||||||
const handleSubmit = (event) => {
|
const handleSubmit = async (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const relays = relayUrls
|
const newRelays = relayUrls
|
||||||
.split("\n")
|
.split("\n")
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.map((url) => url.trim());
|
.map((url) => url.trim());
|
||||||
if (relays.length > 0) {
|
|
||||||
settings.setRelays(relays);
|
if (newRelays.length > 0) {
|
||||||
|
await overwriteRelays(newRelays);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const resetForm = async () => {
|
|
||||||
const urls = await settings.getRelays();
|
|
||||||
setRelayUrls(urls.join("\n"));
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
const resetForm = async () => {
|
||||||
resetForm();
|
setRelayUrls(relays.join("\n"));
|
||||||
}, []);
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit}>
|
||||||
@@ -41,7 +39,7 @@ export const SettingsView = () => {
|
|||||||
required
|
required
|
||||||
size="md"
|
size="md"
|
||||||
rows={10}
|
rows={10}
|
||||||
resize="horizontal"
|
resize="vertical"
|
||||||
/>
|
/>
|
||||||
<FormHelperText>One relay per line</FormHelperText>
|
<FormHelperText>One relay per line</FormHelperText>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React from "react";
|
||||||
import {
|
import {
|
||||||
Heading,
|
Heading,
|
||||||
Tab,
|
Tab,
|
||||||
@@ -8,7 +8,6 @@ import {
|
|||||||
Tabs,
|
Tabs,
|
||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
import { ErrorBoundary } from "../../components/error-boundary";
|
|
||||||
import { UserPostsTab } from "./posts";
|
import { UserPostsTab } from "./posts";
|
||||||
|
|
||||||
export const UserView = () => {
|
export const UserView = () => {
|
||||||
@@ -16,15 +15,15 @@ export const UserView = () => {
|
|||||||
|
|
||||||
if (!params.pubkey) {
|
if (!params.pubkey) {
|
||||||
// TODO: better 404
|
// TODO: better 404
|
||||||
return "no pubkey";
|
throw new Error("No pubkey");
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ErrorBoundary>
|
<>
|
||||||
<Heading>user {params.pubkey}</Heading>
|
<Heading>{params.pubkey}</Heading>
|
||||||
<Tabs>
|
<Tabs>
|
||||||
<TabList>
|
<TabList>
|
||||||
<Tab>Notes</Tab>
|
<Tab>Posts</Tab>
|
||||||
<Tab>Other</Tab>
|
<Tab>Other</Tab>
|
||||||
<Tab>Relays</Tab>
|
<Tab>Relays</Tab>
|
||||||
</TabList>
|
</TabList>
|
||||||
@@ -41,6 +40,6 @@ export const UserView = () => {
|
|||||||
</TabPanel>
|
</TabPanel>
|
||||||
</TabPanels>
|
</TabPanels>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</ErrorBoundary>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@@ -1,9 +1,9 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { Card, CardBody, SkeletonText } from "@chakra-ui/react";
|
import { SkeletonText } from "@chakra-ui/react";
|
||||||
import ReactMarkdown from "react-markdown";
|
|
||||||
import settingsService from "../../services/settings";
|
import settingsService from "../../services/settings";
|
||||||
import { useSignal } from "../../hooks/use-signal";
|
import { useSignal } from "../../hooks/use-signal";
|
||||||
import { useSubscription } from "../../helpers/use-subscription";
|
import { useSubscription } from "../../helpers/use-subscription";
|
||||||
|
import { Post } from "../../components/post";
|
||||||
|
|
||||||
const relayUrls = await settingsService.getRelays();
|
const relayUrls = await settingsService.getRelays();
|
||||||
|
|
||||||
@@ -30,11 +30,5 @@ export const UserPostsTab = ({ pubkey }) => {
|
|||||||
return <SkeletonText />;
|
return <SkeletonText />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return timeline.map((event) => (
|
return timeline.map((event) => <Post key={event.id} event={event} />);
|
||||||
<Card key={event.id}>
|
|
||||||
<CardBody>
|
|
||||||
<ReactMarkdown>{event.content}</ReactMarkdown>
|
|
||||||
</CardBody>
|
|
||||||
</Card>
|
|
||||||
));
|
|
||||||
};
|
};
|
||||||
|
Reference in New Issue
Block a user