fix mobile

This commit is contained in:
Jordi Enric
2025-07-28 20:49:15 +02:00
parent 4b4c452a20
commit fc86446032
5 changed files with 100 additions and 142 deletions
@@ -0,0 +1,71 @@
import { motion, AnimatePresence } from "framer-motion";
import { useState, useEffect } from "react";
import { BrowserWrapper } from "./browser-wrapper";
import { CheckCircle2, Loader2 } from "lucide-react";
import { SyntaxHighlight } from "../syntax-highlight";
import { cn } from "@/lib/utils";
export type CodeStepperStep = {
id: string;
comment: string;
code: string;
browser: (handleStepClick: (index: number) => void) => React.ReactNode;
};
export const CodeStepper = ({ steps }: { steps: CodeStepperStep[] }) => {
const [activeStep, setActiveStep] = useState(0);
const [isLoading, setIsLoading] = useState(false);
const handleStepClick = (index: number) => {
if (index === activeStep) return;
setActiveStep(index);
setIsLoading(true);
setTimeout(() => {
setIsLoading(false);
}, 500);
};
return (
<div className="flex flex-wrap gap-px rounded-3xl bg-slate-800 p-2 md:min-h-[600px] md:flex-nowrap">
<div className="mb-4 w-full max-w-full space-y-px overflow-x-auto font-mono text-sm md:pr-4">
{steps.map((step, index) => (
<div
key={step.id}
className={cn(
`cursor-pointer rounded-2xl p-4 opacity-50 transition-opacity duration-300 hover:bg-slate-700/50`,
{ "bg-slate-700/50 opacity-100": index === activeStep }
)}
onClick={() => handleStepClick(index)}
>
<div className="mb-3 text-slate-400">{`// ` + step.comment}</div>
<pre className="whitespace-pre-wrap text-white md:whitespace-pre-line">
<SyntaxHighlight
code={step.code}
language="tsx"
></SyntaxHighlight>
</pre>
</div>
))}
</div>
<div className="min-h-[400px] w-full min-w-80 md:max-h-full md:max-w-[500px]">
<BrowserWrapper>
<motion.div
key={isLoading ? "loading" : activeStep}
className="p-4"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.3 }}
>
{isLoading ? (
<div className="flex h-full w-full items-center justify-center py-8">
<Loader2 className="animate-spin" />
</div>
) : (
steps[activeStep]?.browser(handleStepClick)
)}
</motion.div>
</BrowserWrapper>
</div>
</div>
);
};
@@ -2,7 +2,7 @@ import { motion } from "framer-motion";
export function BrowserWrapper({ children }: { children: React.ReactNode }) {
return (
<div className="rounded-2xl border border-slate-300 bg-gradient-to-b from-slate-100 to-slate-200 p-2">
<div className="h-full rounded-2xl border border-slate-300 bg-gradient-to-b from-slate-100 to-slate-200 p-2">
<div className="flex items-center gap-2 px-2 pb-1">
{new Array(3).fill(0).map((v, idx) => (
<span
@@ -15,7 +15,7 @@ export function BrowserWrapper({ children }: { children: React.ReactNode }) {
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.4, ease: "easeInOut" }}
className="min-h-[600px] overflow-hidden rounded-xl border border-slate-300"
className="h-full overflow-hidden rounded-xl border border-slate-300 md:min-h-[600px]"
>
{children}
</motion.div>
@@ -6,6 +6,7 @@ import { HttpApiExample } from "./http-api-example";
import { LaptopMinimal, PencilLine } from "lucide-react";
import { TbBrandTypescript } from "react-icons/tb";
import { TbHttpGet } from "react-icons/tb";
import { cn } from "@/lib/utils";
export const HeroImages = () => {
const FEATURES = [
@@ -56,15 +57,17 @@ export const HeroImages = () => {
return (
<Tabs value={currentFeature?.id} className="w-full">
<TabsList className="mb-4 w-full space-x-4">
<TabsList className="flex h-16 w-full justify-start space-x-2 overflow-x-auto md:justify-center md:space-x-4">
{FEATURES.map((f, index) => (
<TabsTrigger
key={`${f.id}-trigger`}
value={f.id}
onClick={() => setCurrentIndex(index)}
className="relative flex cursor-pointer items-center overflow-hidden rounded-full border px-4 py-2 text-lg font-semibold opacity-70 data-[state=active]:border data-[state=active]:border-b data-[state=active]:border-slate-800 data-[state=active]:bg-slate-900 data-[state=active]:text-slate-100 data-[state=active]:opacity-100"
className={cn(
"relative flex min-w-fit cursor-pointer items-center overflow-hidden rounded-full border px-4 py-2 font-semibold opacity-70 data-[state=active]:border-b data-[state=active]:border-slate-300 data-[state=active]:bg-slate-100 data-[state=active]:text-slate-800 data-[state=active]:opacity-100 md:text-lg"
)}
>
{f.icon}
<span className="opacity-70">{f.icon}</span>
{f.title}
</TabsTrigger>
))}
@@ -1,9 +1,4 @@
import { motion, AnimatePresence } from "framer-motion";
import { useState, useEffect } from "react";
import { BrowserWrapper } from "./browser-wrapper";
import { CheckCircle2, Loader2 } from "lucide-react";
import { SyntaxHighlight } from "../syntax-highlight";
import { cn } from "@/lib/utils";
import { CodeStepper } from "./CodeStepper";
export const HttpApiExample = () => {
const posts = [
@@ -27,16 +22,19 @@ export const HttpApiExample = () => {
comment: "fetch posts to build blog homepage",
code: `const response = await fetch('https://api.zenblog.com/blogs/YOUR_BLOG_ID/posts');
const { data: posts } = await response.json();`,
browser: (
browser: () => (
<div>
<h1 className="py-8 text-center text-xl font-medium">
My Startup Blog
</h1>
<div className="grid grid-cols-2 gap-4">
<div className="grid gap-4">
{posts.map((post) => (
<div key={post.title}>
<div className="h-24 rounded-lg bg-gradient-to-br from-blue-500 to-blue-600"></div>
<h3 className="my-4 text-xl font-medium">{post.title}</h3>
<div key={post.title} className="flex gap-4">
<div className="h-16 w-16 rounded-lg bg-gradient-to-br from-blue-500 to-blue-600"></div>
<div>
<h3 className="text-lg font-medium">{post.title}</h3>
<p className="text-sm text-gray-600">{post.published_at}</p>
</div>
</div>
))}
</div>
@@ -48,7 +46,7 @@ const { data: posts } = await response.json();`,
comment: "fetch a single post with its content",
code: `const response = await fetch('https://api.zenblog.com/blogs/YOUR_BLOG_ID/posts/post-slug');
const post = await response.json();`,
browser: (
browser: () => (
<div>
<div className="mb-4 h-24 rounded-lg bg-gradient-to-br from-blue-500 to-blue-600"></div>
<h1 className="text-2xl font-medium">{posts[0].title}</h1>
@@ -67,7 +65,7 @@ const post = await response.json();`,
comment: "fetch posts filtered by category or tags",
code: `const response = await fetch('https://api.zenblog.com/blogs/YOUR_BLOG_ID/posts?category=news&limit=10');
const { data: posts } = await response.json();`,
browser: (
browser: () => (
<div>
<h1 className="py-8 text-center text-xl font-medium">
News Category
@@ -88,60 +86,5 @@ const { data: posts } = await response.json();`,
},
];
const [activeStep, setActiveStep] = useState(0);
const [isLoading, setIsLoading] = useState(false);
const handleStepClick = (index: number) => {
if (index === activeStep) return;
setActiveStep(index);
setIsLoading(true);
setTimeout(() => {
setIsLoading(false);
}, 500);
};
return (
<div className="flex min-h-[600px] gap-px rounded-3xl bg-slate-800 p-2">
<div className="space-y-px pr-4 font-mono text-sm">
{STEPS.map((step, index) => (
<div
key={step.id}
className={cn(
`cursor-pointer rounded-2xl p-4 opacity-50 transition-opacity duration-300 hover:bg-slate-700/50`,
{ "bg-slate-700/50 opacity-100": index === activeStep }
)}
onClick={() => handleStepClick(index)}
>
<div className="mb-3 text-slate-400">{`// ` + step.comment}</div>
<pre className="whitespace-pre-wrap text-white">
<SyntaxHighlight
code={step.code}
language="javascript"
></SyntaxHighlight>
</pre>
</div>
))}
</div>
<div className="w-[500px]">
<BrowserWrapper>
<motion.div
key={isLoading ? "loading" : activeStep}
className="p-4"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.3 }}
>
{isLoading ? (
<div className="flex h-60 w-full items-center justify-center">
<Loader2 className="animate-spin" />
</div>
) : (
STEPS[activeStep]?.browser
)}
</motion.div>
</BrowserWrapper>
</div>
</div>
);
return <CodeStepper steps={STEPS} />;
};
@@ -1,14 +1,7 @@
import { motion, AnimatePresence } from "framer-motion";
import { useState, useEffect } from "react";
import { BrowserWrapper } from "./browser-wrapper";
import { CheckCircle2, Loader2 } from "lucide-react";
import { SyntaxHighlight } from "../syntax-highlight";
import { cn } from "@/lib/utils";
import { CheckCircle2 } from "lucide-react";
import { CodeStepper, CodeStepperStep } from "./CodeStepper";
export const TsApiExample = () => {
const [activeStep, setActiveStep] = useState(0);
const [isLoading, setIsLoading] = useState(false);
const posts = [
{
title: "Blogging Tips for 2025",
@@ -24,14 +17,14 @@ export const TsApiExample = () => {
},
] as const;
const STEPS = [
const STEPS: CodeStepperStep[] = [
{
id: "init",
comment: "1. initialize the zenblog client",
code: `import { createZenblogClient } from "zenblog";
const zenblog = createZenblogClient({ blogId: BLOG_ID });`,
browser: (
browser: (handleStepClick: (index: number) => void) => (
<div>
<div className="flex h-60 w-full flex-col items-center justify-center gap-4 text-center">
<CheckCircle2 className="text-emerald-500" />
@@ -40,7 +33,7 @@ const zenblog = createZenblogClient({ blogId: BLOG_ID });`,
</span>
<button
onClick={() => {
setActiveStep(1);
handleStepClick(1);
}}
className="rounded-full bg-black px-3 py-1.5 font-medium text-white"
>
@@ -54,7 +47,7 @@ const zenblog = createZenblogClient({ blogId: BLOG_ID });`,
id: "getposts",
comment: "2. fetch posts to build blog homepage",
code: `const posts = await zenblog.posts.list();`,
browser: (
browser: () => (
<div>
<h1 className="py-8 text-center text-xl font-medium">
My Startup Blog
@@ -74,7 +67,7 @@ const zenblog = createZenblogClient({ blogId: BLOG_ID });`,
id: "getpost",
comment: "3. fetch a single post with its content",
code: `const post = await zenblog.posts.get({ slug: params.slug });`,
browser: (
browser: () => (
<div>
<div className="mb-4 h-24 rounded-lg bg-gradient-to-br from-orange-500 to-orange-600"></div>
<h1 className="text-2xl font-medium">{posts[0].title}</h1>
@@ -88,59 +81,7 @@ const zenblog = createZenblogClient({ blogId: BLOG_ID });`,
</div>
),
},
];
] as const;
const handleStepClick = (index: number) => {
if (index === activeStep) return;
setActiveStep(index);
setIsLoading(true);
setTimeout(() => {
setIsLoading(false);
}, 500);
};
return (
<div className="flex min-h-[600px] gap-px rounded-3xl bg-slate-800 p-2">
<div className="w-full space-y-px pr-4 font-mono text-sm">
{STEPS.map((step, index) => (
<div
key={step.id}
className={cn(
`cursor-pointer rounded-2xl p-4 opacity-50 transition-opacity duration-300 hover:bg-slate-700/50`,
{ "bg-slate-700/50 opacity-100": index === activeStep }
)}
onClick={() => handleStepClick(index)}
>
<div className="mb-3 text-slate-400">{`// ` + step.comment}</div>
<pre className="whitespace-pre-wrap text-white">
<SyntaxHighlight
code={step.code}
language="tsx"
></SyntaxHighlight>
</pre>
</div>
))}
</div>
<div className="w-[500px]">
<BrowserWrapper>
<motion.div
key={isLoading ? "loading" : activeStep}
className="p-4"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.3 }}
>
{isLoading ? (
<div className="flex h-60 w-full items-center justify-center">
<Loader2 className="animate-spin" />
</div>
) : (
STEPS[activeStep]?.browser
)}
</motion.div>
</BrowserWrapper>
</div>
</div>
);
return <CodeStepper steps={STEPS} />;
};