Merge branch 'master' into 266-zaps-without-account
This commit is contained in:
commit
3d0bb4b32c
|
@ -0,0 +1,6 @@
|
||||||
|
Resources:
|
||||||
|
AWSEBAutoScalingGroup:
|
||||||
|
Type: "AWS::AutoScaling::AutoScalingGroup"
|
||||||
|
Properties:
|
||||||
|
HealthCheckType: ELB
|
||||||
|
HealthCheckGracePeriod: 300
|
|
@ -4,4 +4,4 @@ echo primsa migrate
|
||||||
npm run migrate
|
npm run migrate
|
||||||
|
|
||||||
echo build with npm
|
echo build with npm
|
||||||
npm run build
|
sudo -E -u webapp npm run build
|
|
@ -4,4 +4,4 @@ echo primsa migrate
|
||||||
npm run migrate
|
npm run migrate
|
||||||
|
|
||||||
echo build with npm
|
echo build with npm
|
||||||
npm run build
|
sudo -E -u webapp npm run build
|
|
@ -28,9 +28,9 @@ http {
|
||||||
listen 8008 default_server;
|
listen 8008 default_server;
|
||||||
access_log /var/log/nginx/access.log main;
|
access_log /var/log/nginx/access.log main;
|
||||||
|
|
||||||
client_header_timeout 60;
|
client_header_timeout 90;
|
||||||
client_body_timeout 60;
|
client_body_timeout 90;
|
||||||
keepalive_timeout 60;
|
keepalive_timeout 90;
|
||||||
gzip on;
|
gzip on;
|
||||||
gzip_comp_level 4;
|
gzip_comp_level 4;
|
||||||
gzip_types text/plain text/css application/json application/javascript application/x-javascript text/xml application/xml application/xml+rss text/javascript;
|
gzip_types text/plain text/css application/json application/javascript application/x-javascript text/xml application/xml application/xml+rss text/javascript;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
# syntax=docker/dockerfile:1
|
# syntax=docker/dockerfile:1
|
||||||
|
|
||||||
FROM node:16.16.0-bullseye
|
FROM node:18.17.0-bullseye
|
||||||
|
|
||||||
ENV NODE_ENV=development
|
ENV NODE_ENV=development
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,8 @@ export default {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (!lastReward) return { total: 0, sources: [] }
|
||||||
|
|
||||||
const [result] = await models.$queryRaw`
|
const [result] = await models.$queryRaw`
|
||||||
SELECT coalesce(FLOOR(sum(sats)), 0) as total, json_build_array(
|
SELECT coalesce(FLOOR(sum(sats)), 0) as total, json_build_array(
|
||||||
json_build_object('name', 'donations', 'value', coalesce(FLOOR(sum(sats) FILTER(WHERE type = 'DONATION')), 0)),
|
json_build_object('name', 'donations', 'value', coalesce(FLOOR(sum(sats) FILTER(WHERE type = 'DONATION')), 0)),
|
||||||
|
|
|
@ -35,17 +35,17 @@ async function serialize (models, ...calls) {
|
||||||
if (error.message.includes('SN_REVOKED_OR_EXHAUSTED')) {
|
if (error.message.includes('SN_REVOKED_OR_EXHAUSTED')) {
|
||||||
bail(new Error('faucet has been revoked or is exhausted'))
|
bail(new Error('faucet has been revoked or is exhausted'))
|
||||||
}
|
}
|
||||||
if (error.message.includes('23514')) {
|
|
||||||
bail(new Error('constraint failure'))
|
|
||||||
}
|
|
||||||
if (error.message.includes('SN_INV_PENDING_LIMIT')) {
|
if (error.message.includes('SN_INV_PENDING_LIMIT')) {
|
||||||
bail(new Error('too many pending invoices'))
|
bail(new Error('too many pending invoices'))
|
||||||
}
|
}
|
||||||
if (error.message.includes('SN_INV_EXCEED_BALANCE')) {
|
if (error.message.includes('SN_INV_EXCEED_BALANCE')) {
|
||||||
bail(new Error('pending invoices must not cause balance to exceed 1m sats'))
|
bail(new Error('pending invoices must not cause balance to exceed 1m sats'))
|
||||||
}
|
}
|
||||||
if (error.message.includes('40001')) {
|
if (error.message.includes('40001') || error.code === 'P2034') {
|
||||||
throw new Error('wallet balance serialization failure - retry again')
|
throw new Error('wallet balance serialization failure - try again')
|
||||||
|
}
|
||||||
|
if (error.message.includes('23514') || ['P2002', 'P2003', 'P2004'].includes(error.code)) {
|
||||||
|
bail(new Error('constraint failure'))
|
||||||
}
|
}
|
||||||
bail(error)
|
bail(error)
|
||||||
}
|
}
|
||||||
|
|
|
@ -172,7 +172,7 @@ export default {
|
||||||
}
|
}
|
||||||
switch (f.type) {
|
switch (f.type) {
|
||||||
case 'withdrawal':
|
case 'withdrawal':
|
||||||
f.msats = (-1 * f.msats) - f.msatsFee
|
f.msats = (-1 * Number(f.msats)) - Number(f.msatsFee)
|
||||||
break
|
break
|
||||||
case 'spent':
|
case 'spent':
|
||||||
f.msats *= -1
|
f.msats *= -1
|
||||||
|
|
|
@ -38,14 +38,14 @@ export default async function getSSRApolloClient ({ req, res, me = null }) {
|
||||||
assumeImmutableResults: true,
|
assumeImmutableResults: true,
|
||||||
defaultOptions: {
|
defaultOptions: {
|
||||||
watchQuery: {
|
watchQuery: {
|
||||||
fetchPolicy: 'cache-only',
|
fetchPolicy: 'no-cache',
|
||||||
nextFetchPolicy: 'cache-only',
|
nextFetchPolicy: 'no-cache',
|
||||||
canonizeResults: true,
|
canonizeResults: true,
|
||||||
ssr: true
|
ssr: true
|
||||||
},
|
},
|
||||||
query: {
|
query: {
|
||||||
fetchPolicy: 'cache-first',
|
fetchPolicy: 'no-cache',
|
||||||
nextFetchPolicy: 'cache-only',
|
nextFetchPolicy: 'no-cache',
|
||||||
canonizeResults: true,
|
canonizeResults: true,
|
||||||
ssr: true
|
ssr: true
|
||||||
}
|
}
|
||||||
|
|
|
@ -89,7 +89,7 @@ export function WhenAreaChart ({ data }) {
|
||||||
tick={{ fill: 'var(--theme-grey)' }}
|
tick={{ fill: 'var(--theme-grey)' }}
|
||||||
/>
|
/>
|
||||||
<YAxis tickFormatter={abbrNum} tick={{ fill: 'var(--theme-grey)' }} />
|
<YAxis tickFormatter={abbrNum} tick={{ fill: 'var(--theme-grey)' }} />
|
||||||
<Tooltip labelFormatter={dateFormatter(when)} contentStyle={{ color: 'var(--theme-color)', backgroundColor: 'var(--theme-body)' }} />
|
<Tooltip labelFormatter={dateFormatter(when)} contentStyle={{ color: 'var(--bs-body-color)', backgroundColor: 'var(--bs-body-bg)' }} />
|
||||||
<Legend />
|
<Legend />
|
||||||
{Object.keys(data[0]).filter(v => v !== 'time' && v !== '__typename').map((v, i) =>
|
{Object.keys(data[0]).filter(v => v !== 'time' && v !== '__typename').map((v, i) =>
|
||||||
<Area key={v} type='monotone' dataKey={v} name={v} stackId='1' stroke={COLORS[i]} fill={COLORS[i]} />)}
|
<Area key={v} type='monotone' dataKey={v} name={v} stackId='1' stroke={COLORS[i]} fill={COLORS[i]} />)}
|
||||||
|
@ -124,7 +124,7 @@ export function WhenLineChart ({ data }) {
|
||||||
tick={{ fill: 'var(--theme-grey)' }}
|
tick={{ fill: 'var(--theme-grey)' }}
|
||||||
/>
|
/>
|
||||||
<YAxis tickFormatter={abbrNum} tick={{ fill: 'var(--theme-grey)' }} />
|
<YAxis tickFormatter={abbrNum} tick={{ fill: 'var(--theme-grey)' }} />
|
||||||
<Tooltip labelFormatter={dateFormatter(when)} contentStyle={{ color: 'var(--theme-color)', backgroundColor: 'var(--theme-body)' }} />
|
<Tooltip labelFormatter={dateFormatter(when)} contentStyle={{ color: 'var(--bs-body-color)', backgroundColor: 'var(--bs-body-bg)' }} />
|
||||||
<Legend />
|
<Legend />
|
||||||
{Object.keys(data[0]).filter(v => v !== 'time' && v !== '__typename').map((v, i) =>
|
{Object.keys(data[0]).filter(v => v !== 'time' && v !== '__typename').map((v, i) =>
|
||||||
<Line key={v} type='monotone' dataKey={v} name={v} stroke={COLORS[i]} fill={COLORS[i]} />)}
|
<Line key={v} type='monotone' dataKey={v} name={v} stroke={COLORS[i]} fill={COLORS[i]} />)}
|
||||||
|
@ -160,7 +160,7 @@ export function WhenComposedChart ({ data, lineNames, areaNames, barNames }) {
|
||||||
/>
|
/>
|
||||||
<YAxis yAxisId='left' orientation='left' allowDecimals={false} stroke='var(--theme-grey)' tickFormatter={abbrNum} tick={{ fill: 'var(--theme-grey)' }} />
|
<YAxis yAxisId='left' orientation='left' allowDecimals={false} stroke='var(--theme-grey)' tickFormatter={abbrNum} tick={{ fill: 'var(--theme-grey)' }} />
|
||||||
<YAxis yAxisId='right' orientation='right' allowDecimals={false} stroke='var(--theme-grey)' tickFormatter={abbrNum} tick={{ fill: 'var(--theme-grey)' }} />
|
<YAxis yAxisId='right' orientation='right' allowDecimals={false} stroke='var(--theme-grey)' tickFormatter={abbrNum} tick={{ fill: 'var(--theme-grey)' }} />
|
||||||
<Tooltip labelFormatter={dateFormatter(when)} contentStyle={{ color: 'var(--theme-color)', backgroundColor: 'var(--theme-body)' }} />
|
<Tooltip labelFormatter={dateFormatter(when)} contentStyle={{ color: 'var(--bs-body-color)', backgroundColor: 'var(--bs-body-bg)' }} />
|
||||||
<Legend />
|
<Legend />
|
||||||
{barNames?.map((v, i) =>
|
{barNames?.map((v, i) =>
|
||||||
<Bar yAxisId='right' key={v} type='monotone' dataKey={v} name={v} stroke='var(--bs-info)' fill='var(--bs-info)' />)}
|
<Bar yAxisId='right' key={v} type='monotone' dataKey={v} name={v} stroke='var(--bs-info)' fill='var(--bs-info)' />)}
|
||||||
|
|
|
@ -114,15 +114,19 @@ export default function Comment ({
|
||||||
if (Number(router.query.commentId) === Number(item.id)) {
|
if (Number(router.query.commentId) === Number(item.id)) {
|
||||||
// HACK wait for other comments to collapse if they're collapsed
|
// HACK wait for other comments to collapse if they're collapsed
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
ref.current.scrollIntoView()
|
ref.current.scrollIntoView({ behavior: 'instant', block: 'start' })
|
||||||
ref.current.classList.add('flash-it')
|
ref.current.classList.add('outline-it')
|
||||||
router.replace({
|
|
||||||
pathname: router.pathname,
|
|
||||||
query: { id: router.query.id }
|
|
||||||
}, undefined, { scroll: false })
|
|
||||||
}, 20)
|
}, 20)
|
||||||
}
|
}
|
||||||
}, [item])
|
}, [item.id, router.query.commentId])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (router.query.commentsViewedAt &&
|
||||||
|
me?.id !== item.user?.id &&
|
||||||
|
new Date(item.createdAt).getTime() > router.query.commentsViewedAt) {
|
||||||
|
ref.current.classList.add('outline-new-comment')
|
||||||
|
}
|
||||||
|
}, [item.id])
|
||||||
|
|
||||||
const bottomedOut = depth === COMMENT_DEPTH_LIMIT
|
const bottomedOut = depth === COMMENT_DEPTH_LIMIT
|
||||||
const op = root.user.name === item.user.name
|
const op = root.user.name === item.user.name
|
||||||
|
@ -131,6 +135,8 @@ export default function Comment ({
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={ref} className={includeParent ? '' : `${styles.comment} ${collapse === 'yep' ? styles.collapsed : ''}`}
|
ref={ref} className={includeParent ? '' : `${styles.comment} ${collapse === 'yep' ? styles.collapsed : ''}`}
|
||||||
|
onMouseEnter={() => ref.current.classList.add('outline-new-comment-unset')}
|
||||||
|
onTouchStart={() => ref.current.classList.add('outline-new-comment-unset')}
|
||||||
>
|
>
|
||||||
<div className={`${itemStyles.item} ${styles.item}`}>
|
<div className={`${itemStyles.item} ${styles.item}`}>
|
||||||
{item.meDontLike
|
{item.meDontLike
|
||||||
|
|
|
@ -73,7 +73,7 @@ export default function Comments ({ parentId, pinned, bio, parentCreatedAt, comm
|
||||||
router.push({
|
router.push({
|
||||||
pathname: router.pathname,
|
pathname: router.pathname,
|
||||||
query: { ...router.query, sort }
|
query: { ...router.query, sort }
|
||||||
}, router.asPath, { scroll: false })
|
}, undefined, { scroll: false })
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
: null}
|
: null}
|
||||||
|
|
|
@ -1,5 +1,54 @@
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { getTheme, listenForThemeChange, setTheme } from '../public/dark'
|
|
||||||
|
const handleThemeChange = (dark) => {
|
||||||
|
const root = window.document.documentElement
|
||||||
|
root.setAttribute('data-bs-theme', dark ? 'dark' : 'light')
|
||||||
|
}
|
||||||
|
|
||||||
|
const STORAGE_KEY = 'darkMode'
|
||||||
|
const PREFER_DARK_QUERY = '(prefers-color-scheme: dark)'
|
||||||
|
|
||||||
|
const getTheme = () => {
|
||||||
|
const mql = window.matchMedia(PREFER_DARK_QUERY)
|
||||||
|
const supportsColorSchemeQuery = mql.media === PREFER_DARK_QUERY
|
||||||
|
let localStorageTheme = null
|
||||||
|
try {
|
||||||
|
localStorageTheme = window.localStorage.getItem(STORAGE_KEY)
|
||||||
|
} catch (err) {}
|
||||||
|
const localStorageExists = localStorageTheme !== null
|
||||||
|
if (localStorageExists) {
|
||||||
|
localStorageTheme = JSON.parse(localStorageTheme)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (localStorageExists) {
|
||||||
|
return { user: true, dark: localStorageTheme }
|
||||||
|
} else if (supportsColorSchemeQuery) {
|
||||||
|
return { user: false, dark: mql.matches }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const setTheme = (dark) => {
|
||||||
|
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(dark))
|
||||||
|
handleThemeChange(dark)
|
||||||
|
}
|
||||||
|
|
||||||
|
const listenForThemeChange = (onChange) => {
|
||||||
|
const mql = window.matchMedia(PREFER_DARK_QUERY)
|
||||||
|
mql.onchange = mql => {
|
||||||
|
const { user, dark } = getTheme()
|
||||||
|
if (!user) {
|
||||||
|
handleThemeChange(dark)
|
||||||
|
onChange({ user, dark })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.onstorage = e => {
|
||||||
|
if (e.key === STORAGE_KEY) {
|
||||||
|
const dark = JSON.parse(e.newValue)
|
||||||
|
setTheme(dark)
|
||||||
|
onChange({ user: true, dark })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default function useDarkMode () {
|
export default function useDarkMode () {
|
||||||
const [dark, setDark] = useState()
|
const [dark, setDark] = useState()
|
||||||
|
|
|
@ -5,7 +5,7 @@ import styles from './fee-button.module.css'
|
||||||
import { gql, useQuery } from '@apollo/client'
|
import { gql, useQuery } from '@apollo/client'
|
||||||
import { useFormikContext } from 'formik'
|
import { useFormikContext } from 'formik'
|
||||||
import { useMe } from './me'
|
import { useMe } from './me'
|
||||||
import { ANON_COMMENT_FEE, ANON_POST_FEE } from '../lib/constants'
|
import { SSR, ANON_COMMENT_FEE, ANON_POST_FEE } from '../lib/constants'
|
||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
|
|
||||||
function Receipt ({ cost, repetition, hasImgLink, baseFee, parentId, boost }) {
|
function Receipt ({ cost, repetition, hasImgLink, baseFee, parentId, boost }) {
|
||||||
|
@ -48,7 +48,7 @@ export default function FeeButton ({ parentId, hasImgLink, baseFee, ChildButton,
|
||||||
const query = parentId
|
const query = parentId
|
||||||
? gql`{ itemRepetition(parentId: "${parentId}") }`
|
? gql`{ itemRepetition(parentId: "${parentId}") }`
|
||||||
: gql`{ itemRepetition }`
|
: gql`{ itemRepetition }`
|
||||||
const { data } = useQuery(query, { pollInterval: 1000, nextFetchPolicy: 'cache-and-network' })
|
const { data } = useQuery(query, SSR ? {} : { pollInterval: 1000, nextFetchPolicy: 'cache-and-network' })
|
||||||
const repetition = me ? data?.itemRepetition || 0 : 0
|
const repetition = me ? data?.itemRepetition || 0 : 0
|
||||||
const formik = useFormikContext()
|
const formik = useFormikContext()
|
||||||
const boost = Number(formik?.values?.boost) || 0
|
const boost = Number(formik?.values?.boost) || 0
|
||||||
|
|
|
@ -8,6 +8,8 @@
|
||||||
|
|
||||||
.receipt td {
|
.receipt td {
|
||||||
padding: .25rem .1rem;
|
padding: .25rem .1rem;
|
||||||
|
background-color: var(--theme-inputBg);
|
||||||
|
color: var(--bs-body-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.receipt tfoot {
|
.receipt tfoot {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { gql, useQuery } from '@apollo/client'
|
import { gql, useQuery } from '@apollo/client'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { RewardLine } from '../pages/rewards'
|
import { RewardLine } from '../pages/rewards'
|
||||||
|
import { SSR } from '../lib/constants'
|
||||||
|
|
||||||
const REWARDS = gql`
|
const REWARDS = gql`
|
||||||
{
|
{
|
||||||
|
@ -10,7 +11,7 @@ const REWARDS = gql`
|
||||||
}`
|
}`
|
||||||
|
|
||||||
export default function Rewards () {
|
export default function Rewards () {
|
||||||
const { data } = useQuery(REWARDS, { pollInterval: 60000, nextFetchPolicy: 'cache-and-network' })
|
const { data } = useQuery(REWARDS, SSR ? { ssr: false } : { pollInterval: 60000, nextFetchPolicy: 'cache-and-network' })
|
||||||
const total = data?.expectedRewards?.total
|
const total = data?.expectedRewards?.total
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -11,13 +11,13 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.contrastLink {
|
.contrastLink {
|
||||||
color: var(--theme-color);
|
color: var(--bs-body-color);
|
||||||
}
|
}
|
||||||
.contrastLink:hover {
|
.contrastLink:hover {
|
||||||
color: var(--theme-color);
|
color: var(--bs-body-color);
|
||||||
}
|
}
|
||||||
.contrastLink svg {
|
.contrastLink svg {
|
||||||
fill: var(--theme-color);
|
fill: var(--bs-body-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.version {
|
.version {
|
||||||
|
|
|
@ -72,7 +72,7 @@ export function InputSkeleton ({ label, hint }) {
|
||||||
return (
|
return (
|
||||||
<BootstrapForm.Group>
|
<BootstrapForm.Group>
|
||||||
{label && <BootstrapForm.Label>{label}</BootstrapForm.Label>}
|
{label && <BootstrapForm.Label>{label}</BootstrapForm.Label>}
|
||||||
<div className='form-control clouds' />
|
<div className='form-control clouds' style={{ color: 'transparent' }}>.</div>
|
||||||
{hint &&
|
{hint &&
|
||||||
<BootstrapForm.Text>
|
<BootstrapForm.Text>
|
||||||
{hint}
|
{hint}
|
||||||
|
@ -431,7 +431,7 @@ export function Checkbox ({ children, label, groupClassName, hiddenLabel, extra,
|
||||||
handleChange && handleChange(e.target.checked, helpers.setValue)
|
handleChange && handleChange(e.target.checked, helpers.setValue)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<BootstrapForm.Check.Label className={'d-flex' + (disabled ? ' text-muted' : '')}>
|
<BootstrapForm.Check.Label className={'d-inline-flex flex-nowrap align-items-center' + (disabled ? ' text-muted' : '')}>
|
||||||
<div className='flex-grow-1'>{label}</div>
|
<div className='flex-grow-1'>{label}</div>
|
||||||
{extra &&
|
{extra &&
|
||||||
<div className={styles.checkboxExtra}>
|
<div className={styles.checkboxExtra}>
|
||||||
|
|
|
@ -10,7 +10,7 @@ import Price from './price'
|
||||||
import { useMe } from './me'
|
import { useMe } from './me'
|
||||||
import Head from 'next/head'
|
import Head from 'next/head'
|
||||||
import { signOut } from 'next-auth/react'
|
import { signOut } from 'next-auth/react'
|
||||||
import { useCallback, useEffect, useState } from 'react'
|
import { useCallback, useEffect } from 'react'
|
||||||
import { randInRange } from '../lib/rand'
|
import { randInRange } from '../lib/rand'
|
||||||
import { abbrNum } from '../lib/format'
|
import { abbrNum } from '../lib/format'
|
||||||
import NoteIcon from '../svgs/notification-4-fill.svg'
|
import NoteIcon from '../svgs/notification-4-fill.svg'
|
||||||
|
@ -20,7 +20,7 @@ import CowboyHat from './cowboy-hat'
|
||||||
import { Select } from './form'
|
import { Select } from './form'
|
||||||
import SearchIcon from '../svgs/search-line.svg'
|
import SearchIcon from '../svgs/search-line.svg'
|
||||||
import BackArrow from '../svgs/arrow-left-line.svg'
|
import BackArrow from '../svgs/arrow-left-line.svg'
|
||||||
import { SUBS } from '../lib/constants'
|
import { SSR, SUBS } from '../lib/constants'
|
||||||
import { useLightning } from './lightning'
|
import { useLightning } from './lightning'
|
||||||
import { HAS_NOTIFICATIONS } from '../fragments/notifications'
|
import { HAS_NOTIFICATIONS } from '../fragments/notifications'
|
||||||
|
|
||||||
|
@ -31,21 +31,25 @@ function WalletSummary ({ me }) {
|
||||||
|
|
||||||
function Back () {
|
function Back () {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const [show, setShow] = useState()
|
|
||||||
|
|
||||||
useEffect(() => {
|
return router.asPath !== '/' &&
|
||||||
setShow(typeof window !== 'undefined' && router.asPath !== '/' &&
|
<a
|
||||||
(typeof window.navigation === 'undefined' || window.navigation.canGoBack === undefined || window?.navigation.canGoBack))
|
role='button' tabIndex='0' className='nav-link standalone p-0' onClick={() => {
|
||||||
}, [router.asPath])
|
if (typeof window.navigation === 'undefined' || window.navigation.canGoBack === undefined || window?.navigation.canGoBack) {
|
||||||
|
router.back()
|
||||||
if (show) {
|
} else {
|
||||||
return <a role='button' tabIndex='0' className='nav-link standalone p-0' onClick={() => router.back()}><BackArrow className='theme me-1 me-md-2' width={22} height={22} /></a>
|
router.push('/')
|
||||||
}
|
}
|
||||||
return null
|
}}
|
||||||
|
>
|
||||||
|
<BackArrow className='theme me-1 me-md-2' width={22} height={22} />
|
||||||
|
</a>
|
||||||
}
|
}
|
||||||
|
|
||||||
function NotificationBell () {
|
function NotificationBell () {
|
||||||
const { data } = useQuery(HAS_NOTIFICATIONS, {
|
const { data } = useQuery(HAS_NOTIFICATIONS, SSR
|
||||||
|
? {}
|
||||||
|
: {
|
||||||
pollInterval: 30000,
|
pollInterval: 30000,
|
||||||
nextFetchPolicy: 'cache-and-network'
|
nextFetchPolicy: 'cache-and-network'
|
||||||
})
|
})
|
||||||
|
|
|
@ -38,7 +38,7 @@
|
||||||
background-color: var(--bs-primary);
|
background-color: var(--bs-primary);
|
||||||
top: 3px;
|
top: 3px;
|
||||||
right: 0px;
|
right: 0px;
|
||||||
border: 1px solid var(--theme-body);
|
border: 1px solid var(--bs-body-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.notification {
|
.notification {
|
||||||
|
@ -47,7 +47,7 @@
|
||||||
background-color: var(--bs-danger);
|
background-color: var(--bs-danger);
|
||||||
top: 1px;
|
top: 1px;
|
||||||
right: 8px;
|
right: 8px;
|
||||||
border: 1px solid var(--theme-body);
|
border: 1px solid var(--bs-body-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbarNav {
|
.navbarNav {
|
||||||
|
|
|
@ -5,7 +5,7 @@ import Badge from 'react-bootstrap/Badge'
|
||||||
import Dropdown from 'react-bootstrap/Dropdown'
|
import Dropdown from 'react-bootstrap/Dropdown'
|
||||||
import Countdown from './countdown'
|
import Countdown from './countdown'
|
||||||
import { abbrNum } from '../lib/format'
|
import { abbrNum } from '../lib/format'
|
||||||
import { newComments } from '../lib/new-comments'
|
import { newComments, commentsViewedAt } from '../lib/new-comments'
|
||||||
import { timeSince } from '../lib/time'
|
import { timeSince } from '../lib/time'
|
||||||
import CowboyHat from './cowboy-hat'
|
import CowboyHat from './cowboy-hat'
|
||||||
import { DeleteDropdownItem } from './delete'
|
import { DeleteDropdownItem } from './delete'
|
||||||
|
@ -48,9 +48,22 @@ export default function ItemInfo ({ item, pendingSats, full, commentsText, class
|
||||||
<span>{abbrNum(item.boost)} boost</span>
|
<span>{abbrNum(item.boost)} boost</span>
|
||||||
<span> \ </span>
|
<span> \ </span>
|
||||||
</>}
|
</>}
|
||||||
<Link href={`/items/${item.id}`} title={`${item.commentSats} sats`} className='text-reset'>
|
<Link
|
||||||
|
href={`/items/${item.id}`} onClick={(e) => {
|
||||||
|
const viewedAt = commentsViewedAt(item)
|
||||||
|
if (viewedAt) {
|
||||||
|
e.preventDefault()
|
||||||
|
router.push(
|
||||||
|
`/items/${item.id}?commentsViewedAt=${viewedAt}`,
|
||||||
|
`/items/${item.id}`)
|
||||||
|
}
|
||||||
|
}} title={`${item.commentSats} sats`} className='text-reset position-relative'
|
||||||
|
>
|
||||||
{item.ncomments} {commentsText || 'comments'}
|
{item.ncomments} {commentsText || 'comments'}
|
||||||
{hasNewComments && <>{' '}<Badge className={styles.newComment} bg={null}>new</Badge></>}
|
{hasNewComments &&
|
||||||
|
<span className={styles.notification}>
|
||||||
|
<span className='invisible'>{' '}</span>
|
||||||
|
</span>}
|
||||||
</Link>
|
</Link>
|
||||||
<span> \ </span>
|
<span> \ </span>
|
||||||
<span>
|
<span>
|
||||||
|
|
|
@ -54,7 +54,7 @@ export default function ItemJob ({ item, toc, rank, children }) {
|
||||||
@{item.user.name}<CowboyHat className='ms-1 fill-grey' user={item.user} height={12} width={12} />
|
@{item.user.name}<CowboyHat className='ms-1 fill-grey' user={item.user} height={12} width={12} />
|
||||||
</Link>
|
</Link>
|
||||||
<span> </span>
|
<span> </span>
|
||||||
<Link href={`/items/${item.id}`} title={item.createdAt} className='text-reset'>
|
<Link href={`/items/${item.id}`} title={item.createdAt} className='text-reset' suppressHydrationWarning>
|
||||||
{timeSince(new Date(item.createdAt))}
|
{timeSince(new Date(item.createdAt))}
|
||||||
</Link>
|
</Link>
|
||||||
</span>
|
</span>
|
||||||
|
|
|
@ -12,6 +12,8 @@ import Flag from '../svgs/flag-fill.svg'
|
||||||
import ImageIcon from '../svgs/image-fill.svg'
|
import ImageIcon from '../svgs/image-fill.svg'
|
||||||
import { abbrNum } from '../lib/format'
|
import { abbrNum } from '../lib/format'
|
||||||
import ItemInfo from './item-info'
|
import ItemInfo from './item-info'
|
||||||
|
import { commentsViewedAt } from '../lib/new-comments'
|
||||||
|
import { useRouter } from 'next/router'
|
||||||
|
|
||||||
export function SearchTitle ({ title }) {
|
export function SearchTitle ({ title }) {
|
||||||
return reactStringReplace(title, /:high\[([^\]]+)\]/g, (match, i) => {
|
return reactStringReplace(title, /:high\[([^\]]+)\]/g, (match, i) => {
|
||||||
|
@ -21,6 +23,7 @@ export function SearchTitle ({ title }) {
|
||||||
|
|
||||||
export default function Item ({ item, rank, belowTitle, right, full, children, siblingComments }) {
|
export default function Item ({ item, rank, belowTitle, right, full, children, siblingComments }) {
|
||||||
const titleRef = useRef()
|
const titleRef = useRef()
|
||||||
|
const router = useRouter()
|
||||||
const [pendingSats, setPendingSats] = useState(0)
|
const [pendingSats, setPendingSats] = useState(0)
|
||||||
|
|
||||||
const image = item.url && item.url.startsWith(process.env.NEXT_PUBLIC_IMGPROXY_URL)
|
const image = item.url && item.url.startsWith(process.env.NEXT_PUBLIC_IMGPROXY_URL)
|
||||||
|
@ -39,7 +42,18 @@ export default function Item ({ item, rank, belowTitle, right, full, children, s
|
||||||
: item.meDontLike ? <Flag width={24} height={24} className={styles.dontLike} /> : <UpVote item={item} className={styles.upvote} pendingSats={pendingSats} setPendingSats={setPendingSats} />}
|
: item.meDontLike ? <Flag width={24} height={24} className={styles.dontLike} /> : <UpVote item={item} className={styles.upvote} pendingSats={pendingSats} setPendingSats={setPendingSats} />}
|
||||||
<div className={styles.hunk}>
|
<div className={styles.hunk}>
|
||||||
<div className={`${styles.main} flex-wrap`}>
|
<div className={`${styles.main} flex-wrap`}>
|
||||||
<Link href={`/items/${item.id}`} ref={titleRef} className={`${styles.title} text-reset me-2`}>
|
<Link
|
||||||
|
href={`/items/${item.id}`}
|
||||||
|
onClick={(e) => {
|
||||||
|
const viewedAt = commentsViewedAt(item)
|
||||||
|
if (viewedAt) {
|
||||||
|
e.preventDefault()
|
||||||
|
router.push(
|
||||||
|
`/items/${item.id}?commentsViewedAt=${viewedAt}`,
|
||||||
|
`/items/${item.id}`)
|
||||||
|
}
|
||||||
|
}} ref={titleRef} className={`${styles.title} text-reset me-2`}
|
||||||
|
>
|
||||||
{item.searchTitle ? <SearchTitle title={item.searchTitle} /> : item.title}
|
{item.searchTitle ? <SearchTitle title={item.searchTitle} /> : item.title}
|
||||||
{item.pollCost && <span className={styles.icon}> <PollIcon className='fill-grey ms-1' height={14} width={14} /></span>}
|
{item.pollCost && <span className={styles.icon}> <PollIcon className='fill-grey ms-1' height={14} width={14} /></span>}
|
||||||
{item.bounty > 0 &&
|
{item.bounty > 0 &&
|
||||||
|
|
|
@ -7,6 +7,15 @@
|
||||||
padding-bottom: .15rem;
|
padding-bottom: .15rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.notification {
|
||||||
|
position: absolute;
|
||||||
|
padding: 3px;
|
||||||
|
background-color: var(--bs-info);
|
||||||
|
top: -3px;
|
||||||
|
right: -4px;
|
||||||
|
border: 1px solid var(--bs-body-bg);
|
||||||
|
}
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,19 +8,21 @@ import { CommentFlat } from './comment'
|
||||||
import { SUB_ITEMS } from '../fragments/subs'
|
import { SUB_ITEMS } from '../fragments/subs'
|
||||||
import { LIMIT } from '../lib/cursor'
|
import { LIMIT } from '../lib/cursor'
|
||||||
import ItemFull from './item-full'
|
import ItemFull from './item-full'
|
||||||
|
import { useData } from './use-data'
|
||||||
|
|
||||||
export default function Items ({ ssrData, variables = {}, query, destructureData, rank, noMoreText, Footer, filter = () => true }) {
|
export default function Items ({ ssrData, variables = {}, query, destructureData, rank, noMoreText, Footer, filter = () => true }) {
|
||||||
const { data, fetchMore } = useQuery(query || SUB_ITEMS, { variables })
|
const { data, fetchMore } = useQuery(query || SUB_ITEMS, { variables })
|
||||||
const Foooter = Footer || MoreFooter
|
const Foooter = Footer || MoreFooter
|
||||||
|
const dat = useData(data, ssrData)
|
||||||
|
|
||||||
const { items, pins, cursor } = useMemo(() => {
|
const { items, pins, cursor } = useMemo(() => {
|
||||||
if (!data && !ssrData) return {}
|
if (!dat) return {}
|
||||||
if (destructureData) {
|
if (destructureData) {
|
||||||
return destructureData(data || ssrData)
|
return destructureData(dat)
|
||||||
} else {
|
} else {
|
||||||
return data?.items || ssrData?.items
|
return dat?.items
|
||||||
}
|
}
|
||||||
}, [data, ssrData])
|
}, [dat])
|
||||||
|
|
||||||
const pinMap = useMemo(() =>
|
const pinMap = useMemo(() =>
|
||||||
pins?.reduce((a, p) => { a[p.position] = p; return a }, {}), [pins])
|
pins?.reduce((a, p) => { a[p.position] = p; return a }, {}), [pins])
|
||||||
|
@ -28,7 +30,7 @@ export default function Items ({ ssrData, variables = {}, query, destructureData
|
||||||
const Skeleton = useCallback(() =>
|
const Skeleton = useCallback(() =>
|
||||||
<ItemsSkeleton rank={rank} startRank={items?.length} limit={variables.limit} />, [rank, items])
|
<ItemsSkeleton rank={rank} startRank={items?.length} limit={variables.limit} />, [rank, items])
|
||||||
|
|
||||||
if (!ssrData && !data) {
|
if (!dat) {
|
||||||
return <Skeleton />
|
return <Skeleton />
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,7 @@ import Qr, { QrSkeleton } from './qr'
|
||||||
import styles from './lightning-auth.module.css'
|
import styles from './lightning-auth.module.css'
|
||||||
import BackIcon from '../svgs/arrow-left-line.svg'
|
import BackIcon from '../svgs/arrow-left-line.svg'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
|
import { SSR } from '../lib/constants'
|
||||||
|
|
||||||
function QrAuth ({ k1, encodedUrl, slashtagUrl, callbackUrl }) {
|
function QrAuth ({ k1, encodedUrl, slashtagUrl, callbackUrl }) {
|
||||||
const query = gql`
|
const query = gql`
|
||||||
|
@ -18,7 +19,7 @@ function QrAuth ({ k1, encodedUrl, slashtagUrl, callbackUrl }) {
|
||||||
k1
|
k1
|
||||||
}
|
}
|
||||||
}`
|
}`
|
||||||
const { data } = useQuery(query, { pollInterval: 1000, nextFetchPolicy: 'cache-and-network' })
|
const { data } = useQuery(query, SSR ? {} : { pollInterval: 1000, nextFetchPolicy: 'cache-and-network' })
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data?.lnAuth?.pubkey) {
|
if (data?.lnAuth?.pubkey) {
|
||||||
|
|
|
@ -85,10 +85,13 @@ export default function Login ({ providers, callbackUrl, error, text, Header, Fo
|
||||||
className={`mt-2 ${styles.providerButton}`}
|
className={`mt-2 ${styles.providerButton}`}
|
||||||
key={provider.id}
|
key={provider.id}
|
||||||
type={provider.id.toLowerCase()}
|
type={provider.id.toLowerCase()}
|
||||||
onClick={() => router.push({
|
onClick={() => {
|
||||||
|
const { nodata, ...query } = router.query
|
||||||
|
router.push({
|
||||||
pathname: router.pathname,
|
pathname: router.pathname,
|
||||||
query: { callbackUrl: router.query.callbackUrl, type: provider.name.toLowerCase() }
|
query: { ...query, type: provider.name.toLowerCase() }
|
||||||
})}
|
})
|
||||||
|
}}
|
||||||
text={`${text || 'Login'} with`}
|
text={`${text || 'Login'} with`}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
import React, { useContext } from 'react'
|
import React, { useContext } from 'react'
|
||||||
import { useQuery } from '@apollo/client'
|
import { useQuery } from '@apollo/client'
|
||||||
import { ME } from '../fragments/users'
|
import { ME } from '../fragments/users'
|
||||||
|
import { SSR } from '../lib/constants'
|
||||||
|
|
||||||
export const MeContext = React.createContext({
|
export const MeContext = React.createContext({
|
||||||
me: null
|
me: null
|
||||||
})
|
})
|
||||||
|
|
||||||
export function MeProvider ({ me, children }) {
|
export function MeProvider ({ me, children }) {
|
||||||
const { data } = useQuery(ME, { pollInterval: 1000, nextFetchPolicy: 'cache-and-network' })
|
const { data } = useQuery(ME, SSR ? {} : { pollInterval: 1000, nextFetchPolicy: 'cache-and-network' })
|
||||||
|
|
||||||
const contextValue = {
|
const contextValue = {
|
||||||
me: data?.me || me
|
me: data?.me || me
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import { useState, useEffect, useMemo } from 'react'
|
import { useState, useEffect, useMemo } from 'react'
|
||||||
import { useApolloClient, useQuery } from '@apollo/client'
|
import { useQuery } from '@apollo/client'
|
||||||
import Comment, { CommentSkeleton } from './comment'
|
import Comment, { CommentSkeleton } from './comment'
|
||||||
import Item from './item'
|
import Item from './item'
|
||||||
import ItemJob from './item-job'
|
import ItemJob from './item-job'
|
||||||
import { HAS_NOTIFICATIONS, NOTIFICATIONS } from '../fragments/notifications'
|
import { NOTIFICATIONS } from '../fragments/notifications'
|
||||||
import MoreFooter from './more-footer'
|
import MoreFooter from './more-footer'
|
||||||
import Invite from './invite'
|
import Invite from './invite'
|
||||||
import { ignoreClick } from '../lib/clicks'
|
import { ignoreClick } from '../lib/clicks'
|
||||||
|
@ -20,29 +20,47 @@ import styles from './notifications.module.css'
|
||||||
import { useServiceWorker } from './serviceworker'
|
import { useServiceWorker } from './serviceworker'
|
||||||
import { Checkbox, Form } from './form'
|
import { Checkbox, Form } from './form'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
|
import { useData } from './use-data'
|
||||||
|
|
||||||
function Notification ({ n }) {
|
function Notification ({ n, fresh }) {
|
||||||
switch (n.__typename) {
|
const type = n.__typename
|
||||||
case 'Earn': return <EarnNotification n={n} />
|
|
||||||
case 'Invitification': return <Invitification n={n} />
|
return (
|
||||||
case 'InvoicePaid': return <InvoicePaid n={n} />
|
<NotificationLayout nid={nid(n)} {...defaultOnClick(n)} fresh={fresh}>
|
||||||
case 'Referral': return <Referral n={n} />
|
{
|
||||||
case 'Streak': return <Streak n={n} />
|
(type === 'Earn' && <EarnNotification n={n} />) ||
|
||||||
case 'Votification': return <Votification n={n} />
|
(type === 'Invitification' && <Invitification n={n} />) ||
|
||||||
case 'Mention': return <Mention n={n} />
|
(type === 'InvoicePaid' && <InvoicePaid n={n} />) ||
|
||||||
case 'JobChanged': return <JobChanged n={n} />
|
(type === 'Referral' && <Referral n={n} />) ||
|
||||||
case 'Reply': return <Reply n={n} />
|
(type === 'Streak' && <Streak n={n} />) ||
|
||||||
|
(type === 'Votification' && <Votification n={n} />) ||
|
||||||
|
(type === 'Mention' && <Mention n={n} />) ||
|
||||||
|
(type === 'JobChanged' && <JobChanged n={n} />) ||
|
||||||
|
(type === 'Reply' && <Reply n={n} />)
|
||||||
}
|
}
|
||||||
console.error('__typename not supported:', n.__typename)
|
</NotificationLayout>
|
||||||
return null
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function NotificationLayout ({ children, href, as }) {
|
function NotificationLayout ({ children, nid, href, as, fresh }) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
if (!href) return <div className={fresh ? styles.fresh : ''}>{children}</div>
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className='clickToContext'
|
className={
|
||||||
onClick={(e) => !ignoreClick(e) && router.push(href, as)}
|
`clickToContext ${fresh ? styles.fresh : ''} ${router?.query?.nid === nid ? 'outline-it' : ''}`
|
||||||
|
}
|
||||||
|
onClick={async (e) => {
|
||||||
|
if (ignoreClick(e)) return
|
||||||
|
nid && await router.replace({
|
||||||
|
pathname: router.pathname,
|
||||||
|
query: {
|
||||||
|
...router.query,
|
||||||
|
nid
|
||||||
|
}
|
||||||
|
}, router.asPath, { ...router.options, shallow: true })
|
||||||
|
router.push(href, as)
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
@ -50,6 +68,14 @@ function NotificationLayout ({ children, href, as }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultOnClick = n => {
|
const defaultOnClick = n => {
|
||||||
|
const type = n.__typename
|
||||||
|
if (type === 'Earn') return {}
|
||||||
|
if (type === 'Invitification') return { href: '/invites' }
|
||||||
|
if (type === 'InvoicePaid') return { href: `/invoices/${n.invoice.id}` }
|
||||||
|
if (type === 'Referral') return { href: '/referrals/month' }
|
||||||
|
if (type === 'Streak') return {}
|
||||||
|
|
||||||
|
// Votification, Mention, JobChanged, Reply all have item
|
||||||
if (!n.item.title) {
|
if (!n.item.title) {
|
||||||
const path = n.item.path.split('.')
|
const path = n.item.path.split('.')
|
||||||
if (path.length > COMMENT_DEPTH_LIMIT + 1) {
|
if (path.length > COMMENT_DEPTH_LIMIT + 1) {
|
||||||
|
@ -126,7 +152,7 @@ function EarnNotification ({ n }) {
|
||||||
<HandCoin className='align-self-center fill-boost mx-1' width={24} height={24} style={{ flex: '0 0 24px', transform: 'rotateY(180deg)' }} />
|
<HandCoin className='align-self-center fill-boost mx-1' width={24} height={24} style={{ flex: '0 0 24px', transform: 'rotateY(180deg)' }} />
|
||||||
<div className='ms-2'>
|
<div className='ms-2'>
|
||||||
<div className='fw-bold text-boost'>
|
<div className='fw-bold text-boost'>
|
||||||
you stacked {n.earnedSats} sats in rewards<small className='text-muted ms-1 fw-normal'>{timeSince(new Date(n.sortTime))}</small>
|
you stacked {n.earnedSats} sats in rewards<small className='text-muted ms-1 fw-normal' suppressHydrationWarning>{timeSince(new Date(n.sortTime))}</small>
|
||||||
</div>
|
</div>
|
||||||
{n.sources &&
|
{n.sources &&
|
||||||
<div style={{ fontSize: '80%', color: 'var(--theme-grey)' }}>
|
<div style={{ fontSize: '80%', color: 'var(--theme-grey)' }}>
|
||||||
|
@ -145,7 +171,7 @@ function EarnNotification ({ n }) {
|
||||||
|
|
||||||
function Invitification ({ n }) {
|
function Invitification ({ n }) {
|
||||||
return (
|
return (
|
||||||
<NotificationLayout href='/invites'>
|
<>
|
||||||
<small className='fw-bold text-secondary ms-2'>
|
<small className='fw-bold text-secondary ms-2'>
|
||||||
your invite has been redeemed by {n.invite.invitees.length} stackers
|
your invite has been redeemed by {n.invite.invitees.length} stackers
|
||||||
</small>
|
</small>
|
||||||
|
@ -157,35 +183,31 @@ function Invitification ({ n }) {
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</NotificationLayout>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function InvoicePaid ({ n }) {
|
function InvoicePaid ({ n }) {
|
||||||
return (
|
return (
|
||||||
<NotificationLayout href={`/invoices/${n.invoice.id}`}>
|
|
||||||
<div className='fw-bold text-info ms-2 py-1'>
|
<div className='fw-bold text-info ms-2 py-1'>
|
||||||
<Check className='fill-info me-1' />{n.earnedSats} sats were deposited in your account
|
<Check className='fill-info me-1' />{n.earnedSats} sats were deposited in your account
|
||||||
<small className='text-muted ms-1 fw-normal'>{timeSince(new Date(n.sortTime))}</small>
|
<small className='text-muted ms-1 fw-normal' suppressHydrationWarning>{timeSince(new Date(n.sortTime))}</small>
|
||||||
</div>
|
</div>
|
||||||
</NotificationLayout>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function Referral ({ n }) {
|
function Referral ({ n }) {
|
||||||
return (
|
return (
|
||||||
<NotificationLayout>
|
|
||||||
<small className='fw-bold text-secondary ms-2'>
|
<small className='fw-bold text-secondary ms-2'>
|
||||||
someone joined via one of your <Link href='/referrals/month' className='text-reset'>referral links</Link>
|
someone joined via one of your referral links
|
||||||
<small className='text-muted ms-1 fw-normal'>{timeSince(new Date(n.sortTime))}</small>
|
<small className='text-muted ms-1 fw-normal' suppressHydrationWarning>{timeSince(new Date(n.sortTime))}</small>
|
||||||
</small>
|
</small>
|
||||||
</NotificationLayout>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function Votification ({ n }) {
|
function Votification ({ n }) {
|
||||||
return (
|
return (
|
||||||
<NotificationLayout {...defaultOnClick(n)}>
|
<>
|
||||||
<small className='fw-bold text-success ms-2'>
|
<small className='fw-bold text-success ms-2'>
|
||||||
your {n.item.title ? 'post' : 'reply'} {n.item.fwdUser ? 'forwarded' : 'stacked'} {n.earnedSats} sats{n.item.fwdUser && ` to @${n.item.fwdUser.name}`}
|
your {n.item.title ? 'post' : 'reply'} {n.item.fwdUser ? 'forwarded' : 'stacked'} {n.earnedSats} sats{n.item.fwdUser && ` to @${n.item.fwdUser.name}`}
|
||||||
</small>
|
</small>
|
||||||
|
@ -200,13 +222,13 @@ function Votification ({ n }) {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</NotificationLayout>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function Mention ({ n }) {
|
function Mention ({ n }) {
|
||||||
return (
|
return (
|
||||||
<NotificationLayout {...defaultOnClick(n)}>
|
<>
|
||||||
<small className='fw-bold text-info ms-2'>
|
<small className='fw-bold text-info ms-2'>
|
||||||
you were mentioned in
|
you were mentioned in
|
||||||
</small>
|
</small>
|
||||||
|
@ -220,13 +242,13 @@ function Mention ({ n }) {
|
||||||
</RootProvider>
|
</RootProvider>
|
||||||
</div>)}
|
</div>)}
|
||||||
</div>
|
</div>
|
||||||
</NotificationLayout>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function JobChanged ({ n }) {
|
function JobChanged ({ n }) {
|
||||||
return (
|
return (
|
||||||
<NotificationLayout {...defaultOnClick(n)}>
|
<>
|
||||||
<small className={`fw-bold text-${n.item.status === 'ACTIVE' ? 'success' : 'boost'} ms-1`}>
|
<small className={`fw-bold text-${n.item.status === 'ACTIVE' ? 'success' : 'boost'} ms-1`}>
|
||||||
{n.item.status === 'ACTIVE'
|
{n.item.status === 'ACTIVE'
|
||||||
? 'your job is active again'
|
? 'your job is active again'
|
||||||
|
@ -235,13 +257,12 @@ function JobChanged ({ n }) {
|
||||||
: 'your job has been stopped')}
|
: 'your job has been stopped')}
|
||||||
</small>
|
</small>
|
||||||
<ItemJob item={n.item} />
|
<ItemJob item={n.item} />
|
||||||
</NotificationLayout>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function Reply ({ n }) {
|
function Reply ({ n }) {
|
||||||
return (
|
return (
|
||||||
<NotificationLayout {...defaultOnClick(n)} rootText='replying on:'>
|
|
||||||
<div className='py-2'>
|
<div className='py-2'>
|
||||||
{n.item.title
|
{n.item.title
|
||||||
? <Item item={n.item} />
|
? <Item item={n.item} />
|
||||||
|
@ -253,11 +274,10 @@ function Reply ({ n }) {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</NotificationLayout>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function NotificationAlert () {
|
export function NotificationAlert () {
|
||||||
const [showAlert, setShowAlert] = useState(false)
|
const [showAlert, setShowAlert] = useState(false)
|
||||||
const [hasSubscription, setHasSubscription] = useState(false)
|
const [hasSubscription, setHasSubscription] = useState(false)
|
||||||
const [error, setError] = useState(null)
|
const [error, setError] = useState(null)
|
||||||
|
@ -316,47 +336,39 @@ function NotificationAlert () {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const nid = n => n.__typename + n.id + n.sortTime
|
||||||
|
|
||||||
export default function Notifications ({ ssrData }) {
|
export default function Notifications ({ ssrData }) {
|
||||||
const { data, fetchMore } = useQuery(NOTIFICATIONS)
|
const { data, fetchMore } = useQuery(NOTIFICATIONS)
|
||||||
const client = useApolloClient()
|
const router = useRouter()
|
||||||
|
const dat = useData(data, ssrData)
|
||||||
|
|
||||||
|
const { notifications: { notifications, lastChecked, cursor } } = useMemo(() => {
|
||||||
|
return dat || { notifications: {} }
|
||||||
|
}, [dat])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
client.writeQuery({
|
if (lastChecked && !router?.query?.checkedAt) {
|
||||||
query: HAS_NOTIFICATIONS,
|
router.replace({
|
||||||
data: {
|
pathname: router.pathname,
|
||||||
hasNewNotes: false
|
query: {
|
||||||
|
...router.query,
|
||||||
|
nodata: true, // make sure nodata is set so we don't fetch on back/forward
|
||||||
|
checkedAt: lastChecked
|
||||||
}
|
}
|
||||||
})
|
}, router.asPath, { ...router.options, shallow: true })
|
||||||
}, [client])
|
}
|
||||||
|
}, [router, lastChecked])
|
||||||
|
|
||||||
const { notifications: { notifications, earn, lastChecked, cursor } } = useMemo(() => {
|
if (!dat) return <CommentsFlatSkeleton />
|
||||||
if (!data && !ssrData) return { notifications: {} }
|
|
||||||
return data || ssrData
|
|
||||||
}, [data, ssrData])
|
|
||||||
|
|
||||||
const [fresh, old] = useMemo(() => {
|
|
||||||
if (!notifications) return [[], []]
|
|
||||||
return notifications.reduce((result, n) => {
|
|
||||||
result[new Date(n.sortTime).getTime() > lastChecked ? 0 : 1].push(n)
|
|
||||||
return result
|
|
||||||
},
|
|
||||||
[[], []])
|
|
||||||
}, [notifications, lastChecked])
|
|
||||||
|
|
||||||
if (!data && !ssrData) return <CommentsFlatSkeleton />
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<NotificationAlert />
|
{notifications.map(n =>
|
||||||
<div className='fresh'>
|
<Notification
|
||||||
{earn && <Notification n={earn} key='earn' />}
|
n={n} key={nid(n)}
|
||||||
{fresh.map((n, i) => (
|
fresh={new Date(n.sortTime) > new Date(router?.query?.checkedAt)}
|
||||||
<Notification n={n} key={n.__typename + n.id + n.sortTime} />
|
/>)}
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
{old.map((n, i) => (
|
|
||||||
<Notification n={n} key={n.__typename + n.id + n.sortTime} />
|
|
||||||
))}
|
|
||||||
<MoreFooter cursor={cursor} count={notifications?.length} fetchMore={fetchMore} Skeleton={CommentsFlatSkeleton} noMoreText='NO MORE' />
|
<MoreFooter cursor={cursor} count={notifications?.length} fetchMore={fetchMore} Skeleton={CommentsFlatSkeleton} noMoreText='NO MORE' />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
.clickToContext {
|
|
||||||
border-radius: .4rem;
|
|
||||||
padding: .2rem 0;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.clickToContext:hover {
|
|
||||||
background-color: rgba(0, 0, 0, 0.03);
|
|
||||||
}
|
|
||||||
|
|
||||||
.fresh {
|
.fresh {
|
||||||
background-color: rgba(0, 0, 0, 0.03);
|
background-color: rgba(128, 128, 128, 0.1);
|
||||||
border-radius: .4rem;
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fresh:not(.fresh ~ .fresh) {
|
||||||
|
border-top-left-radius: .4rem;
|
||||||
|
border-top-right-radius: .4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fresh:has(+ :not(.fresh)) {
|
||||||
|
border-bottom-left-radius: .4rem;
|
||||||
|
border-bottom-right-radius: .4rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.alertBtn {
|
.alertBtn {
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { fixedDecimal } from '../lib/format'
|
||||||
import { useMe } from './me'
|
import { useMe } from './me'
|
||||||
import { PRICE } from '../fragments/price'
|
import { PRICE } from '../fragments/price'
|
||||||
import { CURRENCY_SYMBOLS } from '../lib/currency'
|
import { CURRENCY_SYMBOLS } from '../lib/currency'
|
||||||
|
import { SSR } from '../lib/constants'
|
||||||
|
|
||||||
export const PriceContext = React.createContext({
|
export const PriceContext = React.createContext({
|
||||||
price: null,
|
price: null,
|
||||||
|
@ -19,9 +20,13 @@ export function PriceProvider ({ price, children }) {
|
||||||
const fiatCurrency = me?.fiatCurrency
|
const fiatCurrency = me?.fiatCurrency
|
||||||
const { data } = useQuery(PRICE, {
|
const { data } = useQuery(PRICE, {
|
||||||
variables: { fiatCurrency },
|
variables: { fiatCurrency },
|
||||||
|
...(SSR
|
||||||
|
? {}
|
||||||
|
: {
|
||||||
pollInterval: 30000,
|
pollInterval: 30000,
|
||||||
nextFetchPolicy: 'cache-and-network'
|
nextFetchPolicy: 'cache-and-network'
|
||||||
})
|
})
|
||||||
|
})
|
||||||
|
|
||||||
const contextValue = {
|
const contextValue = {
|
||||||
price: data?.price || price,
|
price: data?.price || price,
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
.searchSection.solid {
|
.searchSection.solid {
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
background: var(--theme-body);
|
background: var(--bs-body-bg);
|
||||||
box-shadow: 0 -4px 12px hsl(0deg 0% 59% / 10%);
|
box-shadow: 0 -4px 12px hsl(0deg 0% 59% / 10%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -32,7 +32,7 @@ export default function TopHeader ({ sub, cat }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const what = cat
|
const what = cat
|
||||||
const by = router.query.by || ''
|
const by = router.query.by || (what === 'stackers' ? 'stacked' : 'votes')
|
||||||
const when = router.query.when || ''
|
const when = router.query.when || ''
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -27,7 +27,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.cover {
|
.cover {
|
||||||
background: var(--theme-body);
|
background: var(--bs-body-bg);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
mix-blend-mode: color;
|
mix-blend-mode: color;
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
import { useEffect, useRef, useState } from 'react'
|
||||||
|
|
||||||
|
/*
|
||||||
|
What we want is to use ssrData if it exists, until cache data changes
|
||||||
|
... this prevents item list jitter where the intially rendered items
|
||||||
|
are stale until the cache is rewritten with incoming ssrData
|
||||||
|
*/
|
||||||
|
export function useData (data, ssrData) {
|
||||||
|
// when fresh is true, it means data has been updated after the initial render and it's populated
|
||||||
|
const [fresh, setFresh] = useState(false)
|
||||||
|
|
||||||
|
// on first render, we want to use ssrData if it's available
|
||||||
|
// it's only unavailable on back/forward navigation
|
||||||
|
const ref = useRef(true)
|
||||||
|
const firstRender = ref.current
|
||||||
|
ref.current = false
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!firstRender && !fresh && data) setFresh(true)
|
||||||
|
}, [data])
|
||||||
|
|
||||||
|
// if we don't have data yet, use ssrData
|
||||||
|
// if we have data, but it's not fresh, use ssrData
|
||||||
|
// unless we don't have ssrData
|
||||||
|
if (!data || (!fresh && ssrData)) return ssrData
|
||||||
|
return data
|
||||||
|
}
|
|
@ -7,6 +7,7 @@ import userStyles from './user-header.module.css'
|
||||||
import { useEffect, useMemo, useState } from 'react'
|
import { useEffect, useMemo, useState } from 'react'
|
||||||
import { useQuery } from '@apollo/client'
|
import { useQuery } from '@apollo/client'
|
||||||
import MoreFooter from './more-footer'
|
import MoreFooter from './more-footer'
|
||||||
|
import { useData } from './use-data'
|
||||||
|
|
||||||
// all of this nonsense is to show the stat we are sorting by first
|
// all of this nonsense is to show the stat we are sorting by first
|
||||||
const Stacked = ({ user }) => (<span>{abbrNum(user.stacked)} stacked</span>)
|
const Stacked = ({ user }) => (<span>{abbrNum(user.stacked)} stacked</span>)
|
||||||
|
@ -37,26 +38,25 @@ function seperate (arr, seperator) {
|
||||||
|
|
||||||
export default function UserList ({ ssrData, query, variables, destructureData }) {
|
export default function UserList ({ ssrData, query, variables, destructureData }) {
|
||||||
const { data, fetchMore } = useQuery(query, { variables })
|
const { data, fetchMore } = useQuery(query, { variables })
|
||||||
|
const dat = useData(data, ssrData)
|
||||||
const [statComps, setStatComps] = useState(seperate(STAT_COMPONENTS, Seperator))
|
const [statComps, setStatComps] = useState(seperate(STAT_COMPONENTS, Seperator))
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (variables?.by) {
|
|
||||||
// shift the stat we are sorting by to the front
|
// shift the stat we are sorting by to the front
|
||||||
const comps = [...STAT_COMPONENTS]
|
const comps = [...STAT_COMPONENTS]
|
||||||
setStatComps(seperate([...comps.splice(STAT_POS[variables.by], 1), ...comps], Seperator))
|
setStatComps(seperate([...comps.splice(STAT_POS[variables.by || 0], 1), ...comps], Seperator))
|
||||||
}
|
|
||||||
}, [variables?.by])
|
}, [variables?.by])
|
||||||
|
|
||||||
const { users, cursor } = useMemo(() => {
|
const { users, cursor } = useMemo(() => {
|
||||||
if (!data && !ssrData) return {}
|
if (!dat) return {}
|
||||||
if (destructureData) {
|
if (destructureData) {
|
||||||
return destructureData(data || ssrData)
|
return destructureData(dat)
|
||||||
} else {
|
} else {
|
||||||
return data || ssrData
|
return dat
|
||||||
}
|
}
|
||||||
}, [data, ssrData])
|
}, [dat])
|
||||||
|
|
||||||
if (!ssrData && !data) {
|
if (!dat) {
|
||||||
return <UsersSkeleton />
|
return <UsersSkeleton />
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { ApolloClient, InMemoryCache, HttpLink } from '@apollo/client'
|
import { ApolloClient, InMemoryCache, HttpLink } from '@apollo/client'
|
||||||
import { decodeCursor, LIMIT } from './cursor'
|
import { decodeCursor, LIMIT } from './cursor'
|
||||||
|
import { SSR } from './constants'
|
||||||
|
|
||||||
function isFirstPage (cursor, existingThings) {
|
function isFirstPage (cursor, existingThings) {
|
||||||
if (cursor) {
|
if (cursor) {
|
||||||
|
@ -12,7 +13,6 @@ function isFirstPage (cursor, existingThings) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const SSR = typeof window === 'undefined'
|
|
||||||
const defaultFetchPolicy = SSR ? 'cache-only' : 'cache-first'
|
const defaultFetchPolicy = SSR ? 'cache-only' : 'cache-first'
|
||||||
const defaultNextFetchPolicy = SSR ? 'cache-only' : 'cache-first'
|
const defaultNextFetchPolicy = SSR ? 'cache-only' : 'cache-first'
|
||||||
|
|
||||||
|
@ -158,14 +158,18 @@ function getClient (uri) {
|
||||||
assumeImmutableResults: true,
|
assumeImmutableResults: true,
|
||||||
defaultOptions: {
|
defaultOptions: {
|
||||||
watchQuery: {
|
watchQuery: {
|
||||||
|
initialFetchPolicy: defaultFetchPolicy,
|
||||||
fetchPolicy: defaultFetchPolicy,
|
fetchPolicy: defaultFetchPolicy,
|
||||||
nextFetchPolicy: defaultNextFetchPolicy,
|
nextFetchPolicy: defaultNextFetchPolicy,
|
||||||
canonizeResults: true
|
canonizeResults: true,
|
||||||
|
ssr: SSR
|
||||||
},
|
},
|
||||||
query: {
|
query: {
|
||||||
|
initialFetchPolicy: defaultFetchPolicy,
|
||||||
fetchPolicy: defaultFetchPolicy,
|
fetchPolicy: defaultFetchPolicy,
|
||||||
nextFetchPolicy: defaultNextFetchPolicy,
|
nextFetchPolicy: defaultNextFetchPolicy,
|
||||||
canonizeResults: true
|
canonizeResults: true,
|
||||||
|
ssr: SSR
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -45,3 +45,6 @@ module.exports = {
|
||||||
ANON_POST_FEE: 1000,
|
ANON_POST_FEE: 1000,
|
||||||
ANON_COMMENT_FEE: 100,
|
ANON_COMMENT_FEE: 100,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const OLD_ITEM_DAYS = 3
|
||||||
|
export const SSR = typeof window === 'undefined'
|
||||||
|
|
|
@ -14,13 +14,17 @@ export function commentsViewedAfterComment (rootId, createdAt) {
|
||||||
window.localStorage.setItem(`${COMMENTS_NUM_PREFIX}:${rootId}`, existingRootComments + 1)
|
window.localStorage.setItem(`${COMMENTS_NUM_PREFIX}:${rootId}`, existingRootComments + 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function commentsViewedAt (item) {
|
||||||
|
return window.localStorage.getItem(`${COMMENTS_VIEW_PREFIX}:${item.id}`)
|
||||||
|
}
|
||||||
|
|
||||||
export function newComments (item) {
|
export function newComments (item) {
|
||||||
if (!item.parentId) {
|
if (!item.parentId) {
|
||||||
const commentsViewedAt = window.localStorage.getItem(`${COMMENTS_VIEW_PREFIX}:${item.id}`)
|
const viewedAt = commentsViewedAt(item)
|
||||||
const commentsViewNum = window.localStorage.getItem(`${COMMENTS_NUM_PREFIX}:${item.id}`)
|
const viewNum = window.localStorage.getItem(`${COMMENTS_NUM_PREFIX}:${item.id}`)
|
||||||
|
|
||||||
if (commentsViewedAt && commentsViewNum) {
|
if (viewedAt && viewNum) {
|
||||||
return commentsViewedAt < new Date(item.lastCommentAt).getTime() || commentsViewNum < item.ncomments
|
return viewedAt < new Date(item.lastCommentAt).getTime() || viewNum < item.ncomments
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,296 +0,0 @@
|
||||||
/* eslint-disable */
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
Object.defineProperty(exports, '__esModule', {
|
|
||||||
value: true
|
|
||||||
})
|
|
||||||
exports.getCompoundId = getCompoundId
|
|
||||||
exports.Adapter = exports.PrismaLegacyAdapter = PrismaLegacyAdapter
|
|
||||||
|
|
||||||
const _crypto = require('crypto')
|
|
||||||
|
|
||||||
function getCompoundId (a, b) {
|
|
||||||
return (0, _crypto.createHash)('sha256').update(`${a}:${b}`).digest('hex')
|
|
||||||
}
|
|
||||||
|
|
||||||
function PrismaLegacyAdapter (config) {
|
|
||||||
const {
|
|
||||||
prisma,
|
|
||||||
modelMapping = {
|
|
||||||
User: 'user',
|
|
||||||
Account: 'account',
|
|
||||||
Session: 'session',
|
|
||||||
VerificationRequest: 'verificationRequest'
|
|
||||||
}
|
|
||||||
} = config
|
|
||||||
const {
|
|
||||||
User,
|
|
||||||
Account,
|
|
||||||
Session,
|
|
||||||
VerificationRequest
|
|
||||||
} = modelMapping
|
|
||||||
return {
|
|
||||||
async getAdapter ({
|
|
||||||
session: {
|
|
||||||
maxAge,
|
|
||||||
updateAge
|
|
||||||
},
|
|
||||||
secret,
|
|
||||||
...appOptions
|
|
||||||
}) {
|
|
||||||
const sessionMaxAge = maxAge * 1000
|
|
||||||
const sessionUpdateAge = updateAge * 1000
|
|
||||||
|
|
||||||
const hashToken = token => (0, _crypto.createHash)('sha256').update(`${token}${secret}`).digest('hex')
|
|
||||||
|
|
||||||
return {
|
|
||||||
displayName: 'PRISMA_LEGACY',
|
|
||||||
|
|
||||||
createUser (profile) {
|
|
||||||
let _profile$emailVerifie
|
|
||||||
|
|
||||||
return prisma[User].create({
|
|
||||||
data: {
|
|
||||||
name: profile.name,
|
|
||||||
email: profile.email,
|
|
||||||
image: profile.image,
|
|
||||||
emailVerified: (_profile$emailVerifie = profile.emailVerified) === null || _profile$emailVerifie === void 0 ? void 0 : _profile$emailVerifie.toISOString()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
getUser (id) {
|
|
||||||
return prisma[User].findUnique({
|
|
||||||
where: {
|
|
||||||
id: Number(id)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
getUserByEmail (email) {
|
|
||||||
if (email) {
|
|
||||||
return prisma[User].findUnique({
|
|
||||||
where: {
|
|
||||||
email
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
},
|
|
||||||
|
|
||||||
async getUserByProviderAccountId (providerId, providerAccountId) {
|
|
||||||
const account = await prisma[Account].findUnique({
|
|
||||||
where: {
|
|
||||||
compoundId: getCompoundId(providerId, providerAccountId)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if (account) {
|
|
||||||
return prisma[User].findUnique({
|
|
||||||
where: {
|
|
||||||
id: account.userId
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
},
|
|
||||||
|
|
||||||
updateUser (user) {
|
|
||||||
const {
|
|
||||||
id,
|
|
||||||
name,
|
|
||||||
email,
|
|
||||||
image,
|
|
||||||
emailVerified
|
|
||||||
} = user
|
|
||||||
return prisma[User].update({
|
|
||||||
where: {
|
|
||||||
id
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
name,
|
|
||||||
email,
|
|
||||||
image,
|
|
||||||
emailVerified: emailVerified === null || emailVerified === void 0 ? void 0 : emailVerified.toISOString()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
deleteUser (userId) {
|
|
||||||
return prisma[User].delete({
|
|
||||||
where: {
|
|
||||||
id: userId
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
linkAccount (userId, providerId, providerType, providerAccountId, refreshToken, accessToken, accessTokenExpires) {
|
|
||||||
return prisma[Account].create({
|
|
||||||
data: {
|
|
||||||
accessToken,
|
|
||||||
refreshToken,
|
|
||||||
compoundId: getCompoundId(providerId, providerAccountId),
|
|
||||||
providerAccountId: `${providerAccountId}`,
|
|
||||||
providerId,
|
|
||||||
providerType,
|
|
||||||
accessTokenExpires,
|
|
||||||
userId
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
unlinkAccount (_, providerId, providerAccountId) {
|
|
||||||
return prisma[Account].delete({
|
|
||||||
where: {
|
|
||||||
compoundId: getCompoundId(providerId, providerAccountId)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
createSession (user) {
|
|
||||||
let expires = null
|
|
||||||
|
|
||||||
if (sessionMaxAge) {
|
|
||||||
const dateExpires = new Date()
|
|
||||||
dateExpires.setTime(dateExpires.getTime() + sessionMaxAge)
|
|
||||||
expires = dateExpires.toISOString()
|
|
||||||
}
|
|
||||||
|
|
||||||
return prisma[Session].create({
|
|
||||||
data: {
|
|
||||||
expires,
|
|
||||||
userId: user.id,
|
|
||||||
sessionToken: (0, _crypto.randomBytes)(32).toString('hex'),
|
|
||||||
accessToken: (0, _crypto.randomBytes)(32).toString('hex')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
async getSession (sessionToken) {
|
|
||||||
const session = await prisma[Session].findUnique({
|
|
||||||
where: {
|
|
||||||
sessionToken
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if (session !== null && session !== void 0 && session.expires && new Date() > session.expires) {
|
|
||||||
await prisma[Session].delete({
|
|
||||||
where: {
|
|
||||||
sessionToken
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return session
|
|
||||||
},
|
|
||||||
|
|
||||||
updateSession (session, force) {
|
|
||||||
if (sessionMaxAge && (sessionUpdateAge || sessionUpdateAge === 0) && session.expires) {
|
|
||||||
const dateSessionIsDueToBeUpdated = new Date(session.expires)
|
|
||||||
dateSessionIsDueToBeUpdated.setTime(dateSessionIsDueToBeUpdated.getTime() - sessionMaxAge)
|
|
||||||
dateSessionIsDueToBeUpdated.setTime(dateSessionIsDueToBeUpdated.getTime() + sessionUpdateAge)
|
|
||||||
|
|
||||||
if (new Date() > dateSessionIsDueToBeUpdated) {
|
|
||||||
const newExpiryDate = new Date()
|
|
||||||
newExpiryDate.setTime(newExpiryDate.getTime() + sessionMaxAge)
|
|
||||||
session.expires = newExpiryDate
|
|
||||||
} else if (!force) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (!force) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
|
||||||
id,
|
|
||||||
expires
|
|
||||||
} = session
|
|
||||||
return prisma[Session].update({
|
|
||||||
where: {
|
|
||||||
id
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
expires: expires.toISOString()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
deleteSession (sessionToken) {
|
|
||||||
return prisma[Session].delete({
|
|
||||||
where: {
|
|
||||||
sessionToken
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
async createVerificationRequest (identifier, url, token, _, provider) {
|
|
||||||
const {
|
|
||||||
sendVerificationRequest,
|
|
||||||
maxAge
|
|
||||||
} = provider
|
|
||||||
let expires = null
|
|
||||||
|
|
||||||
if (maxAge) {
|
|
||||||
const dateExpires = new Date()
|
|
||||||
dateExpires.setTime(dateExpires.getTime() + maxAge * 1000)
|
|
||||||
expires = dateExpires.toISOString()
|
|
||||||
}
|
|
||||||
|
|
||||||
const verificationRequest = await prisma[VerificationRequest].create({
|
|
||||||
data: {
|
|
||||||
identifier,
|
|
||||||
token: hashToken(token),
|
|
||||||
expires
|
|
||||||
}
|
|
||||||
})
|
|
||||||
await sendVerificationRequest({
|
|
||||||
identifier,
|
|
||||||
url,
|
|
||||||
token,
|
|
||||||
baseUrl: appOptions.baseUrl,
|
|
||||||
provider
|
|
||||||
})
|
|
||||||
return verificationRequest
|
|
||||||
},
|
|
||||||
|
|
||||||
async getVerificationRequest (identifier, token) {
|
|
||||||
const hashedToken = hashToken(token)
|
|
||||||
const verificationRequest = await prisma[VerificationRequest].findFirst({
|
|
||||||
where: {
|
|
||||||
identifier,
|
|
||||||
token: hashedToken
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if (verificationRequest && verificationRequest.expires && new Date() > verificationRequest.expires) {
|
|
||||||
await prisma[VerificationRequest].deleteMany({
|
|
||||||
where: {
|
|
||||||
identifier,
|
|
||||||
token: hashedToken
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return verificationRequest
|
|
||||||
},
|
|
||||||
|
|
||||||
async deleteVerificationRequest (identifier, token) {
|
|
||||||
await prisma[VerificationRequest].deleteMany({
|
|
||||||
where: {
|
|
||||||
identifier,
|
|
||||||
token: hashToken(token)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -40,12 +40,6 @@ module.exports = withPlausibleProxy()({
|
||||||
source: '/_next/:asset*',
|
source: '/_next/:asset*',
|
||||||
headers: corsHeaders
|
headers: corsHeaders
|
||||||
},
|
},
|
||||||
{
|
|
||||||
source: '/dark.js',
|
|
||||||
headers: [
|
|
||||||
...corsHeaders
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
source: '/.well-known/:slug*',
|
source: '/.well-known/:slug*',
|
||||||
headers: [
|
headers: [
|
||||||
|
@ -120,6 +114,10 @@ module.exports = withPlausibleProxy()({
|
||||||
source: '/.well-known/web-app-origin-association',
|
source: '/.well-known/web-app-origin-association',
|
||||||
destination: '/api/web-app-origin-association'
|
destination: '/api/web-app-origin-association'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
source: '/~:sub/:slug*\\?:query*',
|
||||||
|
destination: '/~/:slug*?:query*&sub=:sub'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
source: '/~:sub/:slug*',
|
source: '/~:sub/:slug*',
|
||||||
destination: '/~/:slug*?sub=:sub'
|
destination: '/~/:slug*?sub=:sub'
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
23
package.json
23
package.json
|
@ -6,11 +6,11 @@
|
||||||
"dev": "NODE_OPTIONS='--trace-warnings' next dev",
|
"dev": "NODE_OPTIONS='--trace-warnings' next dev",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"migrate": "prisma migrate deploy",
|
"migrate": "prisma migrate deploy",
|
||||||
"start": "NODE_OPTIONS='--trace-warnings' next start -p $PORT"
|
"start": "NODE_OPTIONS='--trace-warnings' next start -p $PORT --keepAliveTimeout 120000"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@apollo/client": "^3.7.17",
|
"@apollo/client": "^3.7.17",
|
||||||
"@apollo/server": "^4.8.1",
|
"@apollo/server": "^4.9.0",
|
||||||
"@as-integrations/next": "^2.0.1",
|
"@as-integrations/next": "^2.0.1",
|
||||||
"@auth/prisma-adapter": "^1.0.1",
|
"@auth/prisma-adapter": "^1.0.1",
|
||||||
"@graphql-tools/schema": "^10.0.0",
|
"@graphql-tools/schema": "^10.0.0",
|
||||||
|
@ -22,12 +22,11 @@
|
||||||
"acorn": "^8.10.0",
|
"acorn": "^8.10.0",
|
||||||
"ajv": "^8.12.0",
|
"ajv": "^8.12.0",
|
||||||
"async-retry": "^1.3.1",
|
"async-retry": "^1.3.1",
|
||||||
"aws-sdk": "^2.1422.0",
|
"aws-sdk": "^2.1425.0",
|
||||||
"babel-plugin-inline-react-svg": "^2.0.2",
|
"babel-plugin-inline-react-svg": "^2.0.2",
|
||||||
"bech32": "^2.0.0",
|
"bech32": "^2.0.0",
|
||||||
"bolt11": "^1.4.1",
|
"bolt11": "^1.4.1",
|
||||||
"bootstrap": "^5.3.0",
|
"bootstrap": "^5.3.1",
|
||||||
"browserslist": "^4.21.4",
|
|
||||||
"canonical-json": "0.0.4",
|
"canonical-json": "0.0.4",
|
||||||
"clipboard-copy": "^4.0.1",
|
"clipboard-copy": "^4.0.1",
|
||||||
"cross-fetch": "^4.0.0",
|
"cross-fetch": "^4.0.0",
|
||||||
|
@ -45,18 +44,19 @@
|
||||||
"mdast-util-gfm": "^3.0.0",
|
"mdast-util-gfm": "^3.0.0",
|
||||||
"mdast-util-to-string": "^4.0.0",
|
"mdast-util-to-string": "^4.0.0",
|
||||||
"micromark-extension-gfm": "^3.0.0",
|
"micromark-extension-gfm": "^3.0.0",
|
||||||
"next": "^13.4.12",
|
"next": "^13.4.13-canary.12",
|
||||||
"next-auth": "^4.22.3",
|
"next-auth": "^4.22.3",
|
||||||
"next-plausible": "^3.10.1",
|
"next-plausible": "^3.10.1",
|
||||||
"next-seo": "^6.1.0",
|
"next-seo": "^6.1.0",
|
||||||
"nextjs-progressbar": "0.0.16",
|
|
||||||
"node-s3-url-encode": "^0.0.4",
|
"node-s3-url-encode": "^0.0.4",
|
||||||
"nodemailer": "^6.9.4",
|
"nodemailer": "^6.9.4",
|
||||||
"nostr": "^0.2.8",
|
"nostr": "^0.2.8",
|
||||||
|
"nprogress": "^0.2.0",
|
||||||
"opentimestamps": "^0.4.9",
|
"opentimestamps": "^0.4.9",
|
||||||
"page-metadata-parser": "^1.1.4",
|
"page-metadata-parser": "^1.1.4",
|
||||||
"pageres": "^7.1.0",
|
"pageres": "^7.1.0",
|
||||||
"pg-boss": "^9.0.3",
|
"pg-boss": "^9.0.3",
|
||||||
|
"prisma": "^5.0.0",
|
||||||
"qrcode.react": "^3.1.0",
|
"qrcode.react": "^3.1.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-avatar-editor": "^13.0.0",
|
"react-avatar-editor": "^13.0.0",
|
||||||
|
@ -75,11 +75,11 @@
|
||||||
"remark-gfm": "^3.0.1",
|
"remark-gfm": "^3.0.1",
|
||||||
"remove-markdown": "^0.5.0",
|
"remove-markdown": "^0.5.0",
|
||||||
"sass": "^1.64.1",
|
"sass": "^1.64.1",
|
||||||
"tldts": "^6.0.12",
|
"tldts": "^6.0.13",
|
||||||
"typescript": "^5.1.6",
|
"typescript": "^5.1.6",
|
||||||
"unist-util-visit": "^5.0.0",
|
"unist-util-visit": "^5.0.0",
|
||||||
"url-unshort": "^6.1.0",
|
"url-unshort": "^6.1.0",
|
||||||
"web-push": "^3.6.2",
|
"web-push": "^3.6.4",
|
||||||
"webln": "^0.3.2",
|
"webln": "^0.3.2",
|
||||||
"webpack": "^5.88.2",
|
"webpack": "^5.88.2",
|
||||||
"workbox-navigation-preload": "^7.0.0",
|
"workbox-navigation-preload": "^7.0.0",
|
||||||
|
@ -92,7 +92,7 @@
|
||||||
"yup": "^1.2.0"
|
"yup": "^1.2.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "18.16.1"
|
"node": "18.17.0"
|
||||||
},
|
},
|
||||||
"standard": {
|
"standard": {
|
||||||
"parser": "@babel/eslint-parser",
|
"parser": "@babel/eslint-parser",
|
||||||
|
@ -107,8 +107,7 @@
|
||||||
"@babel/core": "^7.22.9",
|
"@babel/core": "^7.22.9",
|
||||||
"@babel/eslint-parser": "^7.22.9",
|
"@babel/eslint-parser": "^7.22.9",
|
||||||
"@next/eslint-plugin-next": "^13.4.12",
|
"@next/eslint-plugin-next": "^13.4.12",
|
||||||
"eslint": "^8.45.0",
|
"eslint": "^8.46.0",
|
||||||
"prisma": "^5.0.0",
|
|
||||||
"standard": "^17.1.0"
|
"standard": "^17.1.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,6 @@ import { ApolloProvider, gql } from '@apollo/client'
|
||||||
import { MeProvider } from '../components/me'
|
import { MeProvider } from '../components/me'
|
||||||
import PlausibleProvider from 'next-plausible'
|
import PlausibleProvider from 'next-plausible'
|
||||||
import getApolloClient from '../lib/apollo'
|
import getApolloClient from '../lib/apollo'
|
||||||
import NextNProgress from 'nextjs-progressbar'
|
|
||||||
import { PriceProvider } from '../components/price'
|
import { PriceProvider } from '../components/price'
|
||||||
import Head from 'next/head'
|
import Head from 'next/head'
|
||||||
import { useRouter } from 'next/dist/client/router'
|
import { useRouter } from 'next/dist/client/router'
|
||||||
|
@ -12,8 +11,13 @@ import { ShowModalProvider } from '../components/modal'
|
||||||
import ErrorBoundary from '../components/error-boundary'
|
import ErrorBoundary from '../components/error-boundary'
|
||||||
import { LightningProvider } from '../components/lightning'
|
import { LightningProvider } from '../components/lightning'
|
||||||
import { ServiceWorkerProvider } from '../components/serviceworker'
|
import { ServiceWorkerProvider } from '../components/serviceworker'
|
||||||
|
import { SSR } from '../lib/constants'
|
||||||
|
import NProgress from 'nprogress'
|
||||||
|
import 'nprogress/nprogress.css'
|
||||||
|
|
||||||
const SSR = typeof window === 'undefined'
|
NProgress.configure({
|
||||||
|
showSpinner: false
|
||||||
|
})
|
||||||
|
|
||||||
function writeQuery (client, apollo, data) {
|
function writeQuery (client, apollo, data) {
|
||||||
if (apollo && data) {
|
if (apollo && data) {
|
||||||
|
@ -21,8 +25,8 @@ function writeQuery (client, apollo, data) {
|
||||||
query: gql`${apollo.query}`,
|
query: gql`${apollo.query}`,
|
||||||
data,
|
data,
|
||||||
variables: apollo.variables,
|
variables: apollo.variables,
|
||||||
broadcast: !SSR,
|
overwrite: SSR,
|
||||||
overwrite: SSR
|
broadcast: false
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -32,48 +36,49 @@ function MyApp ({ Component, pageProps: { ...props } }) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const nprogressStart = (_, { shallow }) => !shallow && NProgress.start()
|
||||||
|
const nprogressDone = (_, { shallow }) => !shallow && NProgress.done()
|
||||||
|
|
||||||
|
router.events.on('routeChangeStart', nprogressStart)
|
||||||
|
router.events.on('routeChangeComplete', nprogressDone)
|
||||||
|
router.events.on('routeChangeError', nprogressDone)
|
||||||
|
|
||||||
|
if (!props?.apollo) return
|
||||||
// HACK: 'cause there's no way to tell Next to skip SSR
|
// HACK: 'cause there's no way to tell Next to skip SSR
|
||||||
// So every page load, we modify the route in browser history
|
// So every page load, we modify the route in browser history
|
||||||
// to point to the same page but without SSR, ie ?nodata=true
|
// to point to the same page but without SSR, ie ?nodata=true
|
||||||
// this nodata var will get passed to the server on back/foward and
|
// this nodata var will get passed to the server on back/foward and
|
||||||
// 1. prevent data from reloading and 2. perserve scroll
|
// 1. prevent data from reloading and 2. perserve scroll
|
||||||
// (2) is not possible while intercepting nav with beforePopState
|
// (2) is not possible while intercepting nav with beforePopState
|
||||||
if (router.query.nodata) return
|
|
||||||
|
|
||||||
router.replace({
|
router.replace({
|
||||||
pathname: router.pathname,
|
pathname: router.pathname,
|
||||||
query: { ...router.query, nodata: true }
|
query: { ...router.query, nodata: true }
|
||||||
}, router.asPath, { ...router.options, shallow: true }).catch((e) => {
|
}, router.asPath, { ...router.options, shallow: true }).catch((e) => {
|
||||||
// workaround for https://github.com/vercel/next.js/issues/37362
|
// workaround for https://github.com/vercel/next.js/issues/37362
|
||||||
if (!e.cancelled) {
|
if (!e.cancelled) {
|
||||||
|
console.log(e)
|
||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}, [router.pathname, router.query])
|
|
||||||
|
return () => {
|
||||||
|
router.events.off('routeChangeStart', nprogressStart)
|
||||||
|
router.events.off('routeChangeComplete', nprogressDone)
|
||||||
|
router.events.off('routeChangeError', nprogressDone)
|
||||||
|
}
|
||||||
|
}, [router.asPath, props?.apollo])
|
||||||
|
|
||||||
/*
|
/*
|
||||||
If we are on the client, we populate the apollo cache with the
|
If we are on the client, we populate the apollo cache with the
|
||||||
ssr data
|
ssr data
|
||||||
*/
|
*/
|
||||||
const { apollo, ssrData, me, price, ...otherProps } = props
|
const { apollo, ssrData, me, price, ...otherProps } = props
|
||||||
// if we are on the server, useEffect won't run
|
|
||||||
if (SSR && client) {
|
|
||||||
writeQuery(client, apollo, ssrData)
|
|
||||||
}
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
writeQuery(client, apollo, ssrData)
|
writeQuery(client, apollo, ssrData)
|
||||||
}, [client, apollo, ssrData])
|
}, [client, apollo, ssrData])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<NextNProgress
|
|
||||||
color='var(--bs-primary)'
|
|
||||||
startPosition={0.3}
|
|
||||||
stopDelayMs={200}
|
|
||||||
height={2}
|
|
||||||
showOnShallow={false}
|
|
||||||
options={{ showSpinner: false }}
|
|
||||||
/>
|
|
||||||
<Head>
|
<Head>
|
||||||
<meta name='viewport' content='initial-scale=1.0, width=device-width' />
|
<meta name='viewport' content='initial-scale=1.0, width=device-width' />
|
||||||
</Head>
|
</Head>
|
||||||
|
|
|
@ -23,8 +23,43 @@ class MyDocument extends Document {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<meta name='apple-mobile-web-app-capable' content='yes' />
|
<meta name='apple-mobile-web-app-capable' content='yes' />
|
||||||
<meta name='theme-color' content='#000000' />
|
<meta name='theme-color' content='#121214' />
|
||||||
<link rel='apple-touch-icon' href='/icons/icon_x192.png' />
|
<link rel='apple-touch-icon' href='/icons/icon_x192.png' />
|
||||||
|
<Script id='dark-mode-js' strategy='beforeInteractive'>
|
||||||
|
{`const handleThemeChange = (dark) => {
|
||||||
|
const root = window.document.documentElement
|
||||||
|
root.setAttribute('data-bs-theme', dark ? 'dark' : 'light')
|
||||||
|
}
|
||||||
|
|
||||||
|
const STORAGE_KEY = 'darkMode'
|
||||||
|
const PREFER_DARK_QUERY = '(prefers-color-scheme: dark)'
|
||||||
|
|
||||||
|
const getTheme = () => {
|
||||||
|
const mql = window.matchMedia(PREFER_DARK_QUERY)
|
||||||
|
const supportsColorSchemeQuery = mql.media === PREFER_DARK_QUERY
|
||||||
|
let localStorageTheme = null
|
||||||
|
try {
|
||||||
|
localStorageTheme = window.localStorage.getItem(STORAGE_KEY)
|
||||||
|
} catch (err) {}
|
||||||
|
const localStorageExists = localStorageTheme !== null
|
||||||
|
if (localStorageExists) {
|
||||||
|
localStorageTheme = JSON.parse(localStorageTheme)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (localStorageExists) {
|
||||||
|
return { user: true, dark: localStorageTheme }
|
||||||
|
} else if (supportsColorSchemeQuery) {
|
||||||
|
return { user: false, dark: mql.matches }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
(function () {
|
||||||
|
const { dark } = getTheme()
|
||||||
|
handleThemeChange(dark)
|
||||||
|
})()
|
||||||
|
}`}
|
||||||
|
</Script>
|
||||||
<link rel='apple-touch-startup-image' media='screen and (device-width: 430px) and (device-height: 932px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)' href='/splash/iPhone_14_Pro_Max_landscape.png' />
|
<link rel='apple-touch-startup-image' media='screen and (device-width: 430px) and (device-height: 932px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)' href='/splash/iPhone_14_Pro_Max_landscape.png' />
|
||||||
<link rel='apple-touch-startup-image' media='screen and (device-width: 393px) and (device-height: 852px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)' href='/splash/iPhone_14_Pro_landscape.png' />
|
<link rel='apple-touch-startup-image' media='screen and (device-width: 393px) and (device-height: 852px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)' href='/splash/iPhone_14_Pro_landscape.png' />
|
||||||
<link rel='apple-touch-startup-image' media='screen and (device-width: 428px) and (device-height: 926px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)' href='/splash/iPhone_14_Plus__iPhone_13_Pro_Max__iPhone_12_Pro_Max_landscape.png' />
|
<link rel='apple-touch-startup-image' media='screen and (device-width: 428px) and (device-height: 926px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)' href='/splash/iPhone_14_Plus__iPhone_13_Pro_Max__iPhone_12_Pro_Max_landscape.png' />
|
||||||
|
@ -59,7 +94,6 @@ class MyDocument extends Document {
|
||||||
<link rel='apple-touch-startup-image' media='screen and (device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)' href='/splash/10.2__iPad_portrait.png' />
|
<link rel='apple-touch-startup-image' media='screen and (device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)' href='/splash/10.2__iPad_portrait.png' />
|
||||||
<link rel='apple-touch-startup-image' media='screen and (device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)' href='/splash/9.7__iPad_Pro__7.9__iPad_mini__9.7__iPad_Air__9.7__iPad_portrait.png' />
|
<link rel='apple-touch-startup-image' media='screen and (device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)' href='/splash/9.7__iPad_Pro__7.9__iPad_mini__9.7__iPad_Air__9.7__iPad_portrait.png' />
|
||||||
<link rel='apple-touch-startup-image' media='screen and (device-width: 744px) and (device-height: 1133px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)' href='/splash/8.3__iPad_Mini_portrait.png' />
|
<link rel='apple-touch-startup-image' media='screen and (device-width: 744px) and (device-height: 1133px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)' href='/splash/8.3__iPad_Mini_portrait.png' />
|
||||||
<Script src={`${process.env.NEXT_PUBLIC_ASSET_PREFIX}/dark.js`} crossOrigin='' strategy='beforeInteractive' type='module' />
|
|
||||||
</Head>
|
</Head>
|
||||||
<body>
|
<body>
|
||||||
<Main />
|
<Main />
|
||||||
|
|
|
@ -130,7 +130,7 @@ const providers = [
|
||||||
},
|
},
|
||||||
profile: profile => {
|
profile: profile => {
|
||||||
return {
|
return {
|
||||||
...profile,
|
id: profile.id,
|
||||||
name: profile.login
|
name: profile.login
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -140,7 +140,7 @@ const providers = [
|
||||||
clientSecret: process.env.TWITTER_SECRET,
|
clientSecret: process.env.TWITTER_SECRET,
|
||||||
profile: profile => {
|
profile: profile => {
|
||||||
return {
|
return {
|
||||||
...profile,
|
id: profile.id,
|
||||||
name: profile.screen_name
|
name: profile.screen_name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,7 +22,7 @@ const apolloServer = new ApolloServer({
|
||||||
return (error, result) => {
|
return (error, result) => {
|
||||||
const end = process.hrtime.bigint()
|
const end = process.hrtime.bigint()
|
||||||
const ms = (end - start) / 1000000n
|
const ms = (end - start) / 1000000n
|
||||||
if (ms > 5) {
|
if (ms > 50) {
|
||||||
console.log(`Field ${info.parentType.name}.${info.fieldName} took ${ms}ms`)
|
console.log(`Field ${info.parentType.name}.${info.fieldName} took ${ms}ms`)
|
||||||
}
|
}
|
||||||
if (error) {
|
if (error) {
|
||||||
|
|
|
@ -47,7 +47,7 @@ export default async ({ query: { username, amount, nostr } }, res) => {
|
||||||
|
|
||||||
await serialize(models,
|
await serialize(models,
|
||||||
models.$queryRaw`SELECT * FROM create_invoice(${invoice.id}, ${invoice.request},
|
models.$queryRaw`SELECT * FROM create_invoice(${invoice.id}, ${invoice.request},
|
||||||
${expiresAt}, ${Number(amount)}, ${user.id}::INTEGER, ${noteStr || description})`)
|
${expiresAt}::timestamp, ${Number(amount)}, ${user.id}::INTEGER, ${noteStr || description})`)
|
||||||
|
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
pr: invoice.request,
|
pr: invoice.request,
|
||||||
|
|
|
@ -8,10 +8,10 @@ import { INVITE_FIELDS } from '../../fragments/invites'
|
||||||
import getSSRApolloClient from '../../api/ssrApollo'
|
import getSSRApolloClient from '../../api/ssrApollo'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { CenterLayout } from '../../components/layout'
|
import { CenterLayout } from '../../components/layout'
|
||||||
import { authOptions } from '../api/auth/[...nextauth]'
|
import { getAuthOptions } from '../api/auth/[...nextauth]'
|
||||||
|
|
||||||
export async function getServerSideProps ({ req, res, query: { id, error = null } }) {
|
export async function getServerSideProps ({ req, res, query: { id, error = null } }) {
|
||||||
const session = await getServerSession(req, res, authOptions(req))
|
const session = await getServerSession(req, res, getAuthOptions(req))
|
||||||
|
|
||||||
const client = await getSSRApolloClient({ req, res })
|
const client = await getSSRApolloClient({ req, res })
|
||||||
const { data } = await client.query({
|
const { data } = await client.query({
|
||||||
|
|
|
@ -7,6 +7,7 @@ import AccordianItem from '../../components/accordian-item'
|
||||||
import styles from '../../styles/invites.module.css'
|
import styles from '../../styles/invites.module.css'
|
||||||
import Invite from '../../components/invite'
|
import Invite from '../../components/invite'
|
||||||
import { inviteSchema } from '../../lib/validate'
|
import { inviteSchema } from '../../lib/validate'
|
||||||
|
import { SSR } from '../../lib/constants'
|
||||||
|
|
||||||
function InviteForm () {
|
function InviteForm () {
|
||||||
const [createInvite] = useMutation(
|
const [createInvite] = useMutation(
|
||||||
|
@ -93,7 +94,7 @@ export default function Invites () {
|
||||||
...InviteFields
|
...InviteFields
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`, { fetchPolicy: 'cache-and-network' })
|
`, SSR ? {} : { fetchPolicy: 'cache-and-network' })
|
||||||
|
|
||||||
const [active, inactive] = data && data.invites
|
const [active, inactive] = data && data.invites
|
||||||
? data.invites.reduce((result, invite) => {
|
? data.invites.reduce((result, invite) => {
|
||||||
|
|
|
@ -4,10 +4,13 @@ import { QrSkeleton } from '../../components/qr'
|
||||||
import { CenterLayout } from '../../components/layout'
|
import { CenterLayout } from '../../components/layout'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import { INVOICE } from '../../fragments/wallet'
|
import { INVOICE } from '../../fragments/wallet'
|
||||||
|
import { SSR } from '../../lib/constants'
|
||||||
|
|
||||||
export default function FullInvoice ({ id }) {
|
export default function FullInvoice ({ id }) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { data, error } = useQuery(INVOICE, {
|
const { data, error } = useQuery(INVOICE, SSR
|
||||||
|
? {}
|
||||||
|
: {
|
||||||
pollInterval: 1000,
|
pollInterval: 1000,
|
||||||
variables: { id: router.query.id },
|
variables: { id: router.query.id },
|
||||||
nextFetchPolicy: 'cache-and-network'
|
nextFetchPolicy: 'cache-and-network'
|
||||||
|
|
|
@ -37,7 +37,7 @@ function Ots ({ item }) {
|
||||||
: (
|
: (
|
||||||
<pre
|
<pre
|
||||||
className='mb-2 p-2 rounded'
|
className='mb-2 p-2 rounded'
|
||||||
style={{ whiteSpace: 'pre-wrap', wordWrap: 'break-word', border: '1px solid var(--theme-borderColor)', color: 'var(--theme-color)' }}
|
style={{ whiteSpace: 'pre-wrap', wordWrap: 'break-word', border: '1px solid var(--theme-borderColor)', color: 'var(--bs-body-color)' }}
|
||||||
>{itemString}
|
>{itemString}
|
||||||
</pre>)}
|
</pre>)}
|
||||||
<Button href={`/api/ots/preimage/${item.id}`} className='mt-1' variant='grey-medium'>download preimage</Button>
|
<Button href={`/api/ots/preimage/${item.id}`} className='mt-1' variant='grey-medium'>download preimage</Button>
|
||||||
|
|
|
@ -1,13 +1,27 @@
|
||||||
|
import { useEffect } from 'react'
|
||||||
import { getGetServerSideProps } from '../api/ssrApollo'
|
import { getGetServerSideProps } from '../api/ssrApollo'
|
||||||
import Layout from '../components/layout'
|
import Layout from '../components/layout'
|
||||||
import Notifications from '../components/notifications'
|
import Notifications, { NotificationAlert } from '../components/notifications'
|
||||||
import { NOTIFICATIONS } from '../fragments/notifications'
|
import { HAS_NOTIFICATIONS, NOTIFICATIONS } from '../fragments/notifications'
|
||||||
|
import { useApolloClient } from '@apollo/client'
|
||||||
|
|
||||||
export const getServerSideProps = getGetServerSideProps(NOTIFICATIONS)
|
export const getServerSideProps = getGetServerSideProps(NOTIFICATIONS)
|
||||||
|
|
||||||
export default function NotificationPage ({ ssrData }) {
|
export default function NotificationPage ({ ssrData }) {
|
||||||
|
const client = useApolloClient()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
client?.writeQuery({
|
||||||
|
query: HAS_NOTIFICATIONS,
|
||||||
|
data: {
|
||||||
|
hasNewNotes: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<Layout>
|
||||||
|
<NotificationAlert />
|
||||||
<Notifications ssrData={ssrData} />
|
<Notifications ssrData={ssrData} />
|
||||||
</Layout>
|
</Layout>
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { gql } from 'graphql-tag'
|
import { gql } from 'graphql-tag'
|
||||||
import { useEffect, useState } from 'react'
|
import { useMemo } from 'react'
|
||||||
import Button from 'react-bootstrap/Button'
|
import Button from 'react-bootstrap/Button'
|
||||||
import InputGroup from 'react-bootstrap/InputGroup'
|
import InputGroup from 'react-bootstrap/InputGroup'
|
||||||
import { getGetServerSideProps } from '../api/ssrApollo'
|
import { getGetServerSideProps } from '../api/ssrApollo'
|
||||||
|
@ -13,6 +13,7 @@ import { abbrNum } from '../lib/format'
|
||||||
import PageLoading from '../components/page-loading'
|
import PageLoading from '../components/page-loading'
|
||||||
import { useShowModal } from '../components/modal'
|
import { useShowModal } from '../components/modal'
|
||||||
import dynamic from 'next/dynamic'
|
import dynamic from 'next/dynamic'
|
||||||
|
import { SSR } from '../lib/constants'
|
||||||
|
|
||||||
const GrowthPieChart = dynamic(() => import('../components/charts').then(mod => mod.GrowthPieChart), {
|
const GrowthPieChart = dynamic(() => import('../components/charts').then(mod => mod.GrowthPieChart), {
|
||||||
loading: () => <div>Loading...</div>
|
loading: () => <div>Loading...</div>
|
||||||
|
@ -47,11 +48,7 @@ function midnight (tz) {
|
||||||
export const getServerSideProps = getGetServerSideProps(REWARDS)
|
export const getServerSideProps = getGetServerSideProps(REWARDS)
|
||||||
|
|
||||||
export function RewardLine ({ total }) {
|
export function RewardLine ({ total }) {
|
||||||
const [threshold, setThreshold] = useState(0)
|
const threshold = useMemo(() => midnight('America/Chicago'))
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setThreshold(midnight('America/Chicago'))
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -59,14 +56,14 @@ export function RewardLine ({ total }) {
|
||||||
{threshold &&
|
{threshold &&
|
||||||
<Countdown
|
<Countdown
|
||||||
date={threshold}
|
date={threshold}
|
||||||
renderer={props => <small className='text-monospace'> {props.formatted.hours}:{props.formatted.minutes}:{props.formatted.seconds}</small>}
|
renderer={props => <small className='text-monospace' suppressHydrationWarning> {props.formatted.hours}:{props.formatted.minutes}:{props.formatted.seconds}</small>}
|
||||||
/>}
|
/>}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Rewards ({ ssrData }) {
|
export default function Rewards ({ ssrData }) {
|
||||||
const { data } = useQuery(REWARDS, { pollInterval: 1000, nextFetchPolicy: 'cache-and-network' })
|
const { data } = useQuery(REWARDS, SSR ? {} : { pollInterval: 1000, nextFetchPolicy: 'cache-and-network' })
|
||||||
if (!data && !ssrData) return <PageLoading />
|
if (!data && !ssrData) return <PageLoading />
|
||||||
|
|
||||||
const { expectedRewards: { total, sources } } = data || ssrData
|
const { expectedRewards: { total, sources } } = data || ssrData
|
||||||
|
|
|
@ -25,7 +25,7 @@ function satusClass (status) {
|
||||||
|
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'CONFIRMED':
|
case 'CONFIRMED':
|
||||||
return ''
|
return 'text-reset'
|
||||||
case 'PENDING':
|
case 'PENDING':
|
||||||
return 'text-muted'
|
return 'text-muted'
|
||||||
default:
|
default:
|
||||||
|
@ -78,9 +78,9 @@ function Satus ({ status }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<span className='d-block'>
|
||||||
<Icon /><small className={`text-${color} fw-bold ms-2`}>{desc}</small>
|
<Icon /><small className={`text-${color} fw-bold ms-2`}>{desc}</small>
|
||||||
</div>
|
</span>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -110,10 +110,10 @@ function Detail ({ fact }) {
|
||||||
if (!fact.item) {
|
if (!fact.item) {
|
||||||
return (
|
return (
|
||||||
<div className='px-3'>
|
<div className='px-3'>
|
||||||
<div className={satusClass(fact.status)}>
|
<Link className={satusClass(fact.status)} href={`/${fact.type}s/${fact.factId}`}>
|
||||||
{fact.description || 'no invoice description'}
|
{fact.description || 'no invoice description'}
|
||||||
</div>
|
|
||||||
<Satus status={fact.status} />
|
<Satus status={fact.status} />
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -206,7 +206,7 @@ export default function Satistics ({ ssrData }) {
|
||||||
<div className={[styles.type, styles.head].join(' ')}>type</div>
|
<div className={[styles.type, styles.head].join(' ')}>type</div>
|
||||||
<div className={[styles.detail, styles.head].join(' ')}>detail</div>
|
<div className={[styles.detail, styles.head].join(' ')}>detail</div>
|
||||||
<div className={[styles.sats, styles.head].join(' ')}>sats</div>
|
<div className={[styles.sats, styles.head].join(' ')}>sats</div>
|
||||||
{facts.map(f => <Fact key={f.factId} fact={f} />)}
|
{facts.map(f => <Fact key={f.id} fact={f} />)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<MoreFooter cursor={cursor} count={facts?.length} fetchMore={fetchMore} Skeleton={PageLoading} />
|
<MoreFooter cursor={cursor} count={facts?.length} fetchMore={fetchMore} Skeleton={PageLoading} />
|
||||||
|
|
|
@ -245,7 +245,7 @@ export default function Settings ({ ssrData }) {
|
||||||
name='greeterMode'
|
name='greeterMode'
|
||||||
/>
|
/>
|
||||||
<AccordianItem
|
<AccordianItem
|
||||||
headerColor='var(--theme-color)'
|
headerColor='var(--bs-body-color)'
|
||||||
show={settings?.nostrPubkey}
|
show={settings?.nostrPubkey}
|
||||||
header={<h4 className='text-left'>nostr <small><a href='https://github.com/nostr-protocol/nips/blob/master/05.md' target='_blank' rel='noreferrer'>NIP-05</a></small></h4>}
|
header={<h4 className='text-left'>nostr <small><a href='https://github.com/nostr-protocol/nips/blob/master/05.md' target='_blank' rel='noreferrer'>NIP-05</a></small></h4>}
|
||||||
body={
|
body={
|
||||||
|
@ -372,14 +372,19 @@ function AuthMethods ({ methods }) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className='form-label mt-3'>auth methods</div>
|
<div className='form-label mt-3'>auth methods</div>
|
||||||
{err && <Alert variant='danger' onClose={() => {
|
{err && (
|
||||||
|
<Alert
|
||||||
|
variant='danger' onClose={() => {
|
||||||
const { pathname, query: { error, nodata, ...rest } } = router
|
const { pathname, query: { error, nodata, ...rest } } = router
|
||||||
router.replace({
|
router.replace({
|
||||||
pathname,
|
pathname,
|
||||||
query: { nodata, ...rest }
|
query: { nodata, ...rest }
|
||||||
}, { pathname, query: { ...rest } }, { shallow: true })
|
}, { pathname, query: { ...rest } }, { shallow: true })
|
||||||
setErr(undefined)
|
setErr(undefined)
|
||||||
}} dismissible>{err}</Alert>}
|
}} dismissible
|
||||||
|
>{err}
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
{providers?.map(provider => {
|
{providers?.map(provider => {
|
||||||
if (provider === 'email') {
|
if (provider === 'email') {
|
||||||
|
|
|
@ -14,6 +14,7 @@ import Alert from 'react-bootstrap/Alert'
|
||||||
import { CREATE_WITHDRAWL, SEND_TO_LNADDR } from '../fragments/wallet'
|
import { CREATE_WITHDRAWL, SEND_TO_LNADDR } from '../fragments/wallet'
|
||||||
import { getGetServerSideProps } from '../api/ssrApollo'
|
import { getGetServerSideProps } from '../api/ssrApollo'
|
||||||
import { amountSchema, lnAddrSchema, withdrawlSchema } from '../lib/validate'
|
import { amountSchema, lnAddrSchema, withdrawlSchema } from '../lib/validate'
|
||||||
|
import { SSR } from '../lib/constants'
|
||||||
|
|
||||||
export const getServerSideProps = getGetServerSideProps()
|
export const getServerSideProps = getGetServerSideProps()
|
||||||
|
|
||||||
|
@ -210,7 +211,7 @@ function LnQRWith ({ k1, encodedUrl }) {
|
||||||
k1
|
k1
|
||||||
}
|
}
|
||||||
}`
|
}`
|
||||||
const { data } = useQuery(query, { pollInterval: 1000, nextFetchPolicy: 'cache-and-network' })
|
const { data } = useQuery(query, SSR ? {} : { pollInterval: 1000, nextFetchPolicy: 'cache-and-network' })
|
||||||
|
|
||||||
if (data?.lnWith?.withdrawalId) {
|
if (data?.lnWith?.withdrawalId) {
|
||||||
router.push(`/withdrawals/${data.lnWith.withdrawalId}`)
|
router.push(`/withdrawals/${data.lnWith.withdrawalId}`)
|
||||||
|
|
|
@ -6,6 +6,7 @@ import InvoiceStatus from '../../components/invoice-status'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import { WITHDRAWL } from '../../fragments/wallet'
|
import { WITHDRAWL } from '../../fragments/wallet'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
|
import { SSR } from '../../lib/constants'
|
||||||
|
|
||||||
export default function Withdrawl () {
|
export default function Withdrawl () {
|
||||||
return (
|
return (
|
||||||
|
@ -31,7 +32,9 @@ export function WithdrawlSkeleton ({ status }) {
|
||||||
|
|
||||||
function LoadWithdrawl () {
|
function LoadWithdrawl () {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { loading, error, data } = useQuery(WITHDRAWL, {
|
const { loading, error, data } = useQuery(WITHDRAWL, SSR
|
||||||
|
? {}
|
||||||
|
: {
|
||||||
variables: { id: router.query.id },
|
variables: { id: router.query.id },
|
||||||
pollInterval: 1000,
|
pollInterval: 1000,
|
||||||
nextFetchPolicy: 'cache-and-network'
|
nextFetchPolicy: 'cache-and-network'
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
-- prod is set to utc by default but dev might not be
|
||||||
|
CREATE OR REPLACE FUNCTION set_timezone_utc_currentdb()
|
||||||
|
RETURNS INTEGER
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
BEGIN
|
||||||
|
EXECUTE 'ALTER DATABASE '||current_database()||' SET TIMEZONE TO ''UTC''';
|
||||||
|
return 0;
|
||||||
|
EXCEPTION WHEN OTHERS THEN
|
||||||
|
return 0;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
SELECT set_timezone_utc_currentdb();
|
|
@ -0,0 +1,3 @@
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "accounts" ADD COLUMN "oauth_token" TEXT,
|
||||||
|
ADD COLUMN "oauth_token_secret" TEXT;
|
|
@ -443,6 +443,10 @@ model Account {
|
||||||
id_token String?
|
id_token String?
|
||||||
session_state String?
|
session_state String?
|
||||||
|
|
||||||
|
// twitter oauth 1.0 needs these https://authjs.dev/reference/core/providers_twitter#notes
|
||||||
|
oauth_token String?
|
||||||
|
oauth_token_secret String?
|
||||||
|
|
||||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
@@unique([provider, providerAccountId])
|
@@unique([provider, providerAccountId])
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
const handleThemeChange = (dark) => {
|
||||||
|
const root = window.document.documentElement
|
||||||
|
root.setAttribute('data-bs-theme', dark ? 'dark' : 'light')
|
||||||
|
}
|
||||||
|
|
||||||
|
const STORAGE_KEY = 'darkMode'
|
||||||
|
const PREFER_DARK_QUERY = '(prefers-color-scheme: dark)'
|
||||||
|
|
||||||
|
const getTheme = () => {
|
||||||
|
const mql = window.matchMedia(PREFER_DARK_QUERY)
|
||||||
|
const supportsColorSchemeQuery = mql.media === PREFER_DARK_QUERY
|
||||||
|
let localStorageTheme = null
|
||||||
|
try {
|
||||||
|
localStorageTheme = window.localStorage.getItem(STORAGE_KEY)
|
||||||
|
} catch (err) {}
|
||||||
|
const localStorageExists = localStorageTheme !== null
|
||||||
|
if (localStorageExists) {
|
||||||
|
localStorageTheme = JSON.parse(localStorageTheme)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (localStorageExists) {
|
||||||
|
return { user: true, dark: localStorageTheme }
|
||||||
|
} else if (supportsColorSchemeQuery) {
|
||||||
|
return { user: false, dark: mql.matches }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
(function () {
|
||||||
|
const { dark } = getTheme()
|
||||||
|
handleThemeChange(dark)
|
||||||
|
})()
|
||||||
|
}
|
112
public/dark.js
112
public/dark.js
|
@ -1,112 +0,0 @@
|
||||||
const COLORS = {
|
|
||||||
light: {
|
|
||||||
body: '#f5f5f7',
|
|
||||||
color: '#212529',
|
|
||||||
navbarVariant: 'light',
|
|
||||||
navLink: 'rgba(0, 0, 0, 0.55)',
|
|
||||||
navLinkFocus: 'rgba(0, 0, 0, 0.7)',
|
|
||||||
navLinkActive: 'rgba(0, 0, 0, 0.9)',
|
|
||||||
borderColor: '#ced4da',
|
|
||||||
inputBg: '#ffffff',
|
|
||||||
inputDisabledBg: '#e9ecef',
|
|
||||||
dropdownItemColor: 'rgba(0, 0, 0, 0.7)',
|
|
||||||
dropdownItemColorHover: 'rgba(0, 0, 0, 0.9)',
|
|
||||||
commentBg: 'rgba(0, 0, 0, 0.03)',
|
|
||||||
clickToContextColor: 'rgba(0, 0, 0, 0.07)',
|
|
||||||
brandColor: 'rgba(0, 0, 0, 0.9)',
|
|
||||||
grey: '#707070',
|
|
||||||
link: '#007cbe',
|
|
||||||
toolbarActive: 'rgba(0, 0, 0, 0.10)',
|
|
||||||
toolbarHover: 'rgba(0, 0, 0, 0.20)',
|
|
||||||
toolbar: '#ffffff',
|
|
||||||
quoteBar: 'rgb(206, 208, 212)',
|
|
||||||
quoteColor: 'rgb(101, 103, 107)',
|
|
||||||
linkHover: '#004a72',
|
|
||||||
linkVisited: '#537587'
|
|
||||||
},
|
|
||||||
dark: {
|
|
||||||
body: '#000000',
|
|
||||||
inputBg: '#000000',
|
|
||||||
inputDisabledBg: '#000000',
|
|
||||||
navLink: 'rgba(255, 255, 255, 0.55)',
|
|
||||||
navLinkFocus: 'rgba(255, 255, 255, 0.75)',
|
|
||||||
navLinkActive: 'rgba(255, 255, 255, 0.9)',
|
|
||||||
borderColor: 'rgba(255, 255, 255, 0.5)',
|
|
||||||
dropdownItemColor: 'rgba(255, 255, 255, 0.7)',
|
|
||||||
dropdownItemColorHover: 'rgba(255, 255, 255, 0.9)',
|
|
||||||
commentBg: 'rgba(255, 255, 255, 0.04)',
|
|
||||||
clickToContextColor: 'rgba(255, 255, 255, 0.2)',
|
|
||||||
color: '#f8f9fa',
|
|
||||||
brandColor: 'var(--bs-primary)',
|
|
||||||
grey: '#969696',
|
|
||||||
link: '#2e99d1',
|
|
||||||
toolbarActive: 'rgba(255, 255, 255, 0.10)',
|
|
||||||
toolbarHover: 'rgba(255, 255, 255, 0.20)',
|
|
||||||
toolbar: '#3e3f3f',
|
|
||||||
quoteBar: 'rgb(158, 159, 163)',
|
|
||||||
quoteColor: 'rgb(141, 144, 150)',
|
|
||||||
linkHover: '#007cbe',
|
|
||||||
linkVisited: '#56798E'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleThemeChange = (dark) => {
|
|
||||||
const root = window.document.documentElement
|
|
||||||
const colors = COLORS[dark ? 'dark' : 'light']
|
|
||||||
Object.entries(colors).forEach(([varName, value]) => {
|
|
||||||
const cssVarName = `--theme-${varName}`
|
|
||||||
root.style.setProperty(cssVarName, value)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const STORAGE_KEY = 'darkMode'
|
|
||||||
const PREFER_DARK_QUERY = '(prefers-color-scheme: dark)'
|
|
||||||
|
|
||||||
export const getTheme = () => {
|
|
||||||
const mql = window.matchMedia(PREFER_DARK_QUERY)
|
|
||||||
const supportsColorSchemeQuery = mql.media === PREFER_DARK_QUERY
|
|
||||||
let localStorageTheme = null
|
|
||||||
try {
|
|
||||||
localStorageTheme = window.localStorage.getItem(STORAGE_KEY)
|
|
||||||
} catch (err) {}
|
|
||||||
const localStorageExists = localStorageTheme !== null
|
|
||||||
if (localStorageExists) {
|
|
||||||
localStorageTheme = JSON.parse(localStorageTheme)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (localStorageExists) {
|
|
||||||
return { user: true, dark: localStorageTheme }
|
|
||||||
} else if (supportsColorSchemeQuery) {
|
|
||||||
return { user: false, dark: mql.matches }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const setTheme = (dark) => {
|
|
||||||
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(dark))
|
|
||||||
handleThemeChange(dark)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const listenForThemeChange = (onChange) => {
|
|
||||||
const mql = window.matchMedia(PREFER_DARK_QUERY)
|
|
||||||
mql.onchange = mql => {
|
|
||||||
const { user, dark } = getTheme()
|
|
||||||
if (!user) {
|
|
||||||
handleThemeChange(dark)
|
|
||||||
onChange({ user, dark })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
window.onstorage = e => {
|
|
||||||
if (e.key === STORAGE_KEY) {
|
|
||||||
const dark = JSON.parse(e.newValue)
|
|
||||||
setTheme(dark)
|
|
||||||
onChange({ user: true, dark })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
(function () {
|
|
||||||
const { dark } = getTheme()
|
|
||||||
handleThemeChange(dark)
|
|
||||||
})()
|
|
||||||
}
|
|
BIN
public/giphy.gif
BIN
public/giphy.gif
Binary file not shown.
Before Width: | Height: | Size: 936 KiB |
|
@ -89,7 +89,7 @@
|
||||||
],
|
],
|
||||||
"display": "standalone",
|
"display": "standalone",
|
||||||
"orientation": "any",
|
"orientation": "any",
|
||||||
"theme_color": "#000000",
|
"theme_color": "#121214",
|
||||||
"background_color": "#FADA5E",
|
"background_color": "#FADA5E",
|
||||||
"id": "/",
|
"id": "/",
|
||||||
"start_url": "/",
|
"start_url": "/",
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 14 KiB |
|
@ -23,7 +23,10 @@ $theme-colors: (
|
||||||
"grey-darkmode": #8c8c8c,
|
"grey-darkmode": #8c8c8c,
|
||||||
);
|
);
|
||||||
|
|
||||||
$body-bg: #f5f5f7;
|
$body-bg: #fcfcff;
|
||||||
|
$body-bg-dark: #121214;
|
||||||
|
$body-color: #212529;
|
||||||
|
$body-color-dark: #f0f0f0;
|
||||||
$border-radius: .4rem;
|
$border-radius: .4rem;
|
||||||
$enable-transitions: false;
|
$enable-transitions: false;
|
||||||
$enable-gradients: false;
|
$enable-gradients: false;
|
||||||
|
@ -41,6 +44,10 @@ $btn-font-weight: bold;
|
||||||
$btn-focus-width: 0;
|
$btn-focus-width: 0;
|
||||||
$btn-border-width: 0;
|
$btn-border-width: 0;
|
||||||
$btn-focus-box-shadow: none;
|
$btn-focus-box-shadow: none;
|
||||||
|
$form-invalid-border-color: #c03221;
|
||||||
|
$form-invalid-border-color-dark: #c03221;
|
||||||
|
$form-invalid-color: #c03221;
|
||||||
|
$form-invalid-color-dark: #c03221;
|
||||||
$alert-border-width: 0;
|
$alert-border-width: 0;
|
||||||
$close-text-shadow: none;
|
$close-text-shadow: none;
|
||||||
$close-color: inherit;
|
$close-color: inherit;
|
||||||
|
@ -69,11 +76,12 @@ $nav-tabs-link-hover-border-color: transparent;
|
||||||
$nav-tabs-link-active-border-color: #ced4da #ced4da $nav-tabs-link-active-bg;
|
$nav-tabs-link-active-border-color: #ced4da #ced4da $nav-tabs-link-active-bg;
|
||||||
$form-check-input-checked-color: var(--bs-primary);
|
$form-check-input-checked-color: var(--bs-primary);
|
||||||
$form-check-input-checked-bg-color: var(--bs-primary);
|
$form-check-input-checked-bg-color: var(--bs-primary);
|
||||||
$popover-bg: var(--theme-body);
|
$popover-bg: var(--bs-body-bg);
|
||||||
$form-check-input-checked-color: #000;
|
$form-check-input-checked-color: #000;
|
||||||
$tooltip-bg: #5c8001;
|
$tooltip-bg: #5c8001;
|
||||||
$form-select-indicator-color: #808080;
|
$form-select-indicator-color: #808080;
|
||||||
$form-select-indicator: url("data:image/svg+xml, %3Csvg fill='#{$form-select-indicator-color}' 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");
|
$form-select-indicator: url("data:image/svg+xml, %3Csvg fill='#{$form-select-indicator-color}' 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");
|
||||||
|
$form-select-indicator-dark: url("data:image/svg+xml, %3Csvg fill='#{$form-select-indicator-color}' 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");
|
||||||
$form-select-bg-position: right .25rem center;
|
$form-select-bg-position: right .25rem center;
|
||||||
$form-select-bg-size: 1.5rem;
|
$form-select-bg-size: 1.5rem;
|
||||||
$popover-body-padding-y: .5rem;
|
$popover-body-padding-y: .5rem;
|
||||||
|
@ -81,6 +89,49 @@ $popover-max-width: 320px !default;
|
||||||
$popover-border-color: var(--theme-borderColor);
|
$popover-border-color: var(--theme-borderColor);
|
||||||
$grid-gutter-width: 2rem;
|
$grid-gutter-width: 2rem;
|
||||||
|
|
||||||
|
:root, [data-bs-theme=light] {
|
||||||
|
--theme-navLink: rgba(0, 0, 0, 0.55);
|
||||||
|
--theme-navLinkFocus: rgba(0, 0, 0, 0.7);
|
||||||
|
--theme-navLinkActive: rgba(0, 0, 0, 0.9);
|
||||||
|
--theme-borderColor: #ced4da;
|
||||||
|
--theme-inputBg: #ffffff;
|
||||||
|
--theme-inputDisabledBg: #e9ecef;
|
||||||
|
--theme-dropdownItemColor: rgba(0, 0, 0, 0.7);
|
||||||
|
--theme-dropdownItemColorHover: rgba(0, 0, 0, 0.9);
|
||||||
|
--theme-commentBg: rgba(0, 0, 0, 0.03);
|
||||||
|
--theme-clickToContextColor: rgba(0, 0, 0, 0.07);
|
||||||
|
--theme-brandColor: rgba(0, 0, 0, 0.9);
|
||||||
|
--theme-grey: #707070;
|
||||||
|
--theme-link: #007cbe;
|
||||||
|
--theme-quoteBar: rgb(206, 208, 212);
|
||||||
|
--theme-linkHover: #004a72;
|
||||||
|
--theme-linkVisited: #53758;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-bs-theme=dark] {
|
||||||
|
color-scheme: dark;
|
||||||
|
--theme-inputBg: #121211;
|
||||||
|
--theme-inputDisabledBg: #121211;
|
||||||
|
--theme-navLink: rgba(255, 255, 255, 0.55);
|
||||||
|
--theme-navLinkFocus: rgba(255, 255, 255, 0.75);
|
||||||
|
--theme-navLinkActive: rgba(255, 255, 255, 0.9);
|
||||||
|
--theme-borderColor: rgba(255, 255, 255, 0.5);
|
||||||
|
--theme-dropdownItemColor: rgba(255, 255, 255, 0.7);
|
||||||
|
--theme-dropdownItemColorHover: rgba(255, 255, 255, 0.9);
|
||||||
|
--theme-commentBg: rgba(255, 255, 255, 0.025);
|
||||||
|
--theme-clickToContextColor: rgba(255, 255, 255, 0.1);
|
||||||
|
--theme-brandColor: var(--bs-primary);
|
||||||
|
--theme-grey: #969696;
|
||||||
|
--theme-link: #2e99d1;
|
||||||
|
--theme-toolbarActive: rgba(255, 255, 255, 0.10);
|
||||||
|
--theme-toolbarHover: rgba(255, 255, 255, 0.20);
|
||||||
|
--theme-toolbar: #3e3f3f;
|
||||||
|
--theme-quoteBar: rgb(158, 159, 163);
|
||||||
|
--theme-quoteColor: rgb(141, 144, 150);
|
||||||
|
--theme-linkHover: #007cbe;
|
||||||
|
--theme-linkVisited: #56798E;
|
||||||
|
}
|
||||||
|
|
||||||
@import '../node_modules/bootstrap/scss/bootstrap.scss';
|
@import '../node_modules/bootstrap/scss/bootstrap.scss';
|
||||||
|
|
||||||
@media screen and (min-width: 899px) {
|
@media screen and (min-width: 899px) {
|
||||||
|
@ -95,6 +146,15 @@ $grid-gutter-width: 2rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#nprogress .bar {
|
||||||
|
background: var(--bs-primary) !important;
|
||||||
|
height: 2px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#nprogress .peg {
|
||||||
|
box-shadow: 0 0 10px var(--bs-primary), 0 0 5px var(--bs-primary) !important;
|
||||||
|
}
|
||||||
|
|
||||||
.standalone {
|
.standalone {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
@ -145,7 +205,7 @@ dl {
|
||||||
mark {
|
mark {
|
||||||
background-color: #fada5e5e;
|
background-color: #fada5e5e;
|
||||||
padding: 0 0.2rem;
|
padding: 0 0.2rem;
|
||||||
color: var(--theme-color);
|
color: var(--bs-body-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-sm th,
|
.table-sm th,
|
||||||
|
@ -172,8 +232,8 @@ mark {
|
||||||
}
|
}
|
||||||
|
|
||||||
.table {
|
.table {
|
||||||
color: var(--theme-color);
|
color: var(--bs-body-color);
|
||||||
background-color: var(--theme-body);
|
background-color: var(--bs-body-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.table th,
|
.table th,
|
||||||
|
@ -183,13 +243,13 @@ mark {
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-hover tbody tr:hover {
|
.table-hover tbody tr:hover {
|
||||||
color: var(--theme-color);
|
color: var(--bs-body-color);
|
||||||
background-color: var(--theme-clickToContextColor);
|
background-color: var(--theme-clickToContextColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
html, body {
|
html, body {
|
||||||
background: var(--theme-body);
|
background: var(--bs-body-bg) !important;
|
||||||
color: var(--theme-color);
|
color: var(--bs-body-color) !important;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
min-height: 100svh;
|
min-height: 100svh;
|
||||||
}
|
}
|
||||||
|
@ -214,7 +274,7 @@ select,
|
||||||
div[contenteditable],
|
div[contenteditable],
|
||||||
.form-control {
|
.form-control {
|
||||||
background-color: var(--theme-inputBg);
|
background-color: var(--theme-inputBg);
|
||||||
color: var(--theme-color);
|
color: var(--bs-body-color);
|
||||||
border-color: var(--theme-borderColor);
|
border-color: var(--theme-borderColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -256,7 +316,7 @@ select:focus {
|
||||||
div[contenteditable]:focus,
|
div[contenteditable]:focus,
|
||||||
.form-control:focus {
|
.form-control:focus {
|
||||||
background-color: var(--theme-inputBg);
|
background-color: var(--theme-inputBg);
|
||||||
color: var(--theme-color);
|
color: var(--bs-body-color);
|
||||||
outline: 0;
|
outline: 0;
|
||||||
box-shadow: 0 0 0 0.2rem rgb(250 218 94 / 25%);
|
box-shadow: 0 0 0 0.2rem rgb(250 218 94 / 25%);
|
||||||
}
|
}
|
||||||
|
@ -297,11 +357,6 @@ div[contenteditable]:disabled,
|
||||||
fill: #212529;
|
fill: #212529;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fresh {
|
|
||||||
background-color: var(--theme-clickToContextColor);
|
|
||||||
border-radius: .4rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-content {
|
.modal-content {
|
||||||
background-color: var(--theme-inputBg);
|
background-color: var(--theme-inputBg);
|
||||||
border-color: var(--theme-borderColor);
|
border-color: var(--theme-borderColor);
|
||||||
|
@ -406,7 +461,7 @@ footer {
|
||||||
.input-group-text {
|
.input-group-text {
|
||||||
background-color: var(--theme-clickToContextColor);
|
background-color: var(--theme-clickToContextColor);
|
||||||
border-color: var(--theme-borderColor);
|
border-color: var(--theme-borderColor);
|
||||||
color: var(--theme-color);
|
color: var(--bs-body-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
textarea.form-control,
|
textarea.form-control,
|
||||||
|
@ -445,8 +500,8 @@ div[contenteditable] {
|
||||||
}
|
}
|
||||||
|
|
||||||
.popover {
|
.popover {
|
||||||
color: var(--theme-color);
|
color: var(--bs-body-color);
|
||||||
background-color: var(--theme-body);
|
background-color: var(--bs-body-bg);
|
||||||
border-color: var(--theme-borderColor);
|
border-color: var(--theme-borderColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -455,7 +510,7 @@ div[contenteditable] {
|
||||||
}
|
}
|
||||||
|
|
||||||
.popover .arrow::after {
|
.popover .arrow::after {
|
||||||
border-top-color: var(--theme-body);
|
border-top-color: var(--bs-body-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -566,7 +621,7 @@ div[contenteditable]:focus,
|
||||||
}
|
}
|
||||||
|
|
||||||
.fill-theme-color {
|
.fill-theme-color {
|
||||||
fill: var(--theme-color);
|
fill: var(--bs-body-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.fill-warning {
|
.fill-warning {
|
||||||
|
@ -617,6 +672,34 @@ div[contenteditable]:focus,
|
||||||
animation: flash 2s linear 1;
|
animation: flash 2s linear 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes outline {
|
||||||
|
from {
|
||||||
|
box-shadow: inset 0 0 1px 1px var(--bs-info);
|
||||||
|
}
|
||||||
|
90% {
|
||||||
|
box-shadow: inset 0 0 1px 1px var(--bs-info);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
box-shadow: inset 0 0 0px 0px var(--bs-info);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.outline-it {
|
||||||
|
animation: outline 3s linear 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.outline-new-comment {
|
||||||
|
box-shadow: inset 0 0 1px 1px rgba(0, 123, 190, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.outline-new-comment.outline-new-comment-unset {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.outline-new-comment .outline-new-comment {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes spin {
|
@keyframes spin {
|
||||||
0% {
|
0% {
|
||||||
transform: rotate(0deg);
|
transform: rotate(0deg);
|
||||||
|
@ -679,14 +762,15 @@ div[contenteditable]:focus,
|
||||||
|
|
||||||
.tooltip-inner {
|
.tooltip-inner {
|
||||||
padding: 0.1rem 0.3rem;
|
padding: 0.1rem 0.3rem;
|
||||||
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.popover {
|
.popover {
|
||||||
.popover-header {
|
.popover-header {
|
||||||
background-color: var(--theme-body);
|
background-color: var(--bs-body-bg);
|
||||||
color: var(--theme-color);
|
color: var(--bs-body-color);
|
||||||
}
|
}
|
||||||
.popover-body {
|
.popover-body {
|
||||||
color: var(--theme-color);
|
color: var(--bs-body-color);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
# syntax=docker/dockerfile:1
|
# syntax=docker/dockerfile:1
|
||||||
|
|
||||||
FROM node:16.16.0-bullseye
|
FROM node:18.17.0-bullseye
|
||||||
|
|
||||||
ENV NODE_ENV=development
|
ENV NODE_ENV=development
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,7 @@ function earn ({ models }) {
|
||||||
console.log('running', name)
|
console.log('running', name)
|
||||||
|
|
||||||
// compute how much sn earned today
|
// compute how much sn earned today
|
||||||
let [{ sum }] = await models.$queryRaw`
|
const [{ sum: actSum }] = await models.$queryRaw`
|
||||||
SELECT coalesce(sum("ItemAct".msats - coalesce("ReferralAct".msats, 0)), 0) as sum
|
SELECT coalesce(sum("ItemAct".msats - coalesce("ReferralAct".msats, 0)), 0) as sum
|
||||||
FROM "ItemAct"
|
FROM "ItemAct"
|
||||||
JOIN "Item" ON "ItemAct"."itemId" = "Item".id
|
JOIN "Item" ON "ItemAct"."itemId" = "Item".id
|
||||||
|
@ -21,7 +21,24 @@ function earn ({ models }) {
|
||||||
SELECT coalesce(sum(sats), 0) as sum
|
SELECT coalesce(sum(sats), 0) as sum
|
||||||
FROM "Donation"
|
FROM "Donation"
|
||||||
WHERE created_at > now_utc() - INTERVAL '1 day'`
|
WHERE created_at > now_utc() - INTERVAL '1 day'`
|
||||||
sum += donatedSum * 1000n
|
|
||||||
|
// XXX prisma returns wonky types from raw queries ... so be extra
|
||||||
|
// careful with them
|
||||||
|
const sum = Number(actSum) + (Number(donatedSum) * 1000)
|
||||||
|
|
||||||
|
if (sum <= 0) {
|
||||||
|
console.log('done', name, 'no sats to award today')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// extra sanity check on rewards ... if it more than 1m sats, we
|
||||||
|
// probably have a bug somewhere
|
||||||
|
if (sum > 1000000000) {
|
||||||
|
console.log('done', name, 'error: too many sats to award today')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(name, 'giving away', sum, 'msats')
|
||||||
|
|
||||||
/*
|
/*
|
||||||
How earnings (used to) work:
|
How earnings (used to) work:
|
||||||
|
@ -36,11 +53,6 @@ function earn ({ models }) {
|
||||||
Now: 100% of earnings go to zappers of the top 21% of posts/comments
|
Now: 100% of earnings go to zappers of the top 21% of posts/comments
|
||||||
*/
|
*/
|
||||||
|
|
||||||
if (sum <= 0) {
|
|
||||||
console.log('done', name, 'no earning')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// get earners { userId, id, type, rank, proportion }
|
// get earners { userId, id, type, rank, proportion }
|
||||||
const earners = await models.$queryRawUnsafe(`
|
const earners = await models.$queryRawUnsafe(`
|
||||||
WITH item_ratios AS (
|
WITH item_ratios AS (
|
||||||
|
@ -88,19 +100,21 @@ function earn ({ models }) {
|
||||||
const now = new Date(new Date().getTime())
|
const now = new Date(new Date().getTime())
|
||||||
|
|
||||||
// this is just a sanity check because it seems like a good idea
|
// this is just a sanity check because it seems like a good idea
|
||||||
let total = 0n
|
let total = 0
|
||||||
|
|
||||||
// for each earner, serialize earnings
|
// for each earner, serialize earnings
|
||||||
// we do this for each earner because we don't need to serialize
|
// we do this for each earner because we don't need to serialize
|
||||||
// all earner updates together
|
// all earner updates together
|
||||||
earners.forEach(async earner => {
|
earners.forEach(async earner => {
|
||||||
const earnings = BigInt(Math.floor(earner.proportion * sum))
|
const earnings = Math.floor(parseFloat(earner.proportion) * sum)
|
||||||
total += earnings
|
total += earnings
|
||||||
if (total > sum) {
|
if (total > sum) {
|
||||||
console.log('total exceeds sum', name)
|
console.log(name, 'total exceeds sum', total, '>', sum)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('stacker', earner.userId, 'earned', earnings)
|
||||||
|
|
||||||
if (earnings > 0) {
|
if (earnings > 0) {
|
||||||
await serialize(models,
|
await serialize(models,
|
||||||
models.$executeRaw`SELECT earn(${earner.userId}::INTEGER, ${earnings},
|
models.$executeRaw`SELECT earn(${earner.userId}::INTEGER, ${earnings},
|
||||||
|
|
|
@ -60,7 +60,7 @@ function checkWithdrawal ({ boss, models, lnd }) {
|
||||||
const fee = Number(wdrwl.payment.fee_mtokens)
|
const fee = Number(wdrwl.payment.fee_mtokens)
|
||||||
const paid = Number(wdrwl.payment.mtokens) - fee
|
const paid = Number(wdrwl.payment.mtokens) - fee
|
||||||
await serialize(models, models.$executeRaw`
|
await serialize(models, models.$executeRaw`
|
||||||
SELECT confirm_withdrawl(${id}, ${paid}, ${fee})`)
|
SELECT confirm_withdrawl(${id}::INTEGER, ${paid}, ${fee})`)
|
||||||
} else if (wdrwl?.is_failed || notFound) {
|
} else if (wdrwl?.is_failed || notFound) {
|
||||||
let status = 'UNKNOWN_FAILURE'
|
let status = 'UNKNOWN_FAILURE'
|
||||||
if (wdrwl?.failed.is_insufficient_balance) {
|
if (wdrwl?.failed.is_insufficient_balance) {
|
||||||
|
@ -73,7 +73,7 @@ function checkWithdrawal ({ boss, models, lnd }) {
|
||||||
status = 'ROUTE_NOT_FOUND'
|
status = 'ROUTE_NOT_FOUND'
|
||||||
}
|
}
|
||||||
await serialize(models, models.$executeRaw`
|
await serialize(models, models.$executeRaw`
|
||||||
SELECT reverse_withdrawl(${id}, ${status})`)
|
SELECT reverse_withdrawl(${id}::INTEGER, ${status}::"WithdrawlStatus")`)
|
||||||
} else {
|
} else {
|
||||||
// we need to requeue to check again in 5 seconds
|
// we need to requeue to check again in 5 seconds
|
||||||
await boss.send('checkWithdrawal', { id, hash }, walletOptions)
|
await boss.send('checkWithdrawal', { id, hash }, walletOptions)
|
||||||
|
|
Loading…
Reference in New Issue