Restructure blogs as Astro monorepo
Some checks are pending
Build / build (push) Waiting to run

This commit is contained in:
Neeldhara Misra 2026-06-13 21:15:16 +02:00
parent fb5a3b8093
commit 58d8b661a8
1055 changed files with 116254 additions and 89 deletions

View file

@ -0,0 +1,75 @@
export const BLOG_SITES = [
{
key: "research",
title: "Research",
description:
"Notes from papers I am reading, and mini-surveys of themes I'm learning about.",
url: "https://research.neeldhara.blog",
},
{
key: "vibes",
title: "Vibes",
description:
"Conversations with LLMs with an intent to create something meaningful.",
url: "https://vibes.neeldhara.blog",
},
{
key: "puzzles",
title: "Puzzles",
description:
"Problems, puzzles, contest editorials, and mathematical magic.",
url: "https://puzzles.neeldhara.blog",
},
{
key: "reflections",
title: "Reflections",
description: "Random musings, pseudo-advice, how-tos. Core dumped.",
url: "https://reflections.neeldhara.blog",
},
{
key: "poetry",
title: "Poetry",
description: "Failed attempts at organizing thoughts in verse.",
url: "https://poetry.neeldhara.blog",
},
{
key: "reviews",
title: "Reviews",
description:
"Reviews of things I use or consume: software, books, hardware, etc.",
url: "https://reviews.neeldhara.blog",
},
{
key: "art",
title: "Art",
description:
"This exists so I can stay accountable to my doodling practice :)",
url: "https://art.neeldhara.blog",
},
];
export const DEFAULT_BLOG_SITE_KEY = "poetry";
export const BLOG_SITE_KEYS = BLOG_SITES.map((site) => site.key);
function getEnvironmentBlogSiteKey() {
if (typeof process === "undefined") {
return undefined;
}
return process.env?.BLOG_SITE;
}
export function getBlogSite(key = getEnvironmentBlogSiteKey()) {
const normalizedKey = key || DEFAULT_BLOG_SITE_KEY;
const site = BLOG_SITES.find((candidate) => candidate.key === normalizedKey);
if (!site) {
throw new Error(
`Unknown BLOG_SITE "${normalizedKey}". Expected one of: ${BLOG_SITE_KEYS.join(", ")}`,
);
}
return site;
}
export const ACTIVE_BLOG_SITE = getBlogSite();

View 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} />

View 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>
);
}

View 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 };

View 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>
);
};

View file

@ -0,0 +1,229 @@
import React, { useState, useMemo } from 'react';
interface Cycle {
elements: number[];
length: number;
}
interface PermutationData {
permutation: number[];
cycles: Cycle[];
hasLongCycle: boolean;
id: string;
}
// Generate all permutations of [1, 2, ..., n]
function generatePermutations(n: number): number[][] {
if (n === 1) return [[1]];
const result: number[][] = [];
const smaller = generatePermutations(n - 1);
for (const perm of smaller) {
for (let i = 0; i <= perm.length; i++) {
const newPerm = [...perm.slice(0, i), n, ...perm.slice(i)];
result.push(newPerm);
}
}
return result;
}
// Find cycles in a permutation
function findCycles(permutation: number[]): Cycle[] {
const n = permutation.length;
const visited = new Array(n).fill(false);
const cycles: Cycle[] = [];
for (let i = 0; i < n; i++) {
if (!visited[i]) {
const cycle: number[] = [];
let current = i;
while (!visited[current]) {
visited[current] = true;
cycle.push(current + 1); // Convert to 1-indexed
current = permutation[current] - 1; // Convert back to 0-indexed
}
cycles.push({
elements: cycle,
length: cycle.length,
});
}
}
return cycles.sort((a, b) => b.length - a.length);
}
const PermutationExplorer: React.FC = () => {
const [n, setN] = useState(3);
const [filter, setFilter] = useState<'all' | 'long' | 'short'>('all');
const allPermutations = useMemo(() => {
const perms = generatePermutations(n);
return perms.map((perm, idx) => {
const cycles = findCycles(perm);
const hasLongCycle = cycles.some(c => c.length > n / 2);
return {
permutation: perm,
cycles,
hasLongCycle,
id: `perm-${n}-${idx}`,
};
});
}, [n]);
const filteredPermutations = useMemo(() => {
if (filter === 'all') return allPermutations;
if (filter === 'long') return allPermutations.filter(p => p.hasLongCycle);
return allPermutations.filter(p => !p.hasLongCycle);
}, [allPermutations, filter]);
const stats = useMemo(() => {
const withLong = allPermutations.filter(p => p.hasLongCycle).length;
const withoutLong = allPermutations.length - withLong;
return { total: allPermutations.length, withLong, withoutLong };
}, [allPermutations]);
const formatCycleNotation = (cycles: Cycle[]) => {
return cycles.map(cycle => `(${cycle.elements.join(' ')})`).join(' ');
};
const formatStandardNotation = (perm: number[]) => {
return `[${perm.join(', ')}]`;
};
return (
<div className="my-8 p-6 border-2 border-gray-300 rounded-lg bg-white">
<h3 className="text-xl font-semibold mb-4">Permutation Cycle Explorer</h3>
<div className="mb-6 space-y-4">
<div>
<label className="block text-md font-semibold mb-2">
Size (n): {n}
</label>
<div className="flex items-center space-x-4">
{[3, 4, 5].map(size => (
<button
key={size}
onClick={() => setN(size)}
className={`px-4 py-2 rounded font-semibold transition-colors ${
n === size
? 'bg-blue-600 text-white'
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
}`}
>
{size}
</button>
))}
</div>
</div>
<div>
<label className="block text-md font-semibold mb-2">
Filter Permutations:
</label>
<div className="flex items-center space-x-4">
<button
onClick={() => setFilter('all')}
className={`px-4 py-2 rounded font-semibold transition-colors ${
filter === 'all'
? 'bg-blue-600 text-white'
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
}`}
>
All ({stats.total})
</button>
<button
onClick={() => setFilter('long')}
className={`px-4 py-2 rounded font-semibold transition-colors ${
filter === 'long'
? 'bg-red-600 text-white'
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
}`}
>
With Long Cycle ({stats.withLong})
</button>
<button
onClick={() => setFilter('short')}
className={`px-4 py-2 rounded font-semibold transition-colors ${
filter === 'short'
? 'bg-green-600 text-white'
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
}`}
>
Without Long Cycle ({stats.withoutLong})
</button>
</div>
</div>
</div>
<div className="mb-4 p-4 bg-blue-50 rounded">
<p className="text-sm">
<strong>Note:</strong> A "long cycle" is one with length &gt; n/2. For n={n},
that means cycles longer than {Math.floor(n / 2)}.
There are <strong>{stats.withLong}</strong> permutations with at least one long cycle
(about {((stats.withLong / stats.total) * 100).toFixed(1)}%).
</p>
</div>
<div className="space-y-3 max-h-96 overflow-y-auto">
{filteredPermutations.map((perm) => (
<div
key={perm.id}
className={`p-3 rounded border-2 ${
perm.hasLongCycle
? 'bg-red-50 border-red-300'
: 'bg-green-50 border-green-300'
}`}
>
<div className="grid grid-cols-1 md:grid-cols-2 gap-2 text-sm">
<div>
<span className="font-semibold">Standard:</span>{' '}
<code className="bg-white px-2 py-1 rounded">
{formatStandardNotation(perm.permutation)}
</code>
</div>
<div>
<span className="font-semibold">Cycles:</span>{' '}
<code className="bg-white px-2 py-1 rounded">
{formatCycleNotation(perm.cycles)}
</code>
</div>
</div>
<div className="mt-2 text-xs text-gray-600">
Cycle lengths: {perm.cycles.map(c => c.length).join(', ')}
{perm.hasLongCycle && (
<span className="ml-2 text-red-700 font-semibold">
Has long cycle!
</span>
)}
</div>
</div>
))}
</div>
<div className="mt-6 p-4 bg-gray-100 rounded">
<h4 className="font-semibold mb-2">Understanding the Notation:</h4>
<ul className="text-sm space-y-1 list-disc list-inside">
<li>
<strong>Standard notation [a, b, c, ...]:</strong> Position i contains value a[i].
For example, [2,3,1] means box 12, box 23, box 31.
</li>
<li>
<strong>Cycle notation (a b c ...):</strong> Shows the path abc...a.
For example, (1 2 3) means 1231.
</li>
<li>
Permutations with long cycles (shown in red) would cause the loop strategy to fail
for at least one prisoner.
</li>
</ul>
</div>
</div>
);
};
export default PermutationExplorer;

View file

@ -0,0 +1,222 @@
import React, { useState, useEffect } from 'react';
interface GameState {
n: number;
boxes: number[];
currentPrisoner: number;
openedBoxes: number[];
foundNumber: boolean;
gameOver: boolean;
won: boolean;
prisonersCompleted: number;
attemptsLeft: number;
}
const PrisonerGameSimulator: React.FC = () => {
const [n, setN] = useState(10);
const [gameState, setGameState] = useState<GameState | null>(null);
const initializeGame = (numPrisoners: number) => {
// Create a random permutation
const boxes = Array.from({ length: numPrisoners }, (_, i) => i + 1);
for (let i = boxes.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[boxes[i], boxes[j]] = [boxes[j], boxes[i]];
}
setGameState({
n: numPrisoners,
boxes,
currentPrisoner: 1,
openedBoxes: [],
foundNumber: false,
gameOver: false,
won: false,
prisonersCompleted: 0,
attemptsLeft: Math.floor(numPrisoners / 2),
});
};
useEffect(() => {
initializeGame(n);
}, []);
const handleBoxClick = (boxIndex: number) => {
if (!gameState || gameState.gameOver || gameState.openedBoxes.includes(boxIndex)) {
return;
}
const newOpenedBoxes = [...gameState.openedBoxes, boxIndex];
const numberInBox = gameState.boxes[boxIndex];
const foundTarget = numberInBox === gameState.currentPrisoner;
const newAttemptsLeft = gameState.attemptsLeft - 1;
if (foundTarget) {
// Prisoner found their number!
const newPrisonersCompleted = gameState.prisonersCompleted + 1;
if (newPrisonersCompleted === gameState.n) {
// All prisoners succeeded!
setGameState({
...gameState,
openedBoxes: newOpenedBoxes,
foundNumber: true,
gameOver: true,
won: true,
prisonersCompleted: newPrisonersCompleted,
attemptsLeft: newAttemptsLeft,
});
} else {
// Move to next prisoner
setGameState({
...gameState,
currentPrisoner: gameState.currentPrisoner + 1,
openedBoxes: [],
foundNumber: false,
prisonersCompleted: newPrisonersCompleted,
attemptsLeft: Math.floor(gameState.n / 2),
});
}
} else if (newAttemptsLeft === 0) {
// Ran out of attempts - game over
setGameState({
...gameState,
openedBoxes: newOpenedBoxes,
foundNumber: false,
gameOver: true,
won: false,
attemptsLeft: 0,
});
} else {
// Continue opening boxes
setGameState({
...gameState,
openedBoxes: newOpenedBoxes,
attemptsLeft: newAttemptsLeft,
});
}
};
const handleReset = () => {
initializeGame(n);
};
const handleNChange = (newN: number) => {
setN(newN);
initializeGame(newN);
};
if (!gameState) return null;
return (
<div className="my-8 p-6 border-2 border-gray-300 rounded-lg bg-gray-50">
<div className="mb-6">
<label className="block text-lg font-semibold mb-2">
Number of Prisoners: {n}
</label>
<input
type="range"
min="5"
max="100"
value={n}
onChange={(e) => handleNChange(parseInt(e.target.value))}
className="w-full h-2 bg-blue-200 rounded-lg appearance-none cursor-pointer"
disabled={gameState.prisonersCompleted > 0 && !gameState.gameOver}
/>
<div className="flex justify-between text-sm text-gray-600 mt-1">
<span>5</span>
<span>100</span>
</div>
</div>
<div className="mb-4 p-4 bg-blue-100 rounded">
{!gameState.gameOver && (
<>
<p className="text-lg font-semibold">
Prisoner #{gameState.currentPrisoner} is searching for their number
</p>
<p className="text-md">
Attempts remaining: {gameState.attemptsLeft} / {Math.floor(gameState.n / 2)}
</p>
<p className="text-md">
Prisoners completed: {gameState.prisonersCompleted} / {gameState.n}
</p>
</>
)}
{gameState.gameOver && gameState.won && (
<p className="text-2xl font-bold text-green-600">
🎉 SUCCESS! All {gameState.n} prisoners found their numbers!
</p>
)}
{gameState.gameOver && !gameState.won && (
<p className="text-2xl font-bold text-red-600">
GAME OVER! Prisoner #{gameState.currentPrisoner} failed to find their number.
</p>
)}
</div>
<div className="mb-4">
<h3 className="text-md font-semibold mb-2">Click on boxes to open them:</h3>
<div
className="grid gap-2"
style={{
gridTemplateColumns: `repeat(${Math.min(10, Math.ceil(Math.sqrt(gameState.n)))}, minmax(0, 1fr))`,
}}
>
{gameState.boxes.map((number, index) => {
const isOpened = gameState.openedBoxes.includes(index);
const isTarget = number === gameState.currentPrisoner && isOpened;
return (
<button
key={index}
onClick={() => handleBoxClick(index)}
disabled={gameState.gameOver || isOpened}
className={`
aspect-square p-2 rounded border-2 font-semibold text-sm
transition-all duration-200
${isOpened
? isTarget
? 'bg-green-400 border-green-600 text-white'
: 'bg-red-200 border-red-400 text-gray-700'
: 'bg-white border-gray-400 hover:bg-blue-100 hover:border-blue-500 cursor-pointer'
}
${gameState.gameOver || isOpened ? 'cursor-not-allowed' : ''}
`}
>
{isOpened ? (
<div className="flex flex-col items-center justify-center h-full">
<div className="text-xs">Box {index + 1}</div>
<div className="text-lg">{number}</div>
</div>
) : (
<div className="flex items-center justify-center h-full">
{index + 1}
</div>
)}
</button>
);
})}
</div>
</div>
<button
onClick={handleReset}
className="px-6 py-3 bg-blue-600 text-white font-semibold rounded-lg hover:bg-blue-700 transition-colors"
>
Reset Game
</button>
{gameState.openedBoxes.length > 0 && !gameState.gameOver && (
<div className="mt-4 p-3 bg-yellow-100 rounded">
<p className="text-sm">
<strong>Last opened:</strong> Box {gameState.openedBoxes[gameState.openedBoxes.length - 1] + 1}
{' → '}contains number {gameState.boxes[gameState.openedBoxes[gameState.openedBoxes.length - 1]]}
</p>
</div>
)}
</div>
);
};
export default PrisonerGameSimulator;

View file

@ -0,0 +1,225 @@
import React, { useMemo } from 'react';
interface ProbabilityChartProps {
strategy: 'naive' | 'loop';
}
const ProbabilityChart: React.FC<ProbabilityChartProps> = ({ strategy }) => {
const data = useMemo(() => {
const points = [];
const maxN = 50;
for (let n = 2; n <= maxN; n += 2) {
let probability;
if (strategy === 'naive') {
// Naive strategy: (1/2)^n
probability = Math.pow(0.5, n);
} else {
// Loop strategy: 1 - sum(1/k for k from n/2+1 to n)
let sum = 0;
for (let k = Math.floor(n / 2) + 1; k <= n; k++) {
sum += 1 / k;
}
probability = 1 - sum;
}
points.push({ n, probability });
}
return points;
}, [strategy]);
// Find min and max for scaling
const maxProb = Math.max(...data.map(d => d.probability));
const minProb = Math.min(...data.map(d => d.probability));
// Use log scale for naive strategy since values get extremely small
const useLogScale = strategy === 'naive';
const getY = (prob: number, height: number) => {
if (useLogScale) {
const logProb = prob > 0 ? Math.log10(prob) : -100;
const logMax = Math.log10(maxProb);
const logMin = Math.log10(minProb);
return height - ((logProb - logMin) / (logMax - logMin)) * height;
} else {
return height - (prob / maxProb) * height;
}
};
const chartWidth = 600;
const chartHeight = 300;
const padding = { top: 20, right: 20, bottom: 50, left: 70 };
const width = chartWidth - padding.left - padding.right;
const height = chartHeight - padding.top - padding.bottom;
// Create path for the line
const pathData = data.map((point, i) => {
const x = (point.n / 50) * width;
const y = getY(point.probability, height);
return `${i === 0 ? 'M' : 'L'} ${x} ${y}`;
}).join(' ');
// Y-axis labels
const yLabels = useLogScale
? [-5, -10, -15, -20, -25, -30].map(exp => ({
label: `10^${exp}`,
value: Math.pow(10, exp),
}))
: [0, 0.1, 0.2, 0.3, 0.4, 0.5].map(val => ({
label: val.toFixed(1),
value: val,
}));
return (
<div className="my-8 p-6 border-2 border-gray-300 rounded-lg bg-white">
<h3 className="text-xl font-semibold mb-4">
{strategy === 'naive' ? 'Naive Strategy: Random Box Selection' : 'Loop Strategy'}
</h3>
<svg width={chartWidth} height={chartHeight} className="mx-auto">
<g transform={`translate(${padding.left}, ${padding.top})`}>
{/* Grid lines */}
{yLabels.map(({ value }) => {
const y = getY(value, height);
return (
<line
key={value}
x1={0}
y1={y}
x2={width}
y2={y}
stroke="#e5e7eb"
strokeWidth={1}
/>
);
})}
{/* X-axis */}
<line
x1={0}
y1={height}
x2={width}
y2={height}
stroke="#374151"
strokeWidth={2}
/>
{/* Y-axis */}
<line
x1={0}
y1={0}
x2={0}
y2={height}
stroke="#374151"
strokeWidth={2}
/>
{/* Plot line */}
<path
d={pathData}
fill="none"
stroke="#3b82f6"
strokeWidth={3}
strokeLinecap="round"
strokeLinejoin="round"
/>
{/* Data points */}
{data.filter((_, i) => i % 2 === 0).map((point) => {
const x = (point.n / 50) * width;
const y = getY(point.probability, height);
return (
<circle
key={point.n}
cx={x}
cy={y}
r={4}
fill="#3b82f6"
/>
);
})}
{/* X-axis labels */}
{[10, 20, 30, 40, 50].map((n) => {
const x = (n / 50) * width;
return (
<text
key={n}
x={x}
y={height + 25}
textAnchor="middle"
fontSize="14"
fill="#374151"
>
{n}
</text>
);
})}
{/* Y-axis labels */}
{yLabels.map(({ label, value }) => {
const y = getY(value, height);
return (
<text
key={value}
x={-10}
y={y + 5}
textAnchor="end"
fontSize="12"
fill="#374151"
>
{label}
</text>
);
})}
{/* X-axis title */}
<text
x={width / 2}
y={height + 45}
textAnchor="middle"
fontSize="16"
fontWeight="600"
fill="#374151"
>
Number of Prisoners (n)
</text>
{/* Y-axis title */}
<text
x={-height / 2}
y={-50}
textAnchor="middle"
fontSize="16"
fontWeight="600"
fill="#374151"
transform={`rotate(-90, ${-height / 2}, -50)`}
>
Probability of Success
</text>
</g>
</svg>
<div className="mt-4 p-4 bg-gray-100 rounded">
<p className="text-sm">
{strategy === 'naive' ? (
<>
The naive strategy has probability <strong>(1/2)^n</strong> which decreases
exponentially. Note the logarithmic scale on the y-axis!
With 20 prisoners, the probability is already less than one in a million.
</>
) : (
<>
The loop strategy maintains a probability around <strong>31%</strong> regardless
of the number of prisoners, approaching 1 - ln(2) 0.3069 as n .
</>
)}
</p>
</div>
</div>
);
};
export default ProbabilityChart;

