working previews
This commit is contained in:
parent
38df1fcdb7
commit
68e80b615c
|
@ -7,7 +7,7 @@ import typeDefs from './typeDefs'
|
|||
import models from './models'
|
||||
|
||||
export default async function serverSideClient (req) {
|
||||
const session = await getSession({ req })
|
||||
const session = req && await getSession({ req })
|
||||
return new ApolloClient({
|
||||
ssrMode: true,
|
||||
// Instead of "createHttpLink" use SchemaLink here
|
||||
|
|
|
@ -131,15 +131,6 @@ export default {
|
|||
FROM "Item"
|
||||
WHERE "userId" = $1 AND "parentId" IS NOT NULL
|
||||
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
|
||||
},
|
||||
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 } })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,7 +7,6 @@ export default gql`
|
|||
notifications: [Item!]!
|
||||
item(id: ID!): Item
|
||||
userComments(userId: ID!): [Item!]
|
||||
root(id: ID!): Item
|
||||
}
|
||||
|
||||
extend type Mutation {
|
||||
|
@ -34,6 +33,8 @@ export default gql`
|
|||
url: String
|
||||
text: String
|
||||
parentId: Int
|
||||
parent: Item
|
||||
root: Item
|
||||
user: User!
|
||||
depth: Int!
|
||||
sats: Int!
|
||||
|
|
|
@ -4,7 +4,6 @@ import Text from './text'
|
|||
import Link from 'next/link'
|
||||
import Reply from './reply'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { gql, useQuery } from '@apollo/client'
|
||||
import { timeSince } from '../lib/time'
|
||||
import UpVote from './upvote'
|
||||
import Eye from '../svgs/eye-fill.svg'
|
||||
|
@ -12,15 +11,6 @@ import EyeClose from '../svgs/eye-close-line.svg'
|
|||
import { useRouter } from 'next/router'
|
||||
|
||||
function Parent ({ item }) {
|
||||
const { data } = useQuery(
|
||||
gql`{
|
||||
root(id: ${item.id}) {
|
||||
id
|
||||
title
|
||||
}
|
||||
}`
|
||||
)
|
||||
|
||||
const ParentFrag = () => (
|
||||
<>
|
||||
<span> \ </span>
|
||||
|
@ -30,16 +20,16 @@ function Parent ({ item }) {
|
|||
</>
|
||||
)
|
||||
|
||||
if (!data) {
|
||||
if (!item.root) {
|
||||
return <ParentFrag />
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{data.root.id !== item.parentId && <ParentFrag />}
|
||||
{Number(item.root.id) !== Number(item.parentId) && <ParentFrag />}
|
||||
<span> \ </span>
|
||||
<Link href={`/items/${data.root.id}`} passHref>
|
||||
<a onClick={e => e.stopPropagation()} className='text-reset'>root: {data.root.title}</a>
|
||||
<Link href={`/items/${item.root.id}`} passHref>
|
||||
<a onClick={e => e.stopPropagation()} className='text-reset'>root: {item.root.title}</a>
|
||||
</Link>
|
||||
</>
|
||||
)
|
||||
|
|
|
@ -81,7 +81,7 @@ export default function Header () {
|
|||
</div>
|
||||
)
|
||||
} 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>
|
||||
<Navbar.Brand className={`${styles.brand} d-none d-sm-block`}>STACKER NEWS</Navbar.Brand>
|
||||
</Link>
|
||||
<Nav.Item className='text-monospace' style={{ opacity: '.5' }}>
|
||||
<Price />
|
||||
</Nav.Item>
|
||||
</Nav>
|
||||
</Navbar>
|
||||
</Container>
|
||||
|
|
|
@ -2,35 +2,13 @@ import Header from './header'
|
|||
import Head from 'next/head'
|
||||
import Container from 'react-bootstrap/Container'
|
||||
import { LightningProvider } from './lightning'
|
||||
import { useRouter } from 'next/router'
|
||||
import Footer from './footer'
|
||||
import { NextSeo } from 'next-seo'
|
||||
import Seo from './seo'
|
||||
|
||||
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 (
|
||||
<>
|
||||
<NextSeo
|
||||
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'
|
||||
}}
|
||||
/>
|
||||
<Seo />
|
||||
<LightningProvider>
|
||||
<Head>
|
||||
<meta name='viewport' content='initial-scale=1.0, width=device-width' />
|
||||
|
|
|
@ -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'
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
|
@ -13,6 +13,10 @@ export const COMMENT_FIELDS = gql`
|
|||
boost
|
||||
meSats
|
||||
ncomments
|
||||
root {
|
||||
id
|
||||
title
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
|
|
|
@ -14,6 +14,10 @@ export const ITEM_FIELDS = gql`
|
|||
boost
|
||||
meSats
|
||||
ncomments
|
||||
root {
|
||||
id
|
||||
title
|
||||
}
|
||||
}`
|
||||
|
||||
export const MORE_ITEMS = gql`
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -23,6 +23,8 @@
|
|||
"next": "^10.2.3",
|
||||
"next-auth": "^3.13.3",
|
||||
"next-seo": "^4.24.0",
|
||||
"node-webshot": "^1.0.4",
|
||||
"pageres": "^6.2.3",
|
||||
"prisma": "^2.25.0",
|
||||
"qrcode.react": "^1.0.1",
|
||||
"react": "17.0.1",
|
||||
|
@ -31,6 +33,7 @@
|
|||
"react-markdown": "^6.0.2",
|
||||
"react-syntax-highlighter": "^15.4.3",
|
||||
"remark-gfm": "^1.0.0",
|
||||
"remove-markdown": "^0.3.0",
|
||||
"sass": "^1.32.8",
|
||||
"secp256k1": "^4.0.2",
|
||||
"swr": "^0.5.4",
|
||||
|
|
|
@ -3,6 +3,7 @@ import Items from '../components/items'
|
|||
import { gql } from '@apollo/client'
|
||||
import ApolloClient from '../api/client'
|
||||
import UserHeader from '../components/user-header'
|
||||
import Seo from '../components/seo'
|
||||
|
||||
export async function getServerSideProps ({ req, params }) {
|
||||
const { error, data: { user } } = await (await ApolloClient(req)).query({
|
||||
|
@ -36,6 +37,7 @@ export async function getServerSideProps ({ req, params }) {
|
|||
export default function User ({ user }) {
|
||||
return (
|
||||
<Layout>
|
||||
<Seo user={user} />
|
||||
<UserHeader user={user} />
|
||||
<Items variables={{ sort: 'user', userId: user.id }} />
|
||||
</Layout>
|
||||
|
|
|
@ -3,6 +3,7 @@ import { gql } from '@apollo/client'
|
|||
import ApolloClient from '../../api/client'
|
||||
import UserHeader from '../../components/user-header'
|
||||
import CommentsFlat from '../../components/comments-flat'
|
||||
import Seo from '../../components/seo'
|
||||
|
||||
export async function getServerSideProps ({ req, params }) {
|
||||
const { error, data: { user } } = await (await ApolloClient(req)).query({
|
||||
|
@ -36,6 +37,7 @@ export async function getServerSideProps ({ req, params }) {
|
|||
export default function UserComments ({ user }) {
|
||||
return (
|
||||
<Layout>
|
||||
<Seo user={user} />
|
||||
<UserHeader user={user} />
|
||||
<CommentsFlat variables={{ userId: user.id }} includeParent noReply clickToContext />
|
||||
</Layout>
|
||||
|
|
|
@ -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])
|
||||
}
|
|
@ -8,7 +8,7 @@ import { COMMENTS } from '../../fragments/comments'
|
|||
import { ITEM_FIELDS } from '../../fragments/items'
|
||||
import { gql, useQuery } from '@apollo/client'
|
||||
import styles from '../../styles/item.module.css'
|
||||
import Head from 'next/head'
|
||||
import Seo from '../../components/seo'
|
||||
|
||||
export async function getServerSideProps ({ params: { id } }) {
|
||||
return {
|
||||
|
@ -60,13 +60,11 @@ function LoadItem ({ query }) {
|
|||
|
||||
return (
|
||||
<>
|
||||
<Seo item={item} />
|
||||
{item.parentId
|
||||
? <Comment item={item} replyOpen includeParent noComments />
|
||||
: (
|
||||
<>
|
||||
<Head>
|
||||
<title>{item.title} \ stacker news</title>
|
||||
</Head>
|
||||
<Item item={item}>
|
||||
{item.text && <div className='mb-3'><Text>{item.text}</Text></div>}
|
||||
<Reply parentId={item.id} />
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -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
|
||||
// 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
|
||||
const { error, data: { item } } = await (await ApolloClient(req)).query({
|
||||
const { error, data: { item } } = await (await ApolloClient()).query({
|
||||
query:
|
||||
gql`
|
||||
${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 }) {
|
||||
return (
|
||||
<LayoutPreview>
|
||||
<LightningProvider>
|
||||
{item.parentId
|
||||
? <Comment item={item} includeParent noComments />
|
||||
: (
|
||||
<Item item={item}>
|
||||
{item.text && <div className='mb-3'><Text>{item.text}</Text></div>}
|
||||
</Item>
|
||||
)}
|
||||
</LightningProvider>
|
||||
</LayoutPreview>
|
||||
<>
|
||||
<LayoutPreview>
|
||||
<LightningProvider>
|
||||
|
||||
{item.parentId
|
||||
? <Comment item={item} includeParent noReply noComments />
|
||||
: (
|
||||
<Item item={item}>
|
||||
{item.text && <div className='mb-3'><Text>{item.text}</Text></div>}
|
||||
</Item>
|
||||
)}
|
||||
</LightningProvider>
|
||||
</LayoutPreview>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue