solanapolis 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (202) hide show
  1. package/README.md +518 -0
  2. package/bin/solanapolis.js +197 -0
  3. package/convex/_generated/api.d.ts +175 -0
  4. package/convex/_generated/api.js +23 -0
  5. package/convex/_generated/dataModel.d.ts +60 -0
  6. package/convex/_generated/server.d.ts +143 -0
  7. package/convex/_generated/server.js +93 -0
  8. package/convex/agent/conversation.ts +352 -0
  9. package/convex/agent/embeddingsCache.ts +110 -0
  10. package/convex/agent/memory.ts +450 -0
  11. package/convex/agent/schema.ts +53 -0
  12. package/convex/aiChat.ts +54 -0
  13. package/convex/aiTown/agent.ts +382 -0
  14. package/convex/aiTown/agentDescription.ts +27 -0
  15. package/convex/aiTown/agentInputs.ts +155 -0
  16. package/convex/aiTown/agentOperations.ts +178 -0
  17. package/convex/aiTown/conversation.ts +395 -0
  18. package/convex/aiTown/conversationMembership.ts +38 -0
  19. package/convex/aiTown/game.ts +371 -0
  20. package/convex/aiTown/ids.ts +32 -0
  21. package/convex/aiTown/inputHandler.ts +9 -0
  22. package/convex/aiTown/inputs.ts +25 -0
  23. package/convex/aiTown/insertInput.ts +20 -0
  24. package/convex/aiTown/location.ts +32 -0
  25. package/convex/aiTown/main.ts +154 -0
  26. package/convex/aiTown/movement.ts +189 -0
  27. package/convex/aiTown/player.ts +310 -0
  28. package/convex/aiTown/playerDescription.ts +35 -0
  29. package/convex/aiTown/schema.ts +79 -0
  30. package/convex/aiTown/world.ts +65 -0
  31. package/convex/aiTown/worldMap.ts +74 -0
  32. package/convex/chat.ts +79 -0
  33. package/convex/constants.ts +78 -0
  34. package/convex/convex.config.ts +6 -0
  35. package/convex/crons.ts +89 -0
  36. package/convex/engine/abstractGame.ts +199 -0
  37. package/convex/engine/historicalObject.ts +355 -0
  38. package/convex/engine/schema.ts +56 -0
  39. package/convex/http.ts +36 -0
  40. package/convex/init.ts +110 -0
  41. package/convex/messages.ts +53 -0
  42. package/convex/npcCarAgents.ts +415 -0
  43. package/convex/schema.ts +61 -0
  44. package/convex/streaming.ts +23 -0
  45. package/convex/testing.ts +202 -0
  46. package/convex/tsconfig.json +18 -0
  47. package/convex/util/FastIntegerCompression.ts +221 -0
  48. package/convex/util/assertNever.ts +4 -0
  49. package/convex/util/asyncMap.ts +20 -0
  50. package/convex/util/compression.ts +71 -0
  51. package/convex/util/geometry.ts +132 -0
  52. package/convex/util/isSimpleObject.ts +11 -0
  53. package/convex/util/llm.ts +724 -0
  54. package/convex/util/minheap.ts +38 -0
  55. package/convex/util/object.ts +22 -0
  56. package/convex/util/sleep.ts +3 -0
  57. package/convex/util/types.ts +33 -0
  58. package/convex/util/xxhash.ts +228 -0
  59. package/convex/world.ts +257 -0
  60. package/data/animations/campfire.json +45 -0
  61. package/data/animations/gentlesparkle.json +37 -0
  62. package/data/animations/gentlesplash.json +61 -0
  63. package/data/animations/gentlewaterfall.json +61 -0
  64. package/data/animations/windmill.json +78 -0
  65. package/data/characters.ts +121 -0
  66. package/data/convertMap.js +74 -0
  67. package/data/gentle.js +330 -0
  68. package/data/spritesheets/f1.ts +75 -0
  69. package/data/spritesheets/f2.ts +75 -0
  70. package/data/spritesheets/f3.ts +75 -0
  71. package/data/spritesheets/f4.ts +75 -0
  72. package/data/spritesheets/f5.ts +75 -0
  73. package/data/spritesheets/f6.ts +75 -0
  74. package/data/spritesheets/f7.ts +75 -0
  75. package/data/spritesheets/f8.ts +75 -0
  76. package/data/spritesheets/p1.ts +59 -0
  77. package/data/spritesheets/p2.ts +59 -0
  78. package/data/spritesheets/p3.ts +59 -0
  79. package/data/spritesheets/player.ts +59 -0
  80. package/data/spritesheets/types.ts +26 -0
  81. package/eslint.config.mjs +37 -0
  82. package/next.config.ts +7 -0
  83. package/package.json +85 -0
  84. package/postcss.config.mjs +7 -0
  85. package/public/file.svg +1 -0
  86. package/public/globe.svg +1 -0
  87. package/public/helius-icon.svg +84 -0
  88. package/public/helius-logo.svg +85 -0
  89. package/public/next.svg +1 -0
  90. package/public/plane.glb +0 -0
  91. package/public/vercel.svg +1 -0
  92. package/public/window.svg +1 -0
  93. package/scripts/clear-city.ts +74 -0
  94. package/scripts/seed-wallets.ts +185 -0
  95. package/scripts/setup-webhook.ts +73 -0
  96. package/src/app/api/auth/callback/route.ts +6 -0
  97. package/src/app/api/auth/link-wallet/route.ts +6 -0
  98. package/src/app/api/auth/phantom/route.ts +6 -0
  99. package/src/app/api/broadcast-position/route.ts +59 -0
  100. package/src/app/api/leaderboard/route.ts +85 -0
  101. package/src/app/api/network-stats/route.ts +86 -0
  102. package/src/app/api/parcel-reward/route.ts +181 -0
  103. package/src/app/api/queue-status/route.ts +30 -0
  104. package/src/app/api/snapshots/route.ts +37 -0
  105. package/src/app/api/transactions/enhanced/route.ts +57 -0
  106. package/src/app/api/treasury/route.ts +83 -0
  107. package/src/app/api/wallet/[address]/balances/route.ts +124 -0
  108. package/src/app/api/wallet/[address]/identity/route.ts +32 -0
  109. package/src/app/api/wallet/[address]/route.ts +216 -0
  110. package/src/app/api/wallet/[address]/traded-tokens/route.ts +41 -0
  111. package/src/app/api/wallets/route.ts +68 -0
  112. package/src/app/api/webhooks/helius/route.ts +76 -0
  113. package/src/app/auth/callback/page.tsx +29 -0
  114. package/src/app/favicon.ico +0 -0
  115. package/src/app/globals.css +39 -0
  116. package/src/app/layout.tsx +43 -0
  117. package/src/app/page.tsx +16 -0
  118. package/src/components/AITownNPCs.tsx +206 -0
  119. package/src/components/ActivityFeed.tsx +189 -0
  120. package/src/components/AuthPanel.tsx +163 -0
  121. package/src/components/BeachScene.tsx +280 -0
  122. package/src/components/Building.tsx +138 -0
  123. package/src/components/CesiumFlight.tsx +1768 -0
  124. package/src/components/CesiumGlobe.tsx +616 -0
  125. package/src/components/CitizenCard.tsx +442 -0
  126. package/src/components/CitizenCardModal.tsx +153 -0
  127. package/src/components/CityGrid.tsx +313 -0
  128. package/src/components/CityLandmarks.tsx +427 -0
  129. package/src/components/CityScene.tsx +1289 -0
  130. package/src/components/CitySlotsBadge.tsx +68 -0
  131. package/src/components/CockpitHUD.tsx +460 -0
  132. package/src/components/ConvexWrapper.tsx +19 -0
  133. package/src/components/DubaiDistrict.tsx +630 -0
  134. package/src/components/FlightMiniMap.tsx +133 -0
  135. package/src/components/GameChat.tsx +383 -0
  136. package/src/components/GameHUD.tsx +393 -0
  137. package/src/components/Ground.tsx +14 -0
  138. package/src/components/HowItWorksModal.tsx +251 -0
  139. package/src/components/IngestionBanner.tsx +123 -0
  140. package/src/components/InstancedBuildings.tsx +316 -0
  141. package/src/components/InstancedCars.tsx +504 -0
  142. package/src/components/InstancedCityPlanes.tsx +259 -0
  143. package/src/components/InstancedHouses.tsx +246 -0
  144. package/src/components/InstancedLampPosts.tsx +201 -0
  145. package/src/components/InstancedResidentCars.tsx +357 -0
  146. package/src/components/InstancedRoadDashes.tsx +42 -0
  147. package/src/components/InstancedSkyscrapers.tsx +434 -0
  148. package/src/components/InstancedTrees.tsx +67 -0
  149. package/src/components/LeaderboardPanel.tsx +136 -0
  150. package/src/components/MultiplayerPlanes.tsx +128 -0
  151. package/src/components/NetworkStats.tsx +83 -0
  152. package/src/components/NewBuildingSpotlight.tsx +93 -0
  153. package/src/components/ParcelChallengeBanner.tsx +242 -0
  154. package/src/components/ParcelReward.tsx +191 -0
  155. package/src/components/Park.tsx +42 -0
  156. package/src/components/PhantomWrapper.tsx +22 -0
  157. package/src/components/PixelStreamViewer.tsx +335 -0
  158. package/src/components/PlaneMode.tsx +190 -0
  159. package/src/components/PlayerCar.tsx +211 -0
  160. package/src/components/PlayerPlane.tsx +255 -0
  161. package/src/components/ProjectileRenderer.tsx +249 -0
  162. package/src/components/QueueStatusBanner.tsx +86 -0
  163. package/src/components/RealPlayerTags.tsx +82 -0
  164. package/src/components/SceneLighting.tsx +382 -0
  165. package/src/components/SelectionBeam.tsx +59 -0
  166. package/src/components/SwapPanel.tsx +104 -0
  167. package/src/components/SwapParticles.tsx +237 -0
  168. package/src/components/TreasureGate.tsx +505 -0
  169. package/src/components/WalletPanel.tsx +421 -0
  170. package/src/components/WalletSearch.tsx +244 -0
  171. package/src/components/WelcomeOverlay.tsx +135 -0
  172. package/src/components/WindowTooltip.tsx +498 -0
  173. package/src/context/AuthContext.tsx +230 -0
  174. package/src/lib/bot-detection.ts +125 -0
  175. package/src/lib/building-math.ts +136 -0
  176. package/src/lib/building-shader.ts +253 -0
  177. package/src/lib/car-paths.ts +244 -0
  178. package/src/lib/car-system.ts +182 -0
  179. package/src/lib/city-constants.ts +29 -0
  180. package/src/lib/city-slots.ts +35 -0
  181. package/src/lib/city-zoning.ts +64 -0
  182. package/src/lib/collision-map.ts +147 -0
  183. package/src/lib/day-night.ts +252 -0
  184. package/src/lib/export-card.ts +28 -0
  185. package/src/lib/helius-webhook.ts +90 -0
  186. package/src/lib/helius.ts +74 -0
  187. package/src/lib/house-shader.ts +119 -0
  188. package/src/lib/mock-data.ts +56 -0
  189. package/src/lib/multiplayer-manager.ts +329 -0
  190. package/src/lib/plane-physics.ts +66 -0
  191. package/src/lib/player-car.ts +147 -0
  192. package/src/lib/player-plane.ts +200 -0
  193. package/src/lib/projectile-system.ts +272 -0
  194. package/src/lib/skyscraper-types.ts +52 -0
  195. package/src/lib/sound-engine.ts +464 -0
  196. package/src/lib/supabase-admin.ts +9 -0
  197. package/src/lib/supabase.ts +8 -0
  198. package/src/lib/swap-events.ts +70 -0
  199. package/src/middleware.ts +37 -0
  200. package/src/types/phantom.d.ts +16 -0
  201. package/src/types/wallet.ts +20 -0
  202. package/tsconfig.json +34 -0
@@ -0,0 +1,128 @@
1
+ "use client";
2
+ import { useEffect, useRef, useState, MutableRefObject, useMemo } from "react";
3
+ import { useFrame } from "@react-three/fiber";
4
+ import * as THREE from "three";
5
+ import { createClient } from "@/lib/supabase";
6
+ import { PlaneTransform } from "./PlaneMode";
7
+
8
+ // Reusable objects to avoid per-frame GC pressure
9
+ const _targetPos = new THREE.Vector3();
10
+ const _targetQuat = new THREE.Quaternion();
11
+ const _euler = new THREE.Euler();
12
+
13
+ interface RemotePlane {
14
+ wallet: string;
15
+ x: number; y: number; z: number;
16
+ yaw: number; pitch: number;
17
+ lastSeen: number;
18
+ }
19
+
20
+ function walletColor(address: string): string {
21
+ let hash = 0;
22
+ for (let i = 0; i < address.length; i++) hash = ((hash << 5) - hash + address.charCodeAt(i)) | 0;
23
+ const hue = Math.abs(hash) % 360;
24
+ return `hsl(${hue},70%,60%)`;
25
+ }
26
+
27
+ function RemotePlaneMesh({ plane }: { plane: RemotePlane }) {
28
+ const groupRef = useRef<THREE.Group>(null);
29
+ const color = walletColor(plane.wallet);
30
+ const mat = useMemo(() => new THREE.MeshStandardMaterial({ color, metalness: 0.4, roughness: 0.5 }), [color]);
31
+
32
+ useFrame(() => {
33
+ if (!groupRef.current) return;
34
+ _targetPos.set(plane.x, plane.y, plane.z);
35
+ groupRef.current.position.lerp(_targetPos, 0.15);
36
+ _euler.set(plane.pitch, plane.yaw, 0, "YXZ");
37
+ _targetQuat.setFromEuler(_euler);
38
+ groupRef.current.quaternion.slerp(_targetQuat, 0.15);
39
+ });
40
+
41
+ return (
42
+ <group ref={groupRef} position={[plane.x, plane.y, plane.z]}>
43
+ {/* Simplified plane mesh */}
44
+ <mesh material={mat}>
45
+ <boxGeometry args={[0.7, 0.7, 4.5]} />
46
+ </mesh>
47
+ <mesh material={mat}>
48
+ <boxGeometry args={[9, 0.15, 2.0]} />
49
+ </mesh>
50
+ <mesh position={[0, 0, -2.1]} material={mat}>
51
+ <boxGeometry args={[3.5, 0.12, 0.9]} />
52
+ </mesh>
53
+ <mesh position={[0, 0.6, -2.0]} material={mat}>
54
+ <boxGeometry args={[0.12, 1.1, 0.7]} />
55
+ </mesh>
56
+ {/* Colored point light to identify the plane */}
57
+ <pointLight color={color} intensity={3} distance={15} decay={2} position={[0, 2, 0]} />
58
+ </group>
59
+ );
60
+ }
61
+
62
+ interface MultiplayerPlanesProps {
63
+ active: boolean;
64
+ myWallet: string | null;
65
+ positionRef: MutableRefObject<PlaneTransform | null>;
66
+ }
67
+
68
+ export default function MultiplayerPlanes({ active, myWallet, positionRef }: MultiplayerPlanesProps) {
69
+ const [remotePlanes, setRemotePlanes] = useState<RemotePlane[]>([]);
70
+ const channelRef = useRef<ReturnType<ReturnType<typeof createClient>["channel"]> | null>(null);
71
+ const supabaseRef = useRef(createClient());
72
+
73
+ useEffect(() => {
74
+ const supabase = supabaseRef.current;
75
+ const channel = supabase.channel("heliopolis-planes");
76
+ channelRef.current = channel;
77
+
78
+ channel.on("broadcast", { event: "plane-position" }, ({ payload }: { payload: RemotePlane & { wallet: string } }) => {
79
+ if (payload.wallet === myWallet) return;
80
+ setRemotePlanes(prev => {
81
+ const existing = prev.findIndex(p => p.wallet === payload.wallet);
82
+ const updated = { ...payload, lastSeen: Date.now() };
83
+ if (existing >= 0) {
84
+ const next = [...prev];
85
+ next[existing] = updated;
86
+ return next;
87
+ }
88
+ return [...prev, updated];
89
+ });
90
+ });
91
+
92
+ channel.subscribe();
93
+
94
+ const cleanupInterval = setInterval(() => {
95
+ setRemotePlanes(prev => prev.filter(p => Date.now() - p.lastSeen < 5000));
96
+ }, 3000);
97
+
98
+ return () => {
99
+ clearInterval(cleanupInterval);
100
+ channel.unsubscribe();
101
+ };
102
+ }, [myWallet]);
103
+
104
+ // Broadcast own position every 100ms when active
105
+ useEffect(() => {
106
+ if (!active || !myWallet) return;
107
+
108
+ const interval = setInterval(() => {
109
+ const pos = positionRef.current;
110
+ if (!pos || !channelRef.current) return;
111
+ channelRef.current.send({
112
+ type: "broadcast",
113
+ event: "plane-position",
114
+ payload: { wallet: myWallet, ...pos, lastSeen: Date.now() },
115
+ });
116
+ }, 100);
117
+
118
+ return () => clearInterval(interval);
119
+ }, [active, myWallet, positionRef]);
120
+
121
+ return (
122
+ <>
123
+ {remotePlanes.map(plane => (
124
+ <RemotePlaneMesh key={plane.wallet} plane={plane} />
125
+ ))}
126
+ </>
127
+ );
128
+ }
@@ -0,0 +1,83 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState } from "react";
4
+
5
+ interface Stats {
6
+ slot: number;
7
+ epoch: number;
8
+ epochProgress: string;
9
+ tps: number;
10
+ }
11
+
12
+ export default function NetworkStats() {
13
+ const [stats, setStats] = useState<Stats | null>(null);
14
+ const [prevTps, setPrevTps] = useState(0);
15
+
16
+ useEffect(() => {
17
+ async function fetchStats() {
18
+ try {
19
+ const res = await fetch("/api/network-stats");
20
+ if (!res.ok) return;
21
+ const data = await res.json();
22
+ setStats((prev) => {
23
+ if (prev) setPrevTps(prev.tps);
24
+ return data;
25
+ });
26
+ } catch {
27
+ // silently fail
28
+ }
29
+ }
30
+
31
+ fetchStats();
32
+ const interval = setInterval(fetchStats, 10000); // every 10s
33
+ return () => clearInterval(interval);
34
+ }, []);
35
+
36
+ if (!stats) return null;
37
+
38
+ const tpsDelta = stats.tps - prevTps;
39
+ const tpsColor =
40
+ tpsDelta > 0 ? "text-green-300/70" : tpsDelta < 0 ? "text-red-300/70" : "text-white/50";
41
+
42
+ return (
43
+ <div className="absolute bottom-20 sm:bottom-16 right-3 sm:right-40 z-10 bg-black/50 backdrop-blur-xl border border-white/[0.08] rounded-2xl px-3.5 py-2.5 text-xs flex gap-4 items-center">
44
+ {/* TPS */}
45
+ <div className="flex flex-col items-center">
46
+ <span className="text-white/25 text-[9px] uppercase tracking-wider">TPS</span>
47
+ <span className={`font-mono font-semibold text-sm tabular-nums ${tpsColor}`}>
48
+ {stats.tps.toLocaleString()}
49
+ </span>
50
+ </div>
51
+
52
+ {/* Divider */}
53
+ <div className="w-px h-6 bg-white/[0.08]" />
54
+
55
+ {/* Slot */}
56
+ <div className="flex flex-col items-center">
57
+ <span className="text-white/25 text-[9px] uppercase tracking-wider">Slot</span>
58
+ <span className="font-mono text-white/50 text-[11px] tabular-nums">
59
+ {stats.slot.toLocaleString()}
60
+ </span>
61
+ </div>
62
+
63
+ {/* Divider */}
64
+ <div className="w-px h-6 bg-white/[0.08]" />
65
+
66
+ {/* Epoch */}
67
+ <div className="flex flex-col items-center">
68
+ <span className="text-white/25 text-[9px] uppercase tracking-wider">
69
+ Epoch {stats.epoch}
70
+ </span>
71
+ <div className="w-14 h-1.5 bg-white/[0.06] rounded-full overflow-hidden mt-1">
72
+ <div
73
+ className="h-full rounded-full transition-all duration-1000"
74
+ style={{
75
+ width: `${stats.epochProgress}%`,
76
+ background: "linear-gradient(90deg, #6366f1, #E35930)",
77
+ }}
78
+ />
79
+ </div>
80
+ </div>
81
+ </div>
82
+ );
83
+ }
@@ -0,0 +1,93 @@
1
+ "use client";
2
+ import { useRef, useMemo } from "react";
3
+ import { useFrame } from "@react-three/fiber";
4
+ import * as THREE from "three";
5
+
6
+ interface NewBuildingSpotlightProps {
7
+ position: [number, number, number];
8
+ buildingHeight: number;
9
+ }
10
+
11
+ /**
12
+ * Dramatic spotlight + ring for newly placed buildings.
13
+ * Pulses brightly then fades over ~6 seconds.
14
+ */
15
+ export default function NewBuildingSpotlight({ position, buildingHeight }: NewBuildingSpotlightProps) {
16
+ const groupRef = useRef<THREE.Group>(null);
17
+ const beamRef = useRef<THREE.Mesh>(null);
18
+ const glowRef = useRef<THREE.PointLight>(null);
19
+ const ringRef = useRef<THREE.Mesh>(null);
20
+ const startTime = useRef(Date.now());
21
+
22
+ const beamMat = useMemo(() => new THREE.MeshBasicMaterial({
23
+ color: "#E35930",
24
+ transparent: true,
25
+ opacity: 0.25,
26
+ side: THREE.DoubleSide,
27
+ depthWrite: false,
28
+ blending: THREE.AdditiveBlending,
29
+ }), []);
30
+
31
+ const ringMat = useMemo(() => new THREE.MeshBasicMaterial({
32
+ color: "#E35930",
33
+ transparent: true,
34
+ opacity: 0.4,
35
+ side: THREE.DoubleSide,
36
+ depthWrite: false,
37
+ blending: THREE.AdditiveBlending,
38
+ }), []);
39
+
40
+ useFrame(() => {
41
+ if (!groupRef.current) return;
42
+ const elapsed = (Date.now() - startTime.current) / 1000;
43
+
44
+ // Fade out after 4 seconds
45
+ const fade = elapsed < 4 ? 1 : Math.max(0, 1 - (elapsed - 4) / 2);
46
+
47
+ if (fade <= 0) {
48
+ groupRef.current.visible = false;
49
+ return;
50
+ }
51
+
52
+ groupRef.current.visible = true;
53
+
54
+ if (beamRef.current) {
55
+ beamMat.opacity = 0.2 * fade * (0.7 + 0.3 * Math.sin(elapsed * 4));
56
+ }
57
+ if (glowRef.current) {
58
+ glowRef.current.intensity = 15 * fade * (0.7 + 0.3 * Math.sin(elapsed * 4));
59
+ }
60
+ if (ringRef.current) {
61
+ const scale = 1 + elapsed * 1.5;
62
+ ringRef.current.scale.setScalar(Math.min(scale, 4));
63
+ ringMat.opacity = 0.5 * fade * Math.max(0, 1 - elapsed * 0.12);
64
+ }
65
+ });
66
+
67
+ const beamHeight = Math.max(100, buildingHeight + 80);
68
+ const beamY = buildingHeight + beamHeight / 2;
69
+
70
+ return (
71
+ <group ref={groupRef} position={position}>
72
+ {/* Vertical beam */}
73
+ <mesh ref={beamRef} material={beamMat} position={[0, beamY - position[1], 0]}>
74
+ <cylinderGeometry args={[0.15, 2.0, beamHeight, 8, 1, true]} />
75
+ </mesh>
76
+
77
+ {/* Pulsing ring at building top */}
78
+ <mesh ref={ringRef} material={ringMat} position={[0, buildingHeight - position[1] + 1, 0]} rotation={[Math.PI / 2, 0, 0]}>
79
+ <ringGeometry args={[1.5, 2.5, 24]} />
80
+ </mesh>
81
+
82
+ {/* Point light glow */}
83
+ <pointLight
84
+ ref={glowRef}
85
+ position={[0, buildingHeight - position[1] + 2, 0]}
86
+ color="#E35930"
87
+ intensity={15}
88
+ distance={50}
89
+ decay={2}
90
+ />
91
+ </group>
92
+ );
93
+ }
@@ -0,0 +1,242 @@
1
+ "use client";
2
+
3
+ import { useState, useEffect } from "react";
4
+
5
+ interface ParcelChallengeBannerProps {
6
+ walletConnected: boolean;
7
+ walletCount: number;
8
+ }
9
+
10
+ const TOTAL_REWARD_PARCELS = 16;
11
+ const REWARD_PER_PARCEL = 0.069420;
12
+ const TREASURY_ADDRESS = "76QQqndM87DBQAeAnRnjdMSiVBgBNrm55HMa88MRVc7p";
13
+
14
+ interface TreasuryInfo {
15
+ balanceSOL: number;
16
+ parcelsRemaining: number;
17
+ solUsdValue: number | null;
18
+ solPrice: number | null;
19
+ totalUsdValue: number | null;
20
+ }
21
+
22
+ export default function ParcelChallengeBanner({ walletConnected, walletCount }: ParcelChallengeBannerProps) {
23
+ const [visible, setVisible] = useState(false);
24
+ const [dismissed, setDismissed] = useState(false);
25
+ const [treasury, setTreasury] = useState<TreasuryInfo | null>(null);
26
+
27
+ const slotsRemaining = Math.max(0, TOTAL_REWARD_PARCELS - walletCount);
28
+ const allClaimed = slotsRemaining === 0;
29
+
30
+ // Fetch live treasury balance
31
+ useEffect(() => {
32
+ async function fetchTreasury() {
33
+ try {
34
+ const res = await fetch("/api/treasury");
35
+ if (res.ok) {
36
+ const data = await res.json();
37
+ setTreasury({
38
+ balanceSOL: data.balanceSOL,
39
+ parcelsRemaining: data.parcelsRemaining,
40
+ solUsdValue: data.solUsdValue ?? null,
41
+ solPrice: data.solPrice ?? null,
42
+ totalUsdValue: data.totalUsdValue ?? null,
43
+ });
44
+ }
45
+ } catch { /* silent */ }
46
+ }
47
+ fetchTreasury();
48
+ const interval = setInterval(fetchTreasury, 30000); // refresh every 30s
49
+ return () => clearInterval(interval);
50
+ }, []);
51
+
52
+ useEffect(() => {
53
+ const timer = setTimeout(() => {
54
+ if (sessionStorage.getItem("parcel-banner-dismissed")) return;
55
+ setVisible(true);
56
+ }, walletConnected ? 1000 : 3000);
57
+ return () => clearTimeout(timer);
58
+ }, [walletConnected]);
59
+
60
+ if (!visible || dismissed) return null;
61
+
62
+ function handleDismiss() {
63
+ setDismissed(true);
64
+ sessionStorage.setItem("parcel-banner-dismissed", "1");
65
+ }
66
+
67
+ const treasuryBalance = treasury?.balanceSOL ?? null;
68
+ const fundedParcels = treasury?.parcelsRemaining ?? slotsRemaining;
69
+ const depleted = treasuryBalance !== null && treasuryBalance < REWARD_PER_PARCEL;
70
+ const solPrice = treasury?.solPrice ?? null;
71
+ const treasuryUsd = treasury?.solUsdValue ?? null;
72
+ const parcelUsd = solPrice !== null ? (REWARD_PER_PARCEL * solPrice) : null;
73
+
74
+ return (
75
+ <div className="fixed top-20 left-1/2 -translate-x-1/2 z-30 animate-[fadeSlideDown_0.5s_ease-out] w-[95vw] max-w-lg">
76
+ <div className="relative overflow-hidden bg-gradient-to-r from-amber-900/95 via-yellow-900/90 to-orange-900/95 backdrop-blur-2xl border border-amber-500/30 rounded-2xl shadow-2xl shadow-amber-900/40">
77
+ {/* Animated shine */}
78
+ <div className="absolute inset-0 overflow-hidden pointer-events-none">
79
+ <div className="absolute -top-1/2 -left-1/2 w-full h-full bg-gradient-to-br from-white/5 to-transparent rotate-12 animate-[shimmer_3s_ease-in-out_infinite]" />
80
+ </div>
81
+
82
+ {/* Close */}
83
+ <button
84
+ onClick={handleDismiss}
85
+ className="absolute top-3 right-3 w-7 h-7 flex items-center justify-center rounded-full bg-white/10 hover:bg-white/20 text-white/40 hover:text-white/70 text-sm transition-all cursor-pointer z-10"
86
+ >
87
+
88
+ </button>
89
+
90
+ <div className="px-5 py-4">
91
+ {/* Header */}
92
+ <div className="flex items-start gap-3 mb-3">
93
+ <div className="text-3xl mt-0.5 flex-shrink-0 animate-bounce" style={{ animationDuration: "2s" }}>
94
+ 💎
95
+ </div>
96
+ <div>
97
+ <h2 className="text-base font-extrabold text-transparent bg-clip-text bg-gradient-to-r from-amber-200 via-yellow-300 to-amber-200 tracking-tight leading-tight">
98
+ 0.069420 SOL TREASURE HUNT
99
+ </h2>
100
+ <p className="text-[11px] text-amber-300/60 mt-0.5">
101
+ Hidden rewards in city parcels — claim yours first!
102
+ </p>
103
+ </div>
104
+ </div>
105
+
106
+ {/* Stats grid */}
107
+ <div className="grid grid-cols-4 gap-1.5 mb-3">
108
+ <div className="bg-black/30 rounded-xl px-2 py-2 text-center border border-amber-500/10">
109
+ <p className="text-[7px] uppercase tracking-wider text-amber-400/40 mb-0.5">Per Parcel</p>
110
+ <p className="text-xs font-bold text-amber-200 tabular-nums">0.069420</p>
111
+ <p className="text-[7px] text-amber-400/30">
112
+ {parcelUsd !== null ? `≈ $${parcelUsd.toFixed(2)}` : 'SOL'}
113
+ </p>
114
+ </div>
115
+ <div className="bg-black/30 rounded-xl px-2 py-2 text-center border border-amber-500/10">
116
+ <p className="text-[7px] uppercase tracking-wider text-amber-400/40 mb-0.5">Total</p>
117
+ <p className="text-xs font-bold text-amber-200 tabular-nums">{TOTAL_REWARD_PARCELS}</p>
118
+ <p className="text-[7px] text-amber-400/30">parcels</p>
119
+ </div>
120
+ <div className="bg-black/30 rounded-xl px-2 py-2 text-center border border-amber-500/10">
121
+ <p className="text-[7px] uppercase tracking-wider text-amber-400/40 mb-0.5">Can Fund</p>
122
+ <p className={`text-xs font-bold tabular-nums ${depleted ? 'text-red-400' : 'text-emerald-400'}`}>
123
+ {fundedParcels}
124
+ </p>
125
+ <p className="text-[7px] text-amber-400/30">{depleted ? 'empty' : 'left'}</p>
126
+ </div>
127
+ <div className="bg-black/30 rounded-xl px-2 py-2 text-center border border-amber-500/10">
128
+ <p className="text-[7px] uppercase tracking-wider text-amber-400/40 mb-0.5">Treasury</p>
129
+ <p className="text-xs font-bold text-amber-200 tabular-nums">
130
+ {treasuryBalance !== null ? treasuryBalance.toFixed(4) : '...'}
131
+ </p>
132
+ <p className="text-[7px] text-amber-400/30">
133
+ {treasuryUsd !== null ? `≈ $${treasuryUsd.toFixed(2)}` : 'SOL'}
134
+ </p>
135
+ </div>
136
+ </div>
137
+
138
+ {/* SOL price ticker */}
139
+ {solPrice !== null && (
140
+ <div className="flex items-center justify-center gap-2 mb-2">
141
+ <span className="text-[8px] text-amber-400/30">SOL/USD</span>
142
+ <span className="text-[10px] font-bold text-amber-200/70 tabular-nums">
143
+ ${solPrice.toFixed(2)}
144
+ </span>
145
+ <span className="text-[8px] text-amber-400/20">via Helius</span>
146
+ </div>
147
+ )}
148
+
149
+ {/* Treasury wallet address */}
150
+ <div className="bg-black/20 rounded-lg px-3 py-1.5 mb-3 border border-amber-500/8 flex items-center gap-2">
151
+ <span className="text-[9px] text-amber-400/40 whitespace-nowrap">Treasury:</span>
152
+ <a
153
+ href={`https://solscan.io/account/${TREASURY_ADDRESS}`}
154
+ target="_blank"
155
+ rel="noopener noreferrer"
156
+ className="text-[9px] text-amber-300/50 hover:text-amber-200/80 font-mono truncate transition-colors"
157
+ >
158
+ {TREASURY_ADDRESS}
159
+ </a>
160
+ <span className="text-[9px]">🔗</span>
161
+ </div>
162
+
163
+ {/* How it works */}
164
+ <div className="bg-black/20 rounded-xl px-3 py-2.5 border border-amber-500/8 mb-3">
165
+ <p className="text-[9px] uppercase tracking-wider text-amber-400/40 mb-1.5 font-medium">How It Works</p>
166
+ <div className="space-y-1 text-[11px] text-amber-200/70">
167
+ <p className="flex items-center gap-2">
168
+ <span className="text-sm">1️⃣</span>
169
+ <span>Connect your Solana wallet</span>
170
+ </p>
171
+ <p className="flex items-center gap-2">
172
+ <span className="text-sm">2️⃣</span>
173
+ <span>Your building is generated in the city</span>
174
+ </p>
175
+ <p className="flex items-center gap-2">
176
+ <span className="text-sm">3️⃣</span>
177
+ <span>If you&apos;re in the <strong className="text-amber-200">first {TOTAL_REWARD_PARCELS}</strong>, your parcel has hidden SOL!</span>
178
+ </p>
179
+ <p className="flex items-center gap-2">
180
+ <span className="text-sm">4️⃣</span>
181
+ <span>Click <strong className="text-amber-200">&quot;Claim Reward&quot;</strong> to receive 0.069420 SOL</span>
182
+ </p>
183
+ </div>
184
+ </div>
185
+
186
+ {/* CTA */}
187
+ {!walletConnected ? (
188
+ <div className="text-center">
189
+ <p className="text-xs text-amber-300/80 font-medium animate-pulse">
190
+ ⚡ Connect your wallet to check if you qualify! ⚡
191
+ </p>
192
+ </div>
193
+ ) : depleted ? (
194
+ <div className="text-center py-1 bg-red-500/10 rounded-xl border border-red-500/10">
195
+ <p className="text-xs text-red-300/70 font-medium">
196
+ 🚫 Treasury depleted — all rewards have been claimed!
197
+ </p>
198
+ </div>
199
+ ) : allClaimed ? (
200
+ <div className="text-center py-1 bg-red-500/10 rounded-xl border border-red-500/10">
201
+ <p className="text-xs text-red-300/70 font-medium">
202
+ 🚫 All {TOTAL_REWARD_PARCELS} parcel slots filled!
203
+ </p>
204
+ </div>
205
+ ) : (
206
+ <div className="text-center">
207
+ <p className="text-xs text-emerald-300/80 font-medium">
208
+ ✅ {fundedParcels} reward{fundedParcels !== 1 ? 's' : ''} still available — claim your parcel!
209
+ </p>
210
+ </div>
211
+ )}
212
+ </div>
213
+
214
+ {/* Progress bar */}
215
+ <div className="h-1 bg-black/30">
216
+ <div
217
+ className="h-full transition-all duration-1000"
218
+ style={{
219
+ width: `${treasuryBalance !== null
220
+ ? Math.max(0, 100 - (treasuryBalance / (TOTAL_REWARD_PARCELS * REWARD_PER_PARCEL)) * 100)
221
+ : 0}%`,
222
+ background: depleted
223
+ ? "linear-gradient(90deg, #ef4444, #f87171)"
224
+ : "linear-gradient(90deg, #f59e0b, #eab308, #f59e0b)",
225
+ }}
226
+ />
227
+ </div>
228
+ </div>
229
+
230
+ <style jsx>{`
231
+ @keyframes fadeSlideDown {
232
+ from { opacity: 0; transform: translate(-50%, -20px); }
233
+ to { opacity: 1; transform: translate(-50%, 0); }
234
+ }
235
+ @keyframes shimmer {
236
+ 0%, 100% { transform: translateX(-100%) rotate(12deg); }
237
+ 50% { transform: translateX(200%) rotate(12deg); }
238
+ }
239
+ `}</style>
240
+ </div>
241
+ );
242
+ }