working previews

This commit is contained in:
keyan 2021-07-07 19:15:27 -05:00
parent 38df1fcdb7
commit 68e80b615c
17 changed files with 7072 additions and 3022 deletions

View File

@ -7,7 +7,7 @@ import typeDefs from './typeDefs'
import models from './models' import models from './models'
export default async function serverSideClient (req) { export default async function serverSideClient (req) {
const session = await getSession({ req }) const session = req && await getSession({ req })
return new ApolloClient({ return new ApolloClient({
ssrMode: true, ssrMode: true,
// Instead of "createHttpLink" use SchemaLink here // Instead of "createHttpLink" use SchemaLink here

View File

@ -131,15 +131,6 @@ export default {
FROM "Item" FROM "Item"
WHERE "userId" = $1 AND "parentId" IS NOT NULL WHERE "userId" = $1 AND "parentId" IS NOT NULL
ORDER BY created_at DESC`, Number(userId)) ORDER BY created_at DESC`, Number(userId))
},
root: async (parent, { id }, { models }) => {
return (await models.$queryRaw(`
${SELECT}
FROM "Item"
WHERE id = (
SELECT ltree2text(subltree(path, 0, 1))::integer
FROM "Item"
WHERE id = $1)`, Number(id)))[0]
} }
}, },
@ -238,6 +229,24 @@ export default {
}) })
return sats || 0 return sats || 0
},
root: async (item, args, { models }) => {
if (!item.parentId) {
return null
}
return (await models.$queryRaw(`
${SELECT}
FROM "Item"
WHERE id = (
SELECT ltree2text(subltree(path, 0, 1))::integer
FROM "Item"
WHERE id = $1)`, Number(item.id)))[0]
},
parent: async (item, args, { models }) => {
if (!item.parentId) {
return null
}
return await models.item.findUnique({ where: { id: item.parentId } })
} }
} }
} }

View File

@ -7,7 +7,6 @@ export default gql`
notifications: [Item!]! notifications: [Item!]!
item(id: ID!): Item item(id: ID!): Item
userComments(userId: ID!): [Item!] userComments(userId: ID!): [Item!]
root(id: ID!): Item
} }
extend type Mutation { extend type Mutation {
@ -34,6 +33,8 @@ export default gql`
url: String url: String
text: String text: String
parentId: Int parentId: Int
parent: Item
root: Item
user: User! user: User!
depth: Int! depth: Int!
sats: Int! sats: Int!

View File

