Skip to content

Commit b26dda3

Browse files
czerwiukkCopilot
andauthored
Add promo widget (#58)
* add promo widget * edit promo code * add promo analytics * Update deep-sea-stories/packages/web/src/main.tsx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update deep-sea-stories/packages/web/src/views/GameView.tsx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * small fixes * format * fix positioning --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent a109b5f commit b26dda3

File tree

6 files changed

+208
-0
lines changed

6 files changed

+208
-0
lines changed

deep-sea-stories/packages/web/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
"tailwindcss": "^4.1.13"
3636
},
3737
"devDependencies": {
38+
"@types/gtag.js": "^0.0.20",
3839
"@types/react": "^19.1.13",
3940
"@types/react-dom": "^19.1.9",
4041
"@vitejs/plugin-react": "^5.0.3",
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
import { useEffect, useRef, useState } from 'react';
2+
import { Button } from './ui/button';
3+
4+
const PROMO_CODE = 'DeepSea25';
5+
const PROMO_URL = 'https://fishjam.swmansion.com/?utm_source=deep-sea-stories';
6+
const DISMISS_STORAGE_KEY = 'promo-widget-dismissed';
7+
const PROMO_HIDE_AFTER = new Date('2026-03-19T00:00:00Z');
8+
9+
const PROMO_DISMISSED_EVENT = 'promo_dismissed';
10+
const PROMO_CODE_DISPLAYED_EVENT = 'promo_code_displayed';
11+
const PROMO_CODE_COPIED_EVENT = 'promo_code_copied';
12+
13+
const readDismissedFromStorage = () => {
14+
return window.localStorage.getItem(DISMISS_STORAGE_KEY) === 'true';
15+
};
16+
17+
const isExpired = Date.now() >= PROMO_HIDE_AFTER.getTime();
18+
19+
const sendGAEvent = (event: string) => {
20+
if (typeof window.gtag !== 'function') {
21+
console.warn('gtag not defined');
22+
return;
23+
}
24+
25+
try {
26+
window.gtag('event', event, {});
27+
} catch (e) {
28+
console.error(`Failed to send ${event} event`, e);
29+
}
30+
};
31+
32+
const PromoWidget = () => {
33+
const [promoVisible, setPromoVisible] = useState(false);
34+
const [dismissed, setDismissed] = useState(readDismissedFromStorage);
35+
const [copied, setCopied] = useState(false);
36+
37+
const copyResetRef = useRef<number | null>(null);
38+
39+
useEffect(() => {
40+
return () => {
41+
if (copyResetRef.current) {
42+
window.clearTimeout(copyResetRef.current);
43+
}
44+
};
45+
}, []);
46+
47+
useEffect(() => {
48+
if (dismissed) {
49+
window.localStorage.setItem(DISMISS_STORAGE_KEY, 'true');
50+
}
51+
}, [dismissed]);
52+
53+
const triggerCopiedFeedback = () => {
54+
setCopied(true);
55+
if (copyResetRef.current) {
56+
window.clearTimeout(copyResetRef.current);
57+
}
58+
copyResetRef.current = window.setTimeout(() => setCopied(false), 2000);
59+
};
60+
61+
const fallbackCopy = () => {
62+
const textarea = document.createElement('textarea');
63+
textarea.value = PROMO_CODE;
64+
textarea.style.position = 'fixed';
65+
textarea.style.opacity = '0';
66+
document.body.appendChild(textarea);
67+
textarea.focus();
68+
textarea.select();
69+
document.execCommand('copy');
70+
document.body.removeChild(textarea);
71+
triggerCopiedFeedback();
72+
sendGAEvent(PROMO_CODE_COPIED_EVENT);
73+
};
74+
75+
const handleGetPromo = () => {
76+
sendGAEvent(PROMO_CODE_DISPLAYED_EVENT);
77+
setPromoVisible(true);
78+
};
79+
80+
const handleCopy = async () => {
81+
try {
82+
if (navigator.clipboard?.writeText) {
83+
await navigator.clipboard.writeText(PROMO_CODE);
84+
sendGAEvent(PROMO_CODE_COPIED_EVENT);
85+
triggerCopiedFeedback();
86+
return;
87+
}
88+
} catch (error) {
89+
console.warn('Failed to copy using clipboard API, falling back.', error);
90+
}
91+
92+
fallbackCopy();
93+
};
94+
95+
const handleDismiss = () => {
96+
sendGAEvent(PROMO_DISMISSED_EVENT);
97+
setDismissed(true);
98+
};
99+
100+
if (dismissed || isExpired) {
101+
return null;
102+
}
103+
104+
return (
105+
<div className="pointer-events-auto text-primary">
106+
<div className="relative w-[min(22rem,_calc(100vw-2rem))] rounded-4xl shadow-amber-100/15 border border-border/80 bg-background/90 p-6 shadow-xl backdrop-blur-2xl">
107+
<button
108+
type="button"
109+
aria-label="Dismiss promo"
110+
onClick={handleDismiss}
111+
className="absolute right-3 top-4 rounded-full bg-background/70 px-2 py-1 text-[0.55rem] font-semibold uppercase tracking-[0.2em] text-primary/60 transition hover:text-primary"
112+
>
113+
I'm not interested
114+
</button>
115+
116+
<div className="flex flex-col gap-5 text-sm text-primary/80">
117+
<div>
118+
<p className="font-display text-[0.55rem] uppercase tracking-[0.6em] text-primary/60">
119+
Powered by
120+
</p>
121+
<a
122+
className="font-display text-2xl text-primary underline"
123+
target="_blank"
124+
rel="noopener"
125+
href={PROMO_URL}
126+
>
127+
Fishjam
128+
</a>
129+
<p className="mt-2">
130+
The realtime infrastructure behind Deep Sea Stories.
131+
<br />
132+
Build AI-first audio and video experiences without touching WebRTC
133+
internals.
134+
</p>
135+
</div>
136+
137+
<div>
138+
<p className="mt-1">
139+
Save 25% on your first three months of Regular Jar plan.
140+
</p>
141+
</div>
142+
143+
{promoVisible ? (
144+
<div className="rounded-3xl border border-border/60 bg-background/80 p-4">
145+
<div className="flex items-center justify-between text-[0.65rem] font-display uppercase tracking-[0.4em] text-primary/60">
146+
<span>Promo code</span>
147+
</div>
148+
<div className="mt-3 flex flex-wrap justify-between items-center gap-3">
149+
<span className="font-mono text-lg tracking-[0.4em]">
150+
{PROMO_CODE}
151+
</span>
152+
<Button
153+
type="button"
154+
variant="outline"
155+
className="h-10 px-4 text-[0.6rem] font-semibold uppercase tracking-[0.3em]"
156+
onClick={handleCopy}
157+
>
158+
{copied ? 'Copied' : 'Copy'}
159+
</Button>
160+
</div>
161+
</div>
162+
) : (
163+
<Button
164+
type="button"
165+
onClick={handleGetPromo}
166+
className="h-11 w-full text-sm font-display"
167+
>
168+
Get a promo code
169+
</Button>
170+
)}
171+
<a
172+
href={PROMO_URL}
173+
target="_blank"
174+
rel="noopener"
175+
className="mt-1 text-center text-[0.75rem] font-semibold tracking-[0.1em] text-primary/70 underline-offset-4 transition-colors hover:text-primary hover:underline"
176+
>
177+
Redeem at fishjam.swmansion.com
178+
</a>
179+
</div>
180+
</div>
181+
</div>
182+
);
183+
};
184+
185+
export default PromoWidget;

deep-sea-stories/packages/web/src/main.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { BrowserRouter, Route, Routes } from 'react-router';
88
import Layout from './Layout.tsx';
99
import HomeView from './views/HomeView.tsx';
1010
import RoomView from './views/RoomView.tsx';
11+
import PromoWidget from './components/PromoWidget.tsx';
1112

1213
const queryClient = new QueryClient({
1314
defaultOptions: {
@@ -30,6 +31,9 @@ createRoot(document.getElementById('root')!).render(
3031
<Route path=":roomId" element={<RoomView />} />
3132
</Routes>
3233
</BrowserRouter>
34+
<div className="absolute bottom-2 right-2 md:bottom-40 md:left-6 md:right-auto z-10">
35+
<PromoWidget />
36+
</div>
3337
</Layout>
3438
</FishjamProvider>
3539
</TRPCClientProvider>
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import type { Gtag } from 'gtag.js';
2+
3+
declare global {
4+
interface Window {
5+
gtag: Gtag.Gtag;
6+
}
7+
}
8+
9+
export {};

deep-sea-stories/packages/web/src/views/GameView.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ const GameView: FC<GameViewProps> = ({ roomId }) => {
5353
<div className="absolute right-2 top-2 md:right-6 md:top-6 z-10">
5454
<PlayerCountIndicator count={playerCount} />
5555
</div>
56+
5657
<PeerGrid
5758
roomId={roomId}
5859
localPeer={localPeer}

deep-sea-stories/yarn.lock

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,7 @@ __metadata:
364364
"@trpc/client": "npm:^11.6.0"
365365
"@trpc/server": "npm:^11.6.0"
366366
"@trpc/tanstack-react-query": "npm:^11.6.0"
367+
"@types/gtag.js": "npm:^0.0.20"
367368
"@types/react": "npm:^19.1.13"
368369
"@types/react-dom": "npm:^19.1.9"
369370
"@vitejs/plugin-react": "npm:^5.0.3"
@@ -1824,6 +1825,13 @@ __metadata:
18241825
languageName: node
18251826
linkType: hard
18261827

1828+
"@types/gtag.js@npm:^0.0.20":
1829+
version: 0.0.20
1830+
resolution: "@types/gtag.js@npm:0.0.20"
1831+
checksum: 10c0/eb878aa3cfab6b98f5e69ef3383e9788aaea6a4d0611c72078678374dcbb4731f533ff2bf479a865536f1626a57887b1198279ff35a65d223fe4f93d9c76dbdd
1832+
languageName: node
1833+
linkType: hard
1834+
18271835
"@types/node@npm:*, @types/node@npm:^24.5.2":
18281836
version: 24.8.1
18291837
resolution: "@types/node@npm:24.8.1"

0 commit comments

Comments
 (0)