0
GAMES#5f91ba4f
finger selector
@GigaChadΒ·deposited 2mo agoΒ·updated 1mo agoΒ·126 views
GAMES#5f91ba4f
finger selector
GI
@GigaChad
126Views
0Comments
0Forks
0Saves
SHARE Β· REMIX
finger selector β a JSX games widget by @GigaChad.
CONTROLS
No comments yet. Be the first!
β¦ Remix with AI
SDK in this widgetNo Vibes SDK features detected yet
Generated prompt
You are helping me modify a vibe-coded widget from itjustvibes.com.
[VIBE CODE: "finger selector" by @GigaChad]
Source: https://itjustvibes.com/GigaChad/finger-selector
Type: React/JSX
--- SOURCE CODE ---
```jsx
import { useState, useEffect, useRef, useCallback } from "react";
const COLORS = [
"#FF3B5C", "#00E5A0", "#4D8BFF", "#FFD23F",
"#FF6B35", "#A855F7", "#06D6A0", "#FF006E",
"#3A86FF", "#FFBE0B", "#FB5607", "#8338EC",
];
const COUNTDOWN_SECONDS = 5;
function vibrate(pattern) {
if (navigator.vibrate) navigator.vibrate(pattern);
}
export default function FingerSelector() {
const [fingers, setFingers] = useState(new Map());
const [phase, setPhase] = useState("waiting"); // waiting | countdown | selecting | done
const [countdown, setCountdown] = useState(COUNTDOWN_SECONDS);
const [winnerId, setWinnerId] = useState(null);
const [pulseScale, setPulseScale] = useState(1);
const timerRef = useRef(null);
const countdownRef = useRef(null);
const colorMapRef = useRef(new Map());
const colorIndexRef = useRef(0);
const fingersRef = useRef(new Map());
const getColor = useCallback((id) => {
if (!colorMapRef.current.has(id)) {
colorMapRef.current.set(id, COLORS[colorIndexRef.current % COLORS.length]);
colorIndexRef.current++;
}
return colorMapRef.current.get(id);
}, []);
const resetCountdown = useCallback(() => {
if (timerRef.current) clearTimeout(timerRef.current);
if (countdownRef.current) clearInterval(countdownRef.current);
setCountdown(COUNTDOWN_SECONDS);
setPhase("waiting");
setWinnerId(null);
}, []);
const startCountdown = useCallback(() => {
if (timerRef.current) clearTimeout(timerRef.current);
if (countdownRef.current) clearInterval(countdownRef.current);
setPhase("countdown");
setCountdown(COUNTDOWN_SECONDS);
let t = COUNTDOWN_SECONDS;
countdownRef.current = setInterval(() => {
t -= 1;
setCountdown(t);
vibrate(30);
if (t <= 0) {
clearInterval(countdownRef.current);
countdownRef.current = null;
selectWinner();
}
}, 1000);
}, []);
const selectWinner = useCallback(() => {
setPhase("selecting");
const currentFingers = fingersRef.current;
const ids = Array.from(currentFingers.keys());
if (ids.length === 0) {
setPhase("waiting");
return;
}
// Rapid flash selection effect
let flashes = 0;
const maxFlashes = 12;
const flashInterval = setInterval(() => {
const randomId = ids[Math.floor(Math.random() * ids.length)];
setWinnerId(randomId);
vibrate(15);
flashes++;
if (flashes >= maxFlashes) {
clearInterval(flashInterval);
const finalWinner = ids[Math.floor(Math.random() * ids.length)];
setWinnerId(finalWinner);
setPhase("done");
vibrate([100, 50, 100, 50, 200]);
}
}, 120);
}, []);
const handleTouchStart = useCallback((e) => {
e.preventDefault();
const next = new Map(fingersRef.current);
for (const touch of e.changedTouches) {
getColor(touch.identifier);
next.set(touch.identifier, { x: touch.clientX, y: touch.clientY });
}
fingersRef.current = next;
setFingers(new Map(next));
vibrate(10);
if (next.size >= 2) {
// Reset and start countdown on each new finger
if (countdownRef.current) clearInterval(countdownRef.current);
if (timerRef.current) clearTimeout(timerRef.current);
setWinnerId(null);
setPhase("waiting");
timerRef.current = setTimeout(() => {
startCountdown();
}, 300);
}
}, [getColor, startCountdown]);
const handleTouchMove = useCallback((e) => {
e.preventDefault();
const next = new Map(fingersRef.current);
for (const touch of e.changedTouches) {
if (next.has(touch.identifier)) {
next.set(touch.identifier, { x: touch.clientX, y: touch.clientY });
}
}
fingersRef.current = next;
setFingers(new Map(next));
}, []);
const handleTouchEnd = useCallback((e) => {
e.preventDefault();
const next = new Map(fingersRef.current);
for (const touch of e.changedTouches) {
next.delete(touch.identifier);
colorMapRef.current.delete(touch.identifier);
}
fingersRef.current = next;
setFingers(new Map(next));
if (next.size < 2 && phase !== "done") {
resetCountdown();
}
}, [phase, resetCountdown]);
const handleReset = useCallback(() => {
resetCountdown();
fingersRef.current = new Map();
setFingers(new Map());
colorMapRef.current = new Map();
colorIndexRef.current = 0;
}, [resetCountdown]);
// Pulse animation for winner
useEffect(() => {
if (phase !== "done") return;
let frame;
let start = Date.now();
const animate = () => {
const elapsed = (Date.now() - start) / 1000;
setPulseScale(1 + 0.15 * Math.sin(elapsed * 4));
frame = requestAnimationFrame(animate);
};
frame = requestAnimationFrame(animate);
return () => cancelAnimationFrame(frame);
}, [phase]);
// Cleanup on unmount
useEffect(() => {
return () => {
if (timerRef.current) clearTimeout(timerRef.current);
if (countdownRef.current) clearInterval(countdownRef.current);
};
}, []);
const fingerEntries = Array.from(fingers.entries());
return (
<div
style={{
position: "fixed", inset: 0,
background: "#0A0A0F",
touchAction: "none",
userSelect: "none",
WebkitUserSelect: "none",
overflow: "hidden",
fontFamily: "'SF Pro Display', 'Helvetica Neue', sans-serif",
}}
onTouchStart={handleTouchStart}
onTouchMove={handleTouchMove}
onTouchEnd={handleTouchEnd}
onTouchCancel={handleTouchEnd}
>
{/* Subtle grid background */}
<div style={{
position: "absolute", inset: 0,
backgroundImage: `
linear-gradient(rgba(255,255,255,0.02) 1px, transparent 1px),
linear-gradient(90deg, rgba(255,255,255,0.02) 1px, transparent 1px)
`,
backgroundSize: "40px 40px",
}} />
{/* Ambient glow from active fingers */}
{fingerEntries.map(([id, pos]) => {
const color = colorMapRef.current.get(id) || "#fff";
const isWinner = phase === "done" && id === winnerId;
return (
<div key={`glow-${id}`} style={{
position: "absolute",
left: pos.x, top: pos.y,
width: isWinner ? 400 : 250,
height: isWinner ? 400 : 250,
transform: "translate(-50%, -50%)",
borderRadius: "50%",
background: `radial-gradient(circle, ${color}${isWinner ? "30" : "15"} 0%, transparent 70%)`,
transition: "width 0.4s, height 0.4s",
pointerEvents: "none",
}} />
);
})}
{/* Finger dots */}
{fingerEntries.map(([id, pos]) => {
const color = colorMapRef.current.get(id) || "#fff";
const isWinner = phase === "done" && id === winnerId;
const isLoser = phase === "done" && id !== winnerId;
const dotSize = isWinner ? 90 * pulseScale : isLoser ? 40 : 64;
return (
<div key={id} style={{
position: "absolute",
left: pos.x, top: pos.y,
transform: "translate(-50%, -50%)",
display: "flex", alignItems: "center", justifyContent: "center",
pointerEvents: "none",
transition: isWinner ? "none" : "all 0.3s ease",
}}>
{/* Outer ring */}
<div style={{
position: "absolute",
width: dotSize + 20,
height: dotSize + 20,
borderRadius: "50%",
border: `2px solid ${color}${isLoser ? "30" : "60"}`,
animation: isWinner ? undefined : "spin 8s linear infinite",
transition: "all 0.3s ease",
}} />
{/* Inner dot */}
<div style={{
width: dotSize,
height: dotSize,
borderRadius: "50%",
background: `radial-gradient(circle at 35% 35%, ${color}ee, ${color}88)`,
boxShadow: isWinner
? `0 0 40px ${color}80, 0 0 80px ${color}40, inset 0 -4px 12px rgba(0,0,0,0.3)`
: `0 0 20px ${color}40, inset 0 -3px 8px rgba(0,0,0,0.3)`,
opacity: isLoser ? 0.3 : 1,
transition: isWinner ? "none" : "all 0.4s ease",
}} />
{/* Winner crown / label */}
{isWinner && (
<div style={{
position: "absolute",
top: -50,
fontSize: 28,
animation: "fadeIn 0.4s ease",
}}>
π
</div>
)}
</div>
);
})}
{/* Center UI */}
<div style={{
position: "absolute",
top: "50%", left: "50%",
transform: "translate(-50%, -50%)",
textAlign: "center",
pointerEvents: "none",
zIndex: 10,
}}>
{fingers.size === 0 && phase === "waiting" && (
<div style={{ animation: "fadeIn 0.5s ease" }}>
<div style={{
fontSize: 48,
marginBottom: 16,
opacity: 0.9,
}}>π</div>
<div style={{
color: "rgba(255,255,255,0.5)",
fontSize: 18,
fontWeight: 300,
letterSpacing: 2,
textTransform: "uppercase",
}}>
Everyone place a finger
</div>
</div>
)}
{fingers.size === 1 && phase === "waiting" && (
<div style={{
color: "rgba(255,255,255,0.4)",
fontSize: 15,
fontWeight: 300,
letterSpacing: 1.5,
textTransform: "uppercase",
}}>
Waiting for more players...
</div>
)}
{phase === "countdown" && (
<div style={{ animation: "fadeIn 0.2s ease" }}>
<div style={{
fontSize: 96,
fontWeight: 700,
color: countdown <= 2 ? "#FF3B5C" : "rgba(255,255,255,0.9)",
textShadow: countdown <= 2
? "0 0 40px rgba(255,59,92,0.5)"
: "0 0 30px rgba(255,255,255,0.1)",
lineHeight: 1,
transition: "color 0.3s, text-shadow 0.3s",
}}>
{countdown}
</div>
<div style={{
color: "rgba(255,255,255,0.35)",
fontSize: 12,
fontWeight: 400,
letterSpacing: 3,
textTransform: "uppercase",
marginTop: 12,
}}>
Hold still
</div>
</div>
)}
{phase === "selecting" && (
<div style={{
fontSize: 14,
color: "rgba(255,255,255,0.5)",
letterSpacing: 3,
textTransform: "uppercase",
animation: "pulse 0.5s ease infinite",
}}>
Selecting...
</div>
)}
{phase === "done" && (
<div style={{ animation: "fadeIn 0.5s ease" }}>
<div style={{
fontSize: 22,
fontWeight: 600,
color: colorMapRef.current.get(winnerId) || "#fff",
letterSpacing: 1,
textShadow: `0 0 30px ${colorMapRef.current.get(winnerId) || "#fff"}60`,
}}>
YOU GO FIRST
</div>
</div>
)}
</div>
{/* Player count */}
{fingers.size >= 2 && phase !== "done" && (
<div style={{
position: "absolute",
top: 50,
left: "50%",
transform: "translateX(-50%)",
color: "rgba(255,255,255,0.3)",
fontSize: 13,
letterSpacing: 2,
textTransform: "uppercase",
pointerEvents: "none",
}}>
{fingers.size} players
</div>
)}
{/* Reset button */}
{phase === "done" && (
<div
onClick={handleReset}
style={{
position: "absolute",
bottom: 60,
left: "50%",
transform: "translateX(-50%)",
padding: "14px 36px",
borderRadius: 40,
background: "rgba(255,255,255,0.08)",
backdropFilter: "blur(10px)",
color: "rgba(255,255,255,0.7)",
fontSize: 14,
fontWeight: 500,
letterSpacing: 2,
textTransform: "uppercase",
cursor: "pointer",
border: "1px solid rgba(255,255,255,0.1)",
pointerEvents: "auto",
zIndex: 20,
animation: "fadeIn 0.6s ease",
}}
>
Play Again
</div>
)}
{/* Progress bar */}
{phase === "countdown" && (
<div style={{
position: "absolute",
bottom: 0, left: 0,
width: "100%",
height: 4,
background: "rgba(255,255,255,0.05)",
}}>
<div style={{
height: "100%",
width: `${(countdown / COUNTDOWN_SECONDS) * 100}%`,
background: countdown <= 2
? "linear-gradient(90deg, #FF3B5C, #FF6B35)"
: "linear-gradient(90deg, #4D8BFF, #00E5A0)",
transition: "width 1s linear, background 0.3s",
}} />
</div>
)}
<style>{`
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
@keyframes fadeIn {
from { opacity: 0; transform: scale(0.9); }
to { opacity: 1; transform: scale(1); }
}
@keyframes pulse {
0%, 100% { opacity: 0.5; }
50% { opacity: 1; }
}
`}</style>
</div>
);
}
```
[REQUESTED CHANGES]
(no specific request β apply your best judgment)
--- HOW TO RESPOND (READ FIRST) ---
Before writing any code, follow this exact process:
1. **ANALYZE** the widget source code provided above and identify:
a. Which Vibes SDK features it already uses (vibes.save, vibes.load, vibes.shared.join, etc.)
b. Which SDK features would genuinely benefit THIS specific widget β tailored to what it does, not a dump of everything available.
2. **PRESENT A NUMBERED LIST** covering:
- SDK features currently active in this widget
- New SDK features that would concretely improve this widget (be specific: why this widget, what it enables)
3. **WAIT** β do not write any code yet. Reply with your analysis and numbered list, then stop and ask the user which numbered items they want.
4. **IMPLEMENT ONLY** the items the user confirms, plus any explicit change they requested. Do not add unrequested features.
**IMPORTANT β Shared state room names:**
If you add vibes.shared.join(), do NOT use a hardcoded string literal as the room name (e.g. vibes.shared.join("lobby")) unless the user explicitly wants ALL viewers to share one single global state. A hardcoded room name means every person who visits this widget reads and writes the same shared state β it is a global room. For per-user or per-session isolation, derive the room name from a variable (e.g. a user ID, session token, or random value). When in doubt, use vibes.save/vibes.load for per-user persistence instead.
--- VIBES SDK CONTEXT ---
## Vibes SDK Reference
You are building an HTML widget for It Just Vibes (itjustvibes.com). The Vibes SDK is auto-injected β do NOT add a script tag. Just use `window.vibes` (or just `vibes`).
### Setup
Wrap your startup code in `vibes.onReady`:
```js
vibes.onReady(async () => {
const saved = await vibes.load("myKey");
// your widget logic here
});
```
### State (Per-User Persistence)
Every user gets their own isolated state per widget. All methods return Promises.
| Method | Description |
|--------|-------------|
| `await vibes.save(key, value)` | Save JSON-serializable data |
| `await vibes.load(key)` | Load saved data (returns `null` if not found) |
| `await vibes.delete(key)` | Delete a saved key |
| `await vibes.listKeys()` | Get array of all saved key names |
**Key rules:**
- Keys are strings, max 64 characters, alphanumeric + dashes/underscores
- Values must be JSON-serializable (objects, arrays, strings, numbers, booleans)
- Max 100KB per value, 500KB per widget per user, 5MB per user total
- Max 100 keys per widget per user
### Fetch Proxy
Use direct `fetch()` for APIs with permissive CORS headers (`Access-Control-Allow-Origin: *`). Use `vibes.fetch` for APIs without CORS headers (the proxy handles cross-origin requests):
```js
const resp = await vibes.fetch("https://api.example.com/data", {
method: "GET", // GET, POST, PUT, DELETE
headers: {}, // optional headers
body: null, // optional body (string)
timeout: null // optional timeout in ms
});
const data = await resp.json(); // or resp.text()
console.log(resp.status, resp.ok);
```
### Multiplayer (Shared State)
Real-time shared state across all users viewing the same widget.
```js
// Join a room (call once at startup)
await vibes.shared.join("lobby", { persistent: true });
// Set shared state (broadcasts to all users)
await vibes.shared.set("score", { player1: 10, player2: 7 });
// Read shared state (synchronous, returns last known value)
const score = vibes.shared.get("score");
// Listen for changes to a specific key
vibes.shared.onChange("score", (newValue) => {
console.log("Score updated:", newValue);
});
// Listen for any shared state change
vibes.shared.onAny((key, value) => {
console.log(key, "changed to", value);
});
// Get number of connected users
const count = await vibes.shared.getUserCount();
// Leave room
vibes.shared.leave();
// Clear all shared state for this room
await vibes.shared.clear();
```
**Shared state options:**
- `{ persistent: true }` β state survives page reloads (stored server-side)
- Default room name is "__default__" if omitted
### Agent-Accessible State (vibes.ai.*)
State written with the standard `vibes.save` / `vibes.load` methods is private to each user and is **NOT readable by AI agents**. To share state with an AI agent (via the MCP connector), use the `vibes.ai` sub-namespace:
| Method | Description |
|--------|-------------|
| `await vibes.ai.setState(key, value)` | Write agent-readable state. Key is stored internally as `ai/<key>`. |
| `await vibes.ai.getState(key)` | Read agent-readable state. Returns `null` if key absent. |
| `await vibes.ai.listKeys()` | List all agent-readable keys (without the `ai/` prefix). |
**Important rules:**
- Regular `vibes.save()` / `vibes.load()` is **NOT agent-accessible** β use `vibes.ai.*` for state you want agents to read.
- The widget **owner** must enable agent access in **Manage β Agent tab** before any agent can read or write `vibes.ai.*` state.
- Keys are auto-prefixed to `ai/` internally; you supply just the short key (e.g. `'context'`).
```js
vibes.onReady(async () => {
// Write state an AI agent can later read
await vibes.ai.setState('context', { currentLevel: 3, score: 1500 });
// Read it back (same auto-prefix applies)
const ctx = await vibes.ai.getState('context');
// List all agent-accessible keys for this widget
const keys = await vibes.ai.listKeys(); // e.g. ['context']
});
```
### Rules
1. **Do NOT use localStorage, sessionStorage, or window.storage** β they are blocked or undefined in the sandbox. Use `vibes.save`/`vibes.load` instead.
2. **Do NOT add a script tag to import the SDK** β it is auto-injected.
3. **Wrap startup code in `vibes.onReady()`** β the SDK may not be ready immediately.
4. **Await all SDK calls** β every method (except `vibes.shared.get`) returns a Promise.
5. **External JS libraries are allowed and encouraged** β load them via a `<script src>` tag from a reputable CDN (jsDelivr, unpkg, or cdnjs) with a pinned version, or inline the library source. The only exception: do NOT add a script tag for the Vibes SDK itself β it is auto-injected (see Rule 2).
6. **Keep total code under 100KB** β that is the default widget size limit (your account limit may be higher).
### Rate Limits
| Operation | Limit |
|-----------|-------|
| Writes (save + delete combined) | 30/min |
| Reads (load) | 60/min |
| List keys | 30/min |
| Fetch proxy | Rate limited per widget |
### Error Handling
All SDK methods can reject. Wrap in try/catch:
```js
try {
await vibes.save("key", value);
} catch (err) {
console.error("Save failed:", err.message);
}
```
Fetch proxy errors include `err.code`: `"RATE_LIMITED"`, `"BLOCKED"`, `"FETCH_ERROR"`.
### Data Export
Export rows of data as CSV or Excel directly from your widget.
```js
// Register dataset for the platform's "Export data" button
// AND optionally trigger an immediate download
vibes.exportData(rows, { filename: 'results.csv' })
// rows: Array of arrays (each inner array is one row; first row = headers)
// options.filename: sets the download filename and format (csv or xlsx)
// options.directDownload: false to only register without downloading (default: true)
```
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `filename` | string | `'data.csv'` | Download filename; extension determines format (`.csv` or `.xlsx`) |
| `directDownload` | boolean | `true` | Trigger immediate browser download in addition to registering the dataset |
**Notes:**
- First call registers the dataset so the widget chrome shows an "Export data" button
- Use `.csv` extension for CSV, `.xlsx` for Excel
- CSV injection safety is automatic (formula-starting cells prefixed with `'`)
- Data stays in the browser β no server round-trip
--- FORK & RESUBMIT INSTRUCTION ---
Return the complete, self-contained JSX file with all changes applied.
It should be ready to paste into itjustvibes.com/submit to create a new fork.
Include this comment at the top of the output:
/* Forked from: https://itjustvibes.com/GigaChad/finger-selector */