mirror of
https://github.com/jordienr/zenblog.git
synced 2026-08-24 02:34:19 -05:00
new homepage
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 925 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 3.4 MiB |
@@ -0,0 +1,24 @@
|
||||
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="flex items-center gap-2 px-2 pb-1">
|
||||
{new Array(3).fill(0).map((v, idx) => (
|
||||
<span
|
||||
key={`dot-${idx}`}
|
||||
className="block size-3 rounded-full border border-slate-300"
|
||||
></span>
|
||||
))}
|
||||
</div>
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.4, ease: "easeInOut" }}
|
||||
className="min-h-[600px] overflow-hidden rounded-xl border border-slate-300"
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { BrowserWrapper } from "./browser-wrapper";
|
||||
import { TsApiExample } from "./ts-api-example";
|
||||
import { HttpApiExample } from "./http-api-example";
|
||||
import { LaptopMinimal, PencilLine } from "lucide-react";
|
||||
import { TbBrandTypescript } from "react-icons/tb";
|
||||
import { TbHttpGet } from "react-icons/tb";
|
||||
|
||||
export const HeroImages = () => {
|
||||
const FEATURES = [
|
||||
{
|
||||
id: "editor",
|
||||
title: "Editor",
|
||||
icon: <PencilLine className="mr-2 size-5 opacity-80" />,
|
||||
content: (
|
||||
<BrowserWrapper>
|
||||
<img
|
||||
src="/static/hero/editor.png"
|
||||
alt="Editor"
|
||||
className="h-full w-full rounded-lg border-blue-300 object-cover shadow-xl"
|
||||
/>
|
||||
</BrowserWrapper>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "dashboard",
|
||||
title: "Dashboard",
|
||||
icon: <LaptopMinimal className="mr-2 size-5 opacity-80" />,
|
||||
content: (
|
||||
<BrowserWrapper>
|
||||
<img
|
||||
src="/static/hero/dashboard.png"
|
||||
alt="Dashboard"
|
||||
className="h-full w-full rounded-lg border-orange-300 object-cover shadow-xl"
|
||||
/>
|
||||
</BrowserWrapper>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "tsapi",
|
||||
title: "TypeScript",
|
||||
content: <TsApiExample />,
|
||||
icon: <TbBrandTypescript className="mr-2 size-5 opacity-80" />,
|
||||
},
|
||||
{
|
||||
id: "httpapi",
|
||||
title: "HTTP",
|
||||
content: <HttpApiExample />,
|
||||
icon: <TbHttpGet className="mr-2 size-5 opacity-80" />,
|
||||
},
|
||||
];
|
||||
|
||||
const [currentIndex, setCurrentIndex] = useState(0);
|
||||
const currentFeature = FEATURES[currentIndex];
|
||||
|
||||
return (
|
||||
<Tabs value={currentFeature?.id} className="w-full">
|
||||
<TabsList className="mb-4 w-full 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"
|
||||
>
|
||||
{f.icon}
|
||||
{f.title}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
{FEATURES.map((f) => (
|
||||
<TabsContent key={`${f.id}-content`} value={f.id}>
|
||||
{f.content}
|
||||
</TabsContent>
|
||||
))}
|
||||
</Tabs>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,147 @@
|
||||
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 const HttpApiExample = () => {
|
||||
const posts = [
|
||||
{
|
||||
title: "Blogging Tips for 2025",
|
||||
published_at: "2025-07-31",
|
||||
},
|
||||
{
|
||||
title: "Grow your business with SEO",
|
||||
published_at: "2025-08-15",
|
||||
},
|
||||
{
|
||||
title: "The Future of Web Design",
|
||||
published_at: "2025-09-01",
|
||||
},
|
||||
] as const;
|
||||
|
||||
const STEPS = [
|
||||
{
|
||||
id: "fetch-posts",
|
||||
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: (
|
||||
<div>
|
||||
<h1 className="py-8 text-center text-xl font-medium">
|
||||
My Startup Blog
|
||||
</h1>
|
||||
<div className="grid grid-cols-2 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>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "fetch-post",
|
||||
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: (
|
||||
<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>
|
||||
<p>Published on: {posts[0].published_at}</p>
|
||||
<hr />
|
||||
<p className="mt-4 font-mono">
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed
|
||||
ullamcorper, nisl at venenatis facilibus, erat felis aliquet enim,
|
||||
nec luctus ligula leo et quam.
|
||||
</p>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "fetch-filtered",
|
||||
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: (
|
||||
<div>
|
||||
<h1 className="py-8 text-center text-xl font-medium">
|
||||
News Category
|
||||
</h1>
|
||||
<div className="space-y-4">
|
||||
{posts.slice(0, 2).map((post) => (
|
||||
<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>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,146 @@
|
||||
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 const TsApiExample = () => {
|
||||
const [activeStep, setActiveStep] = useState(0);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const posts = [
|
||||
{
|
||||
title: "Blogging Tips for 2025",
|
||||
published_at: "2025-07-31",
|
||||
},
|
||||
{
|
||||
title: "Grow your business with SEO",
|
||||
published_at: "2025-08-15",
|
||||
},
|
||||
{
|
||||
title: "The Future of Web Design",
|
||||
published_at: "2025-09-01",
|
||||
},
|
||||
] as const;
|
||||
|
||||
const STEPS = [
|
||||
{
|
||||
id: "init",
|
||||
comment: "1. initialize the zenblog client",
|
||||
code: `import { createZenblogClient } from "zenblog";
|
||||
|
||||
const zenblog = createZenblogClient({ blogId: BLOG_ID });`,
|
||||
browser: (
|
||||
<div>
|
||||
<div className="flex h-60 w-full flex-col items-center justify-center gap-4 text-center">
|
||||
<CheckCircle2 className="text-emerald-500" />
|
||||
<span className="font-mono text-sm">
|
||||
Zenblog client initialized successfully!
|
||||
</span>
|
||||
<button
|
||||
onClick={() => {
|
||||
setActiveStep(1);
|
||||
}}
|
||||
className="rounded-full bg-black px-3 py-1.5 font-medium text-white"
|
||||
>
|
||||
Next step
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "getposts",
|
||||
comment: "2. fetch posts to build blog homepage",
|
||||
code: `const posts = await zenblog.posts.list();`,
|
||||
browser: (
|
||||
<div>
|
||||
<h1 className="py-8 text-center text-xl font-medium">
|
||||
My Startup Blog
|
||||
</h1>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{posts.map((post) => (
|
||||
<div key={post.title}>
|
||||
<div className="h-24 rounded-lg bg-gradient-to-br from-orange-500 to-orange-600"></div>
|
||||
<h3 className="my-4 text-xl font-medium">{post.title}</h3>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "getpost",
|
||||
comment: "3. fetch a single post with its content",
|
||||
code: `const post = await zenblog.posts.get({ slug: params.slug });`,
|
||||
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>
|
||||
<p>Published on: {posts[0].published_at}</p>
|
||||
<hr />
|
||||
<p className="mt-4 font-mono">
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed
|
||||
ullamcorper, nisl at venenatis facilisis, erat felis aliquet enim,
|
||||
nec luctus ligula leo et quam.
|
||||
</p>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Prism as ReactSyntaxHighlighter } from "react-syntax-highlighter";
|
||||
import { dracula } from "react-syntax-highlighter/dist/esm/styles/prism";
|
||||
|
||||
export const SyntaxHighlight = ({
|
||||
code,
|
||||
language,
|
||||
showLineNumbers = false,
|
||||
}: {
|
||||
code: string;
|
||||
language: string;
|
||||
showLineNumbers?: boolean;
|
||||
}) => {
|
||||
return (
|
||||
<ReactSyntaxHighlighter
|
||||
language={language}
|
||||
style={dracula}
|
||||
showLineNumbers={showLineNumbers}
|
||||
customStyle={{
|
||||
backgroundColor: "transparent",
|
||||
padding: 0,
|
||||
}}
|
||||
>
|
||||
{code}
|
||||
</ReactSyntaxHighlighter>
|
||||
);
|
||||
};
|
||||
+69
-149
@@ -5,8 +5,10 @@ import {
|
||||
FaCheckCircle,
|
||||
FaCode,
|
||||
FaCopy,
|
||||
FaHandPeace,
|
||||
FaImage,
|
||||
FaNetworkWired,
|
||||
FaPencilAlt,
|
||||
FaPencilRuler,
|
||||
FaPenFancy,
|
||||
FaRocket,
|
||||
@@ -32,6 +34,8 @@ import { motion } from "framer-motion";
|
||||
import { toast } from "sonner";
|
||||
import { Marquee } from "@/components/magicui/marquee";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ArrowRight } from "lucide-react";
|
||||
import { HeroImages } from "@/components/Homepage/hero-images";
|
||||
|
||||
const h1Font = Lora({
|
||||
subsets: ["latin"],
|
||||
@@ -40,53 +44,40 @@ const h1Font = Lora({
|
||||
|
||||
const FEATURES = [
|
||||
{
|
||||
icon: <FaRocket />,
|
||||
title: "Get started in minutes",
|
||||
Icon: (props: any) => <FaRocket {...props} />,
|
||||
title: "Speed & Simplicity",
|
||||
description:
|
||||
"No need to configure models, create a blog, write a post, publish.",
|
||||
},
|
||||
// {
|
||||
// icon: <FaPencilAlt />,
|
||||
// title: "More than blogs",
|
||||
// description:
|
||||
// "Manage your website content easily. Blog, job postings, changelog, docs, help center, etc.",
|
||||
// },
|
||||
{
|
||||
icon: <FaNetworkWired />,
|
||||
title: "Easy to integrate",
|
||||
description:
|
||||
"Use our HTTP API to fetch your content and display it on your website however you want. Works with any stack.",
|
||||
"No need to configure models, create a blog, write a post, publish. Get up and running in minutes.",
|
||||
},
|
||||
{
|
||||
icon: <FaCode />,
|
||||
title: "Type safety",
|
||||
description:
|
||||
"Use our typesafe API client to fetch your content. No more parsing issues.",
|
||||
Icon: (props: any) => <FaCode {...props} />,
|
||||
title: "Developer‑First",
|
||||
description: "Type‑safe SDK, REST API, framework-friendly",
|
||||
},
|
||||
{
|
||||
icon: <FaSmile />,
|
||||
Icon: (props: any) => <FaImage {...props} />,
|
||||
title: "Image & Video Hosting",
|
||||
description:
|
||||
"We host your media for you. No need to upload them to another service. Forget about S3 or CDNs.",
|
||||
},
|
||||
{
|
||||
Icon: (props: any) => <FaPencilAlt {...props} />,
|
||||
title: "Writer‑Friendly",
|
||||
description:
|
||||
"Inspired by tools like Notion, we made an editor that is both powerful and simple to use.",
|
||||
},
|
||||
{
|
||||
Icon: (props: any) => <FaHandPeace {...props} />,
|
||||
title: "Fully featured",
|
||||
description:
|
||||
"You can manage tags, categories, authors, multiple blogs, images and videos without configuring anything!",
|
||||
},
|
||||
{
|
||||
Icon: (props: any) => <FaSmile {...props} />,
|
||||
title: "It's not Wordpress",
|
||||
description:
|
||||
"Forget about wordpress headaches. Just focus on writing great content.",
|
||||
},
|
||||
{
|
||||
icon: <FaImage />,
|
||||
title: "Image hosting",
|
||||
description:
|
||||
"We host your images for you. No need to upload them to another service.",
|
||||
},
|
||||
{
|
||||
icon: <FaPencilRuler />,
|
||||
title: "Simple editor",
|
||||
description:
|
||||
"Inspired by tools like Notion, we made an editor that is both powerful and simple to use.",
|
||||
},
|
||||
// {
|
||||
// icon: <FaPalette />,
|
||||
// title: "Themes!",
|
||||
// description:
|
||||
// "We offer free themes for the most popular frameworks. Or you can build your own.",
|
||||
// },
|
||||
];
|
||||
|
||||
const Home = () => {
|
||||
@@ -140,34 +131,30 @@ const Home = () => {
|
||||
<div className={`${h1Font.variable}`}>
|
||||
<div className="flex flex-col">
|
||||
<Navigation />
|
||||
<motion.main
|
||||
className="mt-12 px-6 pb-24"
|
||||
initial={{ opacity: 0, y: 10, filter: "blur(10px)" }}
|
||||
animate={{ opacity: 1, y: 0, filter: "blur(0px)" }}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
<div className="mx-auto mt-20 max-w-5xl px-6 text-center md:mt-12">
|
||||
<main className="mt-12 px-6 pb-24">
|
||||
<div className="mx-auto mt-8 max-w-5xl px-6 text-center">
|
||||
<h1
|
||||
className={`mt-4 text-balance text-4xl font-semibold tracking-tight text-slate-800 md:text-5xl`}
|
||||
className={`mt-4 text-balance text-4xl font-semibold tracking-tight text-slate-800 md:text-6xl`}
|
||||
>
|
||||
Notion-like editor with <br />a simple headless API
|
||||
Launch your blog in minutes
|
||||
</h1>
|
||||
|
||||
<div className="mt-4 text-slate-500">
|
||||
<p className="text-balance text-lg">
|
||||
A simple blogging CMS for devs and marketers
|
||||
<p className="mx-auto max-w-xl text-balance text-lg font-medium">
|
||||
A headless CMS with Notion-style editor, hosted image and
|
||||
video uploads, and a type-safe API.
|
||||
</p>
|
||||
<Link className="mt-6 inline-flex" href="/sign-up">
|
||||
<Button
|
||||
size="default"
|
||||
className="h-10 rounded-xl font-medium"
|
||||
className="h-12 rounded-xl text-lg font-medium"
|
||||
>
|
||||
Start blogging for free
|
||||
Start blogging for free <ArrowRight className="ml-0.5" />
|
||||
</Button>
|
||||
</Link>
|
||||
<div>
|
||||
<code
|
||||
className="mt-4 inline-block cursor-copy rounded-lg px-3 py-1.5 text-sm text-zinc-600 transition-all hover:bg-zinc-100"
|
||||
className="mt-4 inline-block cursor-copy rounded-lg px-3 py-1.5 text-sm text-slate-600 transition-all hover:bg-slate-100"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText("npm i zenblog");
|
||||
toast.success("Copied to clipboard");
|
||||
@@ -178,97 +165,30 @@ const Home = () => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx-auto mt-24 max-w-6xl rounded-xl shadow-xl">
|
||||
<Image
|
||||
className="aspect-square w-full rounded-lg border border-zinc-200 object-cover shadow-sm transition-all md:aspect-video"
|
||||
src="/static/zenblog-ui.webp"
|
||||
loading="lazy"
|
||||
blurDataURL="/static/zenblog-ui.webp"
|
||||
placeholder="blur"
|
||||
width={1200}
|
||||
height={600}
|
||||
alt="The zenblog editor UI"
|
||||
/>
|
||||
|
||||
<div className="mx-auto mt-12 flex max-w-7xl items-center justify-center">
|
||||
<HeroImages />
|
||||
</div>
|
||||
|
||||
<div className="relative mx-auto mt-12 max-w-5xl">
|
||||
<Marquee pauseOnHover className="[--duration:30s]">
|
||||
{tweets.map((tweet) => (
|
||||
<TweetItem key={tweet.username} {...tweet} />
|
||||
))}
|
||||
</Marquee>
|
||||
<div className="from-background pointer-events-none absolute inset-y-0 left-0 w-1/4 bg-gradient-to-r"></div>
|
||||
<div className="from-background pointer-events-none absolute inset-y-0 right-0 w-1/4 bg-gradient-to-l"></div>
|
||||
</div>
|
||||
|
||||
<div className="mx-auto mt-24 max-w-5xl rounded-xl bg-slate-950 p-2 font-mono text-white">
|
||||
<div className="mb-3 p-2 px-4">
|
||||
<FaCode className="text-2xl text-emerald-400" />
|
||||
<h2 className="mt-2 text-xl font-medium">Developer friendly</h2>
|
||||
<p className="mt-2 text-balance font-sans text-sm text-slate-300">
|
||||
Zenblog is very easy to integrate into any stack by using our
|
||||
HTTP API or our typesafe, typescript API client.
|
||||
</p>
|
||||
<Link
|
||||
className="mt-2 flex items-center gap-2 text-emerald-400 underline"
|
||||
href="/docs"
|
||||
>
|
||||
Go to docs
|
||||
</Link>
|
||||
</div>
|
||||
<CodeBlockComponent
|
||||
filename="blog.tsx"
|
||||
language="typescript"
|
||||
highlightedLines={[]}
|
||||
>
|
||||
{`import { createZenblogClient } from "zenblog";
|
||||
|
||||
const client = createZenblogClient({ blogId: "your-blog-id" });
|
||||
|
||||
const posts = await client.posts.list();
|
||||
const post = await client.posts.get({ slug: "hello-world" });`}
|
||||
</CodeBlockComponent>
|
||||
</div>
|
||||
|
||||
<div className="mx-auto mt-24 max-w-5xl rounded-xl border bg-white/80 p-2 backdrop-blur-lg">
|
||||
<div className="p-2 px-4">
|
||||
<FaPenFancy className="text-2xl text-orange-400" />
|
||||
<h2 className="mt-2 text-2xl font-medium">
|
||||
<span className="font-serif italic">Wonderful,</span> simple,
|
||||
writing experience
|
||||
</h2>
|
||||
<p className="mt-2 text-balance font-sans text-sm text-slate-500">
|
||||
Our editor is designed to be both powerful and simple to use.
|
||||
You can focus on writing without worrying about the rest.
|
||||
</p>
|
||||
<Link
|
||||
className="mt-2 flex items-center gap-2 text-orange-600 underline"
|
||||
href="/sign-up"
|
||||
>
|
||||
Try it for free
|
||||
</Link>
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<Image
|
||||
className="w-full rounded-lg border shadow-sm"
|
||||
src="/static/editor-screenshot.webp"
|
||||
width={1200}
|
||||
height={400}
|
||||
alt="The zenblog editor UI"
|
||||
/>
|
||||
</div>
|
||||
<div className="mx-auto mt-12 flex max-w-6xl flex-wrap justify-center gap-6">
|
||||
{tweets.map((tweet) => (
|
||||
<TweetItem key={tweet.username} {...tweet} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mx-auto mt-24 max-w-4xl">
|
||||
<h2 className="text-2xl font-medium">Features</h2>
|
||||
<div className="mt-8 grid grid-cols-1 gap-8 sm:grid-cols-2">
|
||||
<h2 className="text-center text-2xl font-medium">Why Zenblog?</h2>
|
||||
<div className="mx-auto mt-8 divide-y rounded-xl border bg-slate-100/40">
|
||||
{FEATURES.map((feature) => (
|
||||
<div key={feature.title} className="flex flex-col gap-2">
|
||||
<div className="flex items-center gap-2 text-slate-400">
|
||||
{feature.icon && feature.icon}
|
||||
<div
|
||||
key={feature.title}
|
||||
className="border-1 group flex flex-col gap-2 overflow-hidden p-6"
|
||||
>
|
||||
<div className="">
|
||||
<feature.Icon className="size-6 text-slate-400/70 transition-all group-hover:scale-105 group-hover:text-orange-500" />
|
||||
</div>
|
||||
<h3 className="text-lg font-medium">{feature.title}</h3>
|
||||
<p className="max-w-xs text-sm text-zinc-500">
|
||||
<h3 className="text-xl font-medium">{feature.title}</h3>
|
||||
<p className="text-xl text-slate-500">
|
||||
{feature.description}
|
||||
</p>
|
||||
</div>
|
||||
@@ -297,7 +217,10 @@ const post = await client.posts.get({ slug: "hello-world" });`}
|
||||
// image: "/static/zenblog-astro-template.webp",
|
||||
// },
|
||||
].map((fw) => (
|
||||
<article key={fw.id} className="grid md:grid-cols-2">
|
||||
<article
|
||||
key={fw.id}
|
||||
className="grid rounded-xl border border-slate-200 bg-slate-100/50 p-2 md:grid-cols-2"
|
||||
>
|
||||
<div>
|
||||
<Image
|
||||
className="w-full rounded-lg border shadow-sm"
|
||||
@@ -309,10 +232,10 @@ const post = await client.posts.get({ slug: "hello-world" });`}
|
||||
</div>
|
||||
<div className="p-4 text-left">
|
||||
<h3 className="text-lg font-medium">{fw.name}</h3>
|
||||
<p className="text-balance text-sm text-zinc-500">
|
||||
<p className="text-balance text-lg text-slate-400">
|
||||
{fw.desc}
|
||||
</p>
|
||||
<div className="mt-4 flex flex-wrap gap-4 text-orange-600">
|
||||
<div className="mt-4 grid gap-4 text-orange-600">
|
||||
<Link
|
||||
href={fw.link}
|
||||
target="_blank"
|
||||
@@ -323,7 +246,7 @@ const post = await client.posts.get({ slug: "hello-world" });`}
|
||||
<Link
|
||||
href={fw.demo}
|
||||
target="_blank"
|
||||
className="mt-4 flex items-center gap-2 underline"
|
||||
className="mt-1 flex items-center gap-2 underline"
|
||||
>
|
||||
Try the demo
|
||||
</Link>
|
||||
@@ -387,7 +310,7 @@ const post = await client.posts.get({ slug: "hello-world" });`}
|
||||
</Accordion>
|
||||
</div>
|
||||
</section>
|
||||
</motion.main>
|
||||
</main>
|
||||
</div>
|
||||
<Footer />
|
||||
</div>
|
||||
@@ -428,7 +351,7 @@ function PricingItem({
|
||||
return (
|
||||
<div className="flex w-full max-w-lg flex-1 flex-col rounded-lg border px-4 py-3 pb-8 text-left shadow-sm">
|
||||
<h2 className="text-lg font-medium">{title}</h2>
|
||||
<p className="text-sm text-zinc-500">{description}</p>
|
||||
<p className="text-sm text-slate-500">{description}</p>
|
||||
<ul className="mt-6 space-y-3 text-left">
|
||||
{formattedFeatures.map((feature, idx) => (
|
||||
<li
|
||||
@@ -445,9 +368,9 @@ function PricingItem({
|
||||
<div>
|
||||
<p className="mt-8 font-mono text-2xl font-medium">
|
||||
${price}
|
||||
<span className="text-sm text-zinc-500">/month</span>
|
||||
<span className="text-sm text-slate-500">/month</span>
|
||||
</p>
|
||||
<p className="text-xs font-medium text-zinc-500">
|
||||
<p className="text-xs font-medium text-slate-500">
|
||||
${String(+price * 12)} Billed annually
|
||||
</p>
|
||||
</div>
|
||||
@@ -517,10 +440,7 @@ const TweetItem = ({
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"relative flex w-96 flex-col gap-2 rounded-xl p-4 hover:bg-slate-100/70",
|
||||
{
|
||||
"min-h-32": !isThread,
|
||||
}
|
||||
"relative flex w-80 flex-col gap-2 rounded-xl border bg-slate-100/50 p-4"
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -536,7 +456,7 @@ const TweetItem = ({
|
||||
/>
|
||||
<div className={cn("flex flex-col")}>
|
||||
<p className="text-sm font-medium">{name}</p>
|
||||
<p className="text-xs font-medium text-zinc-500">{username}</p>
|
||||
<p className="text-xs font-medium text-slate-500">{username}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
|
||||
@@ -18,7 +18,7 @@ module.exports = {
|
||||
border: {
|
||||
"1": "1px",
|
||||
"2": "2px",
|
||||
DEFAULT: "0.3px solid var(--border-color)",
|
||||
DEFAULT: "1px solid var(--border-color)",
|
||||
},
|
||||
container: {
|
||||
center: true,
|
||||
|
||||
Reference in New Issue
Block a user