Browse Source

style: 🎨

imwald
codytseng 11 months ago
parent
commit
51913a5163
  1. 156
      src/components/BookmarkButton/index.tsx
  2. 194
      src/components/BookmarkList/index.tsx
  3. 136
      src/components/NewNotesButton/index.tsx
  4. 2
      src/components/NoteStats/Likes.tsx
  5. 130
      src/providers/BookmarksProvider.tsx
  6. 2
      tailwind.config.js

156
src/components/BookmarkButton/index.tsx

@ -1,78 +1,78 @@
import { useToast } from '@/hooks' import { useToast } from '@/hooks'
import { useBookmarks } from '@/providers/BookmarksProvider' import { useBookmarks } from '@/providers/BookmarksProvider'
import { useNostr } from '@/providers/NostrProvider' import { useNostr } from '@/providers/NostrProvider'
import { BookmarkIcon, Loader } from 'lucide-react' import { BookmarkIcon, Loader } from 'lucide-react'
import { useMemo, useState } from 'react' import { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Event } from 'nostr-tools' import { Event } from 'nostr-tools'
export default function BookmarkButton({ event }: { event: Event }) { export default function BookmarkButton({ event }: { event: Event }) {
const { t } = useTranslation() const { t } = useTranslation()
const { toast } = useToast() const { toast } = useToast()
const { pubkey: accountPubkey, bookmarkListEvent, checkLogin } = useNostr() const { pubkey: accountPubkey, bookmarkListEvent, checkLogin } = useNostr()
const { addBookmark, removeBookmark } = useBookmarks() const { addBookmark, removeBookmark } = useBookmarks()
const [updating, setUpdating] = useState(false) const [updating, setUpdating] = useState(false)
const isBookmarked = useMemo( const isBookmarked = useMemo(
() => bookmarkListEvent?.tags.some((tag) => tag[0] === 'e' && tag[1] === event.id), () => bookmarkListEvent?.tags.some((tag) => tag[0] === 'e' && tag[1] === event.id),
[bookmarkListEvent, event] [bookmarkListEvent, event]
) )
if (!accountPubkey) return null if (!accountPubkey) return null
const handleBookmark = async (e: React.MouseEvent) => { const handleBookmark = async (e: React.MouseEvent) => {
e.stopPropagation() e.stopPropagation()
checkLogin(async () => { checkLogin(async () => {
if (isBookmarked) return if (isBookmarked) return
setUpdating(true) setUpdating(true)
try { try {
await addBookmark(event) await addBookmark(event)
} catch (error) { } catch (error) {
toast({ toast({
title: t('Bookmark failed'), title: t('Bookmark failed'),
description: (error as Error).message, description: (error as Error).message,
variant: 'destructive' variant: 'destructive'
}) })
} finally { } finally {
setUpdating(false) setUpdating(false)
} }
}) })
} }
const handleRemoveBookmark = async (e: React.MouseEvent) => { const handleRemoveBookmark = async (e: React.MouseEvent) => {
e.stopPropagation() e.stopPropagation()
checkLogin(async () => { checkLogin(async () => {
if (!isBookmarked) return if (!isBookmarked) return
setUpdating(true) setUpdating(true)
try { try {
await removeBookmark(event) await removeBookmark(event)
} catch (error) { } catch (error) {
toast({ toast({
title: t('Remove bookmark failed'), title: t('Remove bookmark failed'),
description: (error as Error).message, description: (error as Error).message,
variant: 'destructive' variant: 'destructive'
}) })
} finally { } finally {
setUpdating(false) setUpdating(false)
} }
}) })
} }
return ( return (
<button <button
className={`flex items-center gap-1 ${ className={`flex items-center gap-1 ${
isBookmarked ? 'text-rose-400' : 'text-muted-foreground' isBookmarked ? 'text-rose-400' : 'text-muted-foreground'
} enabled:hover:text-rose-400 px-3 h-full`} } enabled:hover:text-rose-400 px-3 h-full`}
onClick={isBookmarked ? handleRemoveBookmark : handleBookmark} onClick={isBookmarked ? handleRemoveBookmark : handleBookmark}
disabled={updating} disabled={updating}
title={isBookmarked ? t('Remove bookmark') : t('Bookmark')} title={isBookmarked ? t('Remove bookmark') : t('Bookmark')}
> >
{updating ? ( {updating ? (
<Loader className="animate-spin" /> <Loader className="animate-spin" />
) : ( ) : (
<BookmarkIcon className={isBookmarked ? 'fill-rose-400' : ''} /> <BookmarkIcon className={isBookmarked ? 'fill-rose-400' : ''} />
)} )}
</button> </button>
) )
} }

