diff --git a/apps/website/src/app/(editor)/playgrounds/[playground]/content.tsx b/apps/website/src/app/(editor)/playgrounds/[playground]/content.tsx
deleted file mode 100644
index cdf2b8c..0000000
--- a/apps/website/src/app/(editor)/playgrounds/[playground]/content.tsx
+++ /dev/null
@@ -1,125 +0,0 @@
-'use client';
-
-import {
- ResizableHandle,
- ResizablePanel,
- ResizablePanelGroup,
-} from '@evaluate/components/resizable';
-import { Sheet, SheetContent } from '@evaluate/components/sheet';
-import { useEventListener } from '@evaluate/hooks/event-listener';
-import { useMediaQuery } from '@evaluate/hooks/media-query';
-import type { Runtime } from '@evaluate/shapes';
-import React, { useState } from 'react';
-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';
-
-export default function EditorContent(p: { runtime: Runtime }) {
- const isDesktop = useMediaQuery('lg');
- const Wrapper = isDesktop ? DesktopWrapper : MobileWrapper;
-
- return (
-
-
-
-
-
-
-
-
-
-
-
- );
-}
-
-function DesktopWrapper(p: React.PropsWithChildren) {
- const [explorer, editor, terminal] = React.Children.toArray(p.children);
-
- return (
-
-
- {explorer}
-
-
-
-
-
- {editor}
-
-
-
-
-
- {terminal}
-
-
- );
-}
-
-function MobileWrapper(p: React.PropsWithChildren) {
- const [explorer, editor, terminal] = React.Children.toArray(p.children);
-
- const [explorerOpen, setExplorerOpen] = useState(false);
- useEventListener('mobile-explorer-open-change' as never, setExplorerOpen);
- const [terminalOpen, setTerminalOpen] = useState(false);
- useEventListener('mobile-terminal-open-change' as never, setTerminalOpen);
-
- return (
- <>
-
- setExplorerOpen(false)}
- >
- e.stopPropagation()}
- onKeyUp={(e) => e.stopPropagation()}
- className="h-full rounded-xl border-2 bg-card"
- >
- {explorer}
-
-
-
-
- {editor}
-
-
- setExplorerOpen(false)}
- >
- e.stopPropagation()}
- onKeyUp={(e) => e.stopPropagation()}
- className="h-full rounded-xl border-2 bg-card"
- >
- {terminal}
-
-
-
- >
- );
-}
diff --git a/apps/website/src/app/(editor)/playgrounds/[playground]/loading.tsx b/apps/website/src/app/(editor)/playgrounds/[playground]/loading.tsx
new file mode 100644
index 0000000..d28055a
--- /dev/null
+++ b/apps/website/src/app/(editor)/playgrounds/[playground]/loading.tsx
@@ -0,0 +1,13 @@
+import { EditorWrapperSkeleton } from './wrapper/skeleton';
+
+export default function EditorLoadingPage() {
+ return (
+ <>
+
+
+ >
+ );
+}
diff --git a/apps/website/src/app/(editor)/playgrounds/[playground]/page.tsx b/apps/website/src/app/(editor)/playgrounds/[playground]/page.tsx
index 62525a3..e9379d0 100644
--- a/apps/website/src/app/(editor)/playgrounds/[playground]/page.tsx
+++ b/apps/website/src/app/(editor)/playgrounds/[playground]/page.tsx
@@ -1,27 +1,26 @@
import { fetchRuntimes, getRuntime } from '@evaluate/engine/runtimes';
import { notFound } from 'next/navigation';
import { generateBaseMetadata } from '~/app/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 type { PageProps } from '~/types';
-import EditorContent from './content';
+import { EditorWrapper } from './wrapper';
export async function generateStaticParams() {
const runtimes = await fetchRuntimes();
- return runtimes
- .reduce((ids, runtime) => {
- for (const version of runtime.versions)
- ids.push(`${runtime.id}@${version}`);
- ids.push(runtime.id);
- return ids;
- }, [])
- .map((id) => ({ runtime: id }));
+ return runtimes.map((r) => ({ playground: r.id }));
}
-export async function generateMetadata(p: PageProps<['playground']>) {
- const id = decodeURIComponent(p.params.playground);
- const runtime = await getRuntime(id);
+export async function generateMetadata({
+ params: { playground },
+}: PageProps<['playground']>) {
+ const runtime = await getRuntime(decodeURIComponent(playground));
if (!runtime) notFound();
- return generateBaseMetadata(`/playground/${id}`, {
+ 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] //
@@ -29,20 +28,29 @@ export async function generateMetadata(p: PageProps<['playground']>) {
});
}
-export default async function PlaygroundEditorPage(
- p: PageProps<['playground']>,
-) {
- const id = decodeURIComponent(p.params.playground);
- const runtime = await getRuntime(id);
+export default async function EditorPage({
+ params: { playground },
+}: PageProps<['playground']>) {
+ const runtime = await getRuntime(decodeURIComponent(playground));
if (!runtime) notFound();
return (
<>
-
+
+
+
+
+
+
+
+
+
+
+
>
);
}
diff --git a/apps/website/src/app/(editor)/playgrounds/[playground]/wrapper/desktop.tsx b/apps/website/src/app/(editor)/playgrounds/[playground]/wrapper/desktop.tsx
new file mode 100644
index 0000000..3339750
--- /dev/null
+++ b/apps/website/src/app/(editor)/playgrounds/[playground]/wrapper/desktop.tsx
@@ -0,0 +1,45 @@
+import {
+ ResizableHandle,
+ ResizablePanel,
+ ResizablePanelGroup,
+} from '@evaluate/components/resizable';
+import { Children } from 'react';
+
+export function DesktopWrapper({ children }: React.PropsWithChildren) {
+ const [explorer, editor, terminal] = Children.toArray(children);
+
+ return (
+
+
+ {explorer}
+
+
+
+
+
+ {editor}
+
+
+
+
+
+ {terminal}
+
+
+ );
+}
diff --git a/apps/website/src/app/(editor)/playgrounds/[playground]/wrapper/index.tsx b/apps/website/src/app/(editor)/playgrounds/[playground]/wrapper/index.tsx
new file mode 100644
index 0000000..e55c8a7
--- /dev/null
+++ b/apps/website/src/app/(editor)/playgrounds/[playground]/wrapper/index.tsx
@@ -0,0 +1,16 @@
+'use client';
+
+import { useMediaQuery } from '@evaluate/hooks/media-query';
+import { Children } from 'react';
+import { DesktopWrapper } from './desktop';
+import { MobileWrapper } from './mobile';
+import { EditorWrapperSkeleton } from './skeleton';
+
+export function EditorWrapper({ children }: React.PropsWithChildren) {
+ if (Children.count(children) !== 3) throw new Error('Invalid children');
+ const isDesktop = useMediaQuery('lg');
+
+ if (isDesktop === undefined) return ;
+ if (isDesktop) return {children};
+ return {children};
+}
diff --git a/apps/website/src/app/(editor)/playgrounds/[playground]/wrapper/mobile.tsx b/apps/website/src/app/(editor)/playgrounds/[playground]/wrapper/mobile.tsx
new file mode 100644
index 0000000..19b551b
--- /dev/null
+++ b/apps/website/src/app/(editor)/playgrounds/[playground]/wrapper/mobile.tsx
@@ -0,0 +1,52 @@
+'use client';
+
+import { Sheet, SheetContent } from '@evaluate/components/sheet';
+import { useEventListener } from '@evaluate/hooks/event-listener';
+import { Children, useState } from 'react';
+
+export function MobileWrapper({ children }: React.PropsWithChildren) {
+ const [explorer, editor, terminal] = Children.toArray(children);
+
+ const [explorerOpen, setExplorerOpen] = useState(false);
+ useEventListener('mobile-explorer-open-change' as never, setExplorerOpen);
+ const [terminalOpen, setTerminalOpen] = useState(false);
+ useEventListener('mobile-terminal-open-change' as never, setTerminalOpen);
+
+ return (
+ <>
+
+ setExplorerOpen(false)}
+ >
+ e.stopPropagation()}
+ onKeyUp={(e) => e.stopPropagation()}
+ className="h-full rounded-xl border-2 bg-card"
+ >
+ {explorer}
+
+
+
+
+ {editor}
+
+
+ setExplorerOpen(false)}
+ >
+ e.stopPropagation()}
+ onKeyUp={(e) => e.stopPropagation()}
+ className="h-full rounded-xl border-2 bg-card"
+ >
+ {terminal}
+
+
+
+ >
+ );
+}
diff --git a/apps/website/src/app/(editor)/playgrounds/[playground]/wrapper/skeleton.tsx b/apps/website/src/app/(editor)/playgrounds/[playground]/wrapper/skeleton.tsx
new file mode 100644
index 0000000..0495683
--- /dev/null
+++ b/apps/website/src/app/(editor)/playgrounds/[playground]/wrapper/skeleton.tsx
@@ -0,0 +1,17 @@
+import { Skeleton } from '@evaluate/components/skeleton';
+import { twMerge as cn } from 'tailwind-merge';
+
+export function EditorWrapperSkeleton({ className }: { className?: string }) {
+ return (
+
+
+
+
+
+ );
+}
diff --git a/apps/website/src/app/(playgrounds)/playgrounds/content.tsx b/apps/website/src/app/(playgrounds)/playgrounds/content.tsx
deleted file mode 100644
index feff6e2..0000000
--- a/apps/website/src/app/(playgrounds)/playgrounds/content.tsx
+++ /dev/null
@@ -1,124 +0,0 @@
-'use client';
-
-import { Input } from '@evaluate/components/input';
-import {
- Select,
- SelectContent,
- SelectGroup,
- SelectItem,
- SelectLabel,
- SelectTrigger,
- SelectValue,
-} from '@evaluate/components/select';
-import { toast } from '@evaluate/components/toast';
-import type { PartialRuntime } from '@evaluate/shapes';
-import Fuse from 'fuse.js';
-import {
- ArrowDownWideNarrowIcon,
- CircleDotIcon,
- SearchIcon,
- XIcon,
-} from 'lucide-react';
-import { useDeferredValue, useEffect, useMemo } from 'react';
-import { useHashFragment } from '~/hooks/use-hash-fragment';
-import { useQueryParameter } from '~/hooks/use-query-parameter';
-import { PlaygroundCard } from './_components/playground-card';
-
-export default function PlaygroundsPageContent(p: {
- runtimes: PartialRuntime[];
-}) {
- const [search, setSearch] = useQueryParameter('search');
- const deferredSearch = useDeferredValue(search);
-
- const queriedRuntimes = useMemo(() => {
- if (!deferredSearch) return p.runtimes;
- const engine = new Fuse(p.runtimes, {
- keys: ['name', 'aliases', 'tags'],
- threshold: 0.3,
- });
- return engine.search(deferredSearch).map((result) => result.item);
- }, [p.runtimes, deferredSearch]);
-
- type SortBy = 'popularity' | 'name';
- const [sortBy, setSortBy] = useQueryParameter('sort', 'popularity');
- const sortedRuntimes = useMemo(() => {
- return queriedRuntimes.sort((a, b) => {
- if (sortBy === 'name') return a.name.localeCompare(b.name);
- return b.popularity - a.popularity;
- });
- }, [queriedRuntimes, sortBy]);
-
- const [hash] = useHashFragment();
- useEffect(() => {
- if (hash)
- toast.info('Choose a playground!', {
- icon: ,
- });
- }, [hash]);
-
- return (
-
-
-
- Playgrounds
-
-
- Explore and run code in different programming languages and runtimes.
-
-
- Powered by the Piston execution engine.
-
-
-
-
-
-
-
-
-
- setSearch(e.target.value)}
- />
-
- {search && (
- setSearch('')}
- >
- Clear Search
-
- )}
-
-
-
-
-
-
- {sortedRuntimes.map((runtime) => (
-
- ))}
-
-
-
- );
-}
diff --git a/apps/website/src/app/(playgrounds)/playgrounds/_components/get-colour.ts b/apps/website/src/app/(playgrounds)/playgrounds/get-colour.ts
similarity index 100%
rename from apps/website/src/app/(playgrounds)/playgrounds/_components/get-colour.ts
rename to apps/website/src/app/(playgrounds)/playgrounds/get-colour.ts
diff --git a/apps/website/src/app/(playgrounds)/playgrounds/page.tsx b/apps/website/src/app/(playgrounds)/playgrounds/page.tsx
index 742cc95..0988e9b 100644
--- a/apps/website/src/app/(playgrounds)/playgrounds/page.tsx
+++ b/apps/website/src/app/(playgrounds)/playgrounds/page.tsx
@@ -1,7 +1,6 @@
import { fetchRuntimes } from '@evaluate/engine/runtimes';
-import { Suspense } from 'react';
import { generateBaseMetadata } from '~/app/metadata';
-import PlaygroundsPageContent from './content';
+import { PlaygroundCardList } from './playground-card-list';
export function generateMetadata() {
return generateBaseMetadata('/playgrounds');
@@ -9,9 +8,23 @@ export function generateMetadata() {
export default async function PlaygroundsPage() {
const runtimes = await fetchRuntimes();
+
return (
-
-
-
+
+
+
+ Playgrounds
+
+
+ Explore and run code in different programming languages and runtimes.
+
+
+ Powered by the Piston execution engine.
+
+
+
+
+
+
);
}
diff --git a/apps/website/src/app/(playgrounds)/playgrounds/playground-card-list.tsx b/apps/website/src/app/(playgrounds)/playgrounds/playground-card-list.tsx
new file mode 100644
index 0000000..e6351b7
--- /dev/null
+++ b/apps/website/src/app/(playgrounds)/playgrounds/playground-card-list.tsx
@@ -0,0 +1,110 @@
+'use client';
+
+import { Input } from '@evaluate/components/input';
+import {
+ Select,
+ SelectContent,
+ SelectGroup,
+ SelectItem,
+ SelectLabel,
+ SelectTrigger,
+ SelectValue,
+} from '@evaluate/components/select';
+import { toast } from '@evaluate/components/toast';
+import type { PartialRuntime } from '@evaluate/shapes';
+import Fuse from 'fuse.js';
+import {
+ ArrowDownWideNarrowIcon,
+ CircleDotIcon,
+ SearchIcon,
+ XIcon,
+} from 'lucide-react';
+import { useDeferredValue, useEffect, useMemo } from 'react';
+import { useHashFragment } from '~/hooks/use-hash-fragment';
+import { useQueryParameter } from '~/hooks/use-query-parameter';
+import { PlaygroundCard } from './playground-card';
+
+export function PlaygroundCardList({
+ initialRuntimes,
+}: { initialRuntimes: PartialRuntime[] }) {
+ const [search, setSearch] = useQueryParameter('search');
+ const deferredSearch = useDeferredValue(search);
+ const searchedRuntimes = useMemo(() => {
+ if (!deferredSearch) return initialRuntimes;
+ const engine = new Fuse(initialRuntimes, {
+ keys: ['name', 'aliases', 'tags'],
+ threshold: 0.3,
+ });
+ return engine
+ .search(deferredSearch) //
+ .map((result) => result.item);
+ }, [initialRuntimes, deferredSearch]);
+
+ type SortBy = 'popularity' | 'name';
+ const [sortBy, setSortBy] = useQueryParameter('sort', 'popularity');
+ const sortedRuntimes = useMemo(() => {
+ return searchedRuntimes.sort((a, b) => {
+ if (sortBy === 'name') return a.name.localeCompare(b.name);
+ return b.popularity - a.popularity;
+ });
+ }, [searchedRuntimes, sortBy]);
+
+ const [hash] = useHashFragment();
+ useEffect(() => {
+ if (hash)
+ toast.info('Choose a playground!', {
+ icon: ,
+ });
+ }, [hash]);
+
+ return (
+
+
+
+
+
+ setSearch(e.target.value)}
+ />
+
+ {search && (
+ setSearch('')}
+ >
+ Clear Search
+
+ )}
+
+
+
+
+
+
+ {sortedRuntimes.map((runtime) => (
+
+ ))}
+
+
+ );
+}
diff --git a/apps/website/src/app/(playgrounds)/playgrounds/_components/playground-card.tsx b/apps/website/src/app/(playgrounds)/playgrounds/playground-card.tsx
similarity index 71%
rename from apps/website/src/app/(playgrounds)/playgrounds/_components/playground-card.tsx
rename to apps/website/src/app/(playgrounds)/playgrounds/playground-card.tsx
index 3a071ad..26b8a3b 100644
--- a/apps/website/src/app/(playgrounds)/playgrounds/_components/playground-card.tsx
+++ b/apps/website/src/app/(playgrounds)/playgrounds/playground-card.tsx
@@ -10,11 +10,8 @@ import type { PartialRuntime } from '@evaluate/shapes';
import { CodeIcon } from 'lucide-react';
import Link from 'next/link';
import { useRef, useState } from 'react';
-import {
- type RGB,
- getDominantColour,
-} from '~/app/(playgrounds)/playgrounds/_components/get-colour';
import { ImageWithFallback } from '~/components/image-fallback';
+import { type RGB, getDominantColour } from './get-colour';
declare module 'react' {
namespace CSS {
@@ -24,10 +21,10 @@ declare module 'react' {
}
}
-export function PlaygroundCard(p: {
- runtime: PartialRuntime;
- hash?: string;
-}) {
+export function PlaygroundCard({
+ runtime,
+ hash,
+}: { runtime: PartialRuntime; hash?: string }) {
const imageRef = useRef(null);
const [colour, setColour] = useState();
@@ -40,27 +37,27 @@ export function PlaygroundCard(p: {
}
onLoad={() => setColour(getDominantColour(imageRef.current!))}
/>
- {p.runtime.name}
+ {runtime.name}
- v{p.runtime.versions.at(-1)!}
+ v{runtime.versions.at(-1)!}
- {`Open ${p.runtime.name} Playground`}
+ {`Open ${runtime.name} Playground`}
);
diff --git a/apps/website/src/app/(products)/products/browser-extension/_components/image-carousel.tsx b/apps/website/src/app/(products)/products/browser-extension/image-carousel.tsx
similarity index 100%
rename from apps/website/src/app/(products)/products/browser-extension/_components/image-carousel.tsx
rename to apps/website/src/app/(products)/products/browser-extension/image-carousel.tsx
diff --git a/apps/website/src/app/(products)/products/browser-extension/page.tsx b/apps/website/src/app/(products)/products/browser-extension/page.tsx
index 92b0b0a..ce667c2 100644
--- a/apps/website/src/app/(products)/products/browser-extension/page.tsx
+++ b/apps/website/src/app/(products)/products/browser-extension/page.tsx
@@ -1,6 +1,6 @@
import { Button } from '@evaluate/components/button';
import Link from 'next/link';
-import { ImageCarousel } from './_components/image-carousel';
+import { ImageCarousel } from './image-carousel';
export default function BrowserExtensionPlatformPage() {
return (
@@ -16,81 +16,105 @@ export default function BrowserExtensionPlatformPage() {
quick code execution capabilities.
-
-
+
- Evaluate for Brave
+
+ Evaluate for Firefox
+
diff --git a/apps/website/src/components/explorer/file/item.tsx b/apps/website/src/components/explorer/file/item.tsx
index 4bfa7d4..a4257dd 100644
--- a/apps/website/src/components/explorer/file/item.tsx
+++ b/apps/website/src/components/explorer/file/item.tsx
@@ -45,7 +45,7 @@ export function ExplorerFileItem({ file, meta }: ExplorerFileItem.Props) {
{!meta && (
diff --git a/apps/website/src/components/explorer/folder/item.tsx b/apps/website/src/components/explorer/folder/item.tsx
index c81fb75..edbd975 100644
--- a/apps/website/src/components/explorer/folder/item.tsx
+++ b/apps/website/src/components/explorer/folder/item.tsx
@@ -72,7 +72,7 @@ export function ExplorerFolderItem({ folder }: ExplorerFolderItem.Props) {
diff --git a/packages/components/src/skeleton.tsx b/packages/components/src/skeleton.tsx
new file mode 100644
index 0000000..5ffbd0d
--- /dev/null
+++ b/packages/components/src/skeleton.tsx
@@ -0,0 +1,18 @@
+import React from 'react';
+import { twMerge as cn } from 'tailwind-merge';
+
+void React;
+
+function Skeleton({
+ className,
+ ...props
+}: React.HTMLAttributes
) {
+ return (
+
+ );
+}
+
+export { Skeleton };
diff --git a/packages/hooks/src/media-query.ts b/packages/hooks/src/media-query.ts
index 04198ba..1d917fb 100644
--- a/packages/hooks/src/media-query.ts
+++ b/packages/hooks/src/media-query.ts
@@ -1,4 +1,4 @@
-import { useEffect, useState } from 'react';
+import { useState } from 'react';
export function createMediaQueryHook>(
screens: T,
@@ -8,23 +8,27 @@ export function createMediaQueryHook>(
| Extract
| (`(${'min' | 'max'}-width: ${string})` & {}),
) {
- const [matches, setMatches] = useState(false);
const mediaQuery =
query in screens ? `(min-width: ${screens[query]})` : query;
- useEffect(() => {
+ const [matches, setMatches] = useState();
+
+ useIsomorphicLayoutEffect(() => {
+ if (typeof window === 'undefined') return;
+
const mediaQueryList = window.matchMedia(mediaQuery);
setMatches(mediaQueryList.matches);
- const listener = (e: MediaQueryListEvent) => setMatches(e.matches);
- mediaQueryList.addEventListener('change', listener);
- return () => mediaQueryList.removeEventListener('change', listener);
- }, [mediaQuery]);
+ const syncMatches = () => setMatches(mediaQueryList.matches);
+ mediaQueryList.addEventListener('change', syncMatches);
+ return () => mediaQueryList.removeEventListener('change', syncMatches);
+ }, []);
return matches;
};
}
import tailwindConfig from '@evaluate/style/tailwind-preset';
+import { useIsomorphicLayoutEffect } from './isomorphic-layout-effect';
export const useMediaQuery = //
createMediaQueryHook(tailwindConfig.theme.extend.screens);