feat: update layout

This commit is contained in:
codytseng
2025-08-27 21:56:46 +08:00
parent f41536a793
commit 8b1c2ebe3f
30 changed files with 230 additions and 250 deletions

View File

@@ -18,7 +18,7 @@
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<link rel="icon" href="/favicon.ico" sizes="48x48" />
<link rel="icon" href="/favicon.svg" sizes="any" type="image/svg+xml" />
<meta name="theme-color" content="#09090b" media="(prefers-color-scheme: dark)" />
<meta name="theme-color" content="#171717" media="(prefers-color-scheme: dark)" />
<meta name="theme-color" content="#FFFFFF" media="(prefers-color-scheme: light)" />
<meta property="og:url" content="https://jumble.social" />

View File

@@ -1,5 +1,4 @@
import Sidebar from '@/components/Sidebar'
import { Separator } from '@/components/ui/separator'
import { cn } from '@/lib/utils'
import NoteListPage from '@/pages/primary/NoteListPage'
import HomePage from '@/pages/secondary/HomePage'
@@ -15,6 +14,7 @@ import {
useRef,
useState
} from 'react'
import BottomNavigationBar from './components/BottomNavigationBar'
import TooManyRelaysAlertDialog from './components/TooManyRelaysAlertDialog'
import ExplorePage from './pages/primary/ExplorePage'
import MePage from './pages/primary/MePage'
@@ -90,12 +90,10 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) {
}
])
const [secondaryStack, setSecondaryStack] = useState<TStackItem[]>([])
const [isShared, setIsShared] = useState(false)
const { isSmallScreen } = useScreenSize()
const ignorePopStateRef = useRef(false)
useEffect(() => {
const hasHistoryState = !!history.state
if (['/npub1', '/nprofile1'].some((prefix) => window.location.pathname.startsWith(prefix))) {
window.history.replaceState(
null,
@@ -115,12 +113,6 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) {
}
window.history.pushState(null, '', window.location.href)
if (window.location.pathname !== '/') {
if (
['/users', '/notes', '/relays'].some((path) => window.location.pathname.startsWith(path)) &&
!hasHistoryState
) {
setIsShared(true)
}
const url = window.location.pathname + window.location.search + window.location.hash
setSecondaryStack((prevStack) => {
if (isCurrentPage(prevStack, url)) return prevStack
@@ -248,7 +240,6 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) {
if (secondaryStack.length === 1) {
// back to home page
window.history.replaceState(null, '', '/')
setIsShared(false)
setSecondaryStack([])
} else {
window.history.go(-1)
@@ -301,6 +292,7 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) {
{element}
</div>
))}
<BottomNavigationBar />
<TooManyRelaysAlertDialog />
</NotificationProvider>
</SecondaryPageContext.Provider>
@@ -308,39 +300,6 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) {
)
}
if (isShared && secondaryStack.length > 0) {
return (
<PrimaryPageContext.Provider
value={{
navigate: navigatePrimaryPage,
current: currentPrimaryPage,
display: false
}}
>
<SecondaryPageContext.Provider
value={{
push: pushSecondaryPage,
pop: popSecondaryPage,
currentIndex: secondaryStack[secondaryStack.length - 1].index
}}
>
<NotificationProvider>
<div className="h-screen overflow-hidden max-w-4xl mx-auto border-x">
{secondaryStack.map((item, index) => (
<div
key={item.index}
style={{ display: index === secondaryStack.length - 1 ? 'block' : 'none' }}
>
{item.component}
</div>
))}
</div>
</NotificationProvider>
</SecondaryPageContext.Provider>
</PrimaryPageContext.Provider>
)
}
return (
<PrimaryPageContext.Provider
value={{
@@ -357,11 +316,10 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) {
}}
>
<NotificationProvider>
<div className="flex h-screen overflow-hidden">
<div className="flex h-screen overflow-hidden bg-surface-background">
<Sidebar />
<Separator orientation="vertical" />
<div className="grid grid-cols-2 w-full">
<div className="flex border-r">
<div className="grid grid-cols-2 gap-2 w-full pr-2">
<div className="flex rounded-lg my-2 max-h-screen shadow-md bg-background overflow-hidden">
{primaryPages.map(({ name, element }) => (
<div
key={name}
@@ -374,16 +332,21 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) {
</div>
))}
</div>
<div>
<div className="flex rounded-lg my-2 max-h-screen shadow-md bg-background overflow-hidden">
{secondaryStack.map((item, index) => (
<div
key={item.index}
className="w-full"
style={{ display: index === secondaryStack.length - 1 ? 'block' : 'none' }}
>
{item.component}
</div>
))}
<div key="home" style={{ display: secondaryStack.length === 0 ? 'block' : 'none' }}>
<div
key="home"
className="w-full"
style={{ display: secondaryStack.length === 0 ? 'block' : 'none' }}
>
<HomePage />
</div>
</div>

View File

@@ -8,7 +8,7 @@ export default function BottomNavigationBar() {
return (
<div
className={cn(
'fixed bottom-0 w-full z-40 bg-background/80 backdrop-blur-xl flex items-center justify-around [&_svg]:size-4 [&_svg]:shrink-0'
'fixed bottom-0 w-full z-40 bg-background border-t flex items-center justify-around [&_svg]:size-4 [&_svg]:shrink-0'
)}
style={{
height: 'calc(3rem + env(safe-area-inset-bottom))',

View File

@@ -88,7 +88,7 @@ export default function KindFilter({
key={label}
className={cn(
'cursor-pointer grid gap-1.5 rounded-lg border px-4 py-3',
checked ? 'border-primary bg-primary/20' : 'clickable'
checked ? 'border-primary/60 bg-primary/5' : 'clickable'
)}
onClick={() => {
console.log(checked)
@@ -166,7 +166,7 @@ export default function KindFilter({
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>{trigger}</PopoverTrigger>
<PopoverContent className="w-96" collisionPadding={16}>
<PopoverContent className="w-96" collisionPadding={16} sideOffset={0}>
{content}
</PopoverContent>
</Popover>

View File

@@ -2,6 +2,7 @@ import { Button } from '@/components/ui/button'
import { SimpleUserAvatar } from '@/components/UserAvatar'
import { cn } from '@/lib/utils'
import { useScreenSize } from '@/providers/ScreenSizeProvider'
import { ArrowUp } from 'lucide-react'
import { Event } from 'nostr-tools'
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
@@ -32,34 +33,25 @@ export default function NewNotesButton({
<div
className={cn(
'w-full flex justify-center z-40 pointer-events-none',
isSmallScreen ? 'fixed' : 'absolute bottom-4'
isSmallScreen ? 'fixed' : 'absolute bottom-6'
)}
style={isSmallScreen ? { bottom: 'calc(4rem + env(safe-area-inset-bottom))' } : undefined}
>
<Button
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 py-2 pl-2 pr-3 hover:bg-primary-hover pointer-events-auto"
>
{pubkeys.length > 0 && (
<div className="flex items-center">
{pubkeys.map((pubkey, index) => (
<div
key={pubkey}
className="relative -mr-2.5 last:mr-0"
style={{ zIndex: 3 - index }}
>
<SimpleUserAvatar
userId={pubkey}
size="small"
className="border-primary border-2 group-hover:border-primary-hover"
/>
</div>
<div className="*:data-[slot=avatar]:ring-background flex -space-x-2 *:data-[slot=avatar]:ring-2 *:data-[slot=avatar]:grayscale">
{pubkeys.map((pubkey) => (
<SimpleUserAvatar userId={pubkey} size="small" />
))}
</div>
)}
<div className="text-md font-medium">
{t('Show n new notes', { n: newEvents.length > 99 ? '99+' : newEvents.length })}
</div>
<ArrowUp />
</Button>
</div>
)}

View File

@@ -209,7 +209,7 @@ const NoteList = forwardRef(
setEvents((oldEvents) => [...newEvents, ...oldEvents])
setNewEvents([])
setTimeout(() => {
scrollToTop()
scrollToTop('smooth')
}, 0)
}
@@ -218,7 +218,7 @@ const NoteList = forwardRef(
{filteredNewEvents.length > 0 && (
<NewNotesButton newEvents={filteredNewEvents} onClick={showNewEvents} />
)}
<div ref={topRef} className="scroll-mt-24" />
<div ref={topRef} className="scroll-mt-[calc(6rem+1px)]" />
<PullToRefresh
onRefresh={async () => {
setRefreshCount((count) => count + 1)

View File

@@ -35,7 +35,7 @@ export default function ProfileList({ pubkeys }: { pubkeys: string[] }) {
}, [visiblePubkeys, pubkeys])
return (
<div className="px-4">
<div className="px-4 pt-2">
{visiblePubkeys.map((pubkey, index) => (
<UserItem key={`${index}-${pubkey}`} pubkey={pubkey} />
))}

View File

@@ -12,8 +12,8 @@ const SidebarItem = forwardRef<
return (
<Button
className={cn(
'flex shadow-none items-center bg-transparent w-12 h-12 xl:w-full xl:h-auto p-3 m-0 xl:py-2 xl:px-4 rounded-lg xl:justify-start gap-4 text-lg font-semibold [&_svg]:size-full xl:[&_svg]:size-4',
active && 'text-primary hover:text-primary',
'flex shadow-none items-center transition-colors duration-500 bg-transparent w-12 h-12 xl:w-full xl:h-auto p-3 m-0 xl:py-2 xl:px-3 rounded-lg xl:justify-start gap-4 text-lg font-semibold [&_svg]:size-full xl:[&_svg]:size-4',
active && 'text-primary hover:text-primary bg-primary/10 hover:bg-primary/10',
className
)}
variant="ghost"

View File

@@ -14,7 +14,7 @@ export default function PrimaryPageSidebar() {
if (isSmallScreen) return null
return (
<div className="w-16 xl:w-52 flex flex-col pb-2 pt-4 px-2 justify-between h-full shrink-0">
<div className="w-16 xl:w-52 flex flex-col pb-2 pt-4 px-2 xl:px-4 justify-between h-full shrink-0">
<div className="space-y-2">
<div className="px-3 xl:px-4 mb-6 w-full">
<Icon className="xl:hidden" />

View File

@@ -87,7 +87,7 @@ export default function Tabs({
<div
ref={containerRef}
className={cn(
'sticky flex justify-between top-12 bg-background z-30 px-1 w-full transition-transform',
'sticky flex justify-between top-12 bg-background z-30 px-1 w-full transition-transform border-b',
deepBrowsing && lastScrollTop > threshold ? '-translate-y-[calc(100%+12rem)]' : ''
)}
>

View File

@@ -2,15 +2,18 @@ import { cn } from '@/lib/utils'
export function Titlebar({
children,
className
className,
hideBottomBorder = false
}: {
children?: React.ReactNode
className?: string
hideBottomBorder?: boolean
}) {
return (
<div
className={cn(
'sticky top-0 w-full h-12 z-40 bg-background [&_svg]:size-5 [&_svg]:shrink-0 select-none',
!hideBottomBorder && 'border-b',
className
)}
>

View File

@@ -50,6 +50,14 @@
pointer-events: none;
}
.scrollbar-hide {
-ms-overflow-style: none; /* Internet Explorer 10+ */
scrollbar-width: none; /* Firefox */
}
.scrollbar-hide::-webkit-scrollbar {
display: none; /* Safari and Chrome */
}
@media (hover: hover) and (pointer: fine) {
.clickable:hover {
background-color: hsl(var(--muted) / 0.5);
@@ -70,6 +78,7 @@
}
:root {
--surface-background: 0 0% 98%;
--background: 0 0% 100%;
--foreground: 240 10% 3.9%;
--card: 0 0% 100%;
@@ -79,11 +88,11 @@
--primary: 259 43% 56%;
--primary-hover: 259 43% 65%;
--primary-foreground: 0 0% 98%;
--secondary: 240 4.8% 95.9%;
--secondary: 240 4.8% 94%;
--secondary-foreground: 240 5.9% 10%;
--muted: 240 4.8% 95.9%;
--muted: 240 4.8% 94%;
--muted-foreground: 240 3.8% 46.1%;
--accent: 240 4.8% 95.9%;
--accent: 240 4.8% 94%;
--accent-foreground: 240 5.9% 10%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
@@ -98,11 +107,12 @@
--radius: 0.5rem;
}
.dark {
--background: 240 10% 3.9%;
--surface-background: 240 10% 3.9%;
--background: 0 0% 9%;
--foreground: 0 0% 98%;
--card: 240 10% 3.9%;
--card: 0 0% 9%;
--card-foreground: 0 0% 98%;
--popover: 240 10% 3.9%;
--popover: 0 0% 9%;
--popover-foreground: 0 0% 98%;
--primary: 259 43% 56%;
--primary-hover: 259 43% 65%;

View File

@@ -1,4 +1,3 @@
import BottomNavigationBar from '@/components/BottomNavigationBar'
import ScrollToTopButton from '@/components/ScrollToTopButton'
import { Titlebar } from '@/components/Titlebar'
import { ScrollArea } from '@/components/ui/scroll-area'
@@ -13,12 +12,14 @@ const PrimaryPageLayout = forwardRef(
children,
titlebar,
pageName,
displayScrollToTopButton = false
displayScrollToTopButton = false,
hideTitlebarBottomBorder = false
}: {
children?: React.ReactNode
titlebar: React.ReactNode
pageName: TPrimaryPageName
displayScrollToTopButton?: boolean
hideTitlebarBottomBorder?: boolean
},
ref
) => {
@@ -69,9 +70,10 @@ const PrimaryPageLayout = forwardRef(
paddingBottom: 'calc(env(safe-area-inset-bottom) + 3rem)'
}}
>
<PrimaryPageTitlebar>{titlebar}</PrimaryPageTitlebar>
<PrimaryPageTitlebar hideBottomBorder={hideTitlebarBottomBorder}>
{titlebar}
</PrimaryPageTitlebar>
{children}
<BottomNavigationBar />
</div>
{displayScrollToTopButton && <ScrollToTopButton />}
</DeepBrowsingProvider>
@@ -81,7 +83,7 @@ const PrimaryPageLayout = forwardRef(
return (
<DeepBrowsingProvider active={current === pageName && display} scrollAreaRef={scrollAreaRef}>
<ScrollArea
className="h-screen overflow-auto"
className="h-full overflow-auto"
scrollBarClassName="z-50 pt-12"
ref={scrollAreaRef}
>
@@ -101,6 +103,16 @@ export type TPrimaryPageLayoutRef = {
scrollToTop: () => void
}
function PrimaryPageTitlebar({ children }: { children?: React.ReactNode }) {
return <Titlebar className="p-1">{children}</Titlebar>
function PrimaryPageTitlebar({
children,
hideBottomBorder = false
}: {
children?: React.ReactNode
hideBottomBorder?: boolean
}) {
return (
<Titlebar className="p-1" hideBottomBorder={hideBottomBorder}>
{children}
</Titlebar>
)
}

View File

@@ -1,5 +1,4 @@
import BackButton from '@/components/BackButton'
import BottomNavigationBar from '@/components/BottomNavigationBar'
import ScrollToTopButton from '@/components/ScrollToTopButton'
import { Titlebar } from '@/components/Titlebar'
import { ScrollArea } from '@/components/ui/scroll-area'
@@ -16,6 +15,7 @@ const SecondaryPageLayout = forwardRef(
title,
controls,
hideBackButton = false,
hideTitlebarBottomBorder = false,
displayScrollToTopButton = false
}: {
children?: React.ReactNode
@@ -23,6 +23,7 @@ const SecondaryPageLayout = forwardRef(
title?: React.ReactNode
controls?: React.ReactNode
hideBackButton?: boolean
hideTitlebarBottomBorder?: boolean
displayScrollToTopButton?: boolean
},
ref
@@ -65,9 +66,9 @@ const SecondaryPageLayout = forwardRef(
title={title}
controls={controls}
hideBackButton={hideBackButton}
hideBottomBorder={hideTitlebarBottomBorder}
/>
{children}
<BottomNavigationBar />
</div>
{displayScrollToTopButton && <ScrollToTopButton />}
</DeepBrowsingProvider>
@@ -77,7 +78,7 @@ const SecondaryPageLayout = forwardRef(
return (
<DeepBrowsingProvider active={currentIndex === index} scrollAreaRef={scrollAreaRef}>
<ScrollArea
className="h-screen overflow-auto"
className="h-full overflow-auto"
scrollBarClassName="z-50 pt-12"
ref={scrollAreaRef}
>
@@ -85,6 +86,7 @@ const SecondaryPageLayout = forwardRef(
title={title}
controls={controls}
hideBackButton={hideBackButton}
hideBottomBorder={hideTitlebarBottomBorder}
/>
{children}
<div className="h-4" />
@@ -100,14 +102,19 @@ export default SecondaryPageLayout
export function SecondaryPageTitlebar({
title,
controls,
hideBackButton = false
hideBackButton = false,
hideBottomBorder = false
}: {
title?: React.ReactNode
controls?: React.ReactNode
hideBackButton?: boolean
hideBottomBorder?: boolean
}): JSX.Element {
return (
<Titlebar className="flex gap-1 p-1 items-center justify-between font-semibold">
<Titlebar
className="flex gap-1 p-1 items-center justify-between font-semibold"
hideBottomBorder={hideBottomBorder}
>
{hideBackButton ? (
<div className="flex gap-2 items-center pl-3 w-fit truncate text-lg font-semibold">
{title}

View File

@@ -33,7 +33,12 @@ const MePage = forwardRef((_, ref) => {
if (!pubkey) {
return (
<PrimaryPageLayout ref={ref} pageName="home" titlebar={<MePageTitlebar />}>
<PrimaryPageLayout
ref={ref}
pageName="home"
titlebar={<MePageTitlebar />}
hideTitlebarBottomBorder
>
<div className="flex flex-col p-4 gap-4 overflow-auto">
<AccountManager />
</div>
@@ -42,7 +47,12 @@ const MePage = forwardRef((_, ref) => {
}
return (
<PrimaryPageLayout ref={ref} pageName="home" titlebar={<MePageTitlebar />}>
<PrimaryPageLayout
ref={ref}
pageName="home"
titlebar={<MePageTitlebar />}
hideTitlebarBottomBorder
>
<div className="flex gap-4 items-center p-4">
<SimpleUserAvatar userId={pubkey} size="big" />
<div className="space-y-1 flex-1 w-0">

View File

@@ -34,7 +34,11 @@ export default function FeedButton({ className }: { className?: string }) {
<PopoverTrigger asChild>
<FeedSwitcherTrigger className={className} />
</PopoverTrigger>
<PopoverContent side="bottom" className="w-96 p-4 max-h-[80vh] overflow-auto">
<PopoverContent
sideOffset={0}
side="bottom"
className="w-96 p-4 max-h-[80vh] overflow-auto scrollbar-hide"
>
<FeedSwitcher close={() => setOpen(false)} />
</PopoverContent>
</Popover>

View File

@@ -24,7 +24,7 @@ const NoteListPage = forwardRef((_, ref) => {
useEffect(() => {
if (layoutRef.current) {
layoutRef.current.scrollToTop()
layoutRef.current.scrollToTop('instant')
}
}, [JSON.stringify(relayUrls), feedInfo])

View File

@@ -26,7 +26,7 @@ const GeneralSettingsPage = forwardRef(({ index }: { index?: number }, ref) => {
return (
<SecondaryPageLayout ref={ref} index={index} title={t('General')}>
<div className="space-y-4 mt-2">
<div className="space-y-4 mt-3">
<SettingItem>
<Label htmlFor="languages" className="text-base font-normal">
{t('Languages')}

View File

@@ -30,7 +30,7 @@ const HomePage = forwardRef(({ index }: { index?: number }, ref) => {
if (!recommendedRelayInfos.length) {
return (
<SecondaryPageLayout ref={ref} index={index} hideBackButton>
<SecondaryPageLayout ref={ref} index={index} hideBackButton hideTitlebarBottomBorder>
<div className="text-muted-foreground w-full h-screen flex items-center justify-center">
{t('Welcome! 🥳')}
</div>
@@ -49,8 +49,9 @@ const HomePage = forwardRef(({ index }: { index?: number }, ref) => {
</>
}
hideBackButton
hideTitlebarBottomBorder
>
<div className="px-4">
<div className="px-4 pt-2">
<div className="grid grid-cols-2 gap-3">
{recommendedRelayInfos.map((relayInfo) => (
<RelaySimpleInfo

View File

@@ -1,14 +0,0 @@
import SecondaryPageLayout from '@/layouts/SecondaryPageLayout'
import { forwardRef } from 'react'
const LoadingPage = forwardRef(({ title, index }: { title?: string; index?: number }, ref) => {
return (
<SecondaryPageLayout ref={ref} index={index} title={title}>
<div className="text-muted-foreground text-center">
<div>Loading...</div>
</div>
</SecondaryPageLayout>
)
})
LoadingPage.displayName = 'LoadingPage'
export default LoadingPage

View File

@@ -35,7 +35,7 @@ const NotePage = forwardRef(({ id, index }: { id?: string; index?: number }, ref
if (!event && isFetching) {
return (
<SecondaryPageLayout ref={ref} index={index} title={t('Note')}>
<div className="px-4">
<div className="px-4 pt-3">
<div className="flex items-center space-x-2">
<Skeleton className="w-10 h-10 rounded-full" />
<div className={`flex-1 w-0`}>
@@ -69,7 +69,7 @@ const NotePage = forwardRef(({ id, index }: { id?: string; index?: number }, ref
return (
<SecondaryPageLayout ref={ref} index={index} title={t('Note')} displayScrollToTopButton>
<div className="px-4">
<div className="px-4 pt-3">
{rootITag && <ExternalRoot value={rootITag[1]} />}
{rootEventId !== parentEventId && (
<ParentNote
@@ -132,12 +132,12 @@ function ParentNote({
if (isFetching) {
return (
<div>
<Card className="flex space-x-1 px-1.5 py-1 items-center clickable text-sm text-muted-foreground">
<div className="flex space-x-1 px-[0.4375rem] py-1 items-center rounded-full border clickable text-sm text-muted-foreground">
<Skeleton className="shrink w-4 h-4 rounded-full" />
<div className="py-1 flex-1">
<Skeleton className="h-3" />
</div>
</Card>
</div>
<div className="ml-5 w-px h-3 bg-border" />
</div>
)
@@ -146,9 +146,9 @@ function ParentNote({
return (
<div>
<Card
<div
className={cn(
'flex space-x-1 px-1.5 py-1 items-center clickable text-sm text-muted-foreground',
'flex space-x-1 px-[0.4375rem] py-1 items-center rounded-full border clickable text-sm text-muted-foreground',
event && 'hover:text-foreground'
)}
onClick={() => {
@@ -158,7 +158,7 @@ function ParentNote({
>
{event && <UserAvatar userId={event.pubkey} size="tiny" className="shrink-0" />}
<ContentPreview className="truncate" event={event} />
</Card>
</div>
{isConsecutive ? (
<div className="ml-5 w-px h-3 bg-border" />
) : (

View File

@@ -18,7 +18,7 @@ const RelaySettingsPage = forwardRef(({ id, index }: { id?: string; index?: numb
index={index}
title={t("username's used relays", { username: profile.username })}
>
<div className="px-4">
<div className="px-4 pt-3">
<OthersRelayList userId={id} />
</div>
</SecondaryPageLayout>

View File

@@ -8,7 +8,7 @@ const PostSettingsPage = forwardRef(({ index }: { index?: number }, ref) => {
return (
<SecondaryPageLayout ref={ref} index={index} title={t('Post settings')}>
<div className="px-4 pt-2 space-y-4">
<div className="px-4 pt-3 space-y-4">
<MediaUploadServiceSetting />
</div>
</SecondaryPageLayout>

View File

@@ -124,112 +124,106 @@ const ProfileEditorPage = forwardRef(({ index }: { index?: number }, ref) => {
return (
<SecondaryPageLayout ref={ref} index={index} title={profile.username} controls={controls}>
<div className="px-4">
<div className="relative bg-cover bg-center rounded-lg mb-2">
<Uploader
onUploadSuccess={onBannerUploadSuccess}
onUploadStart={() => setUploadingBanner(true)}
onUploadEnd={() => setUploadingBanner(false)}
className="w-full relative cursor-pointer"
>
<ProfileBanner
banner={banner}
pubkey={account.pubkey}
className="w-full aspect-[3/1] object-cover rounded-lg"
/>
<div className="absolute top-0 bg-muted/30 w-full h-full rounded-lg flex flex-col justify-center items-center">
{uploadingBanner ? (
<Loader size={36} className="animate-spin" />
) : (
<Upload size={36} />
)}
</div>
</Uploader>
<Uploader
onUploadSuccess={onAvatarUploadSuccess}
onUploadStart={() => setUploadingAvatar(true)}
onUploadEnd={() => setUploadingAvatar(false)}
className="w-24 h-24 absolute bottom-0 left-4 translate-y-1/2 border-4 border-background cursor-pointer rounded-full"
>
<Avatar className="w-full h-full">
<AvatarImage src={avatar} className="object-cover object-center" />
<AvatarFallback>
<img src={defaultImage} />
</AvatarFallback>
</Avatar>
<div className="absolute top-0 bg-muted/30 w-full h-full rounded-full flex flex-col justify-center items-center">
{uploadingAvatar ? <Loader className="animate-spin" /> : <Upload />}
</div>
</Uploader>
</div>
<div className="pt-14 flex flex-col gap-4">
<Item>
<Label htmlFor="profile-username-input">{t('Display Name')}</Label>
<Input
id="profile-username-input"
value={username}
onChange={(e) => {
setUsername(e.target.value)
setHasChanged(true)
}}
/>
</Item>
<Item>
<Label htmlFor="profile-about-textarea">{t('Bio')}</Label>
<Textarea
id="profile-about-textarea"
className="h-44"
value={about}
onChange={(e) => {
setAbout(e.target.value)
setHasChanged(true)
}}
/>
</Item>
<Item>
<Label htmlFor="profile-website-input">{t('Website')}</Label>
<Input
id="profile-website-input"
value={website}
onChange={(e) => {
setWebsite(e.target.value)
setHasChanged(true)
}}
/>
</Item>
<Item>
<Label htmlFor="profile-nip05-input">{t('Nostr Address (NIP-05)')}</Label>
<Input
id="profile-nip05-input"
value={nip05}
onChange={(e) => {
setNip05Error('')
setNip05(e.target.value)
setHasChanged(true)
}}
className={nip05Error ? 'border-destructive' : ''}
/>
{nip05Error && <div className="text-xs text-destructive pl-3">{nip05Error}</div>}
</Item>
<Item>
<Label htmlFor="profile-lightning-address-input">
{t('Lightning Address (or LNURL)')}
</Label>
<Input
id="profile-lightning-address-input"
value={lightningAddress}
onChange={(e) => {
setLightningAddressError('')
setLightningAddress(e.target.value)
setHasChanged(true)
}}
className={lightningAddressError ? 'border-destructive' : ''}
/>
{lightningAddressError && (
<div className="text-xs text-destructive pl-3">{lightningAddressError}</div>
)}
</Item>
</div>
<div className="relative bg-cover bg-center mb-2">
<Uploader
onUploadSuccess={onBannerUploadSuccess}
onUploadStart={() => setUploadingBanner(true)}
onUploadEnd={() => setUploadingBanner(false)}
className="w-full relative cursor-pointer"
>
<ProfileBanner
banner={banner}
pubkey={account.pubkey}
className="w-full aspect-[3/1] object-cover"
/>
<div className="absolute top-0 bg-muted/30 w-full h-full flex flex-col justify-center items-center">
{uploadingBanner ? <Loader size={36} className="animate-spin" /> : <Upload size={36} />}
</div>
</Uploader>
<Uploader
onUploadSuccess={onAvatarUploadSuccess}
onUploadStart={() => setUploadingAvatar(true)}
onUploadEnd={() => setUploadingAvatar(false)}
className="w-24 h-24 absolute bottom-0 left-4 translate-y-1/2 border-4 border-background cursor-pointer rounded-full"
>
<Avatar className="w-full h-full">
<AvatarImage src={avatar} className="object-cover object-center" />
<AvatarFallback>
<img src={defaultImage} />
</AvatarFallback>
</Avatar>
<div className="absolute top-0 bg-muted/30 w-full h-full rounded-full flex flex-col justify-center items-center">
{uploadingAvatar ? <Loader className="animate-spin" /> : <Upload />}
</div>
</Uploader>
</div>
<div className="pt-14 px-4 flex flex-col gap-4">
<Item>
<Label htmlFor="profile-username-input">{t('Display Name')}</Label>
<Input
id="profile-username-input"
value={username}
onChange={(e) => {
setUsername(e.target.value)
setHasChanged(true)
}}
/>
</Item>
<Item>
<Label htmlFor="profile-about-textarea">{t('Bio')}</Label>
<Textarea
id="profile-about-textarea"
className="h-44"
value={about}
onChange={(e) => {
setAbout(e.target.value)
setHasChanged(true)
}}
/>
</Item>
<Item>
<Label htmlFor="profile-website-input">{t('Website')}</Label>
<Input
id="profile-website-input"
value={website}
onChange={(e) => {
setWebsite(e.target.value)
setHasChanged(true)
}}
/>
</Item>
<Item>
<Label htmlFor="profile-nip05-input">{t('Nostr Address (NIP-05)')}</Label>
<Input
id="profile-nip05-input"
value={nip05}
onChange={(e) => {
setNip05Error('')
setNip05(e.target.value)
setHasChanged(true)
}}
className={nip05Error ? 'border-destructive' : ''}
/>
{nip05Error && <div className="text-xs text-destructive pl-3">{nip05Error}</div>}
</Item>
<Item>
<Label htmlFor="profile-lightning-address-input">
{t('Lightning Address (or LNURL)')}
</Label>
<Input
id="profile-lightning-address-input"
value={lightningAddress}
onChange={(e) => {
setLightningAddressError('')
setLightningAddress(e.target.value)
setHasChanged(true)
}}
className={lightningAddressError ? 'border-destructive' : ''}
/>
{lightningAddressError && (
<div className="text-xs text-destructive pl-3">{lightningAddressError}</div>
)}
</Item>
</div>
</SecondaryPageLayout>
)

View File

@@ -87,9 +87,9 @@ const ProfilePage = forwardRef(({ id, index }: { id?: string; index?: number },
if (!profile && isFetching) {
return (
<SecondaryPageLayout index={index} ref={ref}>
<div className="sm:px-4">
<div>
<div className="relative bg-cover bg-center mb-2">
<Skeleton className="w-full aspect-[3/1] sm:rounded-lg" />
<Skeleton className="w-full aspect-[3/1] rounded-none" />
<Skeleton className="w-24 h-24 absolute bottom-0 left-3 translate-y-1/2 border-4 border-background rounded-full" />
</div>
</div>
@@ -106,23 +106,17 @@ const ProfilePage = forwardRef(({ id, index }: { id?: string; index?: number },
return (
<SecondaryPageLayout index={index} title={username} displayScrollToTopButton ref={ref}>
<div ref={topContainerRef}>
<div className="sm:px-4">
<div className="relative bg-cover bg-center mb-2">
<ProfileBanner
banner={banner}
pubkey={pubkey}
className="w-full aspect-[3/1] sm:rounded-lg"
/>
<Avatar className="w-24 h-24 absolute left-3 bottom-0 translate-y-1/2 border-4 border-background">
<AvatarImage src={avatar} className="object-cover object-center" />
<AvatarFallback>
<img src={defaultImage} />
</AvatarFallback>
</Avatar>
</div>
<div className="relative bg-cover bg-center mb-2">
<ProfileBanner banner={banner} pubkey={pubkey} className="w-full aspect-[3/1]" />
<Avatar className="w-24 h-24 absolute left-3 bottom-0 translate-y-1/2 border-4 border-background">
<AvatarImage src={avatar} className="object-cover object-center" />
<AvatarFallback>
<img src={defaultImage} />
</AvatarFallback>
</Avatar>
</div>
<div className="px-4">
<div className="flex justify-end h-8 gap-2 items-center max-sm:translate-x-2">
<div className="flex justify-end h-8 gap-2 items-center">
<ProfileOptions pubkey={pubkey} />
{isSelf ? (
<Button

View File

@@ -42,6 +42,7 @@ const RelayPage = forwardRef(({ url, index }: { url?: string; index?: number },
controls={<RelayPageControls url={normalizedUrl} />}
displayScrollToTopButton
>
<div className="h-3 w-full" />
<RelayInfo url={normalizedUrl} />
{relayInfo?.supported_nips?.includes(50) && (
<div className="px-4 py-2">

View File

@@ -22,7 +22,7 @@ const RelaySettingsPage = forwardRef(({ index }: { index?: number }, ref) => {
return (
<SecondaryPageLayout ref={ref} index={index} title={t('Relay settings')}>
<Tabs value={tabValue} onValueChange={setTabValue} className="px-4 pb-4 space-y-4">
<Tabs value={tabValue} onValueChange={setTabValue} className="px-4 py-3 space-y-4">
<TabsList>
<TabsTrigger value="favorite-relays">{t('Favorite Relays')}</TabsTrigger>
<TabsTrigger value="mailbox">{t('Read & Write Relays')}</TabsTrigger>

View File

@@ -27,7 +27,7 @@ const TranslationPage = forwardRef(({ index }: { index?: number }, ref) => {
return (
<SecondaryPageLayout ref={ref} index={index} title={t('Translation')}>
<div className="px-4 pt-2 space-y-4">
<div className="px-4 pt-3 space-y-4">
<div className="space-y-2">
<Label htmlFor="languages" className="text-base font-medium">
{t('Languages')}

View File

@@ -12,7 +12,7 @@ const WalletPage = forwardRef(({ index }: { index?: number }, ref) => {
return (
<SecondaryPageLayout ref={ref} index={index} title={t('Wallet')}>
<div className="px-4 pt-2 space-y-4">
<div className="px-4 pt-3 space-y-4">
<BcButton />
<LightningAddressInput />
<DefaultZapAmountInput />

View File

@@ -10,6 +10,9 @@ export default {
sm: 'calc(var(--radius) - 4px)'
},
colors: {
surface: {
background: 'hsl(var(--surface-background))'
},
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
card: {