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.

-
- + + Chrome + + Evaluate for Chrome + + + - + + Edge + + - + + Opera + + + + +
+ +
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);