finish mvp

This commit is contained in:
keyan 2021-05-24 19:08:56 -05:00
parent a9ea341a7b
commit 80ff13abd6
17 changed files with 7068 additions and 52 deletions

View File

@ -174,12 +174,10 @@ const createItem = async (parent, { title, url, text, parentId }, { me, models }
throw new AuthenticationError('you must be logged in') throw new AuthenticationError('you must be logged in')
} }
console.log('before')
const [item] = await serialize(models, models.$queryRaw( const [item] = await serialize(models, models.$queryRaw(
`${SELECT} FROM create_item($1, $2, $3, $4, $5) AS "Item"`, `${SELECT} FROM create_item($1, $2, $3, $4, $5) AS "Item"`,
title, url, text, Number(parentId), me.name)) title, url, text, Number(parentId), me.name))
item.comments = [] item.comments = []
console.log('after')
return item return item
} }

View File

@ -86,7 +86,12 @@ export default {
} }
// decode invoice to get amount // decode invoice to get amount
const decoded = await decodePaymentRequest({ lnd, request: invoice }) let decoded
try {
decoded = await decodePaymentRequest({ lnd, request: invoice })
} catch (error) {
throw new UserInputError('could not decode invoice')
}
const msatsFee = Number(maxFee) * 1000 const msatsFee = Number(maxFee) * 1000

View File

