diff --git a/api/client.js b/api/client.js new file mode 100644 index 00000000..7ded1ac8 --- /dev/null +++ b/api/client.js @@ -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 diff --git a/api/resolvers/item.js b/api/resolvers/item.js index e79fa29b..6dc160d0 100644 --- a/api/resolvers/item.js +++ b/api/resolvers/item.js @@ -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 } } diff --git a/api/typeDefs/item.js b/api/typeDefs/item.js index 1380ad85..20319998 100644 --- a/api/typeDefs/item.js +++ b/api/typeDefs/item.js @@ -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! } ` diff --git a/components/comment.js b/components/comment.js new file mode 100644 index 00000000..03418f45 --- /dev/null +++ b/components/comment.js @@ -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 ( + <> +
+ +
+
+ + @{item.user.name} + + + {timeSince(new Date(item.createdAt))} + \ + {item.sats} sats + \ + {item.ncomments} replies +
+
+ {item.text} +
+
+
+
+
setReply(!reply)} + > + {reply ? 'cancel' : 'reply'} +
+ {reply && } + {children} + {item.comments + ? item.comments.map((item) => ( + + )) + : null} +
+ + ) +} diff --git a/components/comment.module.css b/components/comment.module.css new file mode 100644 index 00000000..9064d16d --- /dev/null +++ b/components/comment.module.css @@ -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; +} \ No newline at end of file diff --git a/components/form.js b/components/form.js index f02b20af..996ba526 100644 --- a/components/form.js +++ b/components/form.js @@ -62,6 +62,7 @@ export function Form ({ onSubmit(...args).catch(e => setError(e.message))} > diff --git a/components/item.js b/components/item.js index abfa5dd0..6b14e246 100644 --- a/components/item.js +++ b/components/item.js @@ -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 ( -
- -
-
- - {item.title} - {item.url.replace(/(^\w+:|^)\/\//, '')} - -
-
- {item.sats} sats - \ - {item.comments} comments - - @{item.user.name} - - {timeSince(new Date(item.createdAt))} + <> +
+ +
+ +
+ {item.sats} sats + \ + {item.ncomments} comments + \ + + @{item.user.name} + + + {timeSince(new Date(item.createdAt))} +
-
+ {children && ( +
+ {children} + {item.comments + ? item.comments.map((item) => ( + + )) + : null} +
+ )} + ) } diff --git a/components/item.module.css b/components/item.module.css index a5516049..86810d07 100644 --- a/components/item.module.css +++ b/components/item.module.css @@ -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; } \ No newline at end of file diff --git a/components/items.js b/components/items.js new file mode 100644 index 00000000..12b0a51d --- /dev/null +++ b/components/items.js @@ -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
Failed to load
+ if (loading) return
Loading...
+ const { items } = data + return ( +
+ {items.map((item, i) => ( + +
+ {i + 1} +
+ +
+ ))} +
+ ) +} diff --git a/components/items.module.css b/components/items.module.css new file mode 100644 index 00000000..0f04c33e --- /dev/null +++ b/components/items.module.css @@ -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%; +} \ No newline at end of file diff --git a/components/layout.js b/components/layout.js index d60074d1..207fa3a2 100644 --- a/components/layout.js +++ b/components/layout.js @@ -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 ( <>
- {children} + {noContain + ? children + : ( + + {children} + + )} ) } diff --git a/components/reply.js b/components/reply.js new file mode 100644 index 00000000..2e572296 --- /dev/null +++ b/components/reply.js @@ -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 ( +
+
{ + const { + data: { + createComment: { id } + }, + error + } = await createComment({ variables: { ...values, parentId } }) + if (error) { + throw new Error({ message: error.toString() }) + } + console.log('success!', id) + }} + > + + reply +
+
+ ) +} diff --git a/components/reply.module.css b/components/reply.module.css new file mode 100644 index 00000000..4d060507 --- /dev/null +++ b/components/reply.module.css @@ -0,0 +1,4 @@ +.reply { + max-width: 600px; + margin-top: 1rem; +} \ No newline at end of file diff --git a/components/text.js b/components/text.js new file mode 100644 index 00000000..309f0cc2 --- /dev/null +++ b/components/text.js @@ -0,0 +1,9 @@ +import styles from './text.module.css' + +export default function Text ({ children }) { + return ( +
+      {children}
+    
+ ) +} diff --git a/components/text.module.css b/components/text.module.css new file mode 100644 index 00000000..4c3ede34 --- /dev/null +++ b/components/text.module.css @@ -0,0 +1,7 @@ +.text { + font-size: 90%; + white-space: pre-wrap; + word-break: break-word; + font-family: inherit; + margin: 0; +} \ No newline at end of file diff --git a/pages/[username].js b/pages/[username].js new file mode 100644 index 00000000..f0ae0b76 --- /dev/null +++ b/pages/[username].js @@ -0,0 +1,3 @@ +export default function User () { + return
hi
+} diff --git a/pages/api/graphql.js b/pages/api/graphql.js index 171b9093..0db57711 100644 --- a/pages/api/graphql.js +++ b/pages/api/graphql.js @@ -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 } } }) diff --git a/pages/index.js b/pages/index.js index c258f312..98c12118 100644 --- a/pages/index.js +++ b/pages/index.js @@ -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
Failed to load
- if (loading) return
Loading...
- const { items } = data - return ( - <> - - - - {items.map((item, i) => ( - - - - - ))} - -
{i + 1}
-
- - ) -} - export default function Index () { return ( diff --git a/pages/items/[id].js b/pages/items/[id].js new file mode 100644 index 00000000..75de19af --- /dev/null +++ b/pages/items/[id].js @@ -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 ( + + {item.parentId + ? ( + + ) + : ( + <> + + {item.text && {item.text}} + + + + )} + + ) +} diff --git a/pages/items/[id]/[[...extra]].js b/pages/items/[id]/[[...extra]].js deleted file mode 100644 index ed2d780c..00000000 --- a/pages/items/[id]/[[...extra]].js +++ /dev/null @@ -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 ( - - - - - - ) -} \ No newline at end of file diff --git a/pages/post.js b/pages/post.js index 41202544..f7d2c28e 100644 --- a/pages/post.js +++ b/pages/post.js @@ -127,7 +127,7 @@ export function PostForm () { export default function Post () { return ( - +
diff --git a/styles/globals.scss b/styles/globals.scss index 83f8b9d6..43e26fac 100644 --- a/styles/globals.scss +++ b/styles/globals.scss @@ -34,7 +34,6 @@ $container-max-widths: ( sm: 540px, md: 720px, lg: 900px, - xl: 900px ) !default; diff --git a/styles/index.module.css b/styles/index.module.css index b9af554e..f9694e3e 100644 --- a/styles/index.module.css +++ b/styles/index.module.css @@ -2,10 +2,4 @@ width: 100px; height: 100px; background-color: grey; -} - -.grid { - display: grid; - grid-template-columns: auto auto auto; - width: 100%; } \ No newline at end of file