global modal + small fixes/enhancements

This commit is contained in:
keyan 2023-01-10 17:13:37 -06:00
parent e2d7506ebf
commit ae5c6c457f
17 changed files with 238 additions and 222 deletions

View File

@ -3,7 +3,7 @@ import ArrowRight from '../svgs/arrow-right-s-fill.svg'
import ArrowDown from '../svgs/arrow-down-s-fill.svg' import ArrowDown from '../svgs/arrow-down-s-fill.svg'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
export default function AccordianItem ({ header, body, className, headerColor = 'var(--theme-grey)', show }) { export default function AccordianItem ({ header, body, headerColor = 'var(--theme-grey)', show }) {
const [open, setOpen] = useState(show) const [open, setOpen] = useState(show)
useEffect(() => { useEffect(() => {
@ -19,7 +19,6 @@ export default function AccordianItem ({ header, body, className, headerColor =
eventKey='0' eventKey='0'
style={{ cursor: 'pointer', display: 'flex', alignItems: 'center' }} style={{ cursor: 'pointer', display: 'flex', alignItems: 'center' }}
onClick={() => setOpen(!open)} onClick={() => setOpen(!open)}
className={className}
> >
{open {open
? <ArrowDown style={{ fill: headerColor }} height={20} width={20} /> ? <ArrowDown style={{ fill: headerColor }} height={20} width={20} />

View File

@ -1,10 +1,11 @@
import { gql, useMutation } from '@apollo/client' import { gql, useMutation } from '@apollo/client'
import { Dropdown } from 'react-bootstrap' import { Dropdown } from 'react-bootstrap'
import MoreIcon from '../svgs/more-fill.svg' import MoreIcon from '../svgs/more-fill.svg'
import { useFundError } from './fund-error' import FundError from './fund-error'
import { useShowModal } from './modal'
export default function DontLikeThis ({ id }) { export default function DontLikeThis ({ id }) {
const { setError } = useFundError() const showModal = useShowModal()
const [dontLikeThis] = useMutation( const [dontLikeThis] = useMutation(
gql` gql`
@ -41,7 +42,9 @@ export default function DontLikeThis ({ id }) {
}) })
} catch (error) { } catch (error) {
if (error.toString().includes('insufficient funds')) { if (error.toString().includes('insufficient funds')) {
setError(true) showModal(onClose => {
return <FundError onClose={onClose} />
})
} }
} }
}} }}

View File

@ -1,48 +1,15 @@
import { Button, Modal } from 'react-bootstrap'
import React, { useState, useCallback, useContext } from 'react'
import Link from 'next/link' import Link from 'next/link'
import { Button } from 'react-bootstrap'
export const FundErrorContext = React.createContext({ export default function FundError ({ onClose }) {
error: null,
toggleError: () => {}
})
export function FundErrorProvider ({ children }) {
const [error, setError] = useState(false)
const contextValue = {
error,
setError: useCallback(e => setError(e), [])
}
return ( return (
<FundErrorContext.Provider value={contextValue}> <>
{children} <p className='font-weight-bolder'>you need more sats</p>
</FundErrorContext.Provider> <div className='d-flex justify-content-end'>
) <Link href='/wallet?type=fund'>
} <Button variant='success' onClick={onClose}>fund</Button>
</Link>
export function useFundError () { </div>
const { error, setError } = useContext(FundErrorContext) </>
return { error, setError }
}
export function FundErrorModal () {
const { error, setError } = useFundError()
return (
<Modal
show={error}
onHide={() => setError(false)}
>
<div className='modal-close' onClick={() => setError(false)}>X</div>
<Modal.Body>
<p className='font-weight-bolder'>you need more sats</p>
<div className='d-flex justify-content-end'>
<Link href='/wallet?type=fund'>
<Button variant='success' onClick={() => setError(false)}>fund</Button>
</Link>
</div>
</Modal.Body>
</Modal>
) )
} }

View File

@ -1,105 +1,69 @@
import { Button, InputGroup, Modal } from 'react-bootstrap' import { Button, InputGroup } from 'react-bootstrap'
import React, { useState, useCallback, useContext, useRef, useEffect } from 'react' import React, { useState, useRef, useEffect } from 'react'
import * as Yup from 'yup' import * as Yup from 'yup'
import { Form, Input, SubmitButton } from './form' import { Form, Input, SubmitButton } from './form'
import { useMe } from './me' import { useMe } from './me'
import UpBolt from '../svgs/bolt.svg' import UpBolt from '../svgs/bolt.svg'
export const ItemActContext = React.createContext({
item: null,
setItem: () => {}
})
export function ItemActProvider ({ children }) {
const [item, setItem] = useState(null)
const contextValue = {
item,
setItem: useCallback(i => setItem(i), [])
}
return (
<ItemActContext.Provider value={contextValue}>
{children}
</ItemActContext.Provider>
)
}
export function useItemAct () {
const { item, setItem } = useContext(ItemActContext)
return { item, setItem }
}
export const ActSchema = Yup.object({ export const ActSchema = Yup.object({
amount: Yup.number().typeError('must be a number').required('required') amount: Yup.number().typeError('must be a number').required('required')
.positive('must be positive').integer('must be whole') .positive('must be positive').integer('must be whole')
}) })
export function ItemActModal () { export default function ItemAct ({ onClose, itemId, act, strike }) {
const { item, setItem } = useItemAct()
const inputRef = useRef(null) const inputRef = useRef(null)
const me = useMe() const me = useMe()
const [oValue, setOValue] = useState() const [oValue, setOValue] = useState()
useEffect(() => { useEffect(() => {
inputRef.current?.focus() inputRef.current?.focus()
}, [item]) }, [onClose, itemId])
return ( return (
<Modal <Form
show={!!item} initial={{
onHide={() => { amount: me?.tipDefault,
setItem(null) default: false
}}
schema={ActSchema}
onSubmit={async ({ amount }) => {
await act({
variables: {
id: itemId,
sats: Number(amount)
}
})
await strike()
onClose()
}} }}
> >
<div className='modal-close' onClick={() => setItem(null)}>X</div> <Input
<Modal.Body> label='amount'
<Form name='amount'
initial={{ innerRef={inputRef}
amount: me?.tipDefault, overrideValue={oValue}
default: false required
}} autoFocus
schema={ActSchema} append={<InputGroup.Text className='text-monospace'>sats</InputGroup.Text>}
onSubmit={async ({ amount }) => { />
await item.act({ <div>
variables: { {[1, 10, 100, 1000, 10000].map(num =>
id: item.itemId, <Button
sats: Number(amount) size='sm'
} className={`${num > 1 ? 'ml-2' : ''} mb-2`}
}) key={num}
await item.strike() onClick={() => { setOValue(num) }}
setItem(null) >
}} <UpBolt
> className='mr-1'
<Input width={14}
label='amount' height={14}
name='amount' />{num}
innerRef={inputRef} </Button>)}
overrideValue={oValue} </div>
required <div className='d-flex'>
autoFocus <SubmitButton variant='success' className='ml-auto mt-1 px-4' value='TIP'>tip</SubmitButton>
append={<InputGroup.Text className='text-monospace'>sats</InputGroup.Text>} </div>
/> </Form>
<div>
{[1, 10, 100, 1000, 10000].map(num =>
<Button
size='sm'
className={`${num > 1 ? 'ml-2' : ''} mb-2`}
key={num}
onClick={() => { setOValue(num) }}
>
<UpBolt
className='mr-1'
width={14}
height={14}
/>{num}
</Button>)}
</div>
<div className='d-flex'>
<SubmitButton variant='success' className='ml-auto mt-1 px-4' value='TIP'>tip</SubmitButton>
</div>
</Form>
</Modal.Body>
</Modal>
) )
} }

View File

@ -8,7 +8,6 @@ import { Form, Input, SubmitButton } from '../components/form'
import * as Yup from 'yup' import * as Yup from 'yup'
import { useState } from 'react' import { useState } from 'react'
import Alert from 'react-bootstrap/Alert' import Alert from 'react-bootstrap/Alert'
import LayoutCenter from '../components/layout-center'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import { LightningAuth } from './lightning-auth' import { LightningAuth } from './lightning-auth'
@ -56,60 +55,59 @@ export default function Login ({ providers, callbackUrl, error, text, Header, Fo
const [errorMessage, setErrorMessage] = useState(error && (errors[error] ?? errors.default)) const [errorMessage, setErrorMessage] = useState(error && (errors[error] ?? errors.default))
const router = useRouter() const router = useRouter()
if (router.query.type === 'lightning') {
return <LightningAuth callbackUrl={callbackUrl} text={text} />
}
return ( return (
<LayoutCenter> <div className={styles.login}>
{router.query.type === 'lightning' {Header && <Header />}
? <LightningAuth callbackUrl={callbackUrl} text={text} /> {errorMessage &&
: ( <Alert
<div className={styles.login}> variant='danger'
{Header && <Header />} onClose={() => setErrorMessage(undefined)}
{errorMessage && dismissible
<Alert >{errorMessage}
variant='danger' </Alert>}
onClose={() => setErrorMessage(undefined)} <Button
dismissible className={`mt-2 ${styles.providerButton}`}
>{errorMessage} variant='primary'
</Alert>} onClick={() => router.push({
<Button pathname: router.pathname,
className={`mt-2 ${styles.providerButton}`} query: { ...router.query, type: 'lightning' }
variant='primary' })}
onClick={() => router.push({ >
pathname: router.pathname, <LightningIcon
query: { ...router.query, type: 'lightning' } width={20}
})} height={20}
> className='mr-3'
<LightningIcon />{text || 'Login'} with Lightning
width={20} </Button>
height={20} {providers && Object.values(providers).map(provider => {
className='mr-3' if (provider.name === 'Email' || provider.name === 'Lightning') {
/>{text || 'Login'} with Lightning return null
</Button> }
{Object.values(providers).map(provider => { const [variant, Icon] =
if (provider.name === 'Email' || provider.name === 'Lightning') {
return null
}
const [variant, Icon] =
provider.name === 'Twitter' provider.name === 'Twitter'
? ['twitter', TwitterIcon] ? ['twitter', TwitterIcon]
: ['dark', GithubIcon] : ['dark', GithubIcon]
return ( return (
<Button <Button
className={`mt-2 ${styles.providerButton}`} className={`mt-2 ${styles.providerButton}`}
key={provider.name} key={provider.name}
variant={variant} variant={variant}
onClick={() => signIn(provider.id, { callbackUrl })} onClick={() => signIn(provider.id, { callbackUrl })}
> >
<Icon <Icon
className='mr-3' className='mr-3'
/>{text || 'Login'} with {provider.name} />{text || 'Login'} with {provider.name}
</Button> </Button>
) )
})} })}
<div className='mt-2 text-center text-muted font-weight-bold'>or</div> <div className='mt-2 text-center text-muted font-weight-bold'>or</div>
<EmailLoginForm text={text} callbackUrl={callbackUrl} /> <EmailLoginForm text={text} callbackUrl={callbackUrl} />
{Footer && <Footer />} {Footer && <Footer />}
</div>)} </div>
</LayoutCenter>
) )
} }

51
components/modal.js Normal file
View File

@ -0,0 +1,51 @@
import { createContext, useCallback, useContext, useMemo, useState } from 'react'
import { Modal } from 'react-bootstrap'
export const ShowModalContext = createContext(() => null)
export function ShowModalProvider ({ children }) {
const [modal, showModal] = useModal()
const contextValue = showModal
return (
<ShowModalContext.Provider value={contextValue}>
{children}
{modal}
</ShowModalContext.Provider>
)
}
export function useShowModal () {
return useContext(ShowModalContext)
}
export default function useModal () {
const [modalContent, setModalContent] = useState(null)
const onClose = useCallback(() => {
setModalContent(null)
}, [])
const modal = useMemo(() => {
if (modalContent === null) {
return null
}
return (
<Modal onHide={onClose} show={!!modalContent}>
<div className='modal-close' onClick={onClose}>X</div>
<Modal.Body>
{modalContent}
</Modal.Body>
</Modal>
)
}, [modalContent, onClose])
const showModal = useCallback(
(getContent) => {
setModalContent(getContent(onClose))
},
[onClose]
)
return [modal, showModal]
}

View File

@ -6,12 +6,13 @@ import { useMe } from './me'
import styles from './poll.module.css' import styles from './poll.module.css'
import Check from '../svgs/checkbox-circle-fill.svg' import Check from '../svgs/checkbox-circle-fill.svg'
import { signIn } from 'next-auth/client' import { signIn } from 'next-auth/client'
import { useFundError } from './fund-error'
import ActionTooltip from './action-tooltip' import ActionTooltip from './action-tooltip'
import { useShowModal } from './modal'
import FundError from './fund-error'
export default function Poll ({ item }) { export default function Poll ({ item }) {
const me = useMe() const me = useMe()
const { setError } = useFundError() const showModal = useShowModal()
const [pollVote] = useMutation( const [pollVote] = useMutation(
gql` gql`
mutation pollVote($id: ID!) { mutation pollVote($id: ID!) {
@ -60,7 +61,9 @@ export default function Poll ({ item }) {
}) })
} catch (error) { } catch (error) {
if (error.toString().includes('insufficient funds')) { if (error.toString().includes('insufficient funds')) {
setError(true) showModal(onClose => {
return <FundError onClose={onClose} />
})
} }
} }
} }

View File

@ -2,15 +2,16 @@ import { LightningConsumer } from './lightning'
import UpBolt from '../svgs/bolt.svg' import UpBolt from '../svgs/bolt.svg'
import styles from './upvote.module.css' import styles from './upvote.module.css'
import { gql, useMutation } from '@apollo/client' import { gql, useMutation } from '@apollo/client'
import { signIn } from 'next-auth/client' import FundError from './fund-error'
import { useFundError } from './fund-error'
import ActionTooltip from './action-tooltip' import ActionTooltip from './action-tooltip'
import { useItemAct } from './item-act' import ItemAct from './item-act'
import { useMe } from './me' import { useMe } from './me'
import Rainbow from '../lib/rainbow' import Rainbow from '../lib/rainbow'
import { useRef, useState } from 'react' import { useRef, useState } from 'react'
import LongPressable from 'react-longpressable' import LongPressable from 'react-longpressable'
import { Overlay, Popover } from 'react-bootstrap' import { Overlay, Popover } from 'react-bootstrap'
import { useShowModal } from './modal'
import { useRouter } from 'next/router'
const getColor = (meSats) => { const getColor = (meSats) => {
if (!meSats || meSats <= 10) { if (!meSats || meSats <= 10) {
@ -63,8 +64,8 @@ const TipPopover = ({ target, show, handleClose }) => (
) )
export default function UpVote ({ item, className }) { export default function UpVote ({ item, className }) {
const { setError } = useFundError() const showModal = useShowModal()
const { setItem } = useItemAct() const router = useRouter()
const [voteShow, _setVoteShow] = useState(false) const [voteShow, _setVoteShow] = useState(false)
const [tipShow, _setTipShow] = useState(false) const [tipShow, _setTipShow] = useState(false)
const ref = useRef() const ref = useRef()
@ -123,11 +124,14 @@ export default function UpVote ({ item, className }) {
return existingSats + sats return existingSats + sats
}, },
meSats (existingSats = 0) { meSats (existingSats = 0) {
if (existingSats === 0) { if (sats <= me.sats) {
setVoteShow(true) if (existingSats === 0) {
} else { setVoteShow(true)
setTipShow(true) } else {
setTipShow(true)
}
} }
return existingSats + sats return existingSats + sats
}, },
upvotes (existingUpvotes = 0) { upvotes (existingUpvotes = 0) {
@ -183,7 +187,8 @@ export default function UpVote ({ item, className }) {
} }
setTipShow(false) setTipShow(false)
setItem({ itemId: item.id, act, strike }) showModal(onClose =>
<ItemAct onClose={onClose} itemId={item.id} act={act} strike={strike} />)
} }
} }
onShortPress={ onShortPress={
@ -215,13 +220,18 @@ export default function UpVote ({ item, className }) {
}) })
} catch (error) { } catch (error) {
if (error.toString().includes('insufficient funds')) { if (error.toString().includes('insufficient funds')) {
setError(true) showModal(onClose => {
return <FundError onClose={onClose} />
})
return return
} }
throw new Error({ message: error.toString() }) throw new Error({ message: error.toString() })
} }
} }
: signIn : async () => await router.push({
pathname: '/signup',
query: { callbackUrl: window.location.origin + router.asPath }
})
} }
> >
<ActionTooltip notForm disable={item?.mine || fwd2me} overlayText={overlayText()}> <ActionTooltip notForm disable={item?.mine || fwd2me} overlayText={overlayText()}>

