-
Notifications
You must be signed in to change notification settings - Fork 0
Marketing Case Studies #37
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
db64178
feat: installed 'dependecies'
FindMalek 91f61ff
feat: init 'articles'
FindMalek 5c80174
perf: removed 'author'
FindMalek b509bad
chore: intro 'useSubscribeupdates'
FindMalek 383f168
perf: added 'link' hover
FindMalek 18c40b1
fix: resolved minor issues
FindMalek File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | ||
| /> | ||
| </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> | ||
| ) | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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> | ||
| ) | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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> | ||
| ) | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Image alt text and responsive sizes.
fill, provide asizesattribute 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