Function bodies 24 total
pickRandomStyles function · typescript · L75-L79 (5 LOC)src/app/api/generate/route.ts
function pickRandomStyles(count: number, excludeNames: string[] = []): typeof ALL_STYLES {
const available = ALL_STYLES.filter((s) => !excludeNames.includes(s.name));
const shuffled = [...available].sort(() => Math.random() - 0.5);
return shuffled.slice(0, count);
}describePet function · typescript · L81-L108 (28 LOC)src/app/api/generate/route.ts
async function describePet(
image: string,
mimeType: string
): Promise<string> {
const visionModel = genAI.getGenerativeModel({ model: "gemini-2.0-flash" });
const result = await visionModel.generateContent([
{
text: `Analyze this photo and describe the pet in it. Be specific about:
1. What type of animal it is (dog, cat, rabbit, etc.)
2. The breed if identifiable
3. The color/pattern of the fur
4. Any distinctive features (spots, stripes, eye color, etc.)
5. The pose/position in the photo
Respond ONLY with a short description like: "a golden retriever dog with light cream fur, brown eyes, sitting and looking at the camera" or "a tabby cat with orange and black striped fur, green eyes, lying down". Keep it under 30 words. Do not add any other text.`,
},
{
inlineData: {
mimeType: mimeType || "image/jpeg",
data: image,
},
},
]);
const text = result.response.text();
return text.trim();
}POST function · typescript · L110-L199 (90 LOC)src/app/api/generate/route.ts
export async function POST(request: NextRequest) {
try {
const { image, mimeType, excludeStyles } = await request.json();
if (!image) {
return NextResponse.json(
{ error: "No image provided" },
{ status: 400 }
);
}
// Step 1: Describe the pet using Gemini Vision
let petDescription: string;
try {
petDescription = await describePet(image, mimeType);
console.log("Pet description:", petDescription);
} catch (err) {
console.error("Failed to describe pet:", err);
petDescription = "the pet animal shown in the reference photo";
}
// Step 2: Generate art variants with anchored prompts
const selectedStyles = pickRandomStyles(3, excludeStyles || []);
const imageModel = genAI.getGenerativeModel({
model: "gemini-2.0-flash-exp-image-generation",
generationConfig: {
// @ts-expect-error - responseModalities is valid for image generation
responseModalities: ["TEXT", "IMAGE"]POST function · typescript · L3-L66 (64 LOC)src/app/api/order/route.ts
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const {
fullName,
email,
whatsapp,
address,
selectedVariant,
tshirtColor,
tshirtSize,
wompiReference,
wompiTransactionId,
wompiStatus,
} = body;
if (!fullName || !email || !whatsapp || !address) {
return NextResponse.json(
{ error: "Todos los campos son obligatorios" },
{ status: 400 }
);
}
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
return NextResponse.json(
{ error: "Email inválido" },
{ status: 400 }
);
}
const order = {
id: crypto.randomUUID(),
fullName,
email,
whatsapp,
address,
selectedVariant,
tshirtColor,
tshirtSize,
payment: {
wompiReference: wompiReference ?? null,
wompiTransactionId: wompiTransactionId ?? null,
wompiStGET function · typescript · L3-L15 (13 LOC)src/app/api/payphone-config/route.ts
export async function GET() {
const token = process.env.PAYPHONE_TOKEN;
const storeId = process.env.PAYPHONE_STORE_ID;
if (!token || !storeId) {
return NextResponse.json(
{ error: "PayPhone no configurado" },
{ status: 500 }
);
}
return NextResponse.json({ token, storeId });
}POST function · typescript · L4-L108 (105 LOC)src/app/api/payphone-confirm/route.ts
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const { id, clientTransactionId, orderData } = body;
if (!id || !clientTransactionId) {
return NextResponse.json(
{ error: "Parámetros de transacción inválidos" },
{ status: 400 }
);
}
const token = process.env.PAYPHONE_TOKEN;
if (!token) {
console.error("PayPhone token not configured");
return NextResponse.json(
{ error: "Error de configuración del pago" },
{ status: 500 }
);
}
const confirmRes = await fetch(
"https://pay.payphonetodoesposible.com/api/button/V2/Confirm",
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({ id, clientTransactionId }),
}
);
if (!confirmRes.ok) {
const errText = await confirmRes.text();
console.error("PayPhPOST function · typescript · L3-L88 (86 LOC)src/app/api/payphone-payment/route.ts
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const { fullName, email, whatsapp, address, clientTransactionId, amount } = body;
if (!fullName || !email || !whatsapp || !address || !clientTransactionId || !amount) {
return NextResponse.json(
{ error: "Todos los campos son obligatorios" },
{ status: 400 }
);
}
const token = process.env.PAYPHONE_TOKEN;
const storeIdRaw = process.env.PAYPHONE_STORE_ID;
const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || "https://pawartstudio.netlify.app";
if (!token || !storeIdRaw) {
console.error("PayPhone credentials not configured");
return NextResponse.json(
{ error: "Error de configuración del pago" },
{ status: 500 }
);
}
const storeId = storeIdRaw;
// PayPhone trabaja en centavos USD
const amountInCents = Math.round(amount * 100);
// clientTransactionId solo alfanumérico, máx 20 charsProvenance: Repobility (https://repobility.com) — every score reproducible from /scan/
POST function · typescript · L4-L42 (39 LOC)src/app/api/transfer-request/route.ts
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const {
fullName,
email,
whatsapp,
address,
tshirtSize,
tshirtColor,
shippingCost,
subtotal,
variantImage,
} = body;
const caption =
`🏦 <b>SOLICITUD DE TRANSFERENCIA BANCARIA</b>\n\n` +
`👤 <b>Cliente:</b> ${fullName ?? "—"}\n` +
`📧 ${email ?? "—"}\n` +
`📱 ${whatsapp ?? "—"}\n` +
`📍 ${address ?? "—"}\n\n` +
`👕 <b>Talla:</b> ${tshirtSize ?? "—"} | <b>Color:</b> ${tshirtColor ?? "—"}\n` +
`💵 <b>Total:</b> $${subtotal ?? "—"} USD\n` +
`🚚 <b>Envío:</b> $${shippingCost ?? "—"} USD\n\n` +
`⚠️ <b>Pendiente confirmación de pago</b>`;
if (variantImage) {
await sendTelegramPhoto(variantImage, caption);
} else {
await sendTelegramMessage(caption);
}
return NextResponse.json({ success: true });
} catch (error) {
console.error("Transfer request notiPOST function · typescript · L5-L67 (63 LOC)src/app/api/wompi-payment/route.ts
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const { fullName, email, whatsapp, address, totalCOP } = body;
if (!fullName || !email || !whatsapp || !address) {
return NextResponse.json(
{ error: "Todos los campos son obligatorios" },
{ status: 400 }
);
}
if (!totalCOP || typeof totalCOP !== "number" || totalCOP < 1000) {
return NextResponse.json(
{ error: "Monto inválido" },
{ status: 400 }
);
}
const integritySecret = process.env.WOMPI_INTEGRITY_SECRET;
const publicKey = process.env.NEXT_PUBLIC_WOMPI_PUBLIC_KEY;
if (!integritySecret || !publicKey) {
console.error("Wompi credentials not configured");
return NextResponse.json(
{ error: "Error de configuración del pago" },
{ status: 500 }
);
}
// Wompi requiere el monto en centavos (COP × 100)
const amountInCents = Math.round(totalCOP) * 100;
/RootLayout function · typescript · L10-L32 (23 LOC)src/app/layout.tsx
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="es">
<head>
<link
href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap"
rel="stylesheet"
/>
<link
href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,[email protected],0..1&display=swap"
rel="stylesheet"
/>
</head>
<body className="bg-background-light text-slate-900 antialiased">
{children}
</body>
</html>
);
}AiVersionsIcon function · typescript · L5-L307 (303 LOC)src/components/icons/AiVersionsIcon.tsx
export default function AiVersionsIcon({ className = "w-full h-full" }: AiVersionsIconProps) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 400 400"
fill="none"
className={className}
aria-label="Una imagen original genera 3 versiones artísticas con IA"
>
<defs>
<style>{`
@keyframes ai_energyFlow {
from { stroke-dashoffset: 0; }
to { stroke-dashoffset: -26; }
}
@keyframes ai_sparklePulse {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.3; transform: scale(0.55); }
}
@keyframes ai_cardBreath {
0%, 100% { opacity: 1; }
50% { opacity: 0.8; }
}
#ai-line-v1 { animation: ai_energyFlow 1.8s linear 0s infinite; }
#ai-line-v2 { animation: ai_energyFlow 1.8s linear 0.4s infinite; }
#ai-line-v3 { animation: ai_energyFlow 1.8s liAvoidBlurIcon function · typescript · L5-L186 (182 LOC)src/components/icons/AvoidBlurIcon.tsx
export default function AvoidBlurIcon({ className = "w-full h-full" }: AvoidBlurIconProps) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 400 400"
fill="none"
className={className}
aria-label="Evita fotos borrosas o en movimiento"
>
<defs>
<style>{`
@keyframes bl_shake {
0%, 100% { transform: translateX(0px); }
14% { transform: translateX(-10px); }
28% { transform: translateX(10px); }
42% { transform: translateX(-8px); }
57% { transform: translateX(8px); }
71% { transform: translateX(-5px); }
85% { transform: translateX(5px); }
}
@keyframes bl_lineFlicker {
0%, 100% { opacity: 0.72; }
50% { opacity: 0.22; }
}
@keyframes bl_cancelPulse {
0%, 100% { transform: scale(1); }
50% AvoidFarIcon function · typescript · L5-L206 (202 LOC)src/components/icons/AvoidFarIcon.tsx
export default function AvoidFarIcon({ className = "w-full h-full" }: AvoidFarIconProps) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 400 400"
fill="none"
className={className}
aria-label="Evita fotos donde la mascota aparece demasiado lejos"
>
<defs>
<style>{`
@keyframes fl_arrowDown {
0%, 100% { transform: translateY(0px); }
50% { transform: translateY(11px); }
}
@keyframes fl_arrowLeft {
0%, 100% { transform: translateX(0px); }
50% { transform: translateX(-11px); }
}
@keyframes fl_arrowUp {
0%, 100% { transform: translateY(0px); }
50% { transform: translateY(-11px); }
}
@keyframes fl_arrowRight {
0%, 100% { transform: translateX(0px); }
50% { transform: translateX(11px); }
}
@keyframes fl_framePulClearFaceIcon function · typescript · L5-L278 (274 LOC)src/components/icons/ClearFaceIcon.tsx
export default function ClearFaceIcon({ className = "w-full h-full" }: ClearFaceIconProps) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 400 400"
fill="none"
className={className}
aria-label="Cara de la mascota bien visible y encuadrada"
>
<defs>
<style>{`
@keyframes cf_blink {
0%, 86%, 100% { transform: scaleY(1); }
91% { transform: scaleY(0.06); }
}
@keyframes cf_eyeGlow {
0%, 100% { opacity: 0; }
50% { opacity: 0.5; }
}
@keyframes cf_checkBounce {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.12); }
}
@keyframes cf_framePulse {
0%, 100% { opacity: 0.55; }
50% { opacity: 1; }
}
@keyframes cf_focusPulse {
0%, 100% { opacity: 0.3; }
50% { opacity: 0DeliveryIcon function · typescript · L5-L299 (295 LOC)src/components/icons/DeliveryIcon.tsx
export default function DeliveryIcon({ className = "w-full h-full" }: DeliveryIconProps) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 400 400"
fill="none"
className={className}
aria-label="Recibe tu camiseta en casa"
>
<defs>
<style>{`
@keyframes del_boxFloat {
0%, 100% { transform: translateX(0px) translateY(0px); }
50% { transform: translateX(6px) translateY(-6px); }
}
@keyframes del_lineSlide {
0% { stroke-dashoffset: 40; opacity: 0.9; }
100% { stroke-dashoffset: -40; opacity: 0.2; }
}
@keyframes del_tailWag {
0%, 100% { transform: rotate(0deg); }
50% { transform: rotate(24deg); }
}
@keyframes del_glow {
0%, 100% { opacity: 0.18; }
50% { opacity: 0.42; }
}
@keyframes del_sparkle {
0%, 100% { opaciRepobility (the analyzer behind this table) · https://repobility.com
GoodLightingIcon function · typescript · L5-L220 (216 LOC)src/components/icons/GoodLightingIcon.tsx
export default function GoodLightingIcon({ className = "w-full h-full" }: GoodLightingIconProps) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 400 400"
fill="none"
className={className}
aria-label="Buena iluminación natural para la foto"
>
<defs>
<style>{`
@keyframes gl_rayPulse {
0%, 100% { opacity: 0.22; }
50% { opacity: 0.48; }
}
@keyframes gl_sunGlowPulse {
0%, 100% { opacity: 0.38; }
50% { opacity: 0.72; }
}
@keyframes gl_checkPulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.12); }
}
@keyframes gl_blink {
0%, 88%, 100% { transform: scaleY(1); }
93% { transform: scaleY(0.07); }
}
@keyframes gl_sunRotate {
from { transform: rotate(0deg); }
to { transfoTshirtSelectorIcon function · typescript · L5-L298 (294 LOC)src/components/icons/TshirtSelectorIcon.tsx
export default function TshirtSelectorIcon({ className = "w-full h-full" }: TshirtSelectorIconProps) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 400 400"
fill="none"
className={className}
aria-label="Elige el color y talla de tu camiseta"
>
<defs>
<style>{`
@keyframes ts_colorPop {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.18); }
}
@keyframes ts_sizeBounce {
0%, 100% { transform: translateY(0px); }
50% { transform: translateY(-5px); }
}
@keyframes ts_designGlow {
0%, 100% { opacity: 1; }
50% { opacity: 0.78; }
}
@keyframes ts_sparkle {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.25; transform: scale(0.55); }
}
#selected-color {
animation: ts_colorUploadDogIcon function · typescript · L5-L278 (274 LOC)src/components/icons/UploadDogIcon.tsx
export default function UploadDogIcon({ className = "w-full h-full" }: UploadDogIconProps) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 400 400"
fill="none"
className={className}
aria-label="Ilustración de perrito para subir foto"
>
<defs>
<style>{`
@keyframes dog_arrowBounce {
0%, 100% { transform: translateY(0px); }
50% { transform: translateY(-12px); }
}
@keyframes dog_earWiggle {
0%, 100% { transform: rotate(0deg); }
30% { transform: rotate(-8deg); }
70% { transform: rotate(6deg); }
}
@keyframes dog_blink {
0%, 88%, 100% { transform: scaleY(1); }
93% { transform: scaleY(0.06); }
}
@keyframes dog_tailWag {
0%, 100% { transform: rotate(0deg); }
50% { transform: rotate(22deg); }
}
#doremoveWhiteBackground function · typescript · L18-L59 (42 LOC)src/components/TshirtPreview3D.tsx
function removeWhiteBackground(dataUrl: string): Promise<string> {
return new Promise((resolve) => {
const img = new Image();
img.crossOrigin = "anonymous";
img.onload = () => {
const canvas = document.createElement("canvas");
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext("2d")!;
ctx.drawImage(img, 0, 0);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data;
// Threshold: pixels with R, G, B all above this value are considered "white"
const threshold = 235;
// Edge softness: pixels between softMin and threshold get partial transparency
const softMin = 210;
for (let i = 0; i < data.length; i += 4) {
const r = data[i];
const g = data[i + 1];
const b = data[i + 2];
if (r > threshold && g > threshold && b > threshold) {
// Fully white -> fully transparent
data[i + 3] Tshirt function · typescript · L66-L122 (57 LOC)src/components/TshirtPreview3D.tsx
function Tshirt({ processedImageUrl, color }: TshirtProps) {
const { nodes } = useGLTF("/shirt_baked.glb") as any;
const meshRef = useRef<THREE.Mesh>(null);
const decalMaterialRef = useRef<THREE.MeshStandardMaterial>(null);
// Load texture manually to ensure transparency works
const texture = useMemo(() => {
const tex = new THREE.TextureLoader().load(processedImageUrl);
tex.anisotropy = 16;
return tex;
}, [processedImageUrl]);
const targetColor = useMemo(() => new THREE.Color(color), [color]);
useFrame(() => {
if (meshRef.current) {
const mat = meshRef.current.material as THREE.MeshStandardMaterial;
mat.color.lerp(targetColor, 0.1);
}
});
return (
<group dispose={null}>
<mesh
ref={meshRef}
castShadow
receiveShadow
geometry={nodes.T_Shirt_male.geometry}
>
<meshStandardMaterial
color={color}
roughness={0.85}
metalness={0}
side={THREE.DoubLoadingSpinner function · typescript · L124-L133 (10 LOC)src/components/TshirtPreview3D.tsx
function LoadingSpinner() {
return (
<div className="absolute inset-0 flex flex-col items-center justify-center bg-slate-100 rounded-xl">
<div className="w-12 h-12 rounded-full border-4 border-primary/20 border-t-primary animate-spin"></div>
<p className="text-sm text-slate-500 mt-4 font-medium">
Cargando vista 3D...
</p>
</div>
);
}TshirtPreview3D function · typescript · L141-L190 (50 LOC)src/components/TshirtPreview3D.tsx
export default function TshirtPreview3D({
artImageBase64,
artMimeType,
color,
}: TshirtPreview3DProps) {
const [processedUrl, setProcessedUrl] = useState<string | null>(null);
useEffect(() => {
const originalUrl = `data:${artMimeType || "image/png"};base64,${artImageBase64}`;
removeWhiteBackground(originalUrl).then(setProcessedUrl);
}, [artImageBase64, artMimeType]);
if (!processedUrl) {
return (
<div className="relative aspect-square rounded-xl overflow-hidden bg-slate-100">
<LoadingSpinner />
</div>
);
}
return (
<div className="relative aspect-square rounded-xl overflow-hidden bg-slate-100">
<Suspense fallback={<LoadingSpinner />}>
<Canvas
shadows
camera={{ position: [0, 0, 2.5], fov: 25 }}
gl={{ preserveDrawingBuffer: true, antialias: true, alpha: true }}
style={{ background: "transparent" }}
>
<ambientLight intensity={0.6} />
<directionalLisendTelegramMessage function · typescript · L4-L14 (11 LOC)src/lib/telegram.ts
export async function sendTelegramMessage(text: string): Promise<void> {
await fetch(`${TELEGRAM_API}/sendMessage`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
chat_id: CHAT_ID,
text,
parse_mode: "HTML",
}),
});
}Open data scored by Repobility · https://repobility.com
sendTelegramPhoto function · typescript · L16-L45 (30 LOC)src/lib/telegram.ts
export async function sendTelegramPhoto(
base64Image: string,
caption: string
): Promise<void> {
// Extraer el buffer del base64 (puede venir como data:image/...;base64,XXXX o solo XXXX)
const base64Data = base64Image.includes(",")
? base64Image.split(",")[1]
: base64Image;
const mimeType = base64Image.startsWith("data:")
? base64Image.split(";")[0].split(":")[1]
: "image/jpeg";
const ext = mimeType.split("/")[1] || "jpg";
const buffer = Buffer.from(base64Data, "base64");
const formData = new FormData();
formData.append("chat_id", CHAT_ID);
formData.append("caption", caption);
formData.append("parse_mode", "HTML");
formData.append(
"photo",
new Blob([buffer], { type: mimeType }),
`design.${ext}`
);
await fetch(`${TELEGRAM_API}/sendPhoto`, {
method: "POST",
body: formData,
});
}