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
|
const callback = process.env.PUBLIC_URL + req.url
|
||||||
return {
|
return {
|
||||||
redirect: {
|
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))) {
|
if (error || !data || (notFound && notFound(data, vars, me))) {
|
||||||
return {
|
res.writeHead(301, {
|
||||||
notFound: true
|
Location: '/404'
|
||||||
}
|
}).end()
|
||||||
}
|
}
|
||||||
|
|
||||||
props = {
|
props = {
|
||||||
|
|
|
@ -159,7 +159,7 @@ function AnonInfo () {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AnonIcon
|
<AnonIcon
|
||||||
className='fill-muted ms-2 theme' height={22} width={22}
|
className='ms-2 fill-theme-color' height={22} width={22}
|
||||||
onClick={
|
onClick={
|
||||||
(e) =>
|
(e) =>
|
||||||
showModal(onClose =>
|
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 (!user || Number(user.id) === AD_USER_ID) return null
|
||||||
if (Number(user.id) === ANON_USER_ID) {
|
if (Number(user.id) === ANON_USER_ID) {
|
||||||
return (
|
return (
|
||||||
<HatTooltip overlayText={badge ? 'anonymous' : 'posted anonymously'}>
|
<HatTooltip overlayText='anonymous'>
|
||||||
{badge
|
{badge
|
||||||
? (
|
? (
|
||||||
<Badge bg='grey-medium' className='ms-2 d-inline-flex align-items-center'>
|
<Badge bg='grey-medium' className='ms-2 d-inline-flex align-items-center'>
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
.brand {
|
.brand {
|
||||||
font-family: lightning;
|
|
||||||
font-size: 2rem;
|
|
||||||
padding: 0;
|
padding: 0;
|
||||||
line-height: 100%;
|
|
||||||
margin-bottom: -.3rem;
|
|
||||||
margin-right: 0;
|
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 {
|
.postAnon {
|
||||||
|
@ -44,6 +43,10 @@
|
||||||
fill: var(--theme-navLinkActive);
|
fill: var(--theme-navLinkActive);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hide {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.dropdown svg {
|
.dropdown svg {
|
||||||
vertical-align: text-top;
|
vertical-align: text-top;
|
||||||
}
|
}
|
||||||
|
@ -79,6 +82,7 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.price > div {
|
.price > div {
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
.title {
|
.title {
|
||||||
font-weight: 500;
|
|
||||||
white-space: normal;
|
white-space: normal;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
display: inline-block;
|
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 Container from 'react-bootstrap/Container'
|
||||||
import Footer from './footer'
|
import Footer from './footer'
|
||||||
import Seo, { SeoSearch } from './seo'
|
import Seo, { SeoSearch } from './seo'
|
||||||
|
@ -12,51 +14,53 @@ export default function Layout ({
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{seo && <Seo sub={sub} item={item} user={user} />}
|
{seo && <Seo sub={sub} item={item} user={user} />}
|
||||||
<Header sub={sub} />
|
<Navigation sub={sub} />
|
||||||
{contain
|
{contain
|
||||||
? (
|
? (
|
||||||
<Container as='main' className={`px-sm-0 ${containClassName}`}>
|
<Container as='main' className={`px-sm-0 ${styles.contain}`}>
|
||||||
{children}
|
{children}
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
: children}
|
: children}
|
||||||
{footer && <Footer links={footerLinks} />}
|
{footer && <Footer links={footerLinks} />}
|
||||||
|
<NavFooter sub={sub} />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SearchLayout ({ sub, children, ...props }) {
|
export function SearchLayout ({ sub, children, ...props }) {
|
||||||
return (
|
return (
|
||||||
<Layout sub={sub} seo={false} contain={false} {...props}>
|
<Layout sub={sub} seo={false} footer={false} {...props}>
|
||||||
<SeoSearch sub={sub} />
|
<SeoSearch sub={sub} />
|
||||||
<Search sub={sub} />
|
<Search sub={sub} />
|
||||||
<Container as='main' className='pt-3 pb-4 px-sm-0'>
|
{children}
|
||||||
{children}
|
|
||||||
</Container>
|
|
||||||
</Layout>
|
</Layout>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function StaticLayout ({ children, footer = true, footerLinks, ...props }) {
|
export function StaticLayout ({ children, footer = true, footerLinks = false, ...props }) {
|
||||||
return (
|
return (
|
||||||
<div className={styles.page}>
|
<>
|
||||||
<HeaderStatic />
|
<NavStatic />
|
||||||
<main className={`${styles.content} pt-5`}>
|
<div className={styles.page}>
|
||||||
{children}
|
<main className={`${styles.content} ${styles.contain} py-3`}>
|
||||||
</main>
|
{children}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
{footer && <Footer links={footerLinks} />}
|
{footer && <Footer links={footerLinks} />}
|
||||||
</div>
|
<NavFooter />
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CenterLayout ({ children, ...props }) {
|
export function CenterLayout ({ children, ...props }) {
|
||||||
return (
|
return (
|
||||||
<div className={styles.page}>
|
<Layout contain={false} footer={false} {...props}>
|
||||||
<Layout contain={false} {...props}>
|
<div className={styles.page}>
|
||||||
<main className={styles.content}>
|
<main className={styles.content}>
|
||||||
{children}
|
{children}
|
||||||
</main>
|
</main>
|
||||||
</Layout>
|
</div>
|
||||||
</div>
|
</Layout>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,8 +3,8 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-flow: column;
|
flex-flow: column;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
min-height: 100vh;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
margin: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
|
@ -16,7 +16,13 @@
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding-right: 15px;
|
padding-right: 15px;
|
||||||
padding-left: 15px;
|
padding-left: 15px;
|
||||||
margin-top: 1.5rem;
|
margin: 1.5rem 0;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contain {
|
||||||
|
flex-grow: 1;
|
||||||
|
display: flex;
|
||||||
flex-direction: column;
|
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>}
|
</Alert>}
|
||||||
<SubSelect
|
<SubSelect
|
||||||
prependSubs={['pick territory']}
|
prependSubs={['pick territory']}
|
||||||
className='w-auto d-flex'
|
className='d-flex'
|
||||||
noForm
|
noForm
|
||||||
large
|
size='medium'
|
||||||
sub={sub?.name}
|
sub={sub?.name}
|
||||||
info={sub && <TerritoryInfo sub={sub} />}
|
info={sub && <TerritoryInfo sub={sub} />}
|
||||||
hint={sub?.moderated && 'this territory is moderated'}
|
hint={sub?.moderated && 'this territory is moderated'}
|
||||||
|
@ -173,8 +173,8 @@ export default function Post ({ sub }) {
|
||||||
sub={sub?.name}
|
sub={sub?.name}
|
||||||
prependSubs={sub?.name ? undefined : ['pick territory']}
|
prependSubs={sub?.name ? undefined : ['pick territory']}
|
||||||
filterSubs={s => s.postTypes?.includes(type.toUpperCase())}
|
filterSubs={s => s.postTypes?.includes(type.toUpperCase())}
|
||||||
className='w-auto d-flex'
|
className='d-flex'
|
||||||
large
|
size='medium'
|
||||||
label='territory'
|
label='territory'
|
||||||
info={sub && <TerritoryInfo sub={sub} />}
|
info={sub && <TerritoryInfo sub={sub} />}
|
||||||
hint={sub?.moderated && 'this territory is moderated'}
|
hint={sub?.moderated && 'this territory is moderated'}
|
||||||
|
|
|
@ -54,12 +54,12 @@ export default function Search ({ sub }) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={styles.searchSection}>
|
<div className={styles.searchSection}>
|
||||||
<Container className={`px-md-0 ${styles.searchContainer}`}>
|
<Container className={`px-0 ${styles.searchContainer}`}>
|
||||||
<Form
|
<Form
|
||||||
initial={{ q, what, sort, when, from: '', to: '' }}
|
initial={{ q, what, sort, when, from: '', to: '' }}
|
||||||
onSubmit={values => search({ ...values })}
|
onSubmit={values => search({ ...values })}
|
||||||
>
|
>
|
||||||
<div className={`${styles.active} my-3`}>
|
<div className={`${styles.active} mb-3`}>
|
||||||
<Input
|
<Input
|
||||||
name='q'
|
name='q'
|
||||||
required
|
required
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
.searchSection {
|
.searchSection {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
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);
|
background-color: var(--bs-body-bg);
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,7 +42,7 @@ export function useSubs ({ prependSubs = [], sub, filterSubs = () => true, appen
|
||||||
return subs
|
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 router = useRouter()
|
||||||
const subs = useSubs({ prependSubs, sub, filterSubs, appendSubs })
|
const subs = useSubs({ prependSubs, sub, filterSubs, appendSubs })
|
||||||
const valueProps = props.noForm
|
const valueProps = props.noForm
|
||||||
|
@ -105,7 +105,7 @@ export default function SubSelect ({ prependSubs, sub, onChange, large, appendSu
|
||||||
size='sm'
|
size='sm'
|
||||||
{...valueProps}
|
{...valueProps}
|
||||||
{...props}
|
{...props}
|
||||||
className={`${className} ${styles.subSelect} ${large ? 'me-2' : styles.subSelectSmall}`}
|
className={`${className} ${styles.subSelect} ${size === 'large' ? styles.subSelectLarge : size === 'medium' ? styles.subSelectMedium : ''}`}
|
||||||
items={subItems}
|
items={subItems}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
|
@ -5,5 +5,13 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.subSelectSmall {
|
.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 ActionDropdown from './action-dropdown'
|
||||||
import { TerritoryTransferDropdownItem } from './territory-transfer'
|
import { TerritoryTransferDropdownItem } from './territory-transfer'
|
||||||
|
|
||||||
export function TerritoryDetails ({ sub }) {
|
export function TerritoryDetails ({ sub, children }) {
|
||||||
return (
|
return (
|
||||||
<AccordianCard
|
<AccordianCard
|
||||||
header={
|
header={
|
||||||
<small className='text-muted fw-bold align-items-center d-flex'>
|
<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.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.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>}
|
{(sub.nsfw) && <Badge className='ms-2' bg='secondary'>nsfw</Badge>}
|
||||||
</small>
|
</small>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
{children}
|
||||||
<TerritoryInfo sub={sub} />
|
<TerritoryInfo sub={sub} />
|
||||||
</AccordianCard>
|
</AccordianCard>
|
||||||
)
|
)
|
||||||
|
@ -80,43 +81,45 @@ export default function TerritoryHeader ({ sub }) {
|
||||||
<TerritoryPaymentDue sub={sub} />
|
<TerritoryPaymentDue sub={sub} />
|
||||||
<div className='mb-3'>
|
<div className='mb-3'>
|
||||||
<div>
|
<div>
|
||||||
<TerritoryDetails sub={sub} />
|
<TerritoryDetails sub={sub}>
|
||||||
</div>
|
<div className='d-flex my-2 justify-content-end'>
|
||||||
<div className='d-flex my-2 justify-content-end'>
|
{sub.name}
|
||||||
<Share path={`/~${sub.name}`} title={`~${sub.name} stacker news territory`} className='mx-1' />
|
<Share path={`/~${sub.name}`} title={`~${sub.name} stacker news territory`} className='mx-1' />
|
||||||
{me &&
|
{me &&
|
||||||
<>
|
<>
|
||||||
{(isMine
|
{(isMine
|
||||||
? (
|
? (
|
||||||
<Link href={`/~${sub.name}/edit`} className='d-flex align-items-center'>
|
<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>
|
<Button variant='outline-grey border-2 rounded py-0' size='sm'>edit territory</Button>
|
||||||
</Link>)
|
</Link>)
|
||||||
: (
|
: (
|
||||||
<Button
|
<Button
|
||||||
variant='outline-grey border-2 py-0 rounded'
|
variant='outline-grey border-2 py-0 rounded'
|
||||||
size='sm'
|
size='sm'
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
try {
|
try {
|
||||||
await toggleMuteSub({ variables: { name: sub.name } })
|
await toggleMuteSub({ variables: { name: sub.name } })
|
||||||
} catch {
|
} catch {
|
||||||
toaster.danger(`failed to ${sub.meMuteSub ? 'join' : 'mute'} territory`)
|
toaster.danger(`failed to ${sub.meMuteSub ? 'join' : 'mute'} territory`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
toaster.success(`${sub.meMuteSub ? 'joined' : 'muted'} territory`)
|
toaster.success(`${sub.meMuteSub ? 'joined' : 'muted'} territory`)
|
||||||
}}
|
}}
|
||||||
>{sub.meMuteSub ? 'join' : 'mute'} territory
|
>{sub.meMuteSub ? 'join' : 'mute'} territory
|
||||||
</Button>)
|
</Button>)
|
||||||
)}
|
)}
|
||||||
<ActionDropdown>
|
<ActionDropdown>
|
||||||
<ToggleSubSubscriptionDropdownItem sub={sub} />
|
<ToggleSubSubscriptionDropdownItem sub={sub} />
|
||||||
{isMine && (
|
{isMine && (
|
||||||
<>
|
<>
|
||||||
<Dropdown.Divider />
|
<Dropdown.Divider />
|
||||||
<TerritoryTransferDropdownItem sub={sub} />
|
<TerritoryTransferDropdownItem sub={sub} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</ActionDropdown>
|
</ActionDropdown>
|
||||||
</>}
|
</>}
|
||||||
|
</div>
|
||||||
|
</TerritoryDetails>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -69,7 +69,7 @@ export const config = {
|
||||||
// NextJS recommends to not add the CSP header to prefetches and static assets
|
// 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
|
// 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: [
|
missing: [
|
||||||
{ type: 'header', key: 'next-router-prefetch' },
|
{ type: 'header', key: 'next-router-prefetch' },
|
||||||
{ type: 'header', key: 'purpose', value: 'prefetch' }
|
{ type: 'header', key: 'purpose', value: 'prefetch' }
|
||||||
|
|
|
@ -27,6 +27,7 @@
|
||||||
"bolt11": "^1.4.1",
|
"bolt11": "^1.4.1",
|
||||||
"bootstrap": "^5.3.2",
|
"bootstrap": "^5.3.2",
|
||||||
"canonical-json": "0.0.4",
|
"canonical-json": "0.0.4",
|
||||||
|
"classnames": "^2.5.1",
|
||||||
"clipboard-copy": "^4.0.1",
|
"clipboard-copy": "^4.0.1",
|
||||||
"cross-fetch": "^4.0.0",
|
"cross-fetch": "^4.0.0",
|
||||||
"csv-parser": "^3.0.0",
|
"csv-parser": "^3.0.0",
|
||||||
|
@ -7011,9 +7012,9 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/classnames": {
|
"node_modules/classnames": {
|
||||||
"version": "2.3.2",
|
"version": "2.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz",
|
||||||
"integrity": "sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw=="
|
"integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow=="
|
||||||
},
|
},
|
||||||
"node_modules/clean-stack": {
|
"node_modules/clean-stack": {
|
||||||
"version": "2.2.0",
|
"version": "2.2.0",
|
||||||
|
|
|
@ -32,6 +32,7 @@
|
||||||
"bolt11": "^1.4.1",
|
"bolt11": "^1.4.1",
|
||||||
"bootstrap": "^5.3.2",
|
"bootstrap": "^5.3.2",
|
||||||
"canonical-json": "0.0.4",
|
"canonical-json": "0.0.4",
|
||||||
|
"classnames": "^2.5.1",
|
||||||
"clipboard-copy": "^4.0.1",
|
"clipboard-copy": "^4.0.1",
|
||||||
"cross-fetch": "^4.0.0",
|
"cross-fetch": "^4.0.0",
|
||||||
"csv-parser": "^3.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 (
|
return (
|
||||||
<Layout user={user} containClassName={styles.contain}>
|
<Layout user={user} footer={false} containClassName={containClassName}>
|
||||||
<UserHeader user={user} />
|
<UserHeader user={user} />
|
||||||
{children}
|
{children}
|
||||||
</Layout>
|
</Layout>
|
||||||
|
@ -97,7 +97,7 @@ export default function User ({ ssrData }) {
|
||||||
const mine = me?.name === user.name
|
const mine = me?.name === user.name
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<UserLayout user={user}>
|
<UserLayout user={user} containClassName={!user.bio && mine && styles.contain}>
|
||||||
{user.bio
|
{user.bio
|
||||||
? (edit
|
? (edit
|
||||||
? (
|
? (
|
||||||
|
|
|
@ -30,9 +30,9 @@ class MyDocument extends Document {
|
||||||
<Html lang='en'>
|
<Html lang='en'>
|
||||||
<Head nonce={nonce}>
|
<Head nonce={nonce}>
|
||||||
<link rel='manifest' href='/api/site.webmanifest' />
|
<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 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 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.ttf`} as='font' type='font/ttf' crossOrigin='' />
|
||||||
<style
|
<style
|
||||||
nonce={nonce}
|
nonce={nonce}
|
||||||
dangerouslySetInnerHTML={{
|
dangerouslySetInnerHTML={{
|
||||||
|
|
|
@ -26,9 +26,9 @@ export async function getServerSideProps ({ req, res, query: { id, error = null
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!data?.invite) {
|
if (!data?.invite) {
|
||||||
return {
|
res.writeHead(301, {
|
||||||
notFound: true
|
Location: '/404'
|
||||||
}
|
}).end()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (session && res) {
|
if (session && res) {
|
||||||
|
|
|
@ -61,7 +61,8 @@ export default function PostEdit ({ ssrData }) {
|
||||||
<FormType item={item} editThreshold={editThreshold}>
|
<FormType item={item} editThreshold={editThreshold}>
|
||||||
{!item.isJob &&
|
{!item.isJob &&
|
||||||
<SubSelect
|
<SubSelect
|
||||||
className='w-auto d-flex'
|
className='d-flex'
|
||||||
|
size='medium'
|
||||||
label='territory'
|
label='territory'
|
||||||
filterSubs={s => s.name !== 'jobs' && s.postTypes?.includes(itemType)}
|
filterSubs={s => s.name !== 'jobs' && s.postTypes?.includes(itemType)}
|
||||||
onChange={(_, e) => setSub(e.target.value)}
|
onChange={(_, e) => setSub(e.target.value)}
|
||||||
|
|
|
@ -50,7 +50,18 @@ export async function getServerSideProps ({ req, res, query: { callbackUrl, erro
|
||||||
|
|
||||||
function LoginFooter ({ callbackUrl }) {
|
function LoginFooter ({ callbackUrl }) {
|
||||||
return (
|
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}>
|
<StaticLayout footerLinks={false}>
|
||||||
<Login
|
<Login
|
||||||
Footer={() => <LoginFooter callbackUrl={props.callbackUrl} />}
|
Footer={() => <LoginFooter callbackUrl={props.callbackUrl} />}
|
||||||
|
Header={() => <LoginHeader />}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
</StaticLayout>
|
</StaticLayout>
|
||||||
|
|
|
@ -9,14 +9,14 @@ function SignUpHeader () {
|
||||||
<h3 className='w-100 pb-2'>
|
<h3 className='w-100 pb-2'>
|
||||||
Sign up
|
Sign up
|
||||||
</h3>
|
</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 }) {
|
function SignUpFooter ({ callbackUrl }) {
|
||||||
return (
|
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 Layout from '@/components/layout'
|
||||||
import { SUB_FULL, SUB_ITEMS } from '@/fragments/subs'
|
import { SUB_FULL, SUB_ITEMS } from '@/fragments/subs'
|
||||||
import Snl from '@/components/snl'
|
import Snl from '@/components/snl'
|
||||||
import { MadnessBanner, WelcomeBanner } from '@/components/banners'
|
|
||||||
import { useQuery } from '@apollo/client'
|
import { useQuery } from '@apollo/client'
|
||||||
import PageLoading from '@/components/page-loading'
|
import PageLoading from '@/components/page-loading'
|
||||||
import TerritoryHeader from '@/components/territory-header'
|
import TerritoryHeader from '@/components/territory-header'
|
||||||
|
@ -29,7 +28,6 @@ export default function Sub ({ ssrData }) {
|
||||||
: (
|
: (
|
||||||
<>
|
<>
|
||||||
<Snl />
|
<Snl />
|
||||||
<WelcomeBanner Banner={MadnessBanner} />
|
|
||||||
</>)}
|
</>)}
|
||||||
<Items ssrData={ssrData} variables={variables} />
|
<Items ssrData={ssrData} variables={variables} />
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
|
@ -189,6 +189,10 @@ $accordion-button-active-icon-dark: $accordion-button-icon;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.squircle {
|
||||||
|
clip-path: url(#squircleClip);
|
||||||
|
}
|
||||||
|
|
||||||
.btn-outline-grey {
|
.btn-outline-grey {
|
||||||
--bs-btn-color: var(--theme-grey);
|
--bs-btn-color: var(--theme-grey);
|
||||||
--bs-btn-border-color: var(--theme-grey);
|
--bs-btn-border-color: var(--theme-grey);
|
||||||
|
@ -615,14 +619,22 @@ div[contenteditable] {
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
.alert-dismissible .btn-close {
|
.alert-dismissible .btn-close, .offcanvas-header .btn-close {
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
font-family: "lightning";
|
font-family: "lightning";
|
||||||
font-size: 150%;
|
font-size: 150%;
|
||||||
line-height: 1;
|
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;
|
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';
|
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