@ -18,6 +18,8 @@ export default gql`
nitems: Int! nitems: Int!
ncomments: Int! ncomments: Int!
stacked: Int! stacked: Int!
freePosts: Int!
freeComments: Int!
sats: Int! sats: Int!
msats: Int! msats: Int!
} }

View File

@ -48,11 +48,10 @@ export function InputSkeleton ({ label }) {
) )
} }
export function Input ({ label, prepend, append, hint, showValid, noBottomMargin, ...props }) { export function Input ({ label, prepend, append, hint, showValid, groupClassName, ...props }) {
const [field, meta] = props.readOnly ? [{}, {}] : useField(props) const [field, meta] = props.readOnly ? [{}, {}] : useField(props)
return ( return (
<BootstrapForm.Group className={noBottomMargin ? 'mb-0' : ''}> <BootstrapForm.Group className={groupClassName}>
{label && <BootstrapForm.Label>{label}</BootstrapForm.Label>} {label && <BootstrapForm.Label>{label}</BootstrapForm.Label>}
<InputGroup hasValidation> <InputGroup hasValidation>
{prepend && ( {prepend && (
@ -73,18 +72,18 @@ export function Input ({ label, prepend, append, hint, showValid, noBottomMargin
<BootstrapForm.Control.Feedback type='invalid'> <BootstrapForm.Control.Feedback type='invalid'>
{meta.touched && meta.error} {meta.touched && meta.error}
</BootstrapForm.Control.Feedback> </BootstrapForm.Control.Feedback>
{hint && (
<BootstrapForm.Text>
{hint}
</BootstrapForm.Text>
)}
</InputGroup> </InputGroup>
{hint && (
<BootstrapForm.Text>
{hint}
</BootstrapForm.Text>
)}
</BootstrapForm.Group> </BootstrapForm.Group>
) )
} }
export function Form ({ export function Form ({
initial, schema, onSubmit, children, initialError, validateOnBlur, ...props initial, schema, onSubmit, children, initialError, validateImmediately, ...props
}) { }) {
const [error, setError] = useState(initialError) const [error, setError] = useState(initialError)
@ -92,7 +91,8 @@ export function Form ({
<Formik <Formik
initialValues={initial} initialValues={initial}
validationSchema={schema} validationSchema={schema}
validateOnBlur={validateOnBlur} initialTouched={validateImmediately && initial}
validateOnBlur={false}
onSubmit={async (...args) => onSubmit={async (...args) =>
onSubmit && onSubmit(...args).catch(e => setError(e.message || e))} onSubmit && onSubmit(...args).catch(e => setError(e.message || e))}
> >

View File

@ -35,7 +35,7 @@ export function FundErrorModal () {
onHide={() => setError(false)} onHide={() => setError(false)}
> >
<Modal.Body> <Modal.Body>
<p className='font-weight-bolder'>you are out of sats</p> <p className='font-weight-bolder'>you have no sats</p>
<div className='d-flex justify-content-end'> <div className='d-flex justify-content-end'>
<Link href='/wallet?type=fund'> <Link href='/wallet?type=fund'>
<Button variant='success' onClick={() => setError(false)}>fund</Button> <Button variant='success' onClick={() => setError(false)}>fund</Button>

View File

@ -6,20 +6,13 @@ import styles from './header.module.css'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import { Button, Container, NavDropdown } from 'react-bootstrap' import { Button, Container, NavDropdown } from 'react-bootstrap'
import Price from './price' import Price from './price'
import { gql, useQuery } from '@apollo/client' import { useMe } from './me'
function WalletSummary () { function WalletSummary () {
const query = gql` const me = useMe()
{ if (!me) return null
me {
sats
stacked
}
}`
const { data } = useQuery(query, { pollInterval: 1000 })
if (!data) return null
return `[${data.me.stacked},${data.me.sats}]` return `[${me.stacked},${me.sats}]`
} }
export default function Header () { export default function Header () {

35
components/me.js Normal file
View File

@ -0,0 +1,35 @@
import React, { useContext } from 'react'
import { gql, useQuery } from '@apollo/client'
export const MeContext = React.createContext({
me: null
})
export function MeProvider ({ children }) {
const query = gql`
{
me {
id
sats
stacked
freePosts
freeComments
}
}`
const { data } = useQuery(query, { pollInterval: 1000 })
const contextValue = {
me: data ? data.me : null
}
return (
<MeContext.Provider value={contextValue}>
{children}
</MeContext.Provider>
)
}
export function useMe () {
const { me } = useContext(MeContext)
return me
}

View File

@ -3,12 +3,15 @@ import * as Yup from 'yup'
import { gql, useMutation } from '@apollo/client' import { gql, useMutation } from '@apollo/client'
import styles from './reply.module.css' import styles from './reply.module.css'
import { COMMENTS } from '../fragments/comments' import { COMMENTS } from '../fragments/comments'
import { useMe } from './me'
export const CommentSchema = Yup.object({ export const CommentSchema = Yup.object({
text: Yup.string().required('required').trim() text: Yup.string().required('required').trim()
}) })
export default function Reply ({ parentId, onSuccess, autoFocus }) { export default function Reply ({ parentId, onSuccess, autoFocus }) {
const me = useMe()
const [createComment] = useMutation( const [createComment] = useMutation(
gql` gql`
${COMMENTS} ${COMMENTS}
@ -65,6 +68,7 @@ export default function Reply ({ parentId, onSuccess, autoFocus }) {
rows={4} rows={4}
autoFocus={autoFocus} autoFocus={autoFocus}
required required
hint={me?.freeComments ? <span className='text-success'>{me.freeComments} free comments left</span> : null}
/> />
<SubmitButton variant='secondary' className='mt-1'>reply</SubmitButton> <SubmitButton variant='secondary' className='mt-1'>reply</SubmitButton>
</Form> </Form>

View File

@ -8,6 +8,7 @@ import { Form, Input, SubmitButton } from './form'
import InputGroup from 'react-bootstrap/InputGroup' import InputGroup from 'react-bootstrap/InputGroup'
import * as Yup from 'yup' import * as Yup from 'yup'
import { gql, useApolloClient, useMutation } from '@apollo/client' import { gql, useApolloClient, useMutation } from '@apollo/client'
import styles from './user-header.module.css'
const NAME_QUERY = const NAME_QUERY =
gql` gql`
@ -58,6 +59,7 @@ export default function UserHeader ({ user }) {
initial={{ initial={{
name: user.name name: user.name
}} }}
validateImmediately
onSubmit={async ({ name }) => { onSubmit={async ({ name }) => {
if (name === user.name) { if (name === user.name) {
setEditting(false) setEditting(false)
@ -74,7 +76,7 @@ export default function UserHeader ({ user }) {
prepend=<InputGroup.Text>@</InputGroup.Text> prepend=<InputGroup.Text>@</InputGroup.Text>
name='name' name='name'
autoFocus autoFocus
noBottomMargin groupClassName={`mb-0 ${styles.username}`}
showValid showValid
/> />
<Satistics user={user} /> <Satistics user={user} />

View File

@ -0,0 +1,3 @@
.username {
width: 300px;
}

6915
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -9,7 +9,7 @@
}, },
"dependencies": { "dependencies": {
"@apollo/client": "^3.3.13", "@apollo/client": "^3.3.13",
"@prisma/client": "^2.22.1", "@prisma/client": "^2.23.0",
"apollo-server-micro": "^2.21.2", "apollo-server-micro": "^2.21.2",
"async-retry": "^1.3.1", "async-retry": "^1.3.1",
"bootstrap": "^4.6.0", "bootstrap": "^4.6.0",
@ -44,7 +44,7 @@
"babel-plugin-inline-react-svg": "^2.0.1", "babel-plugin-inline-react-svg": "^2.0.1",
"eslint": "^7.22.0", "eslint": "^7.22.0",
"eslint-plugin-compat": "^3.9.0", "eslint-plugin-compat": "^3.9.0",
"prisma": "^2.22.1", "prisma": "^2.23.0",
"standard": "^16.0.3" "standard": "^16.0.3"
} }
} }

View File

@ -2,6 +2,7 @@ import '../styles/globals.scss'
import { ApolloClient, InMemoryCache, ApolloProvider } from '@apollo/client' import { ApolloClient, InMemoryCache, ApolloProvider } from '@apollo/client'
import { Provider } from 'next-auth/client' import { Provider } from 'next-auth/client'
import { FundErrorModal, FundErrorProvider } from '../components/fund-error' import { FundErrorModal, FundErrorProvider } from '../components/fund-error'
import { MeProvider } from '../components/me'
const client = new ApolloClient({ const client = new ApolloClient({
uri: '/api/graphql', uri: '/api/graphql',
@ -11,12 +12,14 @@ const client = new ApolloClient({
function MyApp ({ Component, pageProps }) { function MyApp ({ Component, pageProps }) {
return ( return (
<Provider session={pageProps.session}> <Provider session={pageProps.session}>
<FundErrorProvider> <ApolloProvider client={client}>
<FundErrorModal /> <MeProvider>
<ApolloProvider client={client}> <FundErrorProvider>
<Component {...pageProps} /> <FundErrorModal />
</ApolloProvider> <Component {...pageProps} />
</FundErrorProvider> </FundErrorProvider>
</MeProvider>
</ApolloProvider>
</Provider> </Provider>
) )
} }

View File

@ -6,6 +6,7 @@ import * as Yup from 'yup'
import { gql, useMutation } from '@apollo/client' import { gql, useMutation } from '@apollo/client'
import LayoutCenter from '../components/layout-center' import LayoutCenter from '../components/layout-center'
import { ensureProtocol } from '../lib/url' import { ensureProtocol } from '../lib/url'
import { useMe } from '../components/me'
export const DiscussionSchema = Yup.object({ export const DiscussionSchema = Yup.object({
title: Yup.string().required('required').trim() title: Yup.string().required('required').trim()
@ -115,6 +116,7 @@ export function LinkForm () {
export function PostForm () { export function PostForm () {
const router = useRouter() const router = useRouter()
const me = useMe()
if (!router.query.type) { if (!router.query.type) {
return ( return (
@ -126,6 +128,9 @@ export function PostForm () {
<Link href='/post?type=discussion'> <Link href='/post?type=discussion'>
<Button variant='secondary'>discussion</Button> <Button variant='secondary'>discussion</Button>
</Link> </Link>
{me?.freePosts
? <div className='text-center font-weight-bold mt-3 text-success'>{me.freePosts} free posts left</div>
: null}
</div> </div>
) )
} }

View File

@ -0,0 +1,49 @@
-- AlterTable
ALTER TABLE "users" ADD COLUMN "freeComments" INTEGER NOT NULL DEFAULT 5,
ADD COLUMN "freePosts" INTEGER NOT NULL DEFAULT 2;
-- if user has free comments or posts, use that
CREATE OR REPLACE FUNCTION create_item(title TEXT, url TEXT, text TEXT, parent_id INTEGER, username TEXT)
RETURNS "Item"
LANGUAGE plpgsql
AS $$
DECLARE
user_id INTEGER;
user_sats INTEGER;
free_posts INTEGER;
free_comments INTEGER;
freebie BOOLEAN;
item "Item";
BEGIN
PERFORM ASSERT_SERIALIZED();
SELECT (msats / 1000), id, "freePosts", "freeComments"
INTO user_sats, user_id, free_posts, free_comments
FROM users WHERE name = username;
freebie := (parent_id IS NULL AND free_posts > 0) OR (parent_id IS NOT NULL AND free_comments > 0);
IF NOT freebie AND 1 > user_sats THEN
RAISE EXCEPTION 'SN_INSUFFICIENT_FUNDS';
END IF;
INSERT INTO "Item" (title, url, text, "userId", "parentId", created_at, updated_at)
VALUES (title, url, text, user_id, parent_id, now_utc(), now_utc()) RETURNING * INTO item;
IF freebie THEN
IF parent_id IS NULL THEN
UPDATE users SET "freePosts" = "freePosts" - 1 WHERE id = user_id;
ELSE
UPDATE users SET "freeComments" = "freeComments" - 1 WHERE id = user_id;
END IF;
ELSE
UPDATE users SET msats = msats - 1000 WHERE id = user_id;
INSERT INTO "Vote" (sats, "itemId", "userId", created_at, updated_at)
VALUES (1, item.id, user_id, now_utc(), now_utc());
END IF;
RETURN item;
END;
$$;

View File

@ -24,6 +24,8 @@ model User {
invoices Invoice[] invoices Invoice[]
withdrawls Withdrawl[] withdrawls Withdrawl[]
msats Int @default(0) msats Int @default(0)
freeComments Int @default(5)
freePosts Int @default(2)
@@map(name: "users") @@map(name: "users")
} }

View File

@ -340,22 +340,22 @@
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.9.2.tgz#adea7b6953cbb34651766b0548468e743c6a2353" resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.9.2.tgz#adea7b6953cbb34651766b0548468e743c6a2353"
integrity sha512-VZMYa7+fXHdwIq1TDhSXoVmSPEGM/aa+6Aiq3nVVJ9bXr24zScr+NlKFKC3iPljA7ho/GAZr+d2jOf5GIRC30Q== integrity sha512-VZMYa7+fXHdwIq1TDhSXoVmSPEGM/aa+6Aiq3nVVJ9bXr24zScr+NlKFKC3iPljA7ho/GAZr+d2jOf5GIRC30Q==
"@prisma/client@^2.22.1": "@prisma/client@^2.23.0":
version "2.22.1" version "2.23.0"
resolved "https://registry.yarnpkg.com/@prisma/client/-/client-2.22.1.tgz#10fdcd1532a6baf46dd1c464cad9a54af0532bc8" resolved "https://registry.yarnpkg.com/@prisma/client/-/client-2.23.0.tgz#4bf16ab19b140873ba79bd159da86842b1746e0a"
integrity sha512-JQjbsY6QSfFiovXHEp5WeJHa5p2CuR1ZFPAeYXmUsOAQOaMCrhgQmKAL6w2Q3SRA7ALqPjrKywN9/QfBc4Kp1A== integrity sha512-xsHdo3+wIH0hJVGfKHYTEKtifStjKH0b5t8t7hV32Fypq6+3uxhAi3F25yxuI4XSHXg21nb7Ha82lNwU/0TERA==
dependencies: dependencies:
"@prisma/engines-version" "2.22.0-21.60cc71d884972ab4e897f0277c4b84383dddaf6c" "@prisma/engines-version" "2.23.0-36.adf5e8cba3daf12d456d911d72b6e9418681b28b"
"@prisma/engines-version@2.22.0-21.60cc71d884972ab4e897f0277c4b84383dddaf6c": "@prisma/engines-version@2.23.0-36.adf5e8cba3daf12d456d911d72b6e9418681b28b":
version "2.22.0-21.60cc71d884972ab4e897f0277c4b84383dddaf6c" version "2.23.0-36.adf5e8cba3daf12d456d911d72b6e9418681b28b"
resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-2.22.0-21.60cc71d884972ab4e897f0277c4b84383dddaf6c.tgz#e98ee17217a0ebb54f2f9314fbbfd610b05e6e31" resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-2.23.0-36.adf5e8cba3daf12d456d911d72b6e9418681b28b.tgz#c813279bbea48dedad039b0bc3b044117d2dbaa1"
integrity sha512-OkkVwk6iTzTbwwl8JIKAENyxmh4TFORal55QMKQzrHEY8UzbD0M90mQnmziz3PAopQUZgTFFMlaPAq1WNrLMtA== integrity sha512-VNgnOe+oPQKmy3HOtWi/Q1fvcKZUQkf1OfTD1pzrLBx9tJPejyxt1Mq54L+OOAuYvfrua6bmfojFVLh7uXuWVw==
"@prisma/engines@2.22.0-21.60cc71d884972ab4e897f0277c4b84383dddaf6c": "@prisma/engines@2.23.0-36.adf5e8cba3daf12d456d911d72b6e9418681b28b":
version "2.22.0-21.60cc71d884972ab4e897f0277c4b84383dddaf6c" version "2.23.0-36.adf5e8cba3daf12d456d911d72b6e9418681b28b"
resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-2.22.0-21.60cc71d884972ab4e897f0277c4b84383dddaf6c.tgz#4ccd255e0823605db3d8387a5195b6fdabe3b0c0" resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-2.23.0-36.adf5e8cba3daf12d456d911d72b6e9418681b28b.tgz#440abe0ebef44b6e1bdaf2b4d14fcde9fe74f18c"
integrity sha512-KmWdogrsfsSLYvfqY3cS3QcDGzaEFklE+T6dNJf+k/KPQum4A29IwDalafMwh5cMN8ivZobUbowNSwWJrMT08Q== integrity sha512-Tgk3kggO5B9IT6mimJAw6HSxbFoDAuDKL3sHHSS41EnQm76j/nf4uhGZFPzOQwZWOLeT5ZLO2khr4/FCA9Nkhw==
"@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2": "@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2":
version "1.1.2" version "1.1.2"
@ -4234,12 +4234,12 @@ pretty-format@^3.8.0:
resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-3.8.0.tgz#bfbed56d5e9a776645f4b1ff7aa1a3ac4fa3c385" resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-3.8.0.tgz#bfbed56d5e9a776645f4b1ff7aa1a3ac4fa3c385"
integrity sha1-v77VbV6ad2ZF9LH/eqGjrE+jw4U= integrity sha1-v77VbV6ad2ZF9LH/eqGjrE+jw4U=
prisma@^2.22.1: prisma@^2.23.0:
version "2.22.1" version "2.23.0"
resolved "https://registry.yarnpkg.com/prisma/-/prisma-2.22.1.tgz#884687a90c7b797b34c6110ea413049078c8da6e" resolved "https://registry.yarnpkg.com/prisma/-/prisma-2.23.0.tgz#6464cca0e085ed23b1815013a67c868eff07a7d2"
integrity sha512-hwvCM3zyxgSda/+/p+GW7nz93jRebtMU01wAG7YVVnl0OKU+dpw1wPvPFmQRldkZHk8fTCleYmjc24WaSdVPZQ== integrity sha512-3c/lmDy8nsPcEsfCufvCTJUEuwmAcTPbeGg9fL1qjlvS314duLUA/k2nm3n1rq4ImKqzeC5uaKfvI2IoAfwrJA==
dependencies: dependencies:
"@prisma/engines" "2.22.0-21.60cc71d884972ab4e897f0277c4b84383dddaf6c" "@prisma/engines" "2.23.0-36.adf5e8cba3daf12d456d911d72b6e9418681b28b"
process-nextick-args@~2.0.0: process-nextick-args@~2.0.0:
version "2.0.1" version "2.0.1"