194
src/components/BookmarkList/index.tsx

@ -1,97 +1,97 @@
import { useFetchEvent } from '@/hooks' import { useFetchEvent } from '@/hooks'
import { generateEventIdFromETag } from '@/lib/tag' import { generateEventIdFromETag } from '@/lib/tag'
import { useNostr } from '@/providers/NostrProvider' import { useNostr } from '@/providers/NostrProvider'
import { kinds } from 'nostr-tools' import { kinds } from 'nostr-tools'
import { useEffect, useMemo, useRef, useState } from 'react' import { useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import NoteCard, { NoteCardLoadingSkeleton } from '../NoteCard' import NoteCard, { NoteCardLoadingSkeleton } from '../NoteCard'
const SHOW_COUNT = 10 const SHOW_COUNT = 10
export default function BookmarkList() { export default function BookmarkList() {
const { t } = useTranslation() const { t } = useTranslation()
const { bookmarkListEvent } = useNostr() const { bookmarkListEvent } = useNostr()
const eventIds = useMemo(() => { const eventIds = useMemo(() => {
if (!bookmarkListEvent) return [] if (!bookmarkListEvent) return []
return ( return (
bookmarkListEvent.tags bookmarkListEvent.tags
.map((tag) => (tag[0] === 'e' ? generateEventIdFromETag(tag) : undefined)) .map((tag) => (tag[0] === 'e' ? generateEventIdFromETag(tag) : undefined))
.filter(Boolean) as `nevent1${string}`[] .filter(Boolean) as `nevent1${string}`[]
).reverse() ).reverse()
}, [bookmarkListEvent]) }, [bookmarkListEvent])
const [showCount, setShowCount] = useState(SHOW_COUNT) const [showCount, setShowCount] = useState(SHOW_COUNT)
const bottomRef = useRef<HTMLDivElement | null>(null) const bottomRef = useRef<HTMLDivElement | null>(null)
useEffect(() => { useEffect(() => {
const options = { const options = {
root: null, root: null,
rootMargin: '10px', rootMargin: '10px',
threshold: 0.1 threshold: 0.1
} }
const loadMore = () => { const loadMore = () => {
if (showCount < eventIds.length) { if (showCount < eventIds.length) {
setShowCount((prev) => prev + SHOW_COUNT) setShowCount((prev) => prev + SHOW_COUNT)
} }
} }
const observerInstance = new IntersectionObserver((entries) => { const observerInstance = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) { if (entries[0].isIntersecting) {
loadMore() loadMore()
} }
}, options) }, options)
const currentBottomRef = bottomRef.current const currentBottomRef = bottomRef.current
if (currentBottomRef) { if (currentBottomRef) {
observerInstance.observe(currentBottomRef) observerInstance.observe(currentBottomRef)
} }
return () => { return () => {
if (observerInstance && currentBottomRef) { if (observerInstance && currentBottomRef) {
observerInstance.unobserve(currentBottomRef) observerInstance.unobserve(currentBottomRef)
} }
} }
}, [showCount, eventIds]) }, [showCount, eventIds])
if (eventIds.length === 0) { if (eventIds.length === 0) {
return ( return (
<div className="mt-2 text-sm text-center text-muted-foreground"> <div className="mt-2 text-sm text-center text-muted-foreground">
{t('no bookmarks found')} {t('no bookmarks found')}
</div> </div>
) )
} }
return ( return (
<div> <div>
{eventIds.slice(0, showCount).map((eventId) => ( {eventIds.slice(0, showCount).map((eventId) => (
<BookmarkedNote key={eventId} eventId={eventId} /> <BookmarkedNote key={eventId} eventId={eventId} />
))} ))}
{showCount < eventIds.length ? ( {showCount < eventIds.length ? (
<div ref={bottomRef}> <div ref={bottomRef}>
<NoteCardLoadingSkeleton isPictures={false} /> <NoteCardLoadingSkeleton isPictures={false} />
</div> </div>
) : ( ) : (
<div className="text-center text-sm text-muted-foreground mt-2"> <div className="text-center text-sm text-muted-foreground mt-2">
{t('no more bookmarks')} {t('no more bookmarks')}
</div> </div>
)} )}
</div> </div>
) )
} }
function BookmarkedNote({ eventId }: { eventId: string }) { function BookmarkedNote({ eventId }: { eventId: string }) {
const { event, isFetching } = useFetchEvent(eventId) const { event, isFetching } = useFetchEvent(eventId)
if (isFetching) { if (isFetching) {
return <NoteCardLoadingSkeleton isPictures={false} /> return <NoteCardLoadingSkeleton isPictures={false} />
} }
if (!event || event.kind !== kinds.ShortTextNote) { if (!event || event.kind !== kinds.ShortTextNote) {
return null return null
} }
return <NoteCard event={event} className="w-full" /> return <NoteCard event={event} className="w-full" />
} }

