Skip to content

Commit 07656a1

Browse files
Spins and the Wall shuffle. Socials open the apps.
The reel no longer starts on Lakers. Discord copies, then opens Discord. Export desk saves a real JSON file. About is four beats, no fluff.
1 parent ebac795 commit 07656a1

17 files changed

Lines changed: 260 additions & 58 deletions

HOW-TO-PLAY.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,9 @@ One pull on tape: [docs/tape/demo.mp4](docs/tape/demo.mp4)
3232
| **Save the card** | Downloads `first-bucket-{team}-{wins}.png`. |
3333
| **Share** | Phone: the system share sheet (Messages, TikTok, Snapchat, Discord). Desktop: the tray. |
3434
| **Pin this walk** | Keeps it on the rail. Unpin anytime. Nothing is pinned until you pin it. |
35-
| **X / Threads / Reddit / Facebook** | Open those apps with the walk. |
36-
| **TikTok / Snapchat** | Saves a 9:16 plate. Open the app and post it. |
37-
| **Discord** | Copies the walk for paste. |
35+
| **X / Threads / Reddit / Facebook / WhatsApp / Telegram / LinkedIn** | Opens that app with the walk. |
36+
| **Discord** | Copies the walk, then opens Discord. Paste it. |
37+
| **TikTok / Snapchat / Instagram** | Saves a 9:16 plate, copies the caption, opens the app. |
3838
| **Copy the walk** | Copies the public walk URL so you can paste it. |
3939
| **Open the walk** | Same five, same nights, forever. |
4040

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
**Rip a pack. Five names. Eighty-two nights. Send the card.**
44

5+
Basketball pack · NBA · WNBA · sports cards · holographic foil · WebGL · 82-0
6+
57
A browser basketball pack you can fork. Not a shop. Not a sportsbook. Not the NBA.
68

