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 { 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)
useEffect(() => {
@ -19,7 +19,6 @@ export default function AccordianItem ({ header, body, className, headerColor =
eventKey='0'
style={{ cursor: 'pointer', display: 'flex', alignItems: 'center' }}
onClick={() => setOpen(!open)}
className={className}
>
{open
? <ArrowDown style={{ fill: headerColor }} height={20} width={20} />

View File

@ -1,10 +1,11 @@
import { gql, useMutation } from '@apollo/client'
import { Dropdown } from 'react-bootstrap'
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 }) {
const { setError } = useFundError()
const showModal = useShowModal()
const [dontLikeThis] = useMutation(
gql`
@ -41,7 +42,9 @@ export default function DontLikeThis ({ id }) {
})
} catch (error) {
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 { Button } from 'react-bootstrap'
export const FundErrorContext = React.createContext({
error: null,
toggleError: () => {}
})
export function FundErrorProvider ({ children }) {
const [error, setError] = useState(false)
const contextValue = {
error,
setError: useCallback(e => setError(e), [])
}
export default function FundError ({ onClose }) {
return (
<FundErrorContext.Provider value={contextValue}>
{children}
</FundErrorContext.Provider>
)
}
export function useFundError () {
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>
<>
<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={onClose}>fund</Button>
</Link>
</div>
</>
)
}

View File

@ -1,105 +1,69 @@
import { Button, InputGroup, Modal } from 'react-bootstrap'
import React, { useState, useCallback, useContext, useRef, useEffect } from 'react'
import { Button, InputGroup } from 'react-bootstrap'
import React, { useState, useRef, useEffect } from 'react'
import * as Yup from 'yup'
import { Form, Input, SubmitButton } from './form'
import { useMe } from './me'
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({
amount: Yup.number().typeError('must be a number').required('required')
.positive('must be positive').integer('must be whole')
})
export function ItemActModal () {
const { item, setItem } = useItemAct()
export default function ItemAct ({ onClose, itemId, act, strike }) {
const inputRef = useRef(null)
const me = useMe()
const [oValue, setOValue] = useState()
useEffect(() => {
inputRef.current?.focus()
}, [item])
}, [onClose, itemId])
return (
<Modal
show={!!item}
onHide={() => {
setItem(null)
<Form
initial={{
amount: me?.tipDefault,
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>
<Modal.Body>
<Form
initial={{
amount: me?.tipDefault,
default: false
}}
schema={ActSchema}
onSubmit={async ({ amount }) => {
await item.act({
variables: {
id: item.itemId,
sats: Number(amount)
}
})
await item.strike()
setItem(null)
}}
>
<Input
label='amount'
name='amount'
innerRef={inputRef}
overrideValue={oValue}
required
autoFocus
append={<InputGroup.Text className='text-monospace'>sats</InputGroup.Text>}
/>
<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>
<Input
label='amount'
name='amount'
innerRef={inputRef}
overrideValue={oValue}
required
autoFocus
append={<InputGroup.Text className='text-monospace'>sats</InputGroup.Text>}
/>
<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>
)
}

View File

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

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 Check from '../svgs/checkbox-circle-fill.svg'
import { signIn } from 'next-auth/client'
import { useFundError } from './fund-error'
import ActionTooltip from './action-tooltip'
import { useShowModal } from './modal'
import FundError from './fund-error'
export default function Poll ({ item }) {
const me = useMe()
const { setError } = useFundError()
const showModal = useShowModal()
const [pollVote] = useMutation(
gql`
mutation pollVote($id: ID!) {
@ -60,7 +61,9 @@ export default function Poll ({ item }) {
})
} catch (error) {
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 styles from './upvote.module.css'
import { gql, useMutation } from '@apollo/client'
import { signIn } from 'next-auth/client'
import { useFundError } from './fund-error'
import FundError from './fund-error'
import ActionTooltip from './action-tooltip'
import { useItemAct } from './item-act'
import ItemAct from './item-act'
import { useMe } from './me'
import Rainbow from '../lib/rainbow'
import { useRef, useState } from 'react'
import LongPressable from 'react-longpressable'
import { Overlay, Popover } from 'react-bootstrap'
import { useShowModal } from './modal'
import { useRouter } from 'next/router'
const getColor = (meSats) => {
if (!meSats || meSats <= 10) {
@ -63,8 +64,8 @@ const TipPopover = ({ target, show, handleClose }) => (
)
export default function UpVote ({ item, className }) {
const { setError } = useFundError()
const { setItem } = useItemAct()
const showModal = useShowModal()
const router = useRouter()
const [voteShow, _setVoteShow] = useState(false)
const [tipShow, _setTipShow] = useState(false)
const ref = useRef()
@ -123,11 +124,14 @@ export default function UpVote ({ item, className }) {
return existingSats + sats
},
meSats (existingSats = 0) {
if (existingSats === 0) {
setVoteShow(true)
} else {
setTipShow(true)
if (sats <= me.sats) {
if (existingSats === 0) {
setVoteShow(true)
} else {
setTipShow(true)
}
}
return existingSats + sats
},
upvotes (existingUpvotes = 0) {
@ -183,7 +187,8 @@ export default function UpVote ({ item, className }) {
}
setTipShow(false)
setItem({ itemId: item.id, act, strike })
showModal(onClose =>
<ItemAct onClose={onClose} itemId={item.id} act={act} strike={strike} />)
}
}
onShortPress={
@ -215,13 +220,18 @@ export default function UpVote ({ item, className }) {
})
} catch (error) {
if (error.toString().includes('insufficient funds')) {
setError(true)
showModal(onClose => {
return <FundError onClose={onClose} />
})
return
}
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()}>

View File

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

View File

@ -136,7 +136,8 @@ export default (req, res) => NextAuth(req, res, {
signingKey: process.env.JWT_SIGNING_PRIVATE_KEY
},
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 getSSRApolloClient from '../../api/ssrApollo'
import Link from 'next/link'
import LayoutCenter from '../../components/layout-center'
export async function getServerSideProps ({ req, res, query: { id, error = null } }) {
const session = await getSession({ req })
@ -78,5 +79,9 @@ function InviteHeader ({ invite }) {
}
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 Link from 'next/link'
import LayoutCenter from '../components/layout-center'
import Login from '../components/login'
export async function getServerSideProps ({ req, res, query: { callbackUrl, error = null } }) {
@ -30,9 +31,11 @@ function LoginFooter ({ callbackUrl }) {
export default function LoginPage (props) {
return (
<Login
Footer={() => <LoginFooter callbackUrl={props.callbackUrl} />}
{...props}
/>
<LayoutCenter>
<Login
Footer={() => <LoginFooter callbackUrl={props.callbackUrl} />}
{...props}
/>
</LayoutCenter>
)
}

View File

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

View File

@ -2,7 +2,7 @@
// This will help to prevent a flash if dark mode is the default.
const COLORS = {
light: {
body: '#f5f5f5',
body: '#f5f5f7',
color: '#212529',
navbarVariant: 'light',
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
);
$body-bg: #f5f5f5;
$body-bg: #f5f5f7;
$border-radius: .4rem;
$enable-transitions: false;
$enable-gradients: false;