Browse Source

feat: update layout

imwald
codytseng 7 months ago
parent
commit
8b1c2ebe3f
  1. 2
      index.html
  2. 61
      src/PageManager.tsx
  3. 2
      src/components/BottomNavigationBar/index.tsx
  4. 4
      src/components/KindFilter/index.tsx
  5. 22
      src/components/NewNotesButton/index.tsx
  6. 4
      src/components/NoteList/index.tsx
  7. 2
      src/components/ProfileList/index.tsx
  8. 4
      src/components/Sidebar/SidebarItem.tsx
  9. 2
      src/components/Sidebar/index.tsx
  10. 2
      src/components/Tabs/index.tsx
  11. 5
      src/components/Titlebar/index.tsx
  12. 22
      src/index.css
  13. 26
      src/layouts/PrimaryPageLayout/index.tsx
  14. 17
      src/layouts/SecondaryPageLayout/index.tsx
  15. 14
      src/pages/primary/MePage/index.tsx
  16. 6
      src/pages/primary/NoteListPage/FeedButton.tsx
  17. 2
      src/pages/primary/NoteListPage/index.tsx
  18. 2
      src/pages/secondary/GeneralSettingsPage/index.tsx
  19. 5
      src/pages/secondary/HomePage/index.tsx
  20. 14
      src/pages/secondary/LoadingPage/index.tsx
  21. 14
      src/pages/secondary/NotePage/index.tsx
  22. 2
      src/pages/secondary/OthersRelaySettingsPage/index.tsx
  23. 2
      src/pages/secondary/PostSettingsPage/index.tsx
  24. 206
      src/pages/secondary/ProfileEditorPage/index.tsx
  25. 28
      src/pages/secondary/ProfilePage/index.tsx
  26. 1
      src/pages/secondary/RelayPage/index.tsx
  27. 2
      src/pages/secondary/RelaySettingsPage/index.tsx
  28. 2
      src/pages/secondary/TranslationPage/index.tsx
  29. 2
      src/pages/secondary/WalletPage/index.tsx
  30. 3
      tailwind.config.js

2
index.html

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

61
src/PageManager.tsx

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

2
src/components/BottomNavigationBar/index.tsx

