diff --git a/app/api/spark-credits/[userId]/history/route.ts b/app/api/spark-credits/[userId]/history/route.ts new file mode 100644 index 0000000..8e15fca --- /dev/null +++ b/app/api/spark-credits/[userId]/history/route.ts @@ -0,0 +1,22 @@ +import { NextRequest, NextResponse } from "next/server"; +import { SparkCreditsService } from "@/lib/services/spark-credits"; + +export async function GET( + request: NextRequest, + context: { params: Promise<{ userId: string }> }, +) { + const { userId } = await context.params; + const { searchParams } = request.nextUrl; + const rawLimit = Number(searchParams.get("limit") ?? "20"); + const rawOffset = Number(searchParams.get("offset") ?? "0"); + const limit = Number.isFinite(rawLimit) + ? Math.min(Math.max(rawLimit, 1), 100) + : 20; + const offset = Number.isFinite(rawOffset) ? Math.max(rawOffset, 0) : 0; + const history = await SparkCreditsService.getCreditHistory( + userId, + limit, + offset, + ); + return NextResponse.json(history); +} diff --git a/app/api/spark-credits/[userId]/route.ts b/app/api/spark-credits/[userId]/route.ts new file mode 100644 index 0000000..ef7b741 --- /dev/null +++ b/app/api/spark-credits/[userId]/route.ts @@ -0,0 +1,11 @@ +import { NextRequest, NextResponse } from "next/server"; +import { SparkCreditsService } from "@/lib/services/spark-credits"; + +export async function GET( + _request: NextRequest, + context: { params: Promise<{ userId: string }> }, +) { + const { userId } = await context.params; + const balance = await SparkCreditsService.getBalance(userId); + return NextResponse.json(balance); +} diff --git a/app/profile/[userId]/page.tsx b/app/profile/[userId]/page.tsx index 66e1b30..bb9b6bc 100644 --- a/app/profile/[userId]/page.tsx +++ b/app/profile/[userId]/page.tsx @@ -2,9 +2,13 @@ import { useContributorReputation } from "@/hooks/use-reputation"; import { useBounties } from "@/hooks/use-bounties"; +import { useSparkCreditsHistory } from "@/hooks/use-spark-credits"; import { ReputationCard } from "@/components/reputation/reputation-card"; import { CompletionHistory } from "@/components/reputation/completion-history"; import { MyClaims, type MyClaim } from "@/components/reputation/my-claims"; +import { CreditHistory } from "@/components/reputation/credit-history"; +import { CreditExplainer } from "@/components/reputation/credit-explainer"; +import { CreditBalance } from "@/components/reputation/credit-balance"; import { EarningsSummary, type EarningsSummary as EarningsSummaryType, @@ -38,6 +42,12 @@ export default function ProfilePage() { isError: historyError, } = useCompletionHistory(userId); + const { + data: creditsHistory, + isLoading: creditsHistoryLoading, + isError: creditsHistoryError, + } = useSparkCreditsHistory(userId); + const records = completionData?.records ?? []; const myClaims = useMemo(() => { @@ -206,6 +216,12 @@ export default function ProfilePage() { > My Claims + + Spark Credits + @@ -248,6 +264,24 @@ export default function ProfilePage() { )} + +
+

Spark Credits

+ +
+ {creditsHistoryLoading ? ( + + ) : creditsHistoryError ? ( +
+ Unable to load credit history. +
+ ) : ( +
+ + +
+ )} +
diff --git a/components/global-navbar.tsx b/components/global-navbar.tsx index ef50d75..903e253 100644 --- a/components/global-navbar.tsx +++ b/components/global-navbar.tsx @@ -6,6 +6,7 @@ import { usePathname } from "next/navigation"; import { SearchCommand } from "@/components/search-command"; import { NavRankBadge } from "@/components/leaderboard/nav-rank-badge"; import { WalletSheet } from "@/components/wallet/wallet-sheet"; +import { CreditBalance } from "@/components/reputation/credit-balance"; import { mockWalletInfo } from "@/lib/mock-wallet"; import { Button } from "@/components/ui/button"; import { ModeToggle } from "./mode-toggle"; @@ -61,8 +62,8 @@ export function GlobalNavbar() { href="/transparency" className={`transition-colors hover:text-foreground/80 ${ pathname.startsWith("/transparency") - ? "text-foreground" - : "text-foreground/60" + ? "text-foreground" + : "text-foreground/60" }`} > Transparency @@ -92,6 +93,7 @@ export function GlobalNavbar() {
+ {/* TODO: Replace with actual auth user ID */} ; + } + + if (isError) { + return ( +
+ + +
+ ); + } + + const balance = data?.balance ?? 0; + const isLow = balance <= 1; + + return ( + + + +
+ + {balance} +
+
+ +

Spark Credits

+

+ Each FCFS bounty claim costs 1 Spark Credit. Earn credits by + completing bounties. +

+ {isLow && balance === 0 && ( +

+ No credits remaining — complete a bounty to earn more. +

+ )} + {isLow && balance === 1 && ( +

+ Only 1 credit left — consider completing a bounty soon. +

+ )} +
+
+
+ ); +} diff --git a/components/reputation/credit-explainer.tsx b/components/reputation/credit-explainer.tsx new file mode 100644 index 0000000..ffd0ada --- /dev/null +++ b/components/reputation/credit-explainer.tsx @@ -0,0 +1,102 @@ +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { cn } from "@/lib/utils"; +import { Zap, Trophy, CheckCircle, AlertCircle } from "lucide-react"; + +interface CreditExplainerProps { + className?: string; +} + +export function CreditExplainer({ className }: CreditExplainerProps) { + return ( + + + + + About Spark Credits + + + +

+ Spark Credits are application credits that prevent spam while ensuring + quality contributors always have opportunities. +

+ + {/* How to earn */} +
+

+ + How to Earn Credits +

+
    +
  • + + Complete a bounty — earn 1 credit per completion +
  • +
  • + + Win a competition bounty — earn bonus credits +
  • +
+
+ + {/* Costs */} +
+

+ + Credit Costs +

+
+
+ Claim a FCFS bounty + + 1 + credit + +
+
+ Enter a competition + Free +
+
+
+ + {/* Tips */} +
+

+ + Tips +

+
    +
  • + + • + + + Complete bounties before claiming new ones to keep your credit + balance healthy. + +
  • +
  • + + • + + + Your credit balance is shown in the navbar so you always know + your current standing. + +
  • +
  • + + • + + + Credits cannot be purchased — they can only be earned through + contributions. + +
  • +
+
+
+
+ ); +} diff --git a/components/reputation/credit-history.tsx b/components/reputation/credit-history.tsx new file mode 100644 index 0000000..4340fcd --- /dev/null +++ b/components/reputation/credit-history.tsx @@ -0,0 +1,106 @@ +import { SparkCreditEvent } from "@/types/spark-credits"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { Badge } from "@/components/ui/badge"; +import { formatDistanceToNow } from "date-fns"; +import { cn } from "@/lib/utils"; +import { Zap, TrendingUp, TrendingDown } from "lucide-react"; + +interface CreditHistoryProps { + events: SparkCreditEvent[]; + className?: string; +} + +export function CreditHistory({ events, className }: CreditHistoryProps) { + if (events.length === 0) { + return ( +
+ No credit activity yet. +
+ ); + } + + return ( +
+ +
+ {events.map((event) => { + const isEarned = event.type === "EARNED"; + return ( +
+
+
+
+ {isEarned ? ( + + ) : ( + + )} +
+
+
+ {event.bountyTitle ?? event.description} +
+
+ + {isEarned ? "Earned" : "Spent"} + + + + {formatDistanceToNow(new Date(event.createdAt), { + addSuffix: true, + })} + +
+
+
+ +
+
+ {isEarned ? "+" : "-"} + + {event.amount} +
+
+ + {event.balance} balance +
+
+
+
+ ); + })} +
+
+
+ ); +} diff --git a/hooks/use-spark-credits.ts b/hooks/use-spark-credits.ts new file mode 100644 index 0000000..d5259c6 --- /dev/null +++ b/hooks/use-spark-credits.ts @@ -0,0 +1,33 @@ +import { useQuery } from "@tanstack/react-query"; +import { sparkCreditsApi } from "@/lib/api/spark-credits"; + +export const SPARK_CREDITS_KEYS = { + all: ["sparkCredits"] as const, + balance: (userId: string) => + [...SPARK_CREDITS_KEYS.all, "balance", userId] as const, + history: (userId: string, limit?: number) => + [...SPARK_CREDITS_KEYS.all, "history", userId, limit] as const, +}; + +export function useSparkCreditsBalance(userId: string) { + return useQuery({ + queryKey: SPARK_CREDITS_KEYS.balance(userId), + queryFn: () => sparkCreditsApi.fetchBalance(userId), + enabled: !!userId, + staleTime: 1000 * 60 * 2, // 2 minutes + }); +} + +const DEFAULT_HISTORY_LIMIT = 20; + +export function useSparkCreditsHistory( + userId: string, + limit = DEFAULT_HISTORY_LIMIT, +) { + return useQuery({ + queryKey: SPARK_CREDITS_KEYS.history(userId, limit), + queryFn: () => sparkCreditsApi.fetchHistory(userId, { limit }), + enabled: !!userId, + staleTime: 1000 * 60 * 2, + }); +} diff --git a/lib/api/spark-credits.ts b/lib/api/spark-credits.ts new file mode 100644 index 0000000..89039b7 --- /dev/null +++ b/lib/api/spark-credits.ts @@ -0,0 +1,24 @@ +import { get } from "./client"; +import { + SparkCreditsBalance, + SparkCreditsHistoryResponse, +} from "@/types/spark-credits"; + +const SPARK_CREDITS_ENDPOINT = "/api/spark-credits"; + +export const sparkCreditsApi = { + fetchBalance: async (userId: string): Promise => { + return get(`${SPARK_CREDITS_ENDPOINT}/${userId}`); + }, + + fetchHistory: async ( + userId: string, + params?: { limit?: number; offset?: number }, + ): Promise => { + const query = new URLSearchParams(); + if (params?.limit != null) query.set("limit", String(params.limit)); + if (params?.offset != null) query.set("offset", String(params.offset)); + const url = `${SPARK_CREDITS_ENDPOINT}/${userId}/history${query.toString() ? `?${query.toString()}` : ""}`; + return get(url); + }, +}; diff --git a/lib/services/spark-credits.ts b/lib/services/spark-credits.ts new file mode 100644 index 0000000..0d26ebc --- /dev/null +++ b/lib/services/spark-credits.ts @@ -0,0 +1,98 @@ +import { + SparkCreditsBalance, + SparkCreditEvent, + SparkCreditsHistoryResponse, +} from "@/types/spark-credits"; + +// Mock Data Store (In-memory for prototype) +const MOCK_CREDITS_DB: Record = { + "user-1": 6, + "user-2": 2, + "user-3": 0, +}; + +const MOCK_CREDIT_EVENTS: Record = { + "user-1": [ + { + id: "evt-1", + userId: "user-1", + type: "EARNED", + amount: 1, + bountyId: "bounty-12", + bountyTitle: "Fix authentication bug in login flow", + description: "Earned for completing bounty", + balance: 6, + createdAt: new Date(Date.now() - 1 * 24 * 3600 * 1000).toISOString(), + }, + { + id: "evt-2", + userId: "user-1", + type: "SPENT", + amount: 1, + bountyId: "bounty-11", + bountyTitle: "Add dark mode support", + description: "Spent to claim FCFS bounty", + balance: 5, + createdAt: new Date(Date.now() - 2 * 24 * 3600 * 1000).toISOString(), + }, + { + id: "evt-3", + userId: "user-1", + type: "EARNED", + amount: 1, + bountyId: "bounty-10", + bountyTitle: "Migrate API to GraphQL", + description: "Earned for completing bounty", + balance: 6, + createdAt: new Date(Date.now() - 5 * 24 * 3600 * 1000).toISOString(), + }, + { + id: "evt-4", + userId: "user-1", + type: "SPENT", + amount: 1, + bountyId: "bounty-9", + bountyTitle: "Implement search functionality", + description: "Spent to claim FCFS bounty", + balance: 5, + createdAt: new Date(Date.now() - 7 * 24 * 3600 * 1000).toISOString(), + }, + { + id: "evt-5", + userId: "user-1", + type: "EARNED", + amount: 2, + bountyId: "bounty-8", + bountyTitle: "Build notification system", + description: "Earned for completing bounty", + balance: 6, + createdAt: new Date(Date.now() - 10 * 24 * 3600 * 1000).toISOString(), + }, + ], +}; + +export class SparkCreditsService { + static async getBalance(userId: string): Promise { + await new Promise((resolve) => setTimeout(resolve, 100)); + return { + userId, + balance: MOCK_CREDITS_DB[userId] ?? 0, + }; + } + + static async getCreditHistory( + userId: string, + limit = 20, + offset = 0, + ): Promise { + await new Promise((resolve) => setTimeout(resolve, 100)); + const events = MOCK_CREDIT_EVENTS[userId] ?? []; + const totalCount = events.length; + const sliced = events.slice(offset, offset + limit); + return { + events: sliced, + totalCount, + hasMore: offset + limit < totalCount, + }; + } +} diff --git a/package-lock.json b/package-lock.json index 9adb979..e5c6719 100644 --- a/package-lock.json +++ b/package-lock.json @@ -140,7 +140,7 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@csstools/css-calc": "^2.1.3", @@ -154,7 +154,7 @@ "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, + "devOptional": true, "license": "ISC" }, "node_modules/@babel/code-frame": { @@ -1233,7 +1233,7 @@ "version": "5.1.0", "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", - "dev": true, + "devOptional": true, "funding": [ { "type": "github", @@ -1253,7 +1253,7 @@ "version": "2.1.4", "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", - "dev": true, + "devOptional": true, "funding": [ { "type": "github", @@ -1277,7 +1277,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", - "dev": true, + "devOptional": true, "funding": [ { "type": "github", @@ -1305,7 +1305,7 @@ "version": "3.0.5", "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", - "dev": true, + "devOptional": true, "funding": [ { "type": "github", @@ -1328,7 +1328,7 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", - "dev": true, + "devOptional": true, "funding": [ { "type": "github", @@ -1434,7 +1434,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1451,7 +1450,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1468,7 +1466,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1485,7 +1482,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1502,7 +1498,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1519,7 +1514,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1536,7 +1530,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1553,7 +1546,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1570,7 +1562,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1587,7 +1578,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1604,7 +1594,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1621,7 +1610,6 @@ "cpu": [ "loong64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1638,7 +1626,6 @@ "cpu": [ "mips64el" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1655,7 +1642,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1672,7 +1658,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1689,7 +1674,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1706,7 +1690,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1723,7 +1706,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1740,7 +1722,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1757,7 +1738,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1774,7 +1754,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1791,7 +1770,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1808,7 +1786,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1825,7 +1802,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1842,7 +1818,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1859,7 +1834,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7240,7 +7214,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7254,7 +7227,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7268,7 +7240,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7282,7 +7253,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7296,7 +7266,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7310,7 +7279,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7324,7 +7292,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7338,7 +7305,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7352,7 +7318,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7366,7 +7331,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7380,7 +7344,6 @@ "cpu": [ "loong64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7394,7 +7357,6 @@ "cpu": [ "loong64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7408,7 +7370,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7422,7 +7383,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7436,7 +7396,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7450,7 +7409,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7464,7 +7422,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7478,7 +7435,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7492,7 +7448,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7506,7 +7461,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7520,7 +7474,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7534,7 +7487,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7548,7 +7500,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7562,7 +7513,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7576,7 +7526,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -8455,7 +8404,7 @@ "version": "20.19.33", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.33.tgz", "integrity": "sha512-Rs1bVAIdBs5gbTIKza/tgpMuG1k3U/UMJLWecIMxNdJFDMzcM5LOiLVRYh3PilWEYDIeUDv7bpiHPLPsbydGcw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "undici-types": "~6.21.0" @@ -9292,7 +9241,7 @@ "version": "7.1.4", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">= 14" @@ -10697,7 +10646,7 @@ "version": "4.6.0", "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@asamuzakjp/css-color": "^3.2.0", @@ -10855,7 +10804,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "whatwg-mimetype": "^4.0.0", @@ -10986,7 +10935,7 @@ "version": "10.6.0", "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/decimal.js-light": { @@ -11339,7 +11288,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", - "dev": true, + "devOptional": true, "license": "BSD-2-Clause", "engines": { "node": ">=0.12" @@ -12521,7 +12470,6 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -13389,7 +13337,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "whatwg-encoding": "^3.1.1" @@ -13419,7 +13367,7 @@ "version": "7.0.2", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "agent-base": "^7.1.0", @@ -13433,7 +13381,7 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "agent-base": "^7.1.2", @@ -14032,7 +13980,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/is-regex": { @@ -15360,7 +15308,7 @@ "version": "2.6.1", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", - "dev": true, + "devOptional": true, "license": "MIT", "bin": { "jiti": "lib/jiti-cli.mjs" @@ -15398,7 +15346,7 @@ "version": "26.1.0", "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "cssstyle": "^4.2.1", @@ -15585,7 +15533,7 @@ "version": "1.31.1", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.31.1.tgz", "integrity": "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==", - "dev": true, + "devOptional": true, "license": "MPL-2.0", "dependencies": { "detect-libc": "^2.0.3" @@ -15618,7 +15566,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -15639,7 +15586,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -15660,7 +15606,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -15681,7 +15626,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -15702,7 +15646,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -15723,7 +15666,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -15744,7 +15686,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -15765,7 +15706,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -15786,7 +15726,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -15807,7 +15746,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -15828,7 +15766,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -17578,7 +17515,7 @@ "version": "2.2.23", "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.23.tgz", "integrity": "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/object-assign": { @@ -17912,7 +17849,7 @@ "version": "7.3.0", "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "entities": "^6.0.0" @@ -18309,7 +18246,7 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=6" @@ -19137,7 +19074,7 @@ "version": "0.8.0", "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/run-parallel": { @@ -19223,14 +19160,14 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/saxes": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", - "dev": true, + "devOptional": true, "license": "ISC", "dependencies": { "xmlchars": "^2.2.0" @@ -20101,7 +20038,7 @@ "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/sync-fetch": { @@ -20327,7 +20264,7 @@ "version": "6.1.86", "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "tldts-core": "^6.1.86" @@ -20340,7 +20277,7 @@ "version": "6.1.86", "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/tmpl": { @@ -20367,7 +20304,7 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", - "dev": true, + "devOptional": true, "license": "BSD-3-Clause", "dependencies": { "tldts": "^6.1.32" @@ -20380,7 +20317,7 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "punycode": "^2.3.1" @@ -20737,7 +20674,7 @@ "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/unified": { @@ -21301,7 +21238,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "xml-name-validator": "^5.0.0" @@ -21334,7 +21271,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", - "dev": true, + "devOptional": true, "license": "BSD-2-Clause", "engines": { "node": ">=12" @@ -21345,7 +21282,7 @@ "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "iconv-lite": "0.6.3" @@ -21358,7 +21295,7 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" @@ -21371,7 +21308,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=18" @@ -21381,7 +21318,7 @@ "version": "14.2.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "tr46": "^5.1.0", @@ -21701,7 +21638,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "engines": { "node": ">=18" @@ -21711,7 +21648,7 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/y18n": { @@ -21735,7 +21672,7 @@ "version": "2.8.2", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", - "dev": true, + "devOptional": true, "license": "ISC", "bin": { "yaml": "bin.mjs" diff --git a/types/spark-credits.ts b/types/spark-credits.ts new file mode 100644 index 0000000..82233a0 --- /dev/null +++ b/types/spark-credits.ts @@ -0,0 +1,24 @@ +export interface SparkCreditsBalance { + userId: string; + balance: number; +} + +export type CreditEventType = "EARNED" | "SPENT"; + +export interface SparkCreditEvent { + id: string; + userId: string; + type: CreditEventType; + amount: number; + bountyId: string | null; + bountyTitle: string | null; + description: string; + balance: number; // running balance after this event + createdAt: string; +} + +export interface SparkCreditsHistoryResponse { + events: SparkCreditEvent[]; + totalCount: number; + hasMore: boolean; +}