fix tag case, holdings summary, autocomplete, dedicated page for positions
Build Images and Deploy / Update-PROD-Stack (push) Successful in 1m18s
Build Images and Deploy / Update-PROD-Stack (push) Successful in 1m18s
This commit is contained in:
@@ -0,0 +1,20 @@
|
|||||||
|
import { prisma } from '@/lib/prisma'
|
||||||
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
|
|
||||||
|
export async function GET(req: NextRequest) {
|
||||||
|
const q = req.nextUrl.searchParams.get('q')?.trim().replace(/^#/, '').toLowerCase()
|
||||||
|
if (!q || q.length < 1) return NextResponse.json([])
|
||||||
|
|
||||||
|
const results = await prisma.hashtag.findMany({
|
||||||
|
where: {
|
||||||
|
isActive: true,
|
||||||
|
isBanned: false,
|
||||||
|
tag: { startsWith: q },
|
||||||
|
},
|
||||||
|
orderBy: { currentPrice: 'desc' },
|
||||||
|
take: 8,
|
||||||
|
select: { tag: true, displayTag: true, currentPrice: true },
|
||||||
|
})
|
||||||
|
|
||||||
|
return NextResponse.json(results)
|
||||||
|
}
|
||||||
+60
-2
@@ -4,6 +4,7 @@ import { authOptions } from '@/lib/auth'
|
|||||||
import { HashtagCard } from '@/components/HashtagCard'
|
import { HashtagCard } from '@/components/HashtagCard'
|
||||||
import { TrendingUp, Users, Hash } from 'lucide-react'
|
import { TrendingUp, Users, Hash } from 'lucide-react'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
|
import { formatPnl, pnlColor } from '@/lib/utils'
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic'
|
export const dynamic = 'force-dynamic'
|
||||||
export const revalidate = 0
|
export const revalidate = 0
|
||||||
@@ -37,9 +38,30 @@ async function getStats() {
|
|||||||
return { userCount, hashtagCount, tradeCount, topHashtags, recentTrades }
|
return { userCount, hashtagCount, tradeCount, topHashtags, recentTrades }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getHoldings(userId: string) {
|
||||||
|
const positions = await prisma.position.findMany({
|
||||||
|
where: { userId, shares: { gt: 0 } },
|
||||||
|
include: { hashtag: { select: { tag: true, displayTag: true, currentPrice: true } } },
|
||||||
|
})
|
||||||
|
if (positions.length === 0) return null
|
||||||
|
const withPnl = positions.map((p) => ({
|
||||||
|
...p,
|
||||||
|
pnl:
|
||||||
|
p.positionType === 'LONG'
|
||||||
|
? (p.hashtag.currentPrice - p.avgBuyPrice) * p.shares
|
||||||
|
: (p.avgBuyPrice - p.hashtag.currentPrice) * p.shares,
|
||||||
|
}))
|
||||||
|
const sorted = [...withPnl].sort((a, b) => b.pnl - a.pnl)
|
||||||
|
return {
|
||||||
|
biggestGain: sorted[0].pnl > 0 ? sorted[0] : null,
|
||||||
|
biggestLoss: sorted[sorted.length - 1].pnl < 0 ? sorted[sorted.length - 1] : null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default async function HomePage() {
|
export default async function HomePage() {
|
||||||
const [session, { userCount, hashtagCount, tradeCount, topHashtags, recentTrades }] =
|
const session = await getServerSession(authOptions)
|
||||||
await Promise.all([getServerSession(authOptions), getStats()])
|
const [{ userCount, hashtagCount, tradeCount, topHashtags, recentTrades }, holdings] =
|
||||||
|
await Promise.all([getStats(), session ? getHoldings(session.user.id) : Promise.resolve(null)])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-10">
|
<div className="space-y-10">
|
||||||
@@ -95,6 +117,42 @@ export default async function HomePage() {
|
|||||||
<StatCard icon={<TrendingUp className="h-5 w-5 text-indigo-400" />} label="Trades executed" value={tradeCount.toLocaleString()} />
|
<StatCard icon={<TrendingUp className="h-5 w-5 text-indigo-400" />} label="Trades executed" value={tradeCount.toLocaleString()} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Holdings summary — biggest gain + biggest loss for signed-in users */}
|
||||||
|
{holdings && (holdings.biggestGain ?? holdings.biggestLoss) && (
|
||||||
|
<section>
|
||||||
|
<h2 className="text-lg font-semibold mb-4 flex items-center gap-2">
|
||||||
|
<TrendingUp className="h-5 w-5 text-indigo-400" />
|
||||||
|
Your top positions
|
||||||
|
</h2>
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
|
{holdings.biggestGain && (
|
||||||
|
<Link
|
||||||
|
href={`/hashtag/${holdings.biggestGain.hashtag.tag}`}
|
||||||
|
className="bg-surface-card border border-surface-border rounded-xl p-4 hover:border-indigo-500/50 transition-colors"
|
||||||
|
>
|
||||||
|
<p className="text-xs text-slate-500 mb-1">Biggest gain</p>
|
||||||
|
<p className="font-semibold">#{holdings.biggestGain.hashtag.displayTag}</p>
|
||||||
|
<p className={`text-sm font-medium mt-1 ${pnlColor(holdings.biggestGain.pnl)}`}>
|
||||||
|
{formatPnl(holdings.biggestGain.pnl)}
|
||||||
|
</p>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
{holdings.biggestLoss && (
|
||||||
|
<Link
|
||||||
|
href={`/hashtag/${holdings.biggestLoss.hashtag.tag}`}
|
||||||
|
className="bg-surface-card border border-surface-border rounded-xl p-4 hover:border-indigo-500/50 transition-colors"
|
||||||
|
>
|
||||||
|
<p className="text-xs text-slate-500 mb-1">Biggest loss</p>
|
||||||
|
<p className="font-semibold">#{holdings.biggestLoss.hashtag.displayTag}</p>
|
||||||
|
<p className={`text-sm font-medium mt-1 ${pnlColor(holdings.biggestLoss.pnl)}`}>
|
||||||
|
{formatPnl(holdings.biggestLoss.pnl)}
|
||||||
|
</p>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Top hashtags */}
|
{/* Top hashtags */}
|
||||||
{topHashtags.length > 0 && (
|
{topHashtags.length > 0 && (
|
||||||
<section>
|
<section>
|
||||||
|
|||||||
@@ -0,0 +1,150 @@
|
|||||||
|
import { prisma } from '@/lib/prisma'
|
||||||
|
import { getServerSession } from 'next-auth'
|
||||||
|
import { authOptions } from '@/lib/auth'
|
||||||
|
import { redirect } from 'next/navigation'
|
||||||
|
import { formatCurrency, formatNumber, formatPnl, pnlColor } from '@/lib/utils'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import { Coins } from 'lucide-react'
|
||||||
|
|
||||||
|
export const dynamic = 'force-dynamic'
|
||||||
|
|
||||||
|
function Sparkline({ prices }: { prices: number[] }) {
|
||||||
|
if (prices.length < 2) return <span className="text-slate-600 text-xs">—</span>
|
||||||
|
const min = Math.min(...prices)
|
||||||
|
const max = Math.max(...prices)
|
||||||
|
const range = max - min || 1
|
||||||
|
const w = 80
|
||||||
|
const h = 28
|
||||||
|
const pts = prices
|
||||||
|
.map((p, i) => {
|
||||||
|
const x = (i / (prices.length - 1)) * w
|
||||||
|
const y = h - ((p - min) / range) * (h - 4) - 2
|
||||||
|
return `${x},${y}`
|
||||||
|
})
|
||||||
|
.join(' ')
|
||||||
|
const up = prices[prices.length - 1] >= prices[0]
|
||||||
|
return (
|
||||||
|
<svg width={w} height={h} style={{ overflow: 'visible' }}>
|
||||||
|
<polyline
|
||||||
|
points={pts}
|
||||||
|
fill="none"
|
||||||
|
stroke={up ? '#34d399' : '#f87171'}
|
||||||
|
strokeWidth="1.5"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function PositionsPage() {
|
||||||
|
const session = await getServerSession(authOptions)
|
||||||
|
if (!session) redirect('/auth/signin')
|
||||||
|
|
||||||
|
const positions = await prisma.position.findMany({
|
||||||
|
where: { userId: session.user.id, shares: { gt: 0 } },
|
||||||
|
orderBy: { updatedAt: 'desc' },
|
||||||
|
include: {
|
||||||
|
hashtag: {
|
||||||
|
select: {
|
||||||
|
tag: true,
|
||||||
|
displayTag: true,
|
||||||
|
currentPrice: true,
|
||||||
|
priceHistory: {
|
||||||
|
orderBy: { recordedAt: 'asc' },
|
||||||
|
take: 20,
|
||||||
|
select: { price: true },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="max-w-4xl mx-auto space-y-6">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Coins className="h-6 w-6 text-indigo-400" />
|
||||||
|
<h1 className="text-2xl font-bold">Open Positions</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{positions.length === 0 ? (
|
||||||
|
<div className="text-center py-16 text-slate-500">
|
||||||
|
<Coins className="h-12 w-12 mx-auto mb-3 opacity-30" />
|
||||||
|
<p>You have no open positions.</p>
|
||||||
|
<Link href="/" className="text-indigo-400 hover:text-indigo-300 text-sm mt-2 inline-block">
|
||||||
|
Browse trending hashtags →
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="bg-surface-card border border-surface-border rounded-xl overflow-hidden">
|
||||||
|
{/* Header row */}
|
||||||
|
<div className="grid grid-cols-[1fr_auto_auto_auto_auto_auto_auto] gap-4 px-4 py-2 text-xs text-slate-500 uppercase tracking-wider border-b border-surface-border">
|
||||||
|
<span>Hashtag</span>
|
||||||
|
<span className="text-right">Shares</span>
|
||||||
|
<span className="text-right">Avg buy</span>
|
||||||
|
<span className="text-right">Current</span>
|
||||||
|
<span className="text-right">Cost basis</span>
|
||||||
|
<span className="text-right">Value</span>
|
||||||
|
<span className="text-right">P&L</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="divide-y divide-surface-border">
|
||||||
|
{positions.map((pos) => {
|
||||||
|
const pnl =
|
||||||
|
pos.positionType === 'LONG'
|
||||||
|
? (pos.hashtag.currentPrice - pos.avgBuyPrice) * pos.shares
|
||||||
|
: (pos.avgBuyPrice - pos.hashtag.currentPrice) * pos.shares
|
||||||
|
const costBasis = pos.avgBuyPrice * pos.shares
|
||||||
|
const currentValue = pos.hashtag.currentPrice * pos.shares
|
||||||
|
const pnlPct = costBasis > 0 ? (pnl / costBasis) * 100 : 0
|
||||||
|
const sparkPrices = pos.hashtag.priceHistory.map((h) => h.price)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={pos.id}
|
||||||
|
className="grid grid-cols-[1fr_auto_auto_auto_auto_auto_auto] gap-4 items-center px-4 py-3"
|
||||||
|
>
|
||||||
|
{/* Hashtag + type + sparkline */}
|
||||||
|
<div className="flex items-center gap-3 min-w-0">
|
||||||
|
<Sparkline prices={sparkPrices} />
|
||||||
|
<div className="min-w-0">
|
||||||
|
<Link
|
||||||
|
href={`/hashtag/${pos.hashtag.tag}`}
|
||||||
|
className="font-medium hover:text-indigo-300 truncate block"
|
||||||
|
>
|
||||||
|
#{pos.hashtag.displayTag}
|
||||||
|
</Link>
|
||||||
|
<span
|
||||||
|
className={`text-xs font-medium px-1.5 py-0.5 rounded ${
|
||||||
|
pos.positionType === 'LONG'
|
||||||
|
? 'bg-emerald-500/15 text-emerald-400'
|
||||||
|
: 'bg-red-500/15 text-red-400'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{pos.positionType}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span className="text-right text-sm">{formatNumber(pos.shares)}</span>
|
||||||
|
<span className="text-right text-sm">{formatCurrency(pos.avgBuyPrice)}</span>
|
||||||
|
<span className="text-right text-sm">{formatCurrency(pos.hashtag.currentPrice)}</span>
|
||||||
|
<span className="text-right text-sm text-slate-400">{formatCurrency(costBasis)}</span>
|
||||||
|
<span className="text-right text-sm">{formatCurrency(currentValue)}</span>
|
||||||
|
|
||||||
|
<div className="text-right">
|
||||||
|
<p className={`text-sm font-medium ${pnlColor(pnl)}`}>{formatPnl(pnl)}</p>
|
||||||
|
<p className={`text-xs ${pnlColor(pnlPct)}`}>
|
||||||
|
{pnlPct >= 0 ? '+' : ''}
|
||||||
|
{pnlPct.toFixed(1)}%
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -107,6 +107,14 @@ export default async function ProfilePage({ params }: Props) {
|
|||||||
<h2 className="text-lg font-semibold mb-4 flex items-center gap-2">
|
<h2 className="text-lg font-semibold mb-4 flex items-center gap-2">
|
||||||
<Coins className="h-5 w-5 text-indigo-400" />
|
<Coins className="h-5 w-5 text-indigo-400" />
|
||||||
Open positions
|
Open positions
|
||||||
|
{isOwn && (
|
||||||
|
<Link
|
||||||
|
href="/positions"
|
||||||
|
className="ml-auto text-sm font-normal text-indigo-400 hover:text-indigo-300"
|
||||||
|
>
|
||||||
|
View all →
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
</h2>
|
</h2>
|
||||||
<div className="bg-surface-card border border-surface-border rounded-xl overflow-hidden">
|
<div className="bg-surface-card border border-surface-border rounded-xl overflow-hidden">
|
||||||
<div className="divide-y divide-surface-border">
|
<div className="divide-y divide-surface-border">
|
||||||
|
|||||||
@@ -3,15 +3,20 @@
|
|||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { useSession, signOut } from 'next-auth/react'
|
import { useSession, signOut } from 'next-auth/react'
|
||||||
import { TrendingUp, Search, User, LogOut, Shield, Trophy } from 'lucide-react'
|
import { TrendingUp, Search, User, LogOut, Shield, Trophy } from 'lucide-react'
|
||||||
import { useState } from 'react'
|
import { useState, useRef } from 'react'
|
||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from 'next/navigation'
|
||||||
import { formatCurrency } from '@/lib/utils'
|
import { formatCurrency } from '@/lib/utils'
|
||||||
import { normalizeTag } from '@/lib/utils'
|
import { normalizeTag } from '@/lib/utils'
|
||||||
|
|
||||||
|
type Suggestion = { tag: string; displayTag: string; currentPrice: number }
|
||||||
|
|
||||||
export function Navbar() {
|
export function Navbar() {
|
||||||
const { data: session } = useSession()
|
const { data: session } = useSession()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const [query, setQuery] = useState('')
|
const [query, setQuery] = useState('')
|
||||||
|
const [suggestions, setSuggestions] = useState<Suggestion[]>([])
|
||||||
|
const [showSuggestions, setShowSuggestions] = useState(false)
|
||||||
|
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||||
|
|
||||||
function handleSearch(e: React.FormEvent) {
|
function handleSearch(e: React.FormEvent) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
@@ -19,9 +24,33 @@ export function Navbar() {
|
|||||||
if (tag) {
|
if (tag) {
|
||||||
router.push(`/hashtag/${tag}`)
|
router.push(`/hashtag/${tag}`)
|
||||||
setQuery('')
|
setQuery('')
|
||||||
|
setSuggestions([])
|
||||||
|
setShowSuggestions(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleQueryChange(e: React.ChangeEvent<HTMLInputElement>) {
|
||||||
|
const val = e.target.value
|
||||||
|
setQuery(val)
|
||||||
|
if (debounceRef.current) clearTimeout(debounceRef.current)
|
||||||
|
const normalized = val.replace(/^#/, '').trim().toLowerCase()
|
||||||
|
if (normalized.length < 1) {
|
||||||
|
setSuggestions([])
|
||||||
|
setShowSuggestions(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
debounceRef.current = setTimeout(async () => {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/hashtags/search?q=${encodeURIComponent(normalized)}`)
|
||||||
|
if (res.ok) {
|
||||||
|
const data: Suggestion[] = await res.json()
|
||||||
|
setSuggestions(data)
|
||||||
|
setShowSuggestions(data.length > 0)
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
}, 300)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav className="border-b border-surface-border bg-surface-card">
|
<nav className="border-b border-surface-border bg-surface-card">
|
||||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
@@ -39,10 +68,32 @@ export function Navbar() {
|
|||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={query}
|
value={query}
|
||||||
onChange={(e) => setQuery(e.target.value)}
|
onChange={handleQueryChange}
|
||||||
|
onBlur={() => setTimeout(() => setShowSuggestions(false), 150)}
|
||||||
|
onFocus={() => suggestions.length > 0 && setShowSuggestions(true)}
|
||||||
placeholder="#hashtag"
|
placeholder="#hashtag"
|
||||||
className="w-full bg-surface border border-surface-border rounded-lg pl-9 pr-3 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
className="w-full bg-surface border border-surface-border rounded-lg pl-9 pr-3 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
||||||
/>
|
/>
|
||||||
|
{showSuggestions && suggestions.length > 0 && (
|
||||||
|
<div className="absolute top-full left-0 right-0 mt-1 bg-surface-card border border-surface-border rounded-lg shadow-xl z-50 overflow-hidden">
|
||||||
|
{suggestions.map((s) => (
|
||||||
|
<button
|
||||||
|
key={s.tag}
|
||||||
|
type="button"
|
||||||
|
onMouseDown={() => {
|
||||||
|
router.push(`/hashtag/${s.tag}`)
|
||||||
|
setQuery('')
|
||||||
|
setSuggestions([])
|
||||||
|
setShowSuggestions(false)
|
||||||
|
}}
|
||||||
|
className="w-full flex items-center justify-between px-3 py-2 text-sm hover:bg-surface-border transition-colors"
|
||||||
|
>
|
||||||
|
<span className="font-medium">#{s.displayTag}</span>
|
||||||
|
<span className="text-slate-400 text-xs">{formatCurrency(s.currentPrice)}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
|||||||
+18
-2
@@ -60,7 +60,7 @@ export async function getPostsPerHour(tag: string): Promise<number> {
|
|||||||
*/
|
*/
|
||||||
export async function getPostsData(
|
export async function getPostsData(
|
||||||
tag: string,
|
tag: string,
|
||||||
): Promise<{ postsPerHour: number; relatedTags: string[] }> {
|
): Promise<{ postsPerHour: number; relatedTags: string[]; displayTag?: string }> {
|
||||||
const maxPages = parseInt(process.env.MAX_PAGES_PER_HASHTAG ?? '5', 10)
|
const maxPages = parseInt(process.env.MAX_PAGES_PER_HASHTAG ?? '5', 10)
|
||||||
|
|
||||||
let allPosts: MastodonPost[] = []
|
let allPosts: MastodonPost[] = []
|
||||||
@@ -110,5 +110,21 @@ export async function getPostsData(
|
|||||||
.slice(0, 10)
|
.slice(0, 10)
|
||||||
.map(([name]) => name)
|
.map(([name]) => name)
|
||||||
|
|
||||||
return { postsPerHour, relatedTags }
|
// Derive the most common casing variant for the queried tag itself
|
||||||
|
const casingCounts = new Map<string, number>()
|
||||||
|
for (const post of allPosts) {
|
||||||
|
for (const t of post.tags ?? []) {
|
||||||
|
if (t.name.toLowerCase() === lowerTag) {
|
||||||
|
casingCounts.set(t.name, (casingCounts.get(t.name) ?? 0) + 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let displayTag: string | undefined
|
||||||
|
if (casingCounts.size > 0) {
|
||||||
|
const total = [...casingCounts.values()].reduce((a, b) => a + b, 0)
|
||||||
|
const [topVariant, topCount] = [...casingCounts.entries()].sort((a, b) => b[1] - a[1])[0]
|
||||||
|
if (topCount / total >= 0.5) displayTag = topVariant
|
||||||
|
}
|
||||||
|
|
||||||
|
return { postsPerHour, relatedTags, displayTag }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ export { default } from 'next-auth/middleware'
|
|||||||
export const config = {
|
export const config = {
|
||||||
matcher: [
|
matcher: [
|
||||||
'/profile/:path*',
|
'/profile/:path*',
|
||||||
|
'/positions',
|
||||||
'/admin/:path*',
|
'/admin/:path*',
|
||||||
'/api/trade/:path*',
|
'/api/trade/:path*',
|
||||||
'/api/research/:path*',
|
'/api/research/:path*',
|
||||||
|
|||||||
+3
-1
@@ -62,8 +62,9 @@ const priceWorker = new Worker(
|
|||||||
|
|
||||||
let postsPerHour = 0
|
let postsPerHour = 0
|
||||||
let relatedTags: string[] = []
|
let relatedTags: string[] = []
|
||||||
|
let displayTag: string | undefined
|
||||||
try {
|
try {
|
||||||
;({ postsPerHour, relatedTags } = await getPostsData(tag))
|
;({ postsPerHour, relatedTags, displayTag } = await getPostsData(tag))
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`[price] mastodon error for #${tag}:`, err)
|
console.error(`[price] mastodon error for #${tag}:`, err)
|
||||||
throw err // BullMQ will retry
|
throw err // BullMQ will retry
|
||||||
@@ -101,6 +102,7 @@ const priceWorker = new Worker(
|
|||||||
currentPrice: newPrice,
|
currentPrice: newPrice,
|
||||||
zeroCount: 0,
|
zeroCount: 0,
|
||||||
lastUpdated: new Date(),
|
lastUpdated: new Date(),
|
||||||
|
...(displayTag && displayTag !== hashtag.displayTag ? { displayTag } : {}),
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
prisma.priceHistory.create({
|
prisma.priceHistory.create({
|
||||||
|
|||||||
Reference in New Issue
Block a user