a bunch of new stuff
This commit is contained in:
parent
8ecc81f3f7
commit
ec3f6b922d
|
@ -54,46 +54,55 @@ function nestComments (flat, parentId) {
|
|||
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 {
|
||||
Query: {
|
||||
items: async (parent, args, { models }) => {
|
||||
return await models.$queryRaw(`
|
||||
SELECT id, "created_at" as "createdAt", title, url, text,
|
||||
"userId", nlevel(path)-1 AS depth, ltree2text("path") AS "path"
|
||||
${SELECT}
|
||||
FROM "Item"
|
||||
WHERE "parentId" IS NULL`)
|
||||
},
|
||||
item: async (parent, { id }, { models }) => {
|
||||
const res = await models.$queryRaw(`
|
||||
SELECT id, "created_at" as "createdAt", title, url, text,
|
||||
"parentId", "userId", nlevel(path)-1 AS depth, ltree2text("path") AS "path"
|
||||
return (await models.$queryRaw(`
|
||||
${SELECT}
|
||||
FROM "Item"
|
||||
WHERE id = ${id}`)
|
||||
return res.length ? res[0] : null
|
||||
WHERE id = ${id}`))[0]
|
||||
},
|
||||
flatcomments: async (parent, { parentId }, { models }) => {
|
||||
userItems: async (parent, { userId }, { models }) => {
|
||||
return await models.$queryRaw(`
|
||||
SELECT id, "created_at" as "createdAt", text, "parentId",
|
||||
"userId", nlevel(path)-1 AS depth, ltree2text("path") AS "path"
|
||||
${SELECT}
|
||||
FROM "Item"
|
||||
WHERE path <@ (SELECT path FROM "Item" where id = ${parentId}) AND id != ${parentId}
|
||||
ORDER BY "path"`)
|
||||
WHERE "userId" = ${userId} AND "parentId" IS NULL
|
||||
ORDER BY created_at DESC`)
|
||||
},
|
||||
comments: async (parent, { parentId }, { models }) => {
|
||||
const flat = await models.$queryRaw(`
|
||||
SELECT id, "created_at" as "createdAt", text, "parentId",
|
||||
"userId", nlevel(path)-1 AS depth, ltree2text("path") AS "path"
|
||||
${SELECT}
|
||||
FROM "Item"
|
||||
WHERE path <@ (SELECT path FROM "Item" where id = ${parentId}) AND id != ${parentId}
|
||||
ORDER BY "path"`)
|
||||
return nestComments(flat, parentId)[0]
|
||||
},
|
||||
root: async (parent, { id }, { models }) => {
|
||||
const res = await models.$queryRaw(`
|
||||
SELECT id, title
|
||||
userComments: async (parent, { userId }, { models }) => {
|
||||
return await models.$queryRaw(`
|
||||
${SELECT}
|
||||
FROM "Item"
|
||||
WHERE id = (SELECT ltree2text(subltree(path, 0, 1))::integer FROM "Item" WHERE id = ${id})`)
|
||||
return res.length ? res[0] : null
|
||||
WHERE "userId" = ${userId} AND "parentId" IS NOT 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]
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -2,18 +2,22 @@ export default {
|
|||
Query: {
|
||||
me: async (parent, args, { models, me }) =>
|
||||
me ? await models.user.findUnique({ where: { id: me.id } }) : null,
|
||||
user: async (parent, { id }, { models }) =>
|
||||
await models.user.findUnique({ where: { id } }),
|
||||
user: async (parent, { name }, { models }) => {
|
||||
console.log(name)
|
||||
return await models.user.findUnique({ where: { name } })
|
||||
},
|
||||
users: async (parent, args, { models }) =>
|
||||
await models.user.findMany()
|
||||
},
|
||||
|
||||
User: {
|
||||
messages: async (user, args, { models }) =>
|
||||
await models.message.findMany({
|
||||
where: {
|
||||
userId: user.id
|
||||
}
|
||||
})
|
||||
nitems: async (user, args, { models }) => {
|
||||
return await models.item.count({ where: { userId: user.id, parentId: null } })
|
||||
},
|
||||
ncomments: async (user, args, { models }) => {
|
||||
return await models.item.count({ where: { userId: user.id, parentId: { not: null } } })
|
||||
},
|
||||
stacked: () => 0,
|
||||
sats: () => 0
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,8 +4,9 @@ export default gql`
|
|||
extend type Query {
|
||||
items: [Item!]!
|
||||
item(id: ID!): Item
|
||||
userItems(userId: ID!): [Item!]
|
||||
comments(parentId: ID!): [Item!]!
|
||||
flatcomments(parentId: ID!): [Item!]!
|
||||
userComments(userId: ID!): [Item!]
|
||||
root(id: ID!): Item
|
||||
}
|
||||
|
||||
|
@ -13,6 +14,7 @@ export default gql`
|
|||
createLink(title: String!, url: String): Item!
|
||||
createDiscussion(title: String!, text: String): Item!
|
||||
createComment(text: String!, parentId: ID!): Item!
|
||||
vote(sats: Int): Int!
|
||||
}
|
||||
|
||||
type Item {
|
||||
|
|
|
@ -3,13 +3,16 @@ import { gql } from 'apollo-server-micro'
|
|||
export default gql`
|
||||
extend type Query {
|
||||
me: User
|
||||
user(id: ID!): User
|
||||
user(name: String): User
|
||||
users: [User!]
|
||||
}
|
||||
|
||||
type User {
|
||||
id: ID!
|
||||
name: String
|
||||
messages: [Message!]
|
||||
nitems: Int!
|
||||
ncomments: Int!
|
||||
stacked: Int!
|
||||
sats: Int!
|
||||
}
|
||||
`
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import itemStyles from './item.module.css'
|
||||
import styles from './comment.module.css'
|
||||
import UpVote from '../svgs/lightning-arrow.svg'
|
||||
import Text from './text'
|
||||
import Link from 'next/link'
|
||||
import Reply from './reply'
|
||||
import { useState } from 'react'
|
||||
import { gql, useQuery } from '@apollo/client'
|
||||
import { timeSince } from '../lib/time'
|
||||
import UpVote from './upvote'
|
||||
|
||||
function Parent ({ item }) {
|
||||
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)
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={`${itemStyles.item} ${styles.item}`}>
|
||||
<UpVote width={24} height={24} className={`${itemStyles.upvote} ${styles.upvote}`} />
|
||||
<div className={itemStyles.hunk}>
|
||||
<div className={itemStyles.other}>
|
||||
<Link href={`/@${item.user.name}`} passHref>
|
||||
<a>@{item.user.name}</a>
|
||||
</Link>
|
||||
<span> </span>
|
||||
<span>{timeSince(new Date(item.createdAt))}</span>
|
||||
<span> \ </span>
|
||||
<span>{item.sats} sats</span>
|
||||
<span> \ </span>
|
||||
<Link href={`/items/${item.id}`} passHref>
|
||||
<a className='text-reset'>{item.ncomments} replies</a>
|
||||
</Link>
|
||||
{includeParent && <Parent item={item} />}
|
||||
<div />
|
||||
<div>
|
||||
<div className={`${itemStyles.item} ${styles.item}`}>
|
||||
<UpVote className={styles.upvote} />
|
||||
<div className={itemStyles.hunk}>
|
||||
<div className={itemStyles.other}>
|
||||
<Link href={`/${item.user.name}`} passHref>
|
||||
<a>@{item.user.name}</a>
|
||||
</Link>
|
||||
<span> </span>
|
||||
<span>{timeSince(new Date(item.createdAt))}</span>
|
||||
<span> \ </span>
|
||||
<span>{item.sats} sats</span>
|
||||
<span> \ </span>
|
||||
<Link href={`/items/${item.id}`} passHref>
|
||||
<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 className={styles.text}>
|
||||
<Text>{item.text}</Text>
|
||||
</div>
|
||||
<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 className={`${itemStyles.children} ${styles.children}`}>
|
||||
<div
|
||||
className={`${itemStyles.other} ${styles.reply}`}
|
||||
onClick={() => setReply(!reply)}
|
||||
>
|
||||
{reply ? 'cancel' : 'reply'}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export function CommentSkeleton ({ skeletonChildren }) {
|
||||
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>
|
||||
{reply && <Reply parentId={item.id} onSuccess={() => setReply(replyOpen || false)} cacheId={cacheId} />}
|
||||
{children}
|
||||
</div>
|
||||
<div className={`${itemStyles.children} ${styles.children}`}>
|
||||
<div className={styles.comments}>
|
||||
{item.comments
|
||||
? item.comments.map((item) => (
|
||||
<Comment key={item.id} item={item} />
|
||||
{comments
|
||||
? comments.map((_, i) => (
|
||||
<CommentSkeleton key={i} />
|
||||
))
|
||||
: null}
|
||||
</div>
|
||||
|
|
|
@ -23,4 +23,14 @@
|
|||
.comments {
|
||||
margin-left: 16px;
|
||||
margin-top: .5rem;
|
||||
}
|
||||
|
||||
|
||||
.skeleton .hunk {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.skeleton .text {
|
||||
height: 100px;
|
||||
border-radius: .4rem;
|
||||
}
|
|
@ -1,28 +1,23 @@
|
|||
import { useQuery, gql } from '@apollo/client'
|
||||
import Comment from './comment'
|
||||
import { COMMENTS } from '../fragments'
|
||||
import { useQuery } from '@apollo/client'
|
||||
import Comment, { CommentSkeleton } from './comment'
|
||||
|
||||
export default function Comments ({ parentId }) {
|
||||
const { data } = useQuery(
|
||||
gql`
|
||||
${COMMENTS}
|
||||
export default function Comments ({ query, ...props }) {
|
||||
const { loading, error, data } = useQuery(query)
|
||||
|
||||
{
|
||||
comments(parentId: ${parentId}) {
|
||||
...CommentsRecursive
|
||||
}
|
||||
}`
|
||||
)
|
||||
if (error) return <div>Failed to load!</div>
|
||||
if (loading) {
|
||||
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 (
|
||||
<div className='mt-5'>
|
||||
{data.comments.map(item => (
|
||||
<div key={item.id} className='mt-2'>
|
||||
<Comment item={item} />
|
||||
</div>
|
||||
))}
|
||||
return data.comments.map(item => (
|
||||
<div key={item.id} className='mt-2'>
|
||||
<Comment item={item} {...props} />
|
||||
</div>
|
||||
)
|
||||
))
|
||||
}
|
||||
|
|
|
@ -18,7 +18,11 @@ export default function Header () {
|
|||
if (session) {
|
||||
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.Link onClick={signOut}>logout</Nav.Link>
|
||||
</Nav.Item>
|
||||
|
@ -36,7 +40,7 @@ export default function Header () {
|
|||
<Link href='/' passHref>
|
||||
<Navbar.Brand className={styles.brand}>STACKER NEWS</Navbar.Brand>
|
||||
</Link>
|
||||
<Nav className='mr-auto align-items-center' activeKey={router.pathname}>
|
||||
<Nav className='mr-auto align-items-center' activeKey={router.asPath}>
|
||||
<Nav.Item>
|
||||
<Link href='/recent' passHref>
|
||||
<Nav.Link>recent</Nav.Link>
|
||||
|
@ -48,7 +52,7 @@ export default function Header () {
|
|||
</Link>
|
||||
</Nav.Item>
|
||||
</Nav>
|
||||
<Nav className='ml-auto align-items-center'>
|
||||
<Nav className='ml-auto align-items-center' activeKey={router.asPath}>
|
||||
<Corner />
|
||||
</Nav>
|
||||
</Container>
|
||||
|
|
|
@ -8,4 +8,6 @@
|
|||
|
||||
.navbar {
|
||||
padding: 0rem 1.75rem;
|
||||
background-color: transparent !important;
|
||||
background-image: linear-gradient(#FADA5E, #FADA5E, transparent)
|
||||
}
|
|
@ -1,13 +1,19 @@
|
|||
import Link from 'next/link'
|
||||
import UpVote from '../svgs/lightning-arrow.svg'
|
||||
import styles from './item.module.css'
|
||||
import { timeSince } from '../lib/time'
|
||||
import UpVote from './upvote'
|
||||
|
||||
export default function Item ({ item, children }) {
|
||||
export default function Item ({ item, rank, children }) {
|
||||
return (
|
||||
<>
|
||||
{rank
|
||||
? (
|
||||
<div className={styles.rank}>
|
||||
{rank}
|
||||
</div>)
|
||||
: <div />}
|
||||
<div className={styles.item}>
|
||||
<UpVote width={24} height={24} className={styles.upvote} />
|
||||
<UpVote />
|
||||
<div className={styles.hunk}>
|
||||
<div className={`${styles.main} flex-wrap flex-md-nowrap`}>
|
||||
<Link href={`/items/${item.id}`} passHref>
|
||||
|
@ -22,7 +28,7 @@ export default function Item ({ item, children }) {
|
|||
<a className='text-reset'>{item.ncomments} comments</a>
|
||||
</Link>
|
||||
<span> \ </span>
|
||||
<Link href={`/@${item.user.name}`} passHref>
|
||||
<Link href={`/${item.user.name}`} passHref>
|
||||
<a>@{item.user.name}</a>
|
||||
</Link>
|
||||
<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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,13 +1,3 @@
|
|||
.upvote {
|
||||
fill: grey;
|
||||
min-width: fit-content;
|
||||
}
|
||||
|
||||
.upvote:hover {
|
||||
fill: darkgray;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: 500;
|
||||
white-space: normal;
|
||||
|
@ -45,4 +35,47 @@
|
|||
.children {
|
||||
margin-top: 1rem;
|
||||
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;
|
||||
}
|
|
@ -1,35 +1,27 @@
|
|||
import { gql, useQuery } from '@apollo/client'
|
||||
import React from 'react'
|
||||
import Item from './item'
|
||||
import { useQuery } from '@apollo/client'
|
||||
import Item, { ItemSkeleton } from './item'
|
||||
import styles from './items.module.css'
|
||||
|
||||
export default function Items () {
|
||||
const { loading, error, data } = useQuery(
|
||||
gql`
|
||||
{ items {
|
||||
id
|
||||
createdAt
|
||||
title
|
||||
url
|
||||
user {
|
||||
name
|
||||
}
|
||||
sats
|
||||
ncomments
|
||||
} }`
|
||||
)
|
||||
if (error) return <div>Failed to load</div>
|
||||
if (loading) return <div>Loading...</div>
|
||||
export default function Items ({ query, rank }) {
|
||||
const { loading, error, data } = useQuery(query)
|
||||
if (error) return <div>Failed to load!</div>
|
||||
if (loading) {
|
||||
const items = new Array(30).fill(null)
|
||||
|
||||
return (
|
||||
<div className={styles.grid}>
|
||||
{items.map((_, i) => (
|
||||
<ItemSkeleton rank={i + 1} key={i} />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const { items } = data
|
||||
return (
|
||||
<div className={styles.grid}>
|
||||
{items.map((item, i) => (
|
||||
<React.Fragment key={item.id}>
|
||||
<div className={styles.rank} key={item.id}>
|
||||
{i + 1}
|
||||
</div>
|
||||
<Item item={item} />
|
||||
</React.Fragment>
|
||||
<Item item={item} rank={rank && i + 1} key={item.id} />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -1,12 +1,4 @@
|
|||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
}
|
||||
|
||||
.rank {
|
||||
font-weight: 600;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
color: grey;
|
||||
font-size: 90%;
|
||||
}
|
|
@ -1,19 +1,20 @@
|
|||
import Header from './header'
|
||||
import Container from 'react-bootstrap/Container'
|
||||
import { Lightning } from './lightning'
|
||||
import { LightningProvider } from './lightning'
|
||||
|
||||
export default function Layout ({ noContain, children }) {
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<Lightning />
|
||||
{noContain
|
||||
? children
|
||||
: (
|
||||
<Container className='my-2 py-2 px-sm-0'>
|
||||
{children}
|
||||
</Container>
|
||||
)}
|
||||
<LightningProvider>
|
||||
<Header />
|
||||
{noContain
|
||||
? children
|
||||
: (
|
||||
<Container className='my-2 py-2 px-sm-0'>
|
||||
{children}
|
||||
</Container>
|
||||
)}
|
||||
</LightningProvider>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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 () {
|
||||
const canvasRef = useRef(null)
|
||||
|
@ -14,21 +39,14 @@ export function Lightning () {
|
|||
const bolt = new Bolt(context, {
|
||||
startPoint: [Math.random() * (canvas.width * 0.5) + (canvas.width * 0.25), 0],
|
||||
length: canvas.height,
|
||||
speed: options.speed,
|
||||
spread: options.spread,
|
||||
branches: options.branching
|
||||
speed: 100,
|
||||
spread: 30,
|
||||
branches: 20
|
||||
})
|
||||
bolt.draw()
|
||||
}, [])
|
||||
|
||||
return <canvas className='position-absolute' ref={canvasRef} style={{ zIndex: -1 }} />
|
||||
}
|
||||
|
||||
// Initialize options.
|
||||
const options = {
|
||||
speed: 80,
|
||||
spread: 40,
|
||||
branching: 5
|
||||
return <canvas className='position-fixed' ref={canvasRef} style={{ zIndex: -1 }} />
|
||||
}
|
||||
|
||||
function Bolt (ctx, options) {
|
||||
|
@ -40,6 +58,7 @@ function Bolt (ctx, options) {
|
|||
spread: 50,
|
||||
branches: 10,
|
||||
maxBranches: 10,
|
||||
lineWidth: 3,
|
||||
...options
|
||||
}
|
||||
this.point = [this.options.startPoint[0], this.options.startPoint[1]]
|
||||
|
@ -59,7 +78,7 @@ function Bolt (ctx, options) {
|
|||
ctx.shadowOffsetY = 0
|
||||
ctx.fillStyle = 'rgba(250, 250, 250, 1)'
|
||||
ctx.strokeStyle = 'rgba(250, 218, 94, 1)'
|
||||
ctx.lineWidth = 2
|
||||
ctx.lineWidth = this.options.lineWidth
|
||||
this.draw = (isChild) => {
|
||||
ctx.beginPath()
|
||||
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)
|
||||
)
|
||||
|
||||
// make skinnier?
|
||||
// ctx.lineWidth = ctx.lineWidth * 0.98
|
||||
|
||||
if (rand(0, 99) < this.options.branches && this.children.length < this.options.maxBranches) {
|
||||
this.children.push(new Bolt(ctx, {
|
||||
startPoint: [this.point[0], this.point[1]],
|
||||
|
@ -86,7 +108,8 @@ function Bolt (ctx, options) {
|
|||
resistance: this.options.resistance,
|
||||
speed: this.options.speed - 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) {
|
||||
window.requestAnimationFrame(() => { this.draw() })
|
||||
} else {
|
||||
ctx.canvas.style.opacity = 1
|
||||
this.fade()
|
||||
}
|
||||
}
|
||||
|
||||
this.fade = function () {
|
||||
ctx.shadowColor = 'rgba(250, 250, 250, .5)'
|
||||
ctx.fillStyle = 'rgba(250, 250, 250, .05)'
|
||||
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.canvas.style.opacity -= 0.04
|
||||
if (ctx.canvas.style.opacity <= 0) {
|
||||
ctx.clearRect(0, 0, window.innerWidth, window.innerHeight)
|
||||
return
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ import { Form, Input, SubmitButton } from '../components/form'
|
|||
import * as Yup from 'yup'
|
||||
import { gql, useMutation } from '@apollo/client'
|
||||
import styles from './reply.module.css'
|
||||
import { COMMENTS } from '../fragments'
|
||||
import { COMMENTS } from '../fragments/comments'
|
||||
|
||||
export const CommentSchema = Yup.object({
|
||||
text: Yup.string().required('required').trim()
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
.upvote {
|
||||
fill: grey;
|
||||
min-width: fit-content;
|
||||
}
|
||||
|
||||
.upvote:hover {
|
||||
fill: darkgray;
|
||||
cursor: pointer;
|
||||
}
|
|
@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}`
|
|
@ -43,4 +43,4 @@
|
|||
"prisma": "2.19.0",
|
||||
"standard": "^16.0.3"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,52 @@
|
|||
export default function User () {
|
||||
return <div>hi</div>
|
||||
import Layout from '../components/layout'
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -1,95 +1,11 @@
|
|||
import Layout from '../components/layout'
|
||||
import React from 'react'
|
||||
import Items from '../components/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>
|
||||
// )
|
||||
// }
|
||||
import { ITEMS_FEED } from '../fragments/items'
|
||||
|
||||
export default function Index () {
|
||||
return (
|
||||
<Layout>
|
||||
<Items />
|
||||
<Items query={ITEMS_FEED} rank />
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import Reply from '../../components/reply'
|
|||
import Comment from '../../components/comment'
|
||||
import Text from '../../components/text'
|
||||
import Comments from '../../components/comments'
|
||||
import { COMMENTS } from '../../fragments/comments'
|
||||
|
||||
export async function getServerSideProps ({ params }) {
|
||||
const { error, data: { item } } = await ApolloClient.query({
|
||||
|
@ -41,6 +42,14 @@ export async function getServerSideProps ({ params }) {
|
|||
}
|
||||
|
||||
export default function FullItem ({ item }) {
|
||||
const commentsQuery = gql`
|
||||
${COMMENTS}
|
||||
{
|
||||
comments(parentId: ${item.id}) {
|
||||
...CommentsRecursive
|
||||
}
|
||||
}`
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
{item.parentId
|
||||
|
@ -53,7 +62,9 @@ export default function FullItem ({ item }) {
|
|||
</Item>
|
||||
</>
|
||||
)}
|
||||
<Comments parentId={item.id} />
|
||||
<div className='mt-5'>
|
||||
<Comments query={commentsQuery} />
|
||||
</div>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ model User {
|
|||
image String?
|
||||
items Item[]
|
||||
messages Message[]
|
||||
votes Vote[]
|
||||
|
||||
@@map(name: "users")
|
||||
}
|
||||
|
@ -43,12 +44,27 @@ model Item {
|
|||
parent Item? @relation("ParentChildren", fields: [parentId], references: [id])
|
||||
parentId Int?
|
||||
children Item[] @relation("ParentChildren")
|
||||
votes Vote[]
|
||||
path Unsupported("LTREE")?
|
||||
|
||||
@@index([userId])
|
||||
@@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 {
|
||||
id Int @id @default(autoincrement())
|
||||
createdAt DateTime @default(now()) @map(name: "created_at")
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 2.1 KiB |
|
@ -16,7 +16,7 @@ $form-feedback-icon-invalid: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3
|
|||
$line-height-base: 1.75;
|
||||
$input-btn-padding-y: .42rem;
|
||||
$input-btn-padding-x: .84rem;
|
||||
$btn-padding-y: .5rem;
|
||||
$btn-padding-y: .42rem;
|
||||
$btn-padding-x: 1.1rem;
|
||||
$btn-font-weight: bold;
|
||||
$btn-focus-width: 0;
|
||||
|
@ -59,53 +59,26 @@ $container-max-widths: (
|
|||
color: #ffffff;
|
||||
}
|
||||
|
||||
.flashit{
|
||||
animation: flash ease-out 7s infinite;
|
||||
animation-delay: -3s;
|
||||
.nav-link.active {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
@keyframes flash {
|
||||
from { opacity: 0; }
|
||||
42% { opacity: 0; }
|
||||
43% { opacity: 0.6; }
|
||||
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; }
|
||||
from { filter: brightness(1); background-position: 0 0;}
|
||||
2% { filter: brightness(2.3); }
|
||||
4% { filter: brightness(1.4); }
|
||||
8% { filter: brightness(3); }
|
||||
16% { filter: brightness(1); }
|
||||
to { filter: brightness(1); background-position: 250px 0;}
|
||||
}
|
||||
|
||||
.morphit{
|
||||
animation: flash ease-out 7s infinite;
|
||||
animation-delay: -3s;
|
||||
}
|
||||
|
||||
@keyframes flash {
|
||||
from { opacity: 0; }
|
||||
42% { opacity: 0; }
|
||||
43% { opacity: 0.6; }
|
||||
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;
|
||||
}
|
||||
}
|
||||
.clouds {
|
||||
animation: flash ease-out 3.5s infinite;
|
||||
background: url('/clouds.jpeg');
|
||||
background-color: grey;
|
||||
background-repeat: repeat;
|
||||
background-origin: content-box;
|
||||
background-size: cover;
|
||||
background-attachment: fixed;
|
||||
opacity: .2;
|
||||
}
|
Loading…
Reference in New Issue