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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions website/next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ const nextConfig: NextConfig = {
destination: "/us",
permanent: false,
},
{
source: "/tools",
destination: "/us/tools",
permanent: false,
},
];
},
};
Expand Down
9 changes: 9 additions & 0 deletions website/src/__tests__/pages/static-pages.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import PrivacyPage from "../../app/[countryId]/privacy/page";
import TermsPage from "../../app/[countryId]/terms/page";
import ResearchPage from "../../app/[countryId]/research/page";
import ClaudePluginPage from "../../app/[countryId]/claude-plugin/page";
import ToolsPage from "../../app/[countryId]/tools/page";

describe("static pages", () => {
test("Donate page renders heading", async () => {
Expand Down Expand Up @@ -49,4 +50,12 @@ describe("static pages", () => {
expect(el).toBeTruthy();
expect(el.type).toBeDefined();
});

test("Tools page returns a valid element", async () => {
const el = await ToolsPage({
params: Promise.resolve({ countryId: "us" }),
});
expect(el).toBeTruthy();
expect(el.type).toBeDefined();
});
});
2 changes: 1 addition & 1 deletion website/src/app/[countryId]/brand/writing/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -372,7 +372,7 @@ export default function BrandWritingPage() {
>
PolicyEngine's voice is research-oriented but accessible. We
explain complex policy concepts clearly while maintaining rigor.
When writing about our products, the tone can be more natural and
When writing about our tools, the tone can be more natural and
conversational.
</p>

Expand Down
2 changes: 2 additions & 0 deletions website/src/app/[countryId]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import HeroSection from "@/components/home/HeroSection";
import HomeBlogPreview from "@/components/home/HomeBlogPreview";
import HomeToolsPreview from "@/components/home/HomeToolsPreview";
import HomeTrackerPreview from "@/components/home/HomeTrackerPreview";
import OrgLogos from "@/components/home/OrgLogos";
import FeaturedResearchBanner from "@/components/home/FeaturedResearchBanner";
Expand All @@ -17,6 +18,7 @@ export default async function HomePage({
<HeroSection countryId={countryId} />
<div style={{ fontFamily: typography.fontFamily.primary }}>
<OrgLogos countryId={countryId} />
<HomeToolsPreview countryId={countryId} />
<FeaturedResearchBanner countryId={countryId} />
<HomeBlogPreview countryId={countryId} />
<HomeTrackerPreview countryId={countryId} />
Expand Down
24 changes: 24 additions & 0 deletions website/src/app/[countryId]/tools/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import type { Metadata } from "next";
import ToolsShowcase from "@/components/tools/ToolsShowcase";
import { getToolsForCountry } from "@/data/tools";

export const metadata: Metadata = {
title: "Tools",
description:
"Interactive PolicyEngine tools, calculators, and developer tooling.",
};

export default async function ToolsPage({
params,
}: {
params: Promise<{ countryId: string }>;
}) {
const { countryId } = await params;

return (
<ToolsShowcase
countryId={countryId}
tools={getToolsForCountry(countryId)}
/>
);
}
5 changes: 5 additions & 0 deletions website/src/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -561,6 +561,11 @@ export default function Header() {

const navItems: NavItemSetup[] = [
{ label: "Research", href: `/${countryId}/research`, hasDropdown: false },
{ label: "Tools", href: `/${countryId}/tools`, hasDropdown: false },
{ label: "Model", href: `/${countryId}/model`, hasDropdown: false },
...(countryId === "us"
? [{ label: "API", href: `/${countryId}/api`, hasDropdown: false }]
: []),
{
label: "About",
hasDropdown: true,
Expand Down
211 changes: 211 additions & 0 deletions website/src/components/home/HomeToolsPreview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
import Link from "next/link";
import {
colors,
spacing,
typography,
} from "@policyengine/design-system/tokens";
import { getToolsForCountry } from "@/data/tools";

function ActionLink({
href,
label,
external = false,
}: {
href: string;
label: string;
external?: boolean;
}) {
const style: React.CSSProperties = {
display: "inline-flex",
alignItems: "center",
gap: "8px",
textDecoration: "none",
color: colors.primary[700],
fontWeight: typography.fontWeight.semibold,
fontSize: typography.fontSize.sm,
fontFamily: typography.fontFamily.primary,
};

if (external) {
return (
<a href={href} target="_blank" rel="noopener noreferrer" style={style}>
{label} &rarr;
</a>
);
}

return (
<Link href={href} style={style}>
{label} &rarr;
</Link>
);
}

export default function HomeToolsPreview({
countryId,
}: {
countryId: string;
}) {
const tools = getToolsForCountry(countryId).slice(0, 3);

if (tools.length === 0) return null;

return (
<section
style={{
backgroundColor: colors.white,
paddingTop: spacing["5xl"],
paddingBottom: spacing["5xl"],
borderBottom: `1px solid ${colors.border.light}`,
}}
>
<div
style={{
maxWidth: spacing.layout.content,
margin: "0 auto",
padding: `0 ${spacing.xl}`,
}}
>
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "baseline",
gap: spacing.lg,
marginBottom: spacing["3xl"],
flexWrap: "wrap",
}}
>
<div style={{ maxWidth: "720px" }}>
<p
style={{
margin: 0,
color: colors.primary[600],
fontSize: typography.fontSize.xs,
fontWeight: typography.fontWeight.semibold,
letterSpacing: "0.08em",
textTransform: "uppercase",
fontFamily: typography.fontFamily.primary,
}}
>
Tools
</p>
<h2
style={{
marginTop: spacing.sm,
marginBottom: spacing.md,
color: colors.gray[900],
fontSize: "clamp(30px, 4vw, 40px)",
lineHeight: 1.05,
letterSpacing: "-0.03em",
fontWeight: typography.fontWeight.bold,
fontFamily: typography.fontFamily.primary,
}}
>
Open tools, not just articles.
</h2>
<p
style={{
margin: 0,
color: colors.text.secondary,
fontSize: typography.fontSize.lg,
lineHeight: typography.lineHeight.relaxed,
fontFamily: typography.fontFamily.primary,
}}
>
Explore calculators, developer tools, and analysis tools built
for real use cases.
</p>
</div>
<Link
href={`/${countryId}/tools`}
style={{
textDecoration: "none",
color: colors.primary[700],
fontWeight: typography.fontWeight.semibold,
fontSize: typography.fontSize.sm,
fontFamily: typography.fontFamily.primary,
}}
>
View all tools &rarr;
</Link>
</div>

<div
className="grid gap-6 lg:grid-cols-3"
style={{ alignItems: "stretch" }}
>
{tools.map((tool) => (
<article
key={tool.slug}
style={{
height: "100%",
borderRadius: spacing.radius.feature,
padding: spacing["2xl"],
background:
"linear-gradient(180deg, rgba(249,250,251,1) 0%, rgba(255,255,255,1) 100%)",
border: `1px solid ${colors.border.light}`,
boxShadow: `0 18px 44px -34px ${colors.shadow.dark}`,
display: "flex",
flexDirection: "column",
}}
>
<div
style={{
display: "inline-flex",
alignItems: "center",
alignSelf: "flex-start",
padding: "6px 10px",
borderRadius: "999px",
backgroundColor: colors.primary[50],
color: colors.primary[700],
fontSize: typography.fontSize.xs,
fontWeight: typography.fontWeight.semibold,
letterSpacing: "0.06em",
textTransform: "uppercase",
marginBottom: spacing.lg,
}}
>
{tool.kind}
</div>

<h3
style={{
marginTop: 0,
marginBottom: spacing.md,
color: colors.gray[900],
fontSize: typography.fontSize["2xl"],
lineHeight: 1.12,
fontWeight: typography.fontWeight.bold,
fontFamily: typography.fontFamily.primary,
}}
>
{tool.title}
</h3>

<p
style={{
marginTop: 0,
marginBottom: spacing.lg,
color: colors.text.secondary,
fontSize: typography.fontSize.base,
lineHeight: typography.lineHeight.relaxed,
fontFamily: typography.fontFamily.primary,
flex: 1,
}}
>
{tool.summary}
</p>

<ActionLink
href={tool.primaryAction.href}
label={tool.primaryAction.label}
external={tool.primaryAction.external}
/>
</article>
))}
</div>
</div>
</section>
);
}
Loading
Loading