@ -8,7 +8,7 @@ export default function BottomNavigationBar() {
return ( return (
<div <div
className={cn( 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={{ style={{
height: 'calc(3rem + env(safe-area-inset-bottom))', height: 'calc(3rem + env(safe-area-inset-bottom))',

4
src/components/KindFilter/index.tsx

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

22
src/components/NewNotesButton/index.tsx

@ -2,6 +2,7 @@ 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 { ArrowUp } from 'lucide-react'
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'
@ -32,34 +33,25 @@ export default function NewNotesButton({
<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-6'
)} )}
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 py-2 pl-2 pr-3 hover:bg-primary-hover pointer-events-auto"
> >
{pubkeys.length > 0 && ( {pubkeys.length > 0 && (
<div className="flex items-center"> <div className="*:data-[slot=avatar]:ring-background flex -space-x-2 *:data-[slot=avatar]:ring-2 *:data-[slot=avatar]:grayscale">
{pubkeys.map((pubkey, index) => ( {pubkeys.map((pubkey) => (
<div <SimpleUserAvatar userId={pubkey} size="small" />
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> </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>
<ArrowUp />
</Button> </Button>
</div> </div>
)} )}

4
src/components/NoteList/index.tsx

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

2
src/components/ProfileList/index.tsx

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

4
src/components/Sidebar/SidebarItem.tsx

@ -12,8 +12,8 @@ const SidebarItem = forwardRef<
return ( return (
<Button <Button
className={cn( 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', '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', active && 'text-primary hover:text-primary bg-primary/10 hover:bg-primary/10',
className className
)} )}
variant="ghost" variant="ghost"

2
src/components/Sidebar/index.tsx

@ -14,7 +14,7 @@ export default function PrimaryPageSidebar() {
if (isSmallScreen) return null if (isSmallScreen) return null
return ( 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="space-y-2">
<div className="px-3 xl:px-4 mb-6 w-full"> <div className="px-3 xl:px-4 mb-6 w-full">
<Icon className="xl:hidden" /> <Icon className="xl:hidden" />

2
src/components/Tabs/index.tsx

@ -87,7 +87,7 @@ export default function Tabs({
<div <div
ref={containerRef} ref={containerRef}
className={cn( 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)]' : '' deepBrowsing && lastScrollTop > threshold ? '-translate-y-[calc(100%+12rem)]' : ''
)} )}
> >

5
src/components/Titlebar/index.tsx

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

22
src/index.css

@ -50,6 +50,14 @@
pointer-events: none; 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) { @media (hover: hover) and (pointer: fine) {
.clickable:hover { .clickable:hover {
background-color: hsl(var(--muted) / 0.5); background-color: hsl(var(--muted) / 0.5);
@ -70,6 +78,7 @@
} }
:root { :root {
--surface-background: 0 0% 98%;
--background: 0 0% 100%; --background: 0 0% 100%;
--foreground: 240 10% 3.9%; --foreground: 240 10% 3.9%;
--card: 0 0% 100%; --card: 0 0% 100%;
@ -79,11 +88,11 @@
--primary: 259 43% 56%; --primary: 259 43% 56%;
--primary-hover: 259 43% 65%; --primary-hover: 259 43% 65%;
--primary-foreground: 0 0% 98%; --primary-foreground: 0 0% 98%;
--secondary: 240 4.8% 95.9%; --secondary: 240 4.8% 94%;
--secondary-foreground: 240 5.9% 10%; --secondary-foreground: 240 5.9% 10%;
--muted: 240 4.8% 95.9%; --muted: 240 4.8% 94%;
--muted-foreground: 240 3.8% 46.1%; --muted-foreground: 240 3.8% 46.1%;
--accent: 240 4.8% 95.9%; --accent: 240 4.8% 94%;
--accent-foreground: 240 5.9% 10%; --accent-foreground: 240 5.9% 10%;
--destructive: 0 84.2% 60.2%; --destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%; --destructive-foreground: 0 0% 98%;
@ -98,11 +107,12 @@
--radius: 0.5rem; --radius: 0.5rem;
} }
.dark { .dark {
--background: 240 10% 3.9%; --surface-background: 240 10% 3.9%;
--background: 0 0% 9%;
--foreground: 0 0% 98%; --foreground: 0 0% 98%;
--card: 240 10% 3.9%; --card: 0 0% 9%;
--card-foreground: 0 0% 98%; --card-foreground: 0 0% 98%;
--popover: 240 10% 3.9%; --popover: 0 0% 9%;
--popover-foreground: 0 0% 98%; --popover-foreground: 0 0% 98%;
--primary: 259 43% 56%; --primary: 259 43% 56%;
--primary-hover: 259 43% 65%; --primary-hover: 259 43% 65%;

26
src/layouts/PrimaryPageLayout/index.tsx

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

17
src/layouts/SecondaryPageLayout/index.tsx

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

14
src/pages/primary/MePage/index.tsx

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

6
src/pages/primary/NoteListPage/FeedButton.tsx

@ -34,7 +34,11 @@ export default function FeedButton({ className }: { className?: string }) {
<PopoverTrigger asChild> <PopoverTrigger asChild>
<FeedSwitcherTrigger className={className} /> <FeedSwitcherTrigger className={className} />
</PopoverTrigger> </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)} /> <FeedSwitcher close={() => setOpen(false)} />
</PopoverContent> </PopoverContent>
</Popover> </Popover>

2
src/pages/primary/NoteListPage/index.tsx

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

2
src/pages/secondary/GeneralSettingsPage/index.tsx

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

5
src/pages/secondary/HomePage/index.tsx

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

14
src/pages/secondary/LoadingPage/index.tsx

@ -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

14
src/pages/secondary/NotePage/index.tsx

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

2
src/pages/secondary/OthersRelaySettingsPage/index.tsx

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

2
src/pages/secondary/PostSettingsPage/index.tsx

@ -8,7 +8,7 @@ const PostSettingsPage = forwardRef(({ index }: { index?: number }, ref) => {
return ( return (
<SecondaryPageLayout ref={ref} index={index} title={t('Post settings')}> <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 /> <MediaUploadServiceSetting />
</div> </div>
</SecondaryPageLayout> </SecondaryPageLayout>

206
src/pages/secondary/ProfileEditorPage/index.tsx

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

28
src/pages/secondary/ProfilePage/index.tsx

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

1
src/pages/secondary/RelayPage/index.tsx

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

2
src/pages/secondary/RelaySettingsPage/index.tsx

@ -22,7 +22,7 @@ const RelaySettingsPage = forwardRef(({ index }: { index?: number }, ref) => {
return ( return (
<SecondaryPageLayout ref={ref} index={index} title={t('Relay settings')}> <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> <TabsList>
<TabsTrigger value="favorite-relays">{t('Favorite Relays')}</TabsTrigger> <TabsTrigger value="favorite-relays">{t('Favorite Relays')}</TabsTrigger>
<TabsTrigger value="mailbox">{t('Read & Write Relays')}</TabsTrigger> <TabsTrigger value="mailbox">{t('Read & Write Relays')}</TabsTrigger>

2
src/pages/secondary/TranslationPage/index.tsx

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

2
src/pages/secondary/WalletPage/index.tsx

@ -12,7 +12,7 @@ const WalletPage = forwardRef(({ index }: { index?: number }, ref) => {
return ( return (
<SecondaryPageLayout ref={ref} index={index} title={t('Wallet')}> <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 /> <BcButton />
<LightningAddressInput /> <LightningAddressInput />
<DefaultZapAmountInput /> <DefaultZapAmountInput />

3
tailwind.config.js

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

Loading…
Cancel
Save