mirror of
https://github.com/jordienr/zenblog.git
synced 2026-08-24 02:34:19 -05:00
fix mobile
This commit is contained in:
@@ -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} />;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user