a bunch of new stuff

This commit is contained in:
keyan 2021-04-22 17:14:32 -05:00
parent 8ecc81f3f7
commit ec3f6b922d
30 changed files with 530 additions and 303 deletions

View File

@ -54,46 +54,55 @@ function nestComments (flat, parentId) {
return [result, added] return [result, added]
} }
// we have to do our own query because ltree is unsupported
const SELECT =
`SELECT id, created_at as "createdAt", updated_at as "updatedAt", title,
text, url, "userId", "parentId", ltree2text("path") AS "path"`
export default { export default {
Query: { Query: {
items: async (parent, args, { models }) => { items: async (parent, args, { models }) => {
return await models.$queryRaw(` return await models.$queryRaw(`
SELECT id, "created_at" as "createdAt", title, url, text, ${SELECT}
"userId", nlevel(path)-1 AS depth, ltree2text("path") AS "path"
FROM "Item" FROM "Item"
WHERE "parentId" IS NULL`) WHERE "parentId" IS NULL`)
}, },
item: async (parent, { id }, { models }) => { item: async (parent, { id }, { models }) => {
const res = await models.$queryRaw(` return (await models.$queryRaw(`
SELECT id, "created_at" as "createdAt", title, url, text, ${SELECT}
"parentId", "userId", nlevel(path)-1 AS depth, ltree2text("path") AS "path"
FROM "Item" FROM "Item"
WHERE id = ${id}`) WHERE id = ${id}`))[0]
return res.length ? res[0] : null
}, },
flatcomments: async (parent, { parentId }, { models }) => { userItems: async (parent, { userId }, { models }) => {
return await models.$queryRaw(` return await models.$queryRaw(`
SELECT id, "created_at" as "createdAt", text, "parentId", ${SELECT}
"userId", nlevel(path)-1 AS depth, ltree2text("path") AS "path"
FROM "Item" FROM "Item"
WHERE path <@ (SELECT path FROM "Item" where id = ${parentId}) AND id != ${parentId} WHERE "userId" = ${userId} AND "parentId" IS NULL
ORDER BY "path"`) ORDER BY created_at DESC`)
}, },
comments: async (parent, { parentId }, { models }) => { comments: async (parent, { parentId }, { models }) => {
const flat = await models.$queryRaw(` const flat = await models.$queryRaw(`
SELECT id, "created_at" as "createdAt", text, "parentId", ${SELECT}
"userId", nlevel(path)-1 AS depth, ltree2text("path") AS "path"
FROM "Item" FROM "Item"
WHERE path <@ (SELECT path FROM "Item" where id = ${parentId}) AND id != ${parentId} WHERE path <@ (SELECT path FROM "Item" where id = ${parentId}) AND id != ${parentId}
ORDER BY "path"`) ORDER BY "path"`)
return nestComments(flat, parentId)[0] return nestComments(flat, parentId)[0]
}, },
root: async (parent, { id }, { models }) => { userComments: async (parent, { userId }, { models }) => {
const res = await models.$queryRaw(` return await models.$queryRaw(`
SELECT id, title ${SELECT}
FROM "Item" FROM "Item"
WHERE id = (SELECT ltree2text(subltree(path, 0, 1))::integer FROM "Item" WHERE id = ${id})`) WHERE "userId" = ${userId} AND "parentId" IS NOT NULL
return res.length ? res[0] : null ORDER BY created_at DESC`)
},
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 = ${id})`))[0]
} }
}, },

View File

@ -2,18 +2,22 @@ export default {
Query: { Query: {
me: async (parent, args, { models, me }) => me: async (parent, args, { models, me }) =>
me ? await models.user.findUnique({ where: { id: me.id } }) : null, me ? await models.user.findUnique({ where: { id: me.id } }) : null,
user: async (parent, { id }, { models }) => user: async (parent, { name }, { models }) => {
await models.user.findUnique({ where: { id } }), console.log(name)
return await models.user.findUnique({ where: { name } })
},
users: async (parent, args, { models }) => users: async (parent, args, { models }) =>
await models.user.findMany() await models.user.findMany()
}, },
User: { User: {
messages: async (user, args, { models }) => nitems: async (user, args, { models }) => {
await models.message.findMany({ return await models.item.count({ where: { userId: user.id, parentId: null } })
where: { },
userId: user.id ncomments: async (user, args, { models }) => {
} return await models.item.count({ where: { userId: user.id, parentId: { not: null } } })
}) },
stacked: () => 0,
sats: () => 0
} }
} }

View File

@ -4,8 +4,9 @@ export default gql`
extend type Query { extend type Query {
items: [Item!]! items: [Item!]!
item(id: ID!): Item item(id: ID!): Item
userItems(userId: ID!): [Item!]
comments(parentId: ID!): [Item!]! comments(parentId: ID!): [Item!]!
flatcomments(parentId: ID!): [Item!]! userComments(userId: ID!): [Item!]
root(id: ID!): Item root(id: ID!): Item
} }
@ -13,6 +14,7 @@ export default gql`
createLink(title: String!, url: String): Item! createLink(title: String!, url: String): Item!
createDiscussion(title: String!, text: String): Item! createDiscussion(title: String!, text: String): Item!
createComment(text: String!, parentId: ID!): Item! createComment(text: String!, parentId: ID!): Item!
vote(sats: Int): Int!
} }
type Item { type Item {

View File

@ -3,13 +3,16 @@ import { gql } from 'apollo-server-micro'
export default gql` export default gql`
extend type Query { extend type Query {
me: User me: User
user(id: ID!): User user(name: String): User
users: [User!] users: [User!]
} }
type User { type User {
id: ID! id: ID!
name: String name: String
messages: [Message!] nitems: Int!
ncomments: Int!
stacked: Int!
sats: Int!
} }
` `

View File

@ -1,12 +1,12 @@
import itemStyles from './item.module.css' import itemStyles from './item.module.css'
import styles from './comment.module.css' import styles from './comment.module.css'
import UpVote from '../svgs/lightning-arrow.svg'
import Text from './text' import Text from './text'
import Link from 'next/link' import Link from 'next/link'
import Reply from './reply' import Reply from './reply'
import { useState } from 'react' import { useState } from 'react'
import { gql, useQuery } from '@apollo/client' import { gql, useQuery } from '@apollo/client'
import { timeSince } from '../lib/time' import { timeSince } from '../lib/time'
import UpVote from './upvote'
function Parent ({ item }) { function Parent ({ item }) {
const { data } = useQuery( const { data } = useQuery(
@ -42,46 +42,79 @@ function Parent ({ item }) {
) )
} }
export default function Comment ({ item, children, replyOpen, includeParent, cacheId }) { export default function Comment ({ item, children, replyOpen, includeParent, cacheId, noReply }) {
const [reply, setReply] = useState(replyOpen) const [reply, setReply] = useState(replyOpen)
return ( return (
<> <>
<div className={`${itemStyles.item} ${styles.item}`}> <div />
<UpVote width={24} height={24} className={`${itemStyles.upvote} ${styles.upvote}`} /> <div>
<div className={itemStyles.hunk}> <div className={`${itemStyles.item} ${styles.item}`}>
<div className={itemStyles.other}> <UpVote className={styles.upvote} />
<Link href={`/@${item.user.name}`} passHref> <div className={itemStyles.hunk}>
<a>@{item.user.name}</a> <div className={itemStyles.other}>
</Link> <Link href={`/${item.user.name}`} passHref>
<span> </span> <a>@{item.user.name}</a>
<span>{timeSince(new Date(item.createdAt))}</span> </Link>
<span> \ </span> <span> </span>
<span>{item.sats} sats</span> <span>{timeSince(new Date(item.createdAt))}</span>
<span> \ </span> <span> \ </span>
<Link href={`/items/${item.id}`} passHref> <span>{item.sats} sats</span>
<a className='text-reset'>{item.ncomments} replies</a> <span> \ </span>
</Link> <Link href={`/items/${item.id}`} passHref>
{includeParent && <Parent item={item} />} <a className='text-reset'>{item.ncomments} replies</a>
</Link>
{includeParent && <Parent item={item} />}
</div>
<div className={styles.text}>
<Text>{item.text}</Text>
</div>
</div> </div>
<div className={styles.text}> </div>
<Text>{item.text}</Text> <div className={`${itemStyles.children} ${styles.children}`}>
{!noReply &&
<div
className={`${itemStyles.other} ${styles.reply}`}
onClick={() => setReply(!reply)}
>
{reply ? 'cancel' : 'reply'}
</div>}
{reply && <Reply parentId={item.id} onSuccess={() => setReply(replyOpen || false)} cacheId={cacheId} />}
{children}
<div className={styles.comments}>
{item.comments
? item.comments.map((item) => (
<Comment key={item.id} item={item} />
))
: null}
</div> </div>
</div> </div>
</div> </div>
<div className={`${itemStyles.children} ${styles.children}`}> </>
<div )
className={`${itemStyles.other} ${styles.reply}`} }
onClick={() => setReply(!reply)}
> export function CommentSkeleton ({ skeletonChildren }) {
{reply ? 'cancel' : 'reply'} const comments = skeletonChildren ? new Array(2).fill(null) : []
return (
<>
<div className={`${itemStyles.item} ${itemStyles.skeleton} ${styles.item} ${styles.skeleton}`}>
<UpVote className={styles.upvote} />
<div className={`${itemStyles.hunk} ${styles.hunk}`}>
<div className={itemStyles.other}>
<span className={`${itemStyles.otherItem} ${itemStyles.otherItemLonger} clouds`} />
<span className={`${itemStyles.otherItem} clouds`} />
<span className={`${itemStyles.otherItem} ${itemStyles.otherItemLonger} clouds`} />
</div>
<div className={`${styles.text} clouds`} />
</div> </div>
{reply && <Reply parentId={item.id} onSuccess={() => setReply(replyOpen || false)} cacheId={cacheId} />} </div>
{children} <div className={`${itemStyles.children} ${styles.children}`}>
<div className={styles.comments}> <div className={styles.comments}>
{item.comments {comments
? item.comments.map((item) => ( ? comments.map((_, i) => (
<Comment key={item.id} item={item} /> <CommentSkeleton key={i} />
)) ))
: null} : null}
</div> </div>

View File

@ -23,4 +23,14 @@
.comments { .comments {
margin-left: 16px; margin-left: 16px;
margin-top: .5rem; margin-top: .5rem;
}
.skeleton .hunk {
width: 100%;
}
.skeleton .text {
height: 100px;
border-radius: .4rem;
} }

View File

@ -1,28 +1,23 @@
import { useQuery, gql } from '@apollo/client' import { useQuery } from '@apollo/client'
import Comment from './comment' import Comment, { CommentSkeleton } from './comment'
import { COMMENTS } from '../fragments'
export default function Comments ({ parentId }) { export default function Comments ({ query, ...props }) {
const { data } = useQuery( const { loading, error, data } = useQuery(query)
gql`
${COMMENTS}
{ if (error) return <div>Failed to load!</div>
comments(parentId: ${parentId}) { if (loading) {
...CommentsRecursive const comments = new Array(3).fill(null)
}
}`
)
if (!data) return null return comments.map((_, i) => (
<div key={i} className='mt-2'>
<CommentSkeleton skeletonChildren />
</div>
))
}
return ( return data.comments.map(item => (
<div className='mt-5'> <div key={item.id} className='mt-2'>
{data.comments.map(item => ( <Comment item={item} {...props} />
<div key={item.id} className='mt-2'>
<Comment item={item} />
</div>
))}
</div> </div>
) ))
} }

View File

@ -18,7 +18,11 @@ export default function Header () {
if (session) { if (session) {
return ( return (
<> <>
<Nav.Item>{session.user.name}</Nav.Item> <Nav.Item>
<Link href={'/' + session.user.name} passHref>
<Nav.Link className='text-reset'>@{session.user.name}</Nav.Link>
</Link>
</Nav.Item>
<Nav.Item> <Nav.Item>
<Nav.Link onClick={signOut}>logout</Nav.Link> <Nav.Link onClick={signOut}>logout</Nav.Link>
</Nav.Item> </Nav.Item>
@ -36,7 +40,7 @@ export default function Header () {
<Link href='/' passHref> <Link href='/' passHref>
<Navbar.Brand className={styles.brand}>STACKER NEWS</Navbar.Brand> <Navbar.Brand className={styles.brand}>STACKER NEWS</Navbar.Brand>
</Link> </Link>
<Nav className='mr-auto align-items-center' activeKey={router.pathname}> <Nav className='mr-auto align-items-center' activeKey={router.asPath}>
<Nav.Item> <Nav.Item>
<Link href='/recent' passHref> <Link href='/recent' passHref>
<Nav.Link>recent</Nav.Link> <Nav.Link>recent</Nav.Link>
@ -48,7 +52,7 @@ export default function Header () {
</Link> </Link>
</Nav.Item> </Nav.Item>
</Nav> </Nav>
<Nav className='ml-auto align-items-center'> <Nav className='ml-auto align-items-center' activeKey={router.asPath}>
<Corner /> <Corner />
</Nav> </Nav>
</Container> </Container>

View File

@ -8,4 +8,6 @@
.navbar { .navbar {
padding: 0rem 1.75rem; padding: 0rem 1.75rem;
background-color: transparent !important;
background-image: linear-gradient(#FADA5E, #FADA5E, transparent)
} }

View File

@ -1,13 +1,19 @@
import Link from 'next/link' import Link from 'next/link'
import UpVote from '../svgs/lightning-arrow.svg'
import styles from './item.module.css' import styles from './item.module.css'
import { timeSince } from '../lib/time' import { timeSince } from '../lib/time'
import UpVote from './upvote'
export default function Item ({ item, children }) { export default function Item ({ item, rank, children }) {
return ( return (
<> <>
{rank
? (
<div className={styles.rank}>
{rank}
</div>)
: <div />}
<div className={styles.item}> <div className={styles.item}>
<UpVote width={24} height={24} className={styles.upvote} /> <UpVote />
<div className={styles.hunk}> <div className={styles.hunk}>
<div className={`${styles.main} flex-wrap flex-md-nowrap`}> <div className={`${styles.main} flex-wrap flex-md-nowrap`}>
<Link href={`/items/${item.id}`} passHref> <Link href={`/items/${item.id}`} passHref>
@ -22,7 +28,7 @@ export default function Item ({ item, children }) {
<a className='text-reset'>{item.ncomments} comments</a> <a className='text-reset'>{item.ncomments} comments</a>
</Link> </Link>
<span> \ </span> <span> \ </span>
<Link href={`/@${item.user.name}`} passHref> <Link href={`/${item.user.name}`} passHref>
<a>@{item.user.name}</a> <a>@{item.user.name}</a>
</Link> </Link>
<span> </span> <span> </span>
@ -38,3 +44,28 @@ export default function Item ({ item, children }) {
</> </>
) )
} }
export function ItemSkeleton ({ rank }) {
return (
<>
{rank &&
<div className={styles.rank}>
{rank}
</div>}
<div className={`${styles.item} ${styles.skeleton}`}>
<UpVote />
<div className={styles.hunk}>
<div className={`${styles.main} flex-wrap flex-md-nowrap`}>
<span className={`${styles.title} clouds text-reset flex-md-fill flex-md-shrink-0 mr-2`} />
<span className={`${styles.link} clouds`} />
</div>
<div className={styles.other}>
<span className={`${styles.otherItem} clouds`} />
<span className={`${styles.otherItem} ${styles.otherItemLonger} clouds`} />
<span className={`${styles.otherItem} ${styles.otherItemLonger} clouds`} />
</div>
</div>
</div>
</>
)
}

View File

@ -1,13 +1,3 @@
.upvote {
fill: grey;
min-width: fit-content;
}
.upvote:hover {
fill: darkgray;
cursor: pointer;
}
.title { .title {
font-weight: 500; font-weight: 500;
white-space: normal; white-space: normal;
@ -45,4 +35,47 @@
.children { .children {
margin-top: 1rem; margin-top: 1rem;
margin-left: 24px; margin-left: 24px;
}
.rank {
font-weight: 600;
align-items: center;
display: flex;
color: grey;
font-size: 90%;
}
.skeleton .other {
height: 17px;
align-items: center;
display: flex;
}
.skeleton .title {
background-color: grey;
width: 500px;
border-radius: .4rem;
height: 19px;
margin: 3px 0px;
}
.skeleton .link {
height: 14px;
background-color: grey;
width: 800px;
border-radius: .4rem;
margin: 3px 0px;
}
.skeleton .otherItem {
display: inline-flex;
width: 42px;
height: 70%;
border-radius: .4rem;
background-color: grey;
margin-right: .5rem;
}
.skeleton .otherItemLonger {
width: 60px;
} }

View File

@ -1,35 +1,27 @@
import { gql, useQuery } from '@apollo/client' import { useQuery } from '@apollo/client'
import React from 'react' import Item, { ItemSkeleton } from './item'
import Item from './item'
import styles from './items.module.css' import styles from './items.module.css'
export default function Items () { export default function Items ({ query, rank }) {
const { loading, error, data } = useQuery( const { loading, error, data } = useQuery(query)
gql` if (error) return <div>Failed to load!</div>
{ items { if (loading) {
id const items = new Array(30).fill(null)
createdAt
title return (
url <div className={styles.grid}>
user { {items.map((_, i) => (
name <ItemSkeleton rank={i + 1} key={i} />
} ))}
sats </div>
ncomments )
} }` }
)
if (error) return <div>Failed to load</div>
if (loading) return <div>Loading...</div>
const { items } = data const { items } = data
return ( return (
<div className={styles.grid}> <div className={styles.grid}>
{items.map((item, i) => ( {items.map((item, i) => (
<React.Fragment key={item.id}> <Item item={item} rank={rank && i + 1} key={item.id} />
<div className={styles.rank} key={item.id}>
{i + 1}
</div>
<Item item={item} />
</React.Fragment>
))} ))}
</div> </div>
) )

View File

@ -1,12 +1,4 @@
.grid { .grid {
display: grid; display: grid;
grid-template-columns: auto 1fr; grid-template-columns: auto 1fr;
}
.rank {
font-weight: 600;
align-items: center;
display: flex;
color: grey;
font-size: 90%;
} }

View File

@ -1,19 +1,20 @@
import Header from './header' import Header from './header'
import Container from 'react-bootstrap/Container' import Container from 'react-bootstrap/Container'
import { Lightning } from './lightning' import { LightningProvider } from './lightning'
export default function Layout ({ noContain, children }) { export default function Layout ({ noContain, children }) {
return ( return (
<> <>
<Header /> <LightningProvider>
<Lightning /> <Header />
{noContain {noContain
? children ? children
: ( : (
<Container className='my-2 py-2 px-sm-0'> <Container className='my-2 py-2 px-sm-0'>
{children} {children}
</Container> </Container>
)} )}
</LightningProvider>
</> </>
) )
} }

View File

@ -1,5 +1,30 @@
import React, { useRef, useEffect } from 'react'
import { useRef, useEffect } from 'react' export const LightningContext = React.createContext()
export class LightningProvider extends React.Component {
state = {
bolts: 0,
strike: () => this.setState(state => {
return {
...this.state,
bolts: this.state.bolts + 1
}
})
}
render () {
const { state, props: { children } } = this
return (
<LightningContext.Provider value={state}>
{new Array(this.state.bolts).fill(null).map((_, i) => <Lightning key={i} />)}
{children}
</LightningContext.Provider>
)
}
}
export const LightningConsumer = LightningContext.Consumer
export function Lightning () { export function Lightning () {
const canvasRef = useRef(null) const canvasRef = useRef(null)
@ -14,21 +39,14 @@ export function Lightning () {
const bolt = new Bolt(context, { const bolt = new Bolt(context, {
startPoint: [Math.random() * (canvas.width * 0.5) + (canvas.width * 0.25), 0], startPoint: [Math.random() * (canvas.width * 0.5) + (canvas.width * 0.25), 0],
length: canvas.height, length: canvas.height,
speed: options.speed, speed: 100,
spread: options.spread, spread: 30,
branches: options.branching branches: 20
}) })
bolt.draw() bolt.draw()
}, []) }, [])
return <canvas className='position-absolute' ref={canvasRef} style={{ zIndex: -1 }} /> return <canvas className='position-fixed' ref={canvasRef} style={{ zIndex: -1 }} />
}
// Initialize options.
const options = {
speed: 80,
spread: 40,
branching: 5
} }
function Bolt (ctx, options) { function Bolt (ctx, options) {
@ -40,6 +58,7 @@ function Bolt (ctx, options) {
spread: 50, spread: 50,
branches: 10, branches: 10,
maxBranches: 10, maxBranches: 10,
lineWidth: 3,
...options ...options
} }
this.point = [this.options.startPoint[0], this.options.startPoint[1]] this.point = [this.options.startPoint[0], this.options.startPoint[1]]
@ -59,7 +78,7 @@ function Bolt (ctx, options) {
ctx.shadowOffsetY = 0 ctx.shadowOffsetY = 0
ctx.fillStyle = 'rgba(250, 250, 250, 1)' ctx.fillStyle = 'rgba(250, 250, 250, 1)'
ctx.strokeStyle = 'rgba(250, 218, 94, 1)' ctx.strokeStyle = 'rgba(250, 218, 94, 1)'
ctx.lineWidth = 2 ctx.lineWidth = this.options.lineWidth
this.draw = (isChild) => { this.draw = (isChild) => {
ctx.beginPath() ctx.beginPath()
ctx.moveTo(this.point[0], this.point[1]) ctx.moveTo(this.point[0], this.point[1])
@ -78,6 +97,9 @@ function Bolt (ctx, options) {
Math.pow(this.point[1] - this.options.startPoint[1], 2) Math.pow(this.point[1] - this.options.startPoint[1], 2)
) )
// make skinnier?
// ctx.lineWidth = ctx.lineWidth * 0.98
if (rand(0, 99) < this.options.branches && this.children.length < this.options.maxBranches) { if (rand(0, 99) < this.options.branches && this.children.length < this.options.maxBranches) {
this.children.push(new Bolt(ctx, { this.children.push(new Bolt(ctx, {
startPoint: [this.point[0], this.point[1]], startPoint: [this.point[0], this.point[1]],
@ -86,7 +108,8 @@ function Bolt (ctx, options) {
resistance: this.options.resistance, resistance: this.options.resistance,
speed: this.options.speed - 2, speed: this.options.speed - 2,
spread: this.options.spread - 2, spread: this.options.spread - 2,
branches: this.options.branches branches: this.options.branches,
lineWidth: ctx.lineWidth
})) }))
} }
@ -101,19 +124,14 @@ function Bolt (ctx, options) {
if (d < this.options.length) { if (d < this.options.length) {
window.requestAnimationFrame(() => { this.draw() }) window.requestAnimationFrame(() => { this.draw() })
} else { } else {
ctx.canvas.style.opacity = 1
this.fade() this.fade()
} }
} }
this.fade = function () { this.fade = function () {
ctx.shadowColor = 'rgba(250, 250, 250, .5)' ctx.canvas.style.opacity -= 0.04
ctx.fillStyle = 'rgba(250, 250, 250, .05)' if (ctx.canvas.style.opacity <= 0) {
ctx.fillRect(0, 0, window.innerWidth, window.innerHeight)
const color = ctx.getImageData(0, 0, 1, 1)
console.log(color.data)
if (color.data[0] >= 250 && color.data[3] > 240) {
ctx.fillStyle = 'rgba(250, 250, 250, 1)'
ctx.clearRect(0, 0, window.innerWidth, window.innerHeight) ctx.clearRect(0, 0, window.innerWidth, window.innerHeight)
return return
} }

View File

@ -2,7 +2,7 @@ import { Form, Input, SubmitButton } from '../components/form'
import * as Yup from 'yup' import * as Yup from 'yup'
import { gql, useMutation } from '@apollo/client' import { gql, useMutation } from '@apollo/client'
import styles from './reply.module.css' import styles from './reply.module.css'
import { COMMENTS } from '../fragments' import { COMMENTS } from '../fragments/comments'
export const CommentSchema = Yup.object({ export const CommentSchema = Yup.object({
text: Yup.string().required('required').trim() text: Yup.string().required('required').trim()

17
components/upvote.js Normal file
View File

@ -0,0 +1,17 @@
import { LightningConsumer } from './lightning'
import UpArrow from '../svgs/lightning-arrow.svg'
import styles from './upvote.module.css'
export default function UpVote ({ className }) {
return (
<LightningConsumer>
{({ strike }) =>
<UpArrow
width={24}
height={24}
className={`${styles.upvote} ${className || ''}`}
onClick={strike}
/>}
</LightningConsumer>
)
}

View File

@ -0,0 +1,9 @@
.upvote {
fill: grey;
min-width: fit-content;
}
.upvote:hover {
fill: darkgray;
cursor: pointer;
}

31
components/user-header.js Normal file
View File

@ -0,0 +1,31 @@
import Link from 'next/link'
import { useRouter } from 'next/router'
import Nav from 'react-bootstrap/Nav'
export default function UserHeader ({ user }) {
const router = useRouter()
return (
<>
<h1>@{user.name} <small className='text-success'>[{user.stacked} stacked, {user.sats} sats]</small></h1>
<Nav
activeKey={router.asPath}
>
<Nav.Item>
<Link href={'/' + user.name} passHref>
<Nav.Link>{user.nitems} posts</Nav.Link>
</Link>
</Nav.Item>
<Nav.Item>
<Link href={'/' + user.name + '/comments'} passHref>
<Nav.Link>{user.ncomments} comments</Nav.Link>
</Link>
</Nav.Item>
<Nav.Item>
<Link href={'/' + user.name + '/sativity'} passHref>
<Nav.Link>sativity</Nav.Link>
</Link>
</Nav.Item>
</Nav>
</>
)
}

24
fragments/items.js Normal file
View File

@ -0,0 +1,24 @@
import { gql } from '@apollo/client'
export const ITEM_FIELDS = gql`
fragment ItemFields on Item {
id
parentId
createdAt
title
url
user {
name
}
sats
ncomments
}`
export const ITEMS_FEED = gql`
${ITEM_FIELDS}
{
items {
...ItemFields
}
}`

View File

@ -43,4 +43,4 @@
"prisma": "2.19.0", "prisma": "2.19.0",
"standard": "^16.0.3" "standard": "^16.0.3"
} }
} }

View File

@ -1,3 +1,52 @@
export default function User () { import Layout from '../components/layout'
return <div>hi</div> import Items from '../components/items'
import { ITEM_FIELDS } from '../fragments/items'
import { gql } from '@apollo/client'
import ApolloClient from '../api/client'
import UserHeader from '../components/user-header'
export async function getServerSideProps ({ params }) {
const { error, data: { user } } = await ApolloClient.query({
query:
gql`{
user(name: "${params.username}") {
id
createdAt
name
nitems
ncomments
stacked
sats
}
}`
})
if (!user || error) {
return {
notFound: true
}
}
return {
props: {
user
}
}
}
export default function User ({ user }) {
const query = gql`
${ITEM_FIELDS}
{
items: userItems(userId: ${user.id}) {
...ItemFields
}
}
`
return (
<Layout>
<UserHeader user={user} />
<Items query={query} />
</Layout>
)
} }

View File

@ -0,0 +1,52 @@
import Layout from '../../components/layout'
import Comments from '../../components/comments'
import { COMMENT_FIELDS } from '../../fragments/comments'
import { gql } from '@apollo/client'
import ApolloClient from '../../api/client'
import UserHeader from '../../components/user-header'
export async function getServerSideProps ({ params }) {
const { error, data: { user } } = await ApolloClient.query({
query:
gql`{
user(name: "${params.username}") {
id
createdAt
name
nitems
ncomments
stacked
sats
}
}`
})
if (!user || error) {
return {
notFound: true
}
}
return {
props: {
user
}
}
}
export default function User ({ user }) {
const query = gql`
${COMMENT_FIELDS}
{
comments: userComments(userId: ${user.id}) {
...CommentFields
}
}
`
return (
<Layout>
<UserHeader user={user} />
<Comments query={query} includeParent noReply />
</Layout>
)
}

View File

@ -1,95 +1,11 @@
import Layout from '../components/layout' import Layout from '../components/layout'
import React from 'react'
import Items from '../components/items' import Items from '../components/items'
import { ITEMS_FEED } from '../fragments/items'
// function Users () {
// const { loading, error, data } = useQuery(gql`{ users { id, name } }`)
// if (error) return <div>Failed to load</div>
// if (loading) return <div>Loading...</div>
// const { users } = data
// return (
// <div>
// {users.map(user => (
// <div key={user.id}>{user.name}</div>
// ))}
// </div>
// )
// }
// 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 (
// <div onClick={() => setOpen(true)}>
// {parentId ? 'reply' : 'submit'}
// </div>
// )
// }
// let text
// return (
// <form
// style={{ marginLeft: '5px' }}
// onSubmit={e => {
// e.preventDefault()
// createItem({ variables: { text: text.value, parentId } })
// setOpen(false)
// text.value = ''
// }}
// >
// <textarea
// ref={node => {
// text = node
// }}
// />
// <button type='submit'>Submit</button>
// </form>
// )
// }
export default function Index () { export default function Index () {
return ( return (
<Layout> <Layout>
<Items /> <Items query={ITEMS_FEED} rank />
</Layout> </Layout>
) )
} }

View File

@ -6,6 +6,7 @@ import Reply from '../../components/reply'
import Comment from '../../components/comment' import Comment from '../../components/comment'
import Text from '../../components/text' import Text from '../../components/text'
import Comments from '../../components/comments' import Comments from '../../components/comments'
import { COMMENTS } from '../../fragments/comments'
export async function getServerSideProps ({ params }) { export async function getServerSideProps ({ params }) {
const { error, data: { item } } = await ApolloClient.query({ const { error, data: { item } } = await ApolloClient.query({
@ -41,6 +42,14 @@ export async function getServerSideProps ({ params }) {
} }
export default function FullItem ({ item }) { export default function FullItem ({ item }) {
const commentsQuery = gql`
${COMMENTS}
{
comments(parentId: ${item.id}) {
...CommentsRecursive
}
}`
return ( return (
<Layout> <Layout>
{item.parentId {item.parentId
@ -53,7 +62,9 @@ export default function FullItem ({ item }) {
</Item> </Item>
</> </>
)} )}
<Comments parentId={item.id} /> <div className='mt-5'>
<Comments query={commentsQuery} />
</div>
</Layout> </Layout>
) )
} }

View File

@ -20,6 +20,7 @@ model User {
image String? image String?
items Item[] items Item[]
messages Message[] messages Message[]
votes Vote[]
@@map(name: "users") @@map(name: "users")
} }
@ -43,12 +44,27 @@ model Item {
parent Item? @relation("ParentChildren", fields: [parentId], references: [id]) parent Item? @relation("ParentChildren", fields: [parentId], references: [id])
parentId Int? parentId Int?
children Item[] @relation("ParentChildren") children Item[] @relation("ParentChildren")
votes Vote[]
path Unsupported("LTREE")? path Unsupported("LTREE")?
@@index([userId]) @@index([userId])
@@index([parentId]) @@index([parentId])
} }
model Vote {
id Int @id @default(autoincrement())
createdAt DateTime @default(now()) @map(name: "created_at")
updatedAt DateTime @updatedAt @map(name: "updated_at")
sats Int
item Item @relation(fields: [itemId], references: [id])
itemId Int
user User @relation(fields: [userId], references: [id])
userId Int
@@index([itemId])
@@index([userId])
}
model Account { model Account {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
createdAt DateTime @default(now()) @map(name: "created_at") createdAt DateTime @default(now()) @map(name: "created_at")

BIN
public/clouds.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -16,7 +16,7 @@ $form-feedback-icon-invalid: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3
$line-height-base: 1.75; $line-height-base: 1.75;
$input-btn-padding-y: .42rem; $input-btn-padding-y: .42rem;
$input-btn-padding-x: .84rem; $input-btn-padding-x: .84rem;
$btn-padding-y: .5rem; $btn-padding-y: .42rem;
$btn-padding-x: 1.1rem; $btn-padding-x: 1.1rem;
$btn-font-weight: bold; $btn-font-weight: bold;
$btn-focus-width: 0; $btn-focus-width: 0;
@ -59,53 +59,26 @@ $container-max-widths: (
color: #ffffff; color: #ffffff;
} }
.flashit{ .nav-link.active {
animation: flash ease-out 7s infinite; font-weight: bold;
animation-delay: -3s;
} }
@keyframes flash { @keyframes flash {
from { opacity: 0; } from { filter: brightness(1); background-position: 0 0;}
42% { opacity: 0; } 2% { filter: brightness(2.3); }
43% { opacity: 0.6; } 4% { filter: brightness(1.4); }
44% { opacity: 0.2; } 8% { filter: brightness(3); }
46% { opacity: 1; } 16% { filter: brightness(1); }
50% { opacity: 0; } to { filter: brightness(1); background-position: 250px 0;}
92% { opacity: 0; }
93% { opacity: 0.6; }
94% { opacity: 0.2; }
96% { opacity: 1; }
to { opacity: 0; }
} }
.morphit{ .clouds {
animation: flash ease-out 7s infinite; animation: flash ease-out 3.5s infinite;
animation-delay: -3s; background: url('/clouds.jpeg');
} background-color: grey;
background-repeat: repeat;
@keyframes flash { background-origin: content-box;
from { opacity: 0; } background-size: cover;
42% { opacity: 0; } background-attachment: fixed;
43% { opacity: 0.6; } opacity: .2;
44% { opacity: 0.2; } }
46% { opacity: 1; }
50% { opacity: 0; }
92% { opacity: 0; }
93% { opacity: 0.6; }
94% { opacity: 0.2; }
96% { opacity: 1; }
to { opacity: 0; }
}
.fadeOut {
animation: fadeOut ease 1s;
}
@keyframes fadeOut {
0% {
opacity:1;
}
100% {
opacity:0;
}
}

View File