fixes Markdown
Deploy to VPS / deploy (push) Has been cancelled

This commit is contained in:
2026-04-16 10:29:20 -05:00
parent 69eb449da8
commit 21d0f9ee1c
10 changed files with 170 additions and 44 deletions
@@ -8,6 +8,7 @@ import Image from "next/image";
import Script from "next/script";
import { ArrowLeft, CheckCircle2, Zap, LayoutDashboard, Cpu, PencilRuler, Factory, MapPin, ChevronDown, Play, FileText, Box, Loader2, Maximize, X, ChevronLeft, ChevronRight } from "lucide-react";
import BreathingField from "@/components/visuals/BreathingField";
import AutoPlayVideo from "@/components/AutoPlayVideo";
// 🔥 EL TRUCO DEFINITIVO PARA TYPESCRIPT Y WEB COMPONENTS 🔥
// Al asignar el string a una variable con 'as any', TypeScript deja de
@@ -467,7 +468,7 @@ const renderMarkdown = (text: string, onImageClick: (url: string) => void) => {
</div>
<div className="aspect-video">
{isLocalMp4 ? (
<video src={videoSrc} controls playsInline preload="metadata" className="w-full h-full object-contain" />
<AutoPlayVideo src={videoSrc} className="w-full h-full object-contain" />
) : (
<iframe src={videoSrc} className="w-full h-full" allowFullScreen title="Embedded video" />
)}
@@ -887,7 +888,7 @@ function ExpandedCaseStudy({ node }: { node: any }) {
return (
<div key={`v-${idx}`} className="relative w-full aspect-video rounded-2xl md:rounded-3xl overflow-hidden border border-black/10 dark:border-white/10 shadow-xl bg-black col-span-1 md:col-span-2">
{isLocalMp4 ? (
<video src={videoSrc} controls playsInline preload="metadata" className="absolute inset-0 w-full h-full object-contain" />
<AutoPlayVideo src={videoSrc} className="absolute inset-0 w-full h-full object-contain" />
) : (
<iframe src={videoSrc} className="absolute inset-0 w-full h-full" allowFullScreen title={`Video ${idx + 1}`} />
)}
+8 -11
View File
@@ -5,6 +5,7 @@ import Image from "next/image";
import { prisma } from "@/lib/prisma";
import { ArrowLeft } from "lucide-react";
import BreathingField from "@/components/visuals/BreathingField";
import AutoPlayVideo from "@/components/AutoPlayVideo";
// 🔥 IMPORTACIONES DE IDIOMAS
import { getLocalizedData } from "@/lib/i18nHelper";
@@ -145,7 +146,7 @@ const renderMarkdown = (text: string) => {
return;
}
const ulMatch = trimmed.match(/^[-*]\s*(.*)/);
const ulMatch = trimmed.match(/^[-*]\s+(.*)/);
if (ulMatch) {
isOrderedList = false;
listItems.push(<li key={idx} className="leading-relaxed pl-2">{parseInline(ulMatch[1])}</li>);
@@ -242,17 +243,13 @@ export default async function HeritagePage({ params }: { params: Promise<{ local
</div>
)}
{/* 🔥 BLOQUE DE VIDEO NATIVO (MP4 LOCAL) 🔥 */}
{/* 🔥 BLOQUE DE VIDEO NATIVO (MP4 LOCAL) — AUTOPLAY ON SCROLL 🔥 */}
{sec.type === 'video' && sec.mediaUrl && (
<div className="relative w-full aspect-video rounded-3xl overflow-hidden border border-white/10 shadow-2xl bg-black">
<video
src={`/heritage/videos/${sec.mediaUrl}`}
className="absolute inset-0 w-full h-full object-cover"
autoPlay
loop
muted
playsInline
controls
<AutoPlayVideo
src={`/heritage/videos/${sec.mediaUrl}`}
className="absolute inset-0 w-full h-full object-cover"
loop
/>
</div>
)}
@@ -263,4 +260,4 @@ export default async function HeritagePage({ params }: { params: Promise<{ local
</div>
</main>
);
}
}
@@ -5,6 +5,7 @@ import { X, Wrench, ShoppingBag, ChevronLeft, ChevronRight, Tag, Info, Play, Loc
import { useState } from "react";
import { useUIStore } from "@/lib/store/uiStore";
import { useTranslations } from "next-intl";
import AutoPlayVideo from "@/components/AutoPlayVideo";
// 🔥 IMPORTAMOS TU SUPER PARSER
import { renderMarkdown } from "@/lib/markdownParser";
@@ -73,9 +74,8 @@ export default function PartDetailsModal({ part, isOpen, onClose }: PartDetailsM
{media.length > 0 ? (
<>
{media[currentMediaIdx].endsWith('.mp4') || media[currentMediaIdx].endsWith('.mov') ? (
<video
<AutoPlayVideo
src={`/parts/${part.sku.toLowerCase()}/${media[currentMediaIdx]}`}
controls
className="w-full h-full object-contain"
/>
) : (
@@ -199,4 +199,4 @@ export default function PartDetailsModal({ part, isOpen, onClose }: PartDetailsM
)}
</AnimatePresence>
);
}
}
+81
View File
@@ -0,0 +1,81 @@
"use client";
import { useEffect, useRef } from "react";
interface AutoPlayVideoProps {
src: string;
className?: string;
poster?: string;
threshold?: number;
loop?: boolean;
controls?: boolean;
}
export default function AutoPlayVideo({
src,
className = "",
poster,
threshold = 0.5,
loop = false,
controls = true,
}: AutoPlayVideoProps) {
const videoRef = useRef<HTMLVideoElement>(null);
const userInteractedRef = useRef(false);
useEffect(() => {
const video = videoRef.current;
if (!video) return;
// Track manual user interaction (clicks on play/pause controls).
// We only mark as user interaction if the event was trusted (real user click).
const handlePlay = (e: Event) => {
if ((e as Event).isTrusted) userInteractedRef.current = true;
};
const handlePause = (e: Event) => {
if ((e as Event).isTrusted) userInteractedRef.current = true;
};
video.addEventListener("play", handlePlay);
video.addEventListener("pause", handlePause);
const observer = new IntersectionObserver(
([entry]) => {
if (!video) return;
// If the user has manually controlled the video, respect their decision.
if (userInteractedRef.current) return;
if (entry.isIntersecting) {
video.play().catch(() => {
// Autoplay may be blocked by the browser — that's fine.
});
} else {
video.pause();
}
},
{ threshold }
);
observer.observe(video);
return () => {
observer.disconnect();
video.removeEventListener("play", handlePlay);
video.removeEventListener("pause", handlePause);
};
}, [threshold]);
return (
<video
ref={videoRef}
src={src}
poster={poster}
controls={controls}
muted
playsInline
loop={loop}
preload="metadata"
className={className}
/>
);
}
@@ -2,10 +2,10 @@
import { useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { ArrowRight, Zap, Scale, ShieldCheck, Cpu } from "lucide-react";
// 🔥 Importamos Link de nuestro i18n
import { ArrowRight, Zap, Scale, Cpu } from "lucide-react";
import { Link } from "@/i18n/routing";
import { useTranslations } from "next-intl"; // 🔥
import { useTranslations } from "next-intl";
import { getIconForSlug } from "@/lib/applicationIcons";
export default function ApplicationsDashboard({ dbApps = [] }: { dbApps?: any[] }) {
const activeApps = dbApps.filter(app => app.isActive);
@@ -54,9 +54,14 @@ export default function ApplicationsDashboard({ dbApps = [] }: { dbApps?: any[]
: "bg-transparent border-transparent text-[#86868B] dark:text-[#A1A1A6] hover:bg-black/5 dark:hover:bg-white/5"
}`}
>
<div className={`p-2 rounded-xl ${activeSlug === app.slug ? 'bg-[#0066CC]/10 text-[#0066CC] dark:bg-[#4DA6FF]/10 dark:text-[#4DA6FF]' : 'bg-transparent text-current'}`}>
{app.slug.includes("food") ? <ShieldCheck size={20} /> : <Zap size={20} />}
</div>
{(() => {
const Icon = getIconForSlug(app.slug);
return (
<div className={`p-2 rounded-xl ${activeSlug === app.slug ? 'bg-[#0066CC]/10 text-[#0066CC] dark:bg-[#4DA6FF]/10 dark:text-[#4DA6FF]' : 'bg-transparent text-current'}`}>
<Icon size={20} strokeWidth={1.5} />
</div>
);
})()}
<span className="text-base font-medium">{app.title}</span>
</button>
))}
+3 -15
View File
@@ -1,22 +1,10 @@
"use client";
import { motion } from "framer-motion";
import { ArrowRight, Waves, Microscope, Snowflake, ShieldCheck, ThermometerSun, FlaskConical, Box, Droplets, Zap, Leaf } from "lucide-react";
import { Link } from "@/i18n/routing";
import { useTranslations } from "next-intl";
const getIconForSlug = (slug: string) => {
if (slug.includes("textile")) return Waves;
if (slug.includes("lab")) return Microscope;
if (slug.includes("temper") || slug.includes("defrost")) return Snowflake;
if (slug.includes("pasteuriz")) return ShieldCheck;
if (slug.includes("bak")) return ThermometerSun;
if (slug.includes("vulcaniz")) return FlaskConical;
if (slug.includes("foam")) return Box;
if (slug.includes("print")) return Droplets;
if (slug.includes("cannabis") || slug.includes("herb")) return Leaf;
return Zap;
};
import { ArrowRight } from "lucide-react";
import { motion } from "framer-motion";
import { getIconForSlug } from "@/lib/applicationIcons";
export default function ApplicationsDeep({ dbApps = [] }: { dbApps?: any[] }) {
const t = useTranslations("AppsDeep");
+31 -3
View File
@@ -163,9 +163,37 @@ function MapNode({ marker, isSelected, hqPos, onSelect, isDark, globeMode, camDi
gRef.current.scale.setScalar(scaleFactor * (isSelected ? 1.5 : 1.0));
});
const dist = hqPos.distanceTo(pos);
const apex = hqPos.clone().lerp(pos, 0.5).normalize()
.multiplyScalar(RADIUS + dist * 0.28 + 0.14);
// Calculate the great-circle angle between the two points (0 = same point, π = antipodes)
const dotProduct = hqPos.clone().normalize().dot(pos.clone().normalize());
const angle = Math.acos(Math.max(-1, Math.min(1, dotProduct)));
// Compute apex direction using slerp (spherical interpolation), not lerp (linear).
// For near-antipodal points, lerp returns ~origin and normalize becomes unstable.
// Slerp gives the correct great-circle midpoint on the sphere surface.
let apexDir: THREE.Vector3;
if (angle < 0.001) {
// Same point — fallback
apexDir = hqPos.clone().normalize();
} else if (angle > Math.PI - 0.05) {
// Near antipodes — slerp is undefined, pick a perpendicular direction
// that goes "over the top" of the globe
const arbitrary = Math.abs(hqPos.y) < 0.9 ? new THREE.Vector3(0, 1, 0) : new THREE.Vector3(1, 0, 0);
apexDir = hqPos.clone().normalize().cross(arbitrary).normalize();
} else {
// Standard slerp midpoint
const sinAngle = Math.sin(angle);
const a = Math.sin(0.5 * angle) / sinAngle;
const b = Math.sin(0.5 * angle) / sinAngle;
apexDir = hqPos.clone().normalize().multiplyScalar(a)
.add(pos.clone().normalize().multiplyScalar(b))
.normalize();
}
// Arc height scales with the angle: short arcs stay close to surface,
// long arcs (near antipodes) lift high above the globe.
// angle ranges from 0 to π. Height factor scales from 0.15 to ~1.4 of RADIUS.
const heightFactor = 0.15 + (angle / Math.PI) * 1.25;
const apex = apexDir.multiplyScalar(RADIUS * (1 + heightFactor));
// ── ARC LINE COLORS & OPACITY ──
// Photo mode uses high-contrast colors (orange/yellow) instead of blue
+14
View File
@@ -0,0 +1,14 @@
import { Waves, Microscope, Snowflake, ShieldCheck, ThermometerSun, FlaskConical, Box, Droplets, Zap, Leaf } from "lucide-react";
export const getIconForSlug = (slug: string) => {
if (slug.includes("textile")) return Waves;
if (slug.includes("lab")) return Microscope;
if (slug.includes("temper") || slug.includes("defrost")) return Snowflake;
if (slug.includes("pasteuriz") || slug.includes("cooking")) return ShieldCheck;
if (slug.includes("bak")) return ThermometerSun;
if (slug.includes("vulcaniz") || slug.includes("vucaniz")) return FlaskConical;
if (slug.includes("foam")) return Box;
if (slug.includes("print")) return Droplets;
if (slug.includes("cannabis") || slug.includes("herb")) return Leaf;
return Zap;
};
+5 -4
View File
@@ -1,5 +1,6 @@
import React from "react";
import { Play, Maximize2 } from "lucide-react";
import AutoPlayVideo from "@/components/AutoPlayVideo";
// Nota: No incluí el componente 3D directamente aquí para no complicar dependencias,
// pero soporta tablas, listas, citas, videos e imágenes con lightbox.
@@ -113,7 +114,7 @@ export const renderMarkdown = (text: string, onImageClick?: (url: string) => voi
return;
}
// ── VIDEO ──
// ── VIDEO (autoplay on viewport) ──
const videoMatch = trimmed.match(/^\[VIDEO:(.*?)\]$/);
if (videoMatch) {
pushList(); pushTable();
@@ -123,7 +124,7 @@ export const renderMarkdown = (text: string, onImageClick?: (url: string) => voi
<div className="absolute top-2 left-2 z-10 pointer-events-none bg-black/70 backdrop-blur-sm rounded-full px-2 py-0.5 flex items-center gap-1">
<Play size={10} className="text-white" /><span className="text-[9px] font-bold text-white uppercase tracking-widest">Video</span>
</div>
<video src={videoSrc} controls playsInline className="w-full h-full object-contain" />
<AutoPlayVideo src={videoSrc} className="w-full h-full object-contain" />
</div>
);
return;
@@ -152,7 +153,7 @@ export const renderMarkdown = (text: string, onImageClick?: (url: string) => voi
}
// ── LISTS ──
const ulMatch = trimmed.match(/^[-*]\s*(.*)/);
const ulMatch = trimmed.match(/^[-*]\s+(.*)/);
if (ulMatch) { isOrderedList = false; listItems.push(<li key={idx}>{parseInline(ulMatch[1])}</li>); return; }
const olMatch = trimmed.match(/^\d+\.\s*(.*)/);
@@ -165,4 +166,4 @@ export const renderMarkdown = (text: string, onImageClick?: (url: string) => voi
pushList(); pushTable();
return <>{elements}</>;
};
};