View file

@ -0,0 +1,116 @@
# 100 Prisoners Problem - Interactive Blog Post
This directory contains the interactive React components for the "100 Prisoners Problem" blog post, inspired by the interactive style of Nicky Case's explorable explanations.
## Files Created
### Blog Post
- **`src/content/puzzles/100-prisoners.mdx`** - The main blog post with embedded interactive components
### Interactive Components
1. **`PrisonerGameSimulator.tsx`** - Interactive game simulation
- Allows users to play the prisoner game themselves
- Adjustable number of prisoners (5-100)
- Manual box clicking to experience the problem
- Visual feedback for success/failure
- Tracks current prisoner and attempts remaining
2. **`ProbabilityChart.tsx`** - Probability visualization
- Shows probability curves for naive and loop strategies
- Uses logarithmic scale for naive strategy (values get extremely small)
- Interactive SVG chart with proper axes and labels
- Includes explanatory text
3. **`PermutationExplorer.tsx`** - Cycle structure explorer
- Generates all permutations for n ∈ {3, 4, 5}
- Displays both standard and cycle notation
- Filter by presence of long cycles (> n/2)
- Color-coded display (red for long cycles, green for short)
- Shows statistics about cycle length distribution
4. **`StrategyComparison.tsx`** - Side-by-side strategy comparison
- Compares naive vs. loop strategy on same chart
- Shows probabilities up to n=100
- Includes detailed numerical breakdown
- Highlights the key 31.18% result at n=100
## Features
All components are:
- ✅ Fully interactive with React hooks
- ✅ Styled with Tailwind CSS classes
- ✅ Responsive for mobile and desktop
- ✅ Self-contained with no external dependencies beyond React
- ✅ Client-side only (using `client:only="react"` in Astro)
## How It Works
### The Problem
100 prisoners must each find their own number among 100 boxes by opening at most 50 boxes. If all prisoners succeed, they go free. They can agree on a strategy beforehand but cannot communicate during the challenge.
### The Naive Strategy
Each prisoner opens 50 random boxes. Probability of success: (1/2)^100 ≈ 0%
### The Clever Strategy (Loop Following)
1. Open the box with your own number
2. Look at the number inside
3. Open the box with that number
4. Repeat until you find your number
This exploits the cycle structure of permutations. Success probability: ~31.18%!
### Key Mathematical Insight
The prisoners succeed if and only if the permutation of numbers in boxes has no cycle longer than n/2. For n=100, the probability of this is:
P(success) = 1 - Σ(1/k) for k=51 to 100 ≈ 0.3118
As n→∞, this converges to 1 - ln(2) ≈ 0.3069.
## Usage in MDX
The components are imported and used like this:
\`\`\`mdx
import PrisonerGameSimulator from '@/components/problems/PrisonerGameSimulator.tsx';
import ProbabilityChart from '@/components/problems/ProbabilityChart.tsx';
import PermutationExplorer from '@/components/problems/PermutationExplorer.tsx';
import StrategyComparison from '@/components/problems/StrategyComparison.tsx';
<PrisonerGameSimulator client:only="react" />
<ProbabilityChart strategy="naive" client:only="react" />
<PermutationExplorer client:only="react" />
<StrategyComparison client:only="react" />
\`\`\`
## Design Philosophy
Inspired by Nicky Case's "Parable of the Polygons", these components aim to:
- Make abstract mathematical concepts tangible through interaction
- Allow readers to explore and discover patterns themselves
- Provide immediate visual feedback
- Balance playfulness with mathematical rigor
- Guide understanding through progressive disclosure
## Future Enhancements
Potential additions:
- Auto-play mode for the game simulator using the loop strategy
- Animation showing cycle formation in real-time
- Monte Carlo simulation running multiple trials
- Variant problems (different box-opening rules, malicious director, etc.)
- 3D visualization of cycle structures for larger n
## References
- [Wikipedia: 100 Prisoners Problem](https://en.wikipedia.org/wiki/100_prisoners_problem)
- [Nicky Case's Explorable Explanations](https://ncase.me/)
- Original problem: Gál & Miltersen (2003)

View file

@ -0,0 +1,313 @@
import React, { useMemo } from 'react';
const StrategyComparison: React.FC = () => {
const data = useMemo(() => {
const points = [];
const maxN = 100;
for (let n = 5; n <= maxN; n += 5) {
// Naive strategy
const naiveProb = Math.pow(0.5, n);
// Loop strategy: 1 - sum(1/k for k from n/2+1 to n)
let sum = 0;
for (let k = Math.floor(n / 2) + 1; k <= n; k++) {
sum += 1 / k;
}
const loopProb = 1 - sum;
points.push({ n, naiveProb, loopProb });
}
return points;
}, []);
const chartWidth = 700;
const chartHeight = 350;
const padding = { top: 20, right: 100, bottom: 50, left: 70 };
const width = chartWidth - padding.left - padding.right;
const height = chartHeight - padding.top - padding.bottom;
// Only show loop strategy on linear scale (naive is too small to see)
const maxProb = 0.5;
const getY = (prob: number) => {
return height - (prob / maxProb) * height;
};
const getX = (n: number) => {
return (n / 100) * width;
};
// Create paths for both strategies
const loopPath = data.map((point, i) => {
const x = getX(point.n);
const y = getY(point.loopProb);
return `${i === 0 ? 'M' : 'L'} ${x} ${y}`;
}).join(' ');
// For naive, we'll just show it near zero
const naivePath = data.map((point, i) => {
const x = getX(point.n);
const y = height - 2; // Near the bottom (representing ~0)
return `${i === 0 ? 'M' : 'L'} ${x} ${y}`;
}).join(' ');
return (
<div className="my-8 p-6 border-2 border-gray-300 rounded-lg bg-white">
<h3 className="text-xl font-semibold mb-4">Strategy Comparison</h3>
<svg width={chartWidth} height={chartHeight} className="mx-auto">
<g transform={`translate(${padding.left}, ${padding.top})`}>
{/* Grid lines */}
{[0, 0.1, 0.2, 0.3, 0.4, 0.5].map((val) => {
const y = getY(val);
return (
<g key={val}>
<line
x1={0}
y1={y}
x2={width}
y2={y}
stroke="#e5e7eb"
strokeWidth={1}
/>
<text
x={-10}
y={y + 5}
textAnchor="end"
fontSize="12"
fill="#374151"
>
{val.toFixed(1)}
</text>
</g>
);
})}
{/* X-axis */}
<line
x1={0}
y1={height}
x2={width}
y2={height}
stroke="#374151"
strokeWidth={2}
/>
{/* Y-axis */}
<line
x1={0}
y1={0}
x2={0}
y2={height}
stroke="#374151"
strokeWidth={2}
/>
{/* Loop strategy line */}
<path
d={loopPath}
fill="none"
stroke="#10b981"
strokeWidth={3}
strokeLinecap="round"
strokeLinejoin="round"
/>
{/* Naive strategy line (at the bottom) */}
<path
d={naivePath}
fill="none"
stroke="#ef4444"
strokeWidth={3}
strokeLinecap="round"
strokeLinejoin="round"
strokeDasharray="5,5"
/>
{/* Data points for loop strategy */}
{data.filter((_, i) => i % 2 === 0).map((point) => {
const x = getX(point.n);
const y = getY(point.loopProb);
return (
<circle
key={point.n}
cx={x}
cy={y}
r={4}
fill="#10b981"
/>
);
})}
{/* X-axis labels */}
{[20, 40, 60, 80, 100].map((n) => {
const x = getX(n);
return (
<text
key={n}
x={x}
y={height + 25}
textAnchor="middle"
fontSize="14"
fill="#374151"
>
{n}
</text>
);
})}
{/* X-axis title */}
<text
x={width / 2}
y={height + 45}
textAnchor="middle"
fontSize="16"
fontWeight="600"
fill="#374151"
>
Number of Prisoners (n)
</text>
{/* Y-axis title */}
<text
x={-height / 2}
y={-50}
textAnchor="middle"
fontSize="16"
fontWeight="600"
fill="#374151"
transform={`rotate(-90, ${-height / 2}, -50)`}
>
Probability of Success
</text>
{/* Legend */}
<g transform={`translate(${width + 20}, 20)`}>
<rect x={0} y={0} width={15} height={15} fill="#10b981" />
<text x={20} y={12} fontSize="14" fill="#374151">
Loop Strategy
</text>
<line
x1={0}
y1={35}
x2={15}
y2={35}
stroke="#ef4444"
strokeWidth={3}
strokeDasharray="5,5"
/>
<text x={20} y={40} fontSize="14" fill="#374151">
Naive Strategy
</text>
<text x={20} y={55} fontSize="11" fill="#6b7280">
( 0 for all n)
</text>
</g>
{/* Highlight key value */}
<g>
{(() => {
const n100Point = data.find(p => p.n === 100);
if (n100Point) {
const x = getX(100);
const y = getY(n100Point.loopProb);
return (
<>
<circle
cx={x}
cy={y}
r={6}
fill="none"
stroke="#10b981"
strokeWidth={2}
/>
<line
x1={x}
y1={y - 10}
x2={x}
y2={y - 40}
stroke="#374151"
strokeWidth={1}
/>
<text
x={x}
y={y - 45}
textAnchor="middle"
fontSize="12"
fontWeight="600"
fill="#10b981"
>
31.18%
</text>
</>
);
}
return null;
})()}
</g>
</g>
</svg>
<div className="mt-6 grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="p-4 bg-red-50 border-2 border-red-300 rounded">
<h4 className="font-semibold text-red-900 mb-2"> Naive Strategy</h4>
<p className="text-sm mb-2">
Each prisoner randomly selects 50 boxes.
</p>
<div className="text-xs space-y-1">
<p><strong>n=10:</strong> P {(Math.pow(0.5, 10) * 100).toExponential(2)}%</p>
<p><strong>n=20:</strong> P {(Math.pow(0.5, 20) * 100).toExponential(2)}%</p>
<p><strong>n=50:</strong> P {(Math.pow(0.5, 50) * 100).toExponential(2)}%</p>
<p><strong>n=100:</strong> P {(Math.pow(0.5, 100) * 100).toExponential(2)}%</p>
</div>
<p className="text-xs mt-2 italic">
Essentially impossible for any reasonable n!
</p>
</div>
<div className="p-4 bg-green-50 border-2 border-green-300 rounded">
<h4 className="font-semibold text-green-900 mb-2"> Loop Strategy</h4>
<p className="text-sm mb-2">
Each prisoner follows the cycle starting at their number.
</p>
<div className="text-xs space-y-1">
{(() => {
const calcProb = (n: number) => {
let sum = 0;
for (let k = Math.floor(n / 2) + 1; k <= n; k++) {
sum += 1 / k;
}
return ((1 - sum) * 100).toFixed(2);
};
return (
<>
<p><strong>n=10:</strong> P {calcProb(10)}%</p>
<p><strong>n=20:</strong> P {calcProb(20)}%</p>
<p><strong>n=50:</strong> P {calcProb(50)}%</p>
<p><strong>n=100:</strong> P {calcProb(100)}%</p>
</>
);
})()}
</div>
<p className="text-xs mt-2 italic">
Converges to 1 - ln(2) 30.69% as n
</p>
</div>
</div>
<div className="mt-4 p-4 bg-blue-50 rounded">
<p className="text-sm">
<strong>Key Insight:</strong> The loop strategy transforms an impossible problem
(probability 10<sup>-30</sup>) into a reasonable one (probability 31%).
This astronomical improvement comes from exploiting the mathematical structure
of permutations rather than relying on independent random trials.
</p>
</div>
</div>
);
};
export default StrategyComparison;

View 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&apos;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&apos;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&apos;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&apos;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&apos;re focused on
delivering for developers. If you&apos;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 />
</>
);
}

View 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>
);
}

View file

@ -0,0 +1,57 @@
import { ChevronRight } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
} from "@/components/ui/card";
import { BLOG_SITES } from "@/blog-sites.js";
const features = BLOG_SITES.map((site) => ({
title: site.title,
description: site.description,
href: site.url,
}));
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-1 gap-2.5 sm:grid-cols-2 md:mt-12 lg:mt-20 lg:grid-cols-3 lg:gap-6">
{features.map((feature, index) => (
<Card key={index} className="flex flex-col">
<CardHeader className="max-md:p-3">
<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>
);
}

View 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 };

View file

@ -0,0 +1,135 @@
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);
const hrefFor = (post: any) =>
collection ? `/${collection}/${post.id}/` : `/${post.id}/`;
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={hrefFor(featuredPost)}
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={hrefFor(post)}
>
<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 };

View 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>
);
}

View 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>
);
}

View 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 />
</>
);
}

View 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;

View 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>
);
}

View 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>
);
}

View 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;

View 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>
</>
);
}

View 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>
);
}

View 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&apos;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;

View 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>
);
}

View 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;

View 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;

View 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>
);
}

View 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>
);
};

View 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;

View 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>
);
}

View 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;

View 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 };

View 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 }

View 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 }

View 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 };

View 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,
};

View 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,
};

View 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 };

View 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 };

View 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 };

View 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 };

View 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,
};

View 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 }

View 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 };

View 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 };

View file

@ -0,0 +1,55 @@
// Place any global data in this file.
// You can import this data from anywhere in your site by using the `import` keyword.
import { ACTIVE_BLOG_SITE } from "./blog-sites.js";
export const SITE_TITLE = ACTIVE_BLOG_SITE.title;
export const SITE_DESCRIPTION = ACTIVE_BLOG_SITE.description;
export const SITE_URL = ACTIVE_BLOG_SITE.url;
export const SITE_METADATA = {
title: {
default: SITE_TITLE,
template: `%s | ${SITE_TITLE}`,
},
description: SITE_DESCRIPTION,
keywords: ["Neeldhara", "blog", SITE_TITLE, "research", "writing"],
authors: [{ name: "Neeldhara" }],
creator: "Neeldhara",
publisher: "Neeldhara",
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: SITE_TITLE,
images: [
{
url: "/og-image.jpg",
width: 1200,
height: 630,
alt: SITE_TITLE,
},
],
},
twitter: {
card: "summary_large_image",
title: SITE_TITLE,
description: SITE_DESCRIPTION,
images: ["/og-image.jpg"],
creator: "@neeldhara",
},
};

View 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
![Red-Black Tree Rotation](sketch-placeholder)
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.

View 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.

View file

@ -0,0 +1,63 @@
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 research = defineCollection({
loader: glob({
base: "./src/content/research",
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 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 = {
research,
vibes,
puzzles,
reflections,
poetry,
reviews,
art,
};

View 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.

View 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.

View file

@ -0,0 +1,183 @@
---
title: "The 100 Prisoners Problem: A Beautiful Puzzle in Probability"
description: "An interactive exploration of one of the most counterintuitive puzzles in probability theory, where 100 prisoners can escape with over 30% chance using a clever strategy."
pubDate: "Oct 13 2025"
image: "https://images.unsplash.com/photo-1589829545856-d10d557cf95f?w=400&auto=format&fit=crop&q=60"
authorImage: "/avatar/avatar1.png"
authorName: "Neeldhara"
---
import PrisonerGameSimulator from '@/components/problems/PrisonerGameSimulator.tsx';
import ProbabilityChart from '@/components/problems/ProbabilityChart.tsx';
import PermutationExplorer from '@/components/problems/PermutationExplorer.tsx';
import StrategyComparison from '@/components/problems/StrategyComparison.tsx';
# The 100 Prisoners Problem
Imagine 100 prisoners, numbered 1 to 100, face an unusual challenge. In a room are 100 boxes, also numbered 1 to 100. Inside each box is a randomly assigned number from 1 to 100 (each number appearing exactly once). The prisoners must find their own number by opening boxes, but here's the catch: **each prisoner can open only 50 boxes**.
The prisoners can devise a strategy beforehand, but once the challenge begins, they cannot communicate. If **all 100 prisoners** find their number, everyone goes free. If even one prisoner fails, all remain imprisoned.
What's the best strategy? And what are the odds of success?
## Try It Yourself!
Before we dive into the mathematics, let's experience the problem firsthand. Below is a simulator where you can play the game yourself. Choose the number of prisoners (between 5 and 100), and then try to help each prisoner find their number by clicking on boxes.
<PrisonerGameSimulator client:only="react" />
---
## The Naive Strategy
The most obvious approach is for each prisoner to simply open 50 random boxes. This seems reasonable, but the probability of success is dismal.
For any single prisoner, the probability of finding their number in 50 random boxes out of 100 is exactly 1/2. Since all 100 prisoners must succeed, and their outcomes are independent under random selection, the probability that everyone succeeds is:
$$P(\text\{success\}) = \left(\frac\{1\}\{2\}\right)^\{100\} \approx 7.9 \times 10^\{-31\}$$
That's essentially zero! You're more likely to win the lottery while being struck by lightning... multiple times.
### Naive Strategy: Probability vs Number of Prisoners
Watch how the probability of success plummets as we increase the number of prisoners:
<ProbabilityChart strategy="naive" client:only="react" />
As you can see, even with just 10 prisoners, the probability drops to about 0.1%. With 100 prisoners, it's practically impossible.
---
## The Clever Strategy: Following the Loops
Here's where mathematics reveals something beautiful. Instead of opening random boxes, each prisoner should follow a simple rule:
1. **Start by opening the box with your own number**
2. **Look at the number inside that box**
3. **Open the box with that number next**
4. **Continue following this chain**
Why does this work? The key insight involves understanding permutations and cycles.
### Understanding Cycles in Permutations
When we randomly assign 100 numbers to 100 boxes, we're creating a **permutation** of the numbers 1 through 100. Every permutation can be decomposed into **cycles**.
For example, if:
- Box 1 contains number 4
- Box 4 contains number 7
- Box 7 contains number 1
Then we have a cycle: 1 -> 4 -> 7 -> 1
Let's explore this with smaller numbers:
<PermutationExplorer client:only="react" />
The interactive explorer above lets you:
- Generate all permutations for small values of *n* (3, 4, or 5)
- See both standard notation and cycle notation
- Filter permutations that have at least one "long cycle" (longer than *n*/2)
- Understand why long cycles matter
### Why This Strategy Works
Here's the crucial insight: **A prisoner succeeds if and only if they are in a cycle of length ≤ 50**.
When prisoner k follows the loop strategy:
1. They start at box k
2. They follow a path through boxes determined by the permutation
3. This path forms a cycle that eventually leads back to box k
4. If this cycle has length ≤ 50, they'll find their number within 50 opens
The prisoners **all succeed** if and only if there is **no cycle longer than 50** in the permutation.
### Calculating the Probability
The probability that a random permutation of 100 elements has no cycle longer than 50 is:
$$P(\text\{success\}) = 1 - \sum_\{k=51\}^\{100\} \frac\{1\}\{k\}$$
Computing this sum:
$$P(\text\{success\}) = 1 - \left(\frac\{1\}\{51\} + \frac\{1\}\{52\} + \cdots + \frac\{1\}\{100\}\right) \approx 0.3118$$
That's over **31%**! This is astronomically better than the naive strategy's probability of essentially zero.
### Loop Strategy: Probability vs Number of Prisoners
<StrategyComparison client:only="react" />
## The Mathematics Behind It
### Why Does the Cycle Length Matter?
In a permutation of n elements, prisoner i will find their number within k tries using the loop strategy if and only if i belongs to a cycle of length at most k.
For n = 100 and k = 50:
- The probability that a specific prisoner is in a cycle of length > 50 is: Σ(j=51 to 100) of 1/100
- But we care about whether ANY prisoner is in such a cycle
- The probability that NO cycle has length > 50 is: 1 - Σ(j=51 to 100) of 1/j
### The Harmonic Series Connection
As *n* approaches infinity, if each prisoner can open *n*/2 boxes, the probability of success approaches:
The limit as n approaches infinity is: P_n = 1 - ln(2) ≈ 0.3069
This beautiful result connects combinatorics, probability, and analysis through the harmonic series!
### Optimality
Remarkably, this loop-following strategy is **optimal**. No other strategy can achieve a higher probability of success. The proof involves showing that the problem reduces to avoiding long cycles in a random permutation, and the loop strategy is the unique way to leverage this structure.
---
## Why This Problem is Beautiful
The 100 prisoners problem is beloved by mathematicians because:
1. **Counterintuitive Result**: The fact that 31% >> 0% is shocking. A seemingly hopeless problem becomes tractable with the right insight.
2. **Deep Connection**: It connects permutation theory, probability, and the harmonic series in an elegant way.
3. **Collaborative Success**: Unlike independent trials, the prisoners' fates are intertwined through the structure of the permutation.
4. **Real-World Metaphor**: It illustrates how understanding underlying structure (cycles in permutations) can dramatically improve outcomes.
---
## Variants and Extensions
### What if prisoners can open k < n/2 boxes?
As k decreases below n/2, the probability drops rapidly. The threshold at n/2 is special—it's where the strategy becomes remarkably effective.
### What about communication?
If prisoners could communicate between trials (but not during), they still can't improve the strategy. The beauty is that the strategy requires only coordination before any prisoner enters, not during.
### The Malicious Director
In an interesting variant, suppose the director can see the prisoners' strategy and arrange the boxes adversarially (not randomly). Can the director always force them to fail? Yes! The director can always create a single cycle of length n, guaranteeing failure.
---
## Conclusion
The 100 prisoners problem shows us that:
- Mathematical structure can be hidden in apparent randomness
- Clever strategies can overcome seemingly impossible odds
- Group coordination can be more powerful than independent action
- Beautiful mathematics often lies beneath simple-seeming puzzles
The next time you face a problem that seems impossible, remember: there might be a hidden structure waiting to be discovered, turning hopeless odds into reasonable ones.
---
## Further Reading
- [Gál, A., & Miltersen, P. B. (2003). "The Cell Probe Complexity of Succinct Data Structures"](https://arxiv.org/abs/cs/0310027)
- [Curtin, E., & Warshauer, M. (2006). "The Locker Puzzle"](https://www.jstor.org/stable/27642173)
- [Wikipedia: 100 Prisoners Problem](https://en.wikipedia.org/wiki/100_prisoners_problem)

View 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.

View 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.

View 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.

View 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.

View 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.

View 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.

View 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.

View 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!

View 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.

View 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.

View 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.

View 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.

View 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.

View 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.

View 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.

View 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.

View 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.

View 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.

View 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.

View 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.

View 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.

1
sites/poetry/src/env.d.ts vendored Normal file
View file

@ -0,0 +1 @@
/// <reference types="astro/client" />

View 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
}

View 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>

View 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>

View 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));
}

View 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>

View file

@ -0,0 +1,29 @@
---
import { getCollection, render } from 'astro:content';
import DefaultLayout from '@/layouts/DefaultLayout.astro';
import { BlogPost } from '@/components/sections/blog-post';
import NewsletterSignup from '@/components/sections/newsletter-signup';
import { ACTIVE_BLOG_SITE } from '@/blog-sites.js';
import { SITE_TITLE } from '@/consts';
export async function getStaticPaths() {
const posts = await getCollection(ACTIVE_BLOG_SITE.key);
return posts.map((post) => ({
params: { slug: post.id },
props: post,
}));
}
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>

View 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>

View 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>

View 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>

View file

@ -0,0 +1,23 @@
---
import { getCollection } from 'astro:content';
import DefaultLayout from '@/layouts/DefaultLayout.astro';
import { SITE_TITLE, SITE_DESCRIPTION } from '@/consts';
import { ACTIVE_BLOG_SITE } from '@/blog-sites.js';
import { BlogPosts } from '@/components/sections/blog-posts';
const posts = (await getCollection(ACTIVE_BLOG_SITE.key)).sort(
(a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf(),
);
---
<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">{ACTIVE_BLOG_SITE.title}</h1>
<p class="text-xl text-muted-foreground">{ACTIVE_BLOG_SITE.description}</p>
</div>
<BlogPosts posts={posts} client:only="react" />
</div>
</div>
</DefaultLayout>

View 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>

View 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>

View file

@ -0,0 +1,201 @@
---
layout: ../layouts/BasicLayout.astro
title: Privacy
---
# Privacy Policy
Last updated: April 07, 2021
This Privacy Policy describes Our policies and procedures on the collection, use and disclosure of Your information when You use the Service and tells You about Your privacy rights and how the law protects You.
We use Your Personal data to provide and improve the Service. By using the Service, You agree to the collection and use of information in accordance with this Privacy Policy. This Privacy Policy has been created with the help of the [Privacy Policy Generator](https://www.privacypolicies.com/privacy-policy-generator/).
# Interpretation and Definitions
## Interpretation
The words of which the initial letter is capitalized have meanings defined under the following conditions. The following definitions shall have the same meaning regardless of whether they appear in singular or in plural.
## Definitions
For the purposes of this Privacy Policy:
- **Account** means a unique account created for You to access our Service or parts of our Service.
- **Company** (referred to as either "the Company", "We", "Us" or "Our" in this Agreement) refers to Example Site.
- **Cookies** are small files that are placed on Your computer, mobile device or any other device by a website, containing the details of Your browsing history on that website among its many uses.
- **Country** refers to: California, United States
- **Device** means any device that can access the Service such as a computer, a cellphone or a digital tablet.
- **Personal Data** is any information that relates to an identified or identifiable individual.
- **Service** refers to the Website.
- **Service Provider** means any natural or legal person who processes the data on behalf of the Company. It refers to third-party companies or individuals employed by the Company to facilitate the Service, to provide the Service on behalf of the Company, to perform services related to the Service or to assist the Company in analyzing how the Service is used.
- **Third-party Social Media Service** refers to any website or any social network website through which a User can log in or create an account to use the Service.
- **Usage Data** refers to data collected automatically, either generated by the use of the Service or from the Service infrastructure itself (for example, the duration of a page visit).
- **Website** refers to Example Site, accessible from [www.example.com](https://www.example.com)
- **You** means the individual accessing or using the Service, or the company, or other legal entity on behalf of which such individual is accessing or using the Service, as applicable.
# Collecting and Using Your Personal Data
## Types of Data Collected
### Personal Data
While using Our Service, We may ask You to provide Us with certain personally identifiable information that can be used to contact or identify You. Personally identifiable information may include, but is not limited to:
- Email address
- First name and last name
- Usage Data
### Usage Data
Usage Data is collected automatically when using the Service.
Usage Data may include information such as Your Device's Internet Protocol address (e.g. IP address), browser type, browser version, the pages of our Service that You visit, the time and date of Your visit, the time spent on those pages, unique device identifiers and other diagnostic data.
When You access the Service by or through a mobile device, We may collect certain information automatically, including, but not limited to, the type of mobile device You use, Your mobile device unique ID, the IP address of Your mobile device, Your mobile operating system, the type of mobile Internet browser You use, unique device identifiers and other diagnostic data.
We may also collect information that Your browser sends whenever You visit our Service or when You access the Service by or through a mobile device.
### Tracking Technologies and Cookies
We use Cookies and similar tracking technologies to track the activity on Our Service and store certain information. Tracking technologies used are beacons, tags, and scripts to collect and track information and to improve and analyze Our Service. The technologies We use may include:
- **Cookies or Browser Cookies.** A cookie is a small file placed on Your Device. You can instruct Your browser to refuse all Cookies or to indicate when a Cookie is being sent. However, if You do not accept Cookies, You may not be able to use some parts of our Service. Unless you have adjusted Your browser setting so that it will refuse Cookies, our Service may use Cookies.
- **Flash Cookies.** Certain features of our Service may use local stored objects (or Flash Cookies) to collect and store information about Your preferences or Your activity on our Service. Flash Cookies are not managed by the same browser settings as those used for Browser Cookies. For more information on how You can delete Flash Cookies, please read "Where can I change the settings for disabling, or deleting local shared objects?" available at [https://helpx.adobe.com/flash-player/kb/disable-local-shared-objects-flash.html#main*Where_can_I_change_the_settings_for_disabling\_\_or_deleting_local_shared_objects*](https://helpx.adobe.com/flash-player/kb/disable-local-shared-objects-flash.html#main_Where_can_I_change_the_settings_for_disabling__or_deleting_local_shared_objects_)
- **Web Beacons.** Certain sections of our Service and our emails may contain small electronic files known as web beacons (also referred to as clear gifs, pixel tags, and single-pixel gifs) that permit the Company, for example, to count users who have visited those pages or opened an email and for other related website statistics (for example, recording the popularity of a certain section and verifying system and server integrity).
Cookies can be "Persistent" or "Session" Cookies. Persistent Cookies remain on Your personal computer or mobile device when You go offline, while Session Cookies are deleted as soon as You close Your web browser. Learn more about cookies: [What Are Cookies?](https://www.privacypolicies.com/blog/cookies/).
We use both Session and Persistent Cookies for the purposes set out below:
- **Necessary / Essential Cookies**
Type: Session Cookies
Administered by: Us
Purpose: These Cookies are essential to provide You with services available through the Website and to enable You to use some of its features. They help to authenticate users and prevent fraudulent use of user accounts. Without these Cookies, the services that You have asked for cannot be provided, and We only use these Cookies to provide You with those services.
- **Cookies Policy / Notice Acceptance Cookies**
Type: Persistent Cookies
Administered by: Us
Purpose: These Cookies identify if users have accepted the use of cookies on the Website.
- **Functionality Cookies**
Type: Persistent Cookies
Administered by: Us
Purpose: These Cookies allow us to remember choices You make when You use the Website, such as remembering your login details or language preference. The purpose of these Cookies is to provide You with a more personal experience and to avoid You having to re-enter your preferences every time You use the Website.
For more information about the cookies we use and your choices regarding cookies, please visit our Cookies Policy or the Cookies section of our Privacy Policy.
## Use of Your Personal Data
The Company may use Personal Data for the following purposes:
- **To provide and maintain our Service**, including to monitor the usage of our Service.
- **To manage Your Account:** to manage Your registration as a user of the Service. The Personal Data You provide can give You access to different functionalities of the Service that are available to You as a registered user.
- **For the performance of a contract:** the development, compliance and undertaking of the purchase contract for the products, items or services You have purchased or of any other contract with Us through the Service.
- **To contact You:** To contact You by email, telephone calls, SMS, or other equivalent forms of electronic communication, such as a mobile application's push notifications regarding updates or informative communications related to the functionalities, products or contracted services, including the security updates, when necessary or reasonable for their implementation.
- **To provide You** with news, special offers and general information about other goods, services and events which we offer that are similar to those that you have already purchased or enquired about unless You have opted not to receive such information.
- **To manage Your requests:** To attend and manage Your requests to Us.
- **For business transfers:** We may use Your information to evaluate or conduct a merger, divestiture, restructuring, reorganization, dissolution, or other sale or transfer of some or all of Our assets, whether as a going concern or as part of bankruptcy, liquidation, or similar proceeding, in which Personal Data held by Us about our Service users is among the assets transferred.
- **For other purposes**: We may use Your information for other purposes, such as data analysis, identifying usage trends, determining the effectiveness of our promotional campaigns and to evaluate and improve our Service, products, services, marketing and your experience.
We may share Your personal information in the following situations:
- **With Service Providers:** We may share Your personal information with Service Providers to monitor and analyze the use of our Service, to contact You.
- **For business transfers:** We may share or transfer Your personal information in connection with, or during negotiations of, any merger, sale of Company assets, financing, or acquisition of all or a portion of Our business to another company.
- **With Affiliates:** We may share Your information with Our affiliates, in which case we will require those affiliates to honor this Privacy Policy. Affiliates include Our parent company and any other subsidiaries, joint venture partners or other companies that We control or that are under common control with Us.
- **With business partners:** We may share Your information with Our business partners to offer You certain products, services or promotions.
- **With other users:** when You share personal information or otherwise interact in the public areas with other users, such information may be viewed by all users and may be publicly distributed outside. If You interact with other users or register through a Third-Party Social Media Service, Your contacts on the Third-Party Social Media Service may see Your name, profile, pictures and description of Your activity. Similarly, other users will be able to view descriptions of Your activity, communicate with You and view Your profile.
- **With Your consent**: We may disclose Your personal information for any other purpose with Your consent.
## Retention of Your Personal Data
The Company will retain Your Personal Data only for as long as is necessary for the purposes set out in this Privacy Policy. We will retain and use Your Personal Data to the extent necessary to comply with our legal obligations (for example, if we are required to retain your data to comply with applicable laws), resolve disputes, and enforce our legal agreements and policies.
The Company will also retain Usage Data for internal analysis purposes. Usage Data is generally retained for a shorter period of time, except when this data is used to strengthen the security or to improve the functionality of Our Service, or We are legally obligated to retain this data for longer time periods.
## Transfer of Your Personal Data
Your information, including Personal Data, is processed at the Company's operating offices and in any other places where the parties involved in the processing are located. It means that this information may be transferred to — and maintained on — computers located outside of Your state, province, country or other governmental jurisdiction where the data protection laws may differ than those from Your jurisdiction.
Your consent to this Privacy Policy followed by Your submission of such information represents Your agreement to that transfer.
The Company will take all steps reasonably necessary to ensure that Your data is treated securely and in accordance with this Privacy Policy and no transfer of Your Personal Data will take place to an organization or a country unless there are adequate controls in place including the security of Your data and other personal information.
## Disclosure of Your Personal Data
### Business Transactions
If the Company is involved in a merger, acquisition or asset sale, Your Personal Data may be transferred. We will provide notice before Your Personal Data is transferred and becomes subject to a different Privacy Policy.
### Law enforcement
Under certain circumstances, the Company may be required to disclose Your Personal Data if required to do so by law or in response to valid requests by public authorities (e.g. a court or a government agency).
### Other legal requirements
The Company may disclose Your Personal Data in the good faith belief that such action is necessary to:
- Comply with a legal obligation
- Protect and defend the rights or property of the Company
- Prevent or investigate possible wrongdoing in connection with the Service
- Protect the personal safety of Users of the Service or the public
- Protect against legal liability
## Security of Your Personal Data
The security of Your Personal Data is important to Us, but remember that no method of transmission over the Internet, or method of electronic storage is 100% secure. While We strive to use commercially acceptable means to protect Your Personal Data, We cannot guarantee its absolute security.
# Children's Privacy
Our Service does not address anyone under the age of 13\. We do not knowingly collect personally identifiable information from anyone under the age of 13\. If You are a parent or guardian and You are aware that Your child has provided Us with Personal Data, please contact Us. If We become aware that We have collected Personal Data from anyone under the age of 13 without verification of parental consent, We take steps to remove that information from Our servers.
If We need to rely on consent as a legal basis for processing Your information and Your country requires consent from a parent, We may require Your parent's consent before We collect and use that information.
# Links to Other Websites
Our Service may contain links to other websites that are not operated by Us. If You click on a third party link, You will be directed to that third party's site. We strongly advise You to review the Privacy Policy of every site You visit.
We have no control over and assume no responsibility for the content, privacy policies or practices of any third party sites or services.
# Changes to this Privacy Policy
We may update Our Privacy Policy from time to time. We will notify You of any changes by posting the new Privacy Policy on this page.
We will let You know via email and/or a prominent notice on Our Service, prior to the change becoming effective and update the "Last updated" date at the top of this Privacy Policy.
You are advised to review this Privacy Policy periodically for any changes. Changes to this Privacy Policy are effective when they are posted on this page.
# Contact Us
If you have any questions about this Privacy Policy, You can contact us:
- By email: example@example.com

View file

@ -0,0 +1,20 @@
import rss from "@astrojs/rss";
import { getCollection } from "astro:content";
import { SITE_TITLE, SITE_DESCRIPTION } from "../consts";
import { ACTIVE_BLOG_SITE } from "../blog-sites.js";
export async function GET(context) {
const posts = (await getCollection(ACTIVE_BLOG_SITE.key)).sort(
(a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf(),
);
return rss({
title: SITE_TITLE,
description: SITE_DESCRIPTION,
site: context.site,
items: posts.map((post) => ({
...post.data,
link: `/${post.id}/`,
})),
});
}

View file

@ -0,0 +1,9 @@
---
import DefaultLayout from '@/layouts/DefaultLayout.astro';
import { SITE_TITLE, SITE_DESCRIPTION } from '../consts';
import SignupSection from '@/components/sections/signup-section';
---
<DefaultLayout title={SITE_TITLE} description={SITE_DESCRIPTION}>
<SignupSection />
</DefaultLayout>

View file

@ -0,0 +1,122 @@
---
layout: ../layouts/BasicLayout.astro
title: Privacy
---
# Terms and Conditions
Last updated: April 07, 2021
Please read these terms and conditions carefully before using Our Service.
# Interpretation and Definitions
## Interpretation
The words of which the initial letter is capitalized have meanings defined under the following conditions. The following definitions shall have the same meaning regardless of whether they appear in singular or in plural.
## Definitions
For the purposes of these Terms and Conditions:
- **Affiliate** means an entity that controls, is controlled by or is under common control with a party, where "control" means ownership of 50% or more of the shares, equity interest or other securities entitled to vote for election of directors or other managing authority.
- **Country** refers to: California, United States
- **Company** (referred to as either "the Company", "We", "Us" or "Our" in this Agreement) refers to Example Site.
- **Device** means any device that can access the Service such as a computer, a cellphone or a digital tablet.
- **Service** refers to the Website.
- **Terms and Conditions** (also referred as "Terms") mean these Terms and Conditions that form the entire agreement between You and the Company regarding the use of the Service. This Terms and Conditions agreement has been created with the help of the [Terms and Conditions Generator](https://www.privacypolicies.com/terms-conditions-generator/).
- **Third-party Social Media Service** means any services or content (including data, information, products or services) provided by a third-party that may be displayed, included or made available by the Service.
- **Website** refers to Example Site, accessible from [https://www.example.com](https://www.example.com)
- **You** means the individual accessing or using the Service, or the company, or other legal entity on behalf of which such individual is accessing or using the Service, as applicable.
# Acknowledgment
These are the Terms and Conditions governing the use of this Service and the agreement that operates between You and the Company. These Terms and Conditions set out the rights and obligations of all users regarding the use of the Service.
Your access to and use of the Service is conditioned on Your acceptance of and compliance with these Terms and Conditions. These Terms and Conditions apply to all visitors, users and others who access or use the Service.
By accessing or using the Service You agree to be bound by these Terms and Conditions. If You disagree with any part of these Terms and Conditions then You may not access the Service.
You represent that you are over the age of 18\. The Company does not permit those under 18 to use the Service.
Your access to and use of the Service is also conditioned on Your acceptance of and compliance with the Privacy Policy of the Company. Our Privacy Policy describes Our policies and procedures on the collection, use and disclosure of Your personal information when You use the Application or the Website and tells You about Your privacy rights and how the law protects You. Please read Our Privacy Policy carefully before using Our Service.
# Links to Other Websites
Our Service may contain links to third-party web sites or services that are not owned or controlled by the Company.
The Company has no control over, and assumes no responsibility for, the content, privacy policies, or practices of any third party web sites or services. You further acknowledge and agree that the Company shall not be responsible or liable, directly or indirectly, for any damage or loss caused or alleged to be caused by or in connection with the use of or reliance on any such content, goods or services available on or through any such web sites or services.
We strongly advise You to read the terms and conditions and privacy policies of any third-party web sites or services that You visit.
# Termination
We may terminate or suspend Your access immediately, without prior notice or liability, for any reason whatsoever, including without limitation if You breach these Terms and Conditions.
Upon termination, Your right to use the Service will cease immediately.
# Limitation of Liability
Notwithstanding any damages that You might incur, the entire liability of the Company and any of its suppliers under any provision of this Terms and Your exclusive remedy for all of the foregoing shall be limited to the amount actually paid by You through the Service or 100 USD if You haven't purchased anything through the Service.
To the maximum extent permitted by applicable law, in no event shall the Company or its suppliers be liable for any special, incidental, indirect, or consequential damages whatsoever (including, but not limited to, damages for loss of profits, loss of data or other information, for business interruption, for personal injury, loss of privacy arising out of or in any way related to the use of or inability to use the Service, third-party software and/or third-party hardware used with the Service, or otherwise in connection with any provision of this Terms), even if the Company or any supplier has been advised of the possibility of such damages and even if the remedy fails of its essential purpose.
Some states do not allow the exclusion of implied warranties or limitation of liability for incidental or consequential damages, which means that some of the above limitations may not apply. In these states, each party's liability will be limited to the greatest extent permitted by law.
# "AS IS" and "AS AVAILABLE" Disclaimer
The Service is provided to You "AS IS" and "AS AVAILABLE" and with all faults and defects without warranty of any kind. To the maximum extent permitted under applicable law, the Company, on its own behalf and on behalf of its Affiliates and its and their respective licensors and service providers, expressly disclaims all warranties, whether express, implied, statutory or otherwise, with respect to the Service, including all implied warranties of merchantability, fitness for a particular purpose, title and non-infringement, and warranties that may arise out of course of dealing, course of performance, usage or trade practice. Without limitation to the foregoing, the Company provides no warranty or undertaking, and makes no representation of any kind that the Service will meet Your requirements, achieve any intended results, be compatible or work with any other software, applications, systems or services, operate without interruption, meet any performance or reliability standards or be error free or that any errors or defects can or will be corrected.
Without limiting the foregoing, neither the Company nor any of the company's provider makes any representation or warranty of any kind, express or implied: (i) as to the operation or availability of the Service, or the information, content, and materials or products included thereon; (ii) that the Service will be uninterrupted or error-free; (iii) as to the accuracy, reliability, or currency of any information or content provided through the Service; or (iv) that the Service, its servers, the content, or e-mails sent from or on behalf of the Company are free of viruses, scripts, trojan horses, worms, malware, timebombs or other harmful components.
Some jurisdictions do not allow the exclusion of certain types of warranties or limitations on applicable statutory rights of a consumer, so some or all of the above exclusions and limitations may not apply to You. But in such a case the exclusions and limitations set forth in this section shall be applied to the greatest extent enforceable under applicable law.
# Governing Law
The laws of the Country, excluding its conflicts of law rules, shall govern this Terms and Your use of the Service. Your use of the Application may also be subject to other local, state, national, or international laws.
# Disputes Resolution
If You have any concern or dispute about the Service, You agree to first try to resolve the dispute informally by contacting the Company.
# For European Union (EU) Users
If You are a European Union consumer, you will benefit from any mandatory provisions of the law of the country in which you are resident in.
# United States Legal Compliance
You represent and warrant that (i) You are not located in a country that is subject to the United States government embargo, or that has been designated by the United States government as a "terrorist supporting" country, and (ii) You are not listed on any United States government list of prohibited or restricted parties.
# Severability and Waiver
## Severability
If any provision of these Terms is held to be unenforceable or invalid, such provision will be changed and interpreted to accomplish the objectives of such provision to the greatest extent possible under applicable law and the remaining provisions will continue in full force and effect.
## Waiver
Except as provided herein, the failure to exercise a right or to require performance of an obligation under this Terms shall not effect a party's ability to exercise such right or require such performance at any time thereafter nor shall be the waiver of a breach constitute a waiver of any subsequent breach.
# Translation Interpretation
These Terms and Conditions may have been translated if We have made them available to You on our Service. You agree that the original English text shall prevail in the case of a dispute.
# Changes to These Terms and Conditions
We reserve the right, at Our sole discretion, to modify or replace these Terms at any time. If a revision is material We will make reasonable efforts to provide at least 30 days' notice prior to any new terms taking effect. What constitutes a material change will be determined at Our sole discretion.
By continuing to access or use Our Service after those revisions become effective, You agree to be bound by the revised terms. If You do not agree to the new terms, in whole or in part, please stop using the website and the Service.
# Contact Us
If you have any questions about these Terms and Conditions, You can contact us:
- By email: example@example.com

View file

@ -0,0 +1,229 @@
@import "tailwindcss";
@plugin 'tailwindcss-animate';
@plugin '@tailwindcss/typography';
@variant dark (&:is(.dark *));
@theme {
--font-inter:
"Inter", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
Oxygen;
--font-sans:
"Hubot Sans", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
Roboto, Oxygen;
--font-mono:
"IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
"Liberation Mono";
--font-mona: "Mona Sans", system-ui, -apple-system, BlinkMacSystemFont;
--color-background: hsl(var(--background));
--color-foreground: hsl(var(--foreground));
--color-foreground2: hsl(var(--foreground-2));
--color-card: hsl(var(--card));
--color-card-foreground: hsl(var(--card-foreground));
--color-popover: hsl(var(--popover));
--color-popover-foreground: hsl(var(--popover-foreground));
--color-primary: hsl(var(--primary));
--color-primary-900: hsl(var(--primary-900));
--color-primary-gradient: hsl(var(--primary-gradient));
--color-primary-foreground: hsl(var(--primary-foreground));
--color-secondary: hsl(var(--secondary));
--color-secondary-gradient: hsl(var(--secondary-gradient));
--color-tertiary-gradient: hsl(var(--tertiary-gradient));
--color-secondary-foreground: hsl(var(--secondary-foreground));
--color-muted: hsl(var(--muted));
--color-muted-foreground: hsl(var(--muted-foreground));
--color-accent: hsl(var(--accent));
--color-accent-foreground: hsl(var(--accent-foreground));
--color-destructive: hsl(var(--destructive));
--color-destructive-foreground: hsl(var(--destructive-foreground));
--color-border: hsl(var(--border));
--color-input: hsl(var(--input));
--color-ring: hsl(var(--ring));
--color-chart-1: hsl(var(--chart-1));
--color-chart-2: hsl(var(--chart-2));
--color-chart-3: hsl(var(--chart-3));
--color-chart-4: hsl(var(--chart-4));
--color-chart-5: hsl(var(--chart-5));
--color-sand-50: hsl(var(--sand-50));
--color-sand-100: hsl(var(--sand-100));
--color-sand-600: hsl(var(--sand-600));
--color-sand-700: hsl(var(--sand-700));
--color-mint-50: hsl(var(--mint-50));
--color-mint: hsl(var(--mint));
--animate-accordion-down: accordion-down 0.2s ease-out;
--animate-accordion-up: accordion-up 0.2s ease-out;
--animate-marquee: marquee 25s linear infinite;
@keyframes accordion-down {
from {
height: 0;
}
to {
height: var(--radix-accordion-content-height);
}
}
@keyframes accordion-up {
from {
height: var(--radix-accordion-content-height);
}
to {
height: 0;
}
}
@keyframes marquee {
0% {
transform: translateX(0%);
}
100% {
transform: translateX(-100%);
}
}
}
@utility container {
margin-inline: auto;
padding-inline: 1.5rem;
@media (width >= --theme(--breakpoint-sm)) {
max-width: none;
}
@media (width >= 1400px) {
max-width: 1228px;
}
}
/*
The default border color has changed to `currentColor` in Tailwind CSS v4,
so we've added these compatibility styles to make sure everything still
looks the same as it did with Tailwind CSS v3.
If we ever want to remove these styles, we need to add an explicit border
color utility to any element that depends on these defaults.
*/
@layer base {
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: var(--color-gray-200, currentColor);
}
}
@layer utilities {
html,
body {
overflow-x: hidden;
scroll-behavior: smooth;
}
body {
@apply font-inter relative;
}
h1,
h2 {
@apply font-sans;
}
.mini-title {
@apply text-accent-foreground font-mono text-sm font-semibold tracking-widest;
}
}
@layer components {
button {
cursor: pointer;
}
[class*="border"] {
@apply border-border;
}
}
@layer base {
:root {
--background: 0 0% 98%;
--foreground: 240 10% 4%;
--card: 0 0% 100%;
--card-foreground: 240 10% 4%;
--popover: 0 0% 100%;
--popover-foreground: 240 10% 4%;
--primary: 240 77% 25%;
--primary-900: 240 70% 16%;
--primary-foreground: 0 0% 100%;
--secondary: 0 0% 96%;
--secondary-foreground: 240 100% 31%;
--muted: 0 0% 96%;
--muted-foreground: 0 0% 49%;
--accent: 0 0% 96%;
--accent-foreground: 240 26% 46%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 100%;
--border: 0 0% 0% / 0.09;
--input: 240 6% 23%;
--ring: 240 10% 4%;
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
--primary-gradient: 227 81% 67%;
--secondary-gradient: 125 51% 53%;
--tertiary-gradient: 318 58% 66%;
}
.dark {
--background: 240 10% 4%;
--foreground: 0 0% 98%;
--card: 240 10% 4%;
--card-foreground: 0 0% 98%;
--popover: 240 10% 4%;
--popover-foreground: 0 0% 98%;
--primary: 227 81% 67%;
--primary-900: 227 81% 75%;
--primary-foreground: 240 10% 4%;
--secondary: 240 6% 10%;
--secondary-foreground: 227 81% 67%;
--muted: 240 6% 10%;
--muted-foreground: 0 0% 63%;
--accent: 240 6% 10%;
--accent-foreground: 227 81% 67%;
--destructive: 0 84% 60%;
--destructive-foreground: 0 0% 98%;
--border: 0 0% 100% / 0.1;
--input: 0 0% 98%;
--ring: 227 81% 67%;
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 76%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
--primary-gradient: 240 77% 25%;
--secondary-gradient: 125 51% 53%;
--tertiary-gradient: 318 58% 33%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}