Skip to content

Commit f9e4b67

Browse files
authored
Merge pull request #480 from Codex723/feat/inline-code-snippets
Feat/inline code snippets
2 parents a2ae5da + c4178a7 commit f9e4b67

6 files changed

Lines changed: 254 additions & 7 deletions

File tree

frontend/messages/en.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,9 @@
110110
"generating": "Generating...",
111111
"generate": "Generate Payment Link",
112112
"rateLimitError": "You're creating links too quickly. Try again in {seconds} seconds.",
113-
"retryWait": "Wait {seconds}s…"
113+
"retryWait": "Wait {seconds}s…",
114+
"integrationCode": "Integration Code",
115+
"snippetsHelper": "Code snippets update automatically with your form values."
114116
},
115117
"paymentMetrics": {
116118
"downloadImage": "Download Image",

frontend/messages/es.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,9 @@
110110
"generating": "Generando...",
111111
"generate": "Generar enlace de pago",
112112
"rateLimitError": "Estás creando enlaces demasiado rápido. Inténtalo de nuevo en {seconds} segundos.",
113-
"retryWait": "Espera {seconds}s…"
113+
"retryWait": "Espera {seconds}s…",
114+
"integrationCode": "Código de Integración",
115+
"snippetsHelper": "Los fragmentos de código se actualizan automáticamente con los valores del formulario."
114116
},
115117
"paymentMetrics": {
116118
"downloadImage": "Descargar imagen",

frontend/messages/pt.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,9 @@
110110
"generating": "Gerando...",
111111
"generate": "Gerar link de pagamento",
112112
"rateLimitError": "Você está criando links rápido demais. Tente novamente em {seconds} segundos.",
113-
"retryWait": "Aguarde {seconds}s…"
113+
"retryWait": "Aguarde {seconds}s…",
114+
"integrationCode": "Código de Integração",
115+
"snippetsHelper": "Os trechos de código são atualizados automaticamente com os valores do formulário."
114116
},
115117
"paymentMetrics": {
116118
"downloadImage": "Baixar imagem",

frontend/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"@radix-ui/react-dropdown-menu": "^2.1.16",
1818
"@sentry/nextjs": "^10.46.0",
1919
"@stellar/freighter-api": "^1.7.1",
20+
"@types/prismjs": "^1.26.6",
2021
"@walletconnect/sign-client": "^2.23.9",
2122
"@walletconnect/types": "^2.23.8",
2223
"canvas-confetti": "^1.9.4",
@@ -29,6 +30,7 @@
2930
"next-intl": "^4.8.3",
3031
"next-mdx-remote": "^5.0.0",
3132
"next-themes": "^0.4.6",
33+
"prismjs": "^1.30.0",
3234
"qrcode.react": "^4.2.0",
3335
"react": "^18.3.1",
3436
"react-confetti": "^6.4.0",
@@ -48,6 +50,7 @@
4850
"@types/canvas-confetti": "^1.9.0",
4951
"@types/event-source-polyfill": "^1.0.5",
5052
"@types/node": "25.5.0",
53+
"@types/prismjs": "^1.26.5",
5154
"@types/react": "18.3.28",
5255
"@types/react-dom": "^18.3.7",
5356
"autoprefixer": "^10.4.19",

frontend/src/components/CreatePaymentForm.tsx

Lines changed: 60 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { useTranslations } from "next-intl";
55
import { motion, AnimatePresence, type Variants } from "framer-motion";
66
import confetti from "canvas-confetti";
77
import CopyButton from "./CopyButton";
8+
import IntegrationCodeSnippets from "./IntegrationCodeSnippets";
89
import toast from "react-hot-toast";
910
import Link from "next/link";
1011
import {
@@ -338,6 +339,7 @@ function SuccessCard({ created, onReset, t }: SuccessCardProps) {
338339

339340
export default function CreatePaymentForm() {
340341
const t = useTranslations("createPaymentForm");
342+
const [view, setView] = useState<"form" | "code">("form");
341343
const [amount, setAmount] = useLocalStorage("payment_amount", "");
342344
const [asset, setAsset] = useLocalStorage<"XLM" | "USDC">(
343345
"payment_asset",
@@ -545,15 +547,67 @@ export default function CreatePaymentForm() {
545547
t={t}
546548
/>
547549
) : (
548-
<motion.form
550+
<motion.div
549551
key="form"
550552
variants={formVariants}
551553
initial="visible"
552554
exit="exit"
553-
onSubmit={handleSubmit}
554555
className="flex flex-col gap-6"
555-
noValidate
556556
>
557+
{/* Tab bar */}
558+
<div className="flex gap-1 rounded-xl border border-white/10 bg-white/5 p-1">
559+
<button
560+
type="button"
561+
onClick={() => setView("form")}
562+
className={`relative flex-1 rounded-lg px-3 py-2 text-sm font-medium transition-all ${
563+
view === "form" ? "text-black" : "text-slate-400 hover:text-white"
564+
}`}
565+
>
566+
{view === "form" && (
567+
<motion.div
568+
layoutId="view-tab-bg"
569+
className="absolute inset-0 rounded-lg bg-mint"
570+
transition={{ type: "spring", stiffness: 380, damping: 30 }}
571+
/>
572+
)}
573+
<span className="relative z-10">{t("generate")}</span>
574+
</button>
575+
<button
576+
type="button"
577+
onClick={() => setView("code")}
578+
className={`relative flex-1 rounded-lg px-3 py-2 text-sm font-medium transition-all ${
579+
view === "code" ? "text-black" : "text-slate-400 hover:text-white"
580+
}`}
581+
>
582+
{view === "code" && (
583+
<motion.div
584+
layoutId="view-tab-bg"
585+
className="absolute inset-0 rounded-lg bg-mint"
586+
transition={{ type: "spring", stiffness: 380, damping: 30 }}
587+
/>
588+
)}
589+
<span className="relative z-10">{t("integrationCode")}</span>
590+
</button>
591+
</div>
592+
593+
{view === "code" ? (
594+
<IntegrationCodeSnippets
595+
apiKey={apiKey!}
596+
amount={amount}
597+
asset={asset}
598+
recipient={recipient}
599+
description={description}
600+
usdcIssuer={USDC_ISSUER}
601+
/>
602+
) : (
603+
<motion.form
604+
key="payment-form"
605+
variants={formVariants}
606+
initial="visible"
607+
onSubmit={handleSubmit}
608+
className="flex flex-col gap-6"
609+
noValidate
610+
>
557611
{error && (
558612
<motion.div
559613
initial={{ opacity: 0, y: -8 }}
@@ -834,7 +888,9 @@ export default function CreatePaymentForm() {
834888
)}
835889
<div className="absolute inset-0 -z-10 bg-mint/20 opacity-0 blur-xl transition-opacity group-hover:opacity-100" />
836890
</button>
837-
</motion.form>
891+
</motion.form>
892+
)}
893+
</motion.div>
838894
)}
839895
</AnimatePresence>
840896
);
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
"use client";
2+
3+
import { useState, useEffect, useRef, useMemo } from "react";
4+
import { useTranslations } from "next-intl";
5+
import { motion, AnimatePresence } from "framer-motion";
6+
import CopyButton from "./CopyButton";
7+
import Prism from "prismjs";
8+
import "prismjs/components/prism-bash";
9+
import "prismjs/components/prism-javascript";
10+
import "prismjs/components/prism-python";
11+
12+
const API_URL = process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:4000";
13+
14+
interface IntegrationCodeSnippetsProps {
15+
apiKey: string;
16+
amount: string;
17+
asset: "XLM" | "USDC";
18+
recipient: string;
19+
description: string;
20+
usdcIssuer: string;
21+
}
22+
23+
type Language = "curl" | "node" | "python";
24+
25+
const LANGUAGES: { id: Language; label: string; grammar: string }[] = [
26+
{ id: "curl", label: "cURL", grammar: "bash" },
27+
{ id: "node", label: "Node.js", grammar: "javascript" },
28+
{ id: "python", label: "Python", grammar: "python" },
29+
];
30+
31+
function generateSnippet(
32+
lang: Language,
33+
apiKey: string,
34+
amount: string,
35+
asset: "XLM" | "USDC",
36+
recipient: string,
37+
description: string,
38+
usdcIssuer: string,
39+
): string {
40+
const numAmount = parseFloat(amount) || 0;
41+
const safeRecipient =
42+
recipient || "GXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";
43+
const safeDescription = description || "Payment for services";
44+
45+
const body: Record<string, unknown> = {
46+
amount: numAmount,
47+
asset,
48+
recipient: safeRecipient,
49+
};
50+
if (asset === "USDC") body.asset_issuer = usdcIssuer;
51+
if (description) body.description = safeDescription;
52+
53+
const jsonBody = JSON.stringify(body, null, 2);
54+
55+
switch (lang) {
56+
case "curl":
57+
return `curl -X POST "${API_URL}/api/create-payment" \\
58+
-H "Content-Type: application/json" \\
59+
-H "x-api-key: ${apiKey}" \\
60+
-d '${jsonBody}'`;
61+
62+
case "node":
63+
return `const response = await fetch("${API_URL}/api/create-payment", {
64+
method: "POST",
65+
headers: {
66+
"Content-Type": "application/json",
67+
"x-api-key": "${apiKey}",
68+
},
69+
body: JSON.stringify(${jsonBody}),
70+
});
71+
72+
const data = await response.json();
73+
console.log(data.payment_link);`;
74+
75+
case "python":
76+
return `import requests
77+
78+
response = requests.post(
79+
"${API_URL}/api/create-payment",
80+
headers={
81+
"Content-Type": "application/json",
82+
"x-api-key": "${apiKey}",
83+
},
84+
json=${jsonBody},
85+
)
86+
87+
data = response.json()
88+
print(data["payment_link"])`;
89+
}
90+
}
91+
92+
export default function IntegrationCodeSnippets({
93+
apiKey,
94+
amount,
95+
asset,
96+
recipient,
97+
description,
98+
usdcIssuer,
99+
}: IntegrationCodeSnippetsProps) {
100+
const t = useTranslations("createPaymentForm");
101+
const [activeTab, setActiveTab] = useState<Language>("curl");
102+
const codeRef = useRef<HTMLElement>(null);
103+
104+
const snippet = useMemo(
105+
() => generateSnippet(activeTab, apiKey, amount, asset, recipient, description, usdcIssuer),
106+
[activeTab, apiKey, amount, asset, recipient, description, usdcIssuer],
107+
);
108+
109+
useEffect(() => {
110+
if (codeRef.current) {
111+
Prism.highlightElement(codeRef.current);
112+
}
113+
}, [snippet, activeTab]);
114+
115+
const grammarMap: Record<Language, string> = {
116+
curl: "bash",
117+
node: "javascript",
118+
python: "python",
119+
};
120+
121+
return (
122+
<div className="flex flex-col gap-4">
123+
{/* Tab bar */}
124+
<div className="flex gap-1 rounded-xl border border-white/10 bg-white/5 p-1">
125+
{LANGUAGES.map((lang) => (
126+
<button
127+
key={lang.id}
128+
type="button"
129+
onClick={() => setActiveTab(lang.id)}
130+
className={`relative flex-1 rounded-lg px-3 py-2 text-sm font-medium transition-all ${
131+
activeTab === lang.id
132+
? "text-black"
133+
: "text-slate-400 hover:text-white"
134+
}`}
135+
>
136+
{activeTab === lang.id && (
137+
<motion.div
138+
layoutId="snippet-tab-bg"
139+
className="absolute inset-0 rounded-lg bg-mint"
140+
transition={{ type: "spring", stiffness: 380, damping: 30 }}
141+
/>
142+
)}
143+
<span className="relative z-10">{lang.label}</span>
144+
</button>
145+
))}
146+
</div>
147+
148+
{/* Code block */}
149+
<div className="group relative overflow-hidden rounded-xl border border-white/10 bg-[rgba(2,6,23,0.82)]">
150+
{/* Copy button */}
151+
<div className="absolute right-3 top-3 z-10 opacity-0 transition-opacity group-hover:opacity-100">
152+
<CopyButton text={snippet} />
153+
</div>
154+
155+
<AnimatePresence mode="wait">
156+
<motion.div
157+
key={activeTab}
158+
initial={{ opacity: 0, y: 6 }}
159+
animate={{ opacity: 1, y: 0 }}
160+
exit={{ opacity: 0, y: -6 }}
161+
transition={{ duration: 0.2 }}
162+
className="overflow-x-auto p-4"
163+
>
164+
<pre className="!m-0 !bg-transparent !p-0">
165+
<code
166+
ref={codeRef}
167+
className={`language-${grammarMap[activeTab]} !bg-transparent`}
168+
>
169+
{snippet}
170+
</code>
171+
</pre>
172+
</motion.div>
173+
</AnimatePresence>
174+
</div>
175+
176+
{/* Helper text */}
177+
<p className="text-xs text-slate-500">
178+
{t("snippetsHelper")}
179+
</p>
180+
</div>
181+
);
182+
}

0 commit comments

Comments
 (0)