79
**Live booth:** [first-bucket-studios.vercel.app](https://first-bucket-studios.vercel.app)

SUPPORT.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,13 @@ Save is a native file link (`<a download>` with a PNG data URL). A nested previe
2222

2323
## Share on social did nothing
2424

25-
**Share** is the system sheet on a phone (Messages, TikTok, Snapchat, Discord). On a desktop it opens the tray: X, Threads, Reddit, Facebook, TikTok, Snapchat, Discord.
25+
**Share** is the phone sheet (Messages, TikTok, Snapchat, Discord, Instagram). On a desktop the tray opens the real destination:
2626

27-
TikTok and Snapchat have no web post button. Those buttons save a 9:16 plate and copy the caption — then you post in the app.
27+
- **X / Threads / Reddit / Facebook / WhatsApp / Telegram / LinkedIn** — post the walk in that app.
28+
- **Discord** — copies the walk, then opens Discord. Paste it.
29+
- **TikTok / Snapchat / Instagram** — saves a 9:16 plate, copies the caption, then opens the app. Finish the post there.
30+
31+
Those three have no web post button. The file and the caption still leave this desk.
2832

2933
## Thunder is pinned and I never pinned it
3034

package.json

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,24 @@
22
"name": "first-bucket-studios",
33
"private": true,
44
"license": "MIT",
5-
"description": "A 90-second basketball pack. Tear the foil. Send the card.",
5+
"description": "Rip a basketball pack. Ten cards. Five names. Eighty-two nights. Send the card.",
6+
"keywords": [
7+
"basketball",
8+
"nba",
9+
"wnba",
10+
"sports-cards",
11+
"trading-cards",
12+
"holographic",
13+
"foil",
14+
"webgl",
15+
"pack-opener",
16+
"react",
17+
"typescript",
18+
"tanstack-start",
19+
"vite",
20+
"simulation",
21+
"collector"
22+
],
623
"sideEffects": false,
724
"type": "module",
825
"overrides": {

src/components/file-tray.tsx

Lines changed: 61 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,23 @@ import { X } from "lucide-react";
44
import {
55
copyText,
66
discordCopy,
7+
discordOpen,
78
facebookIntent,
9+
instagramOpen,
10+
linkedinIntent,
811
nativeShare,
912
objectUrl,
13+
openTab,
1014
redditIntent,
1115
registerProofOpener,
1216
saveCardFile,
1317
shareSheetOk,
18+
snapchatOpen,
19+
telegramIntent,
1420
threadsIntent,
21+
tiktokOpen,
1522
tweetIntent,
23+
whatsappIntent,
1624
type Proof,
1725
} from "@/lib/deliver";
1826
import { ASPECTS, lastShareOpts, renderShareCard, type CardAspect } from "@/lib/share-card";
@@ -126,7 +134,7 @@ function FileTray({ proof, onClose }: { proof: Proof; onClose: () => void }) {
126134
await onCopyWalk();
127135
}
128136

129-
async function shareStory(app: "tiktok" | "snap") {
137+
async function shareStory(app: "tiktok" | "snap" | "instagram") {
130138
markDemo("save", app);
131139
if (canShare) {
132140
const result = await nativeShare(sheet, proof.name, proof.caption, shareHref || undefined);
@@ -136,7 +144,9 @@ function FileTray({ proof, onClose }: { proof: Proof; onClose: () => void }) {
136144
try {
137145
await saveCardFile(sheet, proof.name.replace(/\.png$/i, "") + "-story.png");
138146
await copyText([postText, shareHref].filter(Boolean).join("\n"));
139-
setNote(app === "tiktok" ? "9:16 plate saved. Open TikTok and post it." : "9:16 plate saved. Open Snapchat and post it.");
147+
const dest = app === "tiktok" ? tiktokOpen() : app === "snap" ? snapchatOpen() : instagramOpen();
148+
openTab(dest);
149+
setNote("Plate saved. Caption copied. Finish the post in the app.");
140150
} catch {
141151
setNote("Save the plate, then open the app.");
142152
}
@@ -149,7 +159,14 @@ function FileTray({ proof, onClose }: { proof: Proof; onClose: () => void }) {
149159
if (result === "shared" || result === "abort") return;
150160
}
151161
const ok = await copyText(discordCopy(postText, shareHref));
152-
setNote(ok ? "Copied for Discord. Paste it in a chat." : "Copy the walk, then paste in Discord.");
162+
openTab(discordOpen());
163+
setNote(ok ? "Copied. Paste it in Discord." : "Copy the walk, then paste in Discord.");
164+
}
165+
166+
async function onCopyFile() {
167+
const text = await proof.blob.text();
168+
const ok = await copyText(text);
169+
setNote(ok ? "Desk JSON copied." : "Couldn’t copy.");
153170
}
154171

155172
function onOpenWalk() {
@@ -310,14 +327,50 @@ function FileTray({ proof, onClose }: { proof: Proof; onClose: () => void }) {
310327
Facebook
311328
</a>
312329
) : null}
330+
{shareHref ? (
331+
<a
332+
className="tray-btn"
333+
href={whatsappIntent(postText, shareHref)}
334+
target="_blank"
335+
rel="noopener noreferrer"
336+
onClick={() => markDemo("save", "whatsapp")}
337+
>
338+
WhatsApp
339+
</a>
340+
) : null}
341+
{shareHref ? (
342+
<a
343+
className="tray-btn"
344+
href={telegramIntent(postText, shareHref)}
345+
target="_blank"
346+
rel="noopener noreferrer"
347+
onClick={() => markDemo("save", "telegram")}
348+
>
349+
Telegram
350+
</a>
351+
) : null}
352+
{shareHref ? (
353+
<a
354+
className="tray-btn"
355+
href={linkedinIntent(shareHref)}
356+
target="_blank"
357+
rel="noopener noreferrer"
358+
onClick={() => markDemo("save", "linkedin")}
359+
>
360+
LinkedIn
361+
</a>
362+
) : null}
363+
<button type="button" className="tray-btn" onClick={() => void onDiscord()}>
364+
Discord
365+
</button>
313366
<button type="button" className="tray-btn" onClick={() => void shareStory("tiktok")}>
314367
TikTok
315368
</button>
316369
<button type="button" className="tray-btn" onClick={() => void shareStory("snap")}>
317370
Snapchat
318371
</button>
319-
<button type="button" className="tray-btn" onClick={() => void onDiscord()}>
320-
Discord
372+
<button type="button" className="tray-btn" onClick={() => void shareStory("instagram")}>
373+
Instagram
321374
</button>
322375
{walkPath ? (
323376
<button type="button" className="tray-btn" onClick={() => void onCopyWalk()}>
@@ -348,6 +401,9 @@ function FileTray({ proof, onClose }: { proof: Proof; onClose: () => void }) {
348401
) : (
349402
<span className="tray-btn tray-btn-primary opacity-40">Save the file</span>
350403
)}
404+
<button type="button" className="tray-btn" onClick={() => void onCopyFile()}>
405+
Copy JSON
406+
</button>
351407
</div>
352408
)}
353409
</div>

src/components/how-it-works.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,25 +23,25 @@ const BEATS = {
2323
{ n: "01", title: "Rip", body: "Tear the foil. Ten from the whole book. Or open it." },
2424
{ n: "02", title: "Five", body: "No franchise. No era. Turn five names." },
2525
{ n: "03", title: "Circle", body: "Peak, mix, spread. Not 82 nights." },
26-
{ n: "04", title: "Send", body: "The serial is the plate. The URL is the certificate." },
26+
{ n: "04", title: "Send", body: "Save the plate. Share the walk." },
2727
],
2828
wnba: [
2929
{ n: "01", title: "Pull", body: "A W club, era, luck. One pull." },
3030
{ n: "02", title: "Rip", body: "Tear the W pack. Ten carbon backs. Turn five." },
3131
{ n: "03", title: "Forty", body: "Honest length. Not 82." },
32-
{ n: "04", title: "Send", body: "The serial is the plate. The URL is the certificate." },
32+
{ n: "04", title: "Send", body: "Save the plate. Share the walk." },
3333
],
3434
corners: [
3535
{ n: "01", title: "Pull", body: "Franchise, era, luck. One pull." },
3636
{ n: "02", title: "Deal", body: "The pack is G / G / F / F / C. Corners hold." },
3737
{ n: "03", title: "Walk", body: "Then 82 nights, same as the machine." },
38-
{ n: "04", title: "Send", body: "The serial is the plate. The URL is the certificate." },
38+
{ n: "04", title: "Send", body: "Save the plate. Share the walk." },
3939
],
4040
playoff: [
4141
{ n: "01", title: "Pull", body: "Franchise, era, luck. One pull." },
4242
{ n: "02", title: "Rip", body: "Ten face-down. Turn five." },
4343
{ n: "03", title: "Sixteen", body: "Four rounds. Banner or out." },
44-
{ n: "04", title: "Send", body: "The serial is the plate. The URL is the certificate." },
44+
{ n: "04", title: "Send", body: "Save the plate. Share the walk." },
4545
],
4646
keepers: [
4747
{ n: "01", title: "KEEP", body: "That call fills the five. TRADE and CUT stay on the desk." },

src/components/press-furniture.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import type { PointerEvent, ReactNode } from "react";
22
import { Link, useNavigate } from "@tanstack/react-router";
3-
import { useRef, useState } from "react";
3+
import { useEffect, useRef, useState } from "react";
44
import { RipPack } from "@/components/rip-pack";
55
import { useSpecular } from "@/lib/hooks";
66
import { markDemo } from "@/lib/demo-funnel";
7+
import { FRANCHISES } from "@/lib/nba";
78
import { cn } from "@/lib/utils";
89

910
export function PressStage({
@@ -93,6 +94,10 @@ export function StepRail({
9394
export function PackHero() {
9495
const navigate = useNavigate();
9596
const [ripping, setRipping] = useState(false);
97+
const [club, setClub] = useState<string | undefined>(undefined);
98+
useEffect(() => {
99+
setClub(FRANCHISES[Math.floor(Math.random() * FRANCHISES.length)]);
100+
}, []);
96101
const well = useRef<HTMLDivElement>(null);
97102
useSpecular(well);
98103

@@ -139,7 +144,7 @@ export function PackHero() {
139144
>
140145
<div className="pack-float">
141146
<div className="pack-tilt">
142-
<RipPack lot="live:home" ripping={ripping} onRip={go} />
147+
<RipPack lot={`live:home:${club}`} team={club} ripping={ripping} onRip={go} />
143148
</div>
144149
</div>
145150
</div>

src/components/studio-file.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export function ExportStudioButton({
1414
<Button
1515
variant={tone === "ghost" ? "ghost" : "primary"}
1616
onClick={() => {
17-
downloadStudioFile();
17+
void downloadStudioFile();
1818
}}
1919
>
2020
{loud ? "Export the desk" : "Export desk"}

src/components/team-reel.tsx

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { Crest, EraMark, LuckMark, clubAbbr } from "@/components/crest";
33
import { ERAS, FRANCHISES, WNBA_FRANCHISES } from "@/lib/nba";
44
import { LUCKS } from "@/lib/luck";
55
import { tick } from "@/lib/tick";
6-
import { cn } from "@/lib/utils";
6+
import { cn, shuffle } from "@/lib/utils";
77

88
export function TeamReel<T extends string>({
99
items,
@@ -29,15 +29,19 @@ export function TeamReel<T extends string>({
2929
const [rot, setRot] = useState(0);
3030
const [armed, setArmed] = useState(false);
3131
const [landed, setLanded] = useState(false);
32-
const n = Math.max(items.length, 1);
32+
const [faces, setFaces] = useState<T[]>(() => [...items] as T[]);
33+
useEffect(() => {
34+
setFaces(shuffle(items));
35+
}, [items]);
36+
const n = Math.max(faces.length, 1);
3337
const angle = 360 / n;
3438
const radius = rowH / (2 * Math.tan(Math.PI / n));
35-
const land = target ? Math.max(0, items.indexOf(target as T)) : 0;
36-
const clubs = items[0]
37-
? (FRANCHISES as readonly string[]).includes(items[0]) || (WNBA_FRANCHISES as readonly string[]).includes(items[0])
39+
const land = target ? Math.max(0, faces.indexOf(target as T)) : Math.floor(n / 2);
40+
const clubs = faces[0]
41+
? (FRANCHISES as readonly string[]).includes(faces[0]) || (WNBA_FRANCHISES as readonly string[]).includes(faces[0])
3842
: false;
39-
const eras = items[0] ? (ERAS as readonly string[]).includes(items[0]) : false;
40-
const lucks = items[0] ? (LUCKS as readonly string[]).includes(items[0]) : false;
43+
const eras = faces[0] ? (ERAS as readonly string[]).includes(faces[0]) : false;
44+
const lucks = faces[0] ? (LUCKS as readonly string[]).includes(faces[0]) : false;
4145
const idle = !target;
4246
const markSize = compact ? "size-6" : "size-11";
4347

@@ -46,7 +50,7 @@ export function TeamReel<T extends string>({
4650
if (!node) return;
4751
const h = node.offsetHeight;
4852
if (h > 0) setRowH(h);
49-
}, [items, compact]);
53+
}, [faces, compact]);
5054

5155
useEffect(() => {
5256
rested.current = false;
@@ -120,7 +124,7 @@ export function TeamReel<T extends string>({
120124
rest();
121125
}}
122126
>
123-
{items.map((name, i) => (
127+
{faces.map((name, i) => (
124128
<div
125129
key={`${name}-${i}`}
126130
ref={i === 0 ? rowRef : undefined}

src/lib/deliver.test.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,20 @@
11
import assert from "node:assert/strict";
22
import { test } from "node:test";
3-
import { asShareFile, canUseSavePicker, discordCopy, facebookIntent, redditIntent, shareAttempts, shareSheetOk, threadsIntent, tweetIntent } from "./deliver.ts";
3+
import {
4+
asShareFile,
5+
canUseSavePicker,
6+
discordCopy,
7+
discordOpen,
8+
facebookIntent,
9+
linkedinIntent,
10+
redditIntent,
11+
shareAttempts,
12+
shareSheetOk,
13+
telegramIntent,
14+
threadsIntent,
15+
tweetIntent,
16+
whatsappIntent,
17+
} from "./deliver.ts";
418

519
test("share attempts send the file, never files plus url, never text as the card", () => {
620
const file = new File(["png"], "first-bucket-thunder-58.png", { type: "image/png" });
@@ -75,3 +89,12 @@ test("discord copy is caption plus the public walk", () => {
7589
assert.match(text, /Thunder/);
7690
assert.match(text, /first-bucket-studios\.vercel\.app\/walk/);
7791
});
92+
93+
test("whatsapp, telegram, linkedin, and discord open real destinations", () => {
94+
const walk = "https://first-bucket-studios.vercel.app/walk/v1.OKC";
95+
assert.match(whatsappIntent("51–31 Thunder", walk), /wa\.me/);
96+
assert.match(telegramIntent("51–31 Thunder", walk), /t\.me\/share/);
97+
assert.match(linkedinIntent(walk), /linkedin\.com\/sharing/);
98+
assert.equal(discordOpen(), "https://discord.com/channels/@me");
99+
assert.equal(linkedinIntent("/walk/v1.OKC").includes("url="), false);
100+
});

0 commit comments

Comments
 (0)