more progress
This commit is contained in:
parent
bc5d4d4808
commit
28ed42fc29
|
@ -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
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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!
|
||||
}
|
||||
`
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -62,6 +62,7 @@ export function Form ({
|
|||
<Formik
|
||||
initialValues={initial}
|
||||
validationSchema={schema}
|
||||
validateOnBlur={false}
|
||||
onSubmit={(...args) =>
|
||||
onSubmit(...args).catch(e => setError(e.message))}
|
||||
>
|
||||
|
|
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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%;
|
||||
}
|
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
.reply {
|
||||
max-width: 600px;
|
||||
margin-top: 1rem;
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
import styles from './text.module.css'
|
||||
|
||||
export default function Text ({ children }) {
|
||||
return (
|
||||
<pre className={styles.text}>
|
||||
{children}
|
||||
</pre>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
.text {
|
||||
font-size: 90%;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
font-family: inherit;
|
||||
margin: 0;
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
export default function User () {
|
||||
return <div>hi</div>
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -127,7 +127,7 @@ export function PostForm () {
|
|||
|
||||
export default function Post () {
|
||||
return (
|
||||
<Layout>
|
||||
<Layout noContain>
|
||||
<div className={styles.page}>
|
||||
<div className={styles.post}>
|
||||
<PostForm />
|
||||
|
|
|
@ -34,7 +34,6 @@ $container-max-widths: (
|
|||
sm: 540px,
|
||||
md: 720px,
|
||||
lg: 900px,
|
||||
xl: 900px
|
||||
) !default;
|
||||
|
||||
|
||||
|
|
|
@ -2,10 +2,4 @@
|
|||
width: 100px;
|
||||
height: 100px;
|
||||
background-color: grey;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: auto auto auto;
|
||||
width: 100%;
|
||||
}
|
Loading…
Reference in New Issue