View File

@ -1,11 +1,9 @@
import '../styles/globals.scss' import '../styles/globals.scss'
import { ApolloProvider, gql, useQuery } from '@apollo/client' import { ApolloProvider, gql, useQuery } from '@apollo/client'
import { Provider } from 'next-auth/client' import { Provider } from 'next-auth/client'
import { FundErrorModal, FundErrorProvider } from '../components/fund-error'
import { MeProvider } from '../components/me' import { MeProvider } from '../components/me'
import PlausibleProvider from 'next-plausible' import PlausibleProvider from 'next-plausible'
import { LightningProvider } from '../components/lightning' import { LightningProvider } from '../components/lightning'
import { ItemActModal, ItemActProvider } from '../components/item-act'
import getApolloClient from '../lib/apollo' import getApolloClient from '../lib/apollo'
import NextNProgress from 'nextjs-progressbar' import NextNProgress from 'nextjs-progressbar'
import { PriceProvider } from '../components/price' import { PriceProvider } from '../components/price'
@ -14,6 +12,7 @@ import { useRouter } from 'next/dist/client/router'
import { useEffect } from 'react' import { useEffect } from 'react'
import Moon from '../svgs/moon-fill.svg' import Moon from '../svgs/moon-fill.svg'
import Layout from '../components/layout' import Layout from '../components/layout'
import { ShowModalProvider } from '../components/modal'
function CSRWrapper ({ Component, apollo, ...props }) { function CSRWrapper ({ Component, apollo, ...props }) {
const { data, error } = useQuery(gql`${apollo.query}`, { variables: apollo.variables, fetchPolicy: 'cache-first' }) const { data, error } = useQuery(gql`${apollo.query}`, { variables: apollo.variables, fetchPolicy: 'cache-first' })
@ -89,15 +88,11 @@ function MyApp ({ Component, pageProps: { session, ...props } }) {
<MeProvider me={me}> <MeProvider me={me}>
<PriceProvider price={price}> <PriceProvider price={price}>
<LightningProvider> <LightningProvider>
<FundErrorProvider> <ShowModalProvider>
<FundErrorModal /> {data || !apollo?.query
<ItemActProvider> ? <Component {...props} />
<ItemActModal /> : <CSRWrapper Component={Component} {...props} />}
{data || !apollo?.query </ShowModalProvider>
? <Component {...props} />
: <CSRWrapper Component={Component} {...props} />}
</ItemActProvider>
</FundErrorProvider>
</LightningProvider> </LightningProvider>
</PriceProvider> </PriceProvider>
</MeProvider> </MeProvider>

View File

@ -136,7 +136,8 @@ export default (req, res) => NextAuth(req, res, {
signingKey: process.env.JWT_SIGNING_PRIVATE_KEY signingKey: process.env.JWT_SIGNING_PRIVATE_KEY
}, },
pages: { pages: {
signIn: '/login' signIn: '/login',
verifyRequest: '/email'
} }
}) })

