Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import "server-only"
import { NextRequest, NextResponse } from "next/server"
import { pool } from "@/lib/fast-db"

function isValidWalletAddress(address: string): boolean {
return /^0x[a-fA-F0-9]{40}$/.test(address)
}

/**
* GET /api/user-community-activity/[wallet_address]/[entity]
* Returns the latest activity for a single entity for the user. 404 if no record exists.
*/
export async function GET(
_request: NextRequest,
{ params }: { params: Promise<{ wallet_address: string; entity: string }> }
) {
try {
const { wallet_address, entity } = await params

if (!wallet_address) {
return NextResponse.json({ error: "Wallet address is required" }, { status: 400 })
}

if (!isValidWalletAddress(wallet_address)) {
return NextResponse.json({ error: "Invalid wallet address format" }, { status: 400 })
}

const entityTrimmed = typeof entity === "string" ? entity.trim() : ""
if (!entityTrimmed) {
return NextResponse.json({ error: "Entity is required" }, { status: 400 })
}

const address = wallet_address.toLowerCase()

const { rows } = await pool.query(
`SELECT entity, activity, chainid, created_at
FROM (
SELECT entity, activity, chainid, created_at,
ROW_NUMBER() OVER (ORDER BY created_at DESC) AS rn
FROM user_activity
WHERE user_address = $1 AND entity = $2
) sub
WHERE rn = 1`,
[address, entityTrimmed]
)

if (rows.length === 0) {
return NextResponse.json({ error: "Activity not found" }, { status: 404 })
}

const row = rows[0]
return NextResponse.json({
entity: row.entity,
activity: row.activity === true,
chainId: row.chainid ?? null,
createdAt: row.created_at,
})
} catch (err) {
console.error("Error fetching user activity for entity:", err)
return NextResponse.json({ error: "Database query failed" }, { status: 500 })
}
}
20 changes: 20 additions & 0 deletions src/app/api/user-community-activity/entities/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import "server-only"
import { NextResponse } from "next/server"
import { pool } from "@/lib/fast-db"

/**
* GET /api/user-community-activity/entities
* Returns distinct entities that have at least one activity record.
*/
export async function GET() {
try {
const { rows } = await pool.query(`SELECT DISTINCT entity FROM user_activity ORDER BY entity`)

const entities = rows.map((row) => row.entity)

return NextResponse.json({ entities })
} catch (err) {
console.error("Error fetching entities:", err)
return NextResponse.json({ error: "Database query failed" }, { status: 500 })
}
}
66 changes: 66 additions & 0 deletions src/app/api/user-community-activity/entity/[entity]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import "server-only"
import { NextRequest, NextResponse } from "next/server"
import { pool } from "@/lib/fast-db"

/**
* GET /api/user-community-activity/entity/[entity]
* Returns users who have verified activity for the given entity.
* Query params: limit (default 50, max 200), chainId (optional)
*/
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ entity: string }> }
) {
try {
const { entity } = await params
const entityTrimmed = typeof entity === "string" ? entity.trim() : ""

if (!entityTrimmed) {
return NextResponse.json({ error: "Entity is required" }, { status: 400 })
}

const { searchParams } = new URL(request.url)
const limitParam = searchParams.get("limit")
const limitRaw = limitParam ? parseInt(limitParam, 10) : 50
const limit = Math.min(Math.max(limitRaw, 1), 200)

const chainIdParam = searchParams.get("chainId")
const chainId = chainIdParam !== null && chainIdParam !== "" ? parseInt(chainIdParam, 10) : null

const values: unknown[] = [entityTrimmed]
let paramIndex = 2

const chainFilter =
chainId !== null && !Number.isNaN(chainId) ? `AND chainid = $${paramIndex++}` : ""
if (chainId !== null && !Number.isNaN(chainId)) {
values.push(chainId)
}
values.push(limit)

const { rows } = await pool.query(
`SELECT user_address, activity, chainid, created_at
FROM (
SELECT user_address, activity, chainid, created_at,
ROW_NUMBER() OVER (PARTITION BY user_address ORDER BY created_at DESC) AS rn
FROM user_activity
WHERE entity = $1 ${chainFilter}
) sub
WHERE rn = 1
ORDER BY created_at DESC
LIMIT $${paramIndex}`,
values
)

const users = rows.map((row) => ({
wallet: row.user_address,
activity: row.activity === true,
chainId: row.chainid ?? null,
createdAt: row.created_at,
}))

return NextResponse.json({ users })
} catch (err) {
console.error("Error fetching users by entity:", err)
return NextResponse.json({ error: "Database query failed" }, { status: 500 })
}
}
69 changes: 69 additions & 0 deletions src/app/api/user-community-activity/stats/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import "server-only"
import { NextRequest, NextResponse } from "next/server"
import { pool } from "@/lib/fast-db"

/**
* GET /api/user-community-activity/stats
* Returns aggregate stats for user_activity. Query params: entity (optional), chainId (optional)
*/
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url)
const entity = searchParams.get("entity")?.trim() || null
const chainIdParam = searchParams.get("chainId")
const chainId = chainIdParam !== null && chainIdParam !== "" ? parseInt(chainIdParam, 10) : null

const filters: string[] = []
const values: unknown[] = []
let paramIndex = 1

if (entity) {
filters.push(`entity = $${paramIndex}`)
values.push(entity)
paramIndex++
}
if (chainId !== null && !Number.isNaN(chainId)) {
filters.push(`chainid = $${paramIndex}`)
values.push(chainId)
paramIndex++
}

const whereClause = filters.length > 0 ? `WHERE ${filters.join(" AND ")}` : ""

const [totalRes, usersRes, byEntityRes, byChainRes] = await Promise.all([
pool.query(`SELECT COUNT(*)::int AS total FROM user_activity ${whereClause}`, values),
pool.query(
`SELECT COUNT(DISTINCT user_address)::int AS count FROM user_activity ${whereClause}`,
values
),
pool.query(
`SELECT entity, COUNT(*)::int AS count FROM user_activity ${whereClause} GROUP BY entity ORDER BY count DESC`,
values
),
pool.query(
`SELECT chainid, COUNT(*)::int AS count FROM user_activity ${whereClause} GROUP BY chainid ORDER BY count DESC`,
values
),
])

const byEntity: Record<string, number> = {}
for (const row of byEntityRes.rows) {
byEntity[row.entity] = row.count
}

const byChain: Record<number, number> = {}
for (const row of byChainRes.rows) {
byChain[row.chainid] = row.count
}

return NextResponse.json({
totalRecords: totalRes.rows[0]?.total ?? 0,
uniqueUsers: usersRes.rows[0]?.count ?? 0,
byEntity,
byChain,
})
} catch (err) {
console.error("Error fetching user activity stats:", err)
return NextResponse.json({ error: "Database query failed" }, { status: 500 })
}
}
Loading