136
src/components/NewNotesButton/index.tsx

@ -1,68 +1,68 @@
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { SimpleUserAvatar } from '@/components/UserAvatar' import { SimpleUserAvatar } from '@/components/UserAvatar'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { useScreenSize } from '@/providers/ScreenSizeProvider' import { useScreenSize } from '@/providers/ScreenSizeProvider'
import { Event } from 'nostr-tools' import { Event } from 'nostr-tools'
import { useMemo } from 'react' import { useMemo } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
export default function NewNotesButton({ export default function NewNotesButton({
newEvents = [], newEvents = [],
onClick onClick
}: { }: {
newEvents?: Event[] newEvents?: Event[]
onClick?: () => void onClick?: () => void
}) { }) {
const { t } = useTranslation() const { t } = useTranslation()
const { isSmallScreen } = useScreenSize() const { isSmallScreen } = useScreenSize()
const pubkeys = useMemo(() => { const pubkeys = useMemo(() => {
const arr: string[] = [] const arr: string[] = []
for (const event of newEvents) { for (const event of newEvents) {
if (!arr.includes(event.pubkey)) { if (!arr.includes(event.pubkey)) {
arr.push(event.pubkey) arr.push(event.pubkey)
} }
if (arr.length >= 3) break if (arr.length >= 3) break
} }
return arr return arr
}, [newEvents]) }, [newEvents])
return ( return (
<> <>
{newEvents.length > 0 && ( {newEvents.length > 0 && (
<div <div
className={cn( className={cn(
'w-full flex justify-center z-40 pointer-events-none', 'w-full flex justify-center z-40 pointer-events-none',
isSmallScreen ? 'fixed' : 'absolute bottom-4' isSmallScreen ? 'fixed' : 'absolute bottom-4'
)} )}
style={isSmallScreen ? { bottom: 'calc(4rem + env(safe-area-inset-bottom))' } : undefined} style={isSmallScreen ? { bottom: 'calc(4rem + env(safe-area-inset-bottom))' } : undefined}
> >
<Button <Button
onClick={onClick} onClick={onClick}
className="group rounded-full h-fit pl-2 pr-3 hover:bg-primary-hover pointer-events-auto" className="group rounded-full h-fit pl-2 pr-3 hover:bg-primary-hover pointer-events-auto"
> >
{pubkeys.length > 0 && ( {pubkeys.length > 0 && (
<div className="flex items-center"> <div className="flex items-center">
{pubkeys.map((pubkey, index) => ( {pubkeys.map((pubkey, index) => (
<div <div
key={pubkey} key={pubkey}
className="relative -mr-2.5 last:mr-0" className="relative -mr-2.5 last:mr-0"
style={{ zIndex: 3 - index }} style={{ zIndex: 3 - index }}
> >
<SimpleUserAvatar <SimpleUserAvatar
userId={pubkey} userId={pubkey}
size="small" size="small"
className="border-primary border-2 group-hover:border-primary-hover" className="border-primary border-2 group-hover:border-primary-hover"
/> />
</div> </div>
))} ))}
</div> </div>
)} )}
<div className="text-md font-medium"> <div className="text-md font-medium">
{t('Show n new notes', { n: newEvents.length > 99 ? '99+' : newEvents.length })} {t('Show n new notes', { n: newEvents.length > 99 ? '99+' : newEvents.length })}
</div> </div>
</Button> </Button>
</div> </div>
)} )}
</> </>
) )
} }

2
src/components/NoteStats/Likes.tsx

@ -67,7 +67,7 @@ export default function Likes({ event }: { event: Event }) {
like(key, emoji) like(key, emoji)
}} }}
> >
{liking === key ? <Loader className="animate-spin size-5" /> : <Emoji emoji={emoji} />} {liking === key ? <Loader className="animate-spin size-4" /> : <Emoji emoji={emoji} />}
<div className="text-sm">{pubkeys.size}</div> <div className="text-sm">{pubkeys.size}</div>
</div> </div>
))} ))}

130
src/providers/BookmarksProvider.tsx

