more progress

This commit is contained in:
keyan 2021-04-14 18:56:29 -05:00
parent bc5d4d4808
commit 28ed42fc29
23 changed files with 401 additions and 88 deletions

28
api/client.js Normal file
View File

@ -0,0 +1,28 @@
import { ApolloClient, InMemoryCache } from '@apollo/client'
import { SchemaLink } from '@apollo/client/link/schema'
import { mergeSchemas } from 'graphql-tools'
import { getSession } from 'next-auth/client'
import resolvers from './resolvers'
import typeDefs from './typeDefs'
import models from './models'
const client = new ApolloClient({
ssrMode: true,
// Instead of "createHttpLink" use SchemaLink here
link: new SchemaLink({
schema: mergeSchemas({
schemas: typeDefs,
resolvers: resolvers
}),
context: async ({ req }) => {
const session = await getSession({ req })
return {
models,
me: session ? session.user : null
}
}
}),
cache: new InMemoryCache()
})
export default client

View File

@ -30,6 +30,21 @@ const createItem = async (parent, { title, text, url, parentId }, { me, models }
export default {
Query: {
items: async (parent, args, { models }) => {
return await models.$queryRaw(`
SELECT id, "created_at" as "createdAt", title, url, text, "userId", ltree2text("path") AS "path"
FROM "Item"
WHERE "parentId" IS NULL
ORDER BY "path"`)
},
item: async (parent, { id }, { models }) => {
const res = await models.$queryRaw(`
SELECT id, "created_at" as "createdAt", title, url, text, "parentId", "userId", ltree2text("path") AS "path"
FROM "Item"
WHERE id = ${id}
ORDER BY "path"`)
return res.length ? res[0] : null
},
ncomments: async (parent, { parentId }, { models }) => {
return await models.$queryRaw(`
SELECT id, "created_at" as "createdAt", title, url, text, "userId", ltree2text("path") AS "path"
FROM "Item"
@ -62,7 +77,7 @@ export default {
}
if (!parentId) {
throw new UserInputError('Comment must have text', { argumentName: 'text' })
throw new UserInputError('Comment must have parent', { argumentName: 'text' })
}
return await createItem(parent, { text, parentId }, { me, models })
@ -85,7 +100,13 @@ export default {
return path.split('.').length - 1
},
comments: () => 0,
ncomments: async (item, args, { models }) => {
const [{ count }] = await models.$queryRaw`
SELECT count(*)
FROM "Item"
WHERE path <@ text2ltree(${item.id}) AND id != ${item.id}`
return count
},
sats: () => 0
}
}

View File

@ -3,6 +3,8 @@ import { gql } from 'apollo-server-micro'
export default gql`
extend type Query {
items: [Item!]!
item(id: ID!): Item
ncomments(id: ID!): [Item!]!
}
extend type Mutation {
@ -17,9 +19,10 @@ export default gql`
title: String
url: String
text: String
parentId: Int
user: User!
depth: Int!
sats: Int!
comments: Int!
ncomments: Int!
}
`

70
components/comment.js Normal file
View File

@ -0,0 +1,70 @@
import itemStyles from './item.module.css'
import styles from './comment.module.css'
import UpVote from '../svgs/lightning-arrow.svg'
import Text from './text'
import Link from 'next/link'
import Reply from './reply'
import { useState } from 'react'
function timeSince (timeStamp) {
const now = new Date()
const secondsPast = (now.getTime() - timeStamp) / 1000
if (secondsPast < 60) {
return parseInt(secondsPast) + 's'
}
if (secondsPast < 3600) {
return parseInt(secondsPast / 60) + 'm'
}
if (secondsPast <= 86400) {
return parseInt(secondsPast / 3600) + 'h'
}
if (secondsPast > 86400) {
const day = timeStamp.getDate()
const month = timeStamp.toDateString().match(/ [a-zA-Z]*/)[0].replace(' ', '')
const year = timeStamp.getFullYear() === now.getFullYear() ? '' : ' ' + timeStamp.getFullYear()
return day + ' ' + month + year
}
}
export default function Comment ({ item, children }) {
const [reply, setReply] = useState(false)
return (
<>
<div className={`${itemStyles.item} ${styles.item}`}>
<UpVote width={24} height={24} className={`${itemStyles.upvote} ${styles.upvote}`} />
<div className={itemStyles.hunk}>
<div className={itemStyles.other}>
<Link href={`/@${item.user.name}`} passHref>
<a>@{item.user.name}</a>
</Link>
<span> </span>
<span>{timeSince(new Date(item.createdAt))}</span>
<span> \ </span>
<span>{item.sats} sats</span>
<span> \ </span>
<span>{item.ncomments} replies</span>
</div>
<div className={styles.text}>
<Text>{item.text}</Text>
</div>
</div>
</div>
<div className={`${itemStyles.children} ${styles.children}`}>
<div
className={`${itemStyles.other} ${styles.reply}`}
onClick={() => setReply(!reply)}
>
{reply ? 'cancel' : 'reply'}
</div>
{reply && <Reply parentId={item.id} />}
{children}
{item.comments
? item.comments.map((item) => (
<Comment key={item.id} item={item} />
))
: null}
</div>
</>
)
}

View File

@ -0,0 +1,21 @@
.item {
align-items: flex-start;
}
.upvote {
margin-top: .3rem;
}
.text {
margin-top: .1rem;
}
.reply {
font-weight: bold;
cursor: pointer;
text-decoration: underline;
}
.children {
margin-top: .4rem;
}

View File

@ -62,6 +62,7 @@ export function Form ({
<Formik
initialValues={initial}
validationSchema={schema}
validateOnBlur={false}
onSubmit={(...args) =>
onSubmit(...args).catch(e => setError(e.message))}
>

View File

@ -1,3 +1,4 @@
import Link from 'next/link'
import UpVote from '../svgs/lightning-arrow.svg'
import styles from './item.module.css'
@ -21,27 +22,41 @@ function timeSince (timeStamp) {
}
}
export default function Item ({ item }) {
export default function Item ({ item, children }) {
return (
<div className='d-flex justify-content-start align-items-center'>
<UpVote width={32} height={32} className={styles.upvote} />
<div>
<div>
<span>
<span className={styles.title}>{item.title}</span>
<a className={styles.link} href={item.url}>{item.url.replace(/(^\w+:|^)\/\//, '')}</a>
</span>
</div>
<div className={styles.other}>
<span>{item.sats} sats</span>
<span> \ </span>
<span>{item.comments} comments</span>
<span> </span>
<a href='/satoshi'>@{item.user.name}</a>
<span> </span>
<span>{timeSince(new Date(item.createdAt))}</span>
<>
<div className={styles.item}>
<UpVote width={24} height={24} className={styles.upvote} />
<div className={styles.hunk}>
<div className={`${styles.main} flex-wrap flex-md-nowrap`}>
<Link href={`/items/${item.id}`} passHref>
<a className={`${styles.title} text-reset flex-md-fill flex-md-shrink-0 mr-2`}>{item.title}</a>
</Link>
{item.url && <a className={styles.link} href={item.url}>{item.url.replace(/(^\w+:|^)\/\//, '')}</a>}
</div>
<div className={styles.other}>
<span>{item.sats} sats</span>
<span> \ </span>
<span>{item.ncomments} comments</span>
<span> \ </span>
<Link href={`/@${item.user.name}`} passHref>
<a>@{item.user.name}</a>
</Link>
<span> </span>
<span>{timeSince(new Date(item.createdAt))}</span>
</div>
</div>
</div>
</div>
{children && (
<div className={styles.children}>
{children}
{item.comments
? item.comments.map((item) => (
<Item key={item.id} item={item} />
))
: null}
</div>
)}
</>
)
}

View File

@ -1,6 +1,6 @@
.upvote {
fill: grey;
margin-right: .25rem;
min-width: fit-content;
}
.upvote:hover {
@ -10,14 +10,39 @@
.title {
font-weight: 500;
white-space: normal;
margin-right: .5rem;
}
.link {
font-size: 80%;
margin-left: .5rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.other {
font-size: 70%;
color: grey;
}
.item {
display: flex;
justify-content: flex-start;
align-items: center;
min-width: 0;
}
.hunk {
overflow: hidden;
}
.main {
display: flex;
align-items: baseline;
}
.children {
margin-top: 1rem;
margin-left: 24px;
}

36
components/items.js Normal file
View File

@ -0,0 +1,36 @@
import { gql, useQuery } from '@apollo/client'
import React from 'react'
import Item from './item'
import styles from './items.module.css'
export default function Items () {
const { loading, error, data } = useQuery(
gql`
{ items {
id
createdAt
title
url
user {
name
}
sats
ncomments
} }`
)
if (error) return <div>Failed to load</div>
if (loading) return <div>Loading...</div>
const { items } = data
return (
<div className={styles.grid}>
{items.map((item, i) => (
<React.Fragment key={item.id}>
<div className={styles.rank} key={item.id}>
{i + 1}
</div>
<Item item={item} />
</React.Fragment>
))}
</div>
)
}

View File

@ -0,0 +1,12 @@
.grid {
display: grid;
grid-template-columns: auto 1fr;
}
.rank {
font-weight: 600;
align-items: center;
display: flex;
color: grey;
font-size: 90%;
}

View File

@ -1,10 +1,17 @@
import Header from './header'
import Container from 'react-bootstrap/Container'
export default function Layout ({ children }) {
export default function Layout ({ noContain, children }) {
return (
<>
<Header />
{children}
{noContain
? children
: (
<Container className='my-2 py-2 px-sm-0'>
{children}
</Container>
)}
</>
)
}

50
components/reply.js Normal file
View File

@ -0,0 +1,50 @@
import { Form, Input, SubmitButton } from '../components/form'
import * as Yup from 'yup'
import { gql, useMutation } from '@apollo/client'
import styles from './reply.module.css'
export const CommentSchema = Yup.object({
text: Yup.string().required('required').trim()
})
export default function Reply ({ parentId }) {
const [createComment] = useMutation(
gql`
mutation createComment($text: String!, $parentId: ID!) {
createComment(text: $text, parentId: $parentId) {
id
}
}`
)
return (
<div className={styles.reply}>
<Form
initial={{
text: ''
}}
schema={CommentSchema}
onSubmit={async (values) => {
const {
data: {
createComment: { id }
},
error
} = await createComment({ variables: { ...values, parentId } })
if (error) {
throw new Error({ message: error.toString() })
}
console.log('success!', id)
}}
>
<Input
name='text'
as='textarea'
rows={4}
required
/>
<SubmitButton variant='secondary' className='mt-1'>reply</SubmitButton>
</Form>
</div>
)
}

View File

@ -0,0 +1,4 @@
.reply {
max-width: 600px;
margin-top: 1rem;
}

9
components/text.js Normal file
View File

@ -0,0 +1,9 @@
import styles from './text.module.css'
export default function Text ({ children }) {
return (
<pre className={styles.text}>
{children}
</pre>
)
}

View File

@ -0,0 +1,7 @@
.text {
font-size: 90%;
white-space: pre-wrap;
word-break: break-word;
font-family: inherit;
margin: 0;
}

3
pages/[username].js Normal file
View File

@ -0,0 +1,3 @@
export default function User () {
return <div>hi</div>
}

View File

@ -11,7 +11,7 @@ const apolloServer = new ApolloServer({
const session = await getSession({ req })
return {
models,
me: session ? session.user : await models.user.findUnique({ where: { name: 'k00b' } })
me: session ? session.user : null
}
}
})

View File

@ -1,8 +1,6 @@
import { useQuery, gql } from '@apollo/client'
// import styles from '../styles/index.module.css'
import Layout from '../components/layout'
import Item from '../components/item'
import Container from 'react-bootstrap/Container'
import React from 'react'
import Items from '../components/items'
// function Users () {
// const { loading, error, data } = useQuery(gql`{ users { id, name } }`)
@ -88,42 +86,6 @@ import Container from 'react-bootstrap/Container'
// )
// }
function Items () {
const { loading, error, data } = useQuery(
gql`
{ items {
id
createdAt
title
url
user {
name
}
sats
comments
} }`
)
if (error) return <div>Failed to load</div>
if (loading) return <div>Loading...</div>
const { items } = data
return (
<>
<Container className='my-1 py-2 px-sm-0'>
<table>
<tbody>
{items.map((item, i) => (
<tr className='align-items-center' key={item.id}>
<td align='right' className='font-weight-bold text-muted mr-1'>{i + 1}</td>
<td><Item item={item} /></td>
</tr>
))}
</tbody>
</table>
</Container>
</>
)
}
export default function Index () {
return (
<Layout>

59
pages/items/[id].js Normal file
View File

@ -0,0 +1,59 @@
import gql from 'graphql-tag'
import Item from '../../components/item'
import Layout from '../../components/layout'
import ApolloClient from '../../api/client'
import Reply from '../../components/reply'
import Comment from '../../components/comment'
import Text from '../../components/text'
export async function getServerSideProps ({ params }) {
const { error, data: { item } } = await ApolloClient.query({
query:
gql`{
item(id: ${params.id}) {
id
createdAt
title
url
text
parentId
user {
name
}
sats
ncomments
}
}`
})
if (!item || error) {
return {
notFound: true
}
}
return {
props: {
item: item
}
}
}
export default function FullItem ({ item }) {
return (
<Layout>
{item.parentId
? (
<Comment item={item} />
)
: (
<>
<Item item={item}>
{item.text && <Text>{item.text}</Text>}
<Reply parentId={item.id} />
</Item>
</>
)}
</Layout>
)
}

View File

@ -1,13 +0,0 @@
import Container from "react-bootstrap/Container";
import Item from "../../../components/item";
import Layout from "../../../components/layout";
export default function AnItem () {
return (
<Layout>
<Container className='my-4'>
<Item />
</Container>
</Layout>
)
}

View File

@ -127,7 +127,7 @@ export function PostForm () {
export default function Post () {
return (
<Layout>
<Layout noContain>
<div className={styles.page}>
<div className={styles.post}>
<PostForm />

View File

@ -34,7 +34,6 @@ $container-max-widths: (
sm: 540px,
md: 720px,
lg: 900px,
xl: 900px
) !default;

View File

@ -2,10 +2,4 @@
width: 100px;
height: 100px;
background-color: grey;
}
.grid {
display: grid;
grid-template-columns: auto auto auto;
width: 100%;
}