nostr sub
This commit is contained in:
parent
d6c92fec62
commit
a241d683d8
|
@ -146,6 +146,10 @@ function recentClause (type) {
|
|||
}
|
||||
}
|
||||
|
||||
const subClause = (sub, num) => {
|
||||
return sub ? ` AND "subName" = $${num} ` : ` AND ("subName" IS NOT NULL OR "subName" = $${num}) `
|
||||
}
|
||||
|
||||
export default {
|
||||
Query: {
|
||||
itemRepetition: async (parent, { parentId }, { me, models }) => {
|
||||
|
@ -194,10 +198,6 @@ export default {
|
|||
const decodedCursor = decodeCursor(cursor)
|
||||
let items; let user; let pins; let subFull
|
||||
|
||||
const subClause = (num) => {
|
||||
return sub ? ` AND "subName" = $${num} ` : ` AND ("subName" IS NULL OR "subName" = $${num}) `
|
||||
}
|
||||
|
||||
const activeOrMine = () => {
|
||||
return me ? ` AND (status <> 'STOPPED' OR "userId" = ${me.id}) ` : ' AND status <> \'STOPPED\' '
|
||||
}
|
||||
|
@ -229,7 +229,7 @@ export default {
|
|||
${SELECT}
|
||||
FROM "Item"
|
||||
WHERE "parentId" IS NULL AND created_at <= $1
|
||||
${subClause(3)}
|
||||
${subClause(sub, 3)}
|
||||
${activeOrMine()}
|
||||
${await filterClause(me, models)}
|
||||
${recentClause(type)}
|
||||
|
@ -264,7 +264,7 @@ export default {
|
|||
FROM "Item"
|
||||
WHERE "parentId" IS NULL AND created_at <= $1
|
||||
AND "pinId" IS NULL
|
||||
${subClause(3)}
|
||||
${subClause(sub, 3)}
|
||||
AND status = 'ACTIVE' AND "maxBid" > 0
|
||||
ORDER BY "maxBid" DESC, created_at ASC)
|
||||
UNION ALL
|
||||
|
@ -272,7 +272,7 @@ export default {
|
|||
FROM "Item"
|
||||
WHERE "parentId" IS NULL AND created_at <= $1
|
||||
AND "pinId" IS NULL
|
||||
${subClause(3)}
|
||||
${subClause(sub, 3)}
|
||||
AND ((status = 'ACTIVE' AND "maxBid" = 0) OR status = 'NOSATS')
|
||||
ORDER BY created_at DESC)
|
||||
) a
|
||||
|
@ -292,7 +292,7 @@ export default {
|
|||
FROM "Item"
|
||||
WHERE "parentId" IS NULL AND "Item".created_at <= $1 AND "Item".created_at > $3
|
||||
AND "pinId" IS NULL AND NOT bio AND "deletedAt" IS NULL
|
||||
${subClause(4)}
|
||||
${subClause(sub, 4)}
|
||||
${await filterClause(me, models)}
|
||||
${await newTimedOrderByWeightedSats(me, models, 1)}
|
||||
OFFSET $2
|
||||
|
@ -305,7 +305,7 @@ export default {
|
|||
FROM "Item"
|
||||
WHERE "parentId" IS NULL AND "Item".created_at <= $1
|
||||
AND "pinId" IS NULL AND NOT bio AND "deletedAt" IS NULL
|
||||
${subClause(3)}
|
||||
${subClause(sub, 3)}
|
||||
${await filterClause(me, models)}
|
||||
${await newTimedOrderByWeightedSats(me, models, 1)}
|
||||
OFFSET $2
|
||||
|
@ -323,7 +323,8 @@ export default {
|
|||
)
|
||||
FROM "Item"
|
||||
WHERE "pinId" IS NOT NULL
|
||||
) rank_filter WHERE RANK = 1`)
|
||||
${subClause(sub, 1)}
|
||||
) rank_filter WHERE RANK = 1`, sub)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
@ -428,7 +429,7 @@ export default {
|
|||
items
|
||||
}
|
||||
},
|
||||
moreFlatComments: async (parent, { cursor, name, sort, within }, { me, models }) => {
|
||||
moreFlatComments: async (parent, { sub, cursor, name, sort, within }, { me, models }) => {
|
||||
const decodedCursor = decodeCursor(cursor)
|
||||
|
||||
let comments, user
|
||||
|
@ -437,11 +438,13 @@ export default {
|
|||
comments = await models.$queryRaw(`
|
||||
${SELECT}
|
||||
FROM "Item"
|
||||
WHERE "parentId" IS NOT NULL AND created_at <= $1
|
||||
JOIN "Item" root ON "Item"."rootId" = root.id
|
||||
WHERE "Item"."parentId" IS NOT NULL AND "Item".created_at <= $1
|
||||
AND (root."subName" = $3 OR (root."subName" IS NULL AND $3 IS NULL))
|
||||
${await filterClause(me, models)}
|
||||
ORDER BY created_at DESC
|
||||
ORDER BY "Item".created_at DESC
|
||||
OFFSET $2
|
||||
LIMIT ${LIMIT}`, decodedCursor.time, decodedCursor.offset)
|
||||
LIMIT ${LIMIT}`, decodedCursor.time, decodedCursor.offset, sub)
|
||||
break
|
||||
case 'user':
|
||||
if (!name) {
|
||||
|
@ -467,13 +470,15 @@ export default {
|
|||
comments = await models.$queryRaw(`
|
||||
${SELECT}
|
||||
FROM "Item"
|
||||
WHERE "parentId" IS NOT NULL AND "deletedAt" IS NULL
|
||||
JOIN "Item" root ON "Item"."rootId" = root.id
|
||||
WHERE "Item"."parentId" IS NOT NULL AND"Item"."deletedAt" IS NULL
|
||||
AND (root."subName" = $3 OR (root."subName" IS NULL AND $3 IS NULL))
|
||||
AND "Item".created_at <= $1
|
||||
${topClause(within)}
|
||||
${await filterClause(me, models)}
|
||||
${await topOrderByWeightedSats(me, models)}
|
||||
OFFSET $2
|
||||
LIMIT ${LIMIT}`, decodedCursor.time, decodedCursor.offset)
|
||||
LIMIT ${LIMIT}`, decodedCursor.time, decodedCursor.offset, sub)
|
||||
break
|
||||
default:
|
||||
throw new UserInputError('invalid sort type', { argumentName: 'sort' })
|
||||
|
@ -664,7 +669,7 @@ export default {
|
|||
}
|
||||
},
|
||||
upsertPoll: async (parent, { id, ...data }, { me, models }) => {
|
||||
const { forward, boost, title, text, options } = data
|
||||
const { sub, forward, boost, title, text, options } = data
|
||||
if (!me) {
|
||||
throw new AuthenticationError('you must be logged in')
|
||||
}
|
||||
|
@ -693,16 +698,16 @@ export default {
|
|||
throw new AuthenticationError('item does not belong to you')
|
||||
}
|
||||
const [item] = await serialize(models,
|
||||
models.$queryRaw(`${SELECT} FROM update_poll($1, $2, $3, $4, $5, $6) AS "Item"`,
|
||||
Number(id), title, text, Number(boost || 0), options, Number(fwdUser?.id)))
|
||||
models.$queryRaw(`${SELECT} FROM update_poll($1, $2, $3, $4, $5, $6, $7) AS "Item"`,
|
||||
sub || 'bitcoin', Number(id), title, text, Number(boost || 0), options, Number(fwdUser?.id)))
|
||||
|
||||
await createMentions(item, models)
|
||||
item.comments = []
|
||||
return item
|
||||
} else {
|
||||
const [item] = await serialize(models,
|
||||
models.$queryRaw(`${SELECT} FROM create_poll($1, $2, $3, $4, $5, $6, $7, '${ITEM_SPAM_INTERVAL}') AS "Item"`,
|
||||
title, text, 1, Number(boost || 0), Number(me.id), options, Number(fwdUser?.id)))
|
||||
models.$queryRaw(`${SELECT} FROM create_poll($1, $2, $3, $4, $5, $6, $7, $8, '${ITEM_SPAM_INTERVAL}') AS "Item"`,
|
||||
sub || 'bitcoin', title, text, 1, Number(boost || 0), Number(me.id), options, Number(fwdUser?.id)))
|
||||
|
||||
await createMentions(item, models)
|
||||
item.comments = []
|
||||
|
@ -1028,7 +1033,7 @@ export const createMentions = async (item, models) => {
|
|||
}
|
||||
}
|
||||
|
||||
export const updateItem = async (parent, { id, data: { title, url, text, boost, forward, bounty, parentId } }, { me, models }) => {
|
||||
export const updateItem = async (parent, { id, data: { sub, title, url, text, boost, forward, bounty, parentId } }, { me, models }) => {
|
||||
// update iff this item belongs to me
|
||||
const old = await models.item.findUnique({ where: { id: Number(id) } })
|
||||
if (Number(old.userId) !== Number(me?.id)) {
|
||||
|
@ -1059,15 +1064,16 @@ export const updateItem = async (parent, { id, data: { title, url, text, boost,
|
|||
|
||||
const [item] = await serialize(models,
|
||||
models.$queryRaw(
|
||||
`${SELECT} FROM update_item($1, $2, $3, $4, $5, $6, $7) AS "Item"`,
|
||||
Number(id), title, url, text, Number(boost || 0), bounty ? Number(bounty) : null, Number(fwdUser?.id)))
|
||||
`${SELECT} FROM update_item($1, $2, $3, $4, $5, $6, $7, $8) AS "Item"`,
|
||||
old.parentId ? null : sub || 'bitcoin', Number(id), title, url, text,
|
||||
Number(boost || 0), bounty ? Number(bounty) : null, Number(fwdUser?.id)))
|
||||
|
||||
await createMentions(item, models)
|
||||
|
||||
return item
|
||||
}
|
||||
|
||||
const createItem = async (parent, { title, url, text, boost, forward, bounty, parentId }, { me, models }) => {
|
||||
const createItem = async (parent, { sub, title, url, text, boost, forward, bounty, parentId }, { me, models }) => {
|
||||
if (!me) {
|
||||
throw new AuthenticationError('you must be logged in')
|
||||
}
|
||||
|
@ -1091,7 +1097,8 @@ const createItem = async (parent, { title, url, text, boost, forward, bounty, pa
|
|||
const [item] = await serialize(
|
||||
models,
|
||||
models.$queryRaw(
|
||||
`${SELECT} FROM create_item($1, $2, $3, $4, $5, $6, $7, $8, '${ITEM_SPAM_INTERVAL}') AS "Item"`,
|
||||
`${SELECT} FROM create_item($1, $2, $3, $4, $5, $6, $7, $8, $9, '${ITEM_SPAM_INTERVAL}') AS "Item"`,
|
||||
parentId ? null : sub || 'bitcoin',
|
||||
title,
|
||||
url,
|
||||
text,
|
||||
|
|
|
@ -79,7 +79,7 @@ export default {
|
|||
items
|
||||
}
|
||||
},
|
||||
search: async (parent, { q: query, sub, cursor, sort, what, when }, { me, models, search }) => {
|
||||
search: async (parent, { q: query, cursor, sort, what, when }, { me, models, search }) => {
|
||||
const decodedCursor = decodeCursor(cursor)
|
||||
let sitems
|
||||
|
||||
|
@ -105,7 +105,9 @@ export default {
|
|||
const queryArr = query.trim().split(/\s+/)
|
||||
const url = queryArr.find(word => word.startsWith('url:'))
|
||||
const nym = queryArr.find(word => word.startsWith('nym:'))
|
||||
query = queryArr.filter(word => !word.startsWith('url:') && !word.startsWith('nym:')).join(' ')
|
||||
const sub = queryArr.find(word => word.startsWith('~'))
|
||||
const exclude = [url, nym, sub]
|
||||
query = queryArr.filter(word => !exclude.includes(word)).join(' ')
|
||||
|
||||
if (url) {
|
||||
whatArr.push({ wildcard: { url: `*${url.slice(4).toLowerCase()}*` } })
|
||||
|
@ -115,6 +117,10 @@ export default {
|
|||
whatArr.push({ wildcard: { 'user.name': `*${nym.slice(4).toLowerCase()}*` } })
|
||||
}
|
||||
|
||||
if (sub) {
|
||||
whatArr.push({ match: { 'sub.name': sub.slice(1).toLowerCase() } })
|
||||
}
|
||||
|
||||
const sortArr = []
|
||||
switch (sort) {
|
||||
case 'recent':
|
||||
|
@ -204,9 +210,6 @@ export default {
|
|||
bool: {
|
||||
must: [
|
||||
...whatArr,
|
||||
sub
|
||||
? { match: { 'sub.name': sub } }
|
||||
: { bool: { must_not: { exists: { field: 'sub.name' } } } },
|
||||
me
|
||||
? {
|
||||
bool: {
|
||||
|
|
|
@ -3,7 +3,7 @@ import { gql } from 'apollo-server-micro'
|
|||
export default gql`
|
||||
extend type Query {
|
||||
items(sub: String, sort: String, type: String, cursor: String, name: String, within: String): Items
|
||||
moreFlatComments(sort: String!, cursor: String, name: String, within: String): Comments
|
||||
moreFlatComments(sub: String, sort: String!, cursor: String, name: String, within: String): Comments
|
||||
moreBookmarks(cursor: String, name: String!): Items
|
||||
item(id: ID!): Item
|
||||
comments(id: ID!, sort: String): [Item!]!
|
||||
|
@ -12,7 +12,7 @@ export default gql`
|
|||
related(cursor: String, title: String, id: ID, minMatch: String, limit: Int): Items
|
||||
allItems(cursor: String): Items
|
||||
getBountiesByUserName(name: String!, cursor: String, , limit: Int): Items
|
||||
search(q: String, sub: String, cursor: String, what: String, sort: String, when: String): Items
|
||||
search(q: String, cursor: String, what: String, sort: String, when: String): Items
|
||||
auctionPosition(sub: String, id: ID, bid: Int!): Int!
|
||||
itemRepetition(parentId: ID): Int!
|
||||
outlawedItems(cursor: String): Items
|
||||
|
@ -35,12 +35,12 @@ export default gql`
|
|||
extend type Mutation {
|
||||
bookmarkItem(id: ID): Item
|
||||
deleteItem(id: ID): Item
|
||||
upsertLink(id: ID, title: String!, url: String!, boost: Int, forward: String): Item!
|
||||
upsertDiscussion(id: ID, title: String!, text: String, boost: Int, forward: String): Item!
|
||||
upsertBounty(id: ID, title: String!, text: String, bounty: Int!, boost: Int, forward: String): Item!
|
||||
upsertJob(id: ID, sub: ID!, title: String!, company: String!, location: String, remote: Boolean,
|
||||
upsertLink(id: ID, sub: String, title: String!, url: String!, boost: Int, forward: String): Item!
|
||||
upsertDiscussion(id: ID, sub: String, title: String!, text: String, boost: Int, forward: String): Item!
|
||||
upsertBounty(id: ID, sub: String, title: String!, text: String, bounty: Int!, boost: Int, forward: String): Item!
|
||||
upsertJob(id: ID, sub: String!, title: String!, company: String!, location: String, remote: Boolean,
|
||||
text: String!, url: String!, maxBid: Int!, status: String, logo: Int): Item!
|
||||
upsertPoll(id: ID, title: String!, text: String, options: [String!]!, boost: Int, forward: String): Item!
|
||||
upsertPoll(id: ID, sub: String, title: String!, text: String, options: [String!]!, boost: Int, forward: String): Item!
|
||||
createComment(text: String!, parentId: ID!): Item!
|
||||
updateComment(id: ID!, text: String!): Item!
|
||||
dontLikeThis(id: ID!): Boolean!
|
||||
|
|
|
@ -2,12 +2,12 @@ import { gql } from 'apollo-server-micro'
|
|||
|
||||
export default gql`
|
||||
extend type Query {
|
||||
sub(name: ID!): Sub
|
||||
subLatestPost(name: ID!): String
|
||||
sub(name: String!): Sub
|
||||
subLatestPost(name: String!): String
|
||||
}
|
||||
|
||||
type Sub {
|
||||
name: ID!
|
||||
name: String!
|
||||
createdAt: String!
|
||||
updatedAt: String!
|
||||
postTypes: [String!]!
|
||||
|
|
|
@ -10,6 +10,7 @@ import { bountySchema } from '../lib/validate'
|
|||
|
||||
export function BountyForm ({
|
||||
item,
|
||||
sub,
|
||||
editThreshold,
|
||||
titleLabel = 'title',
|
||||
bountyLabel = 'bounty',
|
||||
|
@ -24,6 +25,7 @@ export function BountyForm ({
|
|||
const [upsertBounty] = useMutation(
|
||||
gql`
|
||||
mutation upsertBounty(
|
||||
$sub: String
|
||||
$id: ID
|
||||
$title: String!
|
||||
$bounty: Int!
|
||||
|
@ -32,6 +34,7 @@ export function BountyForm ({
|
|||
$forward: String
|
||||
) {
|
||||
upsertBounty(
|
||||
sub: $sub
|
||||
id: $id
|
||||
title: $title
|
||||
bounty: $bounty
|
||||
|
@ -59,6 +62,7 @@ export function BountyForm ({
|
|||
(async ({ boost, bounty, ...values }) => {
|
||||
const { error } = await upsertBounty({
|
||||
variables: {
|
||||
sub: item?.sub?.name || sub?.name,
|
||||
id: item?.id,
|
||||
boost: boost ? Number(boost) : undefined,
|
||||
bounty: bounty ? Number(bounty) : undefined,
|
||||
|
@ -72,7 +76,8 @@ export function BountyForm ({
|
|||
if (item) {
|
||||
await router.push(`/items/${item.id}`)
|
||||
} else {
|
||||
await router.push('/recent')
|
||||
const prefix = sub?.name ? `/~${sub.name}/` : ''
|
||||
await router.push(prefix + '/recent')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ import { Button } from 'react-bootstrap'
|
|||
import { discussionSchema } from '../lib/validate'
|
||||
|
||||
export function DiscussionForm ({
|
||||
item, editThreshold, titleLabel = 'title',
|
||||
item, sub, editThreshold, titleLabel = 'title',
|
||||
textLabel = 'text', buttonText = 'post',
|
||||
adv, handleSubmit
|
||||
}) {
|
||||
|
@ -23,8 +23,8 @@ export function DiscussionForm ({
|
|||
// const me = useMe()
|
||||
const [upsertDiscussion] = useMutation(
|
||||
gql`
|
||||
mutation upsertDiscussion($id: ID, $title: String!, $text: String, $boost: Int, $forward: String) {
|
||||
upsertDiscussion(id: $id, title: $title, text: $text, boost: $boost, forward: $forward) {
|
||||
mutation upsertDiscussion($sub: String, $id: ID, $title: String!, $text: String, $boost: Int, $forward: String) {
|
||||
upsertDiscussion(sub: $sub, id: $id, title: $title, text: $text, boost: $boost, forward: $forward) {
|
||||
id
|
||||
}
|
||||
}`
|
||||
|
@ -56,7 +56,7 @@ export function DiscussionForm ({
|
|||
schema={schema}
|
||||
onSubmit={handleSubmit || (async ({ boost, ...values }) => {
|
||||
const { error } = await upsertDiscussion({
|
||||
variables: { id: item?.id, boost: boost ? Number(boost) : undefined, ...values }
|
||||
variables: { sub: item?.sub?.name || sub?.name, id: item?.id, boost: boost ? Number(boost) : undefined, ...values }
|
||||
})
|
||||
if (error) {
|
||||
throw new Error({ message: error.toString() })
|
||||
|
@ -65,7 +65,8 @@ export function DiscussionForm ({
|
|||
if (item) {
|
||||
await router.push(`/items/${item.id}`)
|
||||
} else {
|
||||
await router.push('/recent')
|
||||
const prefix = sub?.name ? `/~${sub.name}/` : ''
|
||||
await router.push(prefix + '/recent')
|
||||
}
|
||||
})}
|
||||
storageKeyPrefix={item ? undefined : 'discussion'}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import React from 'react'
|
||||
import LayoutError from './layout-error'
|
||||
import LayoutStatic from './layout-static'
|
||||
import styles from '../styles/404.module.css'
|
||||
|
||||
class ErrorBoundary extends React.Component {
|
||||
|
@ -25,10 +25,10 @@ class ErrorBoundary extends React.Component {
|
|||
if (this.state.hasError) {
|
||||
// You can render any custom fallback UI
|
||||
return (
|
||||
<LayoutError>
|
||||
<LayoutStatic>
|
||||
<Image width='500' height='375' src='/floating.gif' fluid />
|
||||
<h1 className={styles.fourZeroFour} style={{ fontSize: '48px' }}>something went wrong</h1>
|
||||
</LayoutError>
|
||||
</LayoutStatic>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@ import { useEffect, useState } from 'react'
|
|||
// XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
|
||||
const COLORS = {
|
||||
light: {
|
||||
body: '#f5f5f5',
|
||||
body: '#f5f5f7',
|
||||
color: '#212529',
|
||||
navbarVariant: 'light',
|
||||
navLink: 'rgba(0, 0, 0, 0.55)',
|
||||
|
@ -157,9 +157,9 @@ export default function Footer ({ noLinks }) {
|
|||
{!noLinks &&
|
||||
<>
|
||||
{mounted &&
|
||||
<div className='mb-2'>
|
||||
<DarkModeIcon onClick={() => darkMode.toggle()} className='fill-grey theme' />
|
||||
<LnIcon onClick={toggleLightning} width={24} height={24} className='ml-2 fill-grey theme' />
|
||||
<div className='mb-1'>
|
||||
<DarkModeIcon onClick={() => darkMode.toggle()} width={20} height={20} className='fill-grey theme' />
|
||||
<LnIcon onClick={toggleLightning} width={20} height={20} className='ml-2 fill-grey theme' />
|
||||
</div>}
|
||||
<div className='mb-0' style={{ fontWeight: 500 }}>
|
||||
<Link href='/rewards' passHref>
|
||||
|
|
|
@ -16,6 +16,8 @@ import NoteIcon from '../svgs/notification-4-fill.svg'
|
|||
import { useQuery, gql } from '@apollo/client'
|
||||
import LightningIcon from '../svgs/bolt.svg'
|
||||
import CowboyHat from './cowboy-hat'
|
||||
import { Form, Select } from './form'
|
||||
import SearchIcon from '../svgs/search-line.svg'
|
||||
|
||||
function WalletSummary ({ me }) {
|
||||
if (!me) return null
|
||||
|
@ -25,34 +27,44 @@ function WalletSummary ({ me }) {
|
|||
|
||||
export default function Header ({ sub }) {
|
||||
const router = useRouter()
|
||||
const path = router.asPath.split('?')[0]
|
||||
const [fired, setFired] = useState()
|
||||
const [topNavKey, setTopNavKey] = useState('')
|
||||
const [dropNavKey, setDropNavKey] = useState('')
|
||||
const [prefix, setPrefix] = useState('')
|
||||
const [path, setPath] = useState('')
|
||||
const me = useMe()
|
||||
const prefix = sub ? `/~${sub}` : ''
|
||||
|
||||
useEffect(() => {
|
||||
// there's always at least 2 on the split, e.g. '/' yields ['','']
|
||||
const topNavKey = path.split('/')[sub ? 2 : 1]
|
||||
const dropNavKey = path.split('/').slice(sub ? 2 : 1).join('/')
|
||||
const { data: subLatestPost } = useQuery(gql`
|
||||
query subLatestPost($name: ID!) {
|
||||
subLatestPost(name: $name)
|
||||
}
|
||||
`, { variables: { name: 'jobs' }, pollInterval: 600000, fetchPolicy: 'network-only' })
|
||||
const path = router.asPath.split('?')[0]
|
||||
console.log(path, path.split('/')[sub ? 2 : 1], path.split('/').slice(sub ? 2 : 1).join('/'))
|
||||
setPrefix(sub ? `/~${sub}` : '')
|
||||
setTopNavKey(path.split('/')[sub ? 2 : 1] ?? '')
|
||||
setDropNavKey(path.split('/').slice(sub ? 2 : 1).join('/'))
|
||||
setPath(path)
|
||||
}, [sub, router.asPath])
|
||||
|
||||
// const { data: subLatestPost } = useQuery(gql`
|
||||
// query subLatestPost($name: ID!) {
|
||||
// subLatestPost(name: $name)
|
||||
// }
|
||||
// `, { variables: { name: 'jobs' }, pollInterval: 600000, fetchPolicy: 'network-only' })
|
||||
const { data: hasNewNotes } = useQuery(gql`
|
||||
{
|
||||
hasNewNotes
|
||||
}
|
||||
`, { pollInterval: 30000, fetchPolicy: 'cache-and-network' })
|
||||
const [lastCheckedJobs, setLastCheckedJobs] = useState(new Date().getTime())
|
||||
useEffect(() => {
|
||||
if (me) {
|
||||
setLastCheckedJobs(me.lastCheckedJobs)
|
||||
} else {
|
||||
if (sub === 'jobs') {
|
||||
localStorage.setItem('lastCheckedJobs', new Date().getTime())
|
||||
}
|
||||
setLastCheckedJobs(localStorage.getItem('lastCheckedJobs'))
|
||||
}
|
||||
}, [sub])
|
||||
// const [lastCheckedJobs, setLastCheckedJobs] = useState(new Date().getTime())
|
||||
// useEffect(() => {
|
||||
// if (me) {
|
||||
// setLastCheckedJobs(me.lastCheckedJobs)
|
||||
// } else {
|
||||
// if (sub === 'jobs') {
|
||||
// localStorage.setItem('lastCheckedJobs', new Date().getTime())
|
||||
// }
|
||||
// setLastCheckedJobs(localStorage.getItem('lastCheckedJobs'))
|
||||
// }
|
||||
// }, [sub])
|
||||
|
||||
const Corner = () => {
|
||||
if (me) {
|
||||
|
@ -63,7 +75,7 @@ export default function Header ({ sub }) {
|
|||
</Head>
|
||||
<Link href='/notifications' passHref>
|
||||
<Nav.Link eventKey='notifications' className='pl-0 position-relative'>
|
||||
<NoteIcon className='theme' />
|
||||
<NoteIcon height={22} width={22} className='theme' />
|
||||
{hasNewNotes?.hasNewNotes &&
|
||||
<span className={styles.notification}>
|
||||
<span className='invisible'>{' '}</span>
|
||||
|
@ -73,11 +85,9 @@ export default function Header ({ sub }) {
|
|||
<div className='position-relative'>
|
||||
<NavDropdown
|
||||
className={styles.dropdown} title={
|
||||
<Link href={`/${me?.name}`} passHref>
|
||||
<Nav.Link eventKey={me?.name} as='div' className='p-0 d-flex align-items-center' onClick={e => e.preventDefault()}>
|
||||
<Nav.Link eventKey={me?.name} as='span' className='p-0 d-flex align-items-center' onClick={e => e.preventDefault()}>
|
||||
{`@${me?.name}`}<CowboyHat streak={me.streak} />
|
||||
</Nav.Link>
|
||||
</Link>
|
||||
} alignRight
|
||||
>
|
||||
<Link href={'/' + me?.name} passHref>
|
||||
|
@ -142,7 +152,7 @@ export default function Header ({ sub }) {
|
|||
}, [])
|
||||
}
|
||||
return path !== '/login' && path !== '/signup' && !path.startsWith('/invites') &&
|
||||
<div>
|
||||
<div className='ml-auto'>
|
||||
<Button
|
||||
className='align-items-center px-3 py-1 mr-2'
|
||||
id='signup'
|
||||
|
@ -168,12 +178,32 @@ export default function Header ({ sub }) {
|
|||
}
|
||||
}
|
||||
|
||||
const showJobIndicator = sub !== 'jobs' && (!me || me.noteJobIndicator) &&
|
||||
(!lastCheckedJobs || lastCheckedJobs < subLatestPost?.subLatestPost)
|
||||
// const showJobIndicator = sub !== 'jobs' && (!me || me.noteJobIndicator) &&
|
||||
// (!lastCheckedJobs || lastCheckedJobs < subLatestPost?.subLatestPost)
|
||||
|
||||
const NavItems = ({ className }) => {
|
||||
return (
|
||||
<>
|
||||
<Nav.Item className={className}>
|
||||
<Form
|
||||
initial={{
|
||||
sub: sub || 'home'
|
||||
}}
|
||||
>
|
||||
<Select
|
||||
groupClassName='mb-0'
|
||||
onChange={(formik, e) => router.push(e.target.value === 'home' ? '/' : `/~${e.target.value}`)}
|
||||
name='sub'
|
||||
size='sm'
|
||||
items={['home', 'bitcoin', 'nostr', 'jobs']}
|
||||
/>
|
||||
</Form>
|
||||
</Nav.Item>
|
||||
<Nav.Item className={className}>
|
||||
<Link href={prefix + '/'} passHref>
|
||||
<Nav.Link eventKey='' className={styles.navLink}>hot</Nav.Link>
|
||||
</Link>
|
||||
</Nav.Item>
|
||||
<Nav.Item className={className}>
|
||||
<Link href={prefix + '/recent'} passHref>
|
||||
<Nav.Link eventKey='recent' className={styles.navLink}>recent</Nav.Link>
|
||||
|
@ -185,7 +215,7 @@ export default function Header ({ sub }) {
|
|||
<Nav.Link eventKey='top' className={styles.navLink}>top</Nav.Link>
|
||||
</Link>
|
||||
</Nav.Item>}
|
||||
<Nav.Item className={className}>
|
||||
{/* <Nav.Item className={className}>
|
||||
<div className='position-relative'>
|
||||
<Link href='/~jobs' passHref>
|
||||
<Nav.Link active={sub === 'jobs'} className={styles.navLink}>
|
||||
|
@ -197,50 +227,65 @@ export default function Header ({ sub }) {
|
|||
<span className='invisible'>{' '}</span>
|
||||
</span>}
|
||||
</div>
|
||||
</Nav.Item>
|
||||
{me &&
|
||||
<Nav.Item className={className}>
|
||||
<Link href={prefix + '/post'} passHref>
|
||||
<Nav.Link eventKey='post' className={styles.navLinkButton}>post</Nav.Link>
|
||||
</Link>
|
||||
</Nav.Item>}
|
||||
</Nav.Item> */}
|
||||
{/* <Nav.Item className={`text-monospace nav-link mx-auto px-0 ${me?.name.length > 6 ? 'd-none d-lg-flex' : ''}`}>
|
||||
<Price />
|
||||
</Nav.Item> */}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const PostItem = ({ className }) => {
|
||||
return me
|
||||
? (
|
||||
<Nav.Link eventKey='post' className={`${className}`}>
|
||||
<Link href={prefix + '/post'} passHref>
|
||||
<button className='btn btn-md btn-primary px-3 py-1'>post</button>
|
||||
</Link>
|
||||
</Nav.Link>)
|
||||
: null
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Container className='px-sm-0'>
|
||||
<Navbar className='pb-0 pb-md-1'>
|
||||
<Container className='px-0'>
|
||||
<Navbar className='pb-0 pb-md-2'>
|
||||
<Nav
|
||||
className={styles.navbarNav}
|
||||
activeKey={topNavKey}
|
||||
>
|
||||
<div className='d-flex'>
|
||||
<div className='d-flex align-items-center'>
|
||||
<Link href='/' passHref>
|
||||
<Navbar.Brand className={`${styles.brand} d-none d-md-block`}>
|
||||
STACKER NEWS
|
||||
</Navbar.Brand>
|
||||
</Link>
|
||||
<Link href='/' passHref>
|
||||
<Navbar.Brand className={`${styles.brand} d-block d-md-none`}>
|
||||
<Navbar.Brand className={`${styles.brand} d-flex`}>
|
||||
SN
|
||||
</Navbar.Brand>
|
||||
</Link>
|
||||
</div>
|
||||
<NavItems className='d-none d-md-flex' />
|
||||
<Nav.Item className={`text-monospace nav-link px-0 ${me?.name.length > 6 ? 'd-none d-lg-flex' : ''}`}>
|
||||
<Price />
|
||||
<NavItems className='d-none d-md-flex mx-2' />
|
||||
<PostItem className='d-none d-md-flex mx-2' />
|
||||
<Link href='/search' passHref>
|
||||
<Nav.Link eventKey='search' className='position-relative d-none d-md-flex align-items-center mx-2'>
|
||||
<SearchIcon className='theme' width={22} height={22} />
|
||||
</Nav.Link>
|
||||
</Link>
|
||||
<Nav.Item className={`${styles.price} mx-auto align-items-center ${me?.name.length > 10 ? 'd-none d-lg-flex' : ''}`}>
|
||||
<Price className='nav-link text-monospace' />
|
||||
</Nav.Item>
|
||||
<Corner />
|
||||
</Nav>
|
||||
</Navbar>
|
||||
<Navbar className='pt-0 pb-1 d-md-none'>
|
||||
<Navbar className='pt-0 pb-2 d-md-none'>
|
||||
<Nav
|
||||
className={`${styles.navbarNav} justify-content-around`}
|
||||
className={`${styles.navbarNav}`}
|
||||
activeKey={topNavKey}
|
||||
>
|
||||
<NavItems />
|
||||
<NavItems className='mr-1' />
|
||||
<Link href='/search' passHref>
|
||||
<Nav.Link eventKey='search' className='position-relative ml-auto d-flex mr-1'>
|
||||
<SearchIcon className='theme' width={22} height={22} />
|
||||
</Nav.Link>
|
||||
</Link>
|
||||
<PostItem className='mr-0 pr-0' />
|
||||
</Nav>
|
||||
</Navbar>
|
||||
</Container>
|
||||
|
@ -248,32 +293,6 @@ export default function Header ({ sub }) {
|
|||
)
|
||||
}
|
||||
|
||||
const NavItemsStatic = ({ className }) => {
|
||||
return (
|
||||
<>
|
||||
<Nav.Item className={className}>
|
||||
<Link href='/recent' passHref>
|
||||
<Nav.Link className={styles.navLink}>recent</Nav.Link>
|
||||
</Link>
|
||||
</Nav.Item>
|
||||
<Nav.Item className={className}>
|
||||
<Link href='/top/posts/day' passHref>
|
||||
<Nav.Link className={styles.navLink}>top</Nav.Link>
|
||||
</Link>
|
||||
</Nav.Item>
|
||||
<Nav.Item className={className}>
|
||||
<div className='position-relative'>
|
||||
<Link href='/~jobs' passHref>
|
||||
<Nav.Link className={styles.navLink}>
|
||||
jobs
|
||||
</Nav.Link>
|
||||
</Link>
|
||||
</div>
|
||||
</Nav.Item>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export function HeaderStatic () {
|
||||
return (
|
||||
<Container className='px-sm-0'>
|
||||
|
@ -281,29 +300,19 @@ export function HeaderStatic () {
|
|||
<Nav
|
||||
className={styles.navbarNav}
|
||||
>
|
||||
<div className='d-flex'>
|
||||
<div className='d-flex align-items-center'>
|
||||
<Link href='/' passHref>
|
||||
<Navbar.Brand className={`${styles.brand} d-none d-md-block`}>
|
||||
STACKER NEWS
|
||||
</Navbar.Brand>
|
||||
</Link>
|
||||
<Link href='/' passHref>
|
||||
<Navbar.Brand className={`${styles.brand} d-block d-md-none`}>
|
||||
<Navbar.Brand className={`${styles.brand}`}>
|
||||
SN
|
||||
</Navbar.Brand>
|
||||
</Link>
|
||||
|
||||
<Link href='/search' passHref>
|
||||
<Nav.Link eventKey='search' className='position-relative d-flex align-items-center mx-2'>
|
||||
<SearchIcon className='theme' width={22} height={22} />
|
||||
</Nav.Link>
|
||||
</Link>
|
||||
</div>
|
||||
<NavItemsStatic className='d-none d-md-flex' />
|
||||
<Nav.Item className='text-monospace nav-link px-0'>
|
||||
<Price />
|
||||
</Nav.Item>
|
||||
</Nav>
|
||||
</Navbar>
|
||||
<Navbar className='pt-0 pb-1 d-md-none'>
|
||||
<Nav
|
||||
className={`${styles.navbarNav} justify-content-around`}
|
||||
>
|
||||
<NavItemsStatic />
|
||||
</Nav>
|
||||
</Navbar>
|
||||
</Container>
|
||||
|
|
|
@ -48,10 +48,13 @@
|
|||
|
||||
.navbarNav {
|
||||
width: 100%;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
height: 39px;
|
||||
align-content: center;
|
||||
}
|
||||
|
||||
.price {
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import Link from 'next/link'
|
||||
import styles from './item.module.css'
|
||||
import UpVote from './upvote'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useRef } from 'react'
|
||||
import { NOFOLLOW_LIMIT } from '../lib/constants'
|
||||
import Pin from '../svgs/pushpin-fill.svg'
|
||||
import reactStringReplace from 'react-string-replace'
|
||||
|
@ -19,15 +19,8 @@ export function SearchTitle ({ title }) {
|
|||
}
|
||||
|
||||
export default function Item ({ item, rank, belowTitle, right, children }) {
|
||||
const [wrap, setWrap] = useState(false)
|
||||
const titleRef = useRef()
|
||||
|
||||
useEffect(() => {
|
||||
setWrap(
|
||||
Math.ceil(parseFloat(window.getComputedStyle(titleRef.current).lineHeight)) <
|
||||
titleRef.current.clientHeight)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
{rank
|
||||
|
@ -41,7 +34,7 @@ export default function Item ({ item, rank, belowTitle, right, children }) {
|
|||
? <Pin width={24} height={24} className={styles.pin} />
|
||||
: item.meDontLike ? <Flag width={24} height={24} className={`${styles.dontLike}`} /> : <UpVote item={item} className={styles.upvote} />}
|
||||
<div className={styles.hunk}>
|
||||
<div className={`${styles.main} flex-wrap ${wrap ? 'd-inline' : ''}`}>
|
||||
<div className={`${styles.main} flex-wrap`}>
|
||||
<Link href={`/items/${item.id}`} passHref>
|
||||
<a ref={titleRef} className={`${styles.title} text-reset mr-2`}>
|
||||
{item.searchTitle ? <SearchTitle title={item.searchTitle} /> : item.title}
|
||||
|
@ -58,7 +51,7 @@ export default function Item ({ item, rank, belowTitle, right, children }) {
|
|||
<>
|
||||
{/* eslint-disable-next-line */}
|
||||
<a
|
||||
className={`${styles.link} ${wrap ? styles.linkSmall : ''}`} target='_blank' href={item.url}
|
||||
className={`${styles.link} py-half py-md-0`} target='_blank' href={item.url}
|
||||
rel={item.sats + item.boost >= NOFOLLOW_LIMIT ? null : 'nofollow'}
|
||||
>
|
||||
{item.url.replace(/(^https?:|^)\/\//, '')}
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
import Footer from './footer'
|
||||
import { HeaderStatic } from './header'
|
||||
import styles from './layout-center.module.css'
|
||||
import Search from './search'
|
||||
|
||||
export default function LayoutError ({ children, ...props }) {
|
||||
export default function LayoutStatic ({ children, ...props }) {
|
||||
return (
|
||||
<div className={styles.page}>
|
||||
<HeaderStatic />
|
||||
|
@ -11,7 +10,6 @@ export default function LayoutError ({ children, ...props }) {
|
|||
{children}
|
||||
</div>
|
||||
<Footer />
|
||||
<Search />
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -7,7 +7,7 @@ import Search from './search'
|
|||
|
||||
export default function Layout ({
|
||||
sub, noContain, noFooter, noFooterLinks,
|
||||
containClassName, noSeo, children
|
||||
containClassName, noSeo, children, search
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
|
@ -22,7 +22,7 @@ export default function Layout ({
|
|||
</Container>
|
||||
)}
|
||||
{!noFooter && <Footer noLinks={noFooterLinks} />}
|
||||
{!noContain && <Search sub={sub} />}
|
||||
{!noContain && search && <Search sub={sub} />}
|
||||
</LightningProvider>
|
||||
</>
|
||||
)
|
||||
|
|
|
@ -13,7 +13,7 @@ import { Button } from 'react-bootstrap'
|
|||
import { linkSchema } from '../lib/validate'
|
||||
import Moon from '../svgs/moon-fill.svg'
|
||||
|
||||
export function LinkForm ({ item, editThreshold }) {
|
||||
export function LinkForm ({ item, sub, editThreshold }) {
|
||||
const router = useRouter()
|
||||
const client = useApolloClient()
|
||||
const schema = linkSchema(client)
|
||||
|
@ -66,8 +66,8 @@ export function LinkForm ({ item, editThreshold }) {
|
|||
|
||||
const [upsertLink] = useMutation(
|
||||
gql`
|
||||
mutation upsertLink($id: ID, $title: String!, $url: String!, $boost: Int, $forward: String) {
|
||||
upsertLink(id: $id, title: $title, url: $url, boost: $boost, forward: $forward) {
|
||||
mutation upsertLink($sub: String, $id: ID, $title: String!, $url: String!, $boost: Int, $forward: String) {
|
||||
upsertLink(sub: $sub, id: $id, title: $title, url: $url, boost: $boost, forward: $forward) {
|
||||
id
|
||||
}
|
||||
}`
|
||||
|
@ -85,7 +85,7 @@ export function LinkForm ({ item, editThreshold }) {
|
|||
schema={schema}
|
||||
onSubmit={async ({ boost, title, ...values }) => {
|
||||
const { error } = await upsertLink({
|
||||
variables: { id: item?.id, boost: boost ? Number(boost) : undefined, title: title.trim(), ...values }
|
||||
variables: { sub: item?.sub?.name || sub?.name, id: item?.id, boost: boost ? Number(boost) : undefined, title: title.trim(), ...values }
|
||||
})
|
||||
if (error) {
|
||||
throw new Error({ message: error.toString() })
|
||||
|
@ -93,7 +93,8 @@ export function LinkForm ({ item, editThreshold }) {
|
|||
if (item) {
|
||||
await router.push(`/items/${item.id}`)
|
||||
} else {
|
||||
await router.push('/recent')
|
||||
const prefix = sub?.name ? `/~${sub.name}/` : ''
|
||||
await router.push(prefix + '/recent')
|
||||
}
|
||||
}}
|
||||
storageKeyPrefix={item ? undefined : 'link'}
|
||||
|
|
|
@ -10,16 +10,16 @@ import Delete from './delete'
|
|||
import { Button } from 'react-bootstrap'
|
||||
import { pollSchema } from '../lib/validate'
|
||||
|
||||
export function PollForm ({ item, editThreshold }) {
|
||||
export function PollForm ({ item, sub, editThreshold }) {
|
||||
const router = useRouter()
|
||||
const client = useApolloClient()
|
||||
const schema = pollSchema(client)
|
||||
|
||||
const [upsertPoll] = useMutation(
|
||||
gql`
|
||||
mutation upsertPoll($id: ID, $title: String!, $text: String,
|
||||
mutation upsertPoll($sub: String, $id: ID, $title: String!, $text: String,
|
||||
$options: [String!]!, $boost: Int, $forward: String) {
|
||||
upsertPoll(id: $id, title: $title, text: $text,
|
||||
upsertPoll(sub: $sub, id: $id, title: $title, text: $text,
|
||||
options: $options, boost: $boost, forward: $forward) {
|
||||
id
|
||||
}
|
||||
|
@ -42,6 +42,7 @@ export function PollForm ({ item, editThreshold }) {
|
|||
const { error } = await upsertPoll({
|
||||
variables: {
|
||||
id: item?.id,
|
||||
sub: item?.sub?.name || sub?.name,
|
||||
boost: boost ? Number(boost) : undefined,
|
||||
title: title.trim(),
|
||||
options: optionsFiltered,
|
||||
|
@ -54,7 +55,8 @@ export function PollForm ({ item, editThreshold }) {
|
|||
if (item) {
|
||||
await router.push(`/items/${item.id}`)
|
||||
} else {
|
||||
await router.push('/recent')
|
||||
const prefix = sub?.name ? `/~${sub.name}/` : ''
|
||||
await router.push(prefix + '/recent')
|
||||
}
|
||||
}}
|
||||
storageKeyPrefix={item ? undefined : 'poll'}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import React, { useContext, useEffect, useState } from 'react'
|
||||
import { useQuery } from '@apollo/client'
|
||||
import { Button } from 'react-bootstrap'
|
||||
import { fixedDecimal } from '../lib/format'
|
||||
import { useMe } from './me'
|
||||
import { PRICE } from '../fragments/price'
|
||||
|
@ -32,7 +31,7 @@ export function PriceProvider ({ price, children }) {
|
|||
)
|
||||
}
|
||||
|
||||
export default function Price () {
|
||||
export default function Price ({ className }) {
|
||||
const [asSats, setAsSats] = useState(undefined)
|
||||
useEffect(() => {
|
||||
setAsSats(localStorage.getItem('asSats'))
|
||||
|
@ -54,25 +53,27 @@ export default function Price () {
|
|||
}
|
||||
}
|
||||
|
||||
const compClassName = (className || '') + ' text-reset pointer'
|
||||
|
||||
if (asSats === 'yep') {
|
||||
return (
|
||||
<Button className='text-reset p-0 line-height-1' onClick={handleClick} variant='link'>
|
||||
<div className={compClassName} onClick={handleClick} variant='link'>
|
||||
{fixedDecimal(100000000 / price, 0) + ` sats/${fiatSymbol}`}
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (asSats === '1btc') {
|
||||
return (
|
||||
<Button className='text-reset p-0 line-height-1' onClick={handleClick} variant='link'>
|
||||
<div className={compClassName} onClick={handleClick} variant='link'>
|
||||
1sat=1sat
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Button className='text-reset p-0 line-height-1' onClick={handleClick} variant='link'>
|
||||
<div className={compClassName} onClick={handleClick} variant='link'>
|
||||
{fiatSymbol + fixedDecimal(price, 0)}
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,8 +1,14 @@
|
|||
import { Form, Select } from './form'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
export default function RecentHeader ({ type }) {
|
||||
export default function RecentHeader ({ type, sub }) {
|
||||
const router = useRouter()
|
||||
const prefix = sub?.name ? `/~${sub.name}` : ''
|
||||
|
||||
const items = ['posts', 'bounties', 'comments', 'links', 'discussions', 'polls']
|
||||
if (!sub?.name) {
|
||||
items.push('bios')
|
||||
}
|
||||
|
||||
return (
|
||||
<Form
|
||||
|
@ -10,14 +16,14 @@ export default function RecentHeader ({ type }) {
|
|||
type: router.query.type || type || 'posts'
|
||||
}}
|
||||
>
|
||||
<div className='text-muted font-weight-bold mt-1 mb-3 d-flex justify-content-end align-items-center'>
|
||||
<div className='text-muted font-weight-bold mt-0 mb-3 d-flex justify-content-end align-items-center'>
|
||||
<Select
|
||||
groupClassName='mb-0 ml-2'
|
||||
className='w-auto'
|
||||
name='type'
|
||||
size='sm'
|
||||
items={['posts', 'bounties', 'comments', 'links', 'discussions', 'polls', 'bios']}
|
||||
onChange={(formik, e) => router.push(e.target.value === 'posts' ? '/recent' : `/recent/${e.target.value}`)}
|
||||
items={items}
|
||||
onChange={(formik, e) => router.push(prefix + (e.target.value === 'posts' ? '/recent' : `/recent/${e.target.value}`))}
|
||||
/>
|
||||
</div>
|
||||
</Form>
|
||||
|
|
|
@ -49,7 +49,7 @@ export default function Search ({ sub }) {
|
|||
}
|
||||
|
||||
const showSearch = atBottom || searching || router.query.q
|
||||
const filter = router.query.q && !sub
|
||||
const filter = sub !== 'jobs'
|
||||
return (
|
||||
<>
|
||||
<div className={`${styles.searchSection} ${showSearch ? styles.solid : styles.hidden}`}>
|
||||
|
@ -100,7 +100,7 @@ export default function Search ({ sub }) {
|
|||
<Input
|
||||
name='q'
|
||||
required
|
||||
autoFocus={showSearch && !atBottom}
|
||||
autoFocus
|
||||
groupClassName='mr-3 mb-0 flex-grow-1'
|
||||
className='flex-grow-1'
|
||||
clear
|
||||
|
|
|
@ -11,6 +11,7 @@ export default function Share ({ item }) {
|
|||
? (
|
||||
<div className='ml-auto pointer d-flex align-items-center'>
|
||||
<ShareIcon
|
||||
width={20} height={20}
|
||||
className='mx-2 fill-grey theme'
|
||||
onClick={() => {
|
||||
if (navigator.share) {
|
||||
|
@ -29,7 +30,7 @@ export default function Share ({ item }) {
|
|||
: (
|
||||
<Dropdown alignRight className='ml-auto pointer d-flex align-items-center' as='span'>
|
||||
<Dropdown.Toggle variant='success' id='dropdown-basic' as='a'>
|
||||
<ShareIcon className='mx-2 fill-grey theme' />
|
||||
<ShareIcon width={20} height={20} className='mx-2 fill-grey theme' />
|
||||
</Dropdown.Toggle>
|
||||
|
||||
<Dropdown.Menu>
|
||||
|
|
|
@ -26,7 +26,7 @@ export default function Toc ({ text }) {
|
|||
return (
|
||||
<Dropdown alignRight className='d-flex align-items-center'>
|
||||
<Dropdown.Toggle as={CustomToggle} id='dropdown-custom-components'>
|
||||
<TocIcon className='mx-2 fill-grey theme' />
|
||||
<TocIcon width={20} height={20} className='mx-2 fill-grey theme' />
|
||||
</Dropdown.Toggle>
|
||||
|
||||
<Dropdown.Menu as={CustomMenu}>
|
||||
|
|
|
@ -42,8 +42,8 @@ export const COMMENT_FIELDS = gql`
|
|||
export const MORE_FLAT_COMMENTS = gql`
|
||||
${COMMENT_FIELDS}
|
||||
|
||||
query MoreFlatComments($sort: String!, $cursor: String, $name: String, $within: String) {
|
||||
moreFlatComments(sort: $sort, cursor: $cursor, name: $name, within: $within) {
|
||||
query MoreFlatComments($sub: String, $sort: String!, $cursor: String, $name: String, $within: String) {
|
||||
moreFlatComments(sub: $sub, sort: $sort, cursor: $cursor, name: $name, within: $within) {
|
||||
cursor
|
||||
comments {
|
||||
...CommentFields
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { gql } from '@apollo/client'
|
||||
import { ITEM_FIELDS } from './items'
|
||||
import { COMMENT_FIELDS } from './comments'
|
||||
|
||||
export const SUB_FIELDS = gql`
|
||||
fragment SubFields on Sub {
|
||||
|
@ -12,7 +13,7 @@ export const SUB_FIELDS = gql`
|
|||
export const SUB = gql`
|
||||
${SUB_FIELDS}
|
||||
|
||||
query Sub($sub: ID!) {
|
||||
query Sub($sub: String!) {
|
||||
sub(name: $sub) {
|
||||
...SubFields
|
||||
}
|
||||
|
@ -21,11 +22,11 @@ export const SUB = gql`
|
|||
export const SUB_ITEMS = gql`
|
||||
${SUB_FIELDS}
|
||||
${ITEM_FIELDS}
|
||||
query SubRecent($sub: String, $sort: String) {
|
||||
query SubItems($sub: String!, $sort: String, $type: String) {
|
||||
sub(name: $sub) {
|
||||
...SubFields
|
||||
}
|
||||
items(sub: $sub, sort: $sort) {
|
||||
items(sub: $sub, sort: $sort, type: $type) {
|
||||
cursor
|
||||
items {
|
||||
...ItemFields
|
||||
|
@ -42,11 +43,11 @@ export const SUB_ITEMS = gql`
|
|||
export const SUB_SEARCH = gql`
|
||||
${SUB_FIELDS}
|
||||
${ITEM_FIELDS}
|
||||
query SubSearch($sub: String, $q: String, $cursor: String) {
|
||||
query SubSearch($sub: String!, $q: String, $cursor: String) {
|
||||
sub(name: $sub) {
|
||||
...SubFields
|
||||
}
|
||||
search(q: $q, sub: $sub, cursor: $cursor) {
|
||||
search(q: $q, cursor: $cursor) {
|
||||
cursor
|
||||
items {
|
||||
...ItemFields
|
||||
|
@ -57,3 +58,21 @@ export const SUB_SEARCH = gql`
|
|||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const SUB_FLAT_COMMENTS = gql`
|
||||
${SUB_FIELDS}
|
||||
${COMMENT_FIELDS}
|
||||
|
||||
query SubFlatComments($sub: String!, $sort: String!, $cursor: String) {
|
||||
sub(name: $sub) {
|
||||
...SubFields
|
||||
}
|
||||
|
||||
moreFlatComments(sub: $sub, sort: $sort, cursor: $cursor) {
|
||||
cursor
|
||||
comments {
|
||||
...CommentFields
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import { Image } from 'react-bootstrap'
|
||||
import LayoutError from '../components/layout-error'
|
||||
import LayoutStatic from '../components/layout-static'
|
||||
import styles from '../styles/404.module.css'
|
||||
|
||||
export default function fourZeroFour () {
|
||||
return (
|
||||
<LayoutError>
|
||||
<LayoutStatic>
|
||||
<Image width='500' height='376' src='/maze.gif' fluid />
|
||||
<h1 className={styles.fourZeroFour}><span>404</span><span className={styles.notFound}>page not found</span></h1>
|
||||
</LayoutError>
|
||||
</LayoutStatic>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import { Image } from 'react-bootstrap'
|
||||
import LayoutError from '../components/layout-error'
|
||||
import LayoutStatic from '../components/layout-static'
|
||||
import styles from '../styles/404.module.css'
|
||||
|
||||
export default function fourZeroFour () {
|
||||
return (
|
||||
<LayoutError>
|
||||
<LayoutStatic>
|
||||
<Image width='500' height='375' src='/falling.gif' fluid />
|
||||
<h1 className={styles.fourZeroFour}><span>500</span><span className={styles.notFound}>server error</span></h1>
|
||||
</LayoutError>
|
||||
</LayoutStatic>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
import LayoutError from '../components/layout-error'
|
||||
import LayoutStatic from '../components/layout-static'
|
||||
import { Image } from 'react-bootstrap'
|
||||
|
||||
export default function Email () {
|
||||
return (
|
||||
<LayoutError>
|
||||
<LayoutStatic>
|
||||
<div className='p-4 text-center'>
|
||||
<h1>Check your email</h1>
|
||||
<h4 className='pb-4'>A sign in link has been sent to your email address</h4>
|
||||
<Image width='500' height='376' src='/hello.gif' fluid />
|
||||
</div>
|
||||
</LayoutError>
|
||||
</LayoutStatic>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { providers, getSession } from 'next-auth/client'
|
||||
import Link from 'next/link'
|
||||
import LayoutCenter from '../components/layout-center'
|
||||
import LayoutStatic from '../components/layout-static'
|
||||
import Login from '../components/login'
|
||||
|
||||
export async function getServerSideProps ({ req, res, query: { callbackUrl, error = null } }) {
|
||||
|
@ -31,11 +31,11 @@ function LoginFooter ({ callbackUrl }) {
|
|||
|
||||
export default function LoginPage (props) {
|
||||
return (
|
||||
<LayoutCenter>
|
||||
<LayoutStatic>
|
||||
<Login
|
||||
Footer={() => <LoginFooter callbackUrl={props.callbackUrl} />}
|
||||
{...props}
|
||||
/>
|
||||
</LayoutCenter>
|
||||
</LayoutStatic>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -9,6 +9,8 @@ import { getGetServerSideProps } from '../api/ssrApollo'
|
|||
import AccordianItem from '../components/accordian-item'
|
||||
import { PollForm } from '../components/poll-form'
|
||||
import { BountyForm } from '../components/bounty-form'
|
||||
import { Form, Select } from '../components/form'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
export const getServerSideProps = getGetServerSideProps()
|
||||
|
||||
|
@ -61,9 +63,44 @@ export function PostForm () {
|
|||
}
|
||||
}
|
||||
|
||||
export function SubSelect ({ children }) {
|
||||
const router = useRouter()
|
||||
const [sub, setSub] = useState(router.query.sub || 'bitcoin')
|
||||
|
||||
useEffect(() => {
|
||||
setSub(router.query.sub || 'bitcoin')
|
||||
}, [router.query?.sub])
|
||||
|
||||
return (
|
||||
<div className='mb-3 d-flex justify-content-start'>
|
||||
<Form
|
||||
className='w-auto'
|
||||
initial={{
|
||||
sub
|
||||
}}
|
||||
>
|
||||
<Select
|
||||
groupClassName='mb-0'
|
||||
onChange={(formik, e) =>
|
||||
// todo move the form values to the other sub's post form
|
||||
router.push({
|
||||
pathname: `/~${e.target.value}/post`,
|
||||
query: router.query?.type ? { type: router.query.type } : undefined
|
||||
})}
|
||||
name='sub'
|
||||
size='sm'
|
||||
items={router.query?.type ? ['bitcoin', 'nostr'] : ['bitcoin', 'nostr', 'jobs']}
|
||||
/>
|
||||
</Form>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function Post () {
|
||||
return (
|
||||
<LayoutCenter>
|
||||
<SubSelect />
|
||||
<PostForm />
|
||||
</LayoutCenter>
|
||||
)
|
||||
|
|
|
@ -11,7 +11,7 @@ export default function Index ({ data: { search: { items, cursor } } }) {
|
|||
const router = useRouter()
|
||||
|
||||
return (
|
||||
<Layout noSeo>
|
||||
<Layout noSeo search>
|
||||
<SeoSearch />
|
||||
{router.query?.q &&
|
||||
<SearchItems
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { providers, getSession } from 'next-auth/client'
|
||||
import Link from 'next/link'
|
||||
import LayoutCenter from '../components/layout-center'
|
||||
import LayoutStatic from '../components/layout-static'
|
||||
import Login from '../components/login'
|
||||
|
||||
export async function getServerSideProps ({ req, res, query: { callbackUrl, error = null } }) {
|
||||
|
@ -42,13 +42,13 @@ function SignUpFooter ({ callbackUrl }) {
|
|||
|
||||
export default function SignUp ({ ...props }) {
|
||||
return (
|
||||
<LayoutCenter>
|
||||
<LayoutStatic>
|
||||
<Login
|
||||
Header={() => <SignUpHeader />}
|
||||
Footer={() => <SignUpFooter callbackUrl={props.callbackUrl} />}
|
||||
text='Sign up'
|
||||
{...props}
|
||||
/>
|
||||
</LayoutCenter>
|
||||
</LayoutStatic>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ export const getServerSideProps = getGetServerSideProps(USER_SEARCH, { limit: 21
|
|||
|
||||
export default function Index ({ data: { searchUsers } }) {
|
||||
return (
|
||||
<Layout noSeo>
|
||||
<Layout noSeo search>
|
||||
<SeoSearch />
|
||||
<UserList users={searchUsers} />
|
||||
</Layout>
|
||||
|
|
|
@ -2,15 +2,84 @@ import { getGetServerSideProps } from '../../../api/ssrApollo'
|
|||
import { SUB } from '../../../fragments/subs'
|
||||
import LayoutCenter from '../../../components/layout-center'
|
||||
import JobForm from '../../../components/job-form'
|
||||
import Link from 'next/link'
|
||||
import { Button } from 'react-bootstrap'
|
||||
import AccordianItem from '../../../components/accordian-item'
|
||||
import { useMe } from '../../../components/me'
|
||||
import { useRouter } from 'next/router'
|
||||
import { DiscussionForm } from '../../../components/discussion-form'
|
||||
import { LinkForm } from '../../../components/link-form'
|
||||
import { PollForm } from '../../../components/poll-form'
|
||||
import { BountyForm } from '../../../components/bounty-form'
|
||||
import { SubSelect } from '../../post'
|
||||
|
||||
export const getServerSideProps = getGetServerSideProps(SUB, null,
|
||||
data => !data.sub)
|
||||
|
||||
// need to recent list items
|
||||
export function PostForm ({ type, sub }) {
|
||||
const me = useMe()
|
||||
|
||||
const prefix = sub?.name ? `/~${sub.name}` : ''
|
||||
|
||||
if (!type) {
|
||||
return (
|
||||
<div className='align-items-center'>
|
||||
{me?.freePosts && me?.sats < 1
|
||||
? <div className='text-center font-weight-bold mb-3 text-success'>{me.freePosts} free posts left</div>
|
||||
: null}
|
||||
<Link href={prefix + '/post?type=link'}>
|
||||
<Button variant='secondary'>link</Button>
|
||||
</Link>
|
||||
<span className='mx-3 font-weight-bold text-muted'>or</span>
|
||||
<Link href={prefix + '/post?type=discussion'}>
|
||||
<Button variant='secondary'>discussion</Button>
|
||||
</Link>
|
||||
<div className='d-flex mt-3'>
|
||||
<AccordianItem
|
||||
headerColor='#6c757d'
|
||||
header={<div className='font-weight-bold text-muted'>more</div>}
|
||||
body={
|
||||
<div className='align-items-center'>
|
||||
<Link href={prefix + '/post?type=poll'}>
|
||||
<Button variant='info'>poll</Button>
|
||||
</Link>
|
||||
<span className='mx-3 font-weight-bold text-muted'>or</span>
|
||||
<Link href={prefix + '/post?type=bounty'}>
|
||||
<Button variant='info'>bounty</Button>
|
||||
</Link>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (type === 'discussion') {
|
||||
return <DiscussionForm sub={sub} />
|
||||
} else if (type === 'link') {
|
||||
return <LinkForm sub={sub} />
|
||||
} else if (type === 'poll') {
|
||||
return <PollForm sub={sub} />
|
||||
} else if (type === 'bounty') {
|
||||
return <BountyForm sub={sub} />
|
||||
} else {
|
||||
return <JobForm sub={sub} />
|
||||
}
|
||||
}
|
||||
|
||||
export default function Post ({ data: { sub } }) {
|
||||
const router = useRouter()
|
||||
let type = router.query.type
|
||||
|
||||
if (sub.postTypes.length === 1) {
|
||||
type = sub.postTypes[0].toLowerCase()
|
||||
}
|
||||
|
||||
return (
|
||||
<LayoutCenter sub={sub.name}>
|
||||
<JobForm sub={sub} />
|
||||
{sub.name !== 'jobs' && <SubSelect />}
|
||||
<PostForm type={type} sub={sub} />
|
||||
</LayoutCenter>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
import { getGetServerSideProps } from '../../../api/ssrApollo'
|
||||
import Items from '../../../components/items'
|
||||
import Layout from '../../../components/layout'
|
||||
import { SUB_ITEMS } from '../../../fragments/subs'
|
||||
|
||||
const variables = { sort: 'recent' }
|
||||
export const getServerSideProps = getGetServerSideProps(SUB_ITEMS, variables,
|
||||
data => !data.sub)
|
||||
|
||||
// need to recent list items
|
||||
export default function Sub ({ data: { sub: { name }, items: { items, cursor } } }) {
|
||||
return (
|
||||
<Layout sub={name}>
|
||||
<Items
|
||||
items={items} cursor={cursor}
|
||||
variables={{ sub: name, ...variables }} rank
|
||||
/>
|
||||
</Layout>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
import Layout from '../../../../components/layout'
|
||||
import Items from '../../../../components/items'
|
||||
import { getGetServerSideProps } from '../../../../api/ssrApollo'
|
||||
import RecentHeader from '../../../../components/recent-header'
|
||||
import { useRouter } from 'next/router'
|
||||
import { SUB_ITEMS } from '../../../../fragments/subs'
|
||||
|
||||
const variables = { sort: 'recent' }
|
||||
export const getServerSideProps = getGetServerSideProps(SUB_ITEMS, variables, data => !data.sub)
|
||||
|
||||
export default function Index ({ data: { sub, items: { items, cursor } } }) {
|
||||
const router = useRouter()
|
||||
return (
|
||||
<Layout sub={sub?.name}>
|
||||
<RecentHeader sub={sub} />
|
||||
<Items
|
||||
items={items} cursor={cursor}
|
||||
variables={{ ...variables, sub: sub?.name, type: router.query.type }} rank
|
||||
/>
|
||||
</Layout>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
import Layout from '../../../../components/layout'
|
||||
import { getGetServerSideProps } from '../../../../api/ssrApollo'
|
||||
import CommentsFlat from '../../../../components/comments-flat'
|
||||
import RecentHeader from '../../../../components/recent-header'
|
||||
import { SUB_FLAT_COMMENTS } from '../../../../fragments/subs'
|
||||
|
||||
const variables = { sort: 'recent' }
|
||||
export const getServerSideProps = getGetServerSideProps(SUB_FLAT_COMMENTS, variables)
|
||||
|
||||
export default function Index ({ data: { sub, moreFlatComments: { comments, cursor } } }) {
|
||||
return (
|
||||
<Layout>
|
||||
<RecentHeader type='comments' sub={sub} />
|
||||
<CommentsFlat
|
||||
comments={comments} cursor={cursor}
|
||||
variables={{ sort: 'recent', sub: sub?.name }} includeParent noReply
|
||||
/>
|
||||
</Layout>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
import { getGetServerSideProps } from '../../../../api/ssrApollo'
|
||||
import Items from '../../../../components/items'
|
||||
import Layout from '../../../../components/layout'
|
||||
import RecentHeader from '../../../../components/recent-header'
|
||||
import { SUB_ITEMS } from '../../../../fragments/subs'
|
||||
|
||||
const variables = { sort: 'recent' }
|
||||
export const getServerSideProps = getGetServerSideProps(SUB_ITEMS, variables,
|
||||
data => !data.sub)
|
||||
|
||||
// need to recent list items
|
||||
export default function Sub ({ data: { sub, items: { items, cursor } } }) {
|
||||
return (
|
||||
<Layout sub={sub?.name}>
|
||||
<RecentHeader type='posts' sub={sub} />
|
||||
<Items
|
||||
items={items} cursor={cursor}
|
||||
variables={{ sub: sub?.name, ...variables }} rank
|
||||
/>
|
||||
</Layout>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,210 @@
|
|||
-- AlterEnum
|
||||
ALTER TYPE "PostType" ADD VALUE 'BOUNTY';
|
||||
|
||||
DROP FUNCTION IF EXISTS create_item(
|
||||
title TEXT, url TEXT, text TEXT, boost INTEGER, bounty INTEGER,
|
||||
parent_id INTEGER, user_id INTEGER, fwd_user_id INTEGER,
|
||||
spam_within INTERVAL);
|
||||
|
||||
CREATE OR REPLACE FUNCTION create_item(
|
||||
sub TEXT, title TEXT, url TEXT, text TEXT, boost INTEGER, bounty INTEGER,
|
||||
parent_id INTEGER, user_id INTEGER, fwd_user_id INTEGER,
|
||||
spam_within INTERVAL)
|
||||
RETURNS "Item"
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
user_msats BIGINT;
|
||||
cost_msats BIGINT;
|
||||
free_posts INTEGER;
|
||||
free_comments INTEGER;
|
||||
freebie BOOLEAN;
|
||||
item "Item";
|
||||
med_votes FLOAT;
|
||||
BEGIN
|
||||
PERFORM ASSERT_SERIALIZED();
|
||||
|
||||
SELECT msats, "freePosts", "freeComments"
|
||||
INTO user_msats, free_posts, free_comments
|
||||
FROM users WHERE id = user_id;
|
||||
|
||||
cost_msats := 1000 * POWER(10, item_spam(parent_id, user_id, spam_within));
|
||||
-- it's only a freebie if it's a 1 sat cost, they have < 1 sat, boost = 0, and they have freebies left
|
||||
freebie := (cost_msats <= 1000) AND (user_msats < 1000) AND (boost = 0) AND ((parent_id IS NULL AND free_posts > 0) OR (parent_id IS NOT NULL AND free_comments > 0));
|
||||
|
||||
IF NOT freebie AND cost_msats > user_msats THEN
|
||||
RAISE EXCEPTION 'SN_INSUFFICIENT_FUNDS';
|
||||
END IF;
|
||||
|
||||
-- get this user's median item score
|
||||
SELECT COALESCE(percentile_cont(0.5) WITHIN GROUP(ORDER BY "weightedVotes" - "weightedDownVotes"), 0) INTO med_votes FROM "Item" WHERE "userId" = user_id;
|
||||
|
||||
-- if their median votes are positive, start at 0
|
||||
-- if the median votes are negative, start their post with that many down votes
|
||||
-- basically: if their median post is bad, presume this post is too
|
||||
IF med_votes >= 0 THEN
|
||||
med_votes := 0;
|
||||
ELSE
|
||||
med_votes := ABS(med_votes);
|
||||
END IF;
|
||||
|
||||
INSERT INTO "Item"
|
||||
("subName", title, url, text, bounty, "userId", "parentId", "fwdUserId",
|
||||
freebie, "weightedDownVotes", created_at, updated_at)
|
||||
VALUES
|
||||
(sub, title, url, text, bounty, user_id, parent_id, fwd_user_id,
|
||||
freebie, med_votes, now_utc(), now_utc()) RETURNING * INTO item;
|
||||
|
||||
IF freebie THEN
|
||||
IF parent_id IS NULL THEN
|
||||
UPDATE users SET "freePosts" = "freePosts" - 1 WHERE id = user_id;
|
||||
ELSE
|
||||
UPDATE users SET "freeComments" = "freeComments" - 1 WHERE id = user_id;
|
||||
END IF;
|
||||
ELSE
|
||||
UPDATE users SET msats = msats - cost_msats WHERE id = user_id;
|
||||
|
||||
INSERT INTO "ItemAct" (msats, "itemId", "userId", act, created_at, updated_at)
|
||||
VALUES (cost_msats, item.id, user_id, 'FEE', now_utc(), now_utc());
|
||||
END IF;
|
||||
|
||||
IF boost > 0 THEN
|
||||
PERFORM item_act(item.id, user_id, 'BOOST', boost);
|
||||
END IF;
|
||||
|
||||
RETURN item;
|
||||
END;
|
||||
$$;
|
||||
|
||||
DROP FUNCTION IF EXISTS update_item(item_id INTEGER,
|
||||
item_title TEXT, item_url TEXT, item_text TEXT, boost INTEGER,item_bounty INTEGER,
|
||||
fwd_user_id INTEGER);
|
||||
|
||||
CREATE OR REPLACE FUNCTION update_item(
|
||||
sub TEXT, item_id INTEGER, item_title TEXT, item_url TEXT, item_text TEXT, boost INTEGER,
|
||||
item_bounty INTEGER, fwd_user_id INTEGER)
|
||||
RETURNS "Item"
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
user_msats INTEGER;
|
||||
item "Item";
|
||||
BEGIN
|
||||
PERFORM ASSERT_SERIALIZED();
|
||||
|
||||
UPDATE "Item"
|
||||
SET "subName" = sub, title = item_title, url = item_url,
|
||||
text = item_text, bounty = item_bounty, "fwdUserId" = fwd_user_id
|
||||
WHERE id = item_id
|
||||
RETURNING * INTO item;
|
||||
|
||||
IF boost > 0 THEN
|
||||
PERFORM item_act(item.id, item."userId", 'BOOST', boost);
|
||||
END IF;
|
||||
|
||||
RETURN item;
|
||||
END;
|
||||
$$;
|
||||
|
||||
DROP FUNCTION IF EXISTS create_poll(
|
||||
title TEXT, text TEXT, poll_cost INTEGER, boost INTEGER, user_id INTEGER,
|
||||
options TEXT[], fwd_user_id INTEGER, spam_within INTERVAL);
|
||||
|
||||
CREATE OR REPLACE FUNCTION create_poll(
|
||||
sub TEXT, title TEXT, text TEXT, poll_cost INTEGER, boost INTEGER, user_id INTEGER,
|
||||
options TEXT[], fwd_user_id INTEGER, spam_within INTERVAL)
|
||||
RETURNS "Item"
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
item "Item";
|
||||
option TEXT;
|
||||
BEGIN
|
||||
PERFORM ASSERT_SERIALIZED();
|
||||
|
||||
item := create_item(sub, title, null, text, boost, null, user_id, fwd_user_id, spam_within);
|
||||
|
||||
UPDATE "Item" set "pollCost" = poll_cost where id = item.id;
|
||||
FOREACH option IN ARRAY options LOOP
|
||||
INSERT INTO "PollOption" (created_at, updated_at, "itemId", "option") values (now_utc(), now_utc(), item.id, option);
|
||||
END LOOP;
|
||||
|
||||
RETURN item;
|
||||
END;
|
||||
$$;
|
||||
|
||||
DROP FUNCTION IF EXISTS update_poll(
|
||||
id INTEGER, title TEXT, text TEXT, boost INTEGER,
|
||||
options TEXT[], fwd_user_id INTEGER, has_img_link BOOLEAN);
|
||||
|
||||
CREATE OR REPLACE FUNCTION update_poll(
|
||||
sub TEXT, id INTEGER, title TEXT, text TEXT, boost INTEGER,
|
||||
options TEXT[], fwd_user_id INTEGER)
|
||||
RETURNS "Item"
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
item "Item";
|
||||
option TEXT;
|
||||
BEGIN
|
||||
PERFORM ASSERT_SERIALIZED();
|
||||
|
||||
item := update_item(sub, id, title, null, text, boost, fwd_user_id);
|
||||
|
||||
FOREACH option IN ARRAY options LOOP
|
||||
INSERT INTO "PollOption" (created_at, updated_at, "itemId", "option") values (now_utc(), now_utc(), item.id, option);
|
||||
END LOOP;
|
||||
|
||||
RETURN item;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION update_job(item_id INTEGER,
|
||||
item_title TEXT, item_url TEXT, item_text TEXT, job_bid INTEGER, job_company TEXT,
|
||||
job_location TEXT, job_remote BOOLEAN, job_upload_id INTEGER, job_status "Status")
|
||||
RETURNS "Item"
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
user_msats INTEGER;
|
||||
item "Item";
|
||||
BEGIN
|
||||
PERFORM ASSERT_SERIALIZED();
|
||||
-- update item
|
||||
SELECT * INTO item FROM update_item('jobs', item_id, item_title, item_url, item_text, 0, 0, NULL);
|
||||
|
||||
IF item.status <> job_status THEN
|
||||
UPDATE "Item"
|
||||
SET "maxBid" = job_bid, company = job_company, location = job_location, remote = job_remote, "uploadId" = job_upload_id, status = job_status, "statusUpdatedAt" = now_utc()
|
||||
WHERE id = item.id RETURNING * INTO item;
|
||||
ELSE
|
||||
UPDATE "Item"
|
||||
SET "maxBid" = job_bid, company = job_company, location = job_location, remote = job_remote, "uploadId" = job_upload_id
|
||||
WHERE id = item.id RETURNING * INTO item;
|
||||
END IF;
|
||||
|
||||
-- run_auction
|
||||
EXECUTE run_auction(item.id);
|
||||
|
||||
RETURN item;
|
||||
END;
|
||||
$$;
|
||||
|
||||
-- when creating bio, set bio flag so they won't appear on first page
|
||||
CREATE OR REPLACE FUNCTION create_bio(title TEXT, text TEXT, user_id INTEGER)
|
||||
RETURNS "Item"
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
item "Item";
|
||||
BEGIN
|
||||
PERFORM ASSERT_SERIALIZED();
|
||||
|
||||
SELECT * INTO item FROM create_item(NULL, title, NULL, text, 0, 0, NULL, user_id, NULL, '0');
|
||||
|
||||
UPDATE "Item" SET bio = true WHERE id = item.id;
|
||||
UPDATE users SET "bioId" = item.id WHERE id = user_id;
|
||||
|
||||
RETURN item;
|
||||
END;
|
||||
$$;
|
|
@ -0,0 +1,102 @@
|
|||
DROP FUNCTION IF EXISTS create_poll(
|
||||
title TEXT, text TEXT, poll_cost INTEGER, boost INTEGER, user_id INTEGER,
|
||||
options TEXT[], fwd_user_id INTEGER, spam_within INTERVAL);
|
||||
|
||||
CREATE OR REPLACE FUNCTION create_poll(
|
||||
sub TEXT, title TEXT, text TEXT, poll_cost INTEGER, boost INTEGER, user_id INTEGER,
|
||||
options TEXT[], fwd_user_id INTEGER, spam_within INTERVAL)
|
||||
RETURNS "Item"
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
item "Item";
|
||||
option TEXT;
|
||||
BEGIN
|
||||
PERFORM ASSERT_SERIALIZED();
|
||||
|
||||
item := create_item(sub, title, null, text, boost, null, null, user_id, fwd_user_id, spam_within);
|
||||
|
||||
UPDATE "Item" set "pollCost" = poll_cost where id = item.id;
|
||||
FOREACH option IN ARRAY options LOOP
|
||||
INSERT INTO "PollOption" (created_at, updated_at, "itemId", "option") values (now_utc(), now_utc(), item.id, option);
|
||||
END LOOP;
|
||||
|
||||
RETURN item;
|
||||
END;
|
||||
$$;
|
||||
|
||||
DROP FUNCTION IF EXISTS update_poll(
|
||||
id INTEGER, title TEXT, text TEXT, boost INTEGER,
|
||||
options TEXT[], fwd_user_id INTEGER, has_img_link BOOLEAN);
|
||||
|
||||
CREATE OR REPLACE FUNCTION update_poll(
|
||||
sub TEXT, id INTEGER, title TEXT, text TEXT, boost INTEGER,
|
||||
options TEXT[], fwd_user_id INTEGER)
|
||||
RETURNS "Item"
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
item "Item";
|
||||
option TEXT;
|
||||
BEGIN
|
||||
PERFORM ASSERT_SERIALIZED();
|
||||
|
||||
item := update_item(sub, id, title, null, text, boost, null, fwd_user_id);
|
||||
|
||||
FOREACH option IN ARRAY options LOOP
|
||||
INSERT INTO "PollOption" (created_at, updated_at, "itemId", "option") values (now_utc(), now_utc(), item.id, option);
|
||||
END LOOP;
|
||||
|
||||
RETURN item;
|
||||
END;
|
||||
$$;
|
||||
|
||||
-- when creating bio, set bio flag so they won't appear on first page
|
||||
CREATE OR REPLACE FUNCTION create_bio(title TEXT, text TEXT, user_id INTEGER)
|
||||
RETURNS "Item"
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
item "Item";
|
||||
BEGIN
|
||||
PERFORM ASSERT_SERIALIZED();
|
||||
|
||||
SELECT * INTO item FROM create_item(NULL, title, NULL, text, 0, NULL, NULL, user_id, NULL, '0');
|
||||
|
||||
UPDATE "Item" SET bio = true WHERE id = item.id;
|
||||
UPDATE users SET "bioId" = item.id WHERE id = user_id;
|
||||
|
||||
RETURN item;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION update_job(item_id INTEGER,
|
||||
item_title TEXT, item_url TEXT, item_text TEXT, job_bid INTEGER, job_company TEXT,
|
||||
job_location TEXT, job_remote BOOLEAN, job_upload_id INTEGER, job_status "Status")
|
||||
RETURNS "Item"
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
user_msats INTEGER;
|
||||
item "Item";
|
||||
BEGIN
|
||||
PERFORM ASSERT_SERIALIZED();
|
||||
-- update item
|
||||
SELECT * INTO item FROM update_item('jobs', item_id, item_title, item_url, item_text, 0, NULL, NULL);
|
||||
|
||||
IF item.status <> job_status THEN
|
||||
UPDATE "Item"
|
||||
SET "maxBid" = job_bid, company = job_company, location = job_location, remote = job_remote, "uploadId" = job_upload_id, status = job_status, "statusUpdatedAt" = now_utc()
|
||||
WHERE id = item.id RETURNING * INTO item;
|
||||
ELSE
|
||||
UPDATE "Item"
|
||||
SET "maxBid" = job_bid, company = job_company, location = job_location, remote = job_remote, "uploadId" = job_upload_id
|
||||
WHERE id = item.id RETURNING * INTO item;
|
||||
END IF;
|
||||
|
||||
-- run_auction
|
||||
EXECUTE run_auction(item.id);
|
||||
|
||||
RETURN item;
|
||||
END;
|
||||
$$;
|
|
@ -0,0 +1,40 @@
|
|||
-- AlterTable
|
||||
ALTER TABLE "users" ADD COLUMN "subs" TEXT[];
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Subscription" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"subName" CITEXT NOT NULL,
|
||||
"userId" INTEGER NOT NULL,
|
||||
|
||||
PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Subscription" ADD FOREIGN KEY ("subName") REFERENCES "Sub"("name") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Subscription" ADD FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- create the nostr sub ignoring conflicts
|
||||
INSERT INTO "Sub" ("name", "desc", "postTypes", "rankingType")
|
||||
VALUES ('nostr', 'everything nostr related', '{LINK,DISCUSSION,POLL,BOUNTY}', 'WOT') ON CONFLICT DO NOTHING;
|
||||
|
||||
-- create bitcoin sub ignoring conflicts
|
||||
INSERT INTO "Sub" ("name", "desc", "postTypes", "rankingType")
|
||||
VALUES ('bitcoin', 'everything bitcoin related', '{LINK,DISCUSSION,POLL,BOUNTY}', 'WOT') ON CONFLICT DO NOTHING;
|
||||
|
||||
-- all root items with null subName put in bitcoin sub ... unless title has nostr in it
|
||||
UPDATE "Item"
|
||||
SET "subName" = 'bitcoin'
|
||||
WHERE "Item"."subName" IS NULL
|
||||
AND "Item"."parentId" IS NULL
|
||||
AND "Item".title NOT ILIKE '%nostr%';
|
||||
|
||||
UPDATE "Item"
|
||||
SET "subName" = 'nostr'
|
||||
WHERE "Item"."subName" IS NULL
|
||||
AND "Item"."parentId" IS NULL
|
||||
AND "Item".title ILIKE '%nostr%';
|
|
@ -44,6 +44,7 @@ model User {
|
|||
lastCheckedJobs DateTime?
|
||||
photoId Int?
|
||||
photo Upload? @relation(fields: [photoId], references: [id])
|
||||
subs String[]
|
||||
|
||||
// streak
|
||||
streak Int?
|
||||
|
@ -90,6 +91,7 @@ model User {
|
|||
ReferralAct ReferralAct[]
|
||||
Streak Streak[]
|
||||
Bookmarks Bookmark[]
|
||||
Subscriptions Subscription[]
|
||||
|
||||
@@index([createdAt])
|
||||
@@index([inviteId])
|
||||
|
@ -350,6 +352,7 @@ enum PostType {
|
|||
DISCUSSION
|
||||
JOB
|
||||
POLL
|
||||
BOUNTY
|
||||
}
|
||||
|
||||
enum RankingType {
|
||||
|
@ -368,6 +371,17 @@ model Sub {
|
|||
desc String?
|
||||
|
||||
Item Item[]
|
||||
Subscription Subscription[]
|
||||
}
|
||||
|
||||
model Subscription {
|
||||
id Int @id @default(autoincrement())
|
||||
createdAt DateTime @default(now()) @map(name: "created_at")
|
||||
updatedAt DateTime @default(now()) @updatedAt @map(name: "updated_at")
|
||||
subName String @db.Citext
|
||||
sub Sub @relation(fields: [subName], references: [name])
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
userId Int
|
||||
}
|
||||
|
||||
// the active pin is the latest one when it's a recurring cron
|
||||
|
|
|
@ -145,7 +145,7 @@ body {
|
|||
background: var(--theme-body);
|
||||
color: var(--theme-color);
|
||||
min-height: 100vh;
|
||||
height: 100%;
|
||||
min-height: 100svh;
|
||||
}
|
||||
|
||||
a {
|
||||
|
@ -172,6 +172,24 @@ div[contenteditable],
|
|||
border-color: var(--theme-borderColor);
|
||||
}
|
||||
|
||||
|
||||
select.custom-select {
|
||||
background-color: var(--theme-clickToContextColor);
|
||||
color: var(--theme-dropdownItemColor);
|
||||
font-weight: bold;
|
||||
border: none;
|
||||
background-image: url("data:image/svg+xml, %3Csvg fill='%23808080' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 15.0006L7.75732 10.758L9.17154 9.34375L12 12.1722L14.8284 9.34375L16.2426 10.758L12 15.0006Z'%3E%3C/path%3E%3C/svg%3E%0A");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right .25rem center;
|
||||
background-size: 1.5rem;
|
||||
}
|
||||
|
||||
select.custom-select:focus {
|
||||
border-color: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
|
||||
div[contenteditable]:focus,
|
||||
.form-control:focus {
|
||||
background-color: var(--theme-inputBg);
|
||||
|
@ -194,6 +212,11 @@ div[contenteditable]:disabled,
|
|||
cursor: pointer;
|
||||
}
|
||||
|
||||
.py-half {
|
||||
padding-top: .125rem;
|
||||
padding-bottom: .125rem;
|
||||
}
|
||||
|
||||
.clickToContext:hover {
|
||||
background-color: var(--theme-clickToContextColor);
|
||||
}
|
||||
|
@ -273,8 +296,8 @@ div[contenteditable]:disabled,
|
|||
#__next {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
min-height: 100vh;
|
||||
min-height: 100svh;
|
||||
}
|
||||
|
||||
footer {
|
||||
|
|
Loading…
Reference in New Issue