attempts at voting before running into cache issues

This commit is contained in:
keyan 2021-04-26 16:55:15 -05:00
parent 900b70da77
commit c626998952
19 changed files with 247 additions and 103 deletions

View File

@ -6,23 +6,22 @@ import resolvers from './resolvers'
import typeDefs from './typeDefs'
import models from './models'
const client = new ApolloClient({
ssrMode: true,
// Instead of "createHttpLink" use SchemaLink here
link: new SchemaLink({
schema: mergeSchemas({
schemas: typeDefs,
resolvers: resolvers
}),
context: async ({ req }) => {
const session = await getSession({ req })
return {
export default async function serverSideClient (req) {
const session = await getSession({ req })
console.log(session)
return new ApolloClient({
ssrMode: true,
// Instead of "createHttpLink" use SchemaLink here
link: new SchemaLink({
schema: mergeSchemas({
schemas: typeDefs,
resolvers: resolvers
}),
context: {
models,
me: session ? session.user : null
}
}
}),
cache: new InMemoryCache()
})
export default client
}),
cache: new InMemoryCache()
})
}

View File

@ -1,64 +1,5 @@
import { UserInputError, AuthenticationError } from 'apollo-server-micro'
const createItem = async (parent, { title, text, url, parentId }, { me, models }) => {
if (!me) {
throw new AuthenticationError('You must be logged in')
}
const data = {
title,
url,
text,
user: {
connect: {
name: me.name
}
}
}
if (parentId) {
data.parent = {
connect: {
id: parseInt(parentId)
}
}
}
const item = await models.item.create({ data })
item.comments = []
return item
}
function nestComments (flat, parentId) {
const result = []
let added = 0
for (let i = 0; i < flat.length;) {
if (!flat[i].comments) flat[i].comments = []
if (Number(flat[i].parentId) === Number(parentId)) {
result.push(flat[i])
added++
i++
} else if (result.length > 0) {
const item = result[result.length - 1]
const [nested, newAdded] = nestComments(flat.slice(i), item.id)
if (newAdded === 0) {
break
}
item.comments.push(...nested)
i += newAdded
added += newAdded
} else {
break
}
}
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 }) => {
@ -142,6 +83,29 @@ export default {
}
return await createItem(parent, { text, parentId }, { me, models })
},
vote: async (parent, { id, sats = 1 }, { me, models }) => {
// need to make sure we are logged in
if (!me) {
throw new AuthenticationError('You must be logged in')
}
const data = {
sats,
item: {
connect: {
id: parseInt(id)
}
},
user: {
connect: {
name: me.name
}
}
}
await models.vote.create({ data })
return sats
}
},
@ -155,6 +119,96 @@ export default {
WHERE path <@ text2ltree(${item.path}) AND id != ${item.id}`
return count
},
sats: () => 0
sats: async (item, args, { models }) => {
const { sum: { sats } } = await models.vote.aggregate({
sum: {
sats: true
},
where: {
itemId: item.id
}
})
return sats
},
meSats: async (item, args, { me, models }) => {
if (!me) return 0
const { sum: { sats } } = await models.vote.aggregate({
sum: {
sats: true
},
where: {
itemId: item.id,
userId: me.id
}
})
return sats
}
}
}
const createItem = async (parent, { title, text, url, parentId }, { me, models }) => {
if (!me) {
throw new AuthenticationError('You must be logged in')
}
const data = {
title,
url,
text,
item: {
connect: {
}
},
user: {
connect: {
name: me.name
}
}
}
if (parentId) {
data.parent = {
connect: {
id: parseInt(parentId)
}
}
}
const item = await models.item.create({ data })
item.comments = []
return item
}
function nestComments (flat, parentId) {
const result = []
let added = 0
for (let i = 0; i < flat.length;) {
if (!flat[i].comments) flat[i].comments = []
if (Number(flat[i].parentId) === Number(parentId)) {
result.push(flat[i])
added++
i++
} else if (result.length > 0) {
const item = result[result.length - 1]
const [nested, newAdded] = nestComments(flat.slice(i), item.id)
if (newAdded === 0) {
break
}
item.comments.push(...nested)
i += newAdded
added += newAdded
} else {
break
}
}
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"`

View File

@ -15,7 +15,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!
vote(id: ID!, sats: Int): Int!
}
type Item {
@ -28,6 +28,7 @@ export default gql`
user: User!
depth: Int!
sats: Int!
meSats: Int!
ncomments: Int!
comments: [Item!]!
}

View File

@ -50,7 +50,7 @@ export default function Comment ({ item, children, replyOpen, includeParent, cac
<div />
<div>
<div className={`${itemStyles.item} ${styles.item}`}>
<UpVote className={styles.upvote} />
<UpVote itemId={item.id} meSats={item.meSats} className={styles.upvote} />
<div className={itemStyles.hunk}>
<div className={itemStyles.other}>
<Link href={`/${item.user.name}`} passHref>

View File

@ -20,7 +20,7 @@ export default function Header () {
<>
<Nav.Item>
<Link href={'/' + session.user.name} passHref>
<Nav.Link className='text-reset'>@{session.user.name}</Nav.Link>
<Nav.Link>@{session.user.name}</Nav.Link>
</Link>
</Nav.Item>
<Nav.Item>
@ -51,6 +51,9 @@ export default function Header () {
<Nav.Link>post</Nav.Link>
</Link>
</Nav.Item>
<Nav.Item>
<Nav.Link href='https://bitcoinerjobs.co' target='_blank'>jobs</Nav.Link>
</Nav.Item>
</Nav>
<Nav className='ml-auto align-items-center' activeKey={router.asPath.split('?')[0]}>
<Corner />

View File

@ -13,7 +13,7 @@ export default function Item ({ item, rank, children }) {
</div>)
: <div />}
<div className={styles.item}>
<UpVote />
<UpVote itemId={item.id} meSats={item.meSats} />
<div className={styles.hunk}>
<div className={`${styles.main} flex-wrap flex-md-nowrap`}>
<Link href={`/items/${item.id}`} passHref>

View File

@ -24,7 +24,7 @@ export default function Reply ({ parentId, onSuccess, cacheId }) {
cache.modify({
id: cacheId || `Item:${parentId}`,
fields: {
comments (existingCommentRefs = [], { readField }) {
comments (existingCommentRefs = []) {
const newCommentRef = cache.writeFragment({
data: createComment,
fragment: COMMENTS,

View File

@ -1,16 +1,55 @@
import { LightningConsumer } from './lightning'
import UpArrow from '../svgs/lightning-arrow.svg'
import styles from './upvote.module.css'
import { gql, useMutation } from '@apollo/client'
export default function UpVote ({ itemId, meSats, className, item }) {
const [vote] = useMutation(
gql`
mutation vote($id: ID!, $sats: Int!) {
vote(id: $id, sats: $sats)
}`, {
update (cache, { data: { vote } }) {
if (item) {
item.sats += vote
item.meSats += vote
return
}
cache.modify({
id: `Item:${itemId}`,
fields: {
sats (existingSats = 0) {
return existingSats + vote
},
meSats (existingMeSats = 0) {
return existingMeSats + vote
}
}
})
}
}
)
export default function UpVote ({ className }) {
return (
<LightningConsumer>
{({ strike }) =>
<UpArrow
width={24}
height={24}
className={`${styles.upvote} ${className || ''}`}
onClick={strike}
className={
`${styles.upvote}
${className || ''}
${meSats ? (meSats > 1 ? styles.stimi : styles.voted) : ''}`
}
onClick={async () => {
if (!itemId) return
const { error } = await vote({ variables: { id: itemId, sats: 1 } })
if (error) {
throw new Error({ message: error.toString() })
}
strike()
}}
/>}
</LightningConsumer>
)

View File

@ -7,4 +7,14 @@
.upvote:hover {
fill: darkgray;
cursor: pointer;
}
.upvote.voted {
fill: #F6911D;
filter: drop-shadow(0 0 7px #F6911D);
}
.upvote.stimi {
fill: #993DF5;
filter: drop-shadow(0 0 7px #C28BF9);
}

View File

@ -10,6 +10,7 @@ export const COMMENT_FIELDS = gql`
name
}
sats
meSats
ncomments
}
`

View File

@ -11,6 +11,7 @@ export const ITEM_FIELDS = gql`
name
}
sats
meSats
ncomments
}`

View File

@ -5,8 +5,8 @@ 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({
export async function getServerSideProps ({ req, params }) {
const { error, data: { user } } = await (await ApolloClient(req)).query({
query:
gql`{
user(name: "${params.username}") {

View File

@ -1,4 +1,3 @@
import gql from 'graphql-tag'
import Item from '../../components/item'
import Layout from '../../components/layout'
import ApolloClient from '../../api/client'
@ -7,23 +6,18 @@ import Comment from '../../components/comment'
import Text from '../../components/text'
import Comments from '../../components/comments'
import { COMMENTS } from '../../fragments/comments'
import { ITEM_FIELDS } from '../../fragments/items'
import { gql } from '@apollo/client'
export async function getServerSideProps ({ params }) {
const { error, data: { item } } = await ApolloClient.query({
export async function getServerSideProps ({ req, params }) {
const { error, data: { item } } = await (await ApolloClient(req)).query({
query:
gql`{
gql`
${ITEM_FIELDS}
{
item(id: ${params.id}) {
id
createdAt
title
url
...ItemFields
text
parentId
user {
name
}
sats
ncomments
}
}`
})

View File

@ -66,7 +66,7 @@ export default function login ({ providers, csrfToken, error }) {
return (
<Button
className={`d-block mt-2 ${styles.providerButton}`}
className={`mt-2 ${styles.providerButton}`}
key={provider.name}
variant={variant}
onClick={() => signIn(provider.id)}

View File

@ -0,0 +1,26 @@
-- DropIndex
DROP INDEX "item_gist_path_index";
-- CreateTable
CREATE TABLE "Vote" (
"id" SERIAL NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
"sats" INTEGER NOT NULL,
"itemId" INTEGER NOT NULL,
"userId" INTEGER NOT NULL,
PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "Vote.itemId_index" ON "Vote"("itemId");
-- CreateIndex
CREATE INDEX "Vote.userId_index" ON "Vote"("userId");
-- AddForeignKey
ALTER TABLE "Vote" ADD FOREIGN KEY ("itemId") REFERENCES "Item"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Vote" ADD FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -0,0 +1,3 @@
-- AlterTable
ALTER TABLE "Vote" ADD COLUMN "stimi" BOOLEAN NOT NULL DEFAULT false,
ALTER COLUMN "sats" SET DEFAULT 1;

View File

@ -55,7 +55,8 @@ model Vote {
id Int @id @default(autoincrement())
createdAt DateTime @default(now()) @map(name: "created_at")
updatedAt DateTime @updatedAt @map(name: "updated_at")
sats Int
sats Int @default(1)
stimi Boolean @default(false)
item Item @relation(fields: [itemId], references: [id])
itemId Int
user User @relation(fields: [userId], references: [id])

View File

@ -5,6 +5,7 @@ $theme-colors: (
"info" : #007cbe,
"success" : #5c8001,
"twitter" : #1da1f2,
"boost" : #7A0CE9,
);
$body-bg: #fafafa;
@ -50,6 +51,14 @@ $container-max-widths: (
font-weight: bold;
}
.alert-dismissible .close {
font-weight: 300;
}
.nav-item {
font-weight: 500;
}
.form-control:focus {
border-color: #fada5e;
}

View File

@ -1,5 +1,8 @@
.login .providerButton {
width: 100%;
justify-content: center;
align-items: center;
display: flex;
}
.page {