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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion biome.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@
"clientKind": "git",
"useIgnoreFile": true
},
"files": { "ignoreUnknown": false, "includes": ["**"] },
"files": {
"ignoreUnknown": true,
"includes": ["src/**/*.{ts,tsx}"],
"experimentalScannerIgnores": ["node_modules", ".next"]
},
"formatter": { "enabled": true },
"assist": { "actions": { "source": { "organizeImports": "on" } } },
"linter": {
Expand Down
147 changes: 72 additions & 75 deletions bun.lock

Large diffs are not rendered by default.

10 changes: 9 additions & 1 deletion next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,24 @@ import createNextIntlPlugin from "next-intl/plugin";
* for Docker builds.
*/
import "@/env";
import { routing } from "@/i18n/routing";

const nextConfig: NextConfig = {
reactStrictMode: true,
experimental: {
reactCompiler: true,
ppr: true,
},
typescript: {
ignoreBuildErrors: true,
},

async redirects() {
return [...routing.locales.map(locale => ({
source: `/${locale}/playground`,
destination: `/${locale}/playground/bubble`,
permanent: true,
}))]
}
};

const withNextIntl = createNextIntlPlugin();
Expand Down
15 changes: 6 additions & 9 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,36 +13,33 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@icons-pack/react-simple-icons": "^13.3.0",
"@icons-pack/react-simple-icons": "^13.4.0",
"@radix-ui/react-select": "^2.2.5",
"@radix-ui/react-slider": "^1.3.5",
"@radix-ui/react-slot": "^1.2.3",
"@t3-oss/env-nextjs": "^0.13.8",
"@tanstack/react-query": "^5.81.5",
"@tanstack/react-query-devtools": "^5.81.5",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"d3": "^7.9.0",
"hast-util-to-jsx-runtime": "^2.3.6",
"lucide-react": "^0.525.0",
"motion": "^12.23.0",
"next": "15.4.0-canary.111",
"motion": "^12.23.3",
"next": "^15.3.5",
"next-intl": "^4.3.4",
"nuqs": "^2.4.3",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-use-measure": "^2.1.7",
"server-only": "^0.0.1",
"shiki": "^3.7.0",
"tailwind-merge": "^3.3.1",
"tw-animate-css": "^1.3.5",
"zod": "^3.25.71"
"zod": "^4.0.5"
},
"devDependencies": {
"@biomejs/biome": "^2.0.6",
"@biomejs/biome": "2.0.0",
"@tailwindcss/postcss": "^4.1.11",
"@types/d3": "^7.4.3",
"@types/node": "^24.0.10",
"@types/node": "^24.0.13",
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",
"babel-plugin-react-compiler": "^19.1.0-rc.2",
Expand Down
18 changes: 9 additions & 9 deletions src/app/[locale]/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import { notFound } from "next/navigation";
import { hasLocale, type Locale, NextIntlClientProvider } from "next-intl";
import { type Locale, NextIntlClientProvider } from "next-intl";
import { getTranslations, setRequestLocale } from "next-intl/server";
import { routing } from "@/i18n/routing";
import "./globals.css";
import Footer from "@/components/footer";
import Navigation from "@/components/navigation";
import Providers from "@/components/providers";
import { routing } from "@/i18n/routing";
import "@/app/globals.css";
import { NuqsAdapter } from "nuqs/adapters/next/app";

const geistSans = Geist({
variable: "--font-geist-sans",
Expand All @@ -18,6 +18,7 @@ const geistMono = Geist_Mono({
subsets: ["latin"],
});

export const dynamicParams = false;
export function generateStaticParams() {
return routing.locales.map((locale) => ({ locale }));
}
Expand All @@ -26,7 +27,7 @@ export async function generateMetadata({
params,
}: {
params: Promise<{ locale: Locale }>;
}) {
}): Promise<Metadata> {
const { locale } = await params;
const t = await getTranslations({ locale, namespace: "metadata" });

Expand All @@ -44,7 +45,6 @@ export default async function LocaleLayout({
params: Promise<{ locale: Locale }>;
}) {
const { locale } = await params;
if (!hasLocale(routing.locales, locale)) notFound();

// Enable static rendering
setRequestLocale(locale);
Expand All @@ -54,13 +54,13 @@ export default async function LocaleLayout({
<body
className={`${geistSans.variable} ${geistMono.variable} dark font-sans antialiased`}
>
<Providers>
<NuqsAdapter>
<NextIntlClientProvider>
<Navigation />
{children}
<Footer />
</NextIntlClientProvider>
</Providers>
</NuqsAdapter>
</body>
</html>
);
Expand Down
13 changes: 6 additions & 7 deletions src/app/[locale]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ import {
Split,
} from "lucide-react";
import { motion } from "motion/react";
import Link from "next/link";
import { useTranslations } from "next-intl";
import { BubbleBackground } from "@/components/animate-ui/bubble-background";
import { GradientText } from "@/components/animate-ui/gradient-text";
import { RippleButton } from "@/components/animate-ui/ripple-button";
import { Link } from "@/i18n/navigation";

export default function HomePage() {
const t = useTranslations("home");
Expand Down Expand Up @@ -166,31 +166,31 @@ export default function HomePage() {
<BarChart3 className="h-10 w-10 transition-transform duration-300 group-hover:scale-110" />
),
description: t("algorithms.bubble-sort.description"),
queryParam: "bubble",
path: "bubble",
},
{
name: t("algorithms.selection-sort.title"),
icon: (
<SortAsc className="h-10 w-10 transition-transform duration-300 group-hover:scale-110" />
),
description: t("algorithms.selection-sort.description"),
queryParam: "selection",
path: "selection",
},
{
name: t("algorithms.insertion-sort.title"),
icon: (
<Shuffle className="h-10 w-10 transition-transform duration-300 group-hover:scale-110" />
),
description: t("algorithms.insertion-sort.description"),
queryParam: "insertion",
path: "insertion",
},
{
name: t("algorithms.quick-sort.title"),
icon: (
<Split className="h-10 w-10 transition-transform duration-300 group-hover:scale-110" />
),
description: t("algorithms.quick-sort.description"),
queryParam: "quicksort",
path: "quicksort",
},
].map((algorithm, index) => (
<motion.div
Expand All @@ -202,8 +202,7 @@ export default function HomePage() {
>
<Link
href={{
pathname: "/playground",
query: { algorithm: algorithm.queryParam },
pathname: `/playground/${algorithm.path}`,
}}
className="block"
>
Expand Down
32 changes: 32 additions & 0 deletions src/app/[locale]/playground/[algorithm]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { HexagonBackground } from "@/components/animate-ui/hexagon-background";
import PlaygroundState from "@/components/playground-state";
import { highlight } from "@/lib/highlight";
import { ALGORITHMS, type Algorithm, getAlgorithmCode } from "@/lib/utils";

export const dynamicParams = false;
export function generateStaticParams() {
return ALGORITHMS.map((algorithm) => ({ algorithm }));
}

export default async function Playground({
params,
}: {
params: Promise<{ algorithm: Algorithm }>;
}) {
const { algorithm } = (await params) as { algorithm: Algorithm };

const highlightedAlgorithmCode = await highlight(
getAlgorithmCode(algorithm),
"ts",
);

return (
<div className="relative flex items-center justify-center">
<HexagonBackground className="absolute inset-0 z-0" />
<PlaygroundState
algorithm={algorithm}
highlightedAlgorithmCode={highlightedAlgorithmCode}
/>
</div>
);
}
27 changes: 0 additions & 27 deletions src/app/[locale]/playground/page.tsx

This file was deleted.

File renamed without changes.
88 changes: 34 additions & 54 deletions src/components/algorithm-explanation.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
"use client";

import { useQuery } from "@tanstack/react-query";
import { Check, Copy, Loader2 } from "lucide-react";
import { Check, Copy } from "lucide-react";
import { useTranslations } from "next-intl";
import { useState } from "react";
import { type JSX, useState } from "react";
import {
Tabs,
TabsContent,
Expand All @@ -18,33 +17,26 @@ import {
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
type Algorithm,
cn,
codeHighlightOptions,
getAlgorithmCode,
} from "@/lib/utils";
import { type Algorithm, cn, getAlgorithmCode } from "@/lib/utils";
import { RippleButton } from "./animate-ui/ripple-button";

interface AlgorithmExplanationProps {
algorithm: Algorithm;
iterations: number;
swaps: number;
highlightedAlgorithmCode: JSX.Element;
className?: string;
}

export default function AlgorithmExplanation({
algorithm,
iterations,
swaps,
highlightedAlgorithmCode,
className,
}: AlgorithmExplanationProps) {
const t = useTranslations("algorithm-explanation");

const { data, isPending, isError } = useQuery(
codeHighlightOptions(algorithm),
);

const [copied, setCopied] = useState<string | null>(null);

const copyCode = (code: string) => {
Expand Down Expand Up @@ -260,47 +252,35 @@ export default function AlgorithmExplanation({
</TabsContent>

<TabsContent value="code" className="group relative mt-4">
{isPending ? (
<p className="text-muted-foreground text-xs">
<Loader2 className="mx-auto animate-spin text-primary" />
</p>
) : isError ? (
<p className="text-muted-foreground text-xs">
There was an error loading the code.
</p>
) : (
data
)}
{data && (
<RippleButton
onClick={() => copyCode(getAlgorithmCode(algorithm))}
variant="ghost"
size="sm"
className={cn(
"absolute top-1 right-1 flex size-8 scale-0 items-center justify-center bg-background/60 p-0 transition-all duration-200 ease-snappy touch-only:group-focus-within:scale-100 touch-only:group-focus-within:opacity-100 group-hover:scale-100 group-hover:opacity-100",
copied === getAlgorithmCode(algorithm) &&
"scale-100 opacity-100",
)}
type="button"
>
<div className="relative size-4">
<Check
className={cn(
"absolute inset-0 scale-0 text-primary opacity-0 transition-all duration-200 ease-snappy",
copied === getAlgorithmCode(algorithm) &&
"scale-100 opacity-100",
)}
/>
<Copy
className={cn(
"absolute inset-0 scale-100 opacity-100 transition-all duration-200 ease-snappy",
copied === getAlgorithmCode(algorithm) &&
"scale-0 opacity-0",
)}
/>
</div>
</RippleButton>
)}
{highlightedAlgorithmCode}
<RippleButton
onClick={() => copyCode(getAlgorithmCode(algorithm))}
variant="ghost"
size="sm"
className={cn(
"absolute top-1 right-1 flex size-8 scale-0 items-center justify-center bg-background/60 p-0 transition-all duration-200 ease-snappy focus-visible:scale-100 focus-visible:opacity-100 touch-only:group-focus-within:scale-100 touch-only:group-focus-within:opacity-100 group-hover:scale-100 group-hover:opacity-100",
copied === getAlgorithmCode(algorithm) &&
"scale-100 opacity-100",
)}
type="button"
>
<div className="relative size-4">
<Check
className={cn(
"absolute inset-0 scale-0 text-primary opacity-0 transition-all duration-200 ease-snappy",
copied === getAlgorithmCode(algorithm) &&
"scale-100 opacity-100",
)}
/>
<Copy
className={cn(
"absolute inset-0 scale-100 opacity-100 transition-all duration-200 ease-snappy",
copied === getAlgorithmCode(algorithm) &&
"scale-0 opacity-0",
)}
/>
</div>
</RippleButton>
</TabsContent>
</TabsContents>
</Tabs>
Expand Down
Loading