This commit is contained in:
Jordi Enric
2025-02-26 21:15:06 +01:00
parent 6136e7613e
commit 4d1893baaf
19 changed files with 1177 additions and 243 deletions
+8
View File
@@ -0,0 +1,8 @@
---
description:
globs:
---
# Project context
- You're building Zenblog, a headless blogging CMS.
+63
View File
@@ -64,3 +64,66 @@ pre .copy-button {
cursor: pointer;
border-radius: 0 0 0 0.5rem;
}
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 222.2 84% 4.9%;
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
--radius: 0.5rem;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 212.7 26.8% 83.9%;
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}
+13 -8
View File
@@ -1,16 +1,21 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
"rsc": false,
"style": "new-york",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "tailwind.config.js",
"css": "src/styles/globals.css",
"config": "tailwind.config.ts",
"css": "app/globals.css",
"baseColor": "slate",
"cssVariables": false
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils"
}
}
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}
+3 -3
View File
@@ -5,7 +5,7 @@
"scripts": {
"typecheck": "tsc --noEmit",
"build": "next build",
"build:web": "next build",
"build:web": "next build --turbo",
"dev": "concurrently \"next dev -p 8082\" \"npm run stripe:webhook\"",
"stripe:webhook": "stripe listen --forward-to localhost:8082/api/webhooks/stripe",
"lint": "next lint",
@@ -79,8 +79,8 @@
"ai": "^3.1.30",
"bcrypt": "^5.1.1",
"browser-image-compression": "^2.0.2",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^0.2.1",
"date-fns": "^3.6.0",
"dotenv": "^16.4.4",
Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

@@ -0,0 +1,73 @@
import { cn } from "@/lib/utils";
import { ComponentPropsWithoutRef } from "react";
interface MarqueeProps extends ComponentPropsWithoutRef<"div"> {
/**
* Optional CSS class name to apply custom styles
*/
className?: string;
/**
* Whether to reverse the animation direction
* @default false
*/
reverse?: boolean;
/**
* Whether to pause the animation on hover
* @default false
*/
pauseOnHover?: boolean;
/**
* Content to be displayed in the marquee
*/
children: React.ReactNode;
/**
* Whether to animate vertically instead of horizontally
* @default false
*/
vertical?: boolean;
/**
* Number of times to repeat the content
* @default 4
*/
repeat?: number;
}
export function Marquee({
className,
reverse = false,
pauseOnHover = false,
children,
vertical = false,
repeat = 4,
...props
}: MarqueeProps) {
return (
<div
{...props}
className={cn(
"group flex overflow-hidden p-2 [--duration:40s] [--gap:1rem] [gap:var(--gap)]",
{
"flex-row": !vertical,
"flex-col": vertical,
},
className
)}
>
{Array(repeat)
.fill(0)
.map((_, i) => (
<div
key={i}
className={cn("flex shrink-0 justify-around [gap:var(--gap)]", {
"animate-marquee flex-row": !vertical,
"animate-marquee-vertical flex-col": vertical,
"group-hover:[animation-play-state:paused]": pauseOnHover,
"[animation-direction:reverse]": reverse,
})}
>
{children}
</div>
))}
</div>
);
}
+5 -24
View File
@@ -2,7 +2,7 @@ import { z } from "zod";
export const TRIAL_PERIOD_DAYS = 14;
export const PricingPlanId = z.enum(["hobby", "pro", "free"]);
export const PricingPlanId = z.enum(["pro", "free"]);
export type PricingPlanId = z.infer<typeof PricingPlanId>;
export const isPricingPlanId = (value: string): value is PricingPlanId =>
PricingPlanId.safeParse(value).success;
@@ -32,8 +32,7 @@ export type PricingPlan = {
export const MAX_BLOGS_PER_PLAN: Record<PricingPlanId, number> = {
free: 1,
hobby: 3,
pro: 100,
pro: 999,
};
export const PRICING_PLANS: PricingPlan[] = [
@@ -54,30 +53,12 @@ export const PRICING_PLANS: PricingPlan[] = [
"Email support",
],
},
{
id: "hobby",
highlight: false,
title: "Starter",
description: "Perfect for small teams",
monthlyPrice: 29,
yearlyPrice: 288, // will be divided by 12 in pricing page
features: [
`${MAX_BLOGS_PER_PLAN.hobby} blogs`,
"3 authors",
"Unlimited posts",
"Unlimited categories",
"Unlimited tags",
"Unlimited API requests",
"Unlimited media files",
"Email support",
],
},
{
id: "pro",
title: "Professional",
title: "Pro",
description: "For growing teams",
monthlyPrice: 59,
yearlyPrice: 576,
monthlyPrice: 20,
yearlyPrice: 200,
features: [
"Unlimited blogs",
"Unlimited authors",
+3 -16
View File
@@ -1,19 +1,6 @@
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
export function formatDate(
date: string | Date,
options?: Intl.DateTimeFormatOptions
) {
return new Date(date).toLocaleString(
undefined,
options ?? {
month: "long",
day: "numeric",
year: "numeric",
}
);
return twMerge(clsx(inputs))
}
+101
View File
@@ -30,6 +30,8 @@ import { CodeBlockComponent } from "@/components/code-block";
import { Lora } from "next/font/google";
import { motion } from "framer-motion";
import { toast } from "sonner";
import { Marquee } from "@/components/magicui/marquee";
import { cn } from "@/lib/utils";
const h1Font = Lora({
subsets: ["latin"],
@@ -189,6 +191,16 @@ const Home = () => {
/>
</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" />
@@ -451,4 +463,93 @@ function PricingItem({
);
}
const tweets = [
{
content:
"By the way, the CMS is @zenbloghq. A brilliant CMS that simply works. Integrating it with the website didn't take 5 minutes.",
name: "Narix Hine",
username: "leximory",
image: "/static/tweets/leximory.jpg",
},
{
content:
"Just tried @zenbloghq last night and i started a blog and connected to my website with some help from cursor.The site is coming together, time for a real domain I think https://jesselawrence.replit.app/blog",
name: "Jesse",
username: "lawrencejessej",
image: "/static/tweets/lawrencejessej.jpg",
},
{
content:
"I started using @zenbloghq yesterday. I literally got it running on my Astro site in 10 minutes. Really great product",
name: "Alvaro",
username: "metasurfero",
image: "/static/tweets/metasurfero.jpg",
},
{
content: "i use zenblog to manage the blog in zenblog",
name: "jordi",
username: "jordienr",
image: "/static/tweets/jordienr.jpg",
},
{
content: "zenblog gud",
name: "william",
username: "williamhzo",
image: "/static/tweets/williamhzo.jpg",
},
];
const TweetItem = ({
content,
name,
username,
image,
isThread = false,
isLast = false,
}: {
content: string;
name: string;
username: string;
image: string;
isThread?: boolean;
isLast?: boolean;
}) => {
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,
}
)}
>
<div className="flex items-center gap-2">
{isThread && !isLast ? (
<div className="absolute left-[36px] top-[70px] h-full w-px bg-slate-200"></div>
) : null}
<Image
src={image}
alt={name}
width={40}
height={40}
className="z-10 rounded-full border border-slate-100"
/>
<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>
</div>
</div>
<div className="flex flex-col">
<p
className={cn("text-sm", {
"ml-12": isThread,
})}
>
{content}
</p>
</div>
</div>
);
};
export default Home;
+2 -2
View File
@@ -48,7 +48,7 @@ export default function Pricing() {
Yearly (Save money!)
</Button>
</div>
<div className="mt-4 grid gap-4 sm:grid-cols-3">
<div className="mt-4 grid gap-4 sm:grid-cols-2">
{PRICING_PLANS.map((plan) => (
<PricingCard
key={plan.title}
@@ -146,7 +146,7 @@ export function PricingCard({
$
</span>
<span className="text-2xl font-medium text-slate-900">
{yearlyToMonth}
{yearlyToMonth.toFixed(2)}
</span>{" "}
per month <br />
billed ${yearlyPrice} yearly
+64
View File
@@ -2,6 +2,70 @@
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 222.2 84% 4.9%;
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
--radius: 0.5rem;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 212.7 26.8% 83.9%;
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}
* {
@apply antialiased;
}
+148 -69
View File
@@ -15,75 +15,154 @@ module.exports = {
"./src/**/*.{ts,tsx}",
],
theme: {
border: {
DEFAULT: "0.3px solid var(--border-color)",
1: "1px",
2: "2px",
},
container: {
center: true,
padding: "2rem",
screens: {
"2xl": "1400px",
},
},
extend: {
fontFamily: {
default: [
"var(--font-sans)",
"ui-sans-serif",
"system-ui",
"sans-serif",
"Apple Color Emoji",
"Segoe UI Emoji",
"Segoe UI Symbol",
"Noto Color Emoji",
],
mono: [
"var(--font-mono)",
"ui-monospace",
"SFMono-Regular",
"Menlo",
"Monaco",
"Consolas",
"Liberation Mono",
"Courier New",
"monospace",
],
},
textShadow: {
sm: "0 1px 2px var(--tw-shadow-color)",
DEFAULT: "0 2px 4px var(--tw-shadow-color)",
lg: "0 8px 16px var(--tw-shadow-color)",
},
highlight: {
DEFAULT: "shadow",
},
borderColor: {
DEFAULT: "#e4e4e7",
},
colors: {
slate: {
50: "#f9fafb",
150: "#f4f5f7",
},
},
keyframes: {
"accordion-down": {
from: { height: 0 },
to: { height: "var(--radix-accordion-content-height)" },
},
"accordion-up": {
from: { height: "var(--radix-accordion-content-height)" },
to: { height: 0 },
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
},
},
border: {
'1': '1px',
'2': '2px',
DEFAULT: '0.3px solid var(--border-color)'
},
container: {
center: true,
padding: '2rem',
screens: {
'2xl': '1400px'
}
},
extend: {
fontFamily: {
default: [
'var(--font-sans)',
'ui-sans-serif',
'system-ui',
'sans-serif',
'Apple Color Emoji',
'Segoe UI Emoji',
'Segoe UI Symbol',
'Noto Color Emoji'
],
mono: [
'var(--font-mono)',
'ui-monospace',
'SFMono-Regular',
'Menlo',
'Monaco',
'Consolas',
'Liberation Mono',
'Courier New',
'monospace'
]
},
textShadow: {
sm: '0 1px 2px var(--tw-shadow-color)',
DEFAULT: '0 2px 4px var(--tw-shadow-color)',
lg: '0 8px 16px var(--tw-shadow-color)'
},
highlight: {
DEFAULT: 'shadow'
},
borderColor: {
DEFAULT: '#e4e4e7'
},
colors: {
slate: {
'50': '#f9fafb',
'150': '#f4f5f7'
},
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
card: {
DEFAULT: 'hsl(var(--card))',
foreground: 'hsl(var(--card-foreground))'
},
popover: {
DEFAULT: 'hsl(var(--popover))',
foreground: 'hsl(var(--popover-foreground))'
},
primary: {
DEFAULT: 'hsl(var(--primary))',
foreground: 'hsl(var(--primary-foreground))'
},
secondary: {
DEFAULT: 'hsl(var(--secondary))',
foreground: 'hsl(var(--secondary-foreground))'
},
muted: {
DEFAULT: 'hsl(var(--muted))',
foreground: 'hsl(var(--muted-foreground))'
},
accent: {
DEFAULT: 'hsl(var(--accent))',
foreground: 'hsl(var(--accent-foreground))'
},
destructive: {
DEFAULT: 'hsl(var(--destructive))',
foreground: 'hsl(var(--destructive-foreground))'
},
border: 'hsl(var(--border))',
input: 'hsl(var(--input))',
ring: 'hsl(var(--ring))',
chart: {
'1': 'hsl(var(--chart-1))',
'2': 'hsl(var(--chart-2))',
'3': 'hsl(var(--chart-3))',
'4': 'hsl(var(--chart-4))',
'5': 'hsl(var(--chart-5))'
}
},
keyframes: {
'infinite-scroll': {
from: {
transform: 'translateX(0)'
},
to: {
transform: 'translateX(-100%)'
}
},
'accordion-down': {
from: {
height: 0
},
to: {
height: 'var(--radix-accordion-content-height)'
}
},
'accordion-up': {
from: {
height: 'var(--radix-accordion-content-height)'
},
to: {
height: 0
}
},
marquee: {
from: {
transform: 'translateX(0)'
},
to: {
transform: 'translateX(calc(-100% - var(--gap)))'
}
},
'marquee-vertical': {
from: {
transform: 'translateY(0)'
},
to: {
transform: 'translateY(calc(-100% - var(--gap)))'
}
}
},
animation: {
'accordion-down': 'accordion-down 0.2s ease-out',
'accordion-up': 'accordion-up 0.2s ease-out',
'infinite-scroll': 'infinite-scroll 25s linear infinite',
marquee: 'marquee var(--duration) infinite linear',
'marquee-vertical': 'marquee-vertical var(--duration) linear infinite'
},
borderRadius: {
lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)',
sm: 'calc(var(--radius) - 4px)'
}
}
},
plugins: [
require("tailwindcss-animate"),
+694 -121
View File
File diff suppressed because it is too large Load Diff