diff --git a/.babelrc b/.babelrc new file mode 100644 index 00000000..078408f0 --- /dev/null +++ b/.babelrc @@ -0,0 +1,8 @@ +{ + "presets": [ + "next/babel" + ], + "plugins": [ + "inline-react-svg" + ] +} \ No newline at end of file diff --git a/api/resolvers/index.js b/api/resolvers/index.js index d2d6b137..e2d5207c 100644 --- a/api/resolvers/index.js +++ b/api/resolvers/index.js @@ -1,4 +1,5 @@ import user from './user' import message from './message' +import item from './item' -export default [user, message] +export default [user, item, message] diff --git a/api/resolvers/item.js b/api/resolvers/item.js new file mode 100644 index 00000000..b85e4523 --- /dev/null +++ b/api/resolvers/item.js @@ -0,0 +1,61 @@ +import { UserInputError, AuthenticationError } from 'apollo-server-micro' + +export default { + Query: { + items: async (parent, args, { models }) => { + return await models.$queryRaw(` + SELECT id, text, "userId", ltree2text("path") AS "path" + FROM "Item" + ORDER BY "path"`) + } + }, + + Mutation: { + createItem: async (parent, { text, parentId }, { me, models }) => { + if (!me) { + throw new AuthenticationError('You must be logged in') + } + + if (!text) { + throw new UserInputError('Item must have text', { argumentName: 'text' }) + } + + const data = { + text, + user: { + connect: { + name: me.name + } + } + } + + if (parentId) { + data.parent = { + connect: { + id: parseInt(parentId) + } + } + } + + return await models.item.create({ data }) + } + }, + + Item: { + user: async (item, args, { models }) => + await models.user.findUnique({ where: { id: item.userId } }), + depth: async (item, args, { models }) => { + if (item.path) { + return item.path.split('.').length - 1 + } + + // as the result of a mutation, path is not populated + const [{ path }] = await models.$queryRaw` + SELECT ltree2text("path") AS "path" + FROM "Item" + WHERE id = ${item.id}` + + return path.split('.').length - 1 + } + } +} diff --git a/api/typeDefs/index.js b/api/typeDefs/index.js index b4b6cd84..ae161dc3 100644 --- a/api/typeDefs/index.js +++ b/api/typeDefs/index.js @@ -2,6 +2,7 @@ import { gql } from 'apollo-server-micro' import user from './user' import message from './message' +import item from './item' const link = gql` type Query { @@ -17,4 +18,4 @@ const link = gql` } ` -export default [link, user, message] +export default [link, user, item, message] diff --git a/api/typeDefs/item.js b/api/typeDefs/item.js new file mode 100644 index 00000000..99c2dcf4 --- /dev/null +++ b/api/typeDefs/item.js @@ -0,0 +1,18 @@ +import { gql } from 'apollo-server-micro' + +export default gql` + extend type Query { + items: [Item!]! + } + + extend type Mutation { + createItem(text: String!, parentId: ID): Item! + } + + type Item { + id: ID! + text: String! + user: User! + depth: Int! + } +` diff --git a/components/header.js b/components/header.js index 83803d8d..3571ea9f 100644 --- a/components/header.js +++ b/components/header.js @@ -1,28 +1,55 @@ import { signOut, signIn, useSession } from 'next-auth/client' +import Navbar from 'react-bootstrap/Navbar' +import Nav from 'react-bootstrap/Nav' +import Link from 'next/link' +import styles from './header.module.css' +import { useRouter } from 'next/router' export default function Header () { const [session, loading] = useSession() + const router = useRouter() - if (loading) { - return

Validating session ...

- } + const Corner = () => { + if (loading) { + return null + } - if (session) { - return ( - <> -

- {session.user.name} ({session.user.email}) -

- - - ) + if (session) { + return ( + <> + {session.user.name} + + logout + + + ) + } else { + return login + } } return ( - + <> + + + STACKER NEWS + + + + + ) } diff --git a/components/header.module.css b/components/header.module.css new file mode 100644 index 00000000..eefa8bbf --- /dev/null +++ b/components/header.module.css @@ -0,0 +1,11 @@ +.brand { + font-family: lightning; + font-size: 2rem; + padding: 0; + line-height: 100%; + margin-bottom: -.3rem; +} + +.navbar { + padding: 0rem 1.75rem; +} \ No newline at end of file diff --git a/components/layout.js b/components/layout.js new file mode 100644 index 00000000..d60074d1 --- /dev/null +++ b/components/layout.js @@ -0,0 +1,10 @@ +import Header from './header' + +export default function Layout ({ children }) { + return ( + <> +
+ {children} + + ) +} diff --git a/components/price.js b/components/price.js new file mode 100644 index 00000000..cdfa5298 --- /dev/null +++ b/components/price.js @@ -0,0 +1,11 @@ +import useSWR from 'swr' + +const fetcher = url => fetch(url).then(res => res.json()) + +export default function Price () { + const { data } = useSWR('https://api.coinbase.com/v2/prices/BTC-USD/spot', fetcher) + + if (!data) return null + + return data.data.amount +} diff --git a/package.json b/package.json index 93689a5a..f4a2ed2a 100644 --- a/package.json +++ b/package.json @@ -8,12 +8,15 @@ "start": "next start" }, "dependencies": { + "@apollo/client": "^3.3.13", "@prisma/client": "^2.19.0", "apollo-server-micro": "^2.21.2", + "bootstrap": "^4.6.0", "graphql": "^15.5.0", "next": "10.0.9", "next-auth": "^3.13.3", "react": "17.0.1", + "react-bootstrap": "^1.5.2", "react-dom": "17.0.1", "swr": "^0.5.4" }, @@ -31,6 +34,7 @@ }, "devDependencies": { "babel-eslint": "^10.1.0", + "babel-plugin-inline-react-svg": "^2.0.1", "eslint": "^7.22.0", "eslint-plugin-compat": "^3.9.0", "prisma": "2.19.0", diff --git a/pages/_app.js b/pages/_app.js index 70177469..5efc326e 100644 --- a/pages/_app.js +++ b/pages/_app.js @@ -1,7 +1,21 @@ +import 'bootstrap/dist/css/bootstrap.min.css' import '../styles/globals.css' +import { ApolloClient, InMemoryCache, ApolloProvider } from '@apollo/client' +import { Provider } from 'next-auth/client' + +const client = new ApolloClient({ + uri: '/api/graphql', + cache: new InMemoryCache() +}) function MyApp ({ Component, pageProps }) { - return + return ( + + + + + + ) } export default MyApp diff --git a/pages/api/graphql.js b/pages/api/graphql.js index 88035e65..171b9093 100644 --- a/pages/api/graphql.js +++ b/pages/api/graphql.js @@ -2,14 +2,18 @@ import { ApolloServer } from 'apollo-server-micro' import resolvers from '../../api/resolvers' import models from '../../api/models' import typeDefs from '../../api/typeDefs' +import { getSession } from 'next-auth/client' const apolloServer = new ApolloServer({ typeDefs, resolvers, - context: async () => ({ - models, - me: await models.user.findUnique({ where: { name: 'k00b' } }) - }) + context: async ({ req }) => { + const session = await getSession({ req }) + return { + models, + me: session ? session.user : await models.user.findUnique({ where: { name: 'k00b' } }) + } + } }) export const config = { diff --git a/pages/index.js b/pages/index.js index 88f218d4..b06a2735 100644 --- a/pages/index.js +++ b/pages/index.js @@ -1,31 +1,129 @@ -import useSWR from 'swr' -import Header from '../components/header' - -const fetcher = (query) => - fetch('/api/graphql', { - method: 'POST', - headers: { - 'Content-type': 'application/json' - }, - body: JSON.stringify({ query }) - }) - .then((res) => res.json()) - .then((json) => json.data) - -export default function Index () { - const { data, error } = useSWR('{ users { name } }', fetcher) +import { useQuery, gql, useMutation } from '@apollo/client' +import { useState } from 'react' +import { useSession } from 'next-auth/client' +import styles from '../styles/index.module.css' +import Layout from '../components/layout' +function Users () { + const { loading, error, data } = useQuery(gql`{ users { id, name } }`) if (error) return
Failed to load
- if (!data) return
Loading...
- + if (loading) return
Loading...
const { users } = data - return (
-
- {users.map((user, i) => ( -
{user.name}
+ {users.map(user => ( +
{user.name}
))}
) } + +function NewItem ({ parentId }) { + const [session] = useSession() + const [createItem] = useMutation( + gql` + mutation CreateItem($text: String!, $parentId: ID) { + createItem(text: $text, parentId: $parentId) { + id + } + }`, { + update (cache, { data: { createItem } }) { + cache.modify({ + fields: { + items (existingItems = [], { readField }) { + const newItemRef = cache.writeFragment({ + data: createItem, + fragment: gql` + fragment NewItem on Item { + id + user { + name + } + text + depth + } + ` + }) + for (let i = 0; i < existingItems.length; i++) { + if (readField('id', existingItems[i]) === parentId) { + return [...existingItems.slice(0, i), newItemRef, ...existingItems.slice(i)] + } + } + return [newItemRef, ...existingItems] + } + } + }) + } + }) + const [open, setOpen] = useState(false) + + if (!session) return null + + if (!open) { + return ( +
setOpen(true)}> + {parentId ? 'reply' : 'submit'} +
+ ) + } + + let text + return ( +
{ + e.preventDefault() + createItem({ variables: { text: text.value, parentId } }) + setOpen(false) + text.value = '' + }} + > +