14
pages/email.js Normal file
View File

@ -0,0 +1,14 @@
import LayoutError from '../components/layout-error'
import { Image } from 'react-bootstrap'
export default function Email () {
return (
<LayoutError>
<div className='p-4 text-center'>
<h1>Check your email</h1>
<h4 className='pb-4'>A sign in link has been sent to your email address</h4>
<Image width='500' height='376' src='/hello.gif' fluid />
</div>
</LayoutError>
)
}

View File

@ -6,6 +6,7 @@ import { gql } from '@apollo/client'
import { INVITE_FIELDS } from '../../fragments/invites' import { INVITE_FIELDS } from '../../fragments/invites'
import getSSRApolloClient from '../../api/ssrApollo' import getSSRApolloClient from '../../api/ssrApollo'
import Link from 'next/link' import Link from 'next/link'
import LayoutCenter from '../../components/layout-center'
export async function getServerSideProps ({ req, res, query: { id, error = null } }) { export async function getServerSideProps ({ req, res, query: { id, error = null } }) {
const session = await getSession({ req }) const session = await getSession({ req })
@ -78,5 +79,9 @@ function InviteHeader ({ invite }) {
} }
export default function Invite ({ invite, ...props }) { export default function Invite ({ invite, ...props }) {
return <Login Header={() => <InviteHeader invite={invite} />} text='Sign up' {...props} /> return (
<LayoutCenter>
<Login Header={() => <InviteHeader invite={invite} />} text='Sign up' {...props} />
</LayoutCenter>
)
} }

View File

@ -1,5 +1,6 @@
import { providers, getSession } from 'next-auth/client' import { providers, getSession } from 'next-auth/client'
import Link from 'next/link' import Link from 'next/link'
import LayoutCenter from '../components/layout-center'
import Login from '../components/login' import Login from '../components/login'
export async function getServerSideProps ({ req, res, query: { callbackUrl, error = null } }) { export async function getServerSideProps ({ req, res, query: { callbackUrl, error = null } }) {
@ -30,9 +31,11 @@ function LoginFooter ({ callbackUrl }) {
export default function LoginPage (props) { export default function LoginPage (props) {
return ( return (
<Login <LayoutCenter>
Footer={() => <LoginFooter callbackUrl={props.callbackUrl} />} <Login
{...props} Footer={() => <LoginFooter callbackUrl={props.callbackUrl} />}
/> {...props}
/>
</LayoutCenter>
) )
} }

View File

@ -1,5 +1,6 @@
import { providers, getSession } from 'next-auth/client' import { providers, getSession } from 'next-auth/client'
import Link from 'next/link' import Link from 'next/link'
import LayoutCenter from '../components/layout-center'
import Login from '../components/login' import Login from '../components/login'
export async function getServerSideProps ({ req, res, query: { callbackUrl, error = null } }) { export async function getServerSideProps ({ req, res, query: { callbackUrl, error = null } }) {
@ -41,11 +42,13 @@ function SignUpFooter ({ callbackUrl }) {
export default function SignUp ({ ...props }) { export default function SignUp ({ ...props }) {
return ( return (
<Login <LayoutCenter>
Header={() => <SignUpHeader />} <Login
Footer={() => <SignUpFooter callbackUrl={props.callbackUrl} />} Header={() => <SignUpHeader />}
text='Sign up' Footer={() => <SignUpFooter callbackUrl={props.callbackUrl} />}
{...props} text='Sign up'
/> {...props}
/>
</LayoutCenter>
) )
} }

View File

@ -2,7 +2,7 @@
// This will help to prevent a flash if dark mode is the default. // This will help to prevent a flash if dark mode is the default.
const COLORS = { const COLORS = {
light: { light: {
body: '#f5f5f5', body: '#f5f5f7',
color: '#212529', color: '#212529',
navbarVariant: 'light', navbarVariant: 'light',
navLink: 'rgba(0, 0, 0, 0.55)', navLink: 'rgba(0, 0, 0, 0.55)',

BIN
public/hello.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 418 KiB

View File

@ -11,7 +11,7 @@ $theme-colors: (
"grey-darkmode": #8c8c8c "grey-darkmode": #8c8c8c
); );
$body-bg: #f5f5f5; $body-bg: #f5f5f7;
$border-radius: .4rem; $border-radius: .4rem;
$enable-transitions: false; $enable-transitions: false;
$enable-gradients: false; $enable-gradients: false;