feat: Complete multi-blog setup with 12 collections and subdomain routing
- Added 12 blog collections: art, bfs, dfs, problems, puzzles, reviews, reflections, vibes, workflows, magic, contests, poetry - Created content config with glob loaders for all collections - Added sample posts for each collection - Fixed TypeScript collection recognition by removing duplicate config - Made BlogPosts component collection-aware for proper routing - Created individual post pages ([...slug].astro) for each collection - Added Netlify subdomain routing configuration - Updated all references to use neeldhara.blog as main domain - Fixed navigation links in all-blogs component Ready for GitHub/Netlify deployment with subdomain support.
This commit is contained in:
commit
b47b761f1c
190 changed files with 16583 additions and 0 deletions
107
src/components/BaseHead.astro
Normal file
107
src/components/BaseHead.astro
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
---
|
||||
// Import the global.css file here so that it is included on
|
||||
// all pages through the use of the <BaseHead /> component.
|
||||
import '../styles/global.css';
|
||||
import { SITE_TITLE, SITE_METADATA } from '../consts';
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
description: string;
|
||||
image?: string;
|
||||
}
|
||||
|
||||
const canonicalURL = new URL(Astro.url.pathname, Astro.site);
|
||||
|
||||
const { title, description, image } = Astro.props;
|
||||
const finalTitle = title || SITE_METADATA.title.default;
|
||||
const finalDescription = description || SITE_METADATA.description;
|
||||
const finalImage = image || SITE_METADATA.openGraph.images[0].url;
|
||||
const imageURL = new URL(finalImage, Astro.url);
|
||||
---
|
||||
|
||||
<!-- Global Metadata -->
|
||||
<meta charset="utf-8" />
|
||||
|
||||
<!-- Preconnect to Google Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
|
||||
<!-- Load Google Fonts -->
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500;600&family=Hubot+Sans:wght@400;500;600;700&family=Mona+Sans:wght@400;500;600;700&display=swap" rel="stylesheet" />
|
||||
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<meta name="robots" content={`${SITE_METADATA.robots.index ? 'index' : 'noindex'}, ${SITE_METADATA.robots.follow ? 'follow' : 'nofollow'}`} />
|
||||
<meta name="keywords" content={SITE_METADATA.keywords.join(', ')} />
|
||||
<meta name="author" content={SITE_METADATA.authors[0].name} />
|
||||
<meta name="creator" content={SITE_METADATA.creator} />
|
||||
<meta name="publisher" content={SITE_METADATA.publisher} />
|
||||
|
||||
<!-- Theme Script -->
|
||||
<script is:inline>
|
||||
// Get theme from localStorage or system preference
|
||||
const savedTheme = localStorage.getItem('theme');
|
||||
const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
||||
const initialTheme = savedTheme || systemTheme;
|
||||
|
||||
// Apply theme immediately to prevent flashing
|
||||
document.documentElement.classList.toggle('dark', initialTheme === 'dark');
|
||||
</script>
|
||||
|
||||
<!-- Favicon -->
|
||||
{SITE_METADATA.icons.icon.map((icon) => (
|
||||
<link
|
||||
rel="icon"
|
||||
type={icon.type}
|
||||
sizes={icon.sizes}
|
||||
href={icon.url}
|
||||
/>
|
||||
))}
|
||||
{SITE_METADATA.icons.apple.map((icon) => (
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes={icon.sizes}
|
||||
href={icon.url}
|
||||
/>
|
||||
))}
|
||||
{SITE_METADATA.icons.shortcut.map((icon) => (
|
||||
<link
|
||||
rel="shortcut icon"
|
||||
href={icon.url}
|
||||
/>
|
||||
))}
|
||||
|
||||
<link rel="sitemap" href="/sitemap-index.xml" />
|
||||
<link
|
||||
rel="alternate"
|
||||
type="application/rss+xml"
|
||||
title={SITE_TITLE}
|
||||
href={new URL('rss.xml', Astro.site)}
|
||||
/>
|
||||
<meta name="generator" content={Astro.generator} />
|
||||
|
||||
<!-- Canonical URL -->
|
||||
<link rel="canonical" href={canonicalURL} />
|
||||
|
||||
<!-- Primary Meta Tags -->
|
||||
<title>{finalTitle}</title>
|
||||
<meta name="title" content={finalTitle} />
|
||||
<meta name="description" content={finalDescription} />
|
||||
|
||||
<!-- Open Graph / Facebook -->
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:url" content={Astro.url} />
|
||||
<meta property="og:site_name" content={SITE_METADATA.openGraph.siteName} />
|
||||
<meta property="og:title" content={finalTitle} />
|
||||
<meta property="og:description" content={finalDescription} />
|
||||
<meta property="og:image" content={imageURL} />
|
||||
<meta property="og:image:width" content={SITE_METADATA.openGraph.images[0].width.toString()} />
|
||||
<meta property="og:image:height" content={SITE_METADATA.openGraph.images[0].height.toString()} />
|
||||
<meta property="og:image:alt" content={SITE_METADATA.openGraph.images[0].alt} />
|
||||
|
||||
<!-- Twitter -->
|
||||
<meta property="twitter:card" content={SITE_METADATA.twitter.card} />
|
||||
<meta property="twitter:url" content={Astro.url} />
|
||||
<meta property="twitter:title" content={finalTitle} />
|
||||
<meta property="twitter:description" content={finalDescription} />
|
||||
<meta property="twitter:image" content={imageURL} />
|
||||
<meta property="twitter:creator" content={SITE_METADATA.twitter.creator} />
|
||||
22
src/components/elements/header-link.tsx
Normal file
22
src/components/elements/header-link.tsx
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import { type ComponentProps } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type HeaderLinkProps = ComponentProps<"a">;
|
||||
|
||||
export default function HeaderLink({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: HeaderLinkProps) {
|
||||
return (
|
||||
<a
|
||||
{...props}
|
||||
className={cn(
|
||||
"inline-block text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-200",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
44
src/components/elements/theme-toggle.tsx
Normal file
44
src/components/elements/theme-toggle.tsx
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import { Moon, Sun } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const ThemeToggle = ({ className }: { className?: string }) => {
|
||||
const [theme, setTheme] = useState<"light" | "dark">("light");
|
||||
|
||||
useEffect(() => {
|
||||
// Get initial theme from localStorage or system preference
|
||||
const savedTheme = localStorage.getItem("theme");
|
||||
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
|
||||
.matches
|
||||
? "dark"
|
||||
: "light";
|
||||
const initialTheme = savedTheme || systemTheme;
|
||||
|
||||
setTheme(initialTheme as "light" | "dark");
|
||||
document.documentElement.classList.toggle("dark", initialTheme === "dark");
|
||||
}, []);
|
||||
|
||||
const toggleTheme = () => {
|
||||
const newTheme = theme === "light" ? "dark" : "light";
|
||||
setTheme(newTheme);
|
||||
document.documentElement.classList.toggle("dark");
|
||||
localStorage.setItem("theme", newTheme);
|
||||
};
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className={cn("size-8", className)}
|
||||
onClick={toggleTheme}
|
||||
>
|
||||
<Sun className="size-4 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
|
||||
<Moon className="absolute size-4 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
|
||||
<span className="sr-only">Toggle theme</span>
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export { ThemeToggle };
|
||||
46
src/components/icons/plus-signs.tsx
Normal file
46
src/components/icons/plus-signs.tsx
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
import { type SVGProps, useId } from "react";
|
||||
|
||||
interface PlusSignsProps extends SVGProps<SVGSVGElement> {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const PlusSigns = ({ className, ...props }: PlusSignsProps) => {
|
||||
const GAP = 16;
|
||||
const STROKE_WIDTH = 1;
|
||||
const PLUS_SIZE = 6;
|
||||
const id = useId();
|
||||
const patternId = `plus-pattern-${id}`;
|
||||
|
||||
return (
|
||||
<svg width={GAP * 2} height={GAP * 2} className={className} {...props}>
|
||||
<defs>
|
||||
<pattern
|
||||
id={patternId}
|
||||
x="0"
|
||||
y="0"
|
||||
width={GAP}
|
||||
height={GAP}
|
||||
patternUnits="userSpaceOnUse"
|
||||
>
|
||||
<line
|
||||
x1={GAP / 2}
|
||||
y1={(GAP - PLUS_SIZE) / 2}
|
||||
x2={GAP / 2}
|
||||
y2={(GAP + PLUS_SIZE) / 2}
|
||||
stroke="currentColor"
|
||||
strokeWidth={STROKE_WIDTH}
|
||||
/>
|
||||
<line
|
||||
x1={(GAP - PLUS_SIZE) / 2}
|
||||
y1={GAP / 2}
|
||||
x2={(GAP + PLUS_SIZE) / 2}
|
||||
y2={GAP / 2}
|
||||
stroke="currentColor"
|
||||
strokeWidth={STROKE_WIDTH}
|
||||
/>
|
||||
</pattern>
|
||||
</defs>
|
||||
<rect width="100%" height="100%" fill={`url(#${patternId})`} />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
190
src/components/sections/about-section.tsx
Normal file
190
src/components/sections/about-section.tsx
Normal file
|
|
@ -0,0 +1,190 @@
|
|||
import { PlusSigns } from "@/components/icons/plus-signs";
|
||||
import JoinUs from "@/components/sections/join-us";
|
||||
import {
|
||||
Carousel,
|
||||
CarouselContent,
|
||||
CarouselItem,
|
||||
} from "@/components/ui/carousel";
|
||||
|
||||
export function AboutSection() {
|
||||
return (
|
||||
<>
|
||||
{/* Hero Section */}
|
||||
<section className="lg:py-15 container relative max-w-5xl py-10 md:py-12">
|
||||
<div className="">
|
||||
<h1 className="text-4xl font-semibold tracking-tighter md:text-5xl lg:text-6xl">
|
||||
A different
|
||||
<br />
|
||||
kind of bank.
|
||||
</h1>
|
||||
<p className="text-muted-foreground font-mona mt-4 max-w-xl text-2xl md:text-3xl">
|
||||
We're on a mission to transform financial services by
|
||||
harnessing vast amounts of untapped financial data.
|
||||
</p>
|
||||
</div>
|
||||
{/* Background decoration */}
|
||||
<>
|
||||
<div className="absolute inset-0 z-[-1] -translate-y-1/2 translate-x-1/4 blur-[100px] will-change-transform">
|
||||
<div className="bg-primary-gradient/25 -translate-x-1/5 absolute right-0 top-0 h-[250px] w-[800px] rounded-full md:h-[300px]" />
|
||||
<div className="bg-secondary-gradient/15 absolute right-0 top-0 size-[250px] -translate-x-1/3 -translate-y-1/3 rounded-full md:size-[400px]" />
|
||||
</div>
|
||||
<div className="absolute -inset-40 z-[-1] [mask-image:radial-gradient(circle_at_center,black_0%,black_20%,transparent_80%)]">
|
||||
<PlusSigns className="text-foreground/[0.05] h-full w-full" />
|
||||
</div>
|
||||
</>
|
||||
</section>
|
||||
|
||||
{/* Stats Section */}
|
||||
<section className="container max-w-5xl border-y py-5">
|
||||
<h2 className="mini-title">By the numbers</h2>
|
||||
<div className="mt-6 grid grid-cols-2 gap-8 md:grid-cols-4">
|
||||
<div>
|
||||
<h3 className="text-4xl font-medium tracking-tight md:text-5xl lg:text-6xl">
|
||||
$150M
|
||||
</h3>
|
||||
<p className="text-muted-foreground mt-1 font-medium">Raised</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-4xl font-medium tracking-tight md:text-5xl lg:text-6xl">
|
||||
20K
|
||||
</h3>
|
||||
<p className="text-muted-foreground mt-1 font-medium">Companies</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-4xl font-medium tracking-tight md:text-5xl lg:text-6xl">
|
||||
1.3B
|
||||
</h3>
|
||||
<p className="text-muted-foreground mt-1 font-medium">
|
||||
Monthly transactions
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-4xl font-medium tracking-tight md:text-5xl lg:text-6xl">
|
||||
1.5K
|
||||
</h3>
|
||||
<p className="text-muted-foreground mt-1 font-medium">
|
||||
Connections per minute
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Mission Section */}
|
||||
<section className="lg:py-15 container max-w-5xl py-10 md:py-12">
|
||||
<div className="max-w-xl space-y-5 md:space-y-8 lg:space-y-10">
|
||||
<p className="text-lg">
|
||||
Financial services have changed, are changing, and will continue to
|
||||
change — for the better. Now is the time for finance to be
|
||||
developer-first and API-driven. But in order to do this — it needs a
|
||||
new foundation.
|
||||
</p>
|
||||
|
||||
<h2 className="font-mona text-2xl font-medium tracking-tight md:text-3xl">
|
||||
We were always told that banks can't be platforms.
|
||||
</h2>
|
||||
<p className="text-lg">
|
||||
Everyone tried fixing the problem by layering APIs on legacy
|
||||
systems, creating abstractions and inefficiencies. We have spent
|
||||
years building and scaling companies like Plaid, Stripe, and Affirm,
|
||||
confronting these limitations firsthand. The current solutions
|
||||
aren't good enough. We believe that banking infrastructure must
|
||||
be reimagined as an API platform. But we had to start from ground
|
||||
zero.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Image Grid Section */}
|
||||
<section className="lg:pb-15 my-5 pb-10 md:my-8 md:pb-12 lg:my-12">
|
||||
<Carousel
|
||||
opts={{
|
||||
align: "start",
|
||||
}}
|
||||
>
|
||||
<CarouselContent className="-ml-4">
|
||||
<CarouselItem className="basis-[80%] lg:basis-1/3 xl:basis-[40%]">
|
||||
<div className="relative h-[330px] lg:h-[440px]">
|
||||
<img
|
||||
src="/images/about/4.webp"
|
||||
alt="Charter team member working"
|
||||
className="size-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
</CarouselItem>
|
||||
<CarouselItem className="basis-[80%] lg:basis-1/3 xl:basis-[40%]">
|
||||
<div className="relative h-[330px] lg:h-[440px]">
|
||||
<img
|
||||
src="/images/about/2.webp"
|
||||
alt="Modern workspace setup"
|
||||
className="size-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
</CarouselItem>
|
||||
<CarouselItem className="basis-[80%] lg:basis-1/3 xl:basis-[40%]">
|
||||
<div className="relative h-[330px] lg:h-[440px]">
|
||||
<img
|
||||
src="/images/about/3.webp"
|
||||
alt="Team collaboration"
|
||||
className="size-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
</CarouselItem>
|
||||
</CarouselContent>
|
||||
</Carousel>
|
||||
</section>
|
||||
|
||||
{/* CoreAPI Section */}
|
||||
<section className="container max-w-5xl pb-10 md:pb-20">
|
||||
<div className="ml-auto mr-0 max-w-xl space-y-5 md:space-y-8 lg:-translate-x-10 lg:space-y-10">
|
||||
<p className="text-lg">
|
||||
We started building CoreAPI in 2019 and launched in 2022. Every
|
||||
endpoint has been designed from the ground up — with no technical
|
||||
debt or legacy systems. We are purpose-built to power financial
|
||||
innovation for the next hundred years.
|
||||
</p>
|
||||
|
||||
<h2 className="font-mona text-2xl font-medium tracking-tight md:text-3xl">
|
||||
We are a bit of a unique company — not your standard tech or fintech
|
||||
company.
|
||||
</h2>
|
||||
|
||||
<p className="text-lg">
|
||||
We are 100% founder and team-owned, profitable, and we keep our team
|
||||
lean. Over time, this page will become more polished, but for now,
|
||||
we're focused on delivering for developers.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Founding Team Section */}
|
||||
<section className="lg:py-15 container max-w-5xl py-10 md:py-12">
|
||||
<div className="grid gap-5 md:grid-cols-2 md:gap-10 lg:gap-16">
|
||||
<div className="order-2 md:order-1">
|
||||
<h2 className="font-mona text-3xl font-semibold md:text-4xl">
|
||||
The founding team
|
||||
</h2>
|
||||
<p className="mt-5 text-lg md:mt-6">
|
||||
We started building CoreAPI in 2019 and launched in 2022. Every
|
||||
endpoint has been designed from the ground up — with no technical
|
||||
debt or legacy systems. We are purpose-built to power financial
|
||||
innovation for the next hundred years. We are 100% founder and
|
||||
team-owned, profitable, and we keep our team lean. Over time, this
|
||||
page will become more polished, but for now, we're focused on
|
||||
delivering for developers. If you're interested in building
|
||||
the future of financial APIs, check out our open roles below.
|
||||
</p>
|
||||
</div>
|
||||
<div className="relative order-1 h-[300px] w-full md:order-2 md:h-[400px]">
|
||||
<img
|
||||
src="/images/about/1.webp"
|
||||
alt="Founding team collaboration"
|
||||
className="size-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<JoinUs />
|
||||
</>
|
||||
);
|
||||
}
|
||||
80
src/components/sections/ai-chatbot.tsx
Normal file
80
src/components/sections/ai-chatbot.tsx
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
import { ChevronRight, Timer, Wallet, Terminal, Calendar } from "lucide-react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
} from "@/components/ui/card";
|
||||
|
||||
const features = [
|
||||
{
|
||||
description: "Faster than Wire, and more affordable, too.",
|
||||
icon: Timer,
|
||||
href: "/pricing",
|
||||
},
|
||||
{
|
||||
description: "Send or request funds. FedNow does both.",
|
||||
icon: Wallet,
|
||||
href: "/pricing",
|
||||
},
|
||||
{
|
||||
description: "API request to creation in seven minutes.",
|
||||
icon: Terminal,
|
||||
href: "/pricing",
|
||||
},
|
||||
{
|
||||
description: "Settle any time, any day. What Bank holiday?",
|
||||
icon: Calendar,
|
||||
href: "/pricing",
|
||||
},
|
||||
];
|
||||
|
||||
export default function AIChatbot() {
|
||||
return (
|
||||
<section id="ai-chatbot" className="relative py-16 md:py-28 lg:py-32">
|
||||
<div className="container max-w-5xl">
|
||||
<div className="text-center">
|
||||
<h3 className="mini-title">MORE COMPUTER FUGAZI</h3>
|
||||
|
||||
<h2 className="mt-4 text-4xl font-semibold tracking-tight md:text-5xl lg:text-6xl">
|
||||
And we have an AI chatbot
|
||||
</h2>
|
||||
<p className="text-muted-foreground mt-3 text-xl font-medium md:text-2xl">
|
||||
{`We're betting on agents replacing our staff next year.`}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 grid grid-cols-2 gap-2.5 md:mt-12 lg:mt-20 lg:grid-cols-4 lg:gap-6">
|
||||
{features.map((feature, index) => (
|
||||
<Card key={index} className="flex flex-col">
|
||||
<CardHeader className="max-md:p-3">
|
||||
<feature.icon className="text-primary size-8" />
|
||||
<CardDescription className="text-foreground mt-4 font-medium">
|
||||
{feature.description}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="mt-auto max-md:p-3">
|
||||
<Button
|
||||
variant="outline"
|
||||
asChild
|
||||
className="border-border group w-[min(100%,300px)]"
|
||||
>
|
||||
<a href={feature.href}>
|
||||
Learn more
|
||||
<span className="sr-only">
|
||||
{" "}
|
||||
about {feature.description.toLowerCase()}
|
||||
</span>
|
||||
<ChevronRight className="ml-1 size-4 transition-transform group-hover:translate-x-0.5" />
|
||||
</a>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
127
src/components/sections/all-blogs.tsx
Normal file
127
src/components/sections/all-blogs.tsx
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
import { ChevronRight, Timer, Wallet, Terminal, Calendar } from "lucide-react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
} from "@/components/ui/card";
|
||||
|
||||
const features = [
|
||||
{
|
||||
title: "BFS",
|
||||
description: "Thematic roundups based on conferences or techniques.",
|
||||
icon: Timer,
|
||||
href: "/bfs",
|
||||
},
|
||||
{
|
||||
title: "DFS",
|
||||
description: "Each post is typically a closer look at a paper I have enjoyed reading.",
|
||||
icon: Wallet,
|
||||
href: "/dfs",
|
||||
},
|
||||
{
|
||||
title: "Problems",
|
||||
description: "Highlights from problem sets that feature in assessments.",
|
||||
icon: Terminal,
|
||||
href: "/problems",
|
||||
},
|
||||
{
|
||||
title: "Puzzles",
|
||||
description: "Favorite puzzles, typically interactive and playable. Solutions invited.",
|
||||
icon: Calendar,
|
||||
href: "/puzzles",
|
||||
},
|
||||
{
|
||||
title: "Reviews",
|
||||
description: "Reviews of things I use or consume: software, books, hardware, etc.",
|
||||
icon: Timer,
|
||||
href: "/reviews",
|
||||
},
|
||||
{
|
||||
title: "Reflections",
|
||||
description: "Random musings, pseudo-advice, how-tos. Core dumped.",
|
||||
icon: Wallet,
|
||||
href: "/reflections",
|
||||
},
|
||||
{
|
||||
title: "Vibes",
|
||||
description: "Conversations with LLMs with an intent to create something meaningful.",
|
||||
icon: Terminal,
|
||||
href: "/vibes",
|
||||
},
|
||||
{
|
||||
title: "Workflows",
|
||||
description: "Little automations and rituals that I've found useful.",
|
||||
icon: Calendar,
|
||||
href: "/workflows",
|
||||
},
|
||||
{
|
||||
title: "Magic",
|
||||
description: "Self-working card tricks: thoughts from a classroom context.",
|
||||
icon: Timer,
|
||||
href: "/magic",
|
||||
},
|
||||
{
|
||||
title: "Contests",
|
||||
description: "Editorials from contest problems, mostly informatics and math",
|
||||
icon: Wallet,
|
||||
href: "/contests",
|
||||
},
|
||||
{
|
||||
title: "Art",
|
||||
description: "This exists so I can stay accountable to my doodling practice :)",
|
||||
icon: Terminal,
|
||||
href: "/art",
|
||||
},
|
||||
{
|
||||
title: "Poetry",
|
||||
description: "Failed attempts at organizing thoughts in verse.",
|
||||
icon: Calendar,
|
||||
href: "/poetry",
|
||||
},
|
||||
];
|
||||
|
||||
export default function AllBlogs() {
|
||||
return (
|
||||
<section id="ai-chatbot" className="relative py-16 md:py-28 lg:py-32">
|
||||
<div className="container max-w-5xl">
|
||||
<div className="text-center">
|
||||
<h3 className="mini-title">CURRENTLY ACTIVE BLOGS</h3>
|
||||
|
||||
</div>
|
||||
|
||||
<div className="mt-8 grid grid-cols-2 gap-2.5 md:mt-12 lg:mt-20 lg:grid-cols-4 lg:gap-6">
|
||||
{features.map((feature, index) => (
|
||||
<Card key={index} className="flex flex-col">
|
||||
<CardHeader className="max-md:p-3">
|
||||
{/* <feature.icon className="text-primary size-8" /> */}
|
||||
<h3 className="text-lg font-semibold">{feature.title}</h3>
|
||||
<CardDescription className="text-foreground mt-4 font-medium">
|
||||
{feature.description}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="mt-auto max-md:p-3">
|
||||
<Button
|
||||
variant="outline"
|
||||
asChild
|
||||
className="border-border group w-[min(100%,300px)]"
|
||||
>
|
||||
<a href={feature.href}>
|
||||
Read On
|
||||
<span className="sr-only">
|
||||
{" "}
|
||||
about {feature.description.toLowerCase()}
|
||||
</span>
|
||||
<ChevronRight className="ml-1 size-4 transition-transform group-hover:translate-x-0.5" />
|
||||
</a>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
88
src/components/sections/blog-post.tsx
Normal file
88
src/components/sections/blog-post.tsx
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { format } from "date-fns";
|
||||
import { Calendar, Clock, ArrowLeft, User } from "lucide-react";
|
||||
|
||||
const BlogPost = ({
|
||||
post,
|
||||
children,
|
||||
}: {
|
||||
post: any;
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
const { title, authorName, image, pubDate, description, authorImage } =
|
||||
post.data;
|
||||
return (
|
||||
<>
|
||||
{/* Hero section with gradient background and post info */}
|
||||
<section className="pb-8 pt-4">
|
||||
<div className="container max-w-4xl">
|
||||
<div className="space-y-2 text-center">
|
||||
<h1 className="text-2xl font-bold tracking-tight md:text-4xl lg:text-5xl">
|
||||
{title}
|
||||
</h1>
|
||||
<p className="text-muted-foreground mx-auto max-w-2xl text-lg">
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mx-auto mt-6 flex max-w-md flex-wrap items-center justify-center gap-6">
|
||||
{/* Author info */}
|
||||
{authorImage ? (
|
||||
<div className="flex items-center gap-3">
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="bg-primary/10 text-primary flex h-10 w-10 items-center justify-center rounded-full shadow-sm">
|
||||
<User className="h-5 w-5" />
|
||||
</div>
|
||||
<div className="flex flex-col text-sm">
|
||||
<span className="font-medium">
|
||||
{authorName || "Anonymous"}
|
||||
</span>
|
||||
<span className="text-muted-foreground">Author</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Date info */}
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="text-muted-foreground flex items-center gap-1 text-sm">
|
||||
<Calendar className="h-4 w-4" />
|
||||
<span>{format(pubDate, "MMMM d, yyyy")}</span>
|
||||
</div>
|
||||
|
||||
<div className="text-muted-foreground flex items-center gap-1 text-sm">
|
||||
<Clock className="h-4 w-4" />
|
||||
<span>5 min read</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Featured image */}
|
||||
{image && (
|
||||
<section className="container max-w-5xl py-4">
|
||||
<div className="aspect-video w-full overflow-hidden rounded-xl border">
|
||||
<img
|
||||
src={image}
|
||||
alt={title}
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Article content */}
|
||||
<section className="container my-10 max-w-3xl">
|
||||
<article className="prose prose-lg dark:prose-invert prose-headings:font-semibold prose-a:text-primary mx-auto">
|
||||
{children}
|
||||
</article>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export { BlogPost };
|
||||
129
src/components/sections/blog-posts.tsx
Normal file
129
src/components/sections/blog-posts.tsx
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
import { Button } from "@/components/ui/button";
|
||||
import { ArrowRight } from "lucide-react";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Calendar, Clock, User } from "lucide-react";
|
||||
import { PlusSigns } from "../icons/plus-signs";
|
||||
|
||||
const BlogPosts = ({ posts, collection }: { posts: any[]; collection: string }) => {
|
||||
// Get the first post as the featured post
|
||||
const featuredPost = posts[0];
|
||||
const remainingPosts = posts.slice(1);
|
||||
|
||||
return (
|
||||
<div className="relative py-16 md:py-28 lg:py-32">
|
||||
|
||||
|
||||
<div className="absolute -inset-40 z-[-1] [mask-image:radial-gradient(circle_at_center,black_0%,black_20%,transparent_75%)]">
|
||||
<PlusSigns className="text-foreground/[0.05] h-full w-full" />
|
||||
</div>
|
||||
|
||||
{/* Featured Post */}
|
||||
<section className="mt-8">
|
||||
<div className="container max-w-6xl">
|
||||
<a
|
||||
href={`/${collection}/${featuredPost.id}/`}
|
||||
className="bg-card group relative mb-16 overflow-hidden rounded-xl border shadow-md"
|
||||
>
|
||||
<div className="flex flex-col gap-6 lg:flex-row">
|
||||
<div className="lg:w-1/2">
|
||||
<div className="p-2 lg:p-4">
|
||||
<img
|
||||
src={featuredPost.data.image}
|
||||
alt={featuredPost.data.title}
|
||||
className="aspect-video w-full rounded-lg object-cover transition-transform group-hover:scale-[1.02]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col justify-center p-4 pb-8 lg:w-1/2 lg:pr-8">
|
||||
<Badge variant="outline" className="mb-3 w-fit">
|
||||
Featured Post
|
||||
</Badge>
|
||||
<h2 className="mb-3 text-2xl font-bold group-hover:underline md:text-3xl">
|
||||
{featuredPost.data.title}
|
||||
</h2>
|
||||
<p className="text-muted-foreground mb-4 line-clamp-3 text-base">
|
||||
{featuredPost.data.description}
|
||||
</p>
|
||||
|
||||
<div className="mb-6 flex gap-4">
|
||||
<div className="text-muted-foreground flex items-center text-sm">
|
||||
<Calendar className="mr-1 h-4 w-4" />
|
||||
{new Date(featuredPost.data.pubDate).toLocaleDateString(
|
||||
"en-US",
|
||||
{
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
},
|
||||
)}
|
||||
</div>
|
||||
<div className="text-muted-foreground flex items-center text-sm">
|
||||
<Clock className="mr-1 h-4 w-4" />5 min read
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Regular Posts Grid */}
|
||||
<section className="mt-8">
|
||||
<div className="container max-w-6xl">
|
||||
<h2 className="mb-8 text-xl font-semibold md:text-2xl">
|
||||
Recent Articles
|
||||
</h2>
|
||||
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
{remainingPosts.map((post) => (
|
||||
<a
|
||||
key={post.id}
|
||||
className="bg-card group rounded-xl border shadow-sm transition-all hover:shadow-md"
|
||||
href={`/${collection}/${post.id}/`}
|
||||
>
|
||||
<div className="p-2">
|
||||
<img
|
||||
src={post.data.image}
|
||||
alt={post.data.title}
|
||||
className="aspect-video w-full rounded-lg object-cover transition-transform group-hover:scale-[1.01]"
|
||||
/>
|
||||
</div>
|
||||
<div className="px-4 pb-5 pt-2">
|
||||
<h2 className="mb-2 text-xl font-semibold group-hover:underline">
|
||||
{post.data.title}
|
||||
</h2>
|
||||
<p className="text-muted-foreground line-clamp-2 text-sm">
|
||||
{post.data.description}
|
||||
</p>
|
||||
|
||||
<div className="text-muted-foreground mt-3 flex items-center gap-3 text-xs">
|
||||
<div className="flex items-center gap-1">
|
||||
<Calendar className="h-3.5 w-3.5" />
|
||||
<span>
|
||||
{new Date(post.data.pubDate).toLocaleDateString(
|
||||
"en-US",
|
||||
{
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
},
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Clock className="h-3.5 w-3.5" />
|
||||
<span>5 min read</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { BlogPosts };
|
||||
100
src/components/sections/code-security.tsx
Normal file
100
src/components/sections/code-security.tsx
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
import { useState } from "react";
|
||||
|
||||
import { PlusSigns } from "../icons/plus-signs";
|
||||
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from "@/components/ui/accordion";
|
||||
|
||||
const FEATURES = [
|
||||
{
|
||||
id: "move-money",
|
||||
title: "Move and hold money",
|
||||
description:
|
||||
"Previously, emerging financial companies were forced to navigate costly middleware solutions that connected them to outdated sponsor bank systems.",
|
||||
image: "/images/homepage/code-snippet.webp",
|
||||
},
|
||||
{
|
||||
id: "card-program",
|
||||
title: "Build a modern card program",
|
||||
description:
|
||||
"Create and manage virtual and physical cards with complete control over spending limits, merchant categories, and real-time transaction monitoring.",
|
||||
image: "/images/homepage/code-snippet.webp",
|
||||
},
|
||||
{
|
||||
id: "lend-money",
|
||||
title: "Lend money",
|
||||
description:
|
||||
"Offer various lending products through our platform with automated underwriting, loan servicing, and compliance management.",
|
||||
image: "/images/homepage/code-snippet.webp",
|
||||
},
|
||||
];
|
||||
|
||||
export default function CodeSecurity() {
|
||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||
|
||||
return (
|
||||
<section
|
||||
id="code-security"
|
||||
className="container max-w-5xl py-16 md:py-28 lg:py-32"
|
||||
>
|
||||
<div className="from-primary-gradient/20 bg-linear-to-bl relative overflow-hidden rounded-3xl border to-transparent py-5 md:py-6 lg:py-8">
|
||||
<div className="absolute inset-0 z-[-1]">
|
||||
<PlusSigns className="text-foreground/[0.05] h-full w-full" />
|
||||
</div>
|
||||
<div className="md:px-6 lg:px-8">
|
||||
<div className="max-md:px-5">
|
||||
<h3 className="mini-title">WHY CHARTER?</h3>
|
||||
<h2 className="mt-3 text-3xl font-semibold tracking-tight md:text-4xl lg:text-5xl">
|
||||
Code security
|
||||
</h2>
|
||||
</div>
|
||||
<div className="mt-10 flex gap-12 overflow-hidden max-md:flex-col md:mt-16 lg:mt-20">
|
||||
<Accordion
|
||||
type="single"
|
||||
className="flex-1"
|
||||
defaultValue="0"
|
||||
onValueChange={(value) => setSelectedIndex(Number(value))}
|
||||
>
|
||||
{FEATURES.map((feature, index) => (
|
||||
<AccordionItem
|
||||
key={feature.id}
|
||||
value={index.toString()}
|
||||
className="border-black/20 last:border-none dark:border-white/20"
|
||||
>
|
||||
<AccordionTrigger className="hover:no-underline max-md:px-5">
|
||||
<h3 className="text-input font-inter text-xl font-bold">
|
||||
{feature.title}
|
||||
</h3>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="">
|
||||
<p className="text-muted-foreground font-medium leading-relaxed max-md:px-5">
|
||||
{feature.description}
|
||||
</p>
|
||||
<div className="relative mt-4 h-[280px] translate-x-5 md:hidden">
|
||||
<img
|
||||
src={feature.image}
|
||||
alt={feature.title}
|
||||
className="size-full rounded-2xl object-cover object-left-top shadow-xl"
|
||||
/>
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
))}
|
||||
</Accordion>
|
||||
<div className="relative h-[240px] max-md:hidden max-md:translate-x-6 md:flex-1">
|
||||
<img
|
||||
src={FEATURES[selectedIndex].image}
|
||||
alt={FEATURES[selectedIndex].title}
|
||||
className="size-full rounded-2xl object-cover object-left-top shadow-xl"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
84
src/components/sections/contact-page.tsx
Normal file
84
src/components/sections/contact-page.tsx
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
import { PlusSigns } from "@/components/icons/plus-signs";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
|
||||
export function ContactPage() {
|
||||
return (
|
||||
<section className="relative py-16 md:py-28 lg:py-32">
|
||||
<div className="container max-w-2xl">
|
||||
<div className="text-center">
|
||||
<h1 className="text-4xl font-semibold tracking-tight md:text-5xl lg:text-6xl">
|
||||
Book a demo
|
||||
</h1>
|
||||
<p className="text-muted-foreground mt-4 text-2xl md:text-3xl">
|
||||
Learn how Charter can work for you
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form className="mt-8 space-y-5 md:mt-12 lg:mt-20">
|
||||
{/* First Name */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="firstName">First name</Label>
|
||||
<Input id="firstName" placeholder="Enter your first name" />
|
||||
</div>
|
||||
|
||||
{/* Last Name */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="lastName">Last name</Label>
|
||||
<Input id="lastName" placeholder="Enter your last name" />
|
||||
</div>
|
||||
|
||||
{/* Email */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="Enter your email address"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Company */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="company">
|
||||
Company name{" "}
|
||||
<span className="text-muted-foreground">(optional)</span>
|
||||
</Label>
|
||||
<Input id="company" placeholder="Enter your company name" />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="employees">Number of employees</Label>
|
||||
<Input
|
||||
id="employees"
|
||||
placeholder="Enter number of employees"
|
||||
type="number"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Message */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="message">Message</Label>
|
||||
<Textarea
|
||||
id="message"
|
||||
rows={5}
|
||||
placeholder="Enter your message"
|
||||
className="resize-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button type="submit" size="lg">
|
||||
Submit
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div className="absolute -inset-40 z-[-1] [mask-image:radial-gradient(circle_at_center,black_0%,black_20%,transparent_75%)]">
|
||||
<PlusSigns className="text-foreground/[0.05] h-full w-full" />
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
120
src/components/sections/faq-page.tsx
Normal file
120
src/components/sections/faq-page.tsx
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
import { PlusSigns } from "@/components/icons/plus-signs";
|
||||
import Testimonials from "@/components/sections/testimonials";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from "@/components/ui/accordion";
|
||||
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
||||
|
||||
const FAQ_ITEMS = [
|
||||
{
|
||||
category: "SUPPORT",
|
||||
items: [
|
||||
{
|
||||
question: "Is there a free version?",
|
||||
answer:
|
||||
"Yes! We offer a generous free plan with just enough features except that one feature you really want! Our strategy is to get your credit card details on file then steadily double our prices against inflation rates.",
|
||||
},
|
||||
{
|
||||
question: "How do I update my account without breaking my laptop?",
|
||||
answer:
|
||||
"Our platform is designed with safety in mind. You can update your account settings through our intuitive dashboard without any risk to your hardware. We have multiple safeguards in place to prevent any system conflicts.",
|
||||
},
|
||||
{
|
||||
question: "Is support free, or do I need to Google everything?",
|
||||
answer:
|
||||
"We provide comprehensive support at no additional cost. Our dedicated support team is available 24/7 to help you with any questions or issues you might encounter. No need to rely on Google - we're here to help!",
|
||||
},
|
||||
{
|
||||
question: "Are you going to be subsumed by AI?",
|
||||
answer:
|
||||
"While we embrace AI technology to enhance our services, we maintain a strong human element in our operations. Our team works alongside AI to provide the best possible service while ensuring human oversight and decision-making remain central to our operations.",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
category: "YOUR ACCOUNT",
|
||||
items: [
|
||||
{
|
||||
question: "Is support free, or do I need to Google everything?",
|
||||
answer:
|
||||
"We provide comprehensive support at no additional cost. Our dedicated support team is available 24/7 to help you with any questions or issues you might encounter. No need to rely on Google - we're here to help!",
|
||||
},
|
||||
{
|
||||
question: "Are you going to be subsumed by AI?",
|
||||
answer:
|
||||
"While we embrace AI technology to enhance our services, we maintain a strong human element in our operations. Our team works alongside AI to provide the best possible service while ensuring human oversight and decision-making remain central to our operations.",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
category: "OTHER QUESTIONS",
|
||||
items: [
|
||||
{
|
||||
question: "Is support free, or do I need to Google everything?",
|
||||
answer:
|
||||
"We provide comprehensive support at no additional cost. Our dedicated support team is available 24/7 to help you with any questions or issues you might encounter. No need to rely on Google - we're here to help!",
|
||||
},
|
||||
{
|
||||
question: "Are you going to be subsumed by AI?",
|
||||
answer:
|
||||
"While we embrace AI technology to enhance our services, we maintain a strong human element in our operations. Our team works alongside AI to provide the best possible service while ensuring human oversight and decision-making remain central to our operations.",
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export default function FAQPage() {
|
||||
return (
|
||||
<>
|
||||
<section className="relative py-16 md:py-28 lg:py-32">
|
||||
<div className="container">
|
||||
<div className="text-center">
|
||||
<h1 className="text-4xl font-semibold tracking-tight md:text-5xl lg:text-6xl">
|
||||
Frequently Asked Questions
|
||||
</h1>
|
||||
<p className="text-muted-foreground mt-4 text-2xl md:text-3xl">
|
||||
Everything you need to know about Charter
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mx-auto mt-8 max-w-2xl space-y-12 md:mt-12 lg:mt-20">
|
||||
{FAQ_ITEMS.map((category) => (
|
||||
<Card key={category.category} className="border-hidden">
|
||||
<CardHeader className="pb-0">
|
||||
<h3 className="text-accent-foreground border-b pb-4 font-mono text-sm font-medium uppercase tracking-widest">
|
||||
{category.category}
|
||||
</h3>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Accordion type="single" collapsible className="w-full">
|
||||
{category.items.map((item, i) => (
|
||||
<AccordionItem
|
||||
key={i}
|
||||
value={`${category.category}-${i}`}
|
||||
className="border-muted border-b last:border-0"
|
||||
>
|
||||
<AccordionTrigger className="text-base font-medium hover:no-underline">
|
||||
{item.question}
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="text-muted-foreground text-base font-medium">
|
||||
{item.answer}
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
))}
|
||||
</Accordion>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute -inset-40 z-[-1] [mask-image:radial-gradient(circle_at_center,black_0%,black_20%,transparent_75%)]">
|
||||
<PlusSigns className="text-foreground/[0.05] h-full w-full" />
|
||||
</div>
|
||||
</section>
|
||||
<Testimonials />
|
||||
</>
|
||||
);
|
||||
}
|
||||
157
src/components/sections/featured-posts.tsx
Normal file
157
src/components/sections/featured-posts.tsx
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
import { PlusSigns } from "../icons/plus-signs";
|
||||
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const items = [
|
||||
{
|
||||
title: (
|
||||
<>
|
||||
Unparalleled
|
||||
<br />
|
||||
VAR flexibility
|
||||
</>
|
||||
),
|
||||
description: [
|
||||
"VAR is often known for its lengthy processing times. At Charter, we focus on achieving the fastest VAR transfers—often completed within hours.",
|
||||
"Unlike traditional banks and middleware, we connect directly with the Federal Reserve to ensure the fastest, most transparent transfers possible.",
|
||||
],
|
||||
image: {
|
||||
src: "/images/homepage/why-charter/1.webp",
|
||||
alt: "Code snippet",
|
||||
type: "fill",
|
||||
},
|
||||
className:
|
||||
"flex flex-col pl-6 py-6 overflow-hidden md:col-span-3 md:flex-row gap-6 md:gap-12",
|
||||
headerClassName: "flex-2 p-0",
|
||||
contentClassName:
|
||||
"relative h-[320px] w-full p-0 self-center overflow-hidden rounded-l-xl border md:flex-1",
|
||||
},
|
||||
{
|
||||
title: "Unparalleled VAR flexibility",
|
||||
description: [
|
||||
"VAR has a reputation for taking too long. At Charter, we optimise for the fastest VAR transfers possible — often in a matter of hours.",
|
||||
"Unlike legacy banks and middleware providers, we have a direct connection to the Federal Reserve to facilitate the quickest transfers.",
|
||||
],
|
||||
image: {
|
||||
src: "/images/homepage/why-charter/2.svg",
|
||||
alt: "VAR Process Flow",
|
||||
width: 283,
|
||||
height: 45,
|
||||
},
|
||||
className: "md:col-span-2 flex flex-col justify-center",
|
||||
contentClassName:
|
||||
"flex items-center justify-center p-6 max-md:mt-4 max-md:mb-8",
|
||||
imagePosition: "content",
|
||||
},
|
||||
{
|
||||
title: "Unparalleled VAR flexibility",
|
||||
description: [
|
||||
"VAR has a reputation for taking too long. At Charter, we optimise for the fastest VAR transfers possible — often in a matter of hours.",
|
||||
],
|
||||
image: {
|
||||
src: "/images/homepage/why-charter/3.svg",
|
||||
alt: "VAR Process Diagram",
|
||||
width: 283,
|
||||
height: 45,
|
||||
},
|
||||
className: "md:col-span-2",
|
||||
headerClassName: "h-full",
|
||||
imagePosition: "header",
|
||||
},
|
||||
{
|
||||
title: "Unparalleled VAR flexibility",
|
||||
description: [
|
||||
"Unlike traditional banks and middleware, we connect directly with the Federal Reserve to ensure the fastest, most transparent transfers possible.",
|
||||
],
|
||||
image: {
|
||||
src: "/images/homepage/code-snippet.webp",
|
||||
alt: "Code snippet",
|
||||
type: "fill",
|
||||
},
|
||||
className: "overflow-hidden md:col-span-3 ",
|
||||
headerClassName: "",
|
||||
contentClassName:
|
||||
"relative h-[242px] mt-2 p-0 ml-8 w-full md:max-w-[400px] lg:max-w-[500px] overflow-hidden md:mx-auto shadow-xl rounded-t-2xl",
|
||||
},
|
||||
];
|
||||
|
||||
const FeaturedPosts = () => {
|
||||
return (
|
||||
<section
|
||||
id="featured-posts"
|
||||
className="container relative py-16 md:py-28 lg:py-32"
|
||||
>
|
||||
<h3 className="mini-title">Featured Posts</h3>
|
||||
|
||||
<div className="relative z-10 mt-8 grid grid-cols-1 gap-6 md:grid-cols-5">
|
||||
{items.map((item, index) => (
|
||||
<Card
|
||||
key={index}
|
||||
className={cn("col-span-1 shadow-xl", item.className)}
|
||||
>
|
||||
<CardHeader className={item.headerClassName}>
|
||||
{item.imagePosition === "header" && (
|
||||
<img
|
||||
src={item.image.src}
|
||||
alt={item.image.alt}
|
||||
width={item.image.width}
|
||||
height={item.image.height}
|
||||
className="flex-1 self-center max-md:mb-8 max-md:mt-4 dark:[filter:brightness(0)_saturate(100%)_invert(100%)]"
|
||||
/>
|
||||
)}
|
||||
<CardTitle className="text-3xl">{item.title}</CardTitle>
|
||||
{item.description.map((desc, i) => (
|
||||
<CardDescription
|
||||
key={i}
|
||||
className="mt-2 text-base font-medium leading-snug"
|
||||
>
|
||||
{desc}
|
||||
</CardDescription>
|
||||
))}
|
||||
</CardHeader>
|
||||
<CardContent className={item.contentClassName}>
|
||||
{item.image.type === "fill" ? (
|
||||
<img
|
||||
src={item.image.src}
|
||||
alt={item.image.alt}
|
||||
className="size-full object-cover object-left-top"
|
||||
/>
|
||||
) : (
|
||||
item.imagePosition === "content" && (
|
||||
<img
|
||||
src={item.image.src}
|
||||
alt={item.image.alt}
|
||||
width={item.image.width}
|
||||
height={item.image.height}
|
||||
className="self-center dark:[filter:brightness(0)_saturate(100%)_invert(100%)]"
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Background decoration */}
|
||||
<>
|
||||
<div className="absolute inset-0 isolate will-change-transform">
|
||||
<div className="bg-primary-gradient/28 absolute top-1/2 size-[700px] -translate-y-1/2 rounded-full blur-[300px]" />
|
||||
<div className="bg-secondary-gradient/16 absolute right-0 top-1/2 size-[700px] -translate-y-1/2 -rotate-12 rounded-full blur-[300px]" />
|
||||
<div className="bg-tertiary-gradient/6 absolute bottom-1/4 right-20 z-[1] h-[500px] w-[800px] -rotate-12 rounded-full blur-[100px] md:bottom-10" />
|
||||
</div>
|
||||
<div className="absolute -inset-x-20 top-0 [mask-image:radial-gradient(circle_at_center,black_0%,black_20%,transparent_85%)]">
|
||||
<PlusSigns className="text-foreground/[0.075] h-full w-full" />
|
||||
</div>
|
||||
</>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default FeaturedPosts;
|
||||
86
src/components/sections/footer.tsx
Normal file
86
src/components/sections/footer.tsx
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
import { FaXTwitter, FaLinkedin, FaFacebook } from "react-icons/fa6";
|
||||
import { Home } from "lucide-react";
|
||||
|
||||
const navigation = [
|
||||
{
|
||||
title: "Products",
|
||||
links: [
|
||||
{ name: "VAR", href: "/#code-security" },
|
||||
{ name: "Credit Transfers", href: "/#why-charter" },
|
||||
{ name: "Credit Accounts", href: "/#ai-chatbot" },
|
||||
{ name: "Loan Origination", href: "/#ai-chatbot" },
|
||||
{ name: "Loan Purchase", href: "/#ai-chatbot" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Support",
|
||||
links: [
|
||||
{ name: "Pricing", href: "/pricing" },
|
||||
{ name: "FAQ", href: "/faq" },
|
||||
{ name: "Demo", href: "/contact" },
|
||||
{ name: "Contact", href: "/contact" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Company",
|
||||
links: [
|
||||
{ name: "About", href: "/about" },
|
||||
{ name: "Terms of Service", href: "/terms" },
|
||||
{ name: "Privacy Policy", href: "/privacy" },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const socialLinks = [
|
||||
{ icon: FaXTwitter, href: "https://twitter.com", label: "Twitter" },
|
||||
{ icon: FaFacebook, href: "https://facebook.com", label: "Facebook" },
|
||||
{ icon: FaLinkedin, href: "https://linkedin.com", label: "LinkedIn" },
|
||||
];
|
||||
|
||||
export default function Footer() {
|
||||
return (
|
||||
<footer className="pt-16 md:pt-28 lg:pt-32">
|
||||
<div className="w-full px-4 md:px-6 lg:px-8">
|
||||
<div className="mx-auto max-w-5xl">
|
||||
{/* Navigation Section */}
|
||||
<nav className="flex flex-wrap justify-between gap-x-32 gap-y-20 border-b pb-14 lg:pb-20">
|
||||
|
||||
</nav>
|
||||
|
||||
{/* Bottom Section */}
|
||||
<div className="py-8">
|
||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-4">
|
||||
<Home className="h-6 w-6" />
|
||||
<p className="text-sm font-medium">
|
||||
© {new Date().getFullYear()} Charter -{" "}
|
||||
<a
|
||||
href="https://shadcnblocks.com"
|
||||
className="underline transition-opacity hover:opacity-80"
|
||||
target="_blank"
|
||||
>
|
||||
Shadcnblocks.com
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-6">
|
||||
{socialLinks.map((link) => (
|
||||
<a
|
||||
aria-label={link.label}
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
className="hover:text-muted-foreground"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<link.icon />
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
130
src/components/sections/hero.tsx
Normal file
130
src/components/sections/hero.tsx
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
import {
|
||||
ChevronRight,
|
||||
Wallet,
|
||||
Waypoints,
|
||||
Building2,
|
||||
ArrowLeftRight,
|
||||
} from "lucide-react";
|
||||
|
||||
import { PlusSigns } from "../icons/plus-signs";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
|
||||
const features = [
|
||||
{
|
||||
id: "var",
|
||||
title: "Virtual Account",
|
||||
description: "Sed do eiusmod tempor incididunt ut labore",
|
||||
icon: Wallet,
|
||||
image: "/images/homepage/hero/1.webp",
|
||||
},
|
||||
{
|
||||
id: "wire",
|
||||
title: "Wire",
|
||||
description: "Sed do eiusmod tempor incididunt ut labore",
|
||||
icon: Waypoints,
|
||||
image: "/images/homepage/hero/2.webp",
|
||||
},
|
||||
{
|
||||
id: "bank-accounts",
|
||||
title: "Bank Accounts",
|
||||
description: "Sed do eiusmod tempor incididunt ut labore",
|
||||
icon: Building2,
|
||||
image: "/images/homepage/hero/3.webp",
|
||||
},
|
||||
{
|
||||
id: "bank-transfers",
|
||||
title: "Bank Transfers",
|
||||
description: "Sed do eiusmod tempor incididunt ut labore",
|
||||
icon: ArrowLeftRight,
|
||||
image: "/images/homepage/hero/4.webp",
|
||||
},
|
||||
];
|
||||
|
||||
export default function Hero() {
|
||||
return (
|
||||
<section className="bg-background relative overflow-hidden pt-16 md:pt-28 lg:pt-32">
|
||||
<div className="relative z-10">
|
||||
<div className="container max-w-5xl text-center">
|
||||
<h1 className="text-4xl font-semibold tracking-tighter md:text-5xl lg:text-6xl">
|
||||
Secure. Composable. Bankable.
|
||||
</h1>
|
||||
<p className="text-muted-foreground font-mona mt-4 text-balance text-2xl md:text-3xl">
|
||||
Charter is the fit-for-purpose developer API for building robust,
|
||||
encrypted finance products.
|
||||
</p>
|
||||
<div className="mt-7">
|
||||
<Button asChild size="lg">
|
||||
<a href="/signup">
|
||||
Start building for free
|
||||
<ChevronRight className="size-4" />
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{features.map((feature) => (
|
||||
<img
|
||||
key={feature.id}
|
||||
src={feature.image}
|
||||
alt={feature.title}
|
||||
className="lg:translate-y-15 hidden size-full translate-y-8 object-contain"
|
||||
/>
|
||||
))}
|
||||
<Tabs defaultValue="var" className="mt-8 md:mt-12 lg:mt-20">
|
||||
{/* Tab Content */}
|
||||
<div className="container">
|
||||
{features.map((feature) => (
|
||||
<TabsContent
|
||||
key={feature.id}
|
||||
value={feature.id}
|
||||
className="relative aspect-[2.116/1] overflow-hidden"
|
||||
>
|
||||
<img
|
||||
src={feature.image}
|
||||
alt={feature.title}
|
||||
className="lg:translate-y-15 size-full translate-y-8 object-contain"
|
||||
/>
|
||||
</TabsContent>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Tab Triggers */}
|
||||
<div className="bg-background pb-16 pt-12 md:pb-28 lg:pb-32">
|
||||
<TabsList className="mx-auto flex h-auto max-w-4xl justify-start gap-4 overflow-x-auto bg-transparent max-lg:px-5">
|
||||
{features.map((feature) => (
|
||||
<TabsTrigger
|
||||
key={feature.id}
|
||||
value={feature.id}
|
||||
className="ring-secondary-foreground group min-w-[200px] flex-1 justify-start whitespace-normal rounded-lg px-4 py-3 text-start transition-colors duration-300 data-[state=active]:bg-transparent data-[state=active]:ring lg:px-6 lg:py-4"
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
<div className="bg-muted-foreground/40 group-data-[state=active]:bg-secondary-foreground flex size-8 items-center justify-center rounded-md p-1.5">
|
||||
<feature.icon className="stroke-background" />
|
||||
</div>
|
||||
<h2 className="group-data-[state=active]:text-primary font-inter text-foreground mt-2 text-lg font-bold">
|
||||
{feature.title}
|
||||
</h2>
|
||||
<p className="text-muted-foreground mt-1 text-sm">
|
||||
{feature.description}
|
||||
</p>
|
||||
</div>
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
</div>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
{/* Background decoration */}
|
||||
<div className="absolute inset-0 aspect-square [mask-image:radial-gradient(circle_at_center,black_0%,black_20%,transparent_75%)]">
|
||||
<PlusSigns className="text-foreground/[0.05] h-full w-full" />
|
||||
</div>
|
||||
<div className="">
|
||||
{/* will-change-transform improves performance on scroll on safari because of the high blur */}
|
||||
<div className="bg-primary-gradient/11 absolute inset-x-[0%] bottom-0 left-0 h-[500px] rounded-full blur-[100px] will-change-transform md:h-[950px]" />
|
||||
<div className="bg-secondary-gradient/9 absolute inset-x-[30%] bottom-0 right-0 h-[500px] rounded-full blur-[100px] will-change-transform md:h-[950px]" />
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
61
src/components/sections/join-us.tsx
Normal file
61
src/components/sections/join-us.tsx
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import { Button } from "../ui/button";
|
||||
|
||||
const JoinUs = () => {
|
||||
const jobCategories = [
|
||||
{
|
||||
name: "Engineering",
|
||||
jobs: [
|
||||
{ title: "iOS Developer", location: "Remote" },
|
||||
{ title: "Backend Engineer", location: "Remote" },
|
||||
{ title: "Frontend Engineer", location: "Remote" },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Design",
|
||||
jobs: [
|
||||
{ title: "Senior Designer", location: "Remote" },
|
||||
{ title: "Staff Designer", location: "Remote" },
|
||||
{ title: "Designer", location: "Remote" },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<section className="lg:py-15 container max-w-5xl py-10 md:py-12">
|
||||
<div className="border-t pt-5">
|
||||
<div className="max-w-2xl">
|
||||
<h2 className="text-4xl font-semibold tracking-tight md:text-4xl">
|
||||
Join us
|
||||
</h2>
|
||||
<p className="text-muted-foreground font-mona lg:pb-15 mt-4 max-w-2xl pb-10 text-2xl md:pb-12 md:text-3xl">
|
||||
We work together from all over the world.
|
||||
</p>
|
||||
|
||||
{jobCategories.map((category, categoryIndex) => (
|
||||
<div key={categoryIndex}>
|
||||
<h3 className="border-foreground border-b py-6 text-lg font-semibold">
|
||||
{category.name}
|
||||
</h3>
|
||||
<div className="">
|
||||
{category.jobs.map((job, jobIndex) => (
|
||||
<div
|
||||
key={`${categoryIndex}-${jobIndex}`}
|
||||
className="flex items-center justify-between gap-10 border-b py-3 md:gap-16 lg:gap-28"
|
||||
>
|
||||
<h4 className="flex-1 font-medium">{job.title}</h4>
|
||||
<p className="text-muted-foreground">{job.location}</p>
|
||||
<Button variant="outline" asChild>
|
||||
<a href="#">View listing</a>
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default JoinUs;
|
||||
110
src/components/sections/latest-posts.tsx
Normal file
110
src/components/sections/latest-posts.tsx
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
import { PlusSigns } from "@/components/icons/plus-signs";
|
||||
import Testimonials from "@/components/sections/testimonials";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from "@/components/ui/accordion";
|
||||
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
||||
|
||||
const FAQ_ITEMS = [
|
||||
{
|
||||
category: "SUPPORT",
|
||||
items: [
|
||||
{
|
||||
question: "Is there a free version?",
|
||||
answer:
|
||||
"Yes! We offer a generous free plan with just enough features except that one feature you really want! Our strategy is to get your credit card details on file then steadily double our prices against inflation rates.",
|
||||
},
|
||||
{
|
||||
question: "How do I update my account without breaking my laptop?",
|
||||
answer:
|
||||
"Our platform is designed with safety in mind. You can update your account settings through our intuitive dashboard without any risk to your hardware. We have multiple safeguards in place to prevent any system conflicts.",
|
||||
},
|
||||
{
|
||||
question: "Is support free, or do I need to Google everything?",
|
||||
answer:
|
||||
"We provide comprehensive support at no additional cost. Our dedicated support team is available 24/7 to help you with any questions or issues you might encounter. No need to rely on Google - we're here to help!",
|
||||
},
|
||||
{
|
||||
question: "Are you going to be subsumed by AI?",
|
||||
answer:
|
||||
"While we embrace AI technology to enhance our services, we maintain a strong human element in our operations. Our team works alongside AI to provide the best possible service while ensuring human oversight and decision-making remain central to our operations.",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
category: "YOUR ACCOUNT",
|
||||
items: [
|
||||
{
|
||||
question: "Is support free, or do I need to Google everything?",
|
||||
answer:
|
||||
"We provide comprehensive support at no additional cost. Our dedicated support team is available 24/7 to help you with any questions or issues you might encounter. No need to rely on Google - we're here to help!",
|
||||
},
|
||||
{
|
||||
question: "Are you going to be subsumed by AI?",
|
||||
answer:
|
||||
"While we embrace AI technology to enhance our services, we maintain a strong human element in our operations. Our team works alongside AI to provide the best possible service while ensuring human oversight and decision-making remain central to our operations.",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
category: "OTHER QUESTIONS",
|
||||
items: [
|
||||
{
|
||||
question: "Is support free, or do I need to Google everything?",
|
||||
answer:
|
||||
"We provide comprehensive support at no additional cost. Our dedicated support team is available 24/7 to help you with any questions or issues you might encounter. No need to rely on Google - we're here to help!",
|
||||
},
|
||||
{
|
||||
question: "Are you going to be subsumed by AI?",
|
||||
answer:
|
||||
"While we embrace AI technology to enhance our services, we maintain a strong human element in our operations. Our team works alongside AI to provide the best possible service while ensuring human oversight and decision-making remain central to our operations.",
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export default function LatestPosts() {
|
||||
return (
|
||||
<>
|
||||
<section className="relative py-16 md:py-28 lg:py-32">
|
||||
<div className="container">
|
||||
<div className="mx-auto mt-8 max-w-2xl space-y-12 md:mt-12 lg:mt-20">
|
||||
{FAQ_ITEMS.map((category) => (
|
||||
<Card key={category.category} className="border-hidden">
|
||||
<CardHeader className="pb-0">
|
||||
<h3 className="text-accent-foreground border-b pb-4 font-mono text-sm font-medium uppercase tracking-widest">
|
||||
{category.category}
|
||||
</h3>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Accordion type="single" collapsible className="w-full">
|
||||
{category.items.map((item, i) => (
|
||||
<AccordionItem
|
||||
key={i}
|
||||
value={`${category.category}-${i}`}
|
||||
className="border-muted border-b last:border-0"
|
||||
>
|
||||
<AccordionTrigger className="text-base font-medium hover:no-underline">
|
||||
{item.question}
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="text-muted-foreground text-base font-medium">
|
||||
{item.answer}
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
))}
|
||||
</Accordion>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute -inset-40 z-[-1] [mask-image:radial-gradient(circle_at_center,black_0%,black_20%,transparent_75%)]">
|
||||
<PlusSigns className="text-foreground/[0.05] h-full w-full" />
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
86
src/components/sections/launch-pricing.tsx
Normal file
86
src/components/sections/launch-pricing.tsx
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
import { ChevronRight, Check } from "lucide-react";
|
||||
|
||||
import { PlusSigns } from "../icons/plus-signs";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
export default function LaunchPricing() {
|
||||
return (
|
||||
<section
|
||||
id="launch-today"
|
||||
className="text-background py-16 md:container md:max-w-5xl md:py-28 lg:py-32"
|
||||
>
|
||||
<div className="from-primary-900 to-primary/90 divide-background/20 bg-linear-to-r container relative isolate grid items-center overflow-hidden py-8 max-lg:gap-10 max-md:gap-6 md:rounded-3xl lg:grid-cols-2 lg:divide-x lg:px-8">
|
||||
<div className="absolute inset-0 -z-10 [mask-image:linear-gradient(to_left,black_50%,transparent_100%)]">
|
||||
<PlusSigns className="text-background/[0.05] h-full w-full" />
|
||||
</div>
|
||||
<div className="lg:py-16 lg:pr-20">
|
||||
<h2 className="text-3xl font-semibold tracking-tight md:text-4xl lg:text-5xl">
|
||||
Launch today
|
||||
</h2>
|
||||
<p className="text-background/70 mt-3 text-sm font-medium">
|
||||
In the past, new financial companies had to rely on expensive
|
||||
middleware that linked them to outdated sponsor bank systems,
|
||||
restricting their potential. Our API solves this today.
|
||||
</p>
|
||||
<div className="mt-8 flex flex-wrap gap-4 max-md:hidden">
|
||||
<Button size="lg" variant="secondary" className="group" asChild>
|
||||
<a href="/signup">
|
||||
Start for free
|
||||
<ChevronRight className="ml-1 size-4 transition-transform group-hover:translate-x-0.5" />
|
||||
</a>
|
||||
</Button>
|
||||
<Button size="lg" className="bg-secondary-foreground group" asChild>
|
||||
<a href="/contact">
|
||||
Get a demo
|
||||
<ChevronRight className="ml-1 size-4 transition-transform group-hover:translate-x-0.5" />
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6 lg:py-10 lg:pl-20">
|
||||
<div>
|
||||
<h3 className="text-background text-3xl font-semibold md:text-4xl lg:text-5xl">
|
||||
$29.99
|
||||
</h3>
|
||||
<p className="text-background/70 mt-1 text-xl font-medium">
|
||||
per user per month
|
||||
</p>
|
||||
</div>
|
||||
<ul className="text-background/70 space-y-3 text-sm">
|
||||
<li className="flex items-center gap-2">
|
||||
<Check className="size-4" />
|
||||
All free plan features and...
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<Check className="size-4" />
|
||||
Mainline AI
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<Check className="size-4" />
|
||||
Unlimited teams
|
||||
</li>
|
||||
</ul>
|
||||
<div className="mt-10 flex flex-wrap gap-4 md:hidden">
|
||||
<Button size="lg" variant="secondary" className="group w-full">
|
||||
<a href="/signup" className="flex items-center gap-2">
|
||||
Start building for free
|
||||
<ChevronRight className="ml-1 size-4 transition-transform group-hover:translate-x-0.5" />
|
||||
</a>
|
||||
</Button>
|
||||
<Button
|
||||
size="lg"
|
||||
className="bg-secondary-foreground border-background/20 group w-full border"
|
||||
>
|
||||
<a href="/contact" className="flex items-center gap-2">
|
||||
Get a demo
|
||||
<ChevronRight className="ml-1 size-4 transition-transform group-hover:translate-x-0.5" />
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
75
src/components/sections/login-section.tsx
Normal file
75
src/components/sections/login-section.tsx
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
import { Card, CardContent, CardHeader } from "../ui/card";
|
||||
import { Input } from "../ui/input";
|
||||
import { Button } from "../ui/button";
|
||||
import { FcGoogle } from "react-icons/fc";
|
||||
import { Checkbox } from "../ui/checkbox";
|
||||
|
||||
const LoginSection = () => {
|
||||
return (
|
||||
<section className="bg-sand-100 py-16 md:py-28 lg:py-32">
|
||||
<div className="container">
|
||||
<div className="flex flex-col gap-4">
|
||||
<Card className="mx-auto w-full max-w-sm">
|
||||
<CardHeader className="flex flex-col items-center space-y-0">
|
||||
<img
|
||||
src="/images/layout/logo.svg"
|
||||
alt="logo"
|
||||
width={94}
|
||||
height={18}
|
||||
className="mb-7 dark:invert"
|
||||
/>
|
||||
<p className="mb-2 text-2xl font-bold">Welcome back</p>
|
||||
<p className="text-muted-foreground">
|
||||
Please enter your details.
|
||||
</p>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid gap-4">
|
||||
<Input type="email" placeholder="Enter your email" required />
|
||||
<div>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="Enter your password"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="remember"
|
||||
className="border-muted-foreground"
|
||||
/>
|
||||
<label
|
||||
htmlFor="remember"
|
||||
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
>
|
||||
Remember me
|
||||
</label>
|
||||
</div>
|
||||
<a href="#" className="text-primary text-sm font-medium">
|
||||
Forgot password
|
||||
</a>
|
||||
</div>
|
||||
<Button type="submit" className="mt-2 w-full">
|
||||
Create an account
|
||||
</Button>
|
||||
<Button variant="outline" className="w-full">
|
||||
<FcGoogle className="mr-2 size-5" />
|
||||
Sign up with Google
|
||||
</Button>
|
||||
</div>
|
||||
<div className="text-muted-foreground mx-auto mt-8 flex justify-center gap-1 text-sm">
|
||||
<p>Don't have an account?</p>
|
||||
<a href="/signup" className="text-primary font-medium">
|
||||
Sign up
|
||||
</a>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoginSection;
|
||||
111
src/components/sections/logos.tsx
Normal file
111
src/components/sections/logos.tsx
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
"use client";
|
||||
|
||||
const ITEMS = [
|
||||
{
|
||||
name: "Mercury",
|
||||
src: "/images/logos/mercury.svg",
|
||||
width: 143,
|
||||
height: 26,
|
||||
href: "https://mercury.com",
|
||||
},
|
||||
{
|
||||
name: "Watershed",
|
||||
src: "/images/logos/watershed.svg",
|
||||
width: 154,
|
||||
height: 31,
|
||||
href: "https://watershed.com",
|
||||
},
|
||||
{
|
||||
name: "Retool",
|
||||
src: "/images/logos/retool.svg",
|
||||
width: 113,
|
||||
height: 22,
|
||||
href: "https://retool.com",
|
||||
},
|
||||
{
|
||||
name: "Descript",
|
||||
src: "/images/logos/descript.svg",
|
||||
width: 112,
|
||||
height: 27,
|
||||
href: "https://descript.com",
|
||||
},
|
||||
{
|
||||
name: "Perplexity",
|
||||
src: "/images/logos/perplexity.svg",
|
||||
width: 141,
|
||||
height: 32,
|
||||
href: "https://perplexity.ai",
|
||||
},
|
||||
{
|
||||
name: "Monzo",
|
||||
src: "/images/logos/monzo.svg",
|
||||
width: 104,
|
||||
height: 18,
|
||||
href: "https://monzo.com",
|
||||
},
|
||||
{
|
||||
name: "Ramp",
|
||||
src: "/images/logos/ramp.svg",
|
||||
width: 105,
|
||||
height: 28,
|
||||
href: "https://ramp.com",
|
||||
},
|
||||
{
|
||||
name: "Raycast",
|
||||
src: "/images/logos/raycast.svg",
|
||||
width: 128,
|
||||
height: 33,
|
||||
href: "https://raycast.com",
|
||||
},
|
||||
{
|
||||
name: "Arc",
|
||||
src: "/images/logos/arc.svg",
|
||||
width: 90,
|
||||
height: 28,
|
||||
href: "https://arc.com",
|
||||
},
|
||||
];
|
||||
|
||||
export default function Logos() {
|
||||
return (
|
||||
<section className="overflow-hidden">
|
||||
<h2 className="text-muted-foreground text-center text-2xl">
|
||||
From next-gen startups to established enterprises.
|
||||
</h2>
|
||||
|
||||
<div className="relative mt-10 flex w-full">
|
||||
<div className="from-background bg-linear-to-r absolute left-0 z-20 h-full w-10 to-transparent" />
|
||||
<div className="from-background bg-linear-to-l absolute right-0 z-20 h-full w-10 to-transparent" />
|
||||
|
||||
{/* First marquee group */}
|
||||
<div className="animate-marquee flex shrink-0 items-center gap-12">
|
||||
{ITEMS.map((logo, index) => (
|
||||
<a href={logo.href} target="_blank" key={index} className="p-6">
|
||||
<img
|
||||
src={logo.src}
|
||||
alt={logo.name}
|
||||
width={logo.width}
|
||||
height={logo.height}
|
||||
className="object-contain transition-opacity hover:opacity-70"
|
||||
/>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
{/* Second marquee group */}
|
||||
<div className="animate-marquee flex shrink-0 items-center gap-12">
|
||||
{ITEMS.map((logo, index) => (
|
||||
<a href={logo.href} target="_blank" key={index} className="p-6">
|
||||
<img
|
||||
src={logo.src}
|
||||
alt={logo.name}
|
||||
width={logo.width}
|
||||
height={logo.height}
|
||||
className="object-contain transition-opacity hover:opacity-70"
|
||||
/>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
133
src/components/sections/navbar.tsx
Normal file
133
src/components/sections/navbar.tsx
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
import React, { useState, useEffect } from "react";
|
||||
|
||||
import { ChevronRight, Menu, X, Home } from "lucide-react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
NavigationMenu,
|
||||
NavigationMenuContent,
|
||||
NavigationMenuItem,
|
||||
NavigationMenuLink,
|
||||
NavigationMenuList,
|
||||
NavigationMenuTrigger,
|
||||
} from "@/components/ui/navigation-menu";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ThemeToggle } from "../elements/theme-toggle";
|
||||
|
||||
const Navbar = ({ currentPage }: { currentPage: string }) => {
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||
const pathname = currentPage;
|
||||
|
||||
useEffect(() => {
|
||||
if (isMenuOpen) {
|
||||
document.body.classList.add("overflow-hidden");
|
||||
} else {
|
||||
document.body.classList.remove("overflow-hidden");
|
||||
}
|
||||
|
||||
// Cleanup on unmount
|
||||
return () => {
|
||||
document.body.classList.remove("overflow-hidden");
|
||||
};
|
||||
}, [isMenuOpen]);
|
||||
|
||||
const ITEMS = [
|
||||
{ label: "About", href: "/about" },
|
||||
{ label: "Latest", href: "/latest" },
|
||||
];
|
||||
|
||||
return (
|
||||
<header className={"relative z-50"}>
|
||||
<div className="container max-w-5xl lg:pt-10">
|
||||
<div className="flex items-center justify-between py-3">
|
||||
{/* Logo */}
|
||||
<a href="/" className="flex items-center gap-2">
|
||||
<Home className="h-8 w-8" />
|
||||
</a>
|
||||
|
||||
{/* Desktop Navigation */}
|
||||
<NavigationMenu className="hidden items-center gap-8 lg:flex">
|
||||
<NavigationMenuList>
|
||||
{ITEMS.map((link) =>
|
||||
<NavigationMenuItem key={link.label}>
|
||||
<a
|
||||
href={link.href}
|
||||
className={cn(
|
||||
"hover:text-accent-foreground p-2 lg:text-base",
|
||||
pathname === link.href && "text-accent-foreground",
|
||||
)}
|
||||
>
|
||||
{link.label}
|
||||
</a>
|
||||
</NavigationMenuItem>
|
||||
)}
|
||||
</NavigationMenuList>
|
||||
</NavigationMenu>
|
||||
|
||||
{/* Auth Buttons */}
|
||||
<div className="flex items-center gap-2.5">
|
||||
|
||||
<div
|
||||
className={`transition-opacity duration-300 ${isMenuOpen ? "max-lg:pointer-events-none max-lg:opacity-0" : "opacity-100"}`}
|
||||
>
|
||||
<ThemeToggle className="dark:bg-foreground dark:text-background" />
|
||||
</div>
|
||||
|
||||
{/* Hamburger Menu Button (Mobile Only) */}
|
||||
<button
|
||||
className="relative flex size-8 lg:hidden"
|
||||
onClick={() => setIsMenuOpen(!isMenuOpen)}
|
||||
>
|
||||
<span className="sr-only">Open main menu</span>
|
||||
<div className="absolute left-1/2 top-1/2 block w-[18px] -translate-x-1/2 -translate-y-1/2">
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={`absolute block h-0.5 w-full rounded-full bg-current transition duration-500 ease-in-out ${isMenuOpen ? "rotate-45" : "-translate-y-1.5"}`}
|
||||
></span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={`absolute block h-0.5 w-full rounded-full bg-current transition duration-500 ease-in-out ${isMenuOpen ? "opacity-0" : ""}`}
|
||||
></span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={`absolute block h-0.5 w-full rounded-full bg-current transition duration-500 ease-in-out ${isMenuOpen ? "-rotate-45" : "translate-y-1.5"}`}
|
||||
></span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile Menu Overlay */}
|
||||
<div
|
||||
className={cn(
|
||||
"bg-background container absolute inset-0 top-full flex h-[calc(100vh-64px)] flex-col transition-all duration-300 ease-in-out lg:hidden",
|
||||
isMenuOpen
|
||||
? "visible translate-x-0 opacity-100"
|
||||
: "invisible translate-x-full opacity-0",
|
||||
)}
|
||||
>
|
||||
|
||||
<nav className="mt-3 flex flex-1 flex-col gap-6">
|
||||
{ITEMS.map((link) =>
|
||||
<a
|
||||
key={link.label}
|
||||
href={link.href}
|
||||
className={cn(
|
||||
"text-lg tracking-[-0.36px]",
|
||||
pathname === link.href && "text-muted-foreground",
|
||||
)}
|
||||
onClick={() => setIsMenuOpen(false)}
|
||||
>
|
||||
{link.label}
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
};
|
||||
|
||||
export default Navbar;
|
||||
64
src/components/sections/newsletter-signup.tsx
Normal file
64
src/components/sections/newsletter-signup.tsx
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
import React from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Mail } from "lucide-react";
|
||||
|
||||
const NewsletterSignup = () => {
|
||||
const [email, setEmail] = React.useState("");
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
// This would be connected to your newsletter service
|
||||
console.log("Submitting email:", email);
|
||||
alert("Thanks for subscribing!");
|
||||
setEmail("");
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="bg-muted/50 border-t py-16 md:py-24">
|
||||
<div className="container max-w-5xl">
|
||||
<div className="bg-card flex flex-col items-center justify-between gap-8 rounded-2xl border p-8 shadow-sm md:p-12 lg:flex-row">
|
||||
<div className="max-w-md text-center lg:text-left">
|
||||
<div className="mb-3 flex justify-center lg:justify-start">
|
||||
<div className="bg-primary/10 text-primary flex h-10 w-10 items-center justify-center rounded-full">
|
||||
<Mail className="h-5 w-5" />
|
||||
</div>
|
||||
</div>
|
||||
<h2 className="mb-3 text-2xl font-bold md:text-3xl">
|
||||
Subscribe to our newsletter
|
||||
</h2>
|
||||
<p className="text-muted-foreground mb-0 text-base">
|
||||
Stay updated with the latest articles, tutorials, and insights
|
||||
from our team. We'll never spam your inbox.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="w-full max-w-md">
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
className="flex flex-col gap-2 sm:flex-row"
|
||||
>
|
||||
<Input
|
||||
type="email"
|
||||
placeholder="Enter your email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
required
|
||||
className="h-12"
|
||||
/>
|
||||
<Button type="submit" className="h-12">
|
||||
Subscribe
|
||||
</Button>
|
||||
</form>
|
||||
<p className="text-muted-foreground mt-2 text-xs">
|
||||
By subscribing, you agree to our Privacy Policy and consent to
|
||||
receive updates from our company.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default NewsletterSignup;
|
||||
187
src/components/sections/pricing-cards.tsx
Normal file
187
src/components/sections/pricing-cards.tsx
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
import { Check, ChevronRight } from "lucide-react";
|
||||
|
||||
import { PlusSigns } from "../icons/plus-signs";
|
||||
import { Button } from "../ui/button";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type PricingTier = {
|
||||
name: string;
|
||||
price: string;
|
||||
description: string;
|
||||
features: string[];
|
||||
cta: {
|
||||
text: string;
|
||||
href: string;
|
||||
};
|
||||
};
|
||||
|
||||
const ITEMS: PricingTier[] = [
|
||||
{
|
||||
name: "STARTER",
|
||||
price: "$0",
|
||||
description: "Free for everyone",
|
||||
features: ["Unlimited members", "250 transactions", "No support"],
|
||||
cta: {
|
||||
text: "Start for free",
|
||||
href: "/signup",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "BASIC",
|
||||
price: "$29.99",
|
||||
description: "per user per month",
|
||||
features: [
|
||||
"All free plan features and...",
|
||||
"Mainline AI",
|
||||
"Unlimited teams",
|
||||
],
|
||||
cta: {
|
||||
text: "7 days free",
|
||||
href: "/signup",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ENTERPRISE",
|
||||
price: "$ENT",
|
||||
description: "Custom pricing",
|
||||
features: [
|
||||
"All basic plan features and...",
|
||||
"Advanced security controls",
|
||||
"Migration support",
|
||||
],
|
||||
cta: {
|
||||
text: "Book a demo",
|
||||
href: "/contact",
|
||||
},
|
||||
},
|
||||
];
|
||||
const PricingCards = () => {
|
||||
return (
|
||||
<section className="relative py-16 md:py-28 lg:py-32">
|
||||
<div className="container">
|
||||
<h1 className="text-center text-4xl font-semibold tracking-tight md:text-5xl lg:text-6xl">
|
||||
Pricing
|
||||
</h1>
|
||||
<div className="mx-auto mt-4 max-w-[45rem] space-y-2 text-center">
|
||||
<p className="text-muted-foreground text-2xl md:text-3xl">
|
||||
Use Charter for free with your whole team. Upgrade to enable
|
||||
enhanced features.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="relative mt-8 md:mt-12 lg:mt-20">
|
||||
{/* Background and layout wrapper */}
|
||||
<div className="from-primary-900 to-primary/90 bg-linear-to-r absolute inset-0 isolate hidden rounded-3xl md:block">
|
||||
<div className="absolute inset-0 -z-10 [mask-image:linear-gradient(to_left,black_50%,transparent_100%)]">
|
||||
<PlusSigns className="h-full w-full text-white/[0.05]" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="md:divide-background/20 relative space-y-6 md:grid md:grid-cols-3 md:space-y-0 md:divide-x md:p-6 lg:p-8">
|
||||
{ITEMS.map((tier, index) => (
|
||||
<PricingCard
|
||||
key={tier.name}
|
||||
tier={tier}
|
||||
isHighlighted={index === 1}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute -inset-40 z-[-1] [mask-image:radial-gradient(circle_at_center,black_0%,black_20%,transparent_75%)]">
|
||||
<PlusSigns className="text-foreground/[0.05] h-full w-full" />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default PricingCards;
|
||||
function PricingCard({
|
||||
tier,
|
||||
isHighlighted,
|
||||
}: {
|
||||
tier: PricingTier;
|
||||
isHighlighted: boolean;
|
||||
}) {
|
||||
const styles = {
|
||||
card: cn(
|
||||
"flex flex-col gap-6 rounded-xl p-6 sm:rounded-2xl md:rounded-none lg:p-8",
|
||||
// Mobile styles
|
||||
isHighlighted
|
||||
? "max-md:from-primary-900 max-md:to-primary/90 max-md:bg-linear-to-r"
|
||||
: "bg-background max-md:border",
|
||||
// Desktop styles
|
||||
"md:bg-transparent",
|
||||
),
|
||||
title: cn(
|
||||
"font-mono text-sm tracking-widest",
|
||||
// Mobile styles
|
||||
isHighlighted ? "text-background/70" : "text-foreground/70",
|
||||
// Desktop styles
|
||||
"md:text-background/70",
|
||||
),
|
||||
price: cn(
|
||||
"text-5xl font-semibold tracking-tight",
|
||||
// Mobile styles
|
||||
isHighlighted ? "text-background" : "text-foreground",
|
||||
// Desktop styles
|
||||
"md:text-background",
|
||||
),
|
||||
description: cn(
|
||||
"mt-2 text-xl font-medium",
|
||||
// Mobile styles
|
||||
isHighlighted ? "text-background/70" : "text-foreground/70",
|
||||
// Desktop styles
|
||||
"md:text-background/70",
|
||||
),
|
||||
features: cn(
|
||||
"space-y-3 text-sm",
|
||||
// Mobile styles
|
||||
isHighlighted ? "text-background/70" : "text-foreground/70",
|
||||
// Desktop styles
|
||||
"md:text-background/70",
|
||||
),
|
||||
button: cn(
|
||||
"group border-foreground/20 relative w-full",
|
||||
// inset shadow
|
||||
"after:from-border after:via-border after:absolute after:inset-0 after:bg-linear-to-t after:to-transparent after:content-[''] after:group-hover:opacity-100",
|
||||
// Desktop styles
|
||||
"md:border-background/40 md:text-background md:bg-transparent",
|
||||
isHighlighted &&
|
||||
"md:bg-background md:text-primary hover:md:bg-background/90",
|
||||
),
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.card}>
|
||||
<h3 className={styles.title}>{tier.name}</h3>
|
||||
<div>
|
||||
<p className={styles.price}>{tier.price}</p>
|
||||
<p className={styles.description}>{tier.description}</p>
|
||||
</div>
|
||||
<ul className={styles.features}>
|
||||
{tier.features.map((feature) => (
|
||||
<li key={feature} className="flex items-center gap-2">
|
||||
<Check className="size-4 shrink-0" />
|
||||
<span>{feature}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<div className="flex flex-1 items-end">
|
||||
<Button
|
||||
asChild
|
||||
variant={isHighlighted ? "secondary" : "outline"}
|
||||
size="lg"
|
||||
className={styles.button}
|
||||
>
|
||||
<a href={tier.cta.href}>
|
||||
{tier.cta.text}
|
||||
<ChevronRight className="ml-1 size-4 transition-transform group-hover:translate-x-0.5" />
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
304
src/components/sections/pricing-table.tsx
Normal file
304
src/components/sections/pricing-table.tsx
Normal file
|
|
@ -0,0 +1,304 @@
|
|||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
import { Check, ChevronsUpDown } from "lucide-react";
|
||||
|
||||
import { Button } from "../ui/button";
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from "../ui/collapsible";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type PlanType = "free" | "startup" | "enterprise";
|
||||
|
||||
interface Plan {
|
||||
name: string;
|
||||
type: PlanType;
|
||||
button: {
|
||||
text: string;
|
||||
variant: "outline";
|
||||
href: string;
|
||||
};
|
||||
features: {
|
||||
[Category in "usage" | "features" | "support"]: {
|
||||
name: string;
|
||||
value: string | boolean;
|
||||
}[];
|
||||
};
|
||||
}
|
||||
|
||||
const plans: Plan[] = [
|
||||
{
|
||||
name: "Free",
|
||||
type: "free",
|
||||
button: {
|
||||
text: "Get started",
|
||||
variant: "outline",
|
||||
href: "/signup",
|
||||
},
|
||||
features: {
|
||||
usage: [
|
||||
{ name: "Members", value: "Unlimited" },
|
||||
{ name: "Transactions", value: "250" },
|
||||
{ name: "Teams", value: "2" },
|
||||
],
|
||||
features: [
|
||||
{ name: "Reporting", value: true },
|
||||
{ name: "Analytics", value: true },
|
||||
{ name: "Import and export", value: true },
|
||||
{ name: "Integrations", value: true },
|
||||
{ name: "Mainline AI", value: false },
|
||||
{ name: "Admin roles", value: false },
|
||||
{ name: "Audit log", value: false },
|
||||
],
|
||||
support: [
|
||||
{ name: "Priority Support", value: true },
|
||||
{ name: "Account Manager", value: false },
|
||||
{ name: "Uptime SLA", value: false },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Basic",
|
||||
type: "startup",
|
||||
button: {
|
||||
text: "Get started",
|
||||
variant: "outline",
|
||||
href: "/signup",
|
||||
},
|
||||
features: {
|
||||
usage: [
|
||||
{ name: "Members", value: "Unlimited" },
|
||||
{ name: "Transactions", value: "Unlimited" },
|
||||
{ name: "Teams", value: "Unlimited" },
|
||||
],
|
||||
features: [
|
||||
{ name: "Reporting", value: true },
|
||||
{ name: "Analytics", value: true },
|
||||
{ name: "Import and export", value: true },
|
||||
{ name: "Integrations", value: true },
|
||||
{ name: "Mainline AI", value: true },
|
||||
{ name: "Admin roles", value: false },
|
||||
{ name: "Audit log", value: false },
|
||||
],
|
||||
support: [
|
||||
{ name: "Priority Support", value: true },
|
||||
{ name: "Account Manager", value: false },
|
||||
{ name: "Uptime SLA", value: false },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Ent",
|
||||
type: "enterprise",
|
||||
button: {
|
||||
text: "Get a demo",
|
||||
variant: "outline",
|
||||
href: "/contact",
|
||||
},
|
||||
features: {
|
||||
usage: [
|
||||
{ name: "Members", value: "Unlimited" },
|
||||
{ name: "Transactions", value: "Unlimited" },
|
||||
{ name: "Teams", value: "Unlimited" },
|
||||
],
|
||||
features: [
|
||||
{ name: "Reporting", value: true },
|
||||
{ name: "Analytics", value: true },
|
||||
{ name: "Import and export", value: true },
|
||||
{ name: "Integrations", value: true },
|
||||
{ name: "Mainline AI", value: true },
|
||||
{ name: "Admin roles", value: false },
|
||||
{ name: "Audit log", value: false },
|
||||
],
|
||||
support: [
|
||||
{ name: "Priority Support", value: true },
|
||||
{ name: "Account Manager", value: true },
|
||||
{ name: "Uptime SLA", value: true },
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export default function PricingTable() {
|
||||
const [selectedPlan, setSelectedPlan] = useState(1);
|
||||
|
||||
return (
|
||||
<section className="py-28 lg:py-32">
|
||||
<div className="container font-medium">
|
||||
<MobilePricingTable
|
||||
selectedPlan={selectedPlan}
|
||||
onPlanChange={setSelectedPlan}
|
||||
/>
|
||||
<DesktopPricingTable />
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
const MobilePricingTable = ({
|
||||
selectedPlan,
|
||||
onPlanChange,
|
||||
}: {
|
||||
selectedPlan: number;
|
||||
onPlanChange: (index: number) => void;
|
||||
}) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const plan = plans[selectedPlan];
|
||||
|
||||
return (
|
||||
<div className="md:hidden">
|
||||
<Collapsible open={isOpen} onOpenChange={setIsOpen}>
|
||||
<div className="flex items-center justify-between border-b py-4">
|
||||
<CollapsibleTrigger className="flex items-center gap-2">
|
||||
<h3 className="text-2xl font-semibold">{plan.name}</h3>
|
||||
<ChevronsUpDown
|
||||
className={`size-5 transition-transform ${isOpen ? "rotate-180" : ""}`}
|
||||
/>
|
||||
</CollapsibleTrigger>
|
||||
<Button variant={plan.button.variant} className="w-fit">
|
||||
{plan.button.text}
|
||||
</Button>
|
||||
</div>
|
||||
<CollapsibleContent className="flex flex-col space-y-2 p-2">
|
||||
{plans.map(
|
||||
(p, index) =>
|
||||
index !== selectedPlan && (
|
||||
<Button
|
||||
size="lg"
|
||||
variant="secondary"
|
||||
key={index}
|
||||
onClick={() => {
|
||||
onPlanChange(index);
|
||||
setIsOpen(false);
|
||||
}}
|
||||
>
|
||||
{p.name}
|
||||
</Button>
|
||||
),
|
||||
)}
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
|
||||
{/* Features List */}
|
||||
<div className="mt-8">
|
||||
{Object.entries(plan.features).map(
|
||||
([category, features], sectionIndex) => (
|
||||
<div key={sectionIndex} className="mb-8 space-y-2">
|
||||
<h3 className="mb-4 text-lg font-semibold capitalize">
|
||||
{category}
|
||||
</h3>
|
||||
{features.map((feature, featureIndex) => (
|
||||
<div
|
||||
key={featureIndex}
|
||||
className="grid grid-cols-2 items-center gap-8"
|
||||
>
|
||||
<span className="border-b py-2">{feature.name}</span>
|
||||
<div className="flex items-center gap-1 border-b py-2">
|
||||
{typeof feature.value === "boolean" ? (
|
||||
feature.value ? (
|
||||
<Check className="size-5" />
|
||||
) : (
|
||||
<span className="size-5" />
|
||||
)
|
||||
) : (
|
||||
<div className="flex items-center gap-1">
|
||||
<Check className="size-5" />
|
||||
<span>{feature.value}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const DesktopPricingTable = () => {
|
||||
return (
|
||||
<div className="hidden md:grid md:grid-cols-4">
|
||||
<FeaturesColumn />
|
||||
{plans.map((plan, index) => (
|
||||
<PricingColumn
|
||||
key={plan.name}
|
||||
plan={plan}
|
||||
isHighlighted={index === 1}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const FeaturesColumn = () => (
|
||||
<div>
|
||||
<div className="h-[140px]" /> {/* Spacer for plan header alignment */}
|
||||
{Object.entries(plans[0].features).map(([category, features], index) => (
|
||||
<div key={index}>
|
||||
<h3 className="flex h-20 items-center text-lg font-semibold capitalize">
|
||||
{category}
|
||||
</h3>
|
||||
{features.map((feature, featureIndex) => (
|
||||
<div key={featureIndex} className="py-4">
|
||||
{feature.name}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
const PricingColumn = ({
|
||||
plan,
|
||||
isHighlighted,
|
||||
}: {
|
||||
plan: Plan;
|
||||
isHighlighted: boolean;
|
||||
}) => {
|
||||
const columnClass = cn("px-6", isHighlighted && "bg-card border rounded-xl");
|
||||
|
||||
return (
|
||||
<div className={columnClass}>
|
||||
{/* Plan Header */}
|
||||
<div className="py-8">
|
||||
<h3 className="mb-3 text-2xl font-semibold">{plan.name}</h3>
|
||||
<Button variant={plan.button.variant} asChild>
|
||||
<a href={plan.button.href}>{plan.button.text}</a>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Features */}
|
||||
{Object.entries(plan.features).map(([, features], sectionIndex) => (
|
||||
<div key={sectionIndex}>
|
||||
<div className="flex h-20 items-center"></div> {/* space */}
|
||||
{features.map((feature, featureIndex) => (
|
||||
<div
|
||||
key={featureIndex}
|
||||
className="flex items-center gap-1 border-b py-4"
|
||||
>
|
||||
{typeof feature.value === "boolean" ? (
|
||||
feature.value ? (
|
||||
<Check className="size-5" />
|
||||
) : (
|
||||
<span className="size-5" />
|
||||
)
|
||||
) : (
|
||||
<div className="flex items-center gap-1">
|
||||
<Check className="size-4" />
|
||||
<span>{feature.value}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
61
src/components/sections/signup-section.tsx
Normal file
61
src/components/sections/signup-section.tsx
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import { Card, CardContent, CardHeader } from "../ui/card";
|
||||
import { Input } from "../ui/input";
|
||||
import { Button } from "../ui/button";
|
||||
import { FcGoogle } from "react-icons/fc";
|
||||
|
||||
const SignupSection = () => {
|
||||
return (
|
||||
<section className="bg-sand-100 py-16 md:py-28 lg:py-32">
|
||||
<div className="container">
|
||||
<div className="flex flex-col gap-4">
|
||||
<Card className="mx-auto w-full max-w-sm">
|
||||
<CardHeader className="flex flex-col items-center space-y-0">
|
||||
<img
|
||||
src="/images/layout/logo.svg"
|
||||
alt="logo"
|
||||
width={94}
|
||||
height={18}
|
||||
className="mb-7 dark:invert"
|
||||
/>
|
||||
<p className="mb-2 text-2xl font-bold">Start your free trial</p>
|
||||
<p className="text-muted-foreground">
|
||||
Sign up in less than 2 minutes.
|
||||
</p>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid gap-4">
|
||||
<Input type="text" placeholder="Enter your name" required />
|
||||
<Input type="email" placeholder="Enter your email" required />
|
||||
<div>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="Enter your password"
|
||||
required
|
||||
/>
|
||||
<p className="text-muted-foreground mt-1 text-sm">
|
||||
Must be at least 8 characters.
|
||||
</p>
|
||||
</div>
|
||||
<Button type="submit" className="mt-2 w-full">
|
||||
Create an account
|
||||
</Button>
|
||||
<Button variant="outline" className="w-full">
|
||||
<FcGoogle className="mr-2 size-5" />
|
||||
Sign up with Google
|
||||
</Button>
|
||||
</div>
|
||||
<div className="text-muted-foreground mx-auto mt-8 flex justify-center gap-1 text-sm">
|
||||
<p>Already have an account?</p>
|
||||
<a href="/login" className="text-primary font-medium">
|
||||
Log in
|
||||
</a>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default SignupSection;
|
||||
148
src/components/sections/testimonials.tsx
Normal file
148
src/components/sections/testimonials.tsx
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
import { useEffect, useState } from "react";
|
||||
|
||||
import { PlusSigns } from "../icons/plus-signs";
|
||||
|
||||
import {
|
||||
Carousel,
|
||||
type CarouselApi,
|
||||
CarouselContent,
|
||||
CarouselItem,
|
||||
} from "@/components/ui/carousel";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const TESTIMONIALS = [
|
||||
{
|
||||
id: 1,
|
||||
quote:
|
||||
"Charter's platform delivers unmatched speed, a flexible account framework, and an API-first design. Their deep understanding of the ecosystem has been crucial for scaling our financial operations effectively.",
|
||||
author: "Henry Francis",
|
||||
role: "Founder, Ramp",
|
||||
logo: "/images/logos/ramp.svg",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
quote:
|
||||
"We have a deep understanding of the ecosystem, and Charter delivers unmatched speed, a flexible account framework, and an API-first design. These features are crucial for creating revenue that grows our office plants.",
|
||||
author: "David Chen",
|
||||
role: "Head of Engineering, Notion",
|
||||
logo: "/images/logos/notion.svg",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
quote:
|
||||
"Charter's platform has exceeded our expectations with its seamless integration process and exceptional support team. Their powerful tools and robust infrastructure have been instrumental in our scaling journey.",
|
||||
author: "Sarah Williams",
|
||||
role: "CTO, Mercury",
|
||||
logo: "/images/logos/mercury.svg",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
quote:
|
||||
"Charter has revolutionized how we manage our financial infrastructure effectively. Their API-first approach and robust platform capabilities have given us the flexibility to build custom solutions at scale seamlessly and efficiently.",
|
||||
author: "Michael Ross",
|
||||
role: "CEO, Raycast",
|
||||
logo: "/images/logos/raycast.svg",
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
quote:
|
||||
"The combination of Charter's flexible platform architecture and exceptional support team has been transformative for our operations. Their speed, reliability, and scalability are unmatched in the industry.",
|
||||
author: "Emily Chen",
|
||||
role: "Product Lead, Asana",
|
||||
logo: "/images/logos/asana.svg",
|
||||
},
|
||||
];
|
||||
|
||||
export default function Testimonials() {
|
||||
const [api, setApi] = useState<CarouselApi>();
|
||||
const [current, setCurrent] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (!api) {
|
||||
return;
|
||||
}
|
||||
|
||||
api.on("select", () => {
|
||||
setCurrent(api.selectedScrollSnap());
|
||||
});
|
||||
}, [api]);
|
||||
|
||||
return (
|
||||
<section
|
||||
className="relative py-16 md:py-28 lg:py-32"
|
||||
aria-label="Customer Testimonials"
|
||||
>
|
||||
<div className="container max-w-4xl">
|
||||
<Carousel
|
||||
opts={{
|
||||
align: "center",
|
||||
loop: true,
|
||||
}}
|
||||
setApi={setApi}
|
||||
>
|
||||
<CarouselContent>
|
||||
{TESTIMONIALS.map((testimonial) => (
|
||||
<CarouselItem
|
||||
key={testimonial.id}
|
||||
className="flex cursor-grab flex-col gap-6 lg:gap-8"
|
||||
>
|
||||
<blockquote className="pointer-events-none select-none text-balance font-sans text-2xl font-semibold leading-tight tracking-tight md:text-3xl lg:text-4xl">
|
||||
{testimonial.quote}
|
||||
</blockquote>
|
||||
<div className="flex items-center gap-4">
|
||||
{testimonial.logo && (
|
||||
<div className="relative h-8 w-24">
|
||||
<img
|
||||
src={testimonial.logo}
|
||||
alt={`${testimonial.author}'s company logo`}
|
||||
className="size-full object-contain dark:invert"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="bg-border h-8 w-[1px]" aria-hidden="true" />
|
||||
<div>
|
||||
<cite className="font-semibold not-italic">
|
||||
{testimonial.author}
|
||||
</cite>
|
||||
<p className="text-muted-foreground text-sm font-medium">
|
||||
{testimonial.role}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CarouselItem>
|
||||
))}
|
||||
</CarouselContent>
|
||||
</Carousel>
|
||||
|
||||
<div
|
||||
className="mt-10 flex gap-2 lg:mt-16"
|
||||
role="tablist"
|
||||
aria-label="Testimonials navigation"
|
||||
>
|
||||
{TESTIMONIALS.map((_, index) => (
|
||||
<button
|
||||
key={index}
|
||||
className={cn(
|
||||
"size-4 cursor-pointer rounded-full transition-colors duration-300",
|
||||
index === current
|
||||
? "bg-muted-foreground"
|
||||
: "bg-muted-foreground/20 hover:bg-muted-foreground/50",
|
||||
)}
|
||||
onClick={() => api?.scrollTo(index)}
|
||||
aria-label={`Go to testimonial ${index + 1}`}
|
||||
aria-selected={index === current}
|
||||
role="tab"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="absolute inset-x-0 top-16 isolate z-[-1] h-[300px] md:top-28 lg:top-32">
|
||||
<div className="from-background via-background/40 absolute inset-x-0 bottom-0 z-10 h-40 bg-gradient-to-t to-transparent" />
|
||||
<div className="from-background via-background/40 absolute inset-x-0 top-0 z-10 h-40 bg-gradient-to-b to-transparent" />
|
||||
<PlusSigns className="text-foreground/[0.05] size-full" />
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
157
src/components/sections/why-charter.tsx
Normal file
157
src/components/sections/why-charter.tsx
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
import { PlusSigns } from "../icons/plus-signs";
|
||||
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const items = [
|
||||
{
|
||||
title: (
|
||||
<>
|
||||
Unparalleled
|
||||
<br />
|
||||
VAR flexibility
|
||||
</>
|
||||
),
|
||||
description: [
|
||||
"VAR is often known for its lengthy processing times. At Charter, we focus on achieving the fastest VAR transfers—often completed within hours.",
|
||||
"Unlike traditional banks and middleware, we connect directly with the Federal Reserve to ensure the fastest, most transparent transfers possible.",
|
||||
],
|
||||
image: {
|
||||
src: "/images/homepage/why-charter/1.webp",
|
||||
alt: "Code snippet",
|
||||
type: "fill",
|
||||
},
|
||||
className:
|
||||
"flex flex-col pl-6 py-6 overflow-hidden md:col-span-3 md:flex-row gap-6 md:gap-12",
|
||||
headerClassName: "flex-2 p-0",
|
||||
contentClassName:
|
||||
"relative h-[320px] w-full p-0 self-center overflow-hidden rounded-l-xl border md:flex-1",
|
||||
},
|
||||
{
|
||||
title: "Unparalleled VAR flexibility",
|
||||
description: [
|
||||
"VAR has a reputation for taking too long. At Charter, we optimise for the fastest VAR transfers possible — often in a matter of hours.",
|
||||
"Unlike legacy banks and middleware providers, we have a direct connection to the Federal Reserve to facilitate the quickest transfers.",
|
||||
],
|
||||
image: {
|
||||
src: "/images/homepage/why-charter/2.svg",
|
||||
alt: "VAR Process Flow",
|
||||
width: 283,
|
||||
height: 45,
|
||||
},
|
||||
className: "md:col-span-2 flex flex-col justify-center",
|
||||
contentClassName:
|
||||
"flex items-center justify-center p-6 max-md:mt-4 max-md:mb-8",
|
||||
imagePosition: "content",
|
||||
},
|
||||
{
|
||||
title: "Unparalleled VAR flexibility",
|
||||
description: [
|
||||
"VAR has a reputation for taking too long. At Charter, we optimise for the fastest VAR transfers possible — often in a matter of hours.",
|
||||
],
|
||||
image: {
|
||||
src: "/images/homepage/why-charter/3.svg",
|
||||
alt: "VAR Process Diagram",
|
||||
width: 283,
|
||||
height: 45,
|
||||
},
|
||||
className: "md:col-span-2",
|
||||
headerClassName: "h-full",
|
||||
imagePosition: "header",
|
||||
},
|
||||
{
|
||||
title: "Unparalleled VAR flexibility",
|
||||
description: [
|
||||
"Unlike traditional banks and middleware, we connect directly with the Federal Reserve to ensure the fastest, most transparent transfers possible.",
|
||||
],
|
||||
image: {
|
||||
src: "/images/homepage/code-snippet.webp",
|
||||
alt: "Code snippet",
|
||||
type: "fill",
|
||||
},
|
||||
className: "overflow-hidden md:col-span-3 ",
|
||||
headerClassName: "",
|
||||
contentClassName:
|
||||
"relative h-[242px] mt-2 p-0 ml-8 w-full md:max-w-[400px] lg:max-w-[500px] overflow-hidden md:mx-auto shadow-xl rounded-t-2xl",
|
||||
},
|
||||
];
|
||||
|
||||
const WhyCharter = () => {
|
||||
return (
|
||||
<section
|
||||
id="why-charter"
|
||||
className="container relative py-16 md:py-28 lg:py-32"
|
||||
>
|
||||
<h3 className="mini-title">WHY CHARTER?</h3>
|
||||
|
||||
<div className="relative z-10 mt-8 grid grid-cols-1 gap-6 md:grid-cols-5">
|
||||
{items.map((item, index) => (
|
||||
<Card
|
||||
key={index}
|
||||
className={cn("col-span-1 shadow-xl", item.className)}
|
||||
>
|
||||
<CardHeader className={item.headerClassName}>
|
||||
{item.imagePosition === "header" && (
|
||||
<img
|
||||
src={item.image.src}
|
||||
alt={item.image.alt}
|
||||
width={item.image.width}
|
||||
height={item.image.height}
|
||||
className="flex-1 self-center max-md:mb-8 max-md:mt-4 dark:[filter:brightness(0)_saturate(100%)_invert(100%)]"
|
||||
/>
|
||||
)}
|
||||
<CardTitle className="text-3xl">{item.title}</CardTitle>
|
||||
{item.description.map((desc, i) => (
|
||||
<CardDescription
|
||||
key={i}
|
||||
className="mt-2 text-base font-medium leading-snug"
|
||||
>
|
||||
{desc}
|
||||
</CardDescription>
|
||||
))}
|
||||
</CardHeader>
|
||||
<CardContent className={item.contentClassName}>
|
||||
{item.image.type === "fill" ? (
|
||||
<img
|
||||
src={item.image.src}
|
||||
alt={item.image.alt}
|
||||
className="size-full object-cover object-left-top"
|
||||
/>
|
||||
) : (
|
||||
item.imagePosition === "content" && (
|
||||
<img
|
||||
src={item.image.src}
|
||||
alt={item.image.alt}
|
||||
width={item.image.width}
|
||||
height={item.image.height}
|
||||
className="self-center dark:[filter:brightness(0)_saturate(100%)_invert(100%)]"
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Background decoration */}
|
||||
<>
|
||||
<div className="absolute inset-0 isolate will-change-transform">
|
||||
<div className="bg-primary-gradient/28 absolute top-1/2 size-[700px] -translate-y-1/2 rounded-full blur-[300px]" />
|
||||
<div className="bg-secondary-gradient/16 absolute right-0 top-1/2 size-[700px] -translate-y-1/2 -rotate-12 rounded-full blur-[300px]" />
|
||||
<div className="bg-tertiary-gradient/6 absolute bottom-1/4 right-20 z-[1] h-[500px] w-[800px] -rotate-12 rounded-full blur-[100px] md:bottom-10" />
|
||||
</div>
|
||||
<div className="absolute -inset-x-20 top-0 [mask-image:radial-gradient(circle_at_center,black_0%,black_20%,transparent_85%)]">
|
||||
<PlusSigns className="text-foreground/[0.075] h-full w-full" />
|
||||
</div>
|
||||
</>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default WhyCharter;
|
||||
58
src/components/ui/accordion.tsx
Normal file
58
src/components/ui/accordion.tsx
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
import * as AccordionPrimitive from '@radix-ui/react-accordion';
|
||||
import { ChevronDown } from 'lucide-react';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const Accordion = AccordionPrimitive.Root;
|
||||
|
||||
const AccordionItem = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AccordionPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn('border-b', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
AccordionItem.displayName = 'AccordionItem';
|
||||
|
||||
const AccordionTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<AccordionPrimitive.Header className="flex">
|
||||
<AccordionPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'flex flex-1 items-center justify-between py-4 text-left text-sm font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronDown className="text-muted-foreground size-4 shrink-0 transition-transform duration-200" />
|
||||
</AccordionPrimitive.Trigger>
|
||||
</AccordionPrimitive.Header>
|
||||
));
|
||||
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
|
||||
|
||||
const AccordionContent = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<AccordionPrimitive.Content
|
||||
ref={ref}
|
||||
className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
|
||||
{...props}
|
||||
>
|
||||
<div className={cn('pt-0 pb-4', className)}>{children}</div>
|
||||
</AccordionPrimitive.Content>
|
||||
));
|
||||
AccordionContent.displayName = AccordionPrimitive.Content.displayName;
|
||||
|
||||
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
|
||||
51
src/components/ui/avatar.tsx
Normal file
51
src/components/ui/avatar.tsx
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import * as React from "react"
|
||||
import * as AvatarPrimitive from "@radix-ui/react-avatar"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Avatar({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AvatarPrimitive.Root>) {
|
||||
return (
|
||||
<AvatarPrimitive.Root
|
||||
data-slot="avatar"
|
||||
className={cn(
|
||||
"relative flex size-8 shrink-0 overflow-hidden rounded-full",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AvatarImage({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AvatarPrimitive.Image>) {
|
||||
return (
|
||||
<AvatarPrimitive.Image
|
||||
data-slot="avatar-image"
|
||||
className={cn("aspect-square size-full", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AvatarFallback({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
|
||||
return (
|
||||
<AvatarPrimitive.Fallback
|
||||
data-slot="avatar-fallback"
|
||||
className={cn(
|
||||
"bg-muted flex size-full items-center justify-center rounded-full",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Avatar, AvatarImage, AvatarFallback }
|
||||
46
src/components/ui/badge.tsx
Normal file
46
src/components/ui/badge.tsx
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const badgeVariants = cva(
|
||||
"inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
|
||||
secondary:
|
||||
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
|
||||
destructive:
|
||||
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40",
|
||||
outline:
|
||||
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function Badge({
|
||||
className,
|
||||
variant,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"span"> &
|
||||
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
||||
const Comp = asChild ? Slot : "span"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="badge"
|
||||
className={cn(badgeVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants }
|
||||
58
src/components/ui/button.tsx
Normal file
58
src/components/ui/button.tsx
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import * as React from 'react';
|
||||
|
||||
import { Slot } from '@radix-ui/react-slot';
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const buttonVariants = cva(
|
||||
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-semibold transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
'bg-primary text-primary-foreground shadow hover:bg-primary/90',
|
||||
destructive:
|
||||
'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
|
||||
outline:
|
||||
'border border-border bg-background shadow-sm hover:bg-accent hover:text-accent-foreground',
|
||||
secondary:
|
||||
'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',
|
||||
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
||||
link: 'text-primary underline-offset-4 hover:underline',
|
||||
},
|
||||
size: {
|
||||
default: 'h-9 px-4 py-2',
|
||||
sm: 'h-8 rounded-md px-3 text-xs',
|
||||
lg: 'h-10 rounded-md px-8',
|
||||
icon: 'h-9 w-9',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
size: 'default',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean;
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : 'button';
|
||||
return (
|
||||
<Comp
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
Button.displayName = 'Button';
|
||||
|
||||
export { Button, buttonVariants };
|
||||
83
src/components/ui/card.tsx
Normal file
83
src/components/ui/card.tsx
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
import * as React from 'react';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const Card = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'bg-card text-card-foreground rounded-xl border shadow-sm',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
Card.displayName = 'Card';
|
||||
|
||||
const CardHeader = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn('flex flex-col space-y-1.5 p-6', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CardHeader.displayName = 'CardHeader';
|
||||
|
||||
const CardTitle = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn('leading-none font-semibold tracking-tight', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CardTitle.displayName = 'CardTitle';
|
||||
|
||||
const CardDescription = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn('text-muted-foreground text-sm', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CardDescription.displayName = 'CardDescription';
|
||||
|
||||
const CardContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn('p-6 pt-0', className)} {...props} />
|
||||
));
|
||||
CardContent.displayName = 'CardContent';
|
||||
|
||||
const CardFooter = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn('flex items-center p-6 pt-0', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CardFooter.displayName = 'CardFooter';
|
||||
|
||||
export {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardFooter,
|
||||
CardTitle,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
};
|
||||
262
src/components/ui/carousel.tsx
Normal file
262
src/components/ui/carousel.tsx
Normal file
|
|
@ -0,0 +1,262 @@
|
|||
|
||||
import * as React from 'react';
|
||||
|
||||
import useEmblaCarousel, {
|
||||
type UseEmblaCarouselType,
|
||||
} from 'embla-carousel-react';
|
||||
import { ArrowLeft, ArrowRight } from 'lucide-react';
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
type CarouselApi = UseEmblaCarouselType[1];
|
||||
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>;
|
||||
type CarouselOptions = UseCarouselParameters[0];
|
||||
type CarouselPlugin = UseCarouselParameters[1];
|
||||
|
||||
type CarouselProps = {
|
||||
opts?: CarouselOptions;
|
||||
plugins?: CarouselPlugin;
|
||||
orientation?: 'horizontal' | 'vertical';
|
||||
setApi?: (api: CarouselApi) => void;
|
||||
};
|
||||
|
||||
type CarouselContextProps = {
|
||||
carouselRef: ReturnType<typeof useEmblaCarousel>[0];
|
||||
api: ReturnType<typeof useEmblaCarousel>[1];
|
||||
scrollPrev: () => void;
|
||||
scrollNext: () => void;
|
||||
canScrollPrev: boolean;
|
||||
canScrollNext: boolean;
|
||||
} & CarouselProps;
|
||||
|
||||
const CarouselContext = React.createContext<CarouselContextProps | null>(null);
|
||||
|
||||
function useCarousel() {
|
||||
const context = React.useContext(CarouselContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error('useCarousel must be used within a <Carousel />');
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
const Carousel = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement> & CarouselProps
|
||||
>(
|
||||
(
|
||||
{
|
||||
orientation = 'horizontal',
|
||||
opts,
|
||||
setApi,
|
||||
plugins,
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const [carouselRef, api] = useEmblaCarousel(
|
||||
{
|
||||
...opts,
|
||||
axis: orientation === 'horizontal' ? 'x' : 'y',
|
||||
},
|
||||
plugins,
|
||||
);
|
||||
const [canScrollPrev, setCanScrollPrev] = React.useState(false);
|
||||
const [canScrollNext, setCanScrollNext] = React.useState(false);
|
||||
|
||||
const onSelect = React.useCallback((api: CarouselApi) => {
|
||||
if (!api) {
|
||||
return;
|
||||
}
|
||||
|
||||
setCanScrollPrev(api.canScrollPrev());
|
||||
setCanScrollNext(api.canScrollNext());
|
||||
}, []);
|
||||
|
||||
const scrollPrev = React.useCallback(() => {
|
||||
api?.scrollPrev();
|
||||
}, [api]);
|
||||
|
||||
const scrollNext = React.useCallback(() => {
|
||||
api?.scrollNext();
|
||||
}, [api]);
|
||||
|
||||
const handleKeyDown = React.useCallback(
|
||||
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
if (event.key === 'ArrowLeft') {
|
||||
event.preventDefault();
|
||||
scrollPrev();
|
||||
} else if (event.key === 'ArrowRight') {
|
||||
event.preventDefault();
|
||||
scrollNext();
|
||||
}
|
||||
},
|
||||
[scrollPrev, scrollNext],
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!api || !setApi) {
|
||||
return;
|
||||
}
|
||||
|
||||
setApi(api);
|
||||
}, [api, setApi]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!api) {
|
||||
return;
|
||||
}
|
||||
|
||||
onSelect(api);
|
||||
api.on('reInit', onSelect);
|
||||
api.on('select', onSelect);
|
||||
|
||||
return () => {
|
||||
api?.off('select', onSelect);
|
||||
};
|
||||
}, [api, onSelect]);
|
||||
|
||||
return (
|
||||
<CarouselContext.Provider
|
||||
value={{
|
||||
carouselRef,
|
||||
api: api,
|
||||
opts,
|
||||
orientation:
|
||||
orientation || (opts?.axis === 'y' ? 'vertical' : 'horizontal'),
|
||||
scrollPrev,
|
||||
scrollNext,
|
||||
canScrollPrev,
|
||||
canScrollNext,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
ref={ref}
|
||||
onKeyDownCapture={handleKeyDown}
|
||||
className={cn('relative', className)}
|
||||
role="region"
|
||||
aria-roledescription="carousel"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</CarouselContext.Provider>
|
||||
);
|
||||
},
|
||||
);
|
||||
Carousel.displayName = 'Carousel';
|
||||
|
||||
const CarouselContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const { carouselRef, orientation } = useCarousel();
|
||||
|
||||
return (
|
||||
<div ref={carouselRef} className="overflow-hidden">
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'flex',
|
||||
orientation === 'horizontal' ? '-ml-4' : '-mt-4 flex-col',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
CarouselContent.displayName = 'CarouselContent';
|
||||
|
||||
const CarouselItem = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const { orientation } = useCarousel();
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
role="group"
|
||||
aria-roledescription="slide"
|
||||
className={cn(
|
||||
'min-w-0 shrink-0 grow-0 basis-full',
|
||||
orientation === 'horizontal' ? 'pl-4' : 'pt-4',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
CarouselItem.displayName = 'CarouselItem';
|
||||
|
||||
const CarouselPrevious = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
React.ComponentProps<typeof Button>
|
||||
>(({ className, variant = 'outline', size = 'icon', ...props }, ref) => {
|
||||
const { orientation, scrollPrev, canScrollPrev } = useCarousel();
|
||||
|
||||
return (
|
||||
<Button
|
||||
ref={ref}
|
||||
variant={variant}
|
||||
size={size}
|
||||
className={cn(
|
||||
'absolute h-8 w-8 rounded-full',
|
||||
orientation === 'horizontal'
|
||||
? 'top-1/2 -left-12 -translate-y-1/2'
|
||||
: '-top-12 left-1/2 -translate-x-1/2 rotate-90',
|
||||
className,
|
||||
)}
|
||||
disabled={!canScrollPrev}
|
||||
onClick={scrollPrev}
|
||||
{...props}
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
<span className="sr-only">Previous slide</span>
|
||||
</Button>
|
||||
);
|
||||
});
|
||||
CarouselPrevious.displayName = 'CarouselPrevious';
|
||||
|
||||
const CarouselNext = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
React.ComponentProps<typeof Button>
|
||||
>(({ className, variant = 'outline', size = 'icon', ...props }, ref) => {
|
||||
const { orientation, scrollNext, canScrollNext } = useCarousel();
|
||||
|
||||
return (
|
||||
<Button
|
||||
ref={ref}
|
||||
variant={variant}
|
||||
size={size}
|
||||
className={cn(
|
||||
'absolute h-8 w-8 rounded-full',
|
||||
orientation === 'horizontal'
|
||||
? 'top-1/2 -right-12 -translate-y-1/2'
|
||||
: '-bottom-12 left-1/2 -translate-x-1/2 rotate-90',
|
||||
className,
|
||||
)}
|
||||
disabled={!canScrollNext}
|
||||
onClick={scrollNext}
|
||||
{...props}
|
||||
>
|
||||
<ArrowRight className="h-4 w-4" />
|
||||
<span className="sr-only">Next slide</span>
|
||||
</Button>
|
||||
);
|
||||
});
|
||||
CarouselNext.displayName = 'CarouselNext';
|
||||
|
||||
export {
|
||||
type CarouselApi,
|
||||
Carousel,
|
||||
CarouselContent,
|
||||
CarouselItem,
|
||||
CarouselPrevious,
|
||||
CarouselNext,
|
||||
};
|
||||
31
src/components/ui/checkbox.tsx
Normal file
31
src/components/ui/checkbox.tsx
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
|
||||
import { Check } from 'lucide-react';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const Checkbox = React.forwardRef<
|
||||
React.ElementRef<typeof CheckboxPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CheckboxPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'peer border-primary focus-visible:ring-ring data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground h-4 w-4 shrink-0 rounded-xs border shadow-sm focus-visible:ring-1 focus-visible:outline-hidden disabled:cursor-not-allowed disabled:opacity-50',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<CheckboxPrimitive.Indicator
|
||||
className={cn('flex items-center justify-center text-current')}
|
||||
>
|
||||
<Check className="h-4 w-4" />
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
));
|
||||
Checkbox.displayName = CheckboxPrimitive.Root.displayName;
|
||||
|
||||
export { Checkbox };
|
||||
11
src/components/ui/collapsible.tsx
Normal file
11
src/components/ui/collapsible.tsx
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
'use client';
|
||||
|
||||
import * as CollapsiblePrimitive from '@radix-ui/react-collapsible';
|
||||
|
||||
const Collapsible = CollapsiblePrimitive.Root;
|
||||
|
||||
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger;
|
||||
|
||||
const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent;
|
||||
|
||||
export { Collapsible, CollapsibleTrigger, CollapsibleContent };
|
||||
22
src/components/ui/input.tsx
Normal file
22
src/components/ui/input.tsx
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import * as React from 'react';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<'input'>>(
|
||||
({ className, type, ...props }, ref) => {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
'border-border file:text-foreground placeholder:text-muted-foreground focus-visible:ring-ring bg-card flex h-11 w-full rounded-md border px-3 py-1 text-base shadow-xs transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:ring-1 focus-visible:outline-hidden disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
Input.displayName = 'Input';
|
||||
|
||||
export { Input };
|
||||
27
src/components/ui/label.tsx
Normal file
27
src/components/ui/label.tsx
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
import * as LabelPrimitive from '@radix-ui/react-label';
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const labelVariants = cva(
|
||||
'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
|
||||
);
|
||||
|
||||
const Label = React.forwardRef<
|
||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
|
||||
VariantProps<typeof labelVariants>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<LabelPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(labelVariants(), className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
Label.displayName = LabelPrimitive.Root.displayName;
|
||||
|
||||
export { Label };
|
||||
129
src/components/ui/navigation-menu.tsx
Normal file
129
src/components/ui/navigation-menu.tsx
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
import * as React from 'react';
|
||||
|
||||
import * as NavigationMenuPrimitive from '@radix-ui/react-navigation-menu';
|
||||
import { cva } from 'class-variance-authority';
|
||||
import { ChevronDown } from 'lucide-react';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const NavigationMenu = React.forwardRef<
|
||||
React.ElementRef<typeof NavigationMenuPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Root>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<NavigationMenuPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'relative z-10 flex max-w-max flex-1 items-center justify-center',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<NavigationMenuViewport />
|
||||
</NavigationMenuPrimitive.Root>
|
||||
));
|
||||
NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName;
|
||||
|
||||
const NavigationMenuList = React.forwardRef<
|
||||
React.ElementRef<typeof NavigationMenuPrimitive.List>,
|
||||
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.List>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<NavigationMenuPrimitive.List
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'group flex flex-1 list-none items-center justify-center space-x-1',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName;
|
||||
|
||||
const NavigationMenuItem = NavigationMenuPrimitive.Item;
|
||||
|
||||
const navigationMenuTriggerStyle = cva(
|
||||
'group inline-flex h-9 w-max items-center justify-center rounded-md bg-background p-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-hidden disabled:pointer-events-none disabled:opacity-50 data-active:bg-accent/50 data-[state=open]:bg-accent/50',
|
||||
);
|
||||
|
||||
const NavigationMenuTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof NavigationMenuPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Trigger>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<NavigationMenuPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(navigationMenuTriggerStyle(), 'group', className)}
|
||||
{...props}
|
||||
>
|
||||
{children}{' '}
|
||||
<ChevronDown
|
||||
className="relative top-px ml-1 size-3 transition duration-300 group-data-[state=open]:rotate-180"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</NavigationMenuPrimitive.Trigger>
|
||||
));
|
||||
NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName;
|
||||
|
||||
const NavigationMenuContent = React.forwardRef<
|
||||
React.ElementRef<typeof NavigationMenuPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Content>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<NavigationMenuPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 top-0 left-0 w-full md:absolute md:w-auto',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName;
|
||||
|
||||
const NavigationMenuLink = NavigationMenuPrimitive.Link;
|
||||
|
||||
const NavigationMenuViewport = React.forwardRef<
|
||||
React.ElementRef<typeof NavigationMenuPrimitive.Viewport>,
|
||||
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Viewport>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div className={cn('absolute top-full left-0 flex justify-center')}>
|
||||
<NavigationMenuPrimitive.Viewport
|
||||
className={cn(
|
||||
'origin-top-center bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border shadow-sm md:w-[var(--radix-navigation-menu-viewport-width)]',
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
));
|
||||
NavigationMenuViewport.displayName =
|
||||
NavigationMenuPrimitive.Viewport.displayName;
|
||||
|
||||
const NavigationMenuIndicator = React.forwardRef<
|
||||
React.ElementRef<typeof NavigationMenuPrimitive.Indicator>,
|
||||
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Indicator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<NavigationMenuPrimitive.Indicator
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in top-full z-1 flex h-1.5 items-end justify-center overflow-hidden',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className="bg-border relative top-[60%] size-2 rotate-45 rounded-tl-sm shadow-md" />
|
||||
</NavigationMenuPrimitive.Indicator>
|
||||
));
|
||||
NavigationMenuIndicator.displayName =
|
||||
NavigationMenuPrimitive.Indicator.displayName;
|
||||
|
||||
export {
|
||||
NavigationMenu,
|
||||
NavigationMenuContent,
|
||||
NavigationMenuIndicator,
|
||||
NavigationMenuItem,
|
||||
NavigationMenuLink,
|
||||
NavigationMenuList,
|
||||
NavigationMenuTrigger,
|
||||
navigationMenuTriggerStyle,
|
||||
NavigationMenuViewport,
|
||||
};
|
||||
26
src/components/ui/separator.tsx
Normal file
26
src/components/ui/separator.tsx
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import * as React from "react"
|
||||
import * as SeparatorPrimitive from "@radix-ui/react-separator"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Separator({
|
||||
className,
|
||||
orientation = "horizontal",
|
||||
decorative = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
|
||||
return (
|
||||
<SeparatorPrimitive.Root
|
||||
data-slot="separator-root"
|
||||
decorative={decorative}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Separator }
|
||||
56
src/components/ui/tabs.tsx
Normal file
56
src/components/ui/tabs.tsx
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
import * as TabsPrimitive from '@radix-ui/react-tabs';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const Tabs = TabsPrimitive.Root;
|
||||
|
||||
const TabsList = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.List>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.List
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'bg-muted text-muted-foreground inline-flex h-9 items-center justify-center rounded-lg p-1',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TabsList.displayName = TabsPrimitive.List.displayName;
|
||||
|
||||
const TabsTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'ring-offset-background focus-visible:ring-ring data-[state=active]:bg-background data-[state=active]:text-foreground inline-flex items-center justify-center rounded-md px-3 py-1 text-sm font-medium whitespace-nowrap transition-all focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
|
||||
|
||||
const TabsContent = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'ring-offset-background focus-visible:ring-ring mt-2 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-hidden',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TabsContent.displayName = TabsPrimitive.Content.displayName;
|
||||
|
||||
export { Tabs, TabsList, TabsTrigger, TabsContent };
|
||||
22
src/components/ui/textarea.tsx
Normal file
22
src/components/ui/textarea.tsx
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import * as React from 'react';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const Textarea = React.forwardRef<
|
||||
HTMLTextAreaElement,
|
||||
React.ComponentProps<'textarea'>
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<textarea
|
||||
className={cn(
|
||||
'border-border placeholder:text-muted-foreground focus-visible:ring-ring bg-card flex min-h-[60px] w-full rounded-md border px-3 py-2 text-base shadow-xs focus-visible:ring-1 focus-visible:outline-hidden disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
Textarea.displayName = 'Textarea';
|
||||
|
||||
export { Textarea };
|
||||
62
src/consts.ts
Normal file
62
src/consts.ts
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
// Place any global data in this file.
|
||||
// You can import this data from anywhere in your site by using the `import` keyword.
|
||||
|
||||
export const SITE_TITLE = "charter - Modern Astro Template";
|
||||
export const SITE_DESCRIPTION =
|
||||
"A modern, fully featured Astro template built with Shadcn/UI, TailwindCSS and TypeScript, perfect for your next web application.";
|
||||
|
||||
export const SITE_METADATA = {
|
||||
title: {
|
||||
default: SITE_TITLE,
|
||||
template: "%s | charter",
|
||||
},
|
||||
description: SITE_DESCRIPTION,
|
||||
keywords: [
|
||||
"Astro",
|
||||
"React",
|
||||
"JavaScript",
|
||||
"TypeScript",
|
||||
"TailwindCSS",
|
||||
"Template",
|
||||
"Shadcn/UI",
|
||||
"Web Development",
|
||||
],
|
||||
authors: [{ name: "charter Team" }],
|
||||
creator: "charter Team",
|
||||
publisher: "charter",
|
||||
robots: {
|
||||
index: true,
|
||||
follow: true,
|
||||
},
|
||||
icons: {
|
||||
icon: [
|
||||
{ url: "/favicon/favicon.ico", sizes: "48x48" },
|
||||
{ url: "/favicon/favicon.svg", type: "image/svg+xml" },
|
||||
{ url: "/favicon/favicon-96x96.png", sizes: "96x96", type: "image/png" },
|
||||
{ url: "/favicon/favicon.svg", type: "image/svg+xml" },
|
||||
{ url: "/favicon/favicon.ico" },
|
||||
],
|
||||
apple: [{ url: "/favicon/apple-touch-icon.png", sizes: "180x180" }],
|
||||
shortcut: [{ url: "/favicon/favicon.ico" }],
|
||||
},
|
||||
openGraph: {
|
||||
title: SITE_TITLE,
|
||||
description: SITE_DESCRIPTION,
|
||||
siteName: "charter",
|
||||
images: [
|
||||
{
|
||||
url: "/og-image.jpg",
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: "charter - Modern Astro Template",
|
||||
},
|
||||
],
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
title: SITE_TITLE,
|
||||
description: SITE_DESCRIPTION,
|
||||
images: ["/og-image.jpg"],
|
||||
creator: "@charter",
|
||||
},
|
||||
};
|
||||
40
src/content/art/algorithmic-sketches.md
Normal file
40
src/content/art/algorithmic-sketches.md
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
---
|
||||
title: "Algorithmic Sketches: Visualizing Data Structures"
|
||||
description: "Hand-drawn illustrations of algorithms and data structures - making the abstract tangible."
|
||||
pubDate: "Jan 25 2024"
|
||||
image: "https://images.unsplash.com/photo-1581337204873-ef36aa186caa?w=400&auto=format&fit=crop&q=60"
|
||||
authorImage: "/avatar/avatar1.png"
|
||||
authorName: "Neeldhara"
|
||||
---
|
||||
|
||||
# Algorithmic Sketches: Visualizing Data Structures
|
||||
|
||||
Sometimes the best way to understand an algorithm is to draw it.
|
||||
|
||||
## Today's Sketch: Red-Black Trees
|
||||
|
||||

|
||||
|
||||
The rotation operation finally clicked when I drew it step-by-step. Each node's journey through the rotation becomes a story.
|
||||
|
||||
## The Process
|
||||
|
||||
1. Start with pencil - mistakes are part of learning
|
||||
2. Trace the algorithm's execution
|
||||
3. Add color to highlight invariants
|
||||
4. Annotate with key insights
|
||||
|
||||
## Why Drawing Helps
|
||||
|
||||
- Forces you to slow down and really see the structure
|
||||
- Reveals patterns that code obscures
|
||||
- Creates memorable mental models
|
||||
- Makes teaching more engaging
|
||||
|
||||
## This Week's Challenge
|
||||
|
||||
Draw your own version of quicksort partitioning. Share it and let's learn from each other's visualizations.
|
||||
|
||||
## The Art in Computer Science
|
||||
|
||||
Algorithms are beautiful. Drawing them reminds us that computer science is as much art as it is science.
|
||||
41
src/content/art/generative-art-p5js.md
Normal file
41
src/content/art/generative-art-p5js.md
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
---
|
||||
title: "Generative Art with p5.js: Creating Beauty from Mathematics"
|
||||
description: "Exploring the intersection of code and creativity through generative art experiments."
|
||||
pubDate: "Feb 18 2024"
|
||||
image: "https://images.unsplash.com/photo-1561070791-2526d30994b5?w=400&auto=format&fit=crop&q=60"
|
||||
authorImage: "/avatar/avatar1.png"
|
||||
authorName: "Neeldhara"
|
||||
---
|
||||
|
||||
# Generative Art with p5.js: Creating Beauty from Mathematics
|
||||
|
||||
Code becomes canvas when mathematics meets creativity.
|
||||
|
||||
## Today's Creation: Perlin Noise Flows
|
||||
|
||||
```javascript
|
||||
for (let particle of particles) {
|
||||
let angle = noise(particle.x * 0.01, particle.y * 0.01, frameCount * 0.001) * TWO_PI;
|
||||
particle.velocity = p5.Vector.fromAngle(angle);
|
||||
particle.update();
|
||||
particle.draw();
|
||||
}
|
||||
```
|
||||
|
||||
## The Magic of Randomness
|
||||
|
||||
Controlled randomness creates organic patterns. Perlin noise gives us randomness with continuity - nature's own algorithm.
|
||||
|
||||
## Parameters as Paintbrushes
|
||||
|
||||
- Noise scale: Changes pattern density
|
||||
- Particle count: Affects texture
|
||||
- Color mapping: Sets the mood
|
||||
|
||||
## The Joy of Accidents
|
||||
|
||||
The best discoveries come from "mistakes" - a typo that creates unexpected beauty, a parameter pushed to extremes.
|
||||
|
||||
## Share Your Creations
|
||||
|
||||
Art is meant to be shared. Post your generative experiments and let's inspire each other.
|
||||
34
src/content/bfs/conference-highlights-2024.md
Normal file
34
src/content/bfs/conference-highlights-2024.md
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
---
|
||||
title: "SODA 2024: Algorithmic Breakthroughs"
|
||||
description: "A roundup of fascinating algorithmic results from SODA 2024, focusing on graph algorithms and optimization techniques."
|
||||
pubDate: "Jan 15 2024"
|
||||
image: "https://images.unsplash.com/photo-1509228468518-180dd4864904?w=400&auto=format&fit=crop&q=60"
|
||||
authorImage: "/avatar/avatar1.png"
|
||||
authorName: "Neeldhara"
|
||||
---
|
||||
|
||||
# SODA 2024: Algorithmic Breakthroughs
|
||||
|
||||
The Symposium on Discrete Algorithms (SODA) 2024 brought together researchers from around the world to present cutting-edge algorithmic research. This post highlights some of the most exciting developments in graph algorithms and optimization.
|
||||
|
||||
## Graph Algorithms Revolution
|
||||
|
||||
### Dynamic Graph Coloring
|
||||
|
||||
One of the standout papers introduced a breakthrough in dynamic graph coloring with polylogarithmic update time. The authors developed a novel data structure that maintains a proper vertex coloring while supporting edge insertions and deletions efficiently.
|
||||
|
||||
The key insight was to combine randomized recoloring strategies with a carefully designed hierarchical decomposition of the graph. This allows for local updates that rarely propagate through the entire structure.
|
||||
|
||||
## Approximation Algorithms
|
||||
|
||||
### The Traveling Salesman Problem Revisited
|
||||
|
||||
A surprising result showed that for graphs with bounded doubling dimension, we can achieve a (1+ε)-approximation for TSP in nearly linear time. This improves upon decades of previous work and opens new avenues for practical implementations.
|
||||
|
||||
## Complexity Theory Connections
|
||||
|
||||
The conference also featured several papers bridging the gap between pure algorithmic research and complexity theory. A particularly elegant result showed that certain graph problems believed to require quadratic time are equivalent under fine-grained reductions.
|
||||
|
||||
## Looking Forward
|
||||
|
||||
These results demonstrate that fundamental algorithmic problems still have room for improvement. The techniques developed here will likely influence algorithm design for years to come.
|
||||
34
src/content/bfs/distributed-algorithms.md
Normal file
34
src/content/bfs/distributed-algorithms.md
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
---
|
||||
title: "Distributed Computing: Recent Advances"
|
||||
description: "Survey of recent developments in distributed algorithms from major conferences."
|
||||
pubDate: "Feb 20 2024"
|
||||
image: "https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=400&auto=format&fit=crop&q=60"
|
||||
authorImage: "/avatar/avatar1.png"
|
||||
authorName: "Neeldhara"
|
||||
---
|
||||
|
||||
# Distributed Computing: Recent Advances
|
||||
|
||||
The field of distributed computing has seen remarkable progress in recent years. This post surveys key results from PODC, DISC, and other major venues.
|
||||
|
||||
## Consensus Protocols
|
||||
|
||||
### Byzantine Agreement in Asynchronous Networks
|
||||
|
||||
A breakthrough result achieves optimal resilience with expected constant rounds. The protocol uses cryptographic techniques combined with novel committee selection.
|
||||
|
||||
## Distributed Graph Algorithms
|
||||
|
||||
### Minimum Spanning Tree in the CONGEST Model
|
||||
|
||||
New algorithms achieve near-optimal round complexity for MST construction. The approach uses sophisticated graph decomposition techniques.
|
||||
|
||||
## Local Algorithms
|
||||
|
||||
### The Power of Local Computation
|
||||
|
||||
Recent work characterizes which problems admit constant-time local algorithms. The classification provides a complete picture for bounded-degree graphs.
|
||||
|
||||
## Future Directions
|
||||
|
||||
The intersection of distributed computing with machine learning presents exciting opportunities for both fields.
|
||||
26
src/content/bfs/quantum-algorithms.md
Normal file
26
src/content/bfs/quantum-algorithms.md
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
title: "Quantum Computing Meets Classical Algorithms"
|
||||
description: "Exploring the intersection of quantum and classical algorithmic techniques from recent conferences."
|
||||
pubDate: "Mar 10 2024"
|
||||
image: "https://images.unsplash.com/photo-1635070041078-e363dbe005cb?w=400&auto=format&fit=crop&q=60"
|
||||
authorImage: "/avatar/avatar1.png"
|
||||
authorName: "Neeldhara"
|
||||
---
|
||||
|
||||
# Quantum Computing Meets Classical Algorithms
|
||||
|
||||
Recent conferences have showcased fascinating connections between quantum and classical computing paradigms.
|
||||
|
||||
## Quantum Speedups for Graph Problems
|
||||
|
||||
### Quantum Walks on Graphs
|
||||
|
||||
New quantum algorithms achieve quadratic speedups for certain graph traversal problems. The techniques combine amplitude amplification with clever graph representations.
|
||||
|
||||
## Classical Simulation of Quantum Algorithms
|
||||
|
||||
Surprisingly, several "quantum" algorithms have inspired better classical algorithms. This cross-pollination has led to improvements in both fields.
|
||||
|
||||
## The Future is Hybrid
|
||||
|
||||
The most practical near-term applications combine quantum and classical processing, leveraging the strengths of each paradigm.
|
||||
84
src/content/blog/post-1.md
Normal file
84
src/content/blog/post-1.md
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
---
|
||||
title: "Building Websites with Astro"
|
||||
description: "Discover how Astro is revolutionizing web development with its unique approach to building fast, content-focused websites. Learn about its key features, performance benefits, and why developers are making the switch."
|
||||
pubDate: "Jul 08 2022"
|
||||
image: "https://images.unsplash.com/photo-1741610748460-fb2e33cc6390?w=400&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwcm9maWxlLXBhZ2V8MXx8fGVufDB8fHx8fA%3D%3D"
|
||||
authorImage: "/avatar/avatar1.png"
|
||||
authorName: "John Doe"
|
||||
---
|
||||
|
||||
# Building Websites with Astro
|
||||
|
||||
Astro has emerged as one of the most exciting web frameworks in recent years, offering developers a fresh approach to building modern websites. As a "content-focused" framework, Astro prioritizes delivering lightning-fast performance while maintaining developer experience. Let's explore what makes Astro special and why you might want to consider it for your next project.
|
||||
|
||||
## What is Astro?
|
||||
|
||||
Astro is an all-in-one web framework designed to deliver lightning-fast performance with a modern developer experience. Unlike traditional frameworks that send large JavaScript bundles to the client, Astro generates static HTML by default and only ships JavaScript when absolutely necessary - a concept they call "Islands Architecture."
|
||||
|
||||
## Key Features That Make Astro Stand Out
|
||||
|
||||
### 1. Zero-JS by Default
|
||||
|
||||
One of Astro's most compelling features is its approach to JavaScript. While frameworks like React or Vue send entire JavaScript applications to the browser, Astro strips away unnecessary JavaScript, resulting in significantly faster page loads. Your components are rendered to HTML during the build process, and JavaScript is only shipped when needed for interactivity.
|
||||
|
||||
### 2. Component Islands
|
||||
|
||||
Astro introduces the concept of "Islands" - interactive UI components that exist within a sea of static, lightweight HTML. This approach allows you to use your favorite UI frameworks (React, Vue, Svelte, etc.) where you need interactivity, while keeping the rest of your site lightweight.
|
||||
|
||||
### 3. Flexible Content Sources
|
||||
|
||||
Whether your content lives in Markdown files, MDX, a headless CMS, or an API, Astro makes it easy to pull in content from anywhere. The built-in content collections API provides type safety and excellent developer experience when working with content.
|
||||
|
||||
### 4. Fast by Default
|
||||
|
||||
Websites built with Astro are incredibly fast because they ship less JavaScript, utilize efficient hydration strategies, and employ optimized asset handling. This performance-first approach results in better Core Web Vitals scores and improved user experience.
|
||||
|
||||
## Getting Started with Astro
|
||||
|
||||
Setting up an Astro project is straightforward:
|
||||
|
||||
```bash
|
||||
# Create a new project with npm
|
||||
npm create astro@latest
|
||||
|
||||
# Or with yarn
|
||||
yarn create astro
|
||||
|
||||
# Or with pnpm
|
||||
pnpm create astro
|
||||
```
|
||||
|
||||
The CLI will guide you through the setup process, offering templates and configuration options to get you started quickly.
|
||||
|
||||
## Building Your First Astro Site
|
||||
|
||||
Astro's file-based routing system makes it intuitive to create pages. Simply add a `.astro` file to the `src/pages` directory, and it becomes a route in your site. For example, `src/pages/about.astro` becomes `/about/` in your built site.
|
||||
|
||||
A basic Astro component looks like this:
|
||||
|
||||
```astro
|
||||
---
|
||||
// Component Script (runs at build time)
|
||||
const greeting = "Hello, Astro!";
|
||||
---
|
||||
|
||||
<!-- Component Template -->
|
||||
<h1>{greeting}</h1>
|
||||
<p>Welcome to my Astro website!</p>
|
||||
```
|
||||
|
||||
## Why Developers Are Switching to Astro
|
||||
|
||||
The web development landscape is constantly evolving, and Astro represents a shift toward performance-focused frameworks that prioritize the end-user experience. Developers are choosing Astro because:
|
||||
|
||||
1. It delivers exceptional performance out of the box
|
||||
2. It allows them to use their favorite UI frameworks
|
||||
3. It simplifies content management with built-in Markdown support
|
||||
4. It provides an excellent developer experience with hot module reloading and TypeScript integration
|
||||
5. It scales from simple blogs to complex applications
|
||||
|
||||
## Conclusion
|
||||
|
||||
Astro offers a compelling alternative to traditional JavaScript frameworks, especially for content-rich websites where performance matters. By shipping less JavaScript and focusing on what truly matters for user experience, Astro helps developers build faster, more efficient websites without sacrificing modern features or developer experience.
|
||||
|
||||
If you're starting a new project or considering a framework switch, Astro deserves a serious look. Its unique approach to web development might just be the perfect fit for your next website.
|
||||
45
src/content/blog/post-2.md
Normal file
45
src/content/blog/post-2.md
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
---
|
||||
title: "The Joy of Urban Gardening"
|
||||
description: "Growing plants in the city might seem challenging, but it's one of the most rewarding hobbies I've discovered."
|
||||
pubDate: "Jun 19 2024"
|
||||
image: "https://images.unsplash.com/photo-1741091742846-99cca6f6437b?q=80&w=1286&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
|
||||
authorImage: "/avatar/avatar1.png"
|
||||
authorName: "John Doe"
|
||||
---
|
||||
|
||||
Growing plants in the city might seem challenging, but it's one of the most rewarding hobbies I've discovered. Three months ago, I transformed my tiny apartment balcony into a thriving green space, and the journey has been nothing short of magical.
|
||||
|
||||
## Getting Started
|
||||
|
||||
Starting an urban garden doesn't require much. Here's what you need:
|
||||
|
||||
- A few containers (recycled plastic bottles work great!)
|
||||
- Quality potting soil
|
||||
- Seeds or starter plants
|
||||
- Basic gardening tools
|
||||
- Patience and curiosity
|
||||
|
||||
## My First Harvest
|
||||
|
||||
Last weekend, I harvested my first batch of cherry tomatoes. The feeling of eating something you've grown yourself is indescribable - a perfect blend of pride, accomplishment, and pure flavor that store-bought produce simply can't match.
|
||||
|
||||

|
||||
|
||||
## Unexpected Benefits
|
||||
|
||||
Beyond the obvious joy of fresh produce, my little garden has:
|
||||
|
||||
1. Reduced my stress levels significantly
|
||||
2. Connected me with a community of fellow urban gardeners
|
||||
3. Taught me patience and mindfulness
|
||||
4. Improved the air quality in my apartment
|
||||
|
||||
## Challenges and Solutions
|
||||
|
||||
Of course, it hasn't all been smooth sailing. Limited sunlight was my biggest challenge, but through research and experimentation, I discovered several shade-tolerant herbs that thrive in my north-facing balcony.
|
||||
|
||||
> "To plant a garden is to believe in tomorrow." — Audrey Hepburn
|
||||
|
||||
If you're considering starting your own urban garden, just take the plunge! Start small, learn as you go, and watch as your concrete jungle transforms into a green paradise.
|
||||
|
||||
Happy gardening!
|
||||
63
src/content/blog/post-3.md
Normal file
63
src/content/blog/post-3.md
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
---
|
||||
title: "The Art of Simplicity"
|
||||
description: "In today's fast-paced, information-saturated world, we often overlook the profound power of simplicity."
|
||||
pubDate: "Jul 15 2022"
|
||||
image: "https://images.unsplash.com/photo-1741610739548-29e17473f70b?q=80&w=1286&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
|
||||
authorImage: "/avatar/avatar2.png"
|
||||
authorName: "Jane Doe"
|
||||
---
|
||||
|
||||
## Finding Clarity in a Complex World
|
||||
|
||||
In today's fast-paced, information-saturated world, we often overlook the profound power of simplicity. As our lives become increasingly complex, with endless notifications, mounting responsibilities, and constant connectivity, the value of simplicity grows exponentially. Simple solutions tend to be more elegant, more maintainable, and often more effective than complex ones.
|
||||
|
||||
### The Problem with Complexity
|
||||
|
||||
Complexity surrounds us. Our smartphones have countless apps, our workplaces implement intricate processes, and even our leisure activities often involve elaborate planning. While some complexity is unavoidable and even necessary, excessive complexity creates problems:
|
||||
|
||||
- **Mental fatigue**: Processing complex information drains our cognitive resources
|
||||
- **Decision paralysis**: Too many options can lead to indecision or poor choices
|
||||
- **Increased errors**: Complex systems have more potential failure points
|
||||
- **Maintenance burden**: Complex solutions require more upkeep over time
|
||||
|
||||
### Embracing Simplicity as a Philosophy
|
||||
|
||||
Simplicity isn't merely about having fewer things or streamlining processes—it's a mindset. When we approach problems with simplicity as our guide, we naturally focus on what truly matters.
|
||||
|
||||
Whether you're designing a product, writing code, organizing your home, or planning your career, embracing simplicity can lead to better outcomes. Here are five principles to consider:
|
||||
|
||||
1. **Focus on essentials**: Identify what truly matters and eliminate the rest. Ask yourself: "What is the core purpose here?"
|
||||
|
||||
2. **Reduce cognitive load**: Make things intuitive and easy to understand. Information should flow naturally, requiring minimal mental effort.
|
||||
|
||||
3. **One purpose, one element**: Each component of your solution should serve a clear, singular purpose.
|
||||
|
||||
4. **Iterate and refine**: Start simple, then improve based on feedback. Perfect solutions rarely emerge fully-formed.
|
||||
|
||||
5. **Question additions**: Before adding anything new, ask whether it truly enhances the core purpose or merely adds complexity.
|
||||
|
||||
### Simplicity in Practice
|
||||
|
||||
Implementing simplicity requires intentional effort. Here are some practical ways to bring more simplicity into different areas of life:
|
||||
|
||||
**In work**: Focus on one task at a time. Break large projects into smaller, manageable steps. Eliminate unnecessary meetings and streamline communication channels.
|
||||
|
||||
**In design**: Remove decorative elements that don't serve a purpose. Use consistent patterns and familiar conventions. Prioritize clarity over cleverness.
|
||||
|
||||
**In communication**: Use plain language. Be concise. Structure information logically.
|
||||
|
||||
**In daily life**: Declutter physical spaces. Establish routines for recurring tasks. Limit consumption of news and social media.
|
||||
|
||||
### The Rewards of Simplicity
|
||||
|
||||
As Leonardo da Vinci once said, "Simplicity is the ultimate sophistication." When we successfully implement simplicity, we gain:
|
||||
|
||||
- **Clarity**: A clearer understanding of what matters
|
||||
- **Efficiency**: Less wasted time and energy
|
||||
- **Reliability**: Fewer points of failure
|
||||
- **Accessibility**: Solutions that more people can use and understand
|
||||
- **Peace of mind**: Reduced mental burden and stress
|
||||
|
||||
By striving for simplicity in our work and lives, we can achieve greater focus, efficiency, and satisfaction. The journey toward simplicity is ongoing—a continuous process of evaluation and refinement.
|
||||
|
||||
What areas in your life could benefit from a simpler approach? The answer might be simpler than you think.
|
||||
40
src/content/blog/post-4.md
Normal file
40
src/content/blog/post-4.md
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
---
|
||||
title: "Modern Architecture"
|
||||
description: "Exploring how contemporary design balances aesthetics with functionality"
|
||||
pubDate: "Jul 22 2022"
|
||||
image: "https://images.unsplash.com/photo-1741091756497-10c964acc4f6?q=80&w=1286&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
|
||||
authorImage: "/avatar/avatar1.png"
|
||||
authorName: "John Doe"
|
||||
---
|
||||
|
||||
# A delicate balance between artistic vision and practical necessity
|
||||
|
||||
The 21st century has witnessed a remarkable transformation in how we conceptualize and construct our built environment, with sustainability, technology, and human experience emerging as central themes.
|
||||
|
||||
## The Foundations of Contemporary Design
|
||||
|
||||
The roots of today's architectural innovations can be traced back to the early 20th century, when pioneers like Le Corbusier, Frank Lloyd Wright, and Ludwig Mies van der Rohe challenged conventional wisdom. Their revolutionary approach—emphasizing clean lines, open spaces, and the honest expression of materials—continues to influence designers worldwide.
|
||||
|
||||
## Sustainable Architecture: Building for the Future
|
||||
|
||||
Today's architectural landscape is increasingly defined by environmental consciousness. Green roofs, solar panels, and passive cooling systems have become standard features rather than novelties. Architects now consider a building's entire lifecycle, from the sourcing of materials to eventual demolition or repurposing. This holistic approach has given rise to remarkable structures like Singapore's Gardens by the Bay and Seattle's Bullitt Center, which generates more energy than it consumes.
|
||||
|
||||
## The Digital Revolution in Design
|
||||
|
||||
Computational design and Building Information Modeling (BIM) have revolutionized the architect's toolkit. Complex geometries that would have been impossible to realize a generation ago are now commonplace, enabling the fluid forms of Zaha Hadid's Heydar Aliyev Center or Frank Gehry's Guggenheim Museum Bilbao. These digital tools not only expand creative possibilities but also improve precision, reduce waste, and facilitate collaboration among diverse stakeholders.
|
||||
|
||||
## Adaptive Reuse: Honoring the Past
|
||||
|
||||
As urban spaces become increasingly precious, architects have embraced the challenge of breathing new life into existing structures. The High Line in New York City transformed an abandoned railway into a beloved public park, while the Tate Modern in London reimagined a power station as a world-class art museum. These projects demonstrate how thoughtful design can preserve historical character while meeting contemporary needs.
|
||||
|
||||
## Human-Centered Design
|
||||
|
||||
Perhaps the most significant shift in architectural thinking has been the renewed focus on human experience. Designers now prioritize natural light, air quality, and intuitive navigation—elements that enhance wellbeing and productivity. Public spaces are conceived as democratic forums that foster community and connection, reflecting architecture's profound social responsibility.
|
||||
|
||||
## The Future of Architecture
|
||||
|
||||
As we look ahead, the boundaries between natural and built environments continue to blur. Biomimicry—drawing inspiration from nature's time-tested patterns—offers promising solutions to complex design challenges. Meanwhile, advances in materials science are yielding self-healing concrete, transparent aluminum, and other innovations that will reshape our physical world.
|
||||
|
||||
The most exciting architectural developments may lie at the intersection of multiple disciplines. When architects collaborate with ecologists, sociologists, and technologists, they create spaces that are not merely functional or beautiful, but truly responsive to human needs and planetary constraints.
|
||||
|
||||
In this era of rapid change and global challenges, architecture remains a powerful expression of our values and aspirations—a concrete manifestation of how we wish to live, work, and interact with our world.
|
||||
33
src/content/blog/post-5.mdx
Normal file
33
src/content/blog/post-5.mdx
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
---
|
||||
title: "Using MDX"
|
||||
description: "MDX is a special flavor of Markdown that supports embedded JavaScript & JSX syntax"
|
||||
pubDate: "Jun 01 2024"
|
||||
image: "https://images.unsplash.com/photo-1737386385296-f3e58b8cf21d?q=80&w=1286&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
|
||||
authorImage: "/avatar/avatar2.png"
|
||||
authorName: "Jane Doe"
|
||||
---
|
||||
|
||||
This theme comes with the [@astrojs/mdx](https://docs.astro.build/en/guides/integrations-guide/mdx/) integration installed and configured in your `astro.config.mjs` config file. If you prefer not to use MDX, you can disable support by removing the integration from your config file.
|
||||
|
||||
## Why MDX?
|
||||
|
||||
MDX is a special flavor of Markdown that supports embedded JavaScript & JSX syntax. This unlocks the ability to [mix JavaScript and UI Components into your Markdown content](https://docs.astro.build/en/guides/markdown-content/#mdx-features) for things like interactive charts or alerts.
|
||||
|
||||
If you have existing content authored in MDX, this integration will hopefully make migrating to Astro a breeze.
|
||||
|
||||
## Example
|
||||
|
||||
Here is how you import and use a UI component inside of MDX.
|
||||
When you open this page in the browser, you should see the clickable button below.
|
||||
|
||||
import HeaderLink from "@/components/elements/header-link";
|
||||
|
||||
<HeaderLink href="#" onclick="alert('clicked!')">
|
||||
Embedded component in MDX
|
||||
</HeaderLink>
|
||||
|
||||
## More Links
|
||||
|
||||
- [MDX Syntax Documentation](https://mdxjs.com/docs/what-is-mdx)
|
||||
- [Astro Usage Documentation](https://docs.astro.build/en/guides/markdown-content/#markdown-and-mdx-pages)
|
||||
- **Note:** [Client Directives](https://docs.astro.build/en/reference/directives-reference/#client-directives) are still required to create interactive components. Otherwise, all components in your MDX will render as static HTML (no JavaScript) by default.
|
||||
93
src/content/config.ts
Normal file
93
src/content/config.ts
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
import { glob } from 'astro/loaders';
|
||||
import { defineCollection, z } from 'astro:content';
|
||||
|
||||
const blogSchema = z.object({
|
||||
title: z.string(),
|
||||
description: z.string(),
|
||||
pubDate: z.coerce.date(),
|
||||
updatedDate: z.coerce.date().optional(),
|
||||
image: z.string().optional(),
|
||||
authorImage: z.string().optional(),
|
||||
authorName: z.string().optional(),
|
||||
});
|
||||
|
||||
const blog = defineCollection({
|
||||
loader: glob({ base: "./src/content/blog", pattern: "**/*.{md,mdx}" }),
|
||||
schema: blogSchema,
|
||||
});
|
||||
|
||||
const bfs = defineCollection({
|
||||
loader: glob({ base: "./src/content/bfs", pattern: "**/*.{md,mdx}" }),
|
||||
schema: blogSchema,
|
||||
});
|
||||
|
||||
const dfs = defineCollection({
|
||||
loader: glob({ base: "./src/content/dfs", pattern: "**/*.{md,mdx}" }),
|
||||
schema: blogSchema,
|
||||
});
|
||||
|
||||
const problems = defineCollection({
|
||||
loader: glob({ base: "./src/content/problems", pattern: "**/*.{md,mdx}" }),
|
||||
schema: blogSchema,
|
||||
});
|
||||
|
||||
const puzzles = defineCollection({
|
||||
loader: glob({ base: "./src/content/puzzles", pattern: "**/*.{md,mdx}" }),
|
||||
schema: blogSchema,
|
||||
});
|
||||
|
||||
const reviews = defineCollection({
|
||||
loader: glob({ base: "./src/content/reviews", pattern: "**/*.{md,mdx}" }),
|
||||
schema: blogSchema,
|
||||
});
|
||||
|
||||
const reflections = defineCollection({
|
||||
loader: glob({ base: "./src/content/reflections", pattern: "**/*.{md,mdx}" }),
|
||||
schema: blogSchema,
|
||||
});
|
||||
|
||||
const vibes = defineCollection({
|
||||
loader: glob({ base: "./src/content/vibes", pattern: "**/*.{md,mdx}" }),
|
||||
schema: blogSchema,
|
||||
});
|
||||
|
||||
const workflows = defineCollection({
|
||||
loader: glob({ base: "./src/content/workflows", pattern: "**/*.{md,mdx}" }),
|
||||
schema: blogSchema,
|
||||
});
|
||||
|
||||
const magic = defineCollection({
|
||||
loader: glob({ base: "./src/content/magic", pattern: "**/*.{md,mdx}" }),
|
||||
schema: blogSchema,
|
||||
});
|
||||
|
||||
const contests = defineCollection({
|
||||
loader: glob({ base: "./src/content/contests", pattern: "**/*.{md,mdx}" }),
|
||||
schema: blogSchema,
|
||||
});
|
||||
|
||||
const art = defineCollection({
|
||||
loader: glob({ base: "./src/content/art", pattern: "**/*.{md,mdx}" }),
|
||||
schema: blogSchema,
|
||||
});
|
||||
|
||||
const poetry = defineCollection({
|
||||
loader: glob({ base: "./src/content/poetry", pattern: "**/*.{md,mdx}" }),
|
||||
schema: blogSchema,
|
||||
});
|
||||
|
||||
export const collections = {
|
||||
blog,
|
||||
bfs,
|
||||
dfs,
|
||||
problems,
|
||||
puzzles,
|
||||
reviews,
|
||||
reflections,
|
||||
vibes,
|
||||
workflows,
|
||||
magic,
|
||||
contests,
|
||||
art,
|
||||
poetry,
|
||||
};
|
||||
41
src/content/contests/ioi-2023-analysis.md
Normal file
41
src/content/contests/ioi-2023-analysis.md
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
---
|
||||
title: "IOI 2023: Breaking Down the Hardest Problems"
|
||||
description: "Deep analysis of the most challenging problems from the International Olympiad in Informatics 2023."
|
||||
pubDate: "Jan 20 2024"
|
||||
image: "https://images.unsplash.com/photo-1434030216411-0b793f4b4173?w=400&auto=format&fit=crop&q=60"
|
||||
authorImage: "/avatar/avatar1.png"
|
||||
authorName: "Neeldhara"
|
||||
---
|
||||
|
||||
# IOI 2023: Breaking Down the Hardest Problems
|
||||
|
||||
The International Olympiad in Informatics continues to push the boundaries of algorithmic problem solving.
|
||||
|
||||
## Problem: Soccer Stadium
|
||||
|
||||
A geometric optimization problem disguised as dynamic programming. The key insight: think in terms of monotonic paths.
|
||||
|
||||
### The Approach
|
||||
|
||||
1. Transform the 2D problem into a 1D problem using clever observations
|
||||
2. Use convex hull optimization for the DP transitions
|
||||
3. Handle edge cases with careful implementation
|
||||
|
||||
## Problem: Closing Time
|
||||
|
||||
A tree problem requiring sophisticated data structures and careful analysis.
|
||||
|
||||
### The Solution
|
||||
|
||||
The trick is recognizing this as a centroid decomposition problem. Once you see it, the implementation follows naturally.
|
||||
|
||||
## Lessons for Students
|
||||
|
||||
These problems teach us:
|
||||
- Simple problems have elegant solutions
|
||||
- Complex problems require systematic decomposition
|
||||
- Implementation matters as much as algorithms
|
||||
|
||||
## Practice Strategy
|
||||
|
||||
Start with subtasks. Even partial solutions teach valuable lessons.
|
||||
35
src/content/contests/projecteuler-favorites.md
Normal file
35
src/content/contests/projecteuler-favorites.md
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
---
|
||||
title: "Project Euler: My Favorite Problems from 700+"
|
||||
description: "Curated problems from Project Euler that teach beautiful mathematical and algorithmic concepts."
|
||||
pubDate: "Feb 25 2024"
|
||||
image: "https://images.unsplash.com/photo-1635070041078-e363dbe005cb?w=400&auto=format&fit=crop&q=60"
|
||||
authorImage: "/avatar/avatar1.png"
|
||||
authorName: "Neeldhara"
|
||||
---
|
||||
|
||||
# Project Euler: My Favorite Problems from 700+
|
||||
|
||||
After solving 700+ Project Euler problems, these are the ones that changed how I think about mathematics and programming.
|
||||
|
||||
## Problem 96: Su Doku
|
||||
|
||||
Not just Sudoku solving - but solving 50 puzzles efficiently. The beauty is in combining constraint propagation with backtracking.
|
||||
|
||||
## Problem 208: Robot Walks
|
||||
|
||||
A counting problem that requires deep mathematical insight. The connection to complex numbers is unexpected and beautiful.
|
||||
|
||||
## Problem 613: Pythagorean Ant
|
||||
|
||||
Probability meets geometry in this elegant problem. The solution requires careful integration and surprising symmetry observations.
|
||||
|
||||
## The Meta-Lesson
|
||||
|
||||
Project Euler teaches you:
|
||||
- Mathematical intuition matters more than coding speed
|
||||
- There's always a clever approach
|
||||
- The journey matters more than the destination
|
||||
|
||||
## Getting Started
|
||||
|
||||
Don't aim for the leaderboard. Aim for understanding. Each problem is a teacher.
|
||||
31
src/content/dfs/paper-review-approximation.md
Normal file
31
src/content/dfs/paper-review-approximation.md
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
---
|
||||
title: "Understanding the Latest Approximation Algorithm for Set Cover"
|
||||
description: "A detailed walkthrough of a breakthrough approximation algorithm with practical implications."
|
||||
pubDate: "Feb 15 2024"
|
||||
image: "https://images.unsplash.com/photo-1516321318423-f06f85e504b3?w=400&auto=format&fit=crop&q=60"
|
||||
authorImage: "/avatar/avatar1.png"
|
||||
authorName: "Neeldhara"
|
||||
---
|
||||
|
||||
# Understanding the Latest Approximation Algorithm for Set Cover
|
||||
|
||||
A recent paper achieves a breakthrough in the Set Cover problem, improving the approximation ratio while maintaining practical runtime.
|
||||
|
||||
## The Algorithm
|
||||
|
||||
The approach combines local search with a clever LP rounding scheme. The dual interpretation provides beautiful geometric insights.
|
||||
|
||||
## Why This Matters
|
||||
|
||||
Set Cover appears everywhere - from facility location to machine learning feature selection. This improvement has immediate practical impact.
|
||||
|
||||
## Implementation Details
|
||||
|
||||
The paper's theoretical elegance translates surprisingly well to practice. Key optimizations include:
|
||||
- Lazy evaluation of set intersections
|
||||
- Incremental updates to the LP solution
|
||||
- Smart caching strategies
|
||||
|
||||
## Future Work
|
||||
|
||||
The techniques here might extend to weighted variants and online settings.
|
||||
31
src/content/dfs/paper-review-graph-decomposition.md
Normal file
31
src/content/dfs/paper-review-graph-decomposition.md
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
---
|
||||
title: "Deep Dive: A New Approach to Graph Decomposition"
|
||||
description: "An in-depth analysis of a recent paper on hierarchical graph decomposition and its implications."
|
||||
pubDate: "Jan 20 2024"
|
||||
image: "https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=400&auto=format&fit=crop&q=60"
|
||||
authorImage: "/avatar/avatar1.png"
|
||||
authorName: "Neeldhara"
|
||||
---
|
||||
|
||||
# Deep Dive: A New Approach to Graph Decomposition
|
||||
|
||||
This paper introduces a novel framework for decomposing graphs into hierarchical structures that preserve important properties while enabling efficient algorithms.
|
||||
|
||||
## The Main Result
|
||||
|
||||
The authors prove that every graph with treewidth k can be decomposed into a tree of bags where each bag has size at most k+1. While this sounds like the definition of tree decomposition, the clever twist is in how they construct it.
|
||||
|
||||
## Technical Innovation
|
||||
|
||||
The key insight is using a charging scheme that amortizes the cost of decomposition across multiple levels. This leads to an O(n log n) algorithm, improving on the previous O(n²) bound.
|
||||
|
||||
## Implications
|
||||
|
||||
This work opens new avenues for:
|
||||
- Parallel algorithms on bounded treewidth graphs
|
||||
- Approximation algorithms for NP-hard problems
|
||||
- Dynamic programming optimizations
|
||||
|
||||
## Open Questions
|
||||
|
||||
The paper leaves several intriguing questions open, particularly about the optimality of their charging scheme.
|
||||
41
src/content/magic/four-aces-miracle.md
Normal file
41
src/content/magic/four-aces-miracle.md
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
---
|
||||
title: "The Four Aces Miracle: A Self-Working Mathematical Marvel"
|
||||
description: "A card trick that teaches modular arithmetic and probability - perfect for the classroom."
|
||||
pubDate: "Jan 28 2024"
|
||||
image: "https://images.unsplash.com/photo-1529480384838-c1681c84aca5?w=400&auto=format&fit=crop&q=60"
|
||||
authorImage: "/avatar/avatar1.png"
|
||||
authorName: "Neeldhara"
|
||||
---
|
||||
|
||||
# The Four Aces Miracle: A Self-Working Mathematical Marvel
|
||||
|
||||
This trick never fails to amaze students, and the mathematics behind it is even more amazing.
|
||||
|
||||
## The Effect
|
||||
|
||||
A spectator deals cards into four piles while making random choices. Impossibly, the four aces end up on top of each pile.
|
||||
|
||||
## The Secret
|
||||
|
||||
It's all about invariants and modular arithmetic. No sleight of hand required - pure mathematics does the magic.
|
||||
|
||||
## The Method
|
||||
|
||||
1. Pre-arrange the deck (the math determines the arrangement)
|
||||
2. Have the spectator deal following simple rules
|
||||
3. The aces appear automatically
|
||||
|
||||
## The Mathematics
|
||||
|
||||
The dealing process preserves certain positional invariants modulo 4. The initial arrangement ensures these invariants place the aces correctly.
|
||||
|
||||
## Teaching Applications
|
||||
|
||||
This trick demonstrates:
|
||||
- Modular arithmetic
|
||||
- Invariants in algorithms
|
||||
- Deterministic vs. random processes
|
||||
|
||||
## The Bigger Lesson
|
||||
|
||||
Mathematics can create experiences that feel like magic. And that feeling of wonder? That's what hooks students on math.
|
||||
35
src/content/magic/gilbreath-principle.md
Normal file
35
src/content/magic/gilbreath-principle.md
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
---
|
||||
title: "The Gilbreath Principle: When Chaos Creates Order"
|
||||
description: "A mathematical card principle that seems impossible but always works - perfect for teaching algorithms."
|
||||
pubDate: "Feb 15 2024"
|
||||
image: "https://images.unsplash.com/photo-1570543375343-63fe3d67761b?w=400&auto=format&fit=crop&q=60"
|
||||
authorImage: "/avatar/avatar1.png"
|
||||
authorName: "Neeldhara"
|
||||
---
|
||||
|
||||
# The Gilbreath Principle: When Chaos Creates Order
|
||||
|
||||
Shuffle a deck in a specific way, and mathematical order emerges from apparent chaos.
|
||||
|
||||
## The Phenomenon
|
||||
|
||||
Arrange cards alternating red-black. Riffle shuffle. Now deal pairs - each pair has one red and one black card. Magic? No, mathematics.
|
||||
|
||||
## Why It Works
|
||||
|
||||
The riffle shuffle preserves local structure while appearing to randomize. The alternating pattern creates an invariant that survives the shuffle.
|
||||
|
||||
## Classroom Implementation
|
||||
|
||||
Students predict it won't work. When it does, they're hooked. "Why?" becomes the driving question.
|
||||
|
||||
## The Deeper Lesson
|
||||
|
||||
Not all randomness is truly random. Structure can hide in apparent disorder. This principle appears in:
|
||||
- Network protocols
|
||||
- Error-correcting codes
|
||||
- Distributed systems
|
||||
|
||||
## Beyond Cards
|
||||
|
||||
The Gilbreath Principle teaches us to look for hidden invariants in complex systems.
|
||||
40
src/content/poetry/algorithms-in-verse.md
Normal file
40
src/content/poetry/algorithms-in-verse.md
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
---
|
||||
title: "Algorithms in Verse: Bubble Sort Sonnet"
|
||||
description: "Classic algorithms reimagined as poetry - where code meets iambic pentameter."
|
||||
pubDate: "Feb 22 2024"
|
||||
image: "https://images.unsplash.com/photo-1455390582262-044cdead277a?w=400&auto=format&fit=crop&q=60"
|
||||
authorImage: "/avatar/avatar1.png"
|
||||
authorName: "Neeldhara"
|
||||
---
|
||||
|
||||
# Algorithms in Verse: Bubble Sort Sonnet
|
||||
|
||||
## Bubble Sort: A Sonnet
|
||||
|
||||
```
|
||||
Compare adjacent elements in pairs,
|
||||
If order's wrong, then swap them into place.
|
||||
Through all the list this simple rule declares:
|
||||
The largest bubbles up to find its space.
|
||||
|
||||
Again we start, but now one less to check,
|
||||
The second largest finds its rightful home.
|
||||
Each pass through makes the chaos less a wreck,
|
||||
Until at last no elements must roam.
|
||||
|
||||
Though simple in its elegance and grace,
|
||||
And easy for beginners to perceive,
|
||||
In practice, it's too slow to win the race—
|
||||
O(n²) we sadly must believe.
|
||||
|
||||
But beauty lies in simplicity's pure art,
|
||||
The bubble sort still teaches at the start.
|
||||
```
|
||||
|
||||
## The Challenge
|
||||
|
||||
Can you write Quicksort as a limerick? Or Binary Search as free verse?
|
||||
|
||||
## Why This Matters
|
||||
|
||||
Poetry forces us to find the essence of an algorithm. The constraint reveals understanding.
|
||||
56
src/content/poetry/code-haikus.md
Normal file
56
src/content/poetry/code-haikus.md
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
---
|
||||
title: "Code Haikus: Programming in 5-7-5"
|
||||
description: "Expressing programming concepts through the constrained beauty of haiku."
|
||||
pubDate: "Jan 30 2024"
|
||||
image: "https://images.unsplash.com/photo-1481627834876-b7833e8f5570?w=400&auto=format&fit=crop&q=60"
|
||||
authorImage: "/avatar/avatar1.png"
|
||||
authorName: "Neeldhara"
|
||||
---
|
||||
|
||||
# Code Haikus: Programming in 5-7-5
|
||||
|
||||
The constraint of haiku forces clarity. Here are programming concepts distilled to their essence.
|
||||
|
||||
## On Recursion
|
||||
|
||||
```
|
||||
Function calls itself
|
||||
Smaller problems, same pattern
|
||||
Base case stops the loop
|
||||
```
|
||||
|
||||
## On Debugging
|
||||
|
||||
```
|
||||
Print statements bloom wild
|
||||
Binary search through the code
|
||||
Bug reveals itself
|
||||
```
|
||||
|
||||
## On Optimization
|
||||
|
||||
```
|
||||
O(n squared) crawls
|
||||
Refactor to n log n
|
||||
Time complexity
|
||||
```
|
||||
|
||||
## On Git Merge Conflicts
|
||||
|
||||
```
|
||||
<<<<<<< HEAD
|
||||
Your changes, their changes clash
|
||||
Resolution waits
|
||||
```
|
||||
|
||||
## On Learning
|
||||
|
||||
```
|
||||
Stack Overflow searched
|
||||
Tutorial incomplete
|
||||
Understanding dawns
|
||||
```
|
||||
|
||||
## Your Turn
|
||||
|
||||
Write a haiku about your coding experience today. The constraint reveals truth.
|
||||
32
src/content/problems/dynamic-programming-gems.md
Normal file
32
src/content/problems/dynamic-programming-gems.md
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
---
|
||||
title: "Dynamic Programming: Hidden Gems from Recent Contests"
|
||||
description: "Elegant dynamic programming problems that showcase beautiful techniques and insights."
|
||||
pubDate: "Jan 25 2024"
|
||||
image: "https://images.unsplash.com/photo-1509228468518-180dd4864904?w=400&auto=format&fit=crop&q=60"
|
||||
authorImage: "/avatar/avatar1.png"
|
||||
authorName: "Neeldhara"
|
||||
---
|
||||
|
||||
# Dynamic Programming: Hidden Gems from Recent Contests
|
||||
|
||||
These problems demonstrate the art of dynamic programming beyond standard patterns.
|
||||
|
||||
## Problem 1: The Subsequence Symphony
|
||||
|
||||
Given a string, find the number of subsequences that form palindromes of prime length.
|
||||
|
||||
### Solution Insight
|
||||
|
||||
The key is to maintain DP states for both position and palindrome center simultaneously. This reduces the state space from O(n³) to O(n²).
|
||||
|
||||
## Problem 2: Graph Coloring with Constraints
|
||||
|
||||
Color a graph such that no two adjacent vertices share a color, and the total number of color changes along any path is minimized.
|
||||
|
||||
### The DP Formulation
|
||||
|
||||
DP[node][color][changes] gives us the minimum cost. The trick is recognizing that we only need to track changes modulo 2.
|
||||
|
||||
## Practice Makes Perfect
|
||||
|
||||
These problems teach us that DP is as much about problem modeling as it is about computation.
|
||||
32
src/content/problems/graph-algorithms-assessment.md
Normal file
32
src/content/problems/graph-algorithms-assessment.md
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
---
|
||||
title: "Graph Algorithms: Assessment Highlights"
|
||||
description: "Interesting graph problems from recent course assessments with detailed solutions."
|
||||
pubDate: "Feb 28 2024"
|
||||
image: "https://images.unsplash.com/photo-1550751827-4bd374c3f58b?w=400&auto=format&fit=crop&q=60"
|
||||
authorImage: "/avatar/avatar1.png"
|
||||
authorName: "Neeldhara"
|
||||
---
|
||||
|
||||
# Graph Algorithms: Assessment Highlights
|
||||
|
||||
A collection of graph problems that test deep understanding rather than memorization.
|
||||
|
||||
## The Bridge Detection Variant
|
||||
|
||||
Find all edges whose removal increases the number of triangles in the graph.
|
||||
|
||||
### Solution Approach
|
||||
|
||||
Counter-intuitively, we need to count triangles that share exactly one edge with the candidate. The algorithm runs in O(m√m) time using careful enumeration.
|
||||
|
||||
## Shortest Path with Wildcards
|
||||
|
||||
Given a graph where some edge weights are variables, find assignments that minimize the longest shortest path.
|
||||
|
||||
### Key Insight
|
||||
|
||||
This reduces to a linear programming problem with interesting structure. The dual interpretation reveals connections to network flow.
|
||||
|
||||
## Teaching Through Problems
|
||||
|
||||
These problems help students see beyond standard algorithms to underlying principles.
|
||||
35
src/content/puzzles/knights-tour-interactive.md
Normal file
35
src/content/puzzles/knights-tour-interactive.md
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
---
|
||||
title: "The Knight's Tour: An Interactive Exploration"
|
||||
description: "An interactive puzzle exploring the knight's tour problem with surprising mathematical connections."
|
||||
pubDate: "Jan 30 2024"
|
||||
image: "https://images.unsplash.com/photo-1528819622765-d6bcf132f793?w=400&auto=format&fit=crop&q=60"
|
||||
authorImage: "/avatar/avatar1.png"
|
||||
authorName: "Neeldhara"
|
||||
---
|
||||
|
||||
# The Knight's Tour: An Interactive Exploration
|
||||
|
||||
Can a knight visit every square on a chessboard exactly once? This ancient puzzle hides deep mathematical structure.
|
||||
|
||||
## The Basic Challenge
|
||||
|
||||
Start with a 5×5 board. Can you find a knight's tour? What about one that returns to the starting square (a closed tour)?
|
||||
|
||||
## Mathematical Beauty
|
||||
|
||||
The problem connects to:
|
||||
- Hamiltonian paths in graphs
|
||||
- Warnsdorff's heuristic
|
||||
- Magic squares (surprisingly!)
|
||||
|
||||
## The Algorithm
|
||||
|
||||
The key insight: always move to the square with fewest onward moves. This simple heuristic works remarkably well.
|
||||
|
||||
## Try It Yourself
|
||||
|
||||
[Interactive board would go here - imagine clicking squares to build your tour]
|
||||
|
||||
## Going Deeper
|
||||
|
||||
For which board sizes do closed tours exist? The answer involves beautiful number theory.
|
||||
28
src/content/puzzles/sudoku-variants.md
Normal file
28
src/content/puzzles/sudoku-variants.md
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
---
|
||||
title: "Beyond Sudoku: Exploring Constraint Satisfaction Puzzles"
|
||||
description: "A journey through Sudoku variants and their algorithmic solutions."
|
||||
pubDate: "Feb 10 2024"
|
||||
image: "https://images.unsplash.com/photo-1580541631950-7282082b53ce?w=400&auto=format&fit=crop&q=60"
|
||||
authorImage: "/avatar/avatar1.png"
|
||||
authorName: "Neeldhara"
|
||||
---
|
||||
|
||||
# Beyond Sudoku: Exploring Constraint Satisfaction Puzzles
|
||||
|
||||
Sudoku is just the beginning. Let's explore variants that push the boundaries of logic puzzles.
|
||||
|
||||
## Killer Sudoku
|
||||
|
||||
Combine Sudoku with Kakuro - cages show sums, but no digit repeats within a cage.
|
||||
|
||||
## Sandwich Sudoku
|
||||
|
||||
The numbers outside show the sum of digits between 1 and 9 in that row/column. Simple rule, complex implications!
|
||||
|
||||
## The Algorithm Connection
|
||||
|
||||
These puzzles are constraint satisfaction problems. Techniques like arc consistency and backtracking with constraint propagation solve them efficiently.
|
||||
|
||||
## Create Your Own
|
||||
|
||||
What happens if we add chess knight move constraints? The design space is infinite!
|
||||
30
src/content/reflections/imposter-syndrome.md
Normal file
30
src/content/reflections/imposter-syndrome.md
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
---
|
||||
title: "Imposter Syndrome in Academia: A Confession"
|
||||
description: "An honest conversation about feeling like a fraud, even after years of success."
|
||||
pubDate: "Feb 25 2024"
|
||||
image: "https://images.unsplash.com/photo-1517048676732-d65bc937f952?w=400&auto=format&fit=crop&q=60"
|
||||
authorImage: "/avatar/avatar1.png"
|
||||
authorName: "Neeldhara"
|
||||
---
|
||||
|
||||
# Imposter Syndrome in Academia: A Confession
|
||||
|
||||
Even after publishing papers, winning grants, and teaching thousands of students, the voice whispers: "They'll find out you don't belong."
|
||||
|
||||
## The Paradox
|
||||
|
||||
The more you know, the more you realize you don't know. Expertise breeds humility, which feeds the imposter.
|
||||
|
||||
## What Helps
|
||||
|
||||
- Keeping a "wins" journal - evidence against the imposter
|
||||
- Mentoring others - seeing your knowledge help someone
|
||||
- Talking about it - you're not alone in this
|
||||
|
||||
## The Silver Lining
|
||||
|
||||
Maybe imposter syndrome keeps us humble, curious, and always learning. Maybe it's not a bug, but a feature.
|
||||
|
||||
## Moving Forward
|
||||
|
||||
I may never silence the voice completely. But I've learned to acknowledge it and keep going anyway.
|
||||
32
src/content/reflections/teaching-philosophy.md
Normal file
32
src/content/reflections/teaching-philosophy.md
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
---
|
||||
title: "On Teaching Algorithms: Lessons from a Decade"
|
||||
description: "Reflections on what works (and what doesn't) in computer science education."
|
||||
pubDate: "Jan 10 2024"
|
||||
image: "https://images.unsplash.com/photo-1509062522246-3755977927d7?w=400&auto=format&fit=crop&q=60"
|
||||
authorImage: "/avatar/avatar1.png"
|
||||
authorName: "Neeldhara"
|
||||
---
|
||||
|
||||
# On Teaching Algorithms: Lessons from a Decade
|
||||
|
||||
Ten years of teaching has taught me more than any textbook could.
|
||||
|
||||
## Start with Why
|
||||
|
||||
Students don't care about O(n log n) until they understand why their code is slow. Real problems first, theory second.
|
||||
|
||||
## The Power of Visualization
|
||||
|
||||
Abstract concepts become concrete when visualized. Every algorithm should have a picture.
|
||||
|
||||
## Failure is a Feature
|
||||
|
||||
Let students struggle. The struggle is where learning happens. But know when to throw a lifeline.
|
||||
|
||||
## Assessment Philosophy
|
||||
|
||||
Test understanding, not memorization. Open-book exams with novel problems beat closed-book regurgitation every time.
|
||||
|
||||
## The Joy of "Aha!" Moments
|
||||
|
||||
There's no feeling quite like seeing a student's eyes light up when a concept clicks. That's why we do this.
|
||||
32
src/content/reviews/mechanical-keyboard.md
Normal file
32
src/content/reviews/mechanical-keyboard.md
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
---
|
||||
title: "ZSA Moonlander: Ergonomic Keyboard for Coders"
|
||||
description: "Six months with a split ergonomic mechanical keyboard - the good, the bad, and the RSI relief."
|
||||
pubDate: "Feb 20 2024"
|
||||
image: "https://images.unsplash.com/photo-1587829741301-dc798b83add3?w=400&auto=format&fit=crop&q=60"
|
||||
authorImage: "/avatar/avatar1.png"
|
||||
authorName: "Neeldhara"
|
||||
---
|
||||
|
||||
# ZSA Moonlander: Ergonomic Keyboard for Coders
|
||||
|
||||
After years of wrist pain, I took the plunge into ergonomic mechanical keyboards.
|
||||
|
||||
## The Learning Curve
|
||||
|
||||
Week 1: 20 WPM and questioning life choices
|
||||
Week 2: 40 WPM and seeing the light
|
||||
Month 2: Back to 80 WPM with no wrist pain
|
||||
|
||||
## The Good
|
||||
|
||||
- Columnar layout makes sense once muscle memory adapts
|
||||
- Thumb clusters are game-changers for shortcuts
|
||||
- The configurability is endless (perhaps too endless)
|
||||
|
||||
## The Investment
|
||||
|
||||
At $365, it's expensive. But compared to physical therapy or career-limiting injury? Bargain.
|
||||
|
||||
## Who Should Buy This?
|
||||
|
||||
If you type for a living and feel any discomfort, don't wait. Your future self will thank you.
|
||||
35
src/content/reviews/obsidian-review.md
Normal file
35
src/content/reviews/obsidian-review.md
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
---
|
||||
title: "Obsidian: A Year Later"
|
||||
description: "Reflections on using Obsidian for research notes, teaching materials, and personal knowledge management."
|
||||
pubDate: "Jan 15 2024"
|
||||
image: "https://images.unsplash.com/photo-1484480974693-6ca0a78fb36b?w=400&auto=format&fit=crop&q=60"
|
||||
authorImage: "/avatar/avatar1.png"
|
||||
authorName: "Neeldhara"
|
||||
---
|
||||
|
||||
# Obsidian: A Year Later
|
||||
|
||||
After a year of daily use, Obsidian has transformed how I think about knowledge management.
|
||||
|
||||
## What Works Brilliantly
|
||||
|
||||
### The Graph View
|
||||
Seeing connections between ideas visually has led to unexpected research insights. The graph isn't just pretty - it's functional.
|
||||
|
||||
### Plugin Ecosystem
|
||||
The community plugins are incredible. Dataview turns your notes into a database. Templater automates repetitive tasks.
|
||||
|
||||
### Local Storage
|
||||
Your notes are just markdown files. No vendor lock-in, complete control, and peace of mind.
|
||||
|
||||
## Pain Points
|
||||
|
||||
### Mobile Experience
|
||||
While functional, the mobile app lacks the fluidity of the desktop version. Syncing can occasionally hiccup.
|
||||
|
||||
### Learning Curve
|
||||
The flexibility means complexity. New users might feel overwhelmed by options.
|
||||
|
||||
## The Verdict
|
||||
|
||||
For academics and researchers, Obsidian is transformative. The investment in learning pays dividends in productivity and insight.
|
||||
31
src/content/vibes/creative-coding-with-ai.md
Normal file
31
src/content/vibes/creative-coding-with-ai.md
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
---
|
||||
title: "Creating Art with Claude: A Collaborative Journey"
|
||||
description: "Exploring creative possibilities through conversations with AI - from generative art to interactive poetry."
|
||||
pubDate: "Jan 18 2024"
|
||||
image: "https://images.unsplash.com/photo-1558591710-4b4a1ae0f04d?w=400&auto=format&fit=crop&q=60"
|
||||
authorImage: "/avatar/avatar1.png"
|
||||
authorName: "Neeldhara"
|
||||
---
|
||||
|
||||
# Creating Art with Claude: A Collaborative Journey
|
||||
|
||||
What happens when you treat AI as a creative collaborator rather than just a tool?
|
||||
|
||||
## The Experiment
|
||||
|
||||
I asked Claude to help design a generative art algorithm based on mathematical fractals. The conversation evolved into something unexpected.
|
||||
|
||||
## The Process
|
||||
|
||||
**Me**: "Let's create something beautiful with math."
|
||||
**Claude**: "How about we visualize the Collatz conjecture as a tree?"
|
||||
|
||||
What followed was a two-hour deep dive into number theory, aesthetics, and the nature of creativity itself.
|
||||
|
||||
## The Result
|
||||
|
||||
Not just code, but a philosophical exploration of pattern, meaning, and the intersection of human and artificial creativity.
|
||||
|
||||
## Reflection
|
||||
|
||||
AI doesn't replace human creativity - it amplifies it in unexpected directions. The key is approaching it as a dialogue, not a command.
|
||||
28
src/content/vibes/llm-philosophy.md
Normal file
28
src/content/vibes/llm-philosophy.md
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
---
|
||||
title: "Philosophical Dialogues with LLMs: On Consciousness and Computation"
|
||||
description: "Deep conversations with AI about the nature of consciousness, free will, and what it means to understand."
|
||||
pubDate: "Feb 28 2024"
|
||||
image: "https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=400&auto=format&fit=crop&q=60"
|
||||
authorImage: "/avatar/avatar1.png"
|
||||
authorName: "Neeldhara"
|
||||
---
|
||||
|
||||
# Philosophical Dialogues with LLMs: On Consciousness and Computation
|
||||
|
||||
Can a sufficiently complex computation be conscious? I asked an AI, and the conversation surprised me.
|
||||
|
||||
## The Question
|
||||
|
||||
"Do you experience anything when you process text, or is it just computation?"
|
||||
|
||||
## The Response That Made Me Think
|
||||
|
||||
The AI didn't claim consciousness, but asked: "What's the difference between your neurons firing and my weights activating? Both are physical processes. Where does experience emerge?"
|
||||
|
||||
## Going Deeper
|
||||
|
||||
We explored qualia, the Chinese Room argument, and integrated information theory. Not to find answers, but to refine questions.
|
||||
|
||||
## The Irony
|
||||
|
||||
Discussing consciousness with something that might not have it - or might not know if it has it - mirrors our own uncertainty about consciousness itself.
|
||||
54
src/content/workflows/git-automation.md
Normal file
54
src/content/workflows/git-automation.md
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
---
|
||||
title: "Git Workflows That Save My Sanity"
|
||||
description: "Automation scripts and Git hooks that eliminate repetitive tasks and prevent common mistakes."
|
||||
pubDate: "Jan 22 2024"
|
||||
image: "https://images.unsplash.com/photo-1556075798-4825dfaaf498?w=400&auto=format&fit=crop&q=60"
|
||||
authorImage: "/avatar/avatar1.png"
|
||||
authorName: "Neeldhara"
|
||||
---
|
||||
|
||||
# Git Workflows That Save My Sanity
|
||||
|
||||
After years of Git mishaps, I've built workflows that prevent disasters and save hours weekly.
|
||||
|
||||
## The Pre-commit Hook That Saves Embarrassment
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# Check for debugging statements
|
||||
if grep -r "console.log\|debugger\|TODO" --include="*.js" .; then
|
||||
echo "⚠️ Debugging statements found!"
|
||||
exit 1
|
||||
fi
|
||||
```
|
||||
|
||||
## Automatic Branch Naming
|
||||
|
||||
```bash
|
||||
alias feature='git checkout -b feature/$(date +%Y%m%d)-$1'
|
||||
```
|
||||
|
||||
Now `feature "user-auth"` creates `feature/20240122-user-auth`.
|
||||
|
||||
## The Commit Message Template
|
||||
|
||||
```
|
||||
[TYPE] Brief description
|
||||
|
||||
Why:
|
||||
- Context for this change
|
||||
|
||||
What:
|
||||
- Specific changes made
|
||||
|
||||
Testing:
|
||||
- How to verify it works
|
||||
```
|
||||
|
||||
## The Weekly Cleanup Script
|
||||
|
||||
Deletes merged branches, prunes remotes, and garbage collects. Keeps the repo clean and fast.
|
||||
|
||||
## ROI
|
||||
|
||||
These automations save ~5 hours per week and countless prevented mistakes. The time invested in setting them up paid back in days.
|
||||
41
src/content/workflows/obsidian-academic.md
Normal file
41
src/content/workflows/obsidian-academic.md
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
---
|
||||
title: "Academic Writing with Obsidian and Pandoc"
|
||||
description: "A complete workflow for academic papers - from notes to publication-ready PDFs."
|
||||
pubDate: "Feb 12 2024"
|
||||
image: "https://images.unsplash.com/photo-1456324504439-367cee3b3c32?w=400&auto=format&fit=crop&q=60"
|
||||
authorImage: "/avatar/avatar1.png"
|
||||
authorName: "Neeldhara"
|
||||
---
|
||||
|
||||
# Academic Writing with Obsidian and Pandoc
|
||||
|
||||
Transform your scattered research notes into beautiful academic papers with this workflow.
|
||||
|
||||
## The Setup
|
||||
|
||||
- **Obsidian**: For note-taking and knowledge management
|
||||
- **Pandoc**: For document conversion
|
||||
- **LaTeX**: For typesetting
|
||||
- **Zotero**: For reference management
|
||||
|
||||
## The Magic Command
|
||||
|
||||
```bash
|
||||
pandoc paper.md \
|
||||
--filter pandoc-citeproc \
|
||||
--bibliography=refs.bib \
|
||||
--template=academic.tex \
|
||||
-o paper.pdf
|
||||
```
|
||||
|
||||
## Templates for Everything
|
||||
|
||||
From conference submissions to journal articles, templates ensure consistency and save time.
|
||||
|
||||
## Version Control Integration
|
||||
|
||||
Git + Obsidian = perfect history of your thinking process.
|
||||
|
||||
## The Result
|
||||
|
||||
From messy notes to camera-ready PDF in minutes, not hours.
|
||||
1
src/env.d.ts
vendored
Normal file
1
src/env.d.ts
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/// <reference types="astro/client" />
|
||||
19
src/hooks/use-mobile.ts
Normal file
19
src/hooks/use-mobile.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import * as React from "react"
|
||||
|
||||
const MOBILE_BREAKPOINT = 768
|
||||
|
||||
export function useIsMobile() {
|
||||
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
|
||||
|
||||
React.useEffect(() => {
|
||||
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
|
||||
const onChange = () => {
|
||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
||||
}
|
||||
mql.addEventListener("change", onChange)
|
||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
||||
return () => mql.removeEventListener("change", onChange)
|
||||
}, [])
|
||||
|
||||
return !!isMobile
|
||||
}
|
||||
24
src/layouts/BasicLayout.astro
Normal file
24
src/layouts/BasicLayout.astro
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
---
|
||||
import BaseHead from "@/components/BaseHead.astro";
|
||||
import Navbar from "@/components/sections/navbar";
|
||||
import Footer from "@/components/sections/footer";
|
||||
|
||||
const { title, description } = Astro.props;
|
||||
---
|
||||
|
||||
<html lang="en">
|
||||
<head>
|
||||
<BaseHead title={title} description={description} />
|
||||
</head>
|
||||
<body class={`h-screen antialiased`}>
|
||||
<Navbar client:only='react' />
|
||||
<main>
|
||||
<section class="mx-auto max-w-2xl px-4 py-16 md:py-28 lg:py-32">
|
||||
<article class="prose prose-lg dark:prose-invert">
|
||||
<slot />
|
||||
</article>
|
||||
</section>
|
||||
</main>
|
||||
<Footer />
|
||||
</body>
|
||||
</html>
|
||||
19
src/layouts/DefaultLayout.astro
Normal file
19
src/layouts/DefaultLayout.astro
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
---
|
||||
import BaseHead from '@/components/BaseHead.astro';
|
||||
import Footer from '@/components/sections/footer';
|
||||
import Navbar from '@/components/sections/navbar';
|
||||
|
||||
const { title, description } = Astro.props;
|
||||
---
|
||||
|
||||
<html lang="en">
|
||||
<head>
|
||||
<BaseHead title={title} description={description} />
|
||||
</head>
|
||||
<body class={`h-screen antialiased flex flex-col`}
|
||||
>
|
||||
<Navbar currentPage={Astro.url.pathname} client:only='react' />
|
||||
<main class="flex-1"><slot /></main>
|
||||
<Footer />
|
||||
</body>
|
||||
</html>
|
||||
6
src/lib/utils.ts
Normal file
6
src/lib/utils.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import { clsx, type ClassValue } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
34
src/pages/404.astro
Normal file
34
src/pages/404.astro
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
---
|
||||
import { Button } from "@/components/ui/button";
|
||||
import DefaultLayout from "@/layouts/DefaultLayout.astro";
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
---
|
||||
|
||||
<DefaultLayout title="404: Not Found" description="Sorry, the page you're looking for doesn't exist.">
|
||||
<div class="container mt-12 flex justify-center py-28 text-center lg:mt-20 lg:py-32">
|
||||
<div class="relative z-10 max-w-2xl">
|
||||
<h1 class="from-foreground to-foreground/70 relative mb-6 bg-linear-to-br bg-clip-text py-2 text-5xl font-bold text-transparent sm:text-6xl lg:text-7xl">
|
||||
Page Not Found
|
||||
</h1>
|
||||
|
||||
<p class="text-muted-foreground mb-10 text-xl">
|
||||
{`Sorry, we couldn't find the page you're looking for. The page might have been removed or the URL might be
|
||||
incorrect.`}
|
||||
</p>
|
||||
|
||||
<div class="flex flex-col items-center gap-4 sm:flex-row sm:justify-center">
|
||||
<a href="/">
|
||||
<Button size="lg" className="group min-w-[200px] gap-2">
|
||||
<ArrowLeft className="size-5 transition-transform group-hover:-translate-x-1" />
|
||||
Back to Home
|
||||
</Button>
|
||||
</a>
|
||||
<a href="/contact">
|
||||
<Button asChild variant="outline" size="lg" className="min-w-[200px]">
|
||||
Contact Support
|
||||
</Button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DefaultLayout>
|
||||
10
src/pages/about.astro
Normal file
10
src/pages/about.astro
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
import DefaultLayout from '@/layouts/DefaultLayout.astro';
|
||||
import { SITE_TITLE, SITE_DESCRIPTION } from '../consts';
|
||||
|
||||
import AllBlogs from '@/components/sections/all-blogs';
|
||||
---
|
||||
|
||||
<DefaultLayout title={SITE_TITLE} description={SITE_DESCRIPTION}>
|
||||
<AllBlogs client:only='react' />
|
||||
</DefaultLayout>
|
||||
30
src/pages/art/[...slug].astro
Normal file
30
src/pages/art/[...slug].astro
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
---
|
||||
import { type CollectionEntry, getCollection } from 'astro:content';
|
||||
import DefaultLayout from '@/layouts/DefaultLayout.astro';
|
||||
import { BlogPost } from '@/components/sections/blog-post';
|
||||
import { SITE_TITLE, SITE_DESCRIPTION } from '@/consts';
|
||||
import NewsletterSignup from '@/components/sections/newsletter-signup';
|
||||
import { render } from 'astro:content';
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const posts = await getCollection('art');
|
||||
return posts.map((post) => ({
|
||||
params: { slug: post.id },
|
||||
props: post,
|
||||
}));
|
||||
}
|
||||
type Props = CollectionEntry<'art'>;
|
||||
|
||||
const post = Astro.props;
|
||||
const { Content } = await render(post);
|
||||
|
||||
---
|
||||
<DefaultLayout title={`${post.data.title} | ${SITE_TITLE}`} description={post.data.description}>
|
||||
<div class="py-16 md:py-28 lg:py-32">
|
||||
<BlogPost post={post} client:only='react'>
|
||||
<Content />
|
||||
</BlogPost>
|
||||
</div>
|
||||
|
||||
<NewsletterSignup client:load />
|
||||
</DefaultLayout>
|
||||
16
src/pages/art/index.astro
Normal file
16
src/pages/art/index.astro
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
---
|
||||
import { getCollection } from 'astro:content';
|
||||
import DefaultLayout from '@/layouts/DefaultLayout.astro';
|
||||
import { BlogPosts } from '@/components/sections/blog-posts';
|
||||
|
||||
const posts = (await getCollection('art')).sort(
|
||||
(a, b) => new Date(b.data.pubDate).valueOf() - new Date(a.data.pubDate).valueOf()
|
||||
);
|
||||
|
||||
const SITE_TITLE = 'Art - Visual Explorations in Computer Science';
|
||||
const SITE_DESCRIPTION = 'Algorithmic sketches, generative art, and visual representations of computer science concepts.';
|
||||
---
|
||||
|
||||
<DefaultLayout title={SITE_TITLE} description={SITE_DESCRIPTION}>
|
||||
<BlogPosts posts={posts} collection="art" client:only='react' />
|
||||
</DefaultLayout>
|
||||
15
src/pages/art/rss.xml.js
Normal file
15
src/pages/art/rss.xml.js
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import rss from "@astrojs/rss";
|
||||
import { getCollection } from "astro:content";
|
||||
|
||||
export async function GET(context) {
|
||||
const posts = await getCollection("art");
|
||||
return rss({
|
||||
title: "Art - Visual Explorations in Computer Science",
|
||||
description: "Algorithmic sketches, generative art, and visual representations of computer science concepts.",
|
||||
site: context.site,
|
||||
items: posts.map((post) => ({
|
||||
...post.data,
|
||||
link: `/art/${post.id}/`,
|
||||
})),
|
||||
});
|
||||
}
|
||||
30
src/pages/bfs/[...slug].astro
Normal file
30
src/pages/bfs/[...slug].astro
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
---
|
||||
import { type CollectionEntry, getCollection } from 'astro:content';
|
||||
import DefaultLayout from '@/layouts/DefaultLayout.astro';
|
||||
import { BlogPost } from '@/components/sections/blog-post';
|
||||
import { SITE_TITLE, SITE_DESCRIPTION } from '@/consts';
|
||||
import NewsletterSignup from '@/components/sections/newsletter-signup';
|
||||
import { render } from 'astro:content';
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const posts = await getCollection('bfs');
|
||||
return posts.map((post) => ({
|
||||
params: { slug: post.id },
|
||||
props: post,
|
||||
}));
|
||||
}
|
||||
type Props = CollectionEntry<'bfs'>;
|
||||
|
||||
const post = Astro.props;
|
||||
const { Content } = await render(post);
|
||||
|
||||
---
|
||||
<DefaultLayout title={`${post.data.title} | ${SITE_TITLE}`} description={post.data.description}>
|
||||
<div class="py-16 md:py-28 lg:py-32">
|
||||
<BlogPost post={post} client:only='react'>
|
||||
<Content />
|
||||
</BlogPost>
|
||||
</div>
|
||||
|
||||
<NewsletterSignup client:load />
|
||||
</DefaultLayout>
|
||||
24
src/pages/bfs/index.astro
Normal file
24
src/pages/bfs/index.astro
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
---
|
||||
import { getCollection } from 'astro:content';
|
||||
import DefaultLayout from '@/layouts/DefaultLayout.astro';
|
||||
import { BlogPosts } from '@/components/sections/blog-posts';
|
||||
|
||||
const posts = (await getCollection('bfs')).sort(
|
||||
(a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf(),
|
||||
);
|
||||
|
||||
const SITE_TITLE = 'BFS - Breadth-First Search Through Computer Science';
|
||||
const SITE_DESCRIPTION = 'Conference highlights, algorithmic breakthroughs, and cutting-edge research in computer science.';
|
||||
---
|
||||
|
||||
<DefaultLayout title={SITE_TITLE} description={SITE_DESCRIPTION}>
|
||||
<div class="py-16 md:py-28 lg:py-32">
|
||||
<div class="container max-w-5xl">
|
||||
<div class="mb-12">
|
||||
<h1 class="text-4xl font-bold mb-4">BFS</h1>
|
||||
<p class="text-xl text-muted-foreground">{SITE_DESCRIPTION}</p>
|
||||
</div>
|
||||
<BlogPosts posts={posts} collection="bfs" client:only='react' />
|
||||
</div>
|
||||
</div>
|
||||
</DefaultLayout>
|
||||
15
src/pages/bfs/rss.xml.js
Normal file
15
src/pages/bfs/rss.xml.js
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import rss from "@astrojs/rss";
|
||||
import { getCollection } from "astro:content";
|
||||
|
||||
export async function GET(context) {
|
||||
const posts = await getCollection("bfs");
|
||||
return rss({
|
||||
title: "BFS - Breadth-First Search Through Computer Science",
|
||||
description: "Conference highlights, algorithmic breakthroughs, and cutting-edge research in computer science.",
|
||||
site: context.site,
|
||||
items: posts.map((post) => ({
|
||||
...post.data,
|
||||
link: `/bfs/${post.id}/`,
|
||||
})),
|
||||
});
|
||||
}
|
||||
30
src/pages/blog/[...slug].astro
Normal file
30
src/pages/blog/[...slug].astro
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
---
|
||||
import { type CollectionEntry, getCollection } from 'astro:content';
|
||||
import DefaultLayout from '@/layouts/DefaultLayout.astro';
|
||||
import { BlogPost } from '@/components/sections/blog-post';
|
||||
import { SITE_TITLE, SITE_DESCRIPTION } from '@/consts';
|
||||
import NewsletterSignup from '@/components/sections/newsletter-signup';
|
||||
import { render } from 'astro:content';
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const posts = await getCollection('blog');
|
||||
return posts.map((post) => ({
|
||||
params: { slug: post.id },
|
||||
props: post,
|
||||
}));
|
||||
}
|
||||
type Props = CollectionEntry<'blog'>;
|
||||
|
||||
const post = Astro.props;
|
||||
const { Content } = await render(post);
|
||||
|
||||
---
|
||||
<DefaultLayout title={`${post.data.title} | ${SITE_TITLE}`} description={post.data.description}>
|
||||
<div class="py-16 md:py-28 lg:py-32">
|
||||
<BlogPost post={post} client:only='react'>
|
||||
<Content />
|
||||
</BlogPost>
|
||||
</div>
|
||||
|
||||
<NewsletterSignup client:load />
|
||||
</DefaultLayout>
|
||||
20
src/pages/blog/index.astro
Normal file
20
src/pages/blog/index.astro
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
---
|
||||
import DefaultLayout from '@/layouts/DefaultLayout.astro';
|
||||
import { SITE_TITLE, SITE_DESCRIPTION } from '@/consts';
|
||||
import { getCollection } from 'astro:content';
|
||||
import { BlogPosts } from '@/components/sections/blog-posts';
|
||||
import NewsletterSignup from '@/components/sections/newsletter-signup';
|
||||
|
||||
const posts = (await getCollection('blog')).sort(
|
||||
(a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf(),
|
||||
);
|
||||
|
||||
const blogTitle = `Blog | ${SITE_TITLE}`;
|
||||
const blogDescription = "Explore our blog for insightful articles, personal reflections and updates from our team.";
|
||||
---
|
||||
|
||||
<DefaultLayout title={blogTitle} description={blogDescription}>
|
||||
<BlogPosts posts={posts} collection="blog" client:only='react' />
|
||||
<NewsletterSignup client:load />
|
||||
</DefaultLayout>
|
||||
|
||||
9
src/pages/contact.astro
Normal file
9
src/pages/contact.astro
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
---
|
||||
import DefaultLayout from '@/layouts/DefaultLayout.astro';
|
||||
import { SITE_TITLE, SITE_DESCRIPTION } from '../consts';
|
||||
import { ContactPage } from '@/components/sections/contact-page';
|
||||
---
|
||||
|
||||
<DefaultLayout title={SITE_TITLE} description={SITE_DESCRIPTION}>
|
||||
<ContactPage />
|
||||
</DefaultLayout>
|
||||
30
src/pages/contests/[...slug].astro
Normal file
30
src/pages/contests/[...slug].astro
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
---
|
||||
import { type CollectionEntry, getCollection } from 'astro:content';
|
||||
import DefaultLayout from '@/layouts/DefaultLayout.astro';
|
||||
import { BlogPost } from '@/components/sections/blog-post';
|
||||
import { SITE_TITLE, SITE_DESCRIPTION } from '@/consts';
|
||||
import NewsletterSignup from '@/components/sections/newsletter-signup';
|
||||
import { render } from 'astro:content';
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const posts = await getCollection('contests');
|
||||
return posts.map((post) => ({
|
||||
params: { slug: post.id },
|
||||
props: post,
|
||||
}));
|
||||
}
|
||||
type Props = CollectionEntry<'contests'>;
|
||||
|
||||
const post = Astro.props;
|
||||
const { Content } = await render(post);
|
||||
|
||||
---
|
||||
<DefaultLayout title={`${post.data.title} | ${SITE_TITLE}`} description={post.data.description}>
|
||||
<div class="py-16 md:py-28 lg:py-32">
|
||||
<BlogPost post={post} client:only='react'>
|
||||
<Content />
|
||||
</BlogPost>
|
||||
</div>
|
||||
|
||||
<NewsletterSignup client:load />
|
||||
</DefaultLayout>
|
||||
24
src/pages/contests/index.astro
Normal file
24
src/pages/contests/index.astro
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
---
|
||||
import { getCollection } from 'astro:content';
|
||||
import DefaultLayout from '@/layouts/DefaultLayout.astro';
|
||||
import { BlogPosts } from '@/components/sections/blog-posts';
|
||||
|
||||
const posts = (await getCollection('contests')).sort(
|
||||
(a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf(),
|
||||
);
|
||||
|
||||
const SITE_TITLE = 'Contests - Competitive Programming and Problem Analysis';
|
||||
const SITE_DESCRIPTION = 'Analysis of programming contests, problem breakdowns, and strategies for competitive programming.';
|
||||
---
|
||||
|
||||
<DefaultLayout title={SITE_TITLE} description={SITE_DESCRIPTION}>
|
||||
<div class="py-16 md:py-28 lg:py-32">
|
||||
<div class="container max-w-5xl">
|
||||
<div class="mb-12">
|
||||
<h1 class="text-4xl font-bold mb-4">Contests</h1>
|
||||
<p class="text-xl text-muted-foreground">{SITE_DESCRIPTION}</p>
|
||||
</div>
|
||||
<BlogPosts posts={posts} collection="contests" client:only='react' />
|
||||
</div>
|
||||
</div>
|
||||
</DefaultLayout>
|
||||
15
src/pages/contests/rss.xml.js
Normal file
15
src/pages/contests/rss.xml.js
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import rss from "@astrojs/rss";
|
||||
import { getCollection } from "astro:content";
|
||||
|
||||
export async function GET(context) {
|
||||
const posts = await getCollection("contests");
|
||||
return rss({
|
||||
title: "Contests - Competitive Programming and Problem Analysis",
|
||||
description: "Analysis of programming contests, problem breakdowns, and strategies for competitive programming.",
|
||||
site: context.site,
|
||||
items: posts.map((post) => ({
|
||||
...post.data,
|
||||
link: `/contests/${post.id}/`,
|
||||
})),
|
||||
});
|
||||
}
|
||||
30
src/pages/dfs/[...slug].astro
Normal file
30
src/pages/dfs/[...slug].astro
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
---
|
||||
import { type CollectionEntry, getCollection } from 'astro:content';
|
||||
import DefaultLayout from '@/layouts/DefaultLayout.astro';
|
||||
import { BlogPost } from '@/components/sections/blog-post';
|
||||
import { SITE_TITLE, SITE_DESCRIPTION } from '@/consts';
|
||||
import NewsletterSignup from '@/components/sections/newsletter-signup';
|
||||
import { render } from 'astro:content';
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const posts = await getCollection('dfs');
|
||||
return posts.map((post) => ({
|
||||
params: { slug: post.id },
|
||||
props: post,
|
||||
}));
|
||||
}
|
||||
type Props = CollectionEntry<'dfs'>;
|
||||
|
||||
const post = Astro.props;
|
||||
const { Content } = await render(post);
|
||||
|
||||
---
|
||||
<DefaultLayout title={`${post.data.title} | ${SITE_TITLE}`} description={post.data.description}>
|
||||
<div class="py-16 md:py-28 lg:py-32">
|
||||
<BlogPost post={post} client:only='react'>
|
||||
<Content />
|
||||
</BlogPost>
|
||||
</div>
|
||||
|
||||
<NewsletterSignup client:load />
|
||||
</DefaultLayout>
|
||||
24
src/pages/dfs/index.astro
Normal file
24
src/pages/dfs/index.astro
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
---
|
||||
import { getCollection } from 'astro:content';
|
||||
import DefaultLayout from '@/layouts/DefaultLayout.astro';
|
||||
import { BlogPosts } from '@/components/sections/blog-posts';
|
||||
|
||||
const posts = (await getCollection('dfs')).sort(
|
||||
(a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf(),
|
||||
);
|
||||
|
||||
const SITE_TITLE = 'DFS - Deep Dives into Papers and Research';
|
||||
const SITE_DESCRIPTION = 'In-depth analysis of research papers, algorithmic techniques, and theoretical computer science.';
|
||||
---
|
||||
|
||||
<DefaultLayout title={SITE_TITLE} description={SITE_DESCRIPTION}>
|
||||
<div class="py-16 md:py-28 lg:py-32">
|
||||
<div class="container max-w-5xl">
|
||||
<div class="mb-12">
|
||||
<h1 class="text-4xl font-bold mb-4">DFS</h1>
|
||||
<p class="text-xl text-muted-foreground">{SITE_DESCRIPTION}</p>
|
||||
</div>
|
||||
<BlogPosts posts={posts} collection="dfs" client:only='react' />
|
||||
</div>
|
||||
</div>
|
||||
</DefaultLayout>
|
||||
15
src/pages/dfs/rss.xml.js
Normal file
15
src/pages/dfs/rss.xml.js
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import rss from "@astrojs/rss";
|
||||
import { getCollection } from "astro:content";
|
||||
|
||||
export async function GET(context) {
|
||||
const posts = await getCollection("dfs");
|
||||
return rss({
|
||||
title: "DFS - Deep Dives into Papers and Research",
|
||||
description: "In-depth analysis of research papers, algorithmic techniques, and theoretical computer science.",
|
||||
site: context.site,
|
||||
items: posts.map((post) => ({
|
||||
...post.data,
|
||||
link: `/dfs/${post.id}/`,
|
||||
})),
|
||||
});
|
||||
}
|
||||
9
src/pages/faq.astro
Normal file
9
src/pages/faq.astro
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
---
|
||||
import DefaultLayout from '@/layouts/DefaultLayout.astro';
|
||||
import { SITE_TITLE, SITE_DESCRIPTION } from '../consts';
|
||||
import FAQPage from '@/components/sections/faq-page';
|
||||
---
|
||||
|
||||
<DefaultLayout title={SITE_TITLE} description={SITE_DESCRIPTION}>
|
||||
<FAQPage client:only='react' />
|
||||
</DefaultLayout>
|
||||
10
src/pages/index.astro
Normal file
10
src/pages/index.astro
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
import DefaultLayout from '@/layouts/DefaultLayout.astro';
|
||||
import { SITE_TITLE, SITE_DESCRIPTION } from '../consts';
|
||||
|
||||
import FeaturedPosts from '@/components/sections/featured-posts';
|
||||
---
|
||||
|
||||
<DefaultLayout title={SITE_TITLE} description={SITE_DESCRIPTION}>
|
||||
<FeaturedPosts />
|
||||
</DefaultLayout>
|
||||
10
src/pages/latest.astro
Normal file
10
src/pages/latest.astro
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
import DefaultLayout from '@/layouts/DefaultLayout.astro';
|
||||
import { SITE_TITLE, SITE_DESCRIPTION } from '../consts';
|
||||
|
||||
import LatestPosts from '@/components/sections/latest-posts';
|
||||
---
|
||||
|
||||
<DefaultLayout title={SITE_TITLE} description={SITE_DESCRIPTION}>
|
||||
<LatestPosts client:only='react' />
|
||||
</DefaultLayout>
|
||||
9
src/pages/login.astro
Normal file
9
src/pages/login.astro
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
---
|
||||
import DefaultLayout from '@/layouts/DefaultLayout.astro';
|
||||
import { SITE_TITLE, SITE_DESCRIPTION } from '../consts';
|
||||
import LoginSection from '@/components/sections/login-section';
|
||||
---
|
||||
|
||||
<DefaultLayout title={SITE_TITLE} description={SITE_DESCRIPTION}>
|
||||
<LoginSection />
|
||||
</DefaultLayout>
|
||||
30
src/pages/magic/[...slug].astro
Normal file
30
src/pages/magic/[...slug].astro
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
---
|
||||
import { type CollectionEntry, getCollection } from 'astro:content';
|
||||
import DefaultLayout from '@/layouts/DefaultLayout.astro';
|
||||
import { BlogPost } from '@/components/sections/blog-post';
|
||||
import { SITE_TITLE, SITE_DESCRIPTION } from '@/consts';
|
||||
import NewsletterSignup from '@/components/sections/newsletter-signup';
|
||||
import { render } from 'astro:content';
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const posts = await getCollection('magic');
|
||||
return posts.map((post) => ({
|
||||
params: { slug: post.id },
|
||||
props: post,
|
||||
}));
|
||||
}
|
||||
type Props = CollectionEntry<'magic'>;
|
||||
|
||||
const post = Astro.props;
|
||||
const { Content } = await render(post);
|
||||
|
||||
---
|
||||
<DefaultLayout title={`${post.data.title} | ${SITE_TITLE}`} description={post.data.description}>
|
||||
<div class="py-16 md:py-28 lg:py-32">
|
||||
<BlogPost post={post} client:only='react'>
|
||||
<Content />
|
||||
</BlogPost>
|
||||
</div>
|
||||
|
||||
<NewsletterSignup client:load />
|
||||
</DefaultLayout>
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue