feat: latest articles widget
This commit is contained in:
parent
026aaed5a1
commit
8c2833684b
@ -12,7 +12,12 @@ interface IconButtonProps {
|
|||||||
|
|
||||||
const IconButton = ({ onClick, icon, children, className }: IconButtonProps) => {
|
const IconButton = ({ onClick, icon, children, className }: IconButtonProps) => {
|
||||||
return (
|
return (
|
||||||
<button className={classNames("icon", className)} type="button" onClick={onClick}>
|
<button
|
||||||
|
className={classNames(
|
||||||
|
"flex items-center justify-center aspect-square w-10 h-10 !p-0 !m-0 bg-gray-dark text-white",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
onClick={onClick}>
|
||||||
<Icon {...icon} />
|
<Icon {...icon} />
|
||||||
{children}
|
{children}
|
||||||
</button>
|
</button>
|
||||||
|
68
packages/app/src/Components/RightWidgets/articles.tsx
Normal file
68
packages/app/src/Components/RightWidgets/articles.tsx
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import { NostrLink } from "@snort/system";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
|
import { useArticles } from "@/Feed/ArticlesFeed";
|
||||||
|
import { findTag } from "@/Utils";
|
||||||
|
|
||||||
|
import IconButton from "../Button/IconButton";
|
||||||
|
import { ProxyImg } from "../ProxyImg";
|
||||||
|
import ProfilePreview from "../User/ProfilePreview";
|
||||||
|
import { BaseWidget } from "./base";
|
||||||
|
|
||||||
|
export default function LatestArticlesWidget() {
|
||||||
|
const [idx, setIdx] = useState(0);
|
||||||
|
const articles = useArticles();
|
||||||
|
const selected = articles.at(idx);
|
||||||
|
|
||||||
|
function next(i: number) {
|
||||||
|
setIdx(x => {
|
||||||
|
x += i;
|
||||||
|
if (x < 0) {
|
||||||
|
x = articles.length - 1;
|
||||||
|
} else if (x > articles.length) {
|
||||||
|
x = 0;
|
||||||
|
}
|
||||||
|
return x;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!selected) return;
|
||||||
|
const link = NostrLink.fromEvent(selected);
|
||||||
|
const image = findTag(selected, "image");
|
||||||
|
const title = findTag(selected, "title");
|
||||||
|
return (
|
||||||
|
<BaseWidget title={<FormattedMessage defaultMessage="Latest Articles" />}>
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<Link
|
||||||
|
to={`/${link.encode()}`}
|
||||||
|
className="relative rounded-xl overflow-hidden w-full aspect-video"
|
||||||
|
state={selected}>
|
||||||
|
{image ? (
|
||||||
|
<ProxyImg src={image} className="absolute w-full h-full object-fit" />
|
||||||
|
) : (
|
||||||
|
<div className="absolute w-full h-full object-fit bg-gray-dark"></div>
|
||||||
|
)}
|
||||||
|
<div className="absolute bottom-2 left-4 right-4 px-2 py-1 rounded-xl text-lg font-bold text-white bg-black/50">
|
||||||
|
{title}
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
<div>
|
||||||
|
<ProfilePreview
|
||||||
|
pubkey={selected.pubkey}
|
||||||
|
profileImageProps={{
|
||||||
|
subHeader: <></>,
|
||||||
|
}}
|
||||||
|
actions={
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<IconButton icon={{ name: "arrowFront", className: "rotate-180", size: 14 }} onClick={() => next(-1)} />
|
||||||
|
<IconButton icon={{ name: "arrowFront", size: 14 }} onClick={() => next(1)} />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</BaseWidget>
|
||||||
|
);
|
||||||
|
}
|
@ -11,16 +11,16 @@ export interface BaseWidgetProps {
|
|||||||
}
|
}
|
||||||
export function BaseWidget({ children, title, icon, iconClassName, contextMenu }: BaseWidgetProps) {
|
export function BaseWidget({ children, title, icon, iconClassName, contextMenu }: BaseWidgetProps) {
|
||||||
return (
|
return (
|
||||||
<div className="br p bg-gray-ultradark">
|
<div className="b br p bg-gray-ultradark">
|
||||||
{title && (
|
{title && (
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<div className="flex gap-2 items-center text-xl text-white font-semibold mb-1">
|
<div className="flex gap-2 items-center text-xl text-white font-semibold mb-2">
|
||||||
{icon && (
|
{icon && (
|
||||||
<div className="p-2 bg-gray-dark rounded-full">
|
<div className="p-2 bg-gray-dark rounded-full">
|
||||||
<Icon name={icon} className={iconClassName} />
|
<Icon name={icon} className={iconClassName} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div>{title}</div>
|
<div className="text-font-color">{title}</div>
|
||||||
</div>
|
</div>
|
||||||
{contextMenu}
|
{contextMenu}
|
||||||
</div>
|
</div>
|
||||||
|
@ -3,7 +3,7 @@ export enum RightColumnWidget {
|
|||||||
TrendingNotes,
|
TrendingNotes,
|
||||||
TrendingPeople,
|
TrendingPeople,
|
||||||
TrendingHashtags,
|
TrendingHashtags,
|
||||||
TrendingArticls,
|
LatestArticls,
|
||||||
LiveStreams,
|
LiveStreams,
|
||||||
InviteFriends,
|
InviteFriends,
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,10 @@
|
|||||||
import { useContext } from "react";
|
import { useContext } from "react";
|
||||||
|
|
||||||
|
import Note, { NoteProps } from "@/Components/Event/EventComponent";
|
||||||
import { useArticles } from "@/Feed/ArticlesFeed";
|
import { useArticles } from "@/Feed/ArticlesFeed";
|
||||||
import { DeckContext } from "@/Pages/Deck/DeckLayout";
|
import { DeckContext } from "@/Pages/Deck/DeckLayout";
|
||||||
|
|
||||||
import Note from "../../Components/Event/EventComponent";
|
export default function Articles({ noteProps }: { noteProps?: Omit<NoteProps, "data"> }) {
|
||||||
|
|
||||||
const options = {
|
|
||||||
longFormPreview: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function Articles() {
|
|
||||||
const data = useArticles();
|
const data = useArticles();
|
||||||
const deck = useContext(DeckContext);
|
const deck = useContext(DeckContext);
|
||||||
|
|
||||||
@ -19,9 +14,14 @@ export default function Articles() {
|
|||||||
<Note
|
<Note
|
||||||
data={a}
|
data={a}
|
||||||
key={a.id}
|
key={a.id}
|
||||||
options={options}
|
{...noteProps}
|
||||||
|
options={{
|
||||||
|
longFormPreview: true,
|
||||||
|
...noteProps?.options,
|
||||||
|
}}
|
||||||
onClick={ev => {
|
onClick={ev => {
|
||||||
deck?.setArticle(ev);
|
deck?.setArticle(ev);
|
||||||
|
noteProps?.onClick?.(ev);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
@ -2,6 +2,7 @@ import classNames from "classnames";
|
|||||||
import { FormattedMessage } from "react-intl";
|
import { FormattedMessage } from "react-intl";
|
||||||
|
|
||||||
import { RightColumnWidget } from "@/Components/RightWidgets";
|
import { RightColumnWidget } from "@/Components/RightWidgets";
|
||||||
|
import LatestArticlesWidget from "@/Components/RightWidgets/articles";
|
||||||
import { BaseWidget } from "@/Components/RightWidgets/base";
|
import { BaseWidget } from "@/Components/RightWidgets/base";
|
||||||
import InviteFriendsWidget from "@/Components/RightWidgets/invite-friends";
|
import InviteFriendsWidget from "@/Components/RightWidgets/invite-friends";
|
||||||
import MiniStreamWidget from "@/Components/RightWidgets/mini-stream";
|
import MiniStreamWidget from "@/Components/RightWidgets/mini-stream";
|
||||||
@ -23,6 +24,7 @@ export default function RightColumn() {
|
|||||||
RightColumnWidget.InviteFriends,
|
RightColumnWidget.InviteFriends,
|
||||||
//RightColumnWidget.LiveStreams,
|
//RightColumnWidget.LiveStreams,
|
||||||
RightColumnWidget.TrendingNotes,
|
RightColumnWidget.TrendingNotes,
|
||||||
|
RightColumnWidget.LatestArticls,
|
||||||
RightColumnWidget.TrendingPeople,
|
RightColumnWidget.TrendingPeople,
|
||||||
RightColumnWidget.TrendingHashtags,
|
RightColumnWidget.TrendingHashtags,
|
||||||
]
|
]
|
||||||
@ -65,6 +67,8 @@ export default function RightColumn() {
|
|||||||
return <InviteFriendsWidget />;
|
return <InviteFriendsWidget />;
|
||||||
case RightColumnWidget.LiveStreams:
|
case RightColumnWidget.LiveStreams:
|
||||||
return <MiniStreamWidget />;
|
return <MiniStreamWidget />;
|
||||||
|
case RightColumnWidget.LatestArticls:
|
||||||
|
return <LatestArticlesWidget />;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -175,6 +175,10 @@ a.ext {
|
|||||||
-webkit-text-fill-color: transparent;
|
-webkit-text-fill-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.text-outline {
|
||||||
|
-webkit-text-stroke: 1px black;
|
||||||
|
}
|
||||||
|
|
||||||
.br {
|
.br {
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
}
|
}
|
||||||
@ -941,6 +945,7 @@ svg.repeat {
|
|||||||
.pb-safe-area-plus-footer {
|
.pb-safe-area-plus-footer {
|
||||||
padding-bottom: calc(env(safe-area-inset-bottom) + 56px);
|
padding-bottom: calc(env(safe-area-inset-bottom) + 56px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.sm-hide-scrollbar {
|
.sm-hide-scrollbar {
|
||||||
scrollbar-width: none;
|
scrollbar-width: none;
|
||||||
-ms-overflow-style: none;
|
-ms-overflow-style: none;
|
||||||
|
@ -183,6 +183,9 @@
|
|||||||
"2ukA4d": {
|
"2ukA4d": {
|
||||||
"defaultMessage": "{n} hours"
|
"defaultMessage": "{n} hours"
|
||||||
},
|
},
|
||||||
|
"2z7Kky": {
|
||||||
|
"defaultMessage": "Latest Articles"
|
||||||
|
},
|
||||||
"3/onCd": {
|
"3/onCd": {
|
||||||
"defaultMessage": "Replies"
|
"defaultMessage": "Replies"
|
||||||
},
|
},
|
||||||
|
@ -60,6 +60,7 @@
|
|||||||
"2oCF7O": "Followed by friends of friends",
|
"2oCF7O": "Followed by friends of friends",
|
||||||
"2raFAu": "Application-specific data",
|
"2raFAu": "Application-specific data",
|
||||||
"2ukA4d": "{n} hours",
|
"2ukA4d": "{n} hours",
|
||||||
|
"2z7Kky": "Latest Articles",
|
||||||
"3/onCd": "Replies",
|
"3/onCd": "Replies",
|
||||||
"39AHJm": "Sign Up",
|
"39AHJm": "Sign Up",
|
||||||
"3GWu6/": "User Statuses",
|
"3GWu6/": "User Statuses",
|
||||||
|
@ -28,6 +28,7 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
textColor: {
|
textColor: {
|
||||||
secondary: "var(--font-secondary-color)",
|
secondary: "var(--font-secondary-color)",
|
||||||
|
"font-color": "var(--font-color)",
|
||||||
},
|
},
|
||||||
spacing: {
|
spacing: {
|
||||||
px: "1px",
|
px: "1px",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user