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
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ node_modules/
.ignore/
dist/
.next/
.plasmo/
.env*.local
messages.json
sitemap.xml
15 changes: 8 additions & 7 deletions apps/website/next.config.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
// TODO: Bug in Next.js 15.4.x, cannot upgrade, see https://github.com/vercel/next.js/issues/81628

import { fetchAllRuntimes } from '@evaluate/runtimes';

/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
Expand All @@ -15,11 +13,10 @@ const nextConfig = {
permanent: false,
},
{
source: `/:slug(${(await fetchAllRuntimes())
.map((r) => r.id.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
.join('|')})`,
destination: '/playgrounds/:slug',
permanent: true,
// (?:[a-z]{2,3}(-[A-Z][a-z]+)?(-[A-Z]{2}|\d{3})?)
source: '/:locale((?:[a-zA-Z]{2})(?:-[a-zA-Z]{2})?)',
destination: '/:locale/playgrounds',
permanent: false,
},
];
},
Expand Down Expand Up @@ -52,6 +49,10 @@ const nextConfig = {
},
],
},

experimental: {
swcPlugins: [['@sayable/swc-plugin', {}]],
},
};

const truthy = (v) => ['true', 't', '1'].includes(v);
Expand Down
10 changes: 10 additions & 0 deletions apps/website/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
"type": "module",
"scripts": {
"check": "tsc --noEmit",
"prebuild": "sayable extract && sayable compile",
"build": "use-env -p NEXT -P -- next build --no-lint",
"postbuild": "use-env -p NEXT -P -- next-sitemap --config sitemap.config.ts",
"start": "use-env -p NEXT -P -- next start",
"dev": "use-env -p NEXT -- next dev --turbo"
},
Expand All @@ -19,6 +21,7 @@
"@evaluate/runtimes": "workspace:^",
"@evaluate/style": "workspace:^",
"@hookform/resolvers": "^5.1.1",
"@sayable/react": "0.0.0-alpha.8",
"@t3-oss/env-nextjs": "^0.13.8",
"@tanstack/react-query": "^5.83.0",
"@tanstack/react-query-devtools": "^5.83.0",
Expand Down Expand Up @@ -46,6 +49,8 @@
"react-dom": "^19.1.0",
"react-hook-form": "^7.60.0",
"react-hotkeys-hook": "^5.1.0",
"sayable": "0.0.0-alpha.6",
"server-only": "^0.0.1",
"sharp": "^0.34.3",
"tailwind-merge": "^3.3.1",
"type-fest": "^4.41.0",
Expand All @@ -56,10 +61,15 @@
"devDependencies": {
"@million/lint": "^1.0.14",
"@next/bundle-analyzer": "15.3.5",
"@sayable/config": "0.0.0-alpha.4",
"@sayable/factory": "0.0.0-alpha.6",
"@sayable/format-po": "0.0.0-alpha.4",
"@sayable/swc-plugin": "0.0.0-alpha.5",
"@tailwindcss/postcss": "^4.1.11",
"@types/file-saver": "^2.0.7",
"@types/react": "^19.1.8",
"autoprefixer": "^10.4.21",
"next-sitemap": "^4.2.3",
"tailwindcss": "^4.1.11"
}
}
12 changes: 12 additions & 0 deletions apps/website/sayable.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { defineConfig } from '@sayable/config';

export default defineConfig({
sourceLocale: 'en',
locales: ['en'],
catalogues: [
{
include: ['src/**/*.{ts,tsx}'],
output: 'src/locales/{locale}/messages.{extension}',
},
],
});
21 changes: 21 additions & 0 deletions apps/website/sitemap.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { IConfig } from 'next-sitemap';
import sayable from './sayable.config.ts';

export default {
siteUrl: process.env.NEXT_PUBLIC_WEBSITE_URL!,
generateIndexSitemap: false,

transform: (_: unknown, pathname: string) => {
if (!pathname.startsWith('/en/')) return undefined;
return {
loc: pathname.replace('/en/', '/'),
lastmod: new Date().toISOString(),
changefreq: 'weekly',
priority: 0.7,
alternateRefs: sayable.locales.map((locale) => ({
href: `${process.env.NEXT_PUBLIC_WEBSITE_URL!}/${locale}${pathname.replace('/en/', '/')}`,
hreflang: locale,
})),
};
},
} satisfies IConfig;
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
import { Button } from '@evaluate/components/button';
import Link from 'next/link';
import { Say } from '@sayable/react';
import { LocalisedLink } from '~/components/localised-link';

