Skip to content

Commit fefd738

Browse files
committed
feat: analytics script loading
1 parent be95e0b commit fefd738

File tree

11 files changed

+120
-7
lines changed

11 files changed

+120
-7
lines changed
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
-- AlterTable
2+
ALTER TABLE "Dex" ADD COLUMN "analyticsScript" TEXT;

api/prisma/schema.prisma

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ model Dex {
8989
seoThemeColor String? // SEO Theme Color (e.g., "#1a1b23")
9090
seoKeywords String? // SEO Keywords
9191
privyLoginMethods String? // Comma-separated list of Privy login methods (email,passkey,twitter,google)
92+
analyticsScript String? // Custom analytics script (GA, Plausible, etc.) with script tags
9293
isGraduated Boolean @default(false) // Whether the DEX has fully graduated
9394
graduationTxHash String? @unique // Transaction hash used for graduation (prevents reuse)
9495
multisigChainId Int? // Chain ID where the multisig delegate signer link was established

api/src/lib/github.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -676,6 +676,9 @@ function prepareDexConfigContent(
676676
VITE_USE_CUSTOM_PNL_POSTERS: pnlPostersData.length > 0 ? "true" : "false",
677677
VITE_CUSTOM_PNL_POSTER_COUNT: String(pnlPostersData.length),
678678
VITE_TRADING_VIEW_COLOR_CONFIG: config.tradingViewColorConfig || "",
679+
680+
// Analytics
681+
VITE_ANALYTICS_SCRIPT: config.analyticsScript || "",
679682
};
680683

681684
const configJsContent = `window.__RUNTIME_CONFIG__ = ${JSON.stringify(

api/src/lib/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ export interface DexConfig {
104104
seoTwitterHandle: string | null;
105105
seoThemeColor: string | null;
106106
seoKeywords: string | null;
107+
analyticsScript: string | null;
107108
}
108109

109110
/**

api/src/models/dex.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ export function convertDexToDexConfig(dex: Dex): DexConfig {
4848
seoTwitterHandle: dex.seoTwitterHandle,
4949
seoThemeColor: dex.seoThemeColor,
5050
seoKeywords: dex.seoKeywords,
51+
analyticsScript: dex.analyticsScript,
5152
};
5253
}
5354

@@ -88,6 +89,7 @@ function convertValidatedDataToDexConfig(
8889
seoTwitterHandle: validatedData.seoTwitterHandle ?? null,
8990
seoThemeColor: validatedData.seoThemeColor ?? null,
9091
seoKeywords: validatedData.seoKeywords ?? null,
92+
analyticsScript: validatedData.analyticsScript ?? null,
9193
};
9294
}
9395

@@ -300,6 +302,10 @@ export const dexSchema = z.object({
300302
.string()
301303
.max(500, "Keywords must be 500 characters or less")
302304
.nullish(),
305+
analyticsScript: z
306+
.string()
307+
.max(2000, "Analytics script must be 2000 characters or less")
308+
.nullish(),
303309
});
304310

305311
export const dexFormSchema = dexSchema
@@ -566,6 +572,7 @@ export async function createDex(
566572
seoTwitterHandle: validatedData.seoTwitterHandle,
567573
seoThemeColor: validatedData.seoThemeColor,
568574
seoKeywords: validatedData.seoKeywords,
575+
analyticsScript: validatedData.analyticsScript,
569576
swapFeeBps: validatedData.swapFeeBps,
570577
repoUrl: repoUrl,
571578
user: {
@@ -727,6 +734,8 @@ export async function updateDex(
727734
updateData.seoThemeColor = validatedData.seoThemeColor;
728735
if ("seoKeywords" in validatedData)
729736
updateData.seoKeywords = validatedData.seoKeywords;
737+
if ("analyticsScript" in validatedData)
738+
updateData.analyticsScript = validatedData.analyticsScript;
730739

731740
try {
732741
const prismaClient = await getPrisma();
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import React from "react";
2+
3+
interface AnalyticsConfigSectionProps {
4+
analyticsScript: string;
5+
handleInputChange: (
6+
field: string
7+
) => (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => void;
8+
idPrefix?: string;
9+
}
10+
11+
const AnalyticsConfigSection: React.FC<AnalyticsConfigSectionProps> = ({
12+
analyticsScript,
13+
handleInputChange,
14+
idPrefix = "",
15+
}) => {
16+
return (
17+
<div className="flex flex-col gap-4">
18+
<div className="flex flex-col gap-2">
19+
<label
20+
htmlFor={`${idPrefix}analyticsScript`}
21+
className="text-sm font-medium text-gray-200"
22+
>
23+
Analytics Script
24+
</label>
25+
<textarea
26+
id={`${idPrefix}analyticsScript`}
27+
name="analyticsScript"
28+
value={analyticsScript}
29+
onChange={handleInputChange("analyticsScript")}
30+
placeholder="Paste your analytics script here (including <script> tags)&#10;&#10;Example:&#10;<script async src='https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXXXX'></script>&#10;<script>&#10; window.dataLayer = window.dataLayer || [];&#10; function gtag(){dataLayer.push(arguments);}&#10; gtag('js', new Date());&#10; gtag('config', 'G-XXXXXXXXXX');&#10;</script>"
31+
className="w-full px-3 py-2 bg-background-light/30 border border-light/20 rounded-lg text-sm font-mono resize-y min-h-[160px] focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary/50"
32+
maxLength={2000}
33+
/>
34+
<div className="flex items-start gap-2 text-xs text-gray-400">
35+
<div className="flex-shrink-0 mt-0.5">
36+
<div className="i-heroicons:information-circle w-4 h-4" />
37+
</div>
38+
<div className="flex-1">
39+
<p className="mb-2">
40+
Add your analytics tracking script from any provider that uses
41+
script tags. The script will be securely injected into your DEX.
42+
</p>
43+
<p>
44+
<strong>Note:</strong> Include the complete script tags exactly as
45+
provided by your analytics service. The script will be placed in
46+
the DEX's HTML head section.
47+
</p>
48+
</div>
49+
</div>
50+
{analyticsScript && (
51+
<p className="text-xs text-gray-500">
52+
{analyticsScript.length} / 2,000 characters
53+
</p>
54+
)}
55+
</div>
56+
</div>
57+
);
58+
};
59+
60+
export default AnalyticsConfigSection;

app/app/components/DexSectionRenderer.tsx

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import ThemeCustomizationSection from "./ThemeCustomizationSection";
66
import PnLPostersSection from "./PnLPostersSection";
77
import SocialLinksSection from "./SocialLinksSection";
88
import SEOConfigSection from "./SEOConfigSection";
9+
import AnalyticsConfigSection from "./AnalyticsConfigSection";
910
import ReownConfigSection from "./ReownConfigSection";
1011
import PrivyConfigSection from "./PrivyConfigSection";
1112
import BlockchainConfigSection from "./BlockchainConfigSection";
@@ -145,6 +146,19 @@ export const DEX_SECTIONS: DexSectionConfig[] = [
145146
},
146147
{
147148
id: 7,
149+
key: "analyticsConfiguration",
150+
title: "Analytics Configuration",
151+
description:
152+
"Add your analytics tracking script to monitor usage and performance of your DEX. Supports Google Analytics, Plausible, Matomo, and other analytics services. This is completely optional.",
153+
isOptional: true,
154+
component: AnalyticsConfigSection,
155+
getProps: props => ({
156+
analyticsScript: props.analyticsScript,
157+
handleInputChange: props.handleInputChange,
158+
}),
159+
},
160+
{
161+
id: 8,
148162
key: "reownConfiguration",
149163
title: "Reown Configuration",
150164
description:
@@ -157,7 +171,7 @@ export const DEX_SECTIONS: DexSectionConfig[] = [
157171
}),
158172
},
159173
{
160-
id: 8,
174+
id: 9,
161175
key: "privyConfiguration",
162176
title: "Privy Configuration",
163177
description:
@@ -184,7 +198,7 @@ export const DEX_SECTIONS: DexSectionConfig[] = [
184198
: true,
185199
},
186200
{
187-
id: 9,
201+
id: 10,
188202
key: "blockchainConfiguration",
189203
title: "Blockchain Configuration",
190204
description:
@@ -203,7 +217,7 @@ export const DEX_SECTIONS: DexSectionConfig[] = [
203217
}),
204218
},
205219
{
206-
id: 10,
220+
id: 11,
207221
key: "languageSupport",
208222
title: "Language Support",
209223
description:
@@ -216,7 +230,7 @@ export const DEX_SECTIONS: DexSectionConfig[] = [
216230
}),
217231
},
218232
{
219-
id: 11,
233+
id: 12,
220234
key: "navigationMenus",
221235
title: "Navigation Menus",
222236
description:
@@ -235,7 +249,7 @@ export const DEX_SECTIONS: DexSectionConfig[] = [
235249
}),
236250
},
237251
{
238-
id: 12,
252+
id: 13,
239253
key: "serviceDisclaimer",
240254
title: "Service Disclaimer",
241255
description:

app/app/hooks/useDexForm.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ export interface DexSectionProps {
7171
seoTwitterHandle: string;
7272
seoThemeColor: string;
7373
seoKeywords: string;
74+
analyticsScript: string;
7475
walletConnectProjectId: string;
7576
privyAppId: string;
7677
privyTermsOfUse: string;
@@ -141,6 +142,7 @@ export interface DexFormData {
141142
seoTwitterHandle: string;
142143
seoThemeColor: string;
143144
seoKeywords: string;
145+
analyticsScript: string;
144146
}
145147

146148
export interface UseDexFormReturn extends DexFormData {
@@ -188,9 +190,10 @@ export interface UseDexFormReturn extends DexFormData {
188190
setSeoTwitterHandle: (value: string) => void;
189191
setSeoThemeColor: (value: string) => void;
190192
setSeoKeywords: (value: string) => void;
193+
setAnalyticsScript: (value: string) => void;
191194
handleInputChange: (
192195
field: string
193-
) => (e: React.ChangeEvent<HTMLInputElement>) => void;
196+
) => (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => void;
194197
handleImageChange: (field: string) => (blob: Blob | null) => void;
195198
handlePnLPosterChange: (posters: (Blob | null)[]) => void;
196199
populateFromDexData: (
@@ -287,6 +290,7 @@ const initialFormState: DexFormData = {
287290
seoTwitterHandle: "",
288291
seoThemeColor: "",
289292
seoKeywords: "",
293+
analyticsScript: "",
290294
};
291295

292296
export function useDexForm(): UseDexFormReturn {
@@ -388,9 +392,13 @@ export function useDexForm(): UseDexFormReturn {
388392
initialFormState.seoThemeColor
389393
);
390394
const [seoKeywords, setSeoKeywords] = useState(initialFormState.seoKeywords);
395+
const [analyticsScript, setAnalyticsScript] = useState(
396+
initialFormState.analyticsScript
397+
);
391398

392399
const handleInputChange =
393-
(field: string) => (e: React.ChangeEvent<HTMLInputElement>) => {
400+
(field: string) =>
401+
(e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
394402
const value = e.target.value;
395403
switch (field) {
396404
case "brokerName":
@@ -438,6 +446,9 @@ export function useDexForm(): UseDexFormReturn {
438446
case "seoKeywords":
439447
setSeoKeywords(value);
440448
break;
449+
case "analyticsScript":
450+
setAnalyticsScript(value);
451+
break;
441452
}
442453
};
443454

@@ -526,6 +537,8 @@ export function useDexForm(): UseDexFormReturn {
526537
setSeoThemeColor(dexData.seoThemeColor);
527538
if (dexData.seoKeywords !== undefined)
528539
setSeoKeywords(dexData.seoKeywords);
540+
if (dexData.analyticsScript !== undefined)
541+
setAnalyticsScript(dexData.analyticsScript);
529542

530543
if (dexData.themeCSS !== undefined) {
531544
setCurrentTheme(dexData.themeCSS);
@@ -614,6 +627,7 @@ export function useDexForm(): UseDexFormReturn {
614627
seoTwitterHandle,
615628
seoThemeColor,
616629
seoKeywords,
630+
analyticsScript,
617631
},
618632
images: {
619633
primaryLogo: primaryLogoBase64,
@@ -659,6 +673,7 @@ export function useDexForm(): UseDexFormReturn {
659673
seoTwitterHandle,
660674
seoThemeColor,
661675
seoKeywords,
676+
analyticsScript,
662677
]);
663678

664679
const resetForm = () => {
@@ -700,6 +715,7 @@ export function useDexForm(): UseDexFormReturn {
700715
setSeoTwitterHandle(initialFormState.seoTwitterHandle);
701716
setSeoThemeColor(initialFormState.seoThemeColor);
702717
setSeoKeywords(initialFormState.seoKeywords);
718+
setAnalyticsScript(initialFormState.analyticsScript);
703719
};
704720

705721
const generateTheme = useCallback(
@@ -917,6 +933,7 @@ export function useDexForm(): UseDexFormReturn {
917933
seoTwitterHandle,
918934
seoThemeColor,
919935
seoKeywords,
936+
analyticsScript,
920937
walletConnectProjectId,
921938
privyAppId,
922939
privyTermsOfUse,
@@ -975,6 +992,7 @@ export function useDexForm(): UseDexFormReturn {
975992
seoTwitterHandle,
976993
seoThemeColor,
977994
seoKeywords,
995+
analyticsScript,
978996
walletConnectProjectId,
979997
privyAppId,
980998
privyTermsOfUse,
@@ -1057,6 +1075,7 @@ export function useDexForm(): UseDexFormReturn {
10571075
seoTwitterHandle,
10581076
seoThemeColor,
10591077
seoKeywords,
1078+
analyticsScript,
10601079
setBrokerName,
10611080
setTelegramLink,
10621081
setDiscordLink,
@@ -1093,6 +1112,7 @@ export function useDexForm(): UseDexFormReturn {
10931112
setSeoTwitterHandle,
10941113
setSeoThemeColor,
10951114
setSeoKeywords,
1115+
setAnalyticsScript,
10961116
handleInputChange,
10971117
handleImageChange,
10981118
handlePnLPosterChange,

app/app/routes/_layout.dex.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ export default function DexRoute() {
7676
seoTwitterHandle: dexData.seoTwitterHandle || "",
7777
seoThemeColor: dexData.seoThemeColor || "",
7878
seoKeywords: dexData.seoKeywords || "",
79+
analyticsScript: dexData.analyticsScript || "",
7980
themeCSS: dexData.themeCSS,
8081
});
8182

app/app/routes/_layout.dex_.config.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,7 @@ export default function DexConfigRoute() {
184184
seoTwitterHandle: formValues.seoTwitterHandle.trim(),
185185
seoThemeColor: formValues.seoThemeColor.trim(),
186186
seoKeywords: formValues.seoKeywords.trim(),
187+
analyticsScript: formValues.analyticsScript.trim(),
187188
};
188189

189190
const formData = createDexFormData(dexDataToSend, imageBlobs);

0 commit comments

Comments
 (0)