@ -1,65 +1,65 @@
import { createBookmarkDraftEvent } from '@/lib/draft-event' import { createBookmarkDraftEvent } from '@/lib/draft-event'
import client from '@/services/client.service' import client from '@/services/client.service'
import { createContext, useContext } from 'react' import { createContext, useContext } from 'react'
import { useNostr } from './NostrProvider' import { useNostr } from './NostrProvider'
import { Event } from 'nostr-tools' import { Event } from 'nostr-tools'
type TBookmarksContext = { type TBookmarksContext = {
addBookmark: (event: Event) => Promise<void> addBookmark: (event: Event) => Promise<void>
removeBookmark: (event: Event) => Promise<void> removeBookmark: (event: Event) => Promise<void>
} }
const BookmarksContext = createContext<TBookmarksContext | undefined>(undefined) const BookmarksContext = createContext<TBookmarksContext | undefined>(undefined)
export const useBookmarks = () => { export const useBookmarks = () => {
const context = useContext(BookmarksContext) const context = useContext(BookmarksContext)
if (!context) { if (!context) {
throw new Error('useBookmarks must be used within a BookmarksProvider') throw new Error('useBookmarks must be used within a BookmarksProvider')
} }
return context return context
} }
export function BookmarksProvider({ children }: { children: React.ReactNode }) { export function BookmarksProvider({ children }: { children: React.ReactNode }) {
const { pubkey: accountPubkey, publish, updateBookmarkListEvent } = useNostr() const { pubkey: accountPubkey, publish, updateBookmarkListEvent } = useNostr()
const addBookmark = async (event: Event) => { const addBookmark = async (event: Event) => {
if (!accountPubkey) return if (!accountPubkey) return
const bookmarkListEvent = await client.fetchBookmarkListEvent(accountPubkey) const bookmarkListEvent = await client.fetchBookmarkListEvent(accountPubkey)
const currentTags = bookmarkListEvent?.tags || [] const currentTags = bookmarkListEvent?.tags || []
if (currentTags.some((tag) => tag[0] === 'e' && tag[1] === event.id)) return if (currentTags.some((tag) => tag[0] === 'e' && tag[1] === event.id)) return
const newBookmarkDraftEvent = createBookmarkDraftEvent( const newBookmarkDraftEvent = createBookmarkDraftEvent(
[...currentTags, ['e', event.id, client.getEventHint(event.id), '', event.pubkey]], [...currentTags, ['e', event.id, client.getEventHint(event.id), '', event.pubkey]],
bookmarkListEvent?.content bookmarkListEvent?.content
) )
const newBookmarkEvent = await publish(newBookmarkDraftEvent) const newBookmarkEvent = await publish(newBookmarkDraftEvent)
await updateBookmarkListEvent(newBookmarkEvent) await updateBookmarkListEvent(newBookmarkEvent)
} }
const removeBookmark = async (event: Event) => { const removeBookmark = async (event: Event) => {
if (!accountPubkey) return if (!accountPubkey) return
const bookmarkListEvent = await client.fetchBookmarkListEvent(accountPubkey) const bookmarkListEvent = await client.fetchBookmarkListEvent(accountPubkey)
if (!bookmarkListEvent) return if (!bookmarkListEvent) return
const newTags = bookmarkListEvent.tags.filter((tag) => !(tag[0] === 'e' && tag[1] === event.id)) const newTags = bookmarkListEvent.tags.filter((tag) => !(tag[0] === 'e' && tag[1] === event.id))
if (newTags.length === bookmarkListEvent.tags.length) return if (newTags.length === bookmarkListEvent.tags.length) return
const newBookmarkDraftEvent = createBookmarkDraftEvent(newTags, bookmarkListEvent.content) const newBookmarkDraftEvent = createBookmarkDraftEvent(newTags, bookmarkListEvent.content)
const newBookmarkEvent = await publish(newBookmarkDraftEvent) const newBookmarkEvent = await publish(newBookmarkDraftEvent)
await updateBookmarkListEvent(newBookmarkEvent) await updateBookmarkListEvent(newBookmarkEvent)
} }
return ( return (
<BookmarksContext.Provider <BookmarksContext.Provider
value={{ value={{
addBookmark, addBookmark,
removeBookmark removeBookmark
}} }}
> >
{children} {children}
</BookmarksContext.Provider> </BookmarksContext.Provider>
) )
} }

2
tailwind.config.js

@ -23,7 +23,7 @@ export default {
primary: { primary: {
DEFAULT: 'hsl(var(--primary))', DEFAULT: 'hsl(var(--primary))',
foreground: 'hsl(var(--primary-foreground))', foreground: 'hsl(var(--primary-foreground))',
hover: 'hsl(var(--primary-hover))', hover: 'hsl(var(--primary-hover))'
}, },
secondary: { secondary: {
DEFAULT: 'hsl(var(--secondary))', DEFAULT: 'hsl(var(--secondary))',

Loading…
Cancel
Save