export default function PlaygroundNotFound() {
return (
<div className="mt-[20vh] flex flex-col items-center justify-center">
<span className="font-bold text-8xl">404</span>
<h1 className="font-bold text-4xl text-primary">Not Found</h1>
<h1 className="font-bold text-4xl text-primary">
<Say>Not Found</Say>
</h1>
<p className="text-balance text-center text-muted-foreground">
Hmm, we couldn't find the playground you're looking for.
<Say>Hmm, we couldn't find the playground you're looking for.</Say>
</p>

<div className="mt-2">
<Button variant="secondary" asChild>
<Link href="/playgrounds">Browse Playgrounds</Link>
<LocalisedLink href="/playgrounds">
<Say>Browse Playgrounds</Say>
</LocalisedLink>
</Button>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { fetchAllRuntimes, fetchRuntimeById } from '@evaluate/runtimes';
import { notFound } from 'next/navigation';
import { generateBaseMetadata } from '~/app/metadata';
import { generateBaseMetadata } from '~/app/[locale]/metadata';

import { Editor } from '~/components/editor';
import { Explorer } from '~/components/explorer';
import { ExplorerProvider } from '~/components/explorer/use';
import { Terminal } from '~/components/terminal';
import { TerminalProvider } from '~/components/terminal/use';
import say from '~/i18n';
import type { PageProps } from '~/types';
import { EditorWrapper } from './wrapper';

Expand All @@ -14,21 +16,28 @@ export async function generateStaticParams() {
return runtimes.map((r) => ({ playground: r.id }));
}

export async function generateMetadata(props: PageProps<['[playground]']>) {
const playground = (await props.params).playground;
export async function generateMetadata(
props: PageProps<['[locale]', '[playground]']>,
) {
const { locale, playground } = await props.params;
const runtime = await fetchRuntimeById(decodeURIComponent(playground));
if (!runtime) notFound();

return generateBaseMetadata(`/playground/${playground}`, {
title: `${runtime.name} Playground on Evaluate`,
description: `Run code in ${runtime.name} and other programming languages effortlessly with Evaluate. Input your code, optional arguments, and get instant results. Debug, optimize, and elevate your coding experience with our versatile evaluation tools.`,
keywords: [runtime.name, ...runtime.aliases, ...runtime.tags] //
.map((k) => k.toLowerCase()),
});
return generateBaseMetadata(
say.activate(locale),
`/playgrounds/${playground}`,
(say) => ({
title: say`${runtime.name} Online Playground on Evaluate`,
description: say`Run ${runtime.name} and more code snippets online in your browser with Evaluate's code playground.`,
keywords: [runtime.name, ...runtime.aliases, ...runtime.tags].map((k) =>
k.toLowerCase(),
),
}),
);
}

export default async function EditorPage(props: PageProps<['[playground]']>) {
const playground = (await props.params).playground;
const { playground } = await props.params;
const runtime = await fetchRuntimeById(decodeURIComponent(playground));
if (!runtime) notFound();

Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import { fetchAllRuntimes } from '@evaluate/runtimes';
import { generateBaseMetadata } from '~/app/metadata';
import { Say } from '@sayable/react';
import say from '~/i18n';
import type { PageProps } from '~/types';
import { generateBaseMetadata } from '../../metadata';
import { PlaygroundCardList } from './playground-card-list';

export function generateMetadata() {
return generateBaseMetadata('/playgrounds');
export async function generateMetadata(props: PageProps<['[locale]']>) {
const { locale } = await props.params;
return generateBaseMetadata(say.activate(locale), '/playgrounds');
}

export default async function PlaygroundsPage() {
Expand All @@ -13,13 +17,16 @@ export default async function PlaygroundsPage() {
<div className="container flex flex-col gap-6 py-6">
<div className="space-y-6 py-24 text-center">
<h1 className="font-bold text-3xl text-primary tracking-tight md:text-5xl">
Playgrounds
<Say>Playgrounds</Say>
</h1>
<p className="text-balance text-sm md:text-base">
Explore and run code in different programming languages and runtimes.
<Say>
Explore and run code in different programming languages and
runtimes.
</Say>
<br />
<span className="opacity-70">
Powered by the Piston execution engine.
<Say>Powered by the Piston execution engine.</Say>
</span>
</p>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
import { Separator } from '@evaluate/components/separator';
import { toast } from '@evaluate/components/toast';
import type { Runtime } from '@evaluate/runtimes';
import { Say, useSay } from '@sayable/react';
import {
ArrowDownWideNarrowIcon,
CircleDotIcon,
Expand All @@ -30,6 +31,8 @@ export function PlaygroundCardList({
}: {
initialRuntimes: Runtime[];
}) {
const say = useSay();

const [search, setSearch] = useQueryParameter('search');
const deferredSearch = useDeferredValue(search);
const searchedRuntimes = useMemo(() => {
Expand Down Expand Up @@ -57,7 +60,7 @@ export function PlaygroundCardList({
type SortBy = 'popularity' | 'name';
const [sortBy, setSortBy] = useQueryParameter<SortBy>('sort', 'popularity');
const sortedRuntimes = useMemo(() => {
return searchedRuntimes.sort((a, b) => {
return [...searchedRuntimes].sort((a, b) => {
if (sortBy === 'name') return a.name.localeCompare(b.name);
return b.popularity - a.popularity;
});
Expand All @@ -66,27 +69,28 @@ export function PlaygroundCardList({
const [pinnedRuntimeIds] = useLocalStorage<string[]>('evaluate.pinned', []);
const pinnedRuntimes = useMemo(() => {
return pinnedRuntimeIds
.map((id) => initialRuntimes.find((r) => r.id === id)!)
.filter(Boolean);
.map((id) => initialRuntimes.find((r) => r.id === id))
.filter((r): r is Runtime => Boolean(r));
}, [pinnedRuntimeIds, initialRuntimes]);

const [hash] = useHashFragment();
useEffect(() => {
if (hash)
toast.info('Choose a playground!', {
icon: <CircleDotIcon className="size-4" />,
});
}, [hash]);
if (!hash) return;
toast.info(say`Choose a playground!`, {
icon: <CircleDotIcon className="size-4" />,
});
}, [say, hash]);

return (
<div className="space-y-3">
<search className="flex gap-3">
<div role="search" className="flex gap-3">
<div className="relative flex w-full">
<SearchIcon className="absolute top-[27%] left-2 size-4 opacity-50" />

<Input
className="absolute inset-0 h-full w-full pl-7"
placeholder="Search runtime playgrounds..."
placeholder={say`Search runtime playgrounds...`}
aria-label={say`Search runtime playgrounds...`}
value={search ?? ''}
onChange={(e) => setSearch(e.target.value)}
/>
Expand All @@ -97,25 +101,35 @@ export function PlaygroundCardList({
className="absolute top-[27%] right-2 size-4 cursor-pointer opacity-50"
onClick={() => setSearch('')}
>
<span className="sr-only">Clear Search</span>
<span className="sr-only">
<Say>Clear Search</Say>
</span>
</XIcon>
)}
</div>

<Select value={sortBy} onValueChange={setSortBy as never}>
<SelectTrigger className="w-[205px]" icon={ArrowDownWideNarrowIcon}>
<SelectValue placeholder="Sort by..." />
<span className="sr-only">Toggle Sort By Dropdown</span>
<SelectValue placeholder={say`Sort by...`} />
<span className="sr-only">
<Say>Toggle Sort By Dropdown</Say>
</span>
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>Sort By</SelectLabel>
<SelectItem value="popularity">Popularity</SelectItem>
<SelectItem value="name">Name</SelectItem>
<SelectLabel>
<Say>Sort By</Say>
</SelectLabel>
<SelectItem value="popularity">
<Say>Popularity</Say>
</SelectItem>
<SelectItem value="name">
<Say>Name</Say>
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</search>
</div>

{pinnedRuntimes.length > 0 && (
<>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@ import {
CardTitle,
} from '@evaluate/components/card';
import { getRuntimeIconUrl, type Runtime } from '@evaluate/runtimes';
import { Say } from '@sayable/react';
import { CodeIcon, PinIcon } from 'lucide-react';
import Link from 'next/link';
import { useCallback, useMemo, useRef, useState } from 'react';
import { ImageWithFallback } from '~/components/image-fallback';
import { LocalisedLink } from '~/components/localised-link';
import { useLocalStorage } from '~/hooks/local-storage';
import { getDominantColour, type RGB } from './get-colour';

Expand Down Expand Up @@ -72,14 +73,16 @@ export function PlaygroundCard({
<CardDescription>v{runtime.version}</CardDescription>
</CardHeader>

<Link
<LocalisedLink
suppressHydrationWarning
className="absolute inset-0"
href={`/playgrounds/${runtime.id}${hash ? `#${hash}` : ''}`}
prefetch={false}
>
<span className="sr-only">{`Open ${runtime.name} Playground`}</span>
</Link>
<span className="sr-only">
<Say>Open {runtime.name} Playground</Say>
</span>
</LocalisedLink>

<div className="absolute top-0 right-0">
<Button
Expand Down
Loading