enhance navigation

This commit is contained in:
keyan 2024-02-23 09:32:20 -06:00
parent b4cef44a43
commit f2ba61e64b
38 changed files with 913 additions and 103 deletions

View File

@ -82,7 +82,7 @@ export function getGetServerSideProps (
const callback = process.env.PUBLIC_URL + req.url
return {
redirect: {
destination: `/login?callbackUrl=${encodeURIComponent(callback)}`
destination: `/signup?callbackUrl=${encodeURIComponent(callback)}`
}
}
}
@ -111,9 +111,9 @@ export function getGetServerSideProps (
}
if (error || !data || (notFound && notFound(data, vars, me))) {
return {
notFound: true
}
res.writeHead(301, {
Location: '/404'
}).end()
}
props = {

View File

@ -159,7 +159,7 @@ function AnonInfo () {
return (
<AnonIcon
className='fill-muted ms-2 theme' height={22} width={22}
className='ms-2 fill-theme-color' height={22} width={22}
onClick={
(e) =>
showModal(onClose =>

View File

@ -10,7 +10,7 @@ export default function Hat ({ user, badge, className = 'ms-1', height = 16, wid
if (!user || Number(user.id) === AD_USER_ID) return null
if (Number(user.id) === ANON_USER_ID) {
return (
<HatTooltip overlayText={badge ? 'anonymous' : 'posted anonymously'}>
<HatTooltip overlayText='anonymous'>
{badge
? (
<Badge bg='grey-medium' className='ms-2 d-inline-flex align-items-center'>

View File

@ -1,12 +1,11 @@
.brand {
font-family: lightning;
font-size: 2rem;
padding: 0;
line-height: 100%;
margin-bottom: -.3rem;
margin-right: 0;
text-shadow: 0 0 10px var(--bs-primary);
color: var(--theme-brandColor) !important;
}
.brand svg {
fill: var(--theme-brandColor);
filter: drop-shadow(0 0 6px var(--bs-primary));
}
.postAnon {
@ -44,6 +43,10 @@
fill: var(--theme-navLinkActive);
}
.hide {
display: none;
}
.dropdown svg {
vertical-align: text-top;
}
@ -79,6 +82,7 @@
display: flex;
flex-grow: 1;
justify-content: center;
align-items: center;
}
.price > div {

View File

@ -1,5 +1,4 @@
.title {
font-weight: 500;
white-space: normal;
max-width: 100%;
display: inline-block;

View File

@ -1,4 +1,6 @@
import Header, { HeaderStatic } from './header'
import Navigation from './navigation'
import NavFooter from './navigation/mobile/footer'
import NavStatic from './navigation/static'
import Container from 'react-bootstrap/Container'
import Footer from './footer'
import Seo, { SeoSearch } from './seo'
@ -12,51 +14,53 @@ export default function Layout ({
return (
<>
{seo && <Seo sub={sub} item={item} user={user} />}
<Header sub={sub} />
<Navigation sub={sub} />
{contain
? (
<Container as='main' className={`px-sm-0 ${containClassName}`}>
<Container as='main' className={`px-sm-0 ${styles.contain}`}>
{children}
</Container>
)
: children}
{footer && <Footer links={footerLinks} />}
<NavFooter sub={sub} />
</>
)
}
export function SearchLayout ({ sub, children, ...props }) {
return (
<Layout sub={sub} seo={false} contain={false} {...props}>
<Layout sub={sub} seo={false} footer={false} {...props}>
<SeoSearch sub={sub} />
<Search sub={sub} />
<Container as='main' className='pt-3 pb-4 px-sm-0'>
{children}
</Container>
{children}
</Layout>
)
}
export function StaticLayout ({ children, footer = true, footerLinks, ...props }) {
export function StaticLayout ({ children, footer = true, footerLinks = false, ...props }) {
return (
<div className={styles.page}>
<HeaderStatic />
<main className={`${styles.content} pt-5`}>
{children}
</main>
<>
<NavStatic />
<div className={styles.page}>
<main className={`${styles.content} ${styles.contain} py-3`}>
{children}
</main>
</div>
{footer && <Footer links={footerLinks} />}
</div>
<NavFooter />
</>
)
}
export function CenterLayout ({ children, ...props }) {
return (
<div className={styles.page}>
<Layout contain={false} {...props}>
<Layout contain={false} footer={false} {...props}>
<div className={styles.page}>
<main className={styles.content}>
{children}
</main>
</Layout>
</div>
</div>
</Layout>
)
}

View File

@ -3,8 +3,8 @@
display: flex;
flex-flow: column;
height: 100%;
min-height: 100vh;
align-items: center;
margin: auto;
}
.content {
@ -16,7 +16,13 @@
width: 100%;
padding-right: 15px;
padding-left: 15px;
margin-top: 1.5rem;
margin: 1.5rem 0;
flex-direction: column;
}
.contain {
flex-grow: 1;
display: flex;
flex-direction: column;
}

View File

@ -0,0 +1,382 @@
import Link from 'next/link'
import { Button, Dropdown, Nav, Navbar } from 'react-bootstrap'
import styles from '../header.module.css'
import { useRouter } from 'next/router'
import BackArrow from '../../svgs/arrow-left-line.svg'
import { useCallback, useEffect, useState } from 'react'
import Price from '../price'
import SubSelect from '../sub-select'
import { useQuery } from '@apollo/client'
import { HAS_NOTIFICATIONS } from '../../fragments/notifications'
import { ANON_USER_ID, BALANCE_LIMIT_MSATS, SSR } from '../../lib/constants'
import { clearNotifications } from '../../lib/badge'
import Head from 'next/head'
import NoteIcon from '../../svgs/notification-4-fill.svg'
import { useMe } from '../me'
import HiddenWalletSummary from '../hidden-wallet-summary'
import { abbrNum, msatsToSats } from '../../lib/format'
import { useServiceWorker } from '../serviceworker'
import { signOut } from 'next-auth/react'
import Hat from '../hat'
import { randInRange } from '../../lib/rand'
import { useLightning } from '../lightning'
import LightningIcon from '../../svgs/bolt.svg'
import SearchIcon from '../../svgs/search-line.svg'
import classNames from 'classnames'
import SnIcon from '@/svgs/sn.svg'
export function Brand ({ className }) {
return (
<Link href='/' passHref legacyBehavior>
<Navbar.Brand className={classNames(styles.brand, className)}>
<SnIcon width={36} height={36} />
</Navbar.Brand>
</Link>
)
}
export function noNavSelect ({ path, pathname }) {
return (
path.startsWith('/items') ||
path.endsWith('/post') ||
path.endsWith('/edit') ||
path.endsWith('/notifications') ||
path.endsWith('/search') ||
path.startsWith('/wallet') ||
path.startsWith('/rewards') ||
path.startsWith('/referrals') ||
path.startsWith('/live') ||
path.startsWith('/settings') ||
path.startsWith('/referrals') ||
path.startsWith('/invites') ||
path.startsWith('/stackers') ||
path.startsWith('/satistics') ||
path.startsWith('/withdrawals') ||
path.startsWith('/invoices') ||
pathname.startsWith('/[name]') ||
path.startsWith('/territory')
)
}
export function Back () {
const router = useRouter()
const [back, setBack] = useState(router.asPath !== '/')
useEffect(() => {
setBack(router.asPath !== '/' && (typeof window.navigation === 'undefined' || window.navigation.canGoBack === undefined || window?.navigation.canGoBack))
}, [router.asPath])
if (!back) return null
return (
<a
role='button' tabIndex='0' className='nav-link p-0 me-3' onClick={() => {
if (back) {
router.back()
} else {
router.push('/')
}
}}
>
<BackArrow className='theme me-1 me-md-2' width={24} height={24} />
</a>
)
}
export function BackOrBrand ({ className }) {
const router = useRouter()
const [back, setBack] = useState(router.asPath !== '/')
useEffect(() => {
setBack(router.asPath !== '/' && (typeof window.navigation === 'undefined' || window.navigation.canGoBack === undefined || window?.navigation.canGoBack))
}, [router.asPath])
return (
<div className='d-flex align-items-center'>
{back ? <Back /> : <Brand className={className} />}
</div>
)
}
export function SearchItem ({ prefix, className }) {
return (
<Link href={prefix + '/search'} passHref legacyBehavior>
<Nav.Link eventKey='search' className={className}>
<SearchIcon className='theme' width={22} height={28} />
</Nav.Link>
</Link>
)
}
export function NavPrice ({ className }) {
return (
<Nav.Item className={classNames(styles.price, className)}>
<Price className='nav-link text-monospace' />
</Nav.Item>
)
}
const PREPEND_SUBS = ['home']
const APPEND_SUBS = [{ label: '--------', items: ['create'] }]
export function NavSelect ({ sub: subName, className, size }) {
const sub = subName || 'home'
return (
<Nav.Item className={className}>
<SubSelect
sub={sub} prependSubs={PREPEND_SUBS} appendSubs={APPEND_SUBS} noForm
groupClassName='mb-0' size={size}
/>
</Nav.Item>
)
}
export function NavNotifications ({ className }) {
const { data } = useQuery(HAS_NOTIFICATIONS, SSR
? {}
: {
pollInterval: 30000,
nextFetchPolicy: 'cache-and-network',
onCompleted: ({ hasNewNotes }) => {
if (!hasNewNotes) {
clearNotifications()
}
}
})
return (
<>
<Head>
<link rel='shortcut icon' href={data?.hasNewNotes ? '/favicon-notify.png' : '/favicon.png'} />
</Head>
<Link href='/notifications' passHref legacyBehavior>
<Nav.Link eventKey='notifications' className={classNames('position-relative', className)}>
<NoteIcon height={28} width={20} className='theme' />
{data?.hasNewNotes &&
<span className={styles.notification}>
<span className='invisible'>{' '}</span>
</span>}
</Nav.Link>
</Link>
</>
)
}
export function WalletSummary () {
const me = useMe()
if (!me) return null
if (me.privates?.hideWalletBalance) {
return <HiddenWalletSummary abbreviate fixedWidth />
}
return `${abbrNum(me.privates?.sats)}`
}
export function NavWalletSummary () {
const me = useMe()
const walletLimitReached = me?.privates?.sats >= msatsToSats(BALANCE_LIMIT_MSATS)
return (
<Nav.Item>
<Link href='/wallet' passHref legacyBehavior>
<Nav.Link eventKey='wallet' className={`${walletLimitReached ? 'text-warning' : 'text-success'} text-monospace px-0 text-nowrap`}>
<WalletSummary me={me} />
</Nav.Link>
</Link>
</Nav.Item>
)
}
export function MeDropdown ({ me, dropNavKey }) {
if (!me) return null
const { registration: swRegistration, togglePushSubscription } = useServiceWorker()
return (
<div className='position-relative'>
<Dropdown className={styles.dropdown} align='end'>
<Dropdown.Toggle className='nav-link nav-item fw-normal' id='profile' variant='custom'>
<Nav.Link eventKey={me.name} as='span' className='p-0'>
{`@${me.name}`}<Hat user={me} />
</Nav.Link>
</Dropdown.Toggle>
<Dropdown.Menu>
<Link href={'/' + me.name} passHref legacyBehavior>
<Dropdown.Item active={me.name === dropNavKey}>
profile
{me && !me.bioId &&
<div className='p-1 d-inline-block bg-secondary ms-1'>
<span className='invisible'>{' '}</span>
</div>}
</Dropdown.Item>
</Link>
<Link href={'/' + me.name + '/bookmarks'} passHref legacyBehavior>
<Dropdown.Item active={me.name + '/bookmarks' === dropNavKey}>bookmarks</Dropdown.Item>
</Link>
<Link href='/wallet' passHref legacyBehavior>
<Dropdown.Item eventKey='wallet'>wallet</Dropdown.Item>
</Link>
<Link href='/satistics?inc=invoice,withdrawal,stacked,spent' passHref legacyBehavior>
<Dropdown.Item eventKey='satistics'>satistics</Dropdown.Item>
</Link>
<Dropdown.Divider />
<Link href='/referrals/month' passHref legacyBehavior>
<Dropdown.Item eventKey='referrals'>referrals</Dropdown.Item>
</Link>
<Dropdown.Divider />
<div className='d-flex align-items-center'>
<Link href='/settings' passHref legacyBehavior>
<Dropdown.Item eventKey='settings'>settings</Dropdown.Item>
</Link>
</div>
<Dropdown.Divider />
<Dropdown.Item
onClick={async () => {
try {
// order is important because we need to be logged in to delete push subscription on server
const pushSubscription = await swRegistration?.pushManager.getSubscription()
if (pushSubscription) {
await togglePushSubscription()
}
} catch (err) {
// don't prevent signout because of an unsubscription error
console.error(err)
}
await signOut({ callbackUrl: '/' })
}}
>logout
</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
{!me.bioId &&
<span className='position-absolute p-1 bg-secondary' style={{ top: '5px', right: '0px' }}>
<span className='invisible'>{' '}</span>
</span>}
</div>
)
}
export function SignUpButton ({ handleLogin, className = 'py-0' }) {
return (
<Button
className={classNames('align-items-center ps-2 pe-3', className)}
style={{ borderWidth: '2px' }}
id='login'
onClick={() => handleLogin('/signup')}
>
<LightningIcon
width={17}
height={17}
className='me-1'
/>sign up
</Button>
)
}
export function LoginButtons () {
const router = useRouter()
const handleLogin = useCallback(async pathname => await router.push({
pathname,
query: { callbackUrl: window.location.origin + router.asPath }
}), [router])
return (
<>
<Button
className='align-items-center px-3 py-1 mb-2'
id='signup'
style={{ borderWidth: '2px' }}
variant='outline-grey-darkmode'
onClick={() => handleLogin('/login')}
>
login
</Button>
<SignUpButton handleLogin={handleLogin} className='py-1' />
</>
)
}
export function AnonDropdown ({ path }) {
const strike = useLightning()
useEffect(() => {
if (!window.localStorage.getItem('striked')) {
const to = setTimeout(() => {
strike()
window.localStorage.setItem('striked', 'yep')
}, randInRange(3000, 10000))
return () => clearTimeout(to)
}
}, [])
return (
<div className='position-relative'>
<Dropdown className={styles.dropdown} align='end'>
<Dropdown.Toggle className='nav-link nav-item' id='profile' variant='custom'>
<Nav.Link eventKey='anon' as='span' className='p-0 fw-normal'>
@anon<Hat user={{ id: ANON_USER_ID }} />
</Nav.Link>
</Dropdown.Toggle>
<Dropdown.Menu className='p-3'>
<LoginButtons />
</Dropdown.Menu>
</Dropdown>
</div>
)
}
export function Sorts ({ sub, prefix, className }) {
return (
<>
<Nav.Item className={className}>
<Link href={prefix + '/'} passHref legacyBehavior>
<Nav.Link eventKey='' className={styles.navLink}>hot</Nav.Link>
</Link>
</Nav.Item>
<Nav.Item className={className}>
<Link href={prefix + '/recent'} passHref legacyBehavior>
<Nav.Link eventKey='recent' className={styles.navLink}>recent</Nav.Link>
</Link>
</Nav.Item>
{/* <Nav.Item className={className}>
<Link href={prefix + '/random'} passHref legacyBehavior>
<Nav.Link eventKey='random' className={styles.navLink}>random</Nav.Link>
</Link>
</Nav.Item> */}
{sub !== 'jobs' &&
<Nav.Item className={className}>
<Link
href={{
pathname: '/~/top/[type]/[when]',
query: { type: 'posts', when: 'day', sub }
}} as={prefix + '/top/posts/day'} passHref legacyBehavior
>
<Nav.Link eventKey='top' className={styles.navLink}>top</Nav.Link>
</Link>
</Nav.Item>}
</>
)
}
export function PostItem ({ className, prefix }) {
return (
<Link href={prefix + '/post'} className={`${className} btn btn-md btn-primary py-md-1`}>
post
</Link>
)
}
export function MeCorner ({ dropNavKey, me, className }) {
return (
<div className={className}>
<NavNotifications />
<MeDropdown me={me} dropNavKey={dropNavKey} />
<NavWalletSummary />
</div>
)
}
export function AnonCorner ({ dropNavKey, className }) {
return (
<div className={className}>
<AnonDropdown dropNavKey={dropNavKey} />
</div>
)
}

View File

@ -0,0 +1,14 @@
import { Container } from 'react-bootstrap'
import TopBar from './top-bar'
import SecondBar from './second-bar'
export default function Header (props) {
return (
<div className='d-none d-md-block'>
<Container as='header' className='px-0'>
<TopBar {...props} />
<SecondBar {...props} />
</Container>
</div>
)
}

View File

@ -0,0 +1,20 @@
import { Nav, Navbar } from 'react-bootstrap'
import { NavSelect, PostItem, Sorts, noNavSelect } from '../common'
import styles from '../../header.module.css'
export default function SecondBar (props) {
const { prefix, topNavKey, sub } = props
if (noNavSelect(props)) return null
return (
<Navbar className='pt-0 pb-3'>
<Nav
className={styles.navbarNav}
activeKey={topNavKey}
>
<NavSelect sub={sub} size='medium' className='me-1' />
<Sorts {...props} />
<PostItem className='ms-auto me-0 d-none d-md-flex' prefix={prefix} />
</Nav>
</Navbar>
)
}

View File

@ -0,0 +1,24 @@
import { Nav, Navbar } from 'react-bootstrap'
import styles from '../../header.module.css'
import { AnonCorner, Back, Brand, MeCorner, NavPrice, SearchItem } from '../common'
import { useMe } from '../../me'
export default function TopBar ({ prefix, sub, path, topNavKey, dropNavKey }) {
const me = useMe()
return (
<Navbar>
<Nav
className={styles.navbarNav}
activeKey={topNavKey}
>
<Back />
<Brand className='me-1' />
<SearchItem prefix={prefix} className='me-0 ms-0 ms-md-auto d-none d-md-flex' />
<NavPrice className='ms-auto me-0 mx-md-auto d-none d-md-flex' />
{me
? <MeCorner dropNavKey={dropNavKey} me={me} className='d-none d-md-flex' />
: <AnonCorner path={path} className='d-none d-md-flex' />}
</Nav>
</Navbar>
)
}

View File

@ -0,0 +1,25 @@
import { useRouter } from 'next/router'
import DesktopHeader from './desktop/header'
import MobileHeader from './mobile/header'
import StickyBar from './sticky-bar'
export default function Navigation ({ sub }) {
const router = useRouter()
const path = router.asPath.split('?')[0]
const props = {
prefix: sub ? `/~${sub}` : '',
path,
pathname: router.pathname,
topNavKey: path.split('/')[sub ? 2 : 1] ?? '',
dropNavKey: path.split('/').slice(sub ? 2 : 1).join('/'),
sub
}
return (
<>
<DesktopHeader {...props} />
<MobileHeader {...props} />
<StickyBar {...props} />
</>
)
}

View File

@ -0,0 +1,34 @@
import { Nav, Navbar } from 'react-bootstrap'
import { Brand, NavNotifications, PostItem, SearchItem } from '../common'
import { useMe } from '../../me'
import styles from './footer.module.css'
import classNames from 'classnames'
import Offcanvas from './offcanvas'
import { useRouter } from 'next/router'
export default function BottomBar ({ sub }) {
const router = useRouter()
const path = router.asPath.split('?')[0]
const props = {
prefix: sub ? `/~${sub}` : '',
path,
topNavKey: path.split('/')[sub ? 2 : 1] ?? '',
dropNavKey: path.split('/').slice(sub ? 2 : 1).join('/'),
sub
}
const me = useMe()
return (
<div className={classNames('d-block d-md-none', styles.footer)}>
<Navbar className='container px-0'>
<Nav className={styles.footerNav}>
<Offcanvas me={me} {...props} />
<SearchItem {...props} />
<Brand />
<PostItem {...props} className='btn-sm' />
<NavNotifications />
</Nav>
</Navbar>
</div>
)
}

View File

@ -0,0 +1,17 @@
.footer {
position: sticky;
bottom: 0;
width: 100%;
background-color: var(--bs-body-bg);
border-top: 1px solid var(--theme-toolbarActive);
z-index: 1000;
}
.footerNav {
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr 1fr;
justify-items: center;
align-items: center;
width: 100%;
}

View File

@ -0,0 +1,14 @@
import { Container } from 'react-bootstrap'
import TopBar from './top-bar'
import SecondBar from './second-bar'
export default function Header (props) {
return (
<div className='d-block d-md-none'>
<Container as='header' className='px-sm-0'>
<TopBar {...props} />
<SecondBar {...props} />
</Container>
</div>
)
}

View File

@ -0,0 +1,108 @@
import { useState } from 'react'
import { Dropdown, Image, Offcanvas } from 'react-bootstrap'
import { MEDIA_URL } from '@/lib/constants'
import Link from 'next/link'
import { useServiceWorker } from '@/components/serviceworker'
import { signOut } from 'next-auth/react'
import { LoginButtons, NavWalletSummary } from '../common'
import AnonIcon from '@/svgs/spy-fill.svg'
export default function OffCanvas ({ me, dropNavKey }) {
const [show, setShow] = useState(false)
const handleClose = () => setShow(false)
const handleShow = () => setShow(true)
const { registration: swRegistration, togglePushSubscription } = useServiceWorker()
const MeImage = ({ onClick }) => me
? (
<Image
src={me?.photoId ? `${MEDIA_URL}/${me.photoId}` : '/dorian400.jpg'} width='28' height='28'
style={{ clipPath: 'polygon(0 0, 83% 0, 100% 100%, 17% 100%)' }}
onClick={onClick}
/>
)
: <span className='text-muted'><AnonIcon onClick={onClick} width='22' height='22' /></span>
return (
<>
<MeImage onClick={handleShow} />
<Offcanvas style={{ maxWidth: '250px', zIndex: '10000' }} show={show} onHide={handleClose} placement='start'>
<Offcanvas.Header closeButton>
<Offcanvas.Title><NavWalletSummary /></Offcanvas.Title>
</Offcanvas.Header>
<Offcanvas.Body>
<div style={{
'--bs-dropdown-item-padding-y': '.5rem',
'--bs-dropdown-item-padding-x': 0,
'--bs-dropdown-divider-bg': '#ced4da',
'--bs-dropdown-divider-margin-y': '0.5rem',
height: '100%',
display: 'flex',
flexDirection: 'column'
}}
>
{me
? (
<>
<Link href={'/' + me.name} passHref legacyBehavior>
<Dropdown.Item active={me.name === dropNavKey}>
profile
{me && !me.bioId &&
<div className='p-1 d-inline-block bg-secondary ms-1'>
<span className='invisible'>{' '}</span>
</div>}
</Dropdown.Item>
</Link>
<Link href={'/' + me.name + '/bookmarks'} passHref legacyBehavior>
<Dropdown.Item active={me.name + '/bookmarks' === dropNavKey}>bookmarks</Dropdown.Item>
</Link>
<Link href='/wallet' passHref legacyBehavior>
<Dropdown.Item eventKey='wallet'>wallet</Dropdown.Item>
</Link>
<Link href='/satistics?inc=invoice,withdrawal,stacked,spent' passHref legacyBehavior>
<Dropdown.Item eventKey='satistics'>satistics</Dropdown.Item>
</Link>
<Dropdown.Divider />
<Link href='/referrals/month' passHref legacyBehavior>
<Dropdown.Item eventKey='referrals'>referrals</Dropdown.Item>
</Link>
<Dropdown.Divider />
<div className='d-flex align-items-center'>
<Link href='/settings' passHref legacyBehavior>
<Dropdown.Item eventKey='settings'>settings</Dropdown.Item>
</Link>
</div>
<Dropdown.Divider />
<Dropdown.Item
onClick={async () => {
try {
// order is important because we need to be logged in to delete push subscription on server
const pushSubscription = await swRegistration?.pushManager.getSubscription()
if (pushSubscription) {
await togglePushSubscription()
}
} catch (err) {
// don't prevent signout because of an unsubscription error
console.error(err)
}
await signOut({ callbackUrl: '/' })
}}
>logout
</Dropdown.Item>
</>
)
: <LoginButtons />}
<Link href={`/${me?.name || 'anon'}`} className='d-flex flex-row p-2 mt-auto text-muted'>
<MeImage />
<div className='ms-2'>
@{me?.name || 'anon'}
</div>
</Link>
</div>
</Offcanvas.Body>
</Offcanvas>
</>
)
}

View File

@ -0,0 +1,19 @@
import { Nav, Navbar } from 'react-bootstrap'
import { NavPrice, Sorts, noNavSelect } from '../common'
import styles from '../../header.module.css'
export default function SecondBar (props) {
const { topNavKey } = props
if (noNavSelect(props)) return null
return (
<Navbar>
<Nav
className={styles.navbarNav}
activeKey={topNavKey}
>
<Sorts {...props} />
<NavPrice className='justify-content-end' />
</Nav>
</Navbar>
)
}

View File

@ -0,0 +1,25 @@
import { Nav, Navbar } from 'react-bootstrap'
import styles from '../../header.module.css'
import { Back, NavPrice, NavSelect, NavWalletSummary, SignUpButton, noNavSelect } from '../common'
import { useMe } from '@/components/me'
export default function TopBar ({ prefix, sub, path, pathname, topNavKey, dropNavKey }) {
const me = useMe()
return (
<Navbar>
<Nav
className={styles.navbarNav}
activeKey={topNavKey}
>
<Back className='d-flex d-md-none' />
{noNavSelect({ path, pathname })
? (
<>
<NavPrice className='flex-shrink-1' />
{me ? <NavWalletSummary /> : <SignUpButton />}
</>)
: <NavSelect sub={sub} className='w-100' />}
</Nav>
</Navbar>
)
}

View File

@ -0,0 +1,19 @@
import { Container, Nav, Navbar } from 'react-bootstrap'
import styles from '../header.module.css'
import { BackOrBrand, NavPrice, SearchItem } from './common'
export default function StaticHeader () {
return (
<Container as='header' className='px-sm-0'>
<Navbar>
<Nav
className={styles.navbarNav}
>
<BackOrBrand />
<SearchItem />
<NavPrice className='justify-content-end' />
</Nav>
</Navbar>
</Container>
)
}

View File

@ -0,0 +1,59 @@
import { useEffect, useRef } from 'react'
import styles from '@/components/header.module.css'
import { Container, Nav, Navbar } from 'react-bootstrap'
import { NavPrice, MeCorner, AnonCorner, SearchItem, Back, NavWalletSummary, Brand, SignUpButton } from './common'
import { useMe } from '@/components/me'
import classNames from 'classnames'
export default function StickyBar ({ prefix, sub, path, topNavKey, dropNavKey }) {
const ref = useRef()
const sticky = useRef()
const me = useMe()
useEffect(() => {
const observer = new window.IntersectionObserver(([entry]) => {
sticky?.current?.classList.toggle(styles.hide, entry.isIntersecting)
})
ref?.current && observer.observe(ref.current)
return () => {
ref?.current && observer.unobserve(ref.current)
}
}, [ref?.current, sticky?.current])
if (path.endsWith('/search')) return null
return (
<>
<div ref={ref} style={{ position: 'relative', top: '50px' }} />
<div className={styles.hide} style={{ position: 'sticky', borderBottom: '1px solid var(--theme-toolbarActive)', top: '0', backgroundColor: 'var(--bs-body-bg)', zIndex: 1000 }} ref={sticky}>
<Container className='px-0 d-none d-md-block'>
<Navbar className='py-0'>
<Nav
className={styles.navbarNav}
activeKey={topNavKey}
>
<Back />
<Brand className='me-1' />
<SearchItem prefix={prefix} />
<NavPrice />
{me ? <MeCorner dropNavKey={dropNavKey} me={me} className='d-flex' /> : <AnonCorner path={path} className='d-flex' />}
</Nav>
</Navbar>
</Container>
<Container className='px-sm-0 d-block d-md-none'>
<Navbar className='py-0'>
<Nav
className={classNames(styles.navbarNav, 'justify-content-between')}
activeKey={topNavKey}
>
<Back />
<NavPrice className='flex-shrink-1 flex-grow-0' />
{me ? <NavWalletSummary /> : <SignUpButton />}
</Nav>
</Navbar>
</Container>
</div>
</>
)
}

View File

@ -106,9 +106,9 @@ export function PostForm ({ type, sub, children }) {
</Alert>}
<SubSelect
prependSubs={['pick territory']}
className='w-auto d-flex'
className='d-flex'
noForm
large
size='medium'
sub={sub?.name}
info={sub && <TerritoryInfo sub={sub} />}
hint={sub?.moderated && 'this territory is moderated'}
@ -173,8 +173,8 @@ export default function Post ({ sub }) {
sub={sub?.name}
prependSubs={sub?.name ? undefined : ['pick territory']}
filterSubs={s => s.postTypes?.includes(type.toUpperCase())}
className='w-auto d-flex'
large
className='d-flex'
size='medium'
label='territory'
info={sub && <TerritoryInfo sub={sub} />}
hint={sub?.moderated && 'this territory is moderated'}

View File

@ -54,12 +54,12 @@ export default function Search ({ sub }) {
return (
<>
<div className={styles.searchSection}>
<Container className={`px-md-0 ${styles.searchContainer}`}>
<Container className={`px-0 ${styles.searchContainer}`}>
<Form
initial={{ q, what, sort, when, from: '', to: '' }}
onSubmit={values => search({ ...values })}
>
<div className={`${styles.active} my-3`}>
<div className={`${styles.active} mb-3`}>
<Input
name='q'
required

View File

@ -1,7 +1,7 @@
.searchSection {
position: sticky;
top: 0;
box-shadow: 0 4px 12px -4px hsl(0deg 0% 59% / 10%);
border-bottom: 1px solid var(--theme-toolbarActive);
background-color: var(--bs-body-bg);
z-index: 1;
}

View File

@ -42,7 +42,7 @@ export function useSubs ({ prependSubs = [], sub, filterSubs = () => true, appen
return subs
}
export default function SubSelect ({ prependSubs, sub, onChange, large, appendSubs, filterSubs, className, ...props }) {
export default function SubSelect ({ prependSubs, sub, onChange, size, appendSubs, filterSubs, className, ...props }) {
const router = useRouter()
const subs = useSubs({ prependSubs, sub, filterSubs, appendSubs })
const valueProps = props.noForm
@ -105,7 +105,7 @@ export default function SubSelect ({ prependSubs, sub, onChange, large, appendSu
size='sm'
{...valueProps}
{...props}
className={`${className} ${styles.subSelect} ${large ? 'me-2' : styles.subSelectSmall}`}
className={`${className} ${styles.subSelect} ${size === 'large' ? styles.subSelectLarge : size === 'medium' ? styles.subSelectMedium : ''}`}
items={subItems}
/>
)

View File

@ -5,5 +5,13 @@
}
.subSelectSmall {
max-width: 90px !important;
width: 90px !important;
}
.subSelectLarge {
width: 300px !important;
}
.subSelectMedium {
width: 200px !important;
}

View File

@ -13,18 +13,19 @@ import { useToast } from './toast'
import ActionDropdown from './action-dropdown'
import { TerritoryTransferDropdownItem } from './territory-transfer'
export function TerritoryDetails ({ sub }) {
export function TerritoryDetails ({ sub, children }) {
return (
<AccordianCard
header={
<small className='text-muted fw-bold align-items-center d-flex'>
territory details
{sub.name}
{sub.status === 'STOPPED' && <Badge className='ms-2' bg='danger'>archived</Badge>}
{(sub.moderated || sub.moderatedCount > 0) && <Badge className='ms-2' bg='secondary'>moderated{sub.moderatedCount > 0 && ` ${sub.moderatedCount}`}</Badge>}
{(sub.nsfw) && <Badge className='ms-2' bg='secondary'>nsfw</Badge>}
</small>
}
>
{children}
<TerritoryInfo sub={sub} />
</AccordianCard>
)
@ -80,43 +81,45 @@ export default function TerritoryHeader ({ sub }) {
<TerritoryPaymentDue sub={sub} />
<div className='mb-3'>
<div>
<TerritoryDetails sub={sub} />
</div>
<div className='d-flex my-2 justify-content-end'>
<Share path={`/~${sub.name}`} title={`~${sub.name} stacker news territory`} className='mx-1' />
{me &&
<>
{(isMine
? (
<Link href={`/~${sub.name}/edit`} className='d-flex align-items-center'>
<Button variant='outline-grey border-2 rounded py-0' size='sm'>edit territory</Button>
</Link>)
: (
<Button
variant='outline-grey border-2 py-0 rounded'
size='sm'
onClick={async () => {
try {
await toggleMuteSub({ variables: { name: sub.name } })
} catch {
toaster.danger(`failed to ${sub.meMuteSub ? 'join' : 'mute'} territory`)
return
}
toaster.success(`${sub.meMuteSub ? 'joined' : 'muted'} territory`)
}}
>{sub.meMuteSub ? 'join' : 'mute'} territory
</Button>)
<TerritoryDetails sub={sub}>
<div className='d-flex my-2 justify-content-end'>
{sub.name}
<Share path={`/~${sub.name}`} title={`~${sub.name} stacker news territory`} className='mx-1' />
{me &&
<>
{(isMine
? (
<Link href={`/~${sub.name}/edit`} className='d-flex align-items-center'>
<Button variant='outline-grey border-2 rounded py-0' size='sm'>edit territory</Button>
</Link>)
: (
<Button
variant='outline-grey border-2 py-0 rounded'
size='sm'
onClick={async () => {
try {
await toggleMuteSub({ variables: { name: sub.name } })
} catch {
toaster.danger(`failed to ${sub.meMuteSub ? 'join' : 'mute'} territory`)
return
}
toaster.success(`${sub.meMuteSub ? 'joined' : 'muted'} territory`)
}}
>{sub.meMuteSub ? 'join' : 'mute'} territory
</Button>)
)}
<ActionDropdown>
<ToggleSubSubscriptionDropdownItem sub={sub} />
{isMine && (
<>
<Dropdown.Divider />
<TerritoryTransferDropdownItem sub={sub} />
</>
)}
</ActionDropdown>
</>}
<ActionDropdown>
<ToggleSubSubscriptionDropdownItem sub={sub} />
{isMine && (
<>
<Dropdown.Divider />
<TerritoryTransferDropdownItem sub={sub} />
</>
)}
</ActionDropdown>
</>}
</div>
</TerritoryDetails>
</div>
</div>
</>

View File

@ -69,7 +69,7 @@ export const config = {
// NextJS recommends to not add the CSP header to prefetches and static assets
// See https://nextjs.org/docs/app/building-your-application/configuring/content-security-policy
{
source: '/((?!api|_next/static|_next/image|_next/webpack-hmr|favicon.ico).*)',
source: '/((?!api|_next/static|_error|404|500|offline|_next/image|_next/webpack-hmr|favicon.ico).*)',
missing: [
{ type: 'header', key: 'next-router-prefetch' },
{ type: 'header', key: 'purpose', value: 'prefetch' }

7
package-lock.json generated
View File

@ -27,6 +27,7 @@
"bolt11": "^1.4.1",
"bootstrap": "^5.3.2",
"canonical-json": "0.0.4",
"classnames": "^2.5.1",
"clipboard-copy": "^4.0.1",
"cross-fetch": "^4.0.0",
"csv-parser": "^3.0.0",
@ -7011,9 +7012,9 @@
"dev": true
},
"node_modules/classnames": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz",
"integrity": "sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw=="
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz",
"integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow=="
},
"node_modules/clean-stack": {
"version": "2.2.0",

View File

@ -32,6 +32,7 @@
"bolt11": "^1.4.1",
"bootstrap": "^5.3.2",
"canonical-json": "0.0.4",
"classnames": "^2.5.1",
"clipboard-copy": "^4.0.1",
"cross-fetch": "^4.0.0",
"csv-parser": "^3.0.0",

View File

@ -75,9 +75,9 @@ export function BioForm ({ handleDone, bio }) {
)
}
export function UserLayout ({ user, children }) {
export function UserLayout ({ user, children, containClassName }) {
return (
<Layout user={user} containClassName={styles.contain}>
<Layout user={user} footer={false} containClassName={containClassName}>
<UserHeader user={user} />
{children}
</Layout>
@ -97,7 +97,7 @@ export default function User ({ ssrData }) {
const mine = me?.name === user.name
return (
<UserLayout user={user}>
<UserLayout user={user} containClassName={!user.bio && mine && styles.contain}>
{user.bio
? (edit
? (

View File

@ -30,9 +30,9 @@ class MyDocument extends Document {
<Html lang='en'>
<Head nonce={nonce}>
<link rel='manifest' href='/api/site.webmanifest' />
<link rel='preload' href={`${process.env.NEXT_PUBLIC_ASSET_PREFIX}/Lightningvolt-xoqm.woff2`} as='font' type='font/woff2' crossOrigin='' />
<link rel='preload' href={`${process.env.NEXT_PUBLIC_ASSET_PREFIX}/Lightningvolt-xoqm.woff`} as='font' type='font/woff' crossOrigin='' />
<link rel='preload' href={`${process.env.NEXT_PUBLIC_ASSET_PREFIX}/Lightningvolt-xoqm.ttf`} as='font' type='font/ttf' crossOrigin='' />
<link href={`${process.env.NEXT_PUBLIC_ASSET_PREFIX}/Lightningvolt-xoqm.woff2`} as='font' type='font/woff2' crossOrigin='' />
<link href={`${process.env.NEXT_PUBLIC_ASSET_PREFIX}/Lightningvolt-xoqm.woff`} as='font' type='font/woff' crossOrigin='' />
<link href={`${process.env.NEXT_PUBLIC_ASSET_PREFIX}/Lightningvolt-xoqm.ttf`} as='font' type='font/ttf' crossOrigin='' />
<style
nonce={nonce}
dangerouslySetInnerHTML={{

View File

@ -26,9 +26,9 @@ export async function getServerSideProps ({ req, res, query: { id, error = null
})
if (!data?.invite) {
return {
notFound: true
}
res.writeHead(301, {
Location: '/404'
}).end()
}
if (session && res) {

View File

@ -61,7 +61,8 @@ export default function PostEdit ({ ssrData }) {
<FormType item={item} editThreshold={editThreshold}>
{!item.isJob &&
<SubSelect
className='w-auto d-flex'
className='d-flex'
size='medium'
label='territory'
filterSubs={s => s.name !== 'jobs' && s.postTypes?.includes(itemType)}
onChange={(_, e) => setSub(e.target.value)}

View File

@ -50,7 +50,18 @@ export async function getServerSideProps ({ req, res, query: { callbackUrl, erro
function LoginFooter ({ callbackUrl }) {
return (
<small className='fw-bold text-muted pt-4'>Don't have an account? <Link href={{ pathname: '/signup', query: { callbackUrl } }}>sign up</Link></small>
<small className='fw-bold text-muted pt-4'>New to town? <Link href={{ pathname: '/signup', query: { callbackUrl } }}>sign up</Link></small>
)
}
function LoginHeader () {
return (
<>
<h3 className='w-100 pb-2'>
Login
</h3>
<div className='fw-bold text-muted w-100 text-start pb-4'>Ain't you a sight for sore eyes.</div>
</>
)
}
@ -59,6 +70,7 @@ export default function LoginPage (props) {
<StaticLayout footerLinks={false}>
<Login
Footer={() => <LoginFooter callbackUrl={props.callbackUrl} />}
Header={() => <LoginHeader />}
{...props}
/>
</StaticLayout>

View File

@ -9,14 +9,14 @@ function SignUpHeader () {
<h3 className='w-100 pb-2'>
Sign up
</h3>
<div className='fw-bold text-muted pb-4'>Join 21,000+ bitcoiners and start stacking sats today</div>
<div className='fw-bold text-muted w-100 text-start pb-4'>You sure you want to stack sats, pardner?</div>
</>
)
}
function SignUpFooter ({ callbackUrl }) {
return (
<small className='fw-bold text-muted pt-4'>Already have an account? <Link href={{ pathname: '/login', query: { callbackUrl } }}>login</Link></small>
<small className='fw-bold text-muted pt-4'>Been here before? <Link href={{ pathname: '/login', query: { callbackUrl } }}>login</Link></small>
)
}

View File

@ -4,7 +4,6 @@ import Items from '@/components/items'
import Layout from '@/components/layout'
import { SUB_FULL, SUB_ITEMS } from '@/fragments/subs'
import Snl from '@/components/snl'
import { MadnessBanner, WelcomeBanner } from '@/components/banners'
import { useQuery } from '@apollo/client'
import PageLoading from '@/components/page-loading'
import TerritoryHeader from '@/components/territory-header'
@ -29,7 +28,6 @@ export default function Sub ({ ssrData }) {
: (
<>
<Snl />
<WelcomeBanner Banner={MadnessBanner} />
</>)}
<Items ssrData={ssrData} variables={variables} />
</Layout>

View File

@ -189,6 +189,10 @@ $accordion-button-active-icon-dark: $accordion-button-icon;
}
}
.squircle {
clip-path: url(#squircleClip);
}
.btn-outline-grey {
--bs-btn-color: var(--theme-grey);
--bs-btn-border-color: var(--theme-grey);
@ -615,14 +619,22 @@ div[contenteditable] {
font-weight: normal;
}
.alert-dismissible .btn-close {
.alert-dismissible .btn-close, .offcanvas-header .btn-close {
font-weight: 300;
font-family: "lightning";
font-size: 150%;
line-height: 1;
}
.popover-header.alert-dismissible .btn-close {
header .navbar:not(:last-child) {
padding-bottom: 0;
}
header .navbar:not(:first-child) {
padding-top: 0;
}
.popover-header.alert-dismissible .btn-close, .offcanvas-header .btn-close {
padding: .5rem !important;
}
@ -641,7 +653,7 @@ div[contenteditable] {
}
.alert-dismissible .btn-close::after {
.alert-dismissible .btn-close::after, .offcanvas-header .btn-close::after {
content: 'X';
}

1
svgs/sn.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><path fill-rule="evenodd" d="m46.7 96.4 37.858 53.837-71.787 62.934L117.5 155.4l-40.075-52.854 49.412-59.492Zm156.35 41.546-49.416-58.509-34.909 116.771 44.25-67.358 58.509 59.25L241.4 47.725Z"/></svg>

After

Width:  |  Height:  |  Size: 263 B