Skip to content

Commit 4b4c452

Browse files
committed
new homepage
1 parent dd9893d commit 4b4c452

File tree

9 files changed

+484
-142
lines changed

9 files changed

+484
-142
lines changed
925 KB
Loading
3.35 MB
Loading
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { motion } from "framer-motion";
2+
3+
export function BrowserWrapper({ children }: { children: React.ReactNode }) {
4+
return (
5+
<div className="rounded-2xl border border-slate-300 bg-gradient-to-b from-slate-100 to-slate-200 p-2">
6+
<div className="flex items-center gap-2 px-2 pb-1">
7+
{new Array(3).fill(0).map((v, idx) => (
8+
<span
9+
key={`dot-${idx}`}
10+
className="block size-3 rounded-full border border-slate-300"
11+
></span>
12+
))}
13+
</div>
14+
<motion.div
15+
initial={{ opacity: 0 }}
16+
animate={{ opacity: 1 }}
17+
transition={{ duration: 0.4, ease: "easeInOut" }}
18+
className="min-h-[600px] overflow-hidden rounded-xl border border-slate-300"
19+
>
20+
{children}
21+
</motion.div>
22+
</div>
23+
);
24+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs";
2+
import { useEffect, useRef, useState } from "react";
3+
import { BrowserWrapper } from "./browser-wrapper";
4+
import { TsApiExample } from "./ts-api-example";
5+
import { HttpApiExample } from "./http-api-example";
6+
import { LaptopMinimal, PencilLine } from "lucide-react";
7+
import { TbBrandTypescript } from "react-icons/tb";
8+
import { TbHttpGet } from "react-icons/tb";
9+
10+
export const HeroImages = () => {
11+
const FEATURES = [
12+
{
13+
id: "editor",
14+
title: "Editor",
15+
icon: <PencilLine className="mr-2 size-5 opacity-80" />,
16+
content: (
17+
<BrowserWrapper>
18+
<img
19+
src="/static/hero/editor.png"
20+
alt="Editor"
21+
className="h-full w-full rounded-lg border-blue-300 object-cover shadow-xl"
22+
/>
23+
</BrowserWrapper>
24+
),
25+
},
26+
{
27+
id: "dashboard",
28+
title: "Dashboard",
29+
icon: <LaptopMinimal className="mr-2 size-5 opacity-80" />,
30+
content: (
31+
<BrowserWrapper>
32+
<img
33+
src="/static/hero/dashboard.png"
34+
alt="Dashboard"
35+
className="h-full w-full rounded-lg border-orange-300 object-cover shadow-xl"
36+
/>
37+
</BrowserWrapper>
38+
),
39+
},
40+
{
41+
id: "tsapi",
42+
title: "TypeScript",
43+
content: <TsApiExample />,
44+
icon: <TbBrandTypescript className="mr-2 size-5 opacity-80" />,
45+
},
46+
{
47+
id: "httpapi",
48+
title: "HTTP",
49+
content: <HttpApiExample />,
50+
icon: <TbHttpGet className="mr-2 size-5 opacity-80" />,
51+
},
52+
];
53+
54+
const [currentIndex, setCurrentIndex] = useState(0);
55+
const currentFeature = FEATURES[currentIndex];
56+
57+
return (
58+
<Tabs value={currentFeature?.id} className="w-full">
59+
<TabsList className="mb-4 w-full space-x-4">
60+
{FEATURES.map((f, index) => (
61+
<TabsTrigger
62+
key={`${f.id}-trigger`}
63+
value={f.id}
64+
onClick={() => setCurrentIndex(index)}
65+
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"
66+
>
67+
{f.icon}
68+
{f.title}
69+
</TabsTrigger>
70+
))}
71+
</TabsList>
72+
{FEATURES.map((f) => (
73+
<TabsContent key={`${f.id}-content`} value={f.id}>
74+
{f.content}
75+
</TabsContent>
76+
))}
77+
</Tabs>
78+
);
79+
};
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import { motion, AnimatePresence } from "framer-motion";
2+
import { useState, useEffect } from "react";
3+
import { BrowserWrapper } from "./browser-wrapper";
4+
import { CheckCircle2, Loader2 } from "lucide-react";
5+
import { SyntaxHighlight } from "../syntax-highlight";
6+
import { cn } from "@/lib/utils";
7+
8+
export const HttpApiExample = () => {
9+
const posts = [
10+
{
11+
title: "Blogging Tips for 2025",
12+
published_at: "2025-07-31",
13+
},
14+
{
15+
title: "Grow your business with SEO",
16+
published_at: "2025-08-15",
17+
},
18+
{
19+
title: "The Future of Web Design",
20+
published_at: "2025-09-01",
21+
},
22+
] as const;
23+
24+
const STEPS = [
25+
{
26+
id: "fetch-posts",
27+
comment: "fetch posts to build blog homepage",
28+
code: `const response = await fetch('https://api.zenblog.com/blogs/YOUR_BLOG_ID/posts');
29+
const { data: posts } = await response.json();`,
30+
browser: (
31+
<div>
32+
<h1 className="py-8 text-center text-xl font-medium">
33+
My Startup Blog
34+
</h1>
35+
<div className="grid grid-cols-2 gap-4">
36+
{posts.map((post) => (
37+
<div key={post.title}>
38+
<div className="h-24 rounded-lg bg-gradient-to-br from-blue-500 to-blue-600"></div>
39+
<h3 className="my-4 text-xl font-medium">{post.title}</h3>
40+
</div>
41+
))}
42+
</div>
43+
</div>
44+
),
45+
},
46+
{
47+
id: "fetch-post",
48+
comment: "fetch a single post with its content",
49+
code: `const response = await fetch('https://api.zenblog.com/blogs/YOUR_BLOG_ID/posts/post-slug');
50+
const post = await response.json();`,
51+
browser: (
52+
<div>
53+
<div className="mb-4 h-24 rounded-lg bg-gradient-to-br from-blue-500 to-blue-600"></div>
54+
<h1 className="text-2xl font-medium">{posts[0].title}</h1>
55+
<p>Published on: {posts[0].published_at}</p>
56+
<hr />
57+
<p className="mt-4 font-mono">
58+
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed
59+
ullamcorper, nisl at venenatis facilibus, erat felis aliquet enim,
60+
nec luctus ligula leo et quam.
61+
</p>
62+
</div>
63+
),
64+
},
65+
{
66+
id: "fetch-filtered",
67+
comment: "fetch posts filtered by category or tags",
68+
code: `const response = await fetch('https://api.zenblog.com/blogs/YOUR_BLOG_ID/posts?category=news&limit=10');
69+
const { data: posts } = await response.json();`,
70+
browser: (
71+
<div>
72+
<h1 className="py-8 text-center text-xl font-medium">
73+
News Category
74+
</h1>
75+
<div className="space-y-4">
76+
{posts.slice(0, 2).map((post) => (
77+
<div key={post.title} className="flex gap-4">
78+
<div className="h-16 w-16 rounded-lg bg-gradient-to-br from-blue-500 to-blue-600"></div>
79+
<div>
80+
<h3 className="text-lg font-medium">{post.title}</h3>
81+
<p className="text-sm text-gray-600">{post.published_at}</p>
82+
</div>
83+
</div>
84+
))}
85+
</div>
86+
</div>
87+
),
88+
},
89+
];
90+
91+
const [activeStep, setActiveStep] = useState(0);
92+
const [isLoading, setIsLoading] = useState(false);
93+
94+
const handleStepClick = (index: number) => {
95+
if (index === activeStep) return;
96+
97+
setActiveStep(index);
98+
setIsLoading(true);
99+
setTimeout(() => {
100+
setIsLoading(false);
101+
}, 500);
102+
};
103+
104+
return (
105+
<div className="flex min-h-[600px] gap-px rounded-3xl bg-slate-800 p-2">
106+
<div className="space-y-px pr-4 font-mono text-sm">
107+
{STEPS.map((step, index) => (
108+
<div
109+
key={step.id}
110+
className={cn(
111+
`cursor-pointer rounded-2xl p-4 opacity-50 transition-opacity duration-300 hover:bg-slate-700/50`,
112+
{ "bg-slate-700/50 opacity-100": index === activeStep }
113+
)}
114+
onClick={() => handleStepClick(index)}
115+
>
116+
<div className="mb-3 text-slate-400">{`// ` + step.comment}</div>
117+
<pre className="whitespace-pre-wrap text-white">
118+
<SyntaxHighlight
119+
code={step.code}
120+
language="javascript"
121+
></SyntaxHighlight>
122+
</pre>
123+
</div>
124+
))}
125+
</div>
126+
<div className="w-[500px]">
127+
<BrowserWrapper>
128+
<motion.div
129+
key={isLoading ? "loading" : activeStep}
130+
className="p-4"
131+
initial={{ opacity: 0 }}
132+
animate={{ opacity: 1 }}
133+
transition={{ duration: 0.3 }}
134+
>
135+
{isLoading ? (
136+
<div className="flex h-60 w-full items-center justify-center">
137+
<Loader2 className="animate-spin" />
138+
</div>
139+
) : (
140+
STEPS[activeStep]?.browser
141+
)}
142+
</motion.div>
143+
</BrowserWrapper>
144+
</div>
145+
</div>
146+
);
147+
};
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import { motion, AnimatePresence } from "framer-motion";
2+
import { useState, useEffect } from "react";
3+
import { BrowserWrapper } from "./browser-wrapper";
4+
import { CheckCircle2, Loader2 } from "lucide-react";
5+
import { SyntaxHighlight } from "../syntax-highlight";
6+
import { cn } from "@/lib/utils";
7+
8+
export const TsApiExample = () => {
9+
const [activeStep, setActiveStep] = useState(0);
10+
const [isLoading, setIsLoading] = useState(false);
11+
12+
const posts = [
13+
{
14+
title: "Blogging Tips for 2025",
15+
published_at: "2025-07-31",
16+
},
17+
{
18+
title: "Grow your business with SEO",
19+
published_at: "2025-08-15",
20+
},
21+
{
22+
title: "The Future of Web Design",
23+
published_at: "2025-09-01",
24+
},
25+
] as const;
26+
27+
const STEPS = [
28+
{
29+
id: "init",
30+
comment: "1. initialize the zenblog client",
31+
code: `import { createZenblogClient } from "zenblog";
32+
33+
const zenblog = createZenblogClient({ blogId: BLOG_ID });`,
34+
browser: (
35+
<div>
36+
<div className="flex h-60 w-full flex-col items-center justify-center gap-4 text-center">
37+
<CheckCircle2 className="text-emerald-500" />
38+
<span className="font-mono text-sm">
39+
Zenblog client initialized successfully!
40+
</span>
41+
<button
42+
onClick={() => {
43+
setActiveStep(1);
44+
}}
45+
className="rounded-full bg-black px-3 py-1.5 font-medium text-white"
46+
>
47+
Next step
48+
</button>
49+
</div>
50+
</div>
51+
),
52+
},
53+
{
54+
id: "getposts",
55+
comment: "2. fetch posts to build blog homepage",
56+
code: `const posts = await zenblog.posts.list();`,
57+
browser: (
58+
<div>
59+
<h1 className="py-8 text-center text-xl font-medium">
60+
My Startup Blog
61+
</h1>
62+
<div className="grid grid-cols-2 gap-4">
63+
{posts.map((post) => (
64+
<div key={post.title}>
65+
<div className="h-24 rounded-lg bg-gradient-to-br from-orange-500 to-orange-600"></div>
66+
<h3 className="my-4 text-xl font-medium">{post.title}</h3>
67+
</div>
68+
))}
69+
</div>
70+
</div>
71+
),
72+
},
73+
{
74+
id: "getpost",
75+
comment: "3. fetch a single post with its content",
76+
code: `const post = await zenblog.posts.get({ slug: params.slug });`,
77+
browser: (
78+
<div>
79+
<div className="mb-4 h-24 rounded-lg bg-gradient-to-br from-orange-500 to-orange-600"></div>
80+
<h1 className="text-2xl font-medium">{posts[0].title}</h1>
81+
<p>Published on: {posts[0].published_at}</p>
82+
<hr />
83+
<p className="mt-4 font-mono">
84+
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed
85+
ullamcorper, nisl at venenatis facilisis, erat felis aliquet enim,
86+
nec luctus ligula leo et quam.
87+
</p>
88+
</div>
89+
),
90+
},
91+
];
92+
93+
const handleStepClick = (index: number) => {
94+
if (index === activeStep) return;
95+
96+
setActiveStep(index);
97+
setIsLoading(true);
98+
setTimeout(() => {
99+
setIsLoading(false);
100+
}, 500);
101+
};
102+
103+
return (
104+
<div className="flex min-h-[600px] gap-px rounded-3xl bg-slate-800 p-2">
105+
<div className="w-full space-y-px pr-4 font-mono text-sm">
106+
{STEPS.map((step, index) => (
107+
<div
108+
key={step.id}
109+
className={cn(
110+
`cursor-pointer rounded-2xl p-4 opacity-50 transition-opacity duration-300 hover:bg-slate-700/50`,
111+
{ "bg-slate-700/50 opacity-100": index === activeStep }
112+
)}
113+
onClick={() => handleStepClick(index)}
114+
>
115+
<div className="mb-3 text-slate-400">{`// ` + step.comment}</div>
116+
<pre className="whitespace-pre-wrap text-white">
117+
<SyntaxHighlight
118+
code={step.code}
119+
language="tsx"
120+
></SyntaxHighlight>
121+
</pre>
122+
</div>
123+
))}
124+
</div>
125+
<div className="w-[500px]">
126+
<BrowserWrapper>
127+
<motion.div
128+
key={isLoading ? "loading" : activeStep}
129+
className="p-4"
130+
initial={{ opacity: 0 }}
131+
animate={{ opacity: 1 }}
132+
transition={{ duration: 0.3 }}
133+
>
134+
{isLoading ? (
135+
<div className="flex h-60 w-full items-center justify-center">
136+
<Loader2 className="animate-spin" />
137+
</div>
138+
) : (
139+
STEPS[activeStep]?.browser
140+
)}
141+
</motion.div>
142+
</BrowserWrapper>
143+
</div>
144+
</div>
145+
);
146+
};

0 commit comments

Comments
 (0)