@ -4,7 +4,6 @@ import Text from './text'
import Link from 'next/link' import Link from 'next/link'
import Reply from './reply' import Reply from './reply'
import { useEffect, useRef, useState } from 'react' import { useEffect, useRef, useState } from 'react'
import { gql, useQuery } from '@apollo/client'
import { timeSince } from '../lib/time' import { timeSince } from '../lib/time'
import UpVote from './upvote' import UpVote from './upvote'
import Eye from '../svgs/eye-fill.svg' import Eye from '../svgs/eye-fill.svg'
@ -12,15 +11,6 @@ import EyeClose from '../svgs/eye-close-line.svg'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
function Parent ({ item }) { function Parent ({ item }) {
const { data } = useQuery(
gql`{
root(id: ${item.id}) {
id
title
}
}`
)
const ParentFrag = () => ( const ParentFrag = () => (
<> <>
<span> \ </span> <span> \ </span>
@ -30,16 +20,16 @@ function Parent ({ item }) {
</> </>
) )
if (!data) { if (!item.root) {
return <ParentFrag /> return <ParentFrag />
} }
return ( return (
<> <>
{data.root.id !== item.parentId && <ParentFrag />} {Number(item.root.id) !== Number(item.parentId) && <ParentFrag />}
<span> \ </span> <span> \ </span>
<Link href={`/items/${data.root.id}`} passHref> <Link href={`/items/${item.root.id}`} passHref>
<a onClick={e => e.stopPropagation()} className='text-reset'>root: {data.root.title}</a> <a onClick={e => e.stopPropagation()} className='text-reset'>root: {item.root.title}</a>
</Link> </Link>
</> </>
) )

View File

@ -81,7 +81,7 @@ export default function Header () {
</div> </div>
) )
} else { } else {
return path !== '/login' && <Button href='/login' onClick={signIn}>login</Button> return path !== '/login' && <Button id='login' href='/login' onClick={signIn}>login</Button>
} }
} }
@ -133,6 +133,9 @@ export function HeaderPreview () {
<Link href='/' passHref> <Link href='/' passHref>
<Navbar.Brand className={`${styles.brand} d-none d-sm-block`}>STACKER NEWS</Navbar.Brand> <Navbar.Brand className={`${styles.brand} d-none d-sm-block`}>STACKER NEWS</Navbar.Brand>
</Link> </Link>
<Nav.Item className='text-monospace' style={{ opacity: '.5' }}>
<Price />
</Nav.Item>
</Nav> </Nav>
</Navbar> </Navbar>
</Container> </Container>

View File

@ -2,35 +2,13 @@ import Header from './header'
import Head from 'next/head' import Head from 'next/head'
import Container from 'react-bootstrap/Container' import Container from 'react-bootstrap/Container'
import { LightningProvider } from './lightning' import { LightningProvider } from './lightning'
import { useRouter } from 'next/router'
import Footer from './footer' import Footer from './footer'
import { NextSeo } from 'next-seo' import Seo from './seo'
export default function Layout ({ noContain, noFooter, children }) { export default function Layout ({ noContain, noFooter, children }) {
const router = useRouter()
const defaultTitle = router.asPath.split('?')[0].slice(1)
const fullTitle = `${defaultTitle && `${defaultTitle} \\ `}stacker news`
const desc = 'Discuss Bitcoin. Stack sats. News for plebs.'
return ( return (
<> <>
<NextSeo <Seo />
title={fullTitle}
description={desc}
openGraph={{
title: fullTitle,
description: desc,
images: [
{
url: 'https://stacker.news/favicon.png'
}
],
site_name: 'Stacker News'
}}
twitter={{
site: '@stacker_news',
cardType: 'summary_large_image'
}}
/>
<LightningProvider> <LightningProvider>
<Head> <Head>
<meta name='viewport' content='initial-scale=1.0, width=device-width' /> <meta name='viewport' content='initial-scale=1.0, width=device-width' />

51
components/seo.js Normal file
View File

@ -0,0 +1,51 @@
import { NextSeo } from 'next-seo'
import { useRouter } from 'next/router'
import RemoveMarkdown from 'remove-markdown'
export default function Seo ({ item, user }) {
const router = useRouter()
const pathNoQuery = router.asPath.split('?')[0]
const defaultTitle = pathNoQuery.slice(1)
let fullTitle = `${defaultTitle && `${defaultTitle} \\ `}stacker news`
let desc = 'Bitcoin news powered by the Lightning Network. Stack sats with real Bitcoiners.'
if (item) {
if (item.title) {
fullTitle = `${item.title} \\ stacker news`
} else if (item.root) {
fullTitle = `reply on: ${item.root.title} \\ stacker news`
}
if (item.text) {
desc = RemoveMarkdown(item.text)
if (desc) {
desc = desc.replace(/\s+/g, ' ')
}
} else {
desc = `@${item.user.name} stacked ${item.sats} sats ${item.url ? `posting ${item.url}` : ''}`
}
desc += ` [${item.ncomments} comments, ${item.boost} boost]`
}
if (user) {
desc = `@${user.name} has [${user.stacked} stacked, ${user.sats} sats, ${user.nitems} posts, ${user.ncomments} comments]`
}
return (
<NextSeo
title={fullTitle}
description={desc}
openGraph={{
title: fullTitle,
description: desc,
images: [
{
url: 'https://stacker.news/api/capture' + pathNoQuery
}
],
site_name: 'Stacker News'
}}
twitter={{
site: '@stacker_news',
cardType: 'summary_large_image'
}}
/>
)
}

View File

@ -13,6 +13,10 @@ export const COMMENT_FIELDS = gql`
boost boost
meSats meSats
ncomments ncomments
root {
id
title
}
} }
` `

View File

@ -14,6 +14,10 @@ export const ITEM_FIELDS = gql`
boost boost
meSats meSats
ncomments ncomments
root {
id
title
}
}` }`
export const MORE_ITEMS = gql` export const MORE_ITEMS = gql`

9848
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -23,6 +23,8 @@
"next": "^10.2.3", "next": "^10.2.3",
"next-auth": "^3.13.3", "next-auth": "^3.13.3",
"next-seo": "^4.24.0", "next-seo": "^4.24.0",
"node-webshot": "^1.0.4",
"pageres": "^6.2.3",
"prisma": "^2.25.0", "prisma": "^2.25.0",
"qrcode.react": "^1.0.1", "qrcode.react": "^1.0.1",
"react": "17.0.1", "react": "17.0.1",
@ -31,6 +33,7 @@
"react-markdown": "^6.0.2", "react-markdown": "^6.0.2",
"react-syntax-highlighter": "^15.4.3", "react-syntax-highlighter": "^15.4.3",
"remark-gfm": "^1.0.0", "remark-gfm": "^1.0.0",
"remove-markdown": "^0.3.0",
"sass": "^1.32.8", "sass": "^1.32.8",
"secp256k1": "^4.0.2", "secp256k1": "^4.0.2",
"swr": "^0.5.4", "swr": "^0.5.4",

View File

@ -3,6 +3,7 @@ import Items from '../components/items'
import { gql } from '@apollo/client' import { gql } from '@apollo/client'
import ApolloClient from '../api/client' import ApolloClient from '../api/client'
import UserHeader from '../components/user-header' import UserHeader from '../components/user-header'
import Seo from '../components/seo'
export async function getServerSideProps ({ req, params }) { export async function getServerSideProps ({ req, params }) {
const { error, data: { user } } = await (await ApolloClient(req)).query({ const { error, data: { user } } = await (await ApolloClient(req)).query({
@ -36,6 +37,7 @@ export async function getServerSideProps ({ req, params }) {
export default function User ({ user }) { export default function User ({ user }) {
return ( return (
<Layout> <Layout>
<Seo user={user} />
<UserHeader user={user} /> <UserHeader user={user} />
<Items variables={{ sort: 'user', userId: user.id }} /> <Items variables={{ sort: 'user', userId: user.id }} />
</Layout> </Layout>

View File

@ -3,6 +3,7 @@ import { gql } from '@apollo/client'
import ApolloClient from '../../api/client' import ApolloClient from '../../api/client'
import UserHeader from '../../components/user-header' import UserHeader from '../../components/user-header'
import CommentsFlat from '../../components/comments-flat' import CommentsFlat from '../../components/comments-flat'
import Seo from '../../components/seo'
export async function getServerSideProps ({ req, params }) { export async function getServerSideProps ({ req, params }) {
const { error, data: { user } } = await (await ApolloClient(req)).query({ const { error, data: { user } } = await (await ApolloClient(req)).query({
@ -36,6 +37,7 @@ export async function getServerSideProps ({ req, params }) {
export default function UserComments ({ user }) { export default function UserComments ({ user }) {
return ( return (
<Layout> <Layout>
<Seo user={user} />
<UserHeader user={user} /> <UserHeader user={user} />
<CommentsFlat variables={{ userId: user.id }} includeParent noReply clickToContext /> <CommentsFlat variables={{ userId: user.id }} includeParent noReply clickToContext />
</Layout> </Layout>

View File

@ -0,0 +1,11 @@
import Pageres from 'pageres'
import path from 'path'
export default async function handler (req, res) {
const url = 'http://' + path.join('localhost:3000', ...(req.query.path || []))
res.setHeader('Content-Type', 'image/png')
const streams = await new Pageres({ crop: true, delay: 1 })
.src(url, ['600x300'])
.run()
res.status(200).end(streams[0])
}

View File

@ -8,7 +8,7 @@ import { COMMENTS } from '../../fragments/comments'
import { ITEM_FIELDS } from '../../fragments/items' import { ITEM_FIELDS } from '../../fragments/items'
import { gql, useQuery } from '@apollo/client' import { gql, useQuery } from '@apollo/client'
import styles from '../../styles/item.module.css' import styles from '../../styles/item.module.css'
import Head from 'next/head' import Seo from '../../components/seo'
export async function getServerSideProps ({ params: { id } }) { export async function getServerSideProps ({ params: { id } }) {
return { return {
@ -60,13 +60,11 @@ function LoadItem ({ query }) {
return ( return (
<> <>
<Seo item={item} />
{item.parentId {item.parentId
? <Comment item={item} replyOpen includeParent noComments /> ? <Comment item={item} replyOpen includeParent noComments />
: ( : (
<> <>
<Head>
<title>{item.title} \ stacker news</title>
</Head>
<Item item={item}> <Item item={item}>
{item.text && <div className='mb-3'><Text>{item.text}</Text></div>} {item.text && <div className='mb-3'><Text>{item.text}</Text></div>}
<Reply parentId={item.id} /> <Reply parentId={item.id} />

View File

@ -0,0 +1,44 @@
import ApolloClient from '../../api/client'
import { MORE_ITEMS } from '../../fragments/items'
import Item from '../../components/item'
import styles from '../../components/items.module.css'
import LayoutPreview from '../../components/layout-preview'
import { LightningProvider } from '../../components/lightning'
// we can't SSR on the normal page because we'd have to hyrdate the cache
// on the client which is a lot of work, i.e. a bit fat todo
export async function getServerSideProps ({ params }) {
// grab the item on the server side
const { error, data: { moreItems: { items } } } = await (await ApolloClient()).query({
query: MORE_ITEMS,
variables: { sort: 'hot' }
})
if (!items || error) {
return {
notFound: true
}
}
return {
props: {
items
}
}
}
export default function IndexPreview ({ items }) {
return (
<>
<LayoutPreview>
<LightningProvider>
<div className={styles.grid}>
{items.map((item, i) => (
<Item item={item} rank={i + 1} key={item.id} />
))}
</div>
</LightningProvider>
</LayoutPreview>
</>
)
}

View File

@ -9,9 +9,9 @@ import Comment from '../../../components/comment'
// we can't SSR on the normal page because we'd have to hyrdate the cache // we can't SSR on the normal page because we'd have to hyrdate the cache
// on the client which is a lot of work, i.e. a bit fat todo // on the client which is a lot of work, i.e. a bit fat todo
export async function getServerSideProps ({ req, params }) { export async function getServerSideProps ({ params }) {
// grab the item on the server side // grab the item on the server side
const { error, data: { item } } = await (await ApolloClient(req)).query({ const { error, data: { item } } = await (await ApolloClient()).query({
query: query:
gql` gql`
${ITEM_FIELDS} ${ITEM_FIELDS}
@ -36,18 +36,30 @@ export async function getServerSideProps ({ req, params }) {
} }
} }
// export async function getStaticPaths () {
// return {
// paths: [],
// // Enable statically generating additional pages
// // For example: `/posts/3`
// fallback: 'blocking'
// }
// }
export default function ItemPreview ({ item }) { export default function ItemPreview ({ item }) {
return ( return (
<LayoutPreview> <>
<LightningProvider> <LayoutPreview>
{item.parentId <LightningProvider>
? <Comment item={item} includeParent noComments />
: ( {item.parentId
<Item item={item}> ? <Comment item={item} includeParent noReply noComments />
{item.text && <div className='mb-3'><Text>{item.text}</Text></div>} : (
</Item> <Item item={item}>
)} {item.text && <div className='mb-3'><Text>{item.text}</Text></div>}
</LightningProvider> </Item>
</LayoutPreview> )}
</LightningProvider>
</LayoutPreview>
</>
) )
} }