ready for invoices

This commit is contained in:
keyan 2021-05-06 16:15:22 -05:00
parent d4d1169058
commit 4b07edf6f5
26 changed files with 322 additions and 139 deletions

View File

@ -1,21 +1,13 @@
export default {
Query: {
accounts: async (parent, args, { lnd }) => {
console.log('hi')
console.log(lnd.wallet.listAccounts)
lnd.wallet.listAccounts({}, (err, res) => {
console.log(err, res)
})
return []
invoice: async (parent, { id }, { me, models, lnd }) => {
return 'lnbc1500n1psfxyaypp5tmlgpudspqed4qf32xxmc7dhlqrd4glc09x794exz4t2pw8ms38sdpa2fjkzep6yptks7fqt9hh2gzwv4jkggz5dus9gatjdcsyzmrvypvk7atjypzx7cqzpgxqr23ssp529tup4vaxlxnst0lwh9kljpl9n6zg6n6vma5hw78lmnws32x278s9qyyssqxe73jclrlz3u7v7ruwee3n7h70ktsdsfmvpfjkccqxq5wg5h6njhqxar0a9fef5hd09ethwhvsj0dha2qy4tjjdxu08nkqymfs8wghqp6d7kth'
}
},
Mutation: {
createAccount: async (parent, args, { lnd }) => {
lnd.default.newAddress({ type: 'p2wpkh', account: 'default' }, (err, res) => {
console.log(err, res)
})
return 'ok'
createInvoice: async (parent, { amount }, { me, models, lnd }) => {
return 'lnbc1500n1psfxyaypp5tmlgpudspqed4qf32xxmc7dhlqrd4glc09x794exz4t2pw8ms38sdpa2fjkzep6yptks7fqt9hh2gzwv4jkggz5dus9gatjdcsyzmrvypvk7atjypzx7cqzpgxqr23ssp529tup4vaxlxnst0lwh9kljpl9n6zg6n6vma5hw78lmnws32x278s9qyyssqxe73jclrlz3u7v7ruwee3n7h70ktsdsfmvpfjkccqxq5wg5h6njhqxar0a9fef5hd09ethwhvsj0dha2qy4tjjdxu08nkqymfs8wghqp6d7kth'
}
}
}

View File

@ -2,10 +2,10 @@ import { gql } from 'apollo-server-micro'
export default gql`
extend type Query {
accounts: [String!]
invoice(id: ID!): String!
}
extend type Mutation {
createAccount: String!
createInvoice(amount: Int!): String!
}
`

View File

@ -28,7 +28,7 @@ export function Input ({ label, prepend, append, hint, ...props }) {
<InputGroup hasValidation>
{prepend && (
<InputGroup.Prepend>
{prepend}
<InputGroup.Text>{prepend}</InputGroup.Text>
</InputGroup.Prepend>
)}
<BootstrapForm.Control
@ -37,7 +37,7 @@ export function Input ({ label, prepend, append, hint, ...props }) {
/>
{append && (
<InputGroup.Append>
{append}
<InputGroup.Text>{append}</InputGroup.Text>
</InputGroup.Append>
)}
<BootstrapForm.Control.Feedback type='invalid'>

View File

@ -20,18 +20,29 @@ export default function Header () {
if (session) {
return (
<div className='d-flex align-items-center'>
<NavDropdown title={`@${session.user.name}`} alignRight>
<NavDropdown className='pl-0' title={`@${session.user.name}`} alignRight>
<Link href={'/' + session.user.name} passHref>
<NavDropdown.Item>profile</NavDropdown.Item>
</Link>
<Link href='/fund' passHref>
<NavDropdown.Item className='text-success'>fund [0,0]</NavDropdown.Item>
<Link href='/wallet' passHref>
<NavDropdown.Item>wallet</NavDropdown.Item>
</Link>
<div>
<NavDropdown.Divider />
<Link href='/recent' passHref>
<NavDropdown.Item>recent</NavDropdown.Item>
</Link>
<Link href='/post' passHref>
<NavDropdown.Item>post</NavDropdown.Item>
</Link>
<NavDropdown.Item href='https://bitcoinerjobs.co' target='_blank'>jobs</NavDropdown.Item>
</div>
<NavDropdown.Divider />
<NavDropdown.Item onClick={signOut}>logout</NavDropdown.Item>
</NavDropdown>
<Nav.Item>
<Link href='/fund' passHref>
<Nav.Link className='text-success pl-0'>[0,0]</Nav.Link>
<Link href='/wallet' passHref>
<Nav.Link className='text-success px-0'>[0,0]</Nav.Link>
</Link>
</Nav.Item>
</div>
@ -47,22 +58,22 @@ export default function Header () {
<Navbar className={styles.navbar}>
<Nav className='w-100 justify-content-between flex-wrap align-items-center' activeKey={path}>
<Link href='/' passHref>
<Navbar.Brand className={`${styles.brand} mr-2 d-none d-sm-block`}>STACKER NEWS</Navbar.Brand>
<Navbar.Brand className={`${styles.brand} d-none d-sm-block`}>STACKER NEWS</Navbar.Brand>
</Link>
<Link href='/' passHref>
<Navbar.Brand className={`${styles.brand} mr-2 d-block d-sm-none`}>SN</Navbar.Brand>
<Navbar.Brand className={`${styles.brand} d-block d-sm-none`}>SN</Navbar.Brand>
</Link>
<Nav.Item>
<Nav.Item className='d-md-flex d-none'>
<Link href='/recent' passHref>
<Nav.Link className={styles.navLink}>recent</Nav.Link>
</Link>
</Nav.Item>
<Nav.Item>
<Nav.Item className='d-md-flex d-none'>
<Link href='/post' passHref>
<Nav.Link className={styles.navLink}>post</Nav.Link>
</Link>
</Nav.Item>
<Nav.Item>
<Nav.Item className='d-md-flex d-none'>
<Nav.Link href='https://bitcoinerjobs.co' target='_blank' className={styles.navLink}>jobs</Nav.Link>
</Nav.Item>
<Nav.Item style={{ fontFamily: 'monospace', opacity: '.5' }}>

View File

@ -0,0 +1,56 @@
import QRCode from 'qrcode.react'
import { InputGroup } from 'react-bootstrap'
import Moon from '../svgs/moon-fill.svg'
import copy from 'clipboard-copy'
import Thumb from '../svgs/thumb-up-fill.svg'
import { useState } from 'react'
import BootstrapForm from 'react-bootstrap/Form'
import Button from 'react-bootstrap/Button'
export function Invoice ({ invoice }) {
const [copied, setCopied] = useState(false)
const qrValue = 'lightning:' + invoice.toUpperCase()
return (
<>
<div>
<QRCode className='h-auto mw-100' value={qrValue} size={300} />
</div>
<div className='mt-3 w-100'>
<InputGroup onClick={() => {
copy(invoice)
setCopied(true)
setTimeout(() => setCopied(false), 1500)
}}
>
<BootstrapForm.Control type='text' placeholder={invoice} readOnly />
<InputGroup.Append>
<Button>{copied ? <Thumb width={20} height={20} /> : 'copy'}</Button>
</InputGroup.Append>
</InputGroup>
</div>
<InvoiceStatus />
</>
)
}
export function InvoiceStatus ({ skeleton }) {
return (
<div className='d-flex mt-4'>
<Moon className='spin fill-grey' />
<div className='ml-3 text-muted' style={{ fontWeight: '600' }}>{skeleton ? 'generating' : 'waiting for you'}</div>
</div>
)
}
export function InvoiceSkeleton () {
return (
<>
<div className='h-auto w-100 clouds' style={{ paddingTop: 'min(300px, 100%)', maxWidth: '300px' }} />
<div className='mt-3 w-100'>
<div className='w-100 clouds form-control' />
</div>
<InvoiceStatus skeleton />
</>
)
}

View File

View File

@ -6,7 +6,7 @@ export default function Items ({ query, rank }) {
const { loading, error, data } = useQuery(query)
if (error) return <div>Failed to load!</div>
if (loading) {
const items = new Array(30).fill(null)
const items = new Array(20).fill(null)
return (
<div className={styles.grid}>

View File

@ -0,0 +1,14 @@
import Layout from './layout'
import styles from './layout-center.module.css'
export default function LayoutCenter ({ children }) {
return (
<div className={styles.page}>
<Layout noContain>
<div className={styles.content}>
{children}
</div>
</Layout>
</div>
)
}

View File

@ -0,0 +1,24 @@
.page {
width: 100%;
display: flex;
flex-flow: column;
height: 100%;
min-height: 100vh;
align-items: center;
}
.content {
flex-grow: 1;
display: flex;
align-items: center;
justify-content: center;
max-width: 740px;
width: 100%;
padding-right: 15px;
padding-left: 15px;
flex-direction: column;
}
.content form {
width: 100%;
}

View File

@ -12,6 +12,7 @@
"@prisma/client": "^2.19.0",
"apollo-server-micro": "^2.21.2",
"bootstrap": "^4.6.0",
"clipboard-copy": "^4.0.1",
"formik": "^2.2.6",
"graphql": "^15.5.0",
"ln-service": "^51.7.0",

View File

@ -1,9 +0,0 @@
import Layout from '../components/layout'
export default function Fund () {
return (
<Layout>
<div>fund</div>
</Layout>
)
}

34
pages/invoices/[id].js Normal file
View File

@ -0,0 +1,34 @@
import { useQuery } from '@apollo/client'
import gql from 'graphql-tag'
import { Invoice, InvoiceSkeleton } from '../../components/invoice'
import LayoutCenter from '../../components/layout-center'
export async function getServerSideProps ({ params: { id } }) {
return {
props: {
id
}
}
}
export default function FullInvoice ({ id }) {
const query = gql`
{
invoice(id: ${id})
}`
return (
<LayoutCenter>
<LoadInvoice query={query} />
</LayoutCenter>
)
}
function LoadInvoice ({ query }) {
const { loading, error, data } = useQuery(query)
if (error) return <div>error</div>
if (!data || loading) {
return <InvoiceSkeleton />
}
return <Invoice invoice={data.invoice} />
}

View File

@ -1,5 +1,4 @@
import { providers, signIn, getSession, csrfToken } from 'next-auth/client'
import Layout from '../components/layout'
import Button from 'react-bootstrap/Button'
import styles from '../styles/login.module.css'
import GithubIcon from '../svgs/github-fill.svg'
@ -8,6 +7,7 @@ import { Input, SubmitButton, SyncForm } from '../components/form'
import * as Yup from 'yup'
import { useState } from 'react'
import Alert from 'react-bootstrap/Alert'
import LayoutCenter from '../components/layout-center'
export async function getServerSideProps ({ req, res, query: { callbackUrl, error = null } }) {
const session = await getSession({ req })
@ -50,53 +50,51 @@ export default function login ({ providers, csrfToken, error }) {
const [errorMessage, setErrorMessage] = useState(error && (errors[error] ?? errors.default))
return (
<Layout noContain>
<div className={styles.page}>
<div className={styles.login}>
{errorMessage &&
<Alert variant='danger' onClose={() => setErrorMessage(undefined)} dismissible>{errorMessage}</Alert>}
{Object.values(providers).map(provider => {
if (provider.name === 'Email') {
return null
}
const [variant, Icon] =
<LayoutCenter>
<div className={styles.login}>
{errorMessage &&
<Alert variant='danger' onClose={() => setErrorMessage(undefined)} dismissible>{errorMessage}</Alert>}
{Object.values(providers).map(provider => {
if (provider.name === 'Email') {
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)}
>
<Icon
className='mr-3'
/>Login with {provider.name}
</Button>
)
})}
<div className='mt-2 text-center text-muted font-weight-bold'>or</div>
<SyncForm
initial={{
email: ''
}}
schema={EmailSchema}
action='/api/auth/signin/email'
>
<input name='csrfToken' type='hidden' defaultValue={csrfToken} />
<Input
label='Email'
name='email'
placeholder='email@example.com'
required
autoFocus
/>
<SubmitButton variant='secondary' className={styles.providerButton}>Login with Email</SubmitButton>
</SyncForm>
</div>
return (
<Button
className={`mt-2 ${styles.providerButton}`}
key={provider.name}
variant={variant}
onClick={() => signIn(provider.id)}
>
<Icon
className='mr-3'
/>Login with {provider.name}
</Button>
)
})}
<div className='mt-2 text-center text-muted font-weight-bold'>or</div>
<SyncForm
initial={{
email: ''
}}
schema={EmailSchema}
action='/api/auth/signin/email'
>
<input name='csrfToken' type='hidden' defaultValue={csrfToken} />
<Input
label='Email'
name='email'
placeholder='email@example.com'
required
autoFocus
/>
<SubmitButton variant='secondary' className={styles.providerButton}>Login with Email</SubmitButton>
</SyncForm>
</div>
</Layout>
</LayoutCenter>
)
}

View File

@ -1,11 +1,10 @@
import Layout from '../components/layout'
import Button from 'react-bootstrap/Button'
import { Form, Input, SubmitButton } from '../components/form'
import { useRouter } from 'next/router'
import Link from 'next/link'
import styles from '../styles/post.module.css'
import * as Yup from 'yup'
import { gql, useMutation } from '@apollo/client'
import LayoutCenter from '../components/layout-center'
export const DiscussionSchema = Yup.object({
title: Yup.string().required('required').trim()
@ -112,7 +111,7 @@ export function PostForm () {
</Link>
<span className='mx-3 font-weight-bold text-muted'>or</span>
<Link href='/post?type=discussion'>
<Button variant='secondary'> discussion</Button>
<Button variant='secondary'>discussion</Button>
</Link>
</div>
)
@ -127,12 +126,8 @@ export function PostForm () {
export default function Post () {
return (
<Layout noContain>
<div className={styles.page}>
<div className={styles.post}>
<PostForm />
</div>
</div>
</Layout>
<LayoutCenter>
<PostForm />
</LayoutCenter>
)
}

82
pages/wallet.js Normal file
View File

@ -0,0 +1,82 @@
import { useRouter } from 'next/router'
import { Form, Input, SubmitButton } from '../components/form'
import Link from 'next/link'
import Button from 'react-bootstrap/Button'
import * as Yup from 'yup'
import { gql, useMutation } from '@apollo/client'
import { InvoiceSkeleton } from '../components/invoice'
import LayoutCenter from '../components/layout-center'
export default function Wallet () {
return (
<LayoutCenter>
<WalletForm />
</LayoutCenter>
)
}
export function WalletForm () {
const router = useRouter()
if (!router.query.type) {
return (
<div className='align-items-center'>
<Link href='/wallet?type=fund'>
<Button variant='success'>fund</Button>
</Link>
<span className='mx-3 font-weight-bold text-muted'>or</span>
<Link href='/wallet?type=withdrawl'>
<Button variant='success'>withdrawl</Button>
</Link>
</div>
)
}
if (router.query.type === 'fund') {
return <FundForm />
} else {
return <WithdrawlForm />
}
}
export const FundSchema = Yup.object({
amount: Yup.number('must be a number').required('required').positive('must be positive').integer('must be whole')
})
export function FundForm () {
const router = useRouter()
const [createInvoice, { called }] = useMutation(gql`
mutation createInvoice($amount: Int!) {
createInvoice(amount: $amount)
}`)
if (called) {
return <InvoiceSkeleton />
}
return (
<Form
initial={{
amount: 1000
}}
schema={FundSchema}
onSubmit={async ({ amount }) => {
await createInvoice({ variables: { amount } })
router.push('/invoices/1')
}}
>
<Input
label='amount'
name='amount'
required
autoFocus
append='sats'
/>
<SubmitButton variant='success' className='mt-2'>generate invoice</SubmitButton>
</Form>
)
}
export function WithdrawlForm () {
return <div>withdrawl</div>
}

BIN
public/giphy.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 936 KiB

BIN
public/static.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -57,8 +57,6 @@ body {
background-attachment:fixed;
min-height: 100vh;
height: 100%;
z-index: -2;
position: relative;
}
.form-label {
@ -109,6 +107,10 @@ body {
font-weight: bold;
}
.fill-grey {
fill: grey;
}
@keyframes flash {
from { filter: brightness(1);}
2% { filter: brightness(2.3); }
@ -126,5 +128,24 @@ body {
background-origin: content-box;
background-size: cover;
background-attachment: fixed;
opacity: .2;
opacity: .1;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(359deg); }
}
.spin {
animation: spin 2s linear infinite;
}
.static {
background: url('/giphy.gif');
background-color: grey;
background-repeat: repeat;
background-origin: content-box;
background-size: cover;
background-attachment: fixed;
opacity: .1;
}

View File

@ -5,23 +5,6 @@
display: flex;
}
.page {
position: absolute;
width: 100%;
height: 100%;
display: table;
margin: 0;
padding: 0;
top: 0;
z-index: -1;
}
.login {
display: table-cell;
vertical-align: middle;
padding: .5rem;
}
.login > * {
max-width: 300px;
margin: auto;

View File

@ -1,29 +0,0 @@
.page {
position: absolute;
width: 100%;
height: 100%;
display: table;
margin: 0;
padding: 0;
top: 0;
z-index: -1;
padding-right: 15px;
padding-left: 15px
}
.post {
display: table-cell;
vertical-align: middle;
padding: .5rem;
}
.post > form {
max-width: 740px;
margin: 1rem auto;
}
.post > div {
display: flex;
justify-content: center;
margin: 1rem auto;
}

1
svgs/bit-coin-fill.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm-1-6v2h2v-2h1a2.5 2.5 0 0 0 2-4 2.5 2.5 0 0 0-2-4h-1V6h-2v2H8v8h3zm-1-3h4a.5.5 0 1 1 0 1h-4v-1zm0-3h4a.5.5 0 1 1 0 1h-4v-1z"/></svg>

After

Width:  |  Height:  |  Size: 335 B

1
svgs/coin-fill.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M23 12v2c0 3.314-4.925 6-11 6-5.967 0-10.824-2.591-10.995-5.823L1 14v-2c0 3.314 4.925 6 11 6s11-2.686 11-6zM12 4c6.075 0 11 2.686 11 6s-4.925 6-11 6-11-2.686-11-6 4.925-6 11-6z"/></svg>

After

Width:  |  Height:  |  Size: 314 B

1
svgs/hand-coin-fill.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M9.33 11.5h2.17A4.5 4.5 0 0 1 16 16H8.999L9 17h8v-1a5.578 5.578 0 0 0-.886-3H19a5 5 0 0 1 4.516 2.851C21.151 18.972 17.322 21 13 21c-2.761 0-5.1-.59-7-1.625L6 10.071A6.967 6.967 0 0 1 9.33 11.5zM5 19a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1v-9a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v9zM18 5a3 3 0 1 1 0 6 3 3 0 0 1 0-6zm-7-3a3 3 0 1 1 0 6 3 3 0 0 1 0-6z"/></svg>

After

Width:  |  Height:  |  Size: 471 B

1
svgs/moon-fill.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M11.38 2.019a7.5 7.5 0 1 0 10.6 10.6C21.662 17.854 17.316 22 12.001 22 6.477 22 2 17.523 2 12c0-5.315 4.146-9.661 9.38-9.981z"/></svg>

After

Width:  |  Height:  |  Size: 263 B

1
svgs/thumb-up-fill.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M2 9h3v12H2a1 1 0 0 1-1-1V10a1 1 0 0 1 1-1zm5.293-1.293l6.4-6.4a.5.5 0 0 1 .654-.047l.853.64a1.5 1.5 0 0 1 .553 1.57L14.6 8H21a2 2 0 0 1 2 2v2.104a2 2 0 0 1-.15.762l-3.095 7.515a1 1 0 0 1-.925.619H8a1 1 0 0 1-1-1V8.414a1 1 0 0 1 .293-.707z"/></svg>

After

Width:  |  Height:  |  Size: 377 B

View File

@ -1577,6 +1577,11 @@ cli-highlight@^2.1.10:
parse5-htmlparser2-tree-adapter "^6.0.0"
yargs "^16.0.0"
clipboard-copy@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/clipboard-copy/-/clipboard-copy-4.0.1.tgz#326ef9726d4ffe72d9a82a7bbe19379de692017d"
integrity sha512-wOlqdqziE/NNTUJsfSgXmBMIrYmfd5V0HCGsR8uAKHcg+h9NENWINcfRjtWGU77wDHC8B8ijV4hMTGYbrKovng==
cliui@^7.0.2:
version "7.0.4"
resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f"