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.
- package/README.md +518 -0
- package/bin/solanapolis.js +197 -0
- package/convex/_generated/api.d.ts +175 -0
- package/convex/_generated/api.js +23 -0
- package/convex/_generated/dataModel.d.ts +60 -0
- package/convex/_generated/server.d.ts +143 -0
- package/convex/_generated/server.js +93 -0
- package/convex/agent/conversation.ts +352 -0
- package/convex/agent/embeddingsCache.ts +110 -0
- package/convex/agent/memory.ts +450 -0
- package/convex/agent/schema.ts +53 -0
- package/convex/aiChat.ts +54 -0
- package/convex/aiTown/agent.ts +382 -0
- package/convex/aiTown/agentDescription.ts +27 -0
- package/convex/aiTown/agentInputs.ts +155 -0
- package/convex/aiTown/agentOperations.ts +178 -0
- package/convex/aiTown/conversation.ts +395 -0
- package/convex/aiTown/conversationMembership.ts +38 -0
- package/convex/aiTown/game.ts +371 -0
- package/convex/aiTown/ids.ts +32 -0
- package/convex/aiTown/inputHandler.ts +9 -0
- package/convex/aiTown/inputs.ts +25 -0
- package/convex/aiTown/insertInput.ts +20 -0
- package/convex/aiTown/location.ts +32 -0
- package/convex/aiTown/main.ts +154 -0
- package/convex/aiTown/movement.ts +189 -0
- package/convex/aiTown/player.ts +310 -0
- package/convex/aiTown/playerDescription.ts +35 -0
- package/convex/aiTown/schema.ts +79 -0
- package/convex/aiTown/world.ts +65 -0
- package/convex/aiTown/worldMap.ts +74 -0
- package/convex/chat.ts +79 -0
- package/convex/constants.ts +78 -0
- package/convex/convex.config.ts +6 -0
- package/convex/crons.ts +89 -0
- package/convex/engine/abstractGame.ts +199 -0
- package/convex/engine/historicalObject.ts +355 -0
- package/convex/engine/schema.ts +56 -0
- package/convex/http.ts +36 -0
- package/convex/init.ts +110 -0
- package/convex/messages.ts +53 -0
- package/convex/npcCarAgents.ts +415 -0
- package/convex/schema.ts +61 -0
- package/convex/streaming.ts +23 -0
- package/convex/testing.ts +202 -0
- package/convex/tsconfig.json +18 -0
- package/convex/util/FastIntegerCompression.ts +221 -0
- package/convex/util/assertNever.ts +4 -0
- package/convex/util/asyncMap.ts +20 -0
- package/convex/util/compression.ts +71 -0
- package/convex/util/geometry.ts +132 -0
- package/convex/util/isSimpleObject.ts +11 -0
- package/convex/util/llm.ts +724 -0
- package/convex/util/minheap.ts +38 -0
- package/convex/util/object.ts +22 -0
- package/convex/util/sleep.ts +3 -0
- package/convex/util/types.ts +33 -0
- package/convex/util/xxhash.ts +228 -0
- package/convex/world.ts +257 -0
- package/data/animations/campfire.json +45 -0
- package/data/animations/gentlesparkle.json +37 -0
- package/data/animations/gentlesplash.json +61 -0
- package/data/animations/gentlewaterfall.json +61 -0
- package/data/animations/windmill.json +78 -0
- package/data/characters.ts +121 -0
- package/data/convertMap.js +74 -0
- package/data/gentle.js +330 -0
- package/data/spritesheets/f1.ts +75 -0
- package/data/spritesheets/f2.ts +75 -0
- package/data/spritesheets/f3.ts +75 -0
- package/data/spritesheets/f4.ts +75 -0
- package/data/spritesheets/f5.ts +75 -0
- package/data/spritesheets/f6.ts +75 -0
- package/data/spritesheets/f7.ts +75 -0
- package/data/spritesheets/f8.ts +75 -0
- package/data/spritesheets/p1.ts +59 -0
- package/data/spritesheets/p2.ts +59 -0
- package/data/spritesheets/p3.ts +59 -0
- package/data/spritesheets/player.ts +59 -0
- package/data/spritesheets/types.ts +26 -0
- package/eslint.config.mjs +37 -0
- package/next.config.ts +7 -0
- package/package.json +85 -0
- package/postcss.config.mjs +7 -0
- package/public/file.svg +1 -0
- package/public/globe.svg +1 -0
- package/public/helius-icon.svg +84 -0
- package/public/helius-logo.svg +85 -0
- package/public/next.svg +1 -0
- package/public/plane.glb +0 -0
- package/public/vercel.svg +1 -0
- package/public/window.svg +1 -0
- package/scripts/clear-city.ts +74 -0
- package/scripts/seed-wallets.ts +185 -0
- package/scripts/setup-webhook.ts +73 -0
- package/src/app/api/auth/callback/route.ts +6 -0
- package/src/app/api/auth/link-wallet/route.ts +6 -0
- package/src/app/api/auth/phantom/route.ts +6 -0
- package/src/app/api/broadcast-position/route.ts +59 -0
- package/src/app/api/leaderboard/route.ts +85 -0
- package/src/app/api/network-stats/route.ts +86 -0
- package/src/app/api/parcel-reward/route.ts +181 -0
- package/src/app/api/queue-status/route.ts +30 -0
- package/src/app/api/snapshots/route.ts +37 -0
- package/src/app/api/transactions/enhanced/route.ts +57 -0
- package/src/app/api/treasury/route.ts +83 -0
- package/src/app/api/wallet/[address]/balances/route.ts +124 -0
- package/src/app/api/wallet/[address]/identity/route.ts +32 -0
- package/src/app/api/wallet/[address]/route.ts +216 -0
- package/src/app/api/wallet/[address]/traded-tokens/route.ts +41 -0
- package/src/app/api/wallets/route.ts +68 -0
- package/src/app/api/webhooks/helius/route.ts +76 -0
- package/src/app/auth/callback/page.tsx +29 -0
- package/src/app/favicon.ico +0 -0
- package/src/app/globals.css +39 -0
- package/src/app/layout.tsx +43 -0
- package/src/app/page.tsx +16 -0
- package/src/components/AITownNPCs.tsx +206 -0
- package/src/components/ActivityFeed.tsx +189 -0
- package/src/components/AuthPanel.tsx +163 -0
- package/src/components/BeachScene.tsx +280 -0
- package/src/components/Building.tsx +138 -0
- package/src/components/CesiumFlight.tsx +1768 -0
- package/src/components/CesiumGlobe.tsx +616 -0
- package/src/components/CitizenCard.tsx +442 -0
- package/src/components/CitizenCardModal.tsx +153 -0
- package/src/components/CityGrid.tsx +313 -0
- package/src/components/CityLandmarks.tsx +427 -0
- package/src/components/CityScene.tsx +1289 -0
- package/src/components/CitySlotsBadge.tsx +68 -0
- package/src/components/CockpitHUD.tsx +460 -0
- package/src/components/ConvexWrapper.tsx +19 -0
- package/src/components/DubaiDistrict.tsx +630 -0
- package/src/components/FlightMiniMap.tsx +133 -0
- package/src/components/GameChat.tsx +383 -0
- package/src/components/GameHUD.tsx +393 -0
- package/src/components/Ground.tsx +14 -0
- package/src/components/HowItWorksModal.tsx +251 -0
- package/src/components/IngestionBanner.tsx +123 -0
- package/src/components/InstancedBuildings.tsx +316 -0
- package/src/components/InstancedCars.tsx +504 -0
- package/src/components/InstancedCityPlanes.tsx +259 -0
- package/src/components/InstancedHouses.tsx +246 -0
- package/src/components/InstancedLampPosts.tsx +201 -0
- package/src/components/InstancedResidentCars.tsx +357 -0
- package/src/components/InstancedRoadDashes.tsx +42 -0
- package/src/components/InstancedSkyscrapers.tsx +434 -0
- package/src/components/InstancedTrees.tsx +67 -0
- package/src/components/LeaderboardPanel.tsx +136 -0
- package/src/components/MultiplayerPlanes.tsx +128 -0
- package/src/components/NetworkStats.tsx +83 -0
- package/src/components/NewBuildingSpotlight.tsx +93 -0
- package/src/components/ParcelChallengeBanner.tsx +242 -0
- package/src/components/ParcelReward.tsx +191 -0
- package/src/components/Park.tsx +42 -0
- package/src/components/PhantomWrapper.tsx +22 -0
- package/src/components/PixelStreamViewer.tsx +335 -0
- package/src/components/PlaneMode.tsx +190 -0
- package/src/components/PlayerCar.tsx +211 -0
- package/src/components/PlayerPlane.tsx +255 -0
- package/src/components/ProjectileRenderer.tsx +249 -0
- package/src/components/QueueStatusBanner.tsx +86 -0
- package/src/components/RealPlayerTags.tsx +82 -0
- package/src/components/SceneLighting.tsx +382 -0
- package/src/components/SelectionBeam.tsx +59 -0
- package/src/components/SwapPanel.tsx +104 -0
- package/src/components/SwapParticles.tsx +237 -0
- package/src/components/TreasureGate.tsx +505 -0
- package/src/components/WalletPanel.tsx +421 -0
- package/src/components/WalletSearch.tsx +244 -0
- package/src/components/WelcomeOverlay.tsx +135 -0
- package/src/components/WindowTooltip.tsx +498 -0
- package/src/context/AuthContext.tsx +230 -0
- package/src/lib/bot-detection.ts +125 -0
- package/src/lib/building-math.ts +136 -0
- package/src/lib/building-shader.ts +253 -0
- package/src/lib/car-paths.ts +244 -0
- package/src/lib/car-system.ts +182 -0
- package/src/lib/city-constants.ts +29 -0
- package/src/lib/city-slots.ts +35 -0
- package/src/lib/city-zoning.ts +64 -0
- package/src/lib/collision-map.ts +147 -0
- package/src/lib/day-night.ts +252 -0
- package/src/lib/export-card.ts +28 -0
- package/src/lib/helius-webhook.ts +90 -0
- package/src/lib/helius.ts +74 -0
- package/src/lib/house-shader.ts +119 -0
- package/src/lib/mock-data.ts +56 -0
- package/src/lib/multiplayer-manager.ts +329 -0
- package/src/lib/plane-physics.ts +66 -0
- package/src/lib/player-car.ts +147 -0
- package/src/lib/player-plane.ts +200 -0
- package/src/lib/projectile-system.ts +272 -0
- package/src/lib/skyscraper-types.ts +52 -0
- package/src/lib/sound-engine.ts +464 -0
- package/src/lib/supabase-admin.ts +9 -0
- package/src/lib/supabase.ts +8 -0
- package/src/lib/swap-events.ts +70 -0
- package/src/middleware.ts +37 -0
- package/src/types/phantom.d.ts +16 -0
- package/src/types/wallet.ts +20 -0
- package/tsconfig.json +34 -0
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
import { forwardRef } from "react";
|
|
2
|
+
import { WalletBuilding } from "@/types/wallet";
|
|
3
|
+
import {
|
|
4
|
+
floors,
|
|
5
|
+
getBuildingDimensions,
|
|
6
|
+
getBuildingColor,
|
|
7
|
+
} from "@/lib/building-math";
|
|
8
|
+
|
|
9
|
+
interface CitizenCardProps {
|
|
10
|
+
wallet: WalletBuilding;
|
|
11
|
+
identityName?: string | null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function formatAge(days: number): string {
|
|
15
|
+
if (days < 30) return `${days}d`;
|
|
16
|
+
if (days < 365) return `${Math.floor(days / 30)}mo`;
|
|
17
|
+
return `${(days / 365).toFixed(1)}yr`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function formatDate(days: number): string {
|
|
21
|
+
const d = new Date(Date.now() - days * 86400 * 1000);
|
|
22
|
+
return `${String(d.getMonth() + 1).padStart(2, "0")}/${String(d.getDate()).padStart(2, "0")}/${d.getFullYear()}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function formatLastActive(blocktime: number | undefined): string {
|
|
26
|
+
if (!blocktime) return "N/A";
|
|
27
|
+
const d = new Date(blocktime * 1000);
|
|
28
|
+
return `${String(d.getMonth() + 1).padStart(2, "0")}/${String(d.getDate()).padStart(2, "0")}/${d.getFullYear()}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Deterministic class from wallet address */
|
|
32
|
+
function getCitizenClass(address: string): string {
|
|
33
|
+
let hash = 0;
|
|
34
|
+
for (let i = 0; i < address.length; i++) {
|
|
35
|
+
hash = (hash * 31 + address.charCodeAt(i)) | 0;
|
|
36
|
+
}
|
|
37
|
+
const classes = ["PIONEER", "TRADER", "BUILDER", "WHALE", "DEGEN", "HODLER", "FOUNDER", "EXPLORER"];
|
|
38
|
+
return classes[Math.abs(hash) % classes.length];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Deterministic license number from address */
|
|
42
|
+
function getLicenseNo(address: string): string {
|
|
43
|
+
const chars = address.replace(/[^A-Za-z0-9]/g, "").toUpperCase();
|
|
44
|
+
return `SOL-${chars.slice(0, 3)}-${chars.slice(-4)}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const CitizenCard = forwardRef<HTMLDivElement, CitizenCardProps>(
|
|
48
|
+
function CitizenCard({ wallet, identityName }, ref) {
|
|
49
|
+
const dims = getBuildingDimensions(wallet);
|
|
50
|
+
const floorCount = floors(wallet.txnCount);
|
|
51
|
+
const color = getBuildingColor(dims.height);
|
|
52
|
+
const citizenClass = getCitizenClass(wallet.address);
|
|
53
|
+
const licenseNo = getLicenseNo(wallet.address);
|
|
54
|
+
|
|
55
|
+
const truncAddr = `${wallet.address.slice(0, 6)}...${wallet.address.slice(-4)}`;
|
|
56
|
+
|
|
57
|
+
// Building silhouette dimensions (proportional to actual building)
|
|
58
|
+
const silHeight = Math.min(90, Math.max(35, floorCount * 0.7));
|
|
59
|
+
const silWidth = Math.min(50, Math.max(22, dims.width * 18));
|
|
60
|
+
|
|
61
|
+
// Car color matches building
|
|
62
|
+
const carColor = "#E35930";
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<div
|
|
66
|
+
ref={ref}
|
|
67
|
+
style={{
|
|
68
|
+
width: 600,
|
|
69
|
+
height: 360,
|
|
70
|
+
background: "linear-gradient(145deg, #0d0d1a 0%, #121225 40%, #0a0a18 100%)",
|
|
71
|
+
fontFamily: "'Geist Sans', ui-sans-serif, system-ui, sans-serif",
|
|
72
|
+
color: "#ffffff",
|
|
73
|
+
position: "relative",
|
|
74
|
+
overflow: "hidden",
|
|
75
|
+
display: "flex",
|
|
76
|
+
flexDirection: "column",
|
|
77
|
+
borderRadius: 16,
|
|
78
|
+
}}
|
|
79
|
+
>
|
|
80
|
+
{/* Top stripe — gradient like a real DL */}
|
|
81
|
+
<div
|
|
82
|
+
style={{
|
|
83
|
+
height: 5,
|
|
84
|
+
background: "linear-gradient(90deg, #E35930 0%, #c94420 40%, #8b5cf6 100%)",
|
|
85
|
+
width: "100%",
|
|
86
|
+
flexShrink: 0,
|
|
87
|
+
}}
|
|
88
|
+
/>
|
|
89
|
+
|
|
90
|
+
{/* Header bar */}
|
|
91
|
+
<div
|
|
92
|
+
style={{
|
|
93
|
+
display: "flex",
|
|
94
|
+
justifyContent: "space-between",
|
|
95
|
+
alignItems: "center",
|
|
96
|
+
padding: "10px 24px 6px",
|
|
97
|
+
}}
|
|
98
|
+
>
|
|
99
|
+
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
|
100
|
+
<HeliusIcon />
|
|
101
|
+
<div>
|
|
102
|
+
<div
|
|
103
|
+
style={{
|
|
104
|
+
fontSize: 13,
|
|
105
|
+
fontWeight: 800,
|
|
106
|
+
letterSpacing: "0.08em",
|
|
107
|
+
color: "#E35930",
|
|
108
|
+
lineHeight: 1,
|
|
109
|
+
}}
|
|
110
|
+
>
|
|
111
|
+
SOLANAPOLIS
|
|
112
|
+
</div>
|
|
113
|
+
<div
|
|
114
|
+
style={{
|
|
115
|
+
fontSize: 8,
|
|
116
|
+
fontWeight: 600,
|
|
117
|
+
letterSpacing: "0.2em",
|
|
118
|
+
color: "rgba(255,255,255,0.25)",
|
|
119
|
+
textTransform: "uppercase" as const,
|
|
120
|
+
marginTop: 1,
|
|
121
|
+
}}
|
|
122
|
+
>
|
|
123
|
+
Citizen Identification Card
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
<div
|
|
128
|
+
style={{
|
|
129
|
+
fontSize: 9,
|
|
130
|
+
fontWeight: 700,
|
|
131
|
+
letterSpacing: "0.15em",
|
|
132
|
+
color: "#8b5cf6",
|
|
133
|
+
textTransform: "uppercase" as const,
|
|
134
|
+
background: "rgba(139,92,246,0.1)",
|
|
135
|
+
border: "1px solid rgba(139,92,246,0.2)",
|
|
136
|
+
borderRadius: 6,
|
|
137
|
+
padding: "3px 8px",
|
|
138
|
+
}}
|
|
139
|
+
>
|
|
140
|
+
CLASS: {citizenClass}
|
|
141
|
+
</div>
|
|
142
|
+
</div>
|
|
143
|
+
|
|
144
|
+
{/* Main content */}
|
|
145
|
+
<div
|
|
146
|
+
style={{
|
|
147
|
+
flex: 1,
|
|
148
|
+
padding: "8px 24px 12px",
|
|
149
|
+
display: "flex",
|
|
150
|
+
gap: 20,
|
|
151
|
+
}}
|
|
152
|
+
>
|
|
153
|
+
{/* Left: Photo area — building silhouette + car */}
|
|
154
|
+
<div
|
|
155
|
+
style={{
|
|
156
|
+
width: 120,
|
|
157
|
+
height: 140,
|
|
158
|
+
background: "rgba(255,255,255,0.03)",
|
|
159
|
+
border: "1px solid rgba(255,255,255,0.06)",
|
|
160
|
+
borderRadius: 10,
|
|
161
|
+
display: "flex",
|
|
162
|
+
flexDirection: "column",
|
|
163
|
+
alignItems: "center",
|
|
164
|
+
justifyContent: "flex-end",
|
|
165
|
+
flexShrink: 0,
|
|
166
|
+
position: "relative",
|
|
167
|
+
overflow: "hidden",
|
|
168
|
+
}}
|
|
169
|
+
>
|
|
170
|
+
{/* Subtle grid lines like a hologram */}
|
|
171
|
+
<div style={{
|
|
172
|
+
position: "absolute",
|
|
173
|
+
inset: 0,
|
|
174
|
+
backgroundImage: "repeating-linear-gradient(0deg, transparent, transparent 11px, rgba(139,92,246,0.04) 11px, rgba(139,92,246,0.04) 12px), repeating-linear-gradient(90deg, transparent, transparent 11px, rgba(139,92,246,0.04) 11px, rgba(139,92,246,0.04) 12px)",
|
|
175
|
+
}} />
|
|
176
|
+
|
|
177
|
+
{/* Building silhouette */}
|
|
178
|
+
<svg
|
|
179
|
+
width={silWidth}
|
|
180
|
+
height={silHeight}
|
|
181
|
+
viewBox={`0 0 ${silWidth} ${silHeight}`}
|
|
182
|
+
style={{ marginBottom: 4, position: "relative" }}
|
|
183
|
+
>
|
|
184
|
+
<rect
|
|
185
|
+
x={0}
|
|
186
|
+
y={0}
|
|
187
|
+
width={silWidth}
|
|
188
|
+
height={silHeight}
|
|
189
|
+
fill={color}
|
|
190
|
+
rx={1}
|
|
191
|
+
/>
|
|
192
|
+
{/* Window dots */}
|
|
193
|
+
{Array.from({ length: Math.min(6, Math.floor(silHeight / 10)) }).map(
|
|
194
|
+
(_, row) =>
|
|
195
|
+
Array.from({ length: Math.min(4, Math.floor(silWidth / 11)) }).map(
|
|
196
|
+
(_, col) => (
|
|
197
|
+
<rect
|
|
198
|
+
key={`${row}-${col}`}
|
|
199
|
+
x={5 + col * Math.floor((silWidth - 10) / 4)}
|
|
200
|
+
y={5 + row * Math.floor((silHeight - 8) / 6)}
|
|
201
|
+
width={3}
|
|
202
|
+
height={4}
|
|
203
|
+
fill="rgba(255,200,100,0.6)"
|
|
204
|
+
rx={0.5}
|
|
205
|
+
/>
|
|
206
|
+
),
|
|
207
|
+
),
|
|
208
|
+
)}
|
|
209
|
+
{/* Antenna */}
|
|
210
|
+
<rect x={silWidth / 2 - 0.5} y={-6} width={1} height={6} fill={color} />
|
|
211
|
+
<circle cx={silWidth / 2} cy={-7} r={1.5} fill="#E35930" />
|
|
212
|
+
</svg>
|
|
213
|
+
|
|
214
|
+
{/* Car parked at base */}
|
|
215
|
+
<svg width="48" height="18" viewBox="0 0 48 18" style={{ marginBottom: 8 }}>
|
|
216
|
+
{/* Body */}
|
|
217
|
+
<rect x="4" y="6" width="40" height="8" rx="2" fill={carColor} />
|
|
218
|
+
{/* Cabin */}
|
|
219
|
+
<rect x="14" y="2" width="20" height="6" rx="2" fill="#c94420" />
|
|
220
|
+
{/* Windshield */}
|
|
221
|
+
<rect x="15" y="3" width="8" height="4" rx="1" fill="rgba(139,92,246,0.3)" />
|
|
222
|
+
<rect x="25" y="3" width="8" height="4" rx="1" fill="rgba(139,92,246,0.2)" />
|
|
223
|
+
{/* Wheels */}
|
|
224
|
+
<circle cx="12" cy="14" r="3" fill="#222" />
|
|
225
|
+
<circle cx="12" cy="14" r="1.5" fill="#444" />
|
|
226
|
+
<circle cx="36" cy="14" r="3" fill="#222" />
|
|
227
|
+
<circle cx="36" cy="14" r="1.5" fill="#444" />
|
|
228
|
+
{/* Headlights */}
|
|
229
|
+
<rect x="42" y="8" width="3" height="2" rx="1" fill="rgba(255,200,100,0.8)" />
|
|
230
|
+
<rect x="3" y="8" width="3" height="2" rx="1" fill="rgba(255,80,80,0.6)" />
|
|
231
|
+
</svg>
|
|
232
|
+
</div>
|
|
233
|
+
|
|
234
|
+
{/* Right: DL-style info fields */}
|
|
235
|
+
<div style={{ flex: 1, display: "flex", flexDirection: "column", gap: 0 }}>
|
|
236
|
+
{/* Name / Identity */}
|
|
237
|
+
<div style={{ marginBottom: 8 }}>
|
|
238
|
+
<DLField label="DLN" value={licenseNo} mono />
|
|
239
|
+
{identityName && (
|
|
240
|
+
<div style={{ marginTop: 4 }}>
|
|
241
|
+
<DLField label="NAME" value={identityName} />
|
|
242
|
+
</div>
|
|
243
|
+
)}
|
|
244
|
+
</div>
|
|
245
|
+
|
|
246
|
+
{/* Address */}
|
|
247
|
+
<DLField label="WALLET" value={truncAddr} mono />
|
|
248
|
+
|
|
249
|
+
{/* Two-column stats like a real DL */}
|
|
250
|
+
<div style={{ display: "flex", gap: 16, marginTop: 10 }}>
|
|
251
|
+
<div style={{ flex: 1 }}>
|
|
252
|
+
<DLField label="ISS" value={formatDate(wallet.walletAgeDays)} />
|
|
253
|
+
<div style={{ marginTop: 6 }}>
|
|
254
|
+
<DLField label="EXP" value="NEVER" accent />
|
|
255
|
+
</div>
|
|
256
|
+
</div>
|
|
257
|
+
<div style={{ flex: 1 }}>
|
|
258
|
+
<DLField label="HT" value={`${floorCount} floors`} />
|
|
259
|
+
<div style={{ marginTop: 6 }}>
|
|
260
|
+
<DLField label="WT" value={`${dims.width.toFixed(1)} units`} />
|
|
261
|
+
</div>
|
|
262
|
+
</div>
|
|
263
|
+
</div>
|
|
264
|
+
|
|
265
|
+
{/* Bottom stats row */}
|
|
266
|
+
<div
|
|
267
|
+
style={{
|
|
268
|
+
display: "flex",
|
|
269
|
+
gap: 12,
|
|
270
|
+
marginTop: 10,
|
|
271
|
+
padding: "8px 10px",
|
|
272
|
+
background: "rgba(255,255,255,0.02)",
|
|
273
|
+
border: "1px solid rgba(255,255,255,0.04)",
|
|
274
|
+
borderRadius: 8,
|
|
275
|
+
}}
|
|
276
|
+
>
|
|
277
|
+
<MiniStat label="TXN" value={wallet.txnCount.toLocaleString()} />
|
|
278
|
+
<MiniStat label="VOL" value={`${wallet.volumeTraded.toLocaleString()} SOL`} />
|
|
279
|
+
<MiniStat label="TOKENS" value={String(wallet.uniqueTokensSwapped ?? 0)} />
|
|
280
|
+
<MiniStat label="AGE" value={formatAge(wallet.walletAgeDays)} />
|
|
281
|
+
</div>
|
|
282
|
+
</div>
|
|
283
|
+
</div>
|
|
284
|
+
|
|
285
|
+
{/* Footer — barcode style */}
|
|
286
|
+
<div
|
|
287
|
+
style={{
|
|
288
|
+
display: "flex",
|
|
289
|
+
justifyContent: "space-between",
|
|
290
|
+
alignItems: "flex-end",
|
|
291
|
+
padding: "0 24px 12px",
|
|
292
|
+
}}
|
|
293
|
+
>
|
|
294
|
+
{/* Barcode */}
|
|
295
|
+
<svg width="140" height="24" viewBox="0 0 140 24">
|
|
296
|
+
{Array.from({ length: 35 }).map((_, i) => {
|
|
297
|
+
const charCode = wallet.address.charCodeAt(i % wallet.address.length);
|
|
298
|
+
const w = (charCode % 3) + 1;
|
|
299
|
+
return (
|
|
300
|
+
<rect
|
|
301
|
+
key={i}
|
|
302
|
+
x={i * 4}
|
|
303
|
+
y={0}
|
|
304
|
+
width={w}
|
|
305
|
+
height={24}
|
|
306
|
+
fill="rgba(255,255,255,0.15)"
|
|
307
|
+
/>
|
|
308
|
+
);
|
|
309
|
+
})}
|
|
310
|
+
</svg>
|
|
311
|
+
|
|
312
|
+
<div style={{ display: "flex", alignItems: "center", gap: 6 }}>
|
|
313
|
+
<span
|
|
314
|
+
style={{
|
|
315
|
+
fontSize: 9,
|
|
316
|
+
color: "rgba(255,255,255,0.2)",
|
|
317
|
+
}}
|
|
318
|
+
>
|
|
319
|
+
Last active: {formatLastActive(wallet.latestBlocktime)}
|
|
320
|
+
</span>
|
|
321
|
+
<span style={{ fontSize: 9, color: "rgba(255,255,255,0.12)" }}>|</span>
|
|
322
|
+
<div style={{ display: "flex", alignItems: "center", gap: 4 }}>
|
|
323
|
+
<span style={{ fontSize: 9, color: "rgba(255,255,255,0.2)" }}>Powered by</span>
|
|
324
|
+
<HeliusLogo />
|
|
325
|
+
</div>
|
|
326
|
+
</div>
|
|
327
|
+
</div>
|
|
328
|
+
</div>
|
|
329
|
+
);
|
|
330
|
+
},
|
|
331
|
+
);
|
|
332
|
+
|
|
333
|
+
function DLField({ label, value, mono, accent }: { label: string; value: string; mono?: boolean; accent?: boolean }) {
|
|
334
|
+
return (
|
|
335
|
+
<div>
|
|
336
|
+
<div
|
|
337
|
+
style={{
|
|
338
|
+
fontSize: 8,
|
|
339
|
+
fontWeight: 700,
|
|
340
|
+
letterSpacing: "0.15em",
|
|
341
|
+
color: "rgba(255,255,255,0.25)",
|
|
342
|
+
lineHeight: 1,
|
|
343
|
+
marginBottom: 2,
|
|
344
|
+
}}
|
|
345
|
+
>
|
|
346
|
+
{label}
|
|
347
|
+
</div>
|
|
348
|
+
<div
|
|
349
|
+
style={{
|
|
350
|
+
fontFamily: mono
|
|
351
|
+
? "'Geist Mono', ui-monospace, 'Cascadia Code', monospace"
|
|
352
|
+
: "'Geist Sans', ui-sans-serif, system-ui, sans-serif",
|
|
353
|
+
fontSize: 13,
|
|
354
|
+
fontWeight: 600,
|
|
355
|
+
color: accent ? "#E35930" : "rgba(255,255,255,0.75)",
|
|
356
|
+
lineHeight: 1.2,
|
|
357
|
+
}}
|
|
358
|
+
>
|
|
359
|
+
{value}
|
|
360
|
+
</div>
|
|
361
|
+
</div>
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function MiniStat({ label, value }: { label: string; value: string }) {
|
|
366
|
+
return (
|
|
367
|
+
<div style={{ flex: 1 }}>
|
|
368
|
+
<div
|
|
369
|
+
style={{
|
|
370
|
+
fontSize: 7,
|
|
371
|
+
fontWeight: 700,
|
|
372
|
+
letterSpacing: "0.12em",
|
|
373
|
+
color: "rgba(255,255,255,0.2)",
|
|
374
|
+
marginBottom: 2,
|
|
375
|
+
}}
|
|
376
|
+
>
|
|
377
|
+
{label}
|
|
378
|
+
</div>
|
|
379
|
+
<div
|
|
380
|
+
style={{
|
|
381
|
+
fontFamily: "'Geist Mono', ui-monospace, 'Cascadia Code', monospace",
|
|
382
|
+
fontSize: 11,
|
|
383
|
+
color: "rgba(255,255,255,0.6)",
|
|
384
|
+
fontWeight: 500,
|
|
385
|
+
}}
|
|
386
|
+
>
|
|
387
|
+
{value}
|
|
388
|
+
</div>
|
|
389
|
+
</div>
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/** Helius sun icon (small) */
|
|
394
|
+
function HeliusIcon() {
|
|
395
|
+
return (
|
|
396
|
+
<svg width="18" height="18" viewBox="0 0 120 120" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
397
|
+
<path d="M74.6 24.7C70 22.8 64.9 21.7 59.5 21.7C54.2 21.7 49.1 22.8 44.4 24.7L58.3 0.7C58.8-0.2 60.2-0.2 60.7 0.7L74.6 24.7Z" fill="#E35930"/>
|
|
398
|
+
<path d="M40.7 26.5C31.6 31.6 24.7 40.2 22 50.5L11.7 24.3C11.3 23.3 12.2 22.2 13.2 22.4L40.7 26.5Z" fill="#E35930"/>
|
|
399
|
+
<path d="M29 84.4L1 75.8C-0 75.5-0.3 74.1 0.4 73.4L21.2 54.1C20.9 56.2 20.7 58.3 20.7 60.5C20.7 69.5 23.8 77.8 29 84.4Z" fill="#E35930"/>
|
|
400
|
+
<path d="M59 99.3L34.4 116C33.5 116.6 32.3 116 32.2 114.9L30 85.6C37 93.8 47.4 99.1 59 99.3Z" fill="#E35930"/>
|
|
401
|
+
<path d="M89 85.8L86.8 114.9C86.7 116 85.5 116.6 84.6 116L60.1 99.3C71.6 99.1 82 93.9 89 85.8Z" fill="#E35930"/>
|
|
402
|
+
<path d="M118 75.7L90.2 84.3C95.3 77.7 98.3 69.4 98.3 60.5C98.3 58.3 98.1 56.1 97.8 54L118.6 73.3C119.4 74 119.1 75.4 118 75.7Z" fill="#E35930"/>
|
|
403
|
+
<path d="M107.4 24.3L97.1 50.6C94.3 40.3 87.5 31.6 78.4 26.6L105.8 22.4C106.9 22.3 107.8 23.3 107.4 24.3Z" fill="#E35930"/>
|
|
404
|
+
</svg>
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/** Inline Helius logo SVG (icon + wordmark) matching /helius-logo.svg for html-to-image compatibility */
|
|
409
|
+
function HeliusLogo() {
|
|
410
|
+
return (
|
|
411
|
+
<svg
|
|
412
|
+
width="56"
|
|
413
|
+
height="12"
|
|
414
|
+
viewBox="0 0 562 118"
|
|
415
|
+
fill="none"
|
|
416
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
417
|
+
>
|
|
418
|
+
<path d="M74.6261 24.7253C69.9876 22.7665 64.8892 21.6871 59.531 21.6871C54.1727 21.6871 49.0743 22.7665 44.4358 24.7253L58.3013 0.709587C58.8412 -0.229856 60.2007 -0.229856 60.7406 0.709587L74.6161 24.7253H74.6261Z" fill="#E84125"/>
|
|
419
|
+
<path d="M40.7468 26.5242C31.6098 31.5812 24.742 40.2161 22.0128 50.54L11.7061 24.2856C11.3063 23.2762 12.156 22.2168 13.2257 22.3767L40.7468 26.5242Z" fill="#E84125"/>
|
|
420
|
+
<path d="M28.9906 84.4099L0.999611 75.7751C-0.0400563 75.4552 -0.33996 74.126 0.449787 73.3965L21.2431 54.1079C20.9032 56.1767 20.7233 58.3054 20.7233 60.4741C20.7233 69.5088 23.8123 77.8238 28.9906 84.4099Z" fill="#E84125"/>
|
|
421
|
+
<path d="M58.951 99.2611L34.4089 115.981C33.5091 116.591 32.2895 116.011 32.2096 114.922L30.0203 85.6492C37.018 93.8443 47.3747 99.0812 58.951 99.2511V99.2611Z" fill="#E84125"/>
|
|
422
|
+
<path d="M88.9513 85.7692L86.772 114.892C86.692 115.971 85.4724 116.561 84.5727 115.951L60.0806 99.2612C71.6269 99.1013 81.9536 93.9043 88.9513 85.7692Z" fill="#E84125"/>
|
|
423
|
+
<path d="M118.042 75.6851L90.1711 84.28C95.2895 77.7139 98.3385 69.4488 98.3385 60.4741C98.3385 58.2754 98.1486 56.1167 97.7987 54.0179L118.582 73.2965C119.382 74.0361 119.072 75.3653 118.042 75.6751V75.6851Z" fill="#E84125"/>
|
|
424
|
+
<path d="M107.355 24.3356L97.0588 50.5801C94.3396 40.2662 87.4918 31.6413 78.3848 26.5643L105.836 22.4268C106.906 22.2668 107.755 23.3262 107.355 24.3356Z" fill="#E84125"/>
|
|
425
|
+
<path d="M59.5307 107.636C53.8026 107.636 49.1641 112.273 49.1641 118H69.9074C69.9074 112.273 65.2689 107.636 59.5407 107.636H59.5307Z" fill="#E84125"/>
|
|
426
|
+
<path d="M97.4388 90.6563C93.8699 95.1336 94.5997 101.66 99.0782 105.228L112.004 89.0172C107.526 85.4494 100.998 86.1789 97.4288 90.6563H97.4388Z" fill="#E84125"/>
|
|
427
|
+
<path d="M105.936 50.57C107.206 56.1567 112.774 59.6446 118.352 58.3653L113.733 38.1473C108.145 39.4166 104.656 44.9833 105.936 50.56V50.57Z" fill="#E84125"/>
|
|
428
|
+
<path d="M80.4942 18.8588C85.6525 21.3473 91.8505 19.1786 94.3397 14.0116L75.6557 5.01697C73.1665 10.1739 75.3358 16.3702 80.5042 18.8588H80.4942Z" fill="#E84125"/>
|
|
429
|
+
<path d="M38.0978 18.8588C43.2561 16.3702 45.4254 10.1739 42.9362 5.01697L24.2522 14.0116C26.7414 19.1686 32.9394 21.3373 38.0978 18.8488V18.8588Z" fill="#E84125"/>
|
|
430
|
+
<path d="M13.3958 50.57C14.6654 44.9833 11.1765 39.4266 5.58826 38.1573L0.979736 58.3754C6.56795 59.6446 12.1262 56.1567 13.3958 50.57Z" fill="#E84125"/>
|
|
431
|
+
<path d="M8.49731 89.0172L21.4332 105.228C25.9117 101.66 26.6415 95.1336 23.0726 90.6563C19.5038 86.1789 12.9759 85.4494 8.49731 89.0172Z" fill="#E84125"/>
|
|
432
|
+
<path d="M177.773 30.1321V59.8145H212.612V31.8511H228.487V98.4116H212.612V73.9661H177.773V98.4116H161.898V34.4195L177.773 30.1321Z" fill="#E84125"/>
|
|
433
|
+
<path d="M257.227 84.25H305.792L301.503 98.4016H241.363V31.8411H305.802L301.513 45.9927H257.237V58.0855H298.504V72.2371H257.237V84.25H257.227Z" fill="#E84125"/>
|
|
434
|
+
<path d="M331.274 31.8411V84.25H378.119L373.83 98.4016H315.399V31.8411H331.274Z" fill="#E84125"/>
|
|
435
|
+
<path d="M400.861 31.8411V98.4016H384.987V31.8411H400.861Z" fill="#E84125"/>
|
|
436
|
+
<path d="M467.44 31.8411H483.315V71.9872C483.315 91.3757 467.44 99.8607 446.937 99.8607C426.433 99.8607 411.668 90.7661 411.668 71.9872V34.4195L427.543 30.1321V71.9872C427.543 81.7614 434.061 85.4593 447.706 85.4593C461.352 85.4593 467.44 81.5116 467.44 71.9872V31.8411Z" fill="#E84125"/>
|
|
437
|
+
<path d="M491.382 52.0891C491.382 41.8851 499.45 31.8411 515.235 31.8411H560.54L556.252 45.9927H515.405C510.516 45.9927 508.107 49.0808 508.107 52.0791C508.107 55.0773 510.596 58.0855 515.405 58.0855H538.397C553.922 58.0855 561.99 68.1195 561.99 78.3335C561.99 88.5474 554.352 98.4016 538.397 98.4016H491.382L495.671 84.25H538.227C543.036 84.25 545.525 81.4217 545.525 78.3335C545.525 75.2453 543.126 72.1571 538.227 72.1571H515.235C499.19 72.1571 491.382 62.1231 491.382 52.0891Z" fill="#E84125"/>
|
|
438
|
+
</svg>
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
export default CitizenCard;
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef, useState } from "react";
|
|
4
|
+
import { createPortal } from "react-dom";
|
|
5
|
+
import { WalletBuilding } from "@/types/wallet";
|
|
6
|
+
import CitizenCard from "./CitizenCard";
|
|
7
|
+
import {
|
|
8
|
+
exportCardAsPng,
|
|
9
|
+
downloadBlob,
|
|
10
|
+
copyBlobToClipboard,
|
|
11
|
+
} from "@/lib/export-card";
|
|
12
|
+
|
|
13
|
+
interface CitizenCardModalProps {
|
|
14
|
+
wallet: WalletBuilding;
|
|
15
|
+
identityName?: string | null;
|
|
16
|
+
onClose: () => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export default function CitizenCardModal({
|
|
20
|
+
wallet,
|
|
21
|
+
identityName,
|
|
22
|
+
onClose,
|
|
23
|
+
}: CitizenCardModalProps) {
|
|
24
|
+
const cardRef = useRef<HTMLDivElement>(null);
|
|
25
|
+
const [loading, setLoading] = useState(false);
|
|
26
|
+
const [copied, setCopied] = useState(false);
|
|
27
|
+
const [xHint, setXHint] = useState(false);
|
|
28
|
+
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
function onKey(e: KeyboardEvent) {
|
|
31
|
+
if (e.key === "Escape") onClose();
|
|
32
|
+
}
|
|
33
|
+
window.addEventListener("keydown", onKey);
|
|
34
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
35
|
+
}, [onClose]);
|
|
36
|
+
|
|
37
|
+
async function handleDownload() {
|
|
38
|
+
if (!cardRef.current) return;
|
|
39
|
+
setLoading(true);
|
|
40
|
+
try {
|
|
41
|
+
const blob = await exportCardAsPng(cardRef.current);
|
|
42
|
+
downloadBlob(blob, `solanapolis-${wallet.address.slice(0, 8)}.png`);
|
|
43
|
+
} finally {
|
|
44
|
+
setLoading(false);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function handleCopy() {
|
|
49
|
+
if (!cardRef.current) return;
|
|
50
|
+
setLoading(true);
|
|
51
|
+
try {
|
|
52
|
+
const blob = await exportCardAsPng(cardRef.current);
|
|
53
|
+
await copyBlobToClipboard(blob);
|
|
54
|
+
setCopied(true);
|
|
55
|
+
setTimeout(() => setCopied(false), 2000);
|
|
56
|
+
} finally {
|
|
57
|
+
setLoading(false);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function handlePostOnX() {
|
|
62
|
+
if (!cardRef.current) return;
|
|
63
|
+
setLoading(true);
|
|
64
|
+
try {
|
|
65
|
+
const blob = await exportCardAsPng(cardRef.current);
|
|
66
|
+
await copyBlobToClipboard(blob);
|
|
67
|
+
setXHint(true);
|
|
68
|
+
setTimeout(() => setXHint(false), 5000);
|
|
69
|
+
const text = encodeURIComponent(
|
|
70
|
+
"I'm now a citizen of Solanapolis!\n\n[paste your ID card image here]\n\nhttps://solanapolis.com/"
|
|
71
|
+
);
|
|
72
|
+
window.open(
|
|
73
|
+
`https://x.com/intent/post?text=${text}`,
|
|
74
|
+
"_blank",
|
|
75
|
+
"noopener,noreferrer"
|
|
76
|
+
);
|
|
77
|
+
} finally {
|
|
78
|
+
setLoading(false);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return createPortal(
|
|
83
|
+
<div
|
|
84
|
+
className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm"
|
|
85
|
+
onClick={onClose}
|
|
86
|
+
>
|
|
87
|
+
<div
|
|
88
|
+
className="relative mx-4 flex flex-col items-center gap-4"
|
|
89
|
+
onClick={(e) => e.stopPropagation()}
|
|
90
|
+
>
|
|
91
|
+
{/* Close button */}
|
|
92
|
+
<button
|
|
93
|
+
onClick={onClose}
|
|
94
|
+
className="absolute top-2 right-2 z-10 w-7 h-7 flex items-center justify-center rounded-full bg-white/10 text-white/50 hover:text-white hover:bg-white/20 transition-colors cursor-pointer"
|
|
95
|
+
>
|
|
96
|
+
<svg
|
|
97
|
+
width="10"
|
|
98
|
+
height="10"
|
|
99
|
+
viewBox="0 0 10 10"
|
|
100
|
+
fill="none"
|
|
101
|
+
stroke="currentColor"
|
|
102
|
+
strokeWidth="1.5"
|
|
103
|
+
>
|
|
104
|
+
<path d="M1 1l8 8M9 1l-8 8" />
|
|
105
|
+
</svg>
|
|
106
|
+
</button>
|
|
107
|
+
|
|
108
|
+
{/* Card preview */}
|
|
109
|
+
<div className="rounded-xl overflow-hidden shadow-2xl border border-white/[0.08]">
|
|
110
|
+
<CitizenCard
|
|
111
|
+
ref={cardRef}
|
|
112
|
+
wallet={wallet}
|
|
113
|
+
identityName={identityName}
|
|
114
|
+
/>
|
|
115
|
+
</div>
|
|
116
|
+
|
|
117
|
+
{/* Action buttons */}
|
|
118
|
+
<div className="flex gap-3">
|
|
119
|
+
<button
|
|
120
|
+
onClick={handleDownload}
|
|
121
|
+
disabled={loading}
|
|
122
|
+
className="px-4 py-2 bg-[#E35930]/15 hover:bg-[#E35930]/25 border border-[#E35930]/20 rounded-xl text-sm text-[#E35930] transition-colors disabled:opacity-50 cursor-pointer"
|
|
123
|
+
>
|
|
124
|
+
{loading ? "Exporting..." : "Download PNG"}
|
|
125
|
+
</button>
|
|
126
|
+
<button
|
|
127
|
+
onClick={handleCopy}
|
|
128
|
+
disabled={loading}
|
|
129
|
+
className="px-4 py-2 bg-white/[0.06] hover:bg-white/[0.1] border border-white/[0.08] rounded-xl text-sm text-white/60 transition-colors disabled:opacity-50 cursor-pointer"
|
|
130
|
+
>
|
|
131
|
+
{copied ? "Copied!" : "Copy to Clipboard"}
|
|
132
|
+
</button>
|
|
133
|
+
<button
|
|
134
|
+
onClick={handlePostOnX}
|
|
135
|
+
disabled={loading}
|
|
136
|
+
className="px-4 py-2 bg-white/[0.06] hover:bg-white/[0.1] border border-white/[0.08] rounded-xl text-sm text-white/60 transition-colors disabled:opacity-50 cursor-pointer flex items-center gap-1.5"
|
|
137
|
+
>
|
|
138
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor">
|
|
139
|
+
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
|
|
140
|
+
</svg>
|
|
141
|
+
Post on X
|
|
142
|
+
</button>
|
|
143
|
+
</div>
|
|
144
|
+
{xHint && (
|
|
145
|
+
<p className="text-xs text-white/40 text-center">
|
|
146
|
+
Image copied to clipboard — paste it into your tweet
|
|
147
|
+
</p>
|
|
148
|
+
)}
|
|
149
|
+
</div>
|
|
150
|
+
</div>,
|
|
151
|
+
document.body,
|
|
152
|
+
);
|
|
153
|
+
}
|