enhance navigation
This commit is contained in:
parent
b4cef44a43
commit
f2ba61e64b
|
@ -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 = {
|
||||
|
|
|
@ -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 =>
|
||||
|
|
|
@ -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'>
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
.title {
|
||||
font-weight: 500;
|
||||
white-space: normal;
|
||||
max-width: 100%;
|
||||
display: inline-block;
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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} />
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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%;
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -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'}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
)
|
||||
|
|
|
@ -5,5 +5,13 @@
|
|||
}
|
||||
|
||||
.subSelectSmall {
|
||||
max-width: 90px !important;
|
||||
width: 90px !important;
|
||||
}
|
||||
|
||||
.subSelectLarge {
|
||||
width: 300px !important;
|
||||
}
|
||||
|
||||
.subSelectMedium {
|
||||
width: 200px !important;
|
||||
}
|
|
@ -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>
|
||||
</>
|
||||
|
|
|
@ -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' }
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
? (
|
||||
|
|
|
@ -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={{
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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';
|
||||
}
|
||||
|
||||
|
|
|
@ -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 |
Loading…
Reference in New Issue