Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,7 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts

# content-collections generated files
.content-collections/
lib/.content-collections/
114 changes: 114 additions & 0 deletions app/(marketing)/articles/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import Image from "next/image"
import Link from "next/link"
import { notFound } from "next/navigation"
import { allArticles } from "@/content-collections"
import { MDXContent } from "@content-collections/mdx/react"

import { DateFormatter } from "@/lib/date-utils"

import { Icons } from "@/components/shared/icons"
import { buttonVariants } from "@/components/ui/button"
import { Link as CustomLink } from "@/components/ui/link"

interface ArticlePageProps {
params: Promise<{ slug: string }>
}

export async function generateStaticParams() {
return allArticles.map((article) => ({
slug: article.href.split("/").pop(),
}))
}

export async function generateMetadata({ params }: ArticlePageProps) {
const { slug } = await params
const article = allArticles.find(
(article) => article.href === `/articles/${slug}`
)

if (!article) {
notFound()
}

return {
title: `${article.title}`,
description: article.description,
openGraph: {
title: article.title,
description: article.description,
images: article.image ? [article.image] : [],
},
}
}

export default async function ArticlePage({ params }: ArticlePageProps) {
const { slug } = await params

const article = allArticles.find(
(article) => article.href === `/articles/${slug}`
)

if (!article) {
notFound()
}

const { image, title, description, publishedAt, html } = article

return (
<div className="mx-auto w-full max-w-4xl px-4 py-16 sm:px-6 md:py-24">
<div className="relative">
<div className="mb-8 flex items-center justify-between">
<Link
href="/articles"
className={buttonVariants({
variant: "ghost",
size: "sm",
})}
>
<Icons.chevronLeft className="mr-1 size-4" />
Back to Articles
</Link>

<div className="text-muted-foreground text-sm">
{DateFormatter.formatShortDate(publishedAt)}
</div>
</div>

<header className="mb-12">
<h1 className="text-foreground mb-6 text-4xl font-bold tracking-tight sm:text-5xl">
{title}
</h1>

<p className="text-muted-foreground mb-6 text-xl leading-relaxed">
{description}
</p>
</header>

{image && (
<div className="mb-12">
<div className="bg-muted relative aspect-video w-full overflow-hidden rounded-xl border">
<Image
src={image}
alt={`${title} cover image`}
className="object-cover"
fill
priority
/>
Comment on lines +90 to +96
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Image alt text and responsive sizes.

  • Alt text includes the word “image,” which violates the repo guideline. Use a meaningful alt without “image/picture/photo.”
  • With fill, provide a sizes attribute for correct responsive behavior. As per coding guidelines.
 <Image
   src={image}
-  alt={`${title} cover image`}
+  alt={`${title} cover`}
   className="object-cover"
   fill
+  sizes="(max-width: 640px) 100vw, (max-width: 1024px) 90vw, 896px"
   priority
 />
🤖 Prompt for AI Agents
In app/(marketing)/articles/[slug]/page.tsx around lines 91 to 97, the Image
component uses an alt that contains the word "image" and is missing a sizes prop
while using fill; update the alt to a concise descriptive phrase without the
words "image", "photo", or "picture" (for example use `${title} cover` or
another meaningful description) and add a responsive sizes attribute appropriate
for the layout (e.g. a mobile-first sizes string such as "(max-width: 640px)
100vw, 640px" or whatever fits the design breakpoints) so the Image has both an
accessible, guideline-compliant alt and correct responsive sizing when using
fill.

</div>
</div>
)}

{html && (
<article className="prose prose-lg prose-gray dark:prose-invert max-w-none">
<MDXContent
code={html}
components={{
a: CustomLink,
}}
/>
</article>
)}
</div>
</div>
)
}
35 changes: 35 additions & 0 deletions app/(marketing)/articles/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import type { Metadata } from "next"

import { siteConfig } from "@/config/site"

import { MarketingArticlesList } from "@/components/app/marketing-articles-list"
import { MarketingSubscription } from "@/components/app/marketing-subscription"

export const metadata: Metadata = {
title: "Articles",
description:
"Stay updated with the latest security insights, tips, and features from the Zero Locker team",
openGraph: {
title: `Articles | ${siteConfig.name}`,
description:
"Stay updated with the latest security insights, tips, and features from the Zero Locker team",
url: `${siteConfig.url}/articles`,
},
}

export default function ArticlesPage() {
return (
<div className="mx-auto w-full max-w-3xl px-4 py-16 sm:px-6 md:max-w-4xl md:py-14 lg:max-w-5xl">
<h1 className="mb-8 text-3xl font-bold tracking-tight">The Articles</h1>

<MarketingArticlesList />

<div className="pt-12">
<MarketingSubscription
type="articles"
description="Stay updated on our latest articles and insights"
/>
</div>
</div>
)
}
7 changes: 5 additions & 2 deletions app/(marketing)/roadmap/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type { RoadmapItem } from "@/types"
import { siteConfig } from "@/config/site"

import { MarketingRoadmapList } from "@/components/app/marketing-roadmap-list"
import { MarketingRoadmapSubscription } from "@/components/app/marketing-roadmap-subscription"
import { MarketingSubscription } from "@/components/app/marketing-subscription"

export const metadata: Metadata = {
title: "Roadmap",
Expand Down Expand Up @@ -64,7 +64,10 @@ export default function RoadmapPage() {
<MarketingRoadmapList items={roadmapItems} />

<div className="pt-12">
<MarketingRoadmapSubscription />
<MarketingSubscription
type="roadmap"
description="Stay updated on our progress and upcoming features"
/>
</div>
</div>
)
Expand Down
69 changes: 47 additions & 22 deletions components/app/email-roadmap-subscription.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,24 @@ import { siteConfig } from "@/config/site"

import { EmailFooter } from "@/components/shared/email-footer"

interface EmailRoadmapSubscriptionProps {
interface EmailSubscriptionProps {
email: string
type: "roadmap" | "articles"
}

export function EmailRoadmapSubscription({
email,
}: EmailRoadmapSubscriptionProps) {
export function EmailSubscription({ email, type }: EmailSubscriptionProps) {
const isRoadmap = type === "roadmap"
const title = isRoadmap ? "Roadmap" : "Articles"
const linkUrl = isRoadmap ? "/roadmap" : "/articles"
const linkText = isRoadmap ? "View Roadmap" : "Read Articles"

return (
<Html lang="en" dir="ltr">
<Tailwind>
<Head />
<Preview>
You&apos;re now subscribed to {siteConfig.name} updates!
You&apos;re now subscribed to {siteConfig.name} {title.toLowerCase()}{" "}
updates!
</Preview>
<Body className="bg-white py-[24px] font-sans">
<Container className="mx-auto max-w-[600px] bg-white">
Expand Down Expand Up @@ -67,26 +72,45 @@ export function EmailRoadmapSubscription({
<Text className="mb-[12px] text-[14px] font-medium text-gray-900">
What to expect:
</Text>
<Text className="mb-[4px] text-[13px] leading-relaxed text-gray-600">
• Product milestones and feature releases
</Text>
<Text className="mb-[4px] text-[13px] leading-relaxed text-gray-600">
• Roadmap updates and insights
</Text>
<Text className="mb-[4px] text-[13px] leading-relaxed text-gray-600">
• Early access opportunities
</Text>
<Text className="text-[13px] leading-relaxed text-gray-600">
• Tips and best practices
</Text>
{isRoadmap ? (
<>
<Text className="mb-[4px] text-[13px] leading-relaxed text-gray-600">
• Product milestones and feature releases
</Text>
<Text className="mb-[4px] text-[13px] leading-relaxed text-gray-600">
• Roadmap updates and insights
</Text>
<Text className="mb-[4px] text-[13px] leading-relaxed text-gray-600">
• Early access opportunities
</Text>
<Text className="text-[13px] leading-relaxed text-gray-600">
• Tips and best practices
</Text>
</>
) : (
<>
<Text className="mb-[4px] text-[13px] leading-relaxed text-gray-600">
• Technical insights and challenges
</Text>
<Text className="mb-[4px] text-[13px] leading-relaxed text-gray-600">
• Business updates and milestones
</Text>
<Text className="mb-[4px] text-[13px] leading-relaxed text-gray-600">
• Feature announcements
</Text>
<Text className="text-[13px] leading-relaxed text-gray-600">
• Behind-the-scenes content
</Text>
</>
)}
</Section>

<Section className="mb-[24px] text-center">
<Link
href={`${siteConfig.url}/roadmap`}
href={`${siteConfig.url}${linkUrl}`}
className="inline-block rounded-[4px] bg-orange-600 px-[24px] py-[10px] text-[14px] font-medium text-white no-underline"
>
View Roadmap
{linkText}
</Link>
</Section>

Expand All @@ -105,8 +129,9 @@ export function EmailRoadmapSubscription({
)
}

EmailRoadmapSubscription.PreviewProps = {
EmailSubscription.PreviewProps = {
email: "hi@findmalek.com",
} as EmailRoadmapSubscriptionProps
type: "roadmap",
} as EmailSubscriptionProps

export default EmailRoadmapSubscription
export default EmailSubscription
86 changes: 86 additions & 0 deletions components/app/marketing-articles-list.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
"use client"

import { useEffect, useRef, useState } from "react"
import Link from "next/link"
import { allArticles } from "@/content-collections"

import { DateFormatter } from "@/lib/date-utils"

import { Icons } from "@/components/shared/icons"

export function MarketingArticlesList() {
const [showScrollIndicator, setShowScrollIndicator] = useState(true)
const scrollContainerRef = useRef<HTMLDivElement>(null)

useEffect(() => {
const scrollContainer = scrollContainerRef.current
if (!scrollContainer) return

const handleScroll = () => {
const { scrollTop, scrollHeight, clientHeight } = scrollContainer
const isAtBottom = scrollHeight - scrollTop - clientHeight < 10
setShowScrollIndicator(!isAtBottom)
}

// Check initial state
handleScroll()

scrollContainer.addEventListener("scroll", handleScroll)
return () => scrollContainer.removeEventListener("scroll", handleScroll)
}, [])

return (
<div className="relative">
<div
ref={scrollContainerRef}
className="border-border bg-card/50 h-[500px] space-y-8 overflow-y-auto rounded-lg border p-6 pr-4"
>
{allArticles.map((article, index) => (
<Link
key={`${article.title}-${index}`}
href={article.href}
className="hover:bg-muted/50 group flex gap-4 rounded-lg p-4 transition-all"
>
<div className="flex-shrink-0 pt-1.5">
<div className="bg-primary size-2 rounded-full" />
</div>
<div className="flex-1">
<div className="mb-2">
<h2 className="group-hover:text-primary mb-1 text-lg font-medium transition-colors">
{article.title}
</h2>
<p className="text-muted-foreground mb-2 text-sm">
{article.description}
</p>
<div className="text-muted-foreground flex items-center gap-4 text-xs">
<span>
{DateFormatter.formatShortDate(article.publishedAt)}
</span>
</div>
</div>
</div>
<div className="flex-shrink-0 pt-1.5">
<Icons.arrowRight className="text-muted-foreground size-4 opacity-0 transition-all group-hover:translate-x-1 group-hover:opacity-100" />
</div>
</Link>
))}
</div>

{/* Bottom blur gradient */}
<div
className={`from-background via-background/50 pointer-events-none absolute bottom-0 left-0 right-0 h-32 rounded-b-lg bg-gradient-to-t to-transparent transition-opacity duration-500 ${
showScrollIndicator ? "opacity-100" : "opacity-0"
}`}
/>

{/* Bouncing scroll indicator */}
<div
className={`pointer-events-none absolute bottom-4 left-1/2 -translate-x-1/2 animate-bounce transition-opacity duration-500 ${
showScrollIndicator ? "opacity-100" : "opacity-0"
}`}
>
<Icons.down className="text-muted-foreground size-4" />
</div>
</div>
)
}
6 changes: 6 additions & 0 deletions components/app/marketing-header-desktop.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ export function MarketingHeaderDesktop() {
</Link>
{!isMobile && (
<nav className="flex items-center gap-6">
<Link
href="/articles"
className="text-muted-foreground hover:text-foreground text-sm font-medium transition-colors"
>
Articles
</Link>
<Link
href="/roadmap"
className="text-muted-foreground hover:text-foreground text-sm font-medium transition-colors"
Expand Down
Loading