update widget list

This commit is contained in:
2023-11-11 09:14:23 +07:00
parent 0710996a0d
commit 1c3119577f
11 changed files with 316 additions and 563 deletions

View File

@@ -21,7 +21,7 @@ export function ArticleWidget({ widget }: { widget: Widget }) {
const { ndk, relayUrls, fetcher } = useNDK();
const { status, data, hasNextPage, isFetchingNextPage, fetchNextPage } =
useInfiniteQuery({
queryKey: ['widget-' + widget.id],
queryKey: ['widget-article'],
initialPageParam: 0,
queryFn: async ({
signal,

View File

@@ -21,7 +21,7 @@ export function FileWidget({ widget }: { widget: Widget }) {
const { ndk, relayUrls, fetcher } = useNDK();
const { status, data, hasNextPage, isFetchingNextPage, fetchNextPage } =
useInfiniteQuery({
queryKey: ['widget-' + widget.id],
queryKey: ['widget-media'],
initialPageParam: 0,
queryFn: async ({
signal,

View File

@@ -23,7 +23,7 @@ export function GroupWidget({ widget }: { widget: Widget }) {
const { relayUrls, ndk, fetcher } = useNDK();
const { status, data, hasNextPage, isFetchingNextPage, fetchNextPage } =
useInfiniteQuery({
queryKey: ['widget-' + widget.id],
queryKey: [`widget-${widget.id}`],
initialPageParam: 0,
queryFn: async ({
signal,

View File

@@ -18,7 +18,7 @@ export function HashtagWidget({ widget }: { widget: Widget }) {
const { ndk, relayUrls, fetcher } = useNDK();
const { status, data, hasNextPage, isFetchingNextPage, fetchNextPage } =
useInfiniteQuery({
queryKey: ['widget-' + widget.id],
queryKey: [`widget-${widget.content}`],
initialPageParam: 0,
queryFn: async ({
signal,

View File

@@ -13,3 +13,5 @@ export * from './other/wrapper';
export * from './other/liveUpdater';
export * from './other/toggleWidgetList';
export * from './other/widgetList';
export * from './other/addGroupFeeds';
export * from './other/addHashtagFeeds';

View File

@@ -0,0 +1,132 @@
import * as Dialog from '@radix-ui/react-dialog';
import { useState } from 'react';
import { useStorage } from '@libs/storage/provider';
import {
ArrowRightCircleIcon,
CancelIcon,
CheckCircleIcon,
GroupFeedsIcon,
PlusIcon,
} from '@shared/icons';
import { User } from '@shared/user';
import { WIDGET_KIND } from '@stores/constants';
import { useWidget } from '@utils/hooks/useWidget';
export function AddGroupFeeds({ currentWidgetId }: { currentWidgetId: string }) {
const { db } = useStorage();
const { replaceWidget } = useWidget();
const [title, setTitle] = useState<string>('');
const [users, setUsers] = useState<Array<string>>([]);
// toggle follow state
const toggleUser = (pubkey: string) => {
const arr = users.includes(pubkey)
? users.filter((i) => i !== pubkey)
: [...users, pubkey];
setUsers(arr);
};
const submit = async () => {
replaceWidget.mutate({
currentId: currentWidgetId,
widget: {
kind: WIDGET_KIND.group,
title: title || 'Group',
content: JSON.stringify(users),
},
});
};
return (
<Dialog.Root>
<div className="inline-flex h-14 w-full items-center justify-between rounded-lg bg-white px-3 hover:shadow-md hover:shadow-neutral-200/50 dark:hover:shadow-neutral-800/50">
<div className="inline-flex items-center gap-2.5">
<div className="inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-md bg-neutral-100">
<GroupFeedsIcon className="h-4 w-4" />
</div>
<p className="font-medium">Group feeds</p>
</div>
<Dialog.Trigger asChild>
<button
type="button"
className="inline-flex h-6 items-center gap-1 rounded-md bg-neutral-100 pl-1.5 pr-2.5 text-sm font-medium hover:bg-blue-500 hover:text-white dark:bg-neutral-900"
>
<PlusIcon className="h-3 w-3" />
Add
</button>
</Dialog.Trigger>
</div>
<Dialog.Portal>
<Dialog.Overlay className="fixed inset-0 z-50 bg-black/20 backdrop-blur-sm dark:bg-black/20" />
<Dialog.Content className="fixed inset-0 z-50 flex min-h-full items-center justify-center">
<div className="relative h-min w-full max-w-xl rounded-xl bg-neutral-50 dark:bg-neutral-950">
<div className="w-full shrink-0 rounded-t-xl border-b border-neutral-100 px-3 py-5 dark:border-neutral-900">
<div className="flex items-center justify-between">
<Dialog.Title className="text-lg font-semibold leading-none text-neutral-900 dark:text-neutral-100">
Adding group feeds
</Dialog.Title>
<Dialog.Close className="inline-flex h-6 w-6 items-center justify-center rounded-md text-neutral-900 hover:bg-neutral-200 dark:text-neutral-100 dark:hover:bg-neutral-800">
<CancelIcon className="h-4 w-4" />
</Dialog.Close>
</div>
</div>
<div className="flex w-full flex-col gap-3 px-3">
<div className="flex flex-col gap-1 pt-2">
<label
htmlFor="name"
className="font-medium text-neutral-700 dark:text-neutral-300"
>
Name
</label>
<input
name="name"
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="Nostrichs..."
className="relative h-11 w-full rounded-lg bg-neutral-100 px-3 py-1 text-neutral-900 !outline-none placeholder:text-neutral-500 dark:bg-neutral-900 dark:text-neutral-100 dark:placeholder:text-neutral-300"
/>
</div>
<div className="flex flex-col gap-1">
<span className="font-medium text-neutral-700 dark:text-neutral-300">
Users
</span>
<div className="flex h-[420px] flex-col overflow-y-auto rounded-xl bg-neutral-100 py-2 dark:bg-neutral-900">
{db.account.circles.map((item: string) => (
<button
key={item}
type="button"
onClick={() => toggleUser(item)}
className="inline-flex transform items-center justify-between px-3 py-2 hover:bg-neutral-200 dark:hover:bg-neutral-800"
>
<User pubkey={item} variant="simple" />
{users.includes(item) ? (
<CheckCircleIcon className="h-5 w-5 text-teal-500" />
) : null}
</button>
))}
</div>
</div>
</div>
<div className="mt-2 px-3 py-3">
<button
type="submit"
disabled={users.length < 1}
onClick={submit}
className="inline-flex h-9 w-full items-center justify-between gap-2 rounded-lg bg-blue-500 px-6 font-medium text-white hover:bg-blue-600 focus:outline-none disabled:opacity-50"
>
<span className="w-5" />
<span>Add {users.length} user to group feeds</span>
<ArrowRightCircleIcon className="h-5 w-5" />
</button>
</div>
</div>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
);
}

View File

@@ -0,0 +1,136 @@
import * as Dialog from '@radix-ui/react-dialog';
import { Resolver, useForm } from 'react-hook-form';
import { CancelIcon, GroupFeedsIcon, PlusIcon } from '@shared/icons';
import { HASHTAGS, WIDGET_KIND } from '@stores/constants';
import { useWidget } from '@utils/hooks/useWidget';
type FormValues = {
hashtag: string;
};
const resolver: Resolver<FormValues> = async (values) => {
return {
values: values.hashtag ? values : {},
errors: !values.hashtag
? {
hashtag: {
type: 'required',
message: 'This is required.',
},
}
: {},
};
};
export function AddHashtagFeeds({ currentWidgetId }: { currentWidgetId: string }) {
const { replaceWidget } = useWidget();
const {
register,
setValue,
setError,
handleSubmit,
formState: { errors },
} = useForm<FormValues>({ resolver });
const onSubmit = async (data: FormValues) => {
try {
replaceWidget.mutate({
currentId: currentWidgetId,
widget: {
kind: WIDGET_KIND.hashtag,
title: data.hashtag,
content: data.hashtag.replace('#', ''),
},
});
} catch (e) {
setError('hashtag', {
type: 'custom',
message: e,
});
}
};
return (
<Dialog.Root>
<div className="inline-flex h-14 w-full items-center justify-between rounded-lg bg-white px-3 hover:shadow-md hover:shadow-neutral-200/50 dark:hover:shadow-neutral-800/50">
<div className="inline-flex items-center gap-2.5">
<div className="inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-md bg-neutral-100">
<GroupFeedsIcon className="h-4 w-4" />
</div>
<p className="font-medium">Hashtag</p>
</div>
<Dialog.Trigger asChild>
<button
type="button"
className="inline-flex h-6 items-center gap-1 rounded-md bg-neutral-100 pl-1.5 pr-2.5 text-sm font-medium hover:bg-blue-500 hover:text-white dark:bg-neutral-900"
>
<PlusIcon className="h-3 w-3" />
Add
</button>
</Dialog.Trigger>
</div>
<Dialog.Portal>
<Dialog.Overlay className="fixed inset-0 z-50 bg-black/20 backdrop-blur-sm dark:bg-black/20" />
<Dialog.Content className="fixed inset-0 z-50 flex min-h-full items-center justify-center">
<div className="relative h-min w-full max-w-xl rounded-xl bg-neutral-50 dark:bg-neutral-950">
<div className="w-full shrink-0 rounded-t-xl border-b border-neutral-100 px-3 py-5 dark:border-neutral-900">
<div className="flex items-center justify-between">
<Dialog.Title className="text-lg font-semibold leading-none text-neutral-900 dark:text-neutral-100">
Adding hashtag feeds
</Dialog.Title>
<Dialog.Close className="inline-flex h-6 w-6 items-center justify-center rounded-md text-neutral-900 hover:bg-neutral-200 dark:text-neutral-100 dark:hover:bg-neutral-800">
<CancelIcon className="h-4 w-4" />
</Dialog.Close>
</div>
</div>
<div className="flex w-full flex-col gap-3 px-3 py-3">
<form
onSubmit={handleSubmit(onSubmit)}
className="mb-0 flex flex-col gap-2"
>
<div className="flex flex-col gap-1">
<input
{...register('hashtag', { required: true, minLength: 1 })}
placeholder="Enter a hashtag"
className="relative h-11 w-full rounded-lg bg-neutral-100 px-3 py-1 text-neutral-900 !outline-none placeholder:text-neutral-500 dark:bg-neutral-900 dark:text-neutral-100 dark:placeholder:text-neutral-300"
/>
<span className="text-sm text-red-400">
{errors.hashtag && <p>{errors.hashtag.message}</p>}
</span>
</div>
<div className="flex flex-col gap-2">
<span className="text-sm font-medium text-neutral-700 dark:text-neutral-300">
Suggestions:
</span>
<div className="flex flex-wrap items-center justify-start gap-2">
{HASHTAGS.map((item) => (
<button
key={item.hashtag}
type="button"
onClick={() => setValue('hashtag', item.hashtag)}
className="inline-flex h-6 w-min items-center justify-center rounded-md bg-neutral-100 px-2 text-sm hover:bg-neutral-200 dark:bg-neutral-900 dark:hover:bg-neutral-800"
>
{item.hashtag}
</button>
))}
</div>
</div>
<div className="mt-2 flex flex-col items-center justify-center gap-2">
<button
type="submit"
className="inline-flex h-9 w-full items-center justify-center rounded-lg bg-blue-500 font-medium text-white hover:bg-blue-600 focus:outline-none disabled:opacity-50"
>
Add
</button>
</div>
</form>
</div>
</div>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
);
}

View File

@@ -1,5 +1,4 @@
import { NDKEvent, NDKKind, NDKUser } from '@nostr-dev-kit/ndk';
import { useQuery } from '@tanstack/react-query';
import { useEffect, useState } from 'react';
import { toast } from 'sonner';
@@ -8,7 +7,6 @@ import { useStorage } from '@libs/storage/provider';
import { FollowIcon, UnfollowIcon } from '@shared/icons';
import { compactNumber } from '@utils/number';
import { shortenKey } from '@utils/shortenKey';
export interface Profile {
@@ -17,23 +15,12 @@ export interface Profile {
}
export function NostrBandUserProfile({ data }: { data: Profile }) {
const { db } = useStorage();
const { ndk } = useNDK();
const { status, data: userStats } = useQuery({
queryKey: ['user-stats', data.pubkey],
queryFn: async () => {
const res = await fetch(`https://api.nostr.band/v0/stats/profile/${data.pubkey}`);
return res.json();
},
refetchOnMount: false,
refetchOnReconnect: false,
refetchOnWindowFocus: false,
staleTime: Infinity,
});
const embedProfile = data.profile ? JSON.parse(data.profile.content) : null;
const profile = embedProfile;
const { db } = useStorage();
const { ndk } = useNDK();
const [followed, setFollowed] = useState(false);
const follow = async (pubkey: string) => {
@@ -90,8 +77,8 @@ export function NostrBandUserProfile({ data }: { data: Profile }) {
}
return (
<div className="h-min w-full px-3 pb-3">
<div className="rounded-xl bg-neutral-100 px-5 py-5 dark:bg-neutral-900">
<div className="mb-3 h-min w-full px-3">
<div className="rounded-xl bg-neutral-50 px-5 py-5 dark:bg-neutral-950">
<div className="flex items-center justify-between">
<div className="inline-flex items-center gap-2">
<img
@@ -128,45 +115,9 @@ export function NostrBandUserProfile({ data }: { data: Profile }) {
)}
</div>
</div>
<div className="mt-2 whitespace-pre-line break-words text-neutral-900 dark:text-neutral-100">
<div className="mt-2 line-clamp-5 whitespace-pre-line break-all text-neutral-900 dark:text-neutral-100">
{profile.about || profile.bio}
</div>
<div className="mt-5">
{status === 'pending' ? (
<p>Loading...</p>
) : (
<div className="flex w-full items-center gap-8">
<div className="inline-flex flex-col gap-1">
<span className="font-semibold leading-none text-neutral-900 dark:text-neutral-100">
{userStats.stats[data.pubkey].followers_pubkey_count ?? 0}
</span>
<span className="text-sm leading-none text-neutral-900 dark:text-neutral-100/50">
Followers
</span>
</div>
<div className="inline-flex flex-col gap-1">
<span className="font-semibold leading-none text-neutral-900 dark:text-neutral-100">
{userStats.stats[data.pubkey].pub_following_pubkey_count ?? 0}
</span>
<span className="text-sm leading-none text-neutral-900 dark:text-neutral-100/50">
Following
</span>
</div>
<div className="inline-flex flex-col gap-1">
<span className="font-semibold leading-none text-neutral-900 dark:text-neutral-100">
{userStats.stats[data.pubkey].zaps_received
? compactNumber.format(
userStats.stats[data.pubkey].zaps_received.msats / 1000
)
: 0}
</span>
<span className="text-sm leading-none text-neutral-900 dark:text-neutral-100/50">
Zaps received
</span>
</div>
</div>
)}
</div>
</div>
</div>
);

View File

@@ -1,14 +1,6 @@
import {
ArticleIcon,
BellIcon,
GroupFeedsIcon,
HashtagIcon,
MediaIcon,
PlusIcon,
TrendingIcon,
} from '@shared/icons';
import { ArticleIcon, BellIcon, MediaIcon, PlusIcon } from '@shared/icons';
import { TitleBar } from '@shared/titleBar';
import { WidgetWrapper } from '@shared/widgets';
import { AddGroupFeeds, AddHashtagFeeds, WidgetWrapper } from '@shared/widgets';
import { TOPICS, WIDGET_KIND } from '@stores/constants';
@@ -32,7 +24,7 @@ export function WidgetList({ widget }: { widget: Widget }) {
(topic, index) => (
<div
key={index}
className="inline-flex h-14 w-full items-center justify-between rounded-lg bg-white px-3 hover:shadow-md hover:shadow-neutral-200/50 dark:hover:shadow-neutral-800/50"
className="inline-flex h-14 w-full items-center justify-between rounded-lg bg-neutral-50 px-3 ring-1 ring-transparent hover:ring-neutral-200 dark:bg-neutral-950 dark:hover:ring-neutral-800"
>
<div className="inline-flex items-center gap-2.5">
<div className="h-9 w-9 shrink-0 rounded-md">
@@ -71,15 +63,27 @@ export function WidgetList({ widget }: { widget: Widget }) {
Newsfeed
</h3>
<div className="flex flex-col gap-3">
<AddGroupFeeds currentWidgetId={widget.id} />
<AddHashtagFeeds currentWidgetId={widget.id} />
<div className="inline-flex h-14 w-full items-center justify-between rounded-lg bg-white px-3 hover:shadow-md hover:shadow-neutral-200/50 dark:hover:shadow-neutral-800/50">
<div className="inline-flex items-center gap-2.5">
<div className="inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-md bg-neutral-100">
<ArticleIcon className="h-4 w-4" />
</div>
<p className="font-medium">Article</p>
<p className="font-medium">Articles</p>
</div>
<button
type="button"
onClick={() =>
replaceWidget.mutate({
currentId: widget.id,
widget: {
kind: WIDGET_KIND.article,
title: 'Articles',
content: JSON.stringify({ global: true }),
},
})
}
className="inline-flex h-6 items-center gap-1 rounded-md bg-neutral-100 pl-1.5 pr-2.5 text-sm font-medium hover:bg-blue-500 hover:text-white dark:bg-neutral-900"
>
<PlusIcon className="h-3 w-3" />
@@ -95,73 +99,16 @@ export function WidgetList({ widget }: { widget: Widget }) {
</div>
<button
type="button"
className="inline-flex h-6 items-center gap-1 rounded-md bg-neutral-100 pl-1.5 pr-2.5 text-sm font-medium hover:bg-blue-500 hover:text-white dark:bg-neutral-900"
>
<PlusIcon className="h-3 w-3" />
Add
</button>
</div>
<div className="inline-flex h-14 w-full items-center justify-between rounded-lg bg-white px-3 hover:shadow-md hover:shadow-neutral-200/50 dark:hover:shadow-neutral-800/50">
<div className="inline-flex items-center gap-2.5">
<div className="inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-md bg-neutral-100">
<GroupFeedsIcon className="h-4 w-4" />
</div>
<p className="font-medium">Group feeds</p>
</div>
<button
type="button"
className="inline-flex h-6 items-center gap-1 rounded-md bg-neutral-100 pl-1.5 pr-2.5 text-sm font-medium hover:bg-blue-500 hover:text-white dark:bg-neutral-900"
>
<PlusIcon className="h-3 w-3" />
Add
</button>
</div>
<div className="inline-flex h-14 w-full items-center justify-between rounded-lg bg-white px-3 hover:shadow-md hover:shadow-neutral-200/50 dark:hover:shadow-neutral-800/50">
<div className="inline-flex items-center gap-2.5">
<div className="inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-md bg-neutral-100">
<HashtagIcon className="h-4 w-4" />
</div>
<p className="font-medium">Hashtag</p>
</div>
<button
type="button"
className="inline-flex h-6 items-center gap-1 rounded-md bg-neutral-100 pl-1.5 pr-2.5 text-sm font-medium hover:bg-blue-500 hover:text-white dark:bg-neutral-900"
>
<PlusIcon className="h-3 w-3" />
Add
</button>
</div>
</div>
</div>
<div className="rounded-xl bg-neutral-100 p-3 dark:bg-neutral-900">
<h3 className="mb-2.5 text-sm font-semibold uppercase text-neutral-700 dark:text-neutral-300">
Nostr Band
</h3>
<div className="flex flex-col gap-3">
<div className="inline-flex h-14 w-full items-center justify-between rounded-lg bg-white px-3 hover:shadow-md hover:shadow-neutral-200/50 dark:hover:shadow-neutral-800/50">
<div className="inline-flex items-center gap-2.5">
<div className="inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-md bg-neutral-100">
<TrendingIcon className="h-4 w-4" />
</div>
<p className="font-medium">Trending posts</p>
</div>
<button
type="button"
className="inline-flex h-6 items-center gap-1 rounded-md bg-neutral-100 pl-1.5 pr-2.5 text-sm font-medium hover:bg-blue-500 hover:text-white dark:bg-neutral-900"
>
<PlusIcon className="h-3 w-3" />
Add
</button>
</div>
<div className="inline-flex h-14 w-full items-center justify-between rounded-lg bg-white px-3 hover:shadow-md hover:shadow-neutral-200/50 dark:hover:shadow-neutral-800/50">
<div className="inline-flex items-center gap-2.5">
<div className="inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-md bg-neutral-100">
<TrendingIcon className="h-4 w-4" />
</div>
<p className="font-medium">Trending users</p>
</div>
<button
type="button"
onClick={() =>
replaceWidget.mutate({
currentId: widget.id,
widget: {
kind: WIDGET_KIND.file,
title: 'Media',
content: JSON.stringify({ global: true }),
},
})
}
className="inline-flex h-6 items-center gap-1 rounded-md bg-neutral-100 pl-1.5 pr-2.5 text-sm font-medium hover:bg-blue-500 hover:text-white dark:bg-neutral-900"
>
<PlusIcon className="h-3 w-3" />
@@ -184,6 +131,16 @@ export function WidgetList({ widget }: { widget: Widget }) {
</div>
<button
type="button"
onClick={() =>
replaceWidget.mutate({
currentId: widget.id,
widget: {
kind: WIDGET_KIND.notification,
title: 'Notification',
content: '',
},
})
}
className="inline-flex h-6 items-center gap-1 rounded-md bg-neutral-100 pl-1.5 pr-2.5 text-sm font-medium hover:bg-blue-500 hover:text-white dark:bg-neutral-900"
>
<PlusIcon className="h-3 w-3" />