working previews
This commit is contained in:
parent
38df1fcdb7
commit
68e80b615c
|
@ -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
|
||||||
|
|
|
@ -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 } })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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!
|
||||||
|
|
|
@ -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>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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' />
|
||||||
|
|
|
@ -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
|
boost
|
||||||
meSats
|
meSats
|
||||||
ncomments
|
ncomments
|
||||||
|
root {
|
||||||
|
id
|
||||||
|
title
|
||||||
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
|
|
@ -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`
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -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",
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 { 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} />
|
||||||
|
|
|
@ -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
|
// 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>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue