diff --git a/langs/en/examples-src/$descriptor.json b/langs/en/examples-src/$descriptor.json new file mode 100644 index 00000000..95e1c458 --- /dev/null +++ b/langs/en/examples-src/$descriptor.json @@ -0,0 +1,14 @@ +[ + "counter", + "todos", + "forms", + "cssanimations", + "context", + "clock", + "ethasketch", + "scoreboard", + "asyncresource", + "suspensetabs", + "simpletodos", + "simpletodoshyperscript" +] diff --git a/langs/en/examples-src/README.md b/langs/en/examples-src/README.md new file mode 100644 index 00000000..cd304b54 --- /dev/null +++ b/langs/en/examples-src/README.md @@ -0,0 +1,26 @@ +# README of /examples-src/ + +This folder contains all the existing examples. + + +## Creating an example + +1. add a new folder like `/examples-src/new-example` (`new-example` is later used as the example ID) +2. fill the `/examples-src/new-example` folder with all the files of the new example +3. add an `/examples-src/new-example/$descriptor.json` file with meaningful data (see existing examples) + +NOTE: only the files included in the `$descriptor.files` list will be published + + +## Publishing an example + +1. add the example ID to the `/examples-src/$descriptor.json` file + +NOTE: only the examples included in the `$descriptor` list will be published + + +## Rollup + +The `generate-json-folders` (rollup plugin) creates a bunch of JSON files based on the structure of the `examples-src` tree and its `$descriptor` files. + +Just run `$ yarn build` to see the result in `/examples/`. diff --git a/langs/en/examples-src/asyncresource/$descriptor.json b/langs/en/examples-src/asyncresource/$descriptor.json new file mode 100644 index 00000000..d8dd20a6 --- /dev/null +++ b/langs/en/examples-src/asyncresource/$descriptor.json @@ -0,0 +1,5 @@ +{ + "name": "Complex/Async Resource", + "description": "Ajax requests to SWAPI with Promise cancellation", + "files": ["main.jsx"] +} \ No newline at end of file diff --git a/langs/en/examples-src/asyncresource/main.jsx b/langs/en/examples-src/asyncresource/main.jsx new file mode 100644 index 00000000..f80207be --- /dev/null +++ b/langs/en/examples-src/asyncresource/main.jsx @@ -0,0 +1,27 @@ +import { createSignal, createResource } from "solid-js"; +import { render } from "solid-js/web"; + +const fetchUser = async (id) => + (await fetch(`https://swapi.dev/api/people/${id}/`)).json(); + +const App = () => { + const [userId, setUserId] = createSignal(); + const [user] = createResource(userId, fetchUser); + + return ( + <> + setUserId(e.currentTarget.value)} + /> + {user.loading && "Loading..."} +
+
{JSON.stringify(user(), null, 2)}
+
+ + ); +}; + +render(App, document.getElementById("app")); \ No newline at end of file diff --git a/langs/en/examples-src/clock/$descriptor.json b/langs/en/examples-src/clock/$descriptor.json new file mode 100644 index 00000000..09d0e56e --- /dev/null +++ b/langs/en/examples-src/clock/$descriptor.json @@ -0,0 +1,5 @@ +{ + "name": "Complex/Clock", + "description": "Demonstrates Solid reactivity with a real-time clock example", + "files": ["main.tsx","Clock.tsx","Lines.tsx","Hand.tsx","utils.tsx","styles.css"] +} \ No newline at end of file diff --git a/langs/en/examples-src/clock/Clock.tsx b/langs/en/examples-src/clock/Clock.tsx new file mode 100644 index 00000000..a3389afc --- /dev/null +++ b/langs/en/examples-src/clock/Clock.tsx @@ -0,0 +1,51 @@ +import { createSignal, onCleanup } from 'solid-js'; +import { Hand } from './Hand'; +import { Lines } from './Lines'; +import { createAnimationLoop } from './utils'; +import type { Accessor, Component } from 'solid-js'; + +const getSecondsSinceMidnight = (): number => (Date.now() - new Date().setHours(0, 0, 0, 0)) / 1000; + +type ClockFaceProps = { + hour: string; + minute: string; + second: string; + subsecond: string; +}; + +export const ClockFace: Component = (props) => ( + + + {/* static */} + + + + {/* dynamic */} + + + + + + +); + +export const Clock: Component = () => { + const [time, setTime] = createSignal(getSecondsSinceMidnight()); + const dispose = createAnimationLoop(() => { + setTime(getSecondsSinceMidnight()); + }); + onCleanup(dispose); + + const rotate = (rotate: number, fixed: number = 1) => `rotate(${(rotate * 360).toFixed(fixed)})`; + + const subsecond = () => rotate(time() % 1); + const second = () => rotate((time() % 60) / 60); + const minute = () => rotate(((time() / 60) % 60) / 60); + const hour = () => rotate(((time() / 60 / 60) % 12) / 12); + + return ( +
+ +
+ ); +}; diff --git a/langs/en/examples-src/clock/Hand.tsx b/langs/en/examples-src/clock/Hand.tsx new file mode 100644 index 00000000..4391317d --- /dev/null +++ b/langs/en/examples-src/clock/Hand.tsx @@ -0,0 +1,18 @@ +import { Component, splitProps } from 'solid-js'; + +type HandProps = { rotate: string; class: string; length: number; width: number; fixed?: boolean }; + +export const Hand: Component = (props) => { + const [local, rest] = splitProps(props, ['rotate', 'length', 'width', 'fixed']); + return ( + + ); +}; diff --git a/langs/en/examples-src/clock/Lines.tsx b/langs/en/examples-src/clock/Lines.tsx new file mode 100644 index 00000000..662896c6 --- /dev/null +++ b/langs/en/examples-src/clock/Lines.tsx @@ -0,0 +1,21 @@ +import { Hand } from './Hand'; +import { Component, splitProps, For } from 'solid-js'; + +type LinesProps = { + numberOfLines: number; + class: string; + length: number; + width: number; +}; + +const rotate = (index: number, length: number) => `rotate(${(360 * index) / length})`; + +export const Lines: Component = (props) => { + const [local, rest] = splitProps(props, ['numberOfLines']); + + return ( + + {(_, index) => } + + ); +}; diff --git a/langs/en/examples-src/clock/main.tsx b/langs/en/examples-src/clock/main.tsx new file mode 100644 index 00000000..235250ea --- /dev/null +++ b/langs/en/examples-src/clock/main.tsx @@ -0,0 +1,5 @@ +import { render } from 'solid-js/web'; +import { Clock } from './Clock'; +import './styles.css'; + +render(() => , document.getElementById('app')!); diff --git a/langs/en/examples-src/clock/styles.css b/langs/en/examples-src/clock/styles.css new file mode 100644 index 00000000..22c6056c --- /dev/null +++ b/langs/en/examples-src/clock/styles.css @@ -0,0 +1,20 @@ +.clock { + display: flex; + justify-content: center; + align-items: center; + flex-wrap: wrap; + height: 100vh; +} + +.subsecond { + color: silver; +} + +.hour, +.minute { + color: black; +} + +.second { + color: tomato; +} diff --git a/langs/en/examples-src/clock/utils.tsx b/langs/en/examples-src/clock/utils.tsx new file mode 100644 index 00000000..d7487148 --- /dev/null +++ b/langs/en/examples-src/clock/utils.tsx @@ -0,0 +1,48 @@ +// ported from voby https://github.com/vobyjs/voby/blob/master/src/hooks/use_scheduler.ts +import { Accessor } from 'solid-js'; + +type FN = ( + ...args: Arguments +) => Return; +type MaybeAccessor = Accessor | T; +const isFunction = (value: unknown): value is (...args: unknown[]) => unknown => + typeof value === 'function'; +const unwrap = (maybeValue: MaybeAccessor): T => + isFunction(maybeValue) ? maybeValue() : maybeValue; + +export const createScheduler = ({ + loop, + callback, + cancel, + schedule, +}: { + loop?: MaybeAccessor; + callback: MaybeAccessor>; + cancel: FN<[T]>; + schedule: (callback: FN<[U]>) => T; +}): (() => void) => { + let tickId: T; + const work = (): void => { + if (unwrap(loop)) tick(); + unwrap(callback); + }; + + const tick = (): void => { + tickId = schedule(work); + }; + + const dispose = (): void => { + cancel(tickId); + }; + + tick(); + return dispose; +}; + +export const createAnimationLoop = (callback: FrameRequestCallback) => + createScheduler({ + callback, + loop: true, + cancel: cancelAnimationFrame, + schedule: requestAnimationFrame, + }); diff --git a/langs/en/examples-src/context/$descriptor.json b/langs/en/examples-src/context/$descriptor.json new file mode 100644 index 00000000..ae25e286 --- /dev/null +++ b/langs/en/examples-src/context/$descriptor.json @@ -0,0 +1,5 @@ +{ + "name": "Basic/Context", + "description": "A simple color picker using Context", + "files": ["main.tsx","theme.tsx"] +} \ No newline at end of file diff --git a/langs/en/examples-src/context/main.tsx b/langs/en/examples-src/context/main.tsx new file mode 100644 index 00000000..36a7a614 --- /dev/null +++ b/langs/en/examples-src/context/main.tsx @@ -0,0 +1,34 @@ +import { render } from "solid-js/web"; +import { ThemeProvider, useTheme } from "./theme"; + +function App() { + const [theme, { changeColor }] = useTheme(); + + return ( + <> +

+ {theme.title} +

+ changeColor(e.currentTarget.value)} + /> + + + ); +} + +render( + () => ( + + + + ), + document.getElementById("app")! +); diff --git a/langs/en/examples-src/context/theme.tsx b/langs/en/examples-src/context/theme.tsx new file mode 100644 index 00000000..7ddc95eb --- /dev/null +++ b/langs/en/examples-src/context/theme.tsx @@ -0,0 +1,48 @@ +import { createContext, useContext, ParentComponent } from "solid-js"; +import { createStore } from "solid-js/store"; + +export type ThemeContextState = { + readonly color: string; + readonly title: string; +}; +export type ThemeContextValue = [ + state: ThemeContextState, + actions: { + changeColor: (color: string) => void; + changeTitle: (title: string) => void; + } +]; + +const defaultState = { + color: "#66e6ac", + title: "Fallback Title", +}; + +const ThemeContext = createContext([ + defaultState, + { + changeColor: () => undefined, + changeTitle: () => undefined, + }, +]); + +export const ThemeProvider: ParentComponent<{ + color?: string; + title?: string; +}> = (props) => { + const [state, setState] = createStore({ + color: props.color ?? defaultState.color, + title: props.title ?? defaultState.title, + }); + + const changeColor = (color: string) => setState("color", color); + const changeTitle = (title: string) => setState("title", title); + + return ( + + {props.children} + + ); +}; + +export const useTheme = () => useContext(ThemeContext); diff --git a/langs/en/examples-src/counter/$descriptor.json b/langs/en/examples-src/counter/$descriptor.json new file mode 100644 index 00000000..5c160030 --- /dev/null +++ b/langs/en/examples-src/counter/$descriptor.json @@ -0,0 +1,5 @@ +{ + "name": "Basic/Counter", + "description": "A simple standard counter example", + "files": ["main.jsx"] +} \ No newline at end of file diff --git a/langs/en/examples-src/counter/main.jsx b/langs/en/examples-src/counter/main.jsx new file mode 100644 index 00000000..708e5aa1 --- /dev/null +++ b/langs/en/examples-src/counter/main.jsx @@ -0,0 +1,14 @@ +import { createSignal, onCleanup } from "solid-js"; +import { render } from "solid-js/web"; + +const CountingComponent = () => { + const [count, setCount] = createSignal(0); + const interval = setInterval( + () => setCount(c => c + 1), + 1000 + ); + onCleanup(() => clearInterval(interval)); + return
Count value is {count()}
; +}; + +render(() => , document.getElementById("app")); \ No newline at end of file diff --git a/langs/en/examples-src/counterstore/$descriptor.json b/langs/en/examples-src/counterstore/$descriptor.json new file mode 100644 index 00000000..7668bc47 --- /dev/null +++ b/langs/en/examples-src/counterstore/$descriptor.json @@ -0,0 +1,3 @@ +{ + "files": ["main.jsx","CounterStore.jsx"] +} \ No newline at end of file diff --git a/langs/en/examples-src/counterstore/CounterStore.jsx b/langs/en/examples-src/counterstore/CounterStore.jsx new file mode 100644 index 00000000..c4ee33cc --- /dev/null +++ b/langs/en/examples-src/counterstore/CounterStore.jsx @@ -0,0 +1,33 @@ +import { createSignal, createContext, useContext, Component } from "solid-js"; + +type CounterStore = [ + () => number, + { increment?: () => void; decrement?: () => void } +]; + +const CounterContext = createContext([() => 0, {}]); + +export const CounterProvider: Component<{ count: number }> = props => { + const [count, setCount] = createSignal(props.count || 0), + store: CounterStore = [ + count, + { + increment() { + setCount(c => c + 1); + }, + decrement() { + setCount(c => c - 1); + } + } + ]; + + return ( + + {props.children} + + ); +}; + +export function useCounter() { + return useContext(CounterContext); +} \ No newline at end of file diff --git a/langs/en/examples-src/counterstore/main.jsx b/langs/en/examples-src/counterstore/main.jsx new file mode 100644 index 00000000..e47a120f --- /dev/null +++ b/langs/en/examples-src/counterstore/main.jsx @@ -0,0 +1,23 @@ +import { render } from "solid-js/web"; +import { CounterProvider, useCounter } from "CounterStore.tsx"; + +const MiddleComponent = () => ; + +const NestedComponent = () => { + const [count, { increment, decrement }] = useCounter(); + return ( + <> +

{count()}

+ + + + ); +}; + +const App = () => ( + + + +); + +render(App, document.getElementById("app")); \ No newline at end of file diff --git a/langs/en/examples-src/cssanimations/$descriptor.json b/langs/en/examples-src/cssanimations/$descriptor.json new file mode 100644 index 00000000..023d35de --- /dev/null +++ b/langs/en/examples-src/cssanimations/$descriptor.json @@ -0,0 +1,5 @@ +{ + "name": "Basic/CSS Animations", + "description": "Using Solid Transition Group", + "files": ["main.jsx","styles.css"] +} \ No newline at end of file diff --git a/langs/en/examples-src/cssanimations/main.jsx b/langs/en/examples-src/cssanimations/main.jsx new file mode 100644 index 00000000..8d36d438 --- /dev/null +++ b/langs/en/examples-src/cssanimations/main.jsx @@ -0,0 +1,121 @@ +import { createSignal, For, Match, Switch } from "solid-js"; +import { render } from "solid-js/web"; +import { Transition, TransitionGroup } from "solid-transition-group"; +import "./styles.css" + +function shuffle(array) { + return array.sort(() => Math.random() - 0.5); +} +let nextId = 10; + +const App = () => { + const [show, toggleShow] = createSignal(true), + [select, setSelect] = createSignal(0), + [numList, setNumList] = createSignal([1, 2, 3, 4, 5, 6, 7, 8, 9]), + randomIndex = () => Math.floor(Math.random() * numList().length); + + return ( + <> + +
+ Transition: + + {show() && ( +
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris + facilisis enim libero, at lacinia diam fermentum id. Pellentesque + habitant morbi tristique senectus et netus. +
+ )} +
+
+ Animation: + + {show() && ( +
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris + facilisis enim libero, at lacinia diam fermentum id. Pellentesque + habitant morbi tristique senectus et netus. +
+ )} +
+
+ Custom JS: + { + const a = el.animate([{ opacity: 0 }, { opacity: 1 }], { + duration: 600 + }); + a.finished.then(done); + }} + onExit={(el, done) => { + const a = el.animate([{ opacity: 1 }, { opacity: 0 }], { + duration: 600 + }); + a.finished.then(done); + }} + > + {show() && ( +
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris + facilisis enim libero, at lacinia diam fermentum id. Pellentesque + habitant morbi tristique senectus et netus. +
+ )} +
+
+ Switch OutIn +
+ + + + +

The First

+
+ +

The Second

+
+ +

The Third

+
+
+
+ Group +
+ + + +
+ + {(r) => {r}} + + + ); +}; + +render(App, document.getElementById("app")); \ No newline at end of file diff --git a/langs/en/examples-src/cssanimations/styles.css b/langs/en/examples-src/cssanimations/styles.css new file mode 100644 index 00000000..14108246 --- /dev/null +++ b/langs/en/examples-src/cssanimations/styles.css @@ -0,0 +1,57 @@ +.container { + position: relative; +} + +.fade-enter-active, +.fade-exit-active { + transition: opacity 0.5s; +} +.fade-enter, +.fade-exit-to { + opacity: 0; +} + +.slide-fade-enter-active { + transition: all 0.3s ease; +} +.slide-fade-exit-active { + transition: all 0.8s cubic-bezier(1, 0.5, 0.8, 1); +} +.slide-fade-enter, +.slide-fade-exit-to { + transform: translateX(10px); + opacity: 0; +} + +.bounce-enter-active { + animation: bounce-in 0.5s; +} +.bounce-exit-active { + animation: bounce-in 0.5s reverse; +} +@keyframes bounce-in { + 0% { + transform: scale(0); + } + 50% { + transform: scale(1.5); + } + 100% { + transform: scale(1); + } +} + +.list-item { + transition: all 0.5s; + display: inline-block; + margin-right: 10px; +} + +.list-item-enter, +.list-item-exit-to { + opacity: 0; + transform: translateY(30px); +} +.list-item-exit-active { + position: absolute; +} diff --git a/langs/en/examples-src/ethasketch/$descriptor.json b/langs/en/examples-src/ethasketch/$descriptor.json new file mode 100644 index 00000000..c729e695 --- /dev/null +++ b/langs/en/examples-src/ethasketch/$descriptor.json @@ -0,0 +1,5 @@ +{ + "name": "Complex/Etch A Sketch", + "description": "Uses Index and createMemo to create a grid graphic", + "files": ["main.tsx","styles.css"] +} \ No newline at end of file diff --git a/langs/en/examples-src/ethasketch/main.tsx b/langs/en/examples-src/ethasketch/main.tsx new file mode 100644 index 00000000..7034331b --- /dev/null +++ b/langs/en/examples-src/ethasketch/main.tsx @@ -0,0 +1,69 @@ +// Project idea from https://www.theodinproject.com/paths/foundations/courses/foundations/lessons/etch-a-sketch-project +import { render } from "solid-js/web"; +import { createSignal, createMemo, Index } from "solid-js"; + +import "./styles.css"; + +const maxGridPixelWidth = 500; + +function randomHexColorString(): string { + return "#" + Math.floor(Math.random() * 16777215).toString(16); +} + +function clampGridSideLength(newSideLength: number): number { + return Math.min(Math.max(newSideLength, 0), 100); +} + +function EtchASketch() { + const [gridSideLength, setGridSideLength] = createSignal(10); + const gridTemplateString = createMemo( + () => + `repeat(${gridSideLength()}, ${maxGridPixelWidth / gridSideLength()}px)` + ); + + return ( + <> +
+ + + setGridSideLength( + clampGridSideLength(e.currentTarget.valueAsNumber) + ) + } + /> +
+
+ + {() => ( +
{ + const eventEl = event.currentTarget; + + eventEl.style.backgroundColor = randomHexColorString(); + + setTimeout(() => { + eventEl.style.backgroundColor = "initial"; + }, 500); + }} + >
+ )} +
+
+ + ); +} + +render(() => , document.getElementById("app")); diff --git a/langs/en/examples-src/ethasketch/styles.css b/langs/en/examples-src/ethasketch/styles.css new file mode 100644 index 00000000..2b6b5cdf --- /dev/null +++ b/langs/en/examples-src/ethasketch/styles.css @@ -0,0 +1,7 @@ +.cell { + outline: 1px solid #1f1f1f; +} + +.dark .cell { + outline: 1px solid #efefef; +} diff --git a/langs/en/examples-src/forms/$descriptor.json b/langs/en/examples-src/forms/$descriptor.json new file mode 100644 index 00000000..1684210a --- /dev/null +++ b/langs/en/examples-src/forms/$descriptor.json @@ -0,0 +1,5 @@ +{ + "name": "Basic/Form Validation", + "description": "HTML 5 validators with custom async validation", + "files": ["main.jsx","validation.jsx","styles.css"] +} \ No newline at end of file diff --git a/langs/en/examples-src/forms/main.jsx b/langs/en/examples-src/forms/main.jsx new file mode 100644 index 00000000..fb76963f --- /dev/null +++ b/langs/en/examples-src/forms/main.jsx @@ -0,0 +1,76 @@ +// @ts-nocheck +import { render } from "solid-js/web"; +import { createStore } from "solid-js/store"; +import { useForm } from "./validation"; +import "./styles.css"; + +const EMAILS = ["johnsmith@outlook.com", "mary@gmail.com", "djacobs@move.org"]; + +function fetchUserName(name) { + return new Promise((resolve) => { + setTimeout(() => resolve(EMAILS.indexOf(name) > -1), 200); + }); +} + +const ErrorMessage = (props) => {props.error}; + +const App = () => { + const { validate, formSubmit, errors } = useForm({ + errorClass: "error-input" + }); + const [fields, setFields] = createStore(); + const fn = (form) => { + // form.submit() + console.log("Done"); + }; + const userNameExists = async ({ value }) => { + const exists = await fetchUserName(value); + return exists && `${value} is already being used`; + }; + const matchesPassword = ({ value }) => + value === fields.password ? false : "Passwords must Match"; + + return ( +
+

Sign Up

+
+ + {errors.email && } +
+
+ setFields("password", e.target.value)} + use:validate + /> + {errors.password && } +
+
+ + {errors.confirmpassword && ( + + )} +
+ + +
+ ); +}; + +render(App, document.getElementById("app")); diff --git a/langs/en/examples-src/forms/styles.css b/langs/en/examples-src/forms/styles.css new file mode 100644 index 00000000..0098ed1f --- /dev/null +++ b/langs/en/examples-src/forms/styles.css @@ -0,0 +1,13 @@ +input { + display: inline-block; + padding: 4px; + margin-top: 10px; + margin-bottom: 10px; +} +.error-message { + color: red; + padding: 8px; +} +.error-input { + box-shadow: 0px 0px 2px 1px red; +} \ No newline at end of file diff --git a/langs/en/examples-src/forms/validation.jsx b/langs/en/examples-src/forms/validation.jsx new file mode 100644 index 00000000..8493aadd --- /dev/null +++ b/langs/en/examples-src/forms/validation.jsx @@ -0,0 +1,61 @@ +import { createStore } from "solid-js/store"; + +function checkValid({ element, validators = [] }, setErrors, errorClass) { + return async () => { + element.setCustomValidity(""); + element.checkValidity(); + let message = element.validationMessage; + if (!message) { + for (const validator of validators) { + const text = await validator(element); + if (text) { + element.setCustomValidity(text); + break; + } + } + message = element.validationMessage; + } + if (message) { + errorClass && element.classList.toggle(errorClass, true); + setErrors({ [element.name]: message }); + } + }; +} + +export function useForm({ errorClass }) { + const [errors, setErrors] = createStore({}), + fields = {}; + + const validate = (ref, accessor) => { + const validators = accessor() || []; + let config; + fields[ref.name] = config = { element: ref, validators }; + ref.onblur = checkValid(config, setErrors, errorClass); + ref.oninput = () => { + if (!errors[ref.name]) return; + setErrors({ [ref.name]: undefined }); + errorClass && ref.classList.toggle(errorClass, false); + }; + }; + + const formSubmit = (ref, accessor) => { + const callback = accessor() || (() => {}); + ref.setAttribute("novalidate", ""); + ref.onsubmit = async (e) => { + e.preventDefault(); + let errored = false; + + for (const k in fields) { + const field = fields[k]; + await checkValid(field, setErrors, errorClass)(); + if (!errored && field.element.validationMessage) { + field.element.focus(); + errored = true; + } + } + !errored && callback(ref); + }; + }; + + return { validate, formSubmit, errors }; +} diff --git a/langs/en/examples-src/routing/$descriptor.json b/langs/en/examples-src/routing/$descriptor.json new file mode 100644 index 00000000..270be50d --- /dev/null +++ b/langs/en/examples-src/routing/$descriptor.json @@ -0,0 +1,3 @@ +{ + "files": ["main.jsx"] +} \ No newline at end of file diff --git a/langs/en/examples-src/routing/main.jsx b/langs/en/examples-src/routing/main.jsx new file mode 100644 index 00000000..69cdd1ad --- /dev/null +++ b/langs/en/examples-src/routing/main.jsx @@ -0,0 +1,65 @@ +import { createSignal, onCleanup, Component } from "solid-js"; +import { render, Switch, Match } from "solid-js/web"; + +function createRouteHandler() { + const [location, setLocation] = createSignal( + window.location.hash.slice(1) || "home" + ), + locationHandler = () => setLocation(window.location.hash.slice(1)); + window.addEventListener("hashchange", locationHandler); + onCleanup(() => window.removeEventListener("hashchange", locationHandler)); + return (match: string) => match === location(); +} + +const Home: Component = () => ( + <> +

Welcome to this Simple Routing Example

+

Click the links in the Navigation above to load different routes.

+ +); + +const Profile: Component = () => ( + <> +

Your Profile

+

This section could be about you.

+ +); + +const Settings: Component = () => ( + <> +

Settings

+

All that configuration you never really ever want to look at.

+ +); + +const App = () => { + const matches = createRouteHandler(); + return ( + <> + + + + + + + + + + + + + + ); +}; + +render(App, document.getElementById("app")); \ No newline at end of file diff --git a/langs/en/examples-src/scoreboard/$descriptor.json b/langs/en/examples-src/scoreboard/$descriptor.json new file mode 100644 index 00000000..13d3a201 --- /dev/null +++ b/langs/en/examples-src/scoreboard/$descriptor.json @@ -0,0 +1,5 @@ +{ + "name": "Complex/Scoreboard", + "description": "Make use of hooks to do simple transitions", + "files": ["main.jsx"] +} \ No newline at end of file diff --git a/langs/en/examples-src/scoreboard/main.jsx b/langs/en/examples-src/scoreboard/main.jsx new file mode 100644 index 00000000..feb9c7fb --- /dev/null +++ b/langs/en/examples-src/scoreboard/main.jsx @@ -0,0 +1,122 @@ +import { + createMemo, + createSignal, + createComputed, + onCleanup, + For +} from "solid-js"; +import { createStore } from "solid-js/store"; +import { render } from "solid-js/web"; + +const App = () => { + let newName, newScore; + const [state, setState] = createStore({ + players: [ + { name: "Mark", score: 3 }, + { name: "Troy", score: 2 }, + { name: "Jenny", score: 1 }, + { name: "David", score: 8 } + ] + }), + lastPos = new WeakMap(), + curPos = new WeakMap(), + getSorted = createMemo((list = []) => { + list.forEach((p, i) => lastPos.set(p, i)); + const newList = state.players.slice().sort((a, b) => { + if (b.score === a.score) return a.name.localeCompare(b.name); // stabalize the sort + return b.score - a.score; + }); + let updated = newList.length !== list.length; + newList.forEach( + (p, i) => lastPos.get(p) !== i && (updated = true) && curPos.set(p, i) + ); + return updated ? newList : list; + }), + handleAddClick = () => { + const name = newName.value, + score = +newScore.value; + if (!name.length || isNaN(score)) return; + setState("players", (p) => [...p, { name: name, score: score }]); + newName.value = newScore.value = ""; + }, + handleDeleteClick = (player) => { + const idx = state.players.indexOf(player); + setState("players", (p) => [...p.slice(0, idx), ...p.slice(idx + 1)]); + }, + handleScoreChange = (player, { target }) => { + const score = +target.value; + const idx = state.players.indexOf(player); + if (isNaN(+score) || idx < 0) return; + setState("players", idx, "score", score); + }, + createStyles = (player) => { + const [style, setStyle] = createSignal(); + createComputed(() => { + getSorted(); + const offset = lastPos.get(player) * 18 - curPos.get(player) * 18, + t = setTimeout(() => + setStyle({ transition: "250ms", transform: null }) + ); + setStyle({ + transform: `translateY(${offset}px)`, + transition: null + }); + onCleanup(() => clearTimeout(t)); + }); + return style; + }; + + return ( +
+
+ + {(player) => { + const getStyles = createStyles(player), + { name } = player; + return ( +
+
{name}
+
{player.score}
+
+ ); + }} +
+
+
+ + {(player) => { + const { name, score } = player; + return ( +
+ {name} + + +
+ ); + }} +
+
+ + + +
+
+
+ ); +}; + +render(App, document.getElementById("app")); \ No newline at end of file diff --git a/langs/en/examples-src/simpletodos/$descriptor.json b/langs/en/examples-src/simpletodos/$descriptor.json new file mode 100644 index 00000000..be30704f --- /dev/null +++ b/langs/en/examples-src/simpletodos/$descriptor.json @@ -0,0 +1,5 @@ +{ + "name": "Complex/Simple Todos Template Literals", + "description": "Simple Todos using Lit DOM Expressions", + "files": ["main.jsx"] +} \ No newline at end of file diff --git a/langs/en/examples-src/simpletodos/main.jsx b/langs/en/examples-src/simpletodos/main.jsx new file mode 100644 index 00000000..b0ebbc1c --- /dev/null +++ b/langs/en/examples-src/simpletodos/main.jsx @@ -0,0 +1,79 @@ +import { createEffect, For } from "solid-js"; +import { createStore } from "solid-js/store"; +import { render } from "solid-js/web"; +import html from "solid-js/html"; + +function createLocalStore(initState) { + const [state, setState] = createStore(initState); + if (localStorage.todos) setState(JSON.parse(localStorage.todos)); + createEffect(() => (localStorage.todos = JSON.stringify(state))); + return [state, setState]; +} + +const App = () => { + const [state, setState] = createLocalStore({ + todos: [], + newTitle: "", + idCounter: 0 + }); + + return html` +
+

Simple Todos Example

+ state.newTitle} + oninput=${(e) => setState({ newTitle: e.target.value })} + /> + + <${For} each=${() => state.todos} + >${(todo) => + html` +
+ { + const idx = state.todos.findIndex((t) => t.id === todo.id); + setState("todos", idx, { done: e.target.checked }); + }} + /> + { + const idx = state.todos.findIndex((t) => t.id === todo.id); + setState("todos", idx, { title: e.target.value }); + }} + /> + +
+ `} + +
+ `; +}; + +render(App, document.getElementById("app")); \ No newline at end of file diff --git a/langs/en/examples-src/simpletodoshyperscript/$descriptor.json b/langs/en/examples-src/simpletodoshyperscript/$descriptor.json new file mode 100644 index 00000000..dc0b43f5 --- /dev/null +++ b/langs/en/examples-src/simpletodoshyperscript/$descriptor.json @@ -0,0 +1,5 @@ +{ + "name": "Complex/Simple Todos Hyperscript", + "description": "Simple Todos using Hyper DOM Expressions", + "files": ["main.jsx"] +} \ No newline at end of file diff --git a/langs/en/examples-src/simpletodoshyperscript/main.jsx b/langs/en/examples-src/simpletodoshyperscript/main.jsx new file mode 100644 index 00000000..edb3308c --- /dev/null +++ b/langs/en/examples-src/simpletodoshyperscript/main.jsx @@ -0,0 +1,86 @@ +import { createEffect, For } from "solid-js"; +import { createStore } from "solid-js/store"; +import { render } from "solid-js/web"; +import h from "solid-js/h"; + +function createLocalStore(initState) { + const [state, setState] = createStore(initState); + if (localStorage.todos) setState(JSON.parse(localStorage.todos)); + createEffect(() => (localStorage.todos = JSON.stringify(state))); + return [state, setState]; +} + +const App = () => { + const [state, setState] = createLocalStore({ + todos: [], + newTitle: "", + idCounter: 0 + }); + return [ + h("h3", "Simple Todos Example"), + h("input", { + type: "text", + placeholder: "enter todo and click +", + value: () => state.newTitle, + onInput: (e) => setState("newTitle", e.target.value) + }), + h( + "button", + { + onClick: () => + setState((s) => ({ + idCounter: s.idCounter + 1, + todos: [ + ...s.todos, + { + id: state.idCounter, + title: state.newTitle, + done: false + } + ], + newTitle: "" + })) + }, + "+" + ), + h(For, { each: () => state.todos }, (todo) => + h( + "div", + h("input", { + type: "checkbox", + checked: todo.done, + onChange: (e) => + setState( + "todos", + state.todos.findIndex((t) => t.id === todo.id), + { + done: e.target.checked + } + ) + }), + h("input", { + type: "text", + value: todo.title, + onChange: (e) => + setState( + "todos", + state.todos.findIndex((t) => t.id === todo.id), + { + title: e.target.value + } + ) + }), + h( + "button", + { + onClick: () => + setState("todos", (t) => t.filter((t) => t.id !== todo.id)) + }, + "x" + ) + ) + ) + ]; +}; + +render(App, document.getElementById("app")); \ No newline at end of file diff --git a/langs/en/examples-src/styledjsx/$descriptor.json b/langs/en/examples-src/styledjsx/$descriptor.json new file mode 100644 index 00000000..dae02405 --- /dev/null +++ b/langs/en/examples-src/styledjsx/$descriptor.json @@ -0,0 +1,3 @@ +{ + "files": ["main.jsx","tab1.jsx"] +} \ No newline at end of file diff --git a/langs/en/examples-src/styledjsx/main.jsx b/langs/en/examples-src/styledjsx/main.jsx new file mode 100644 index 00000000..004efad9 --- /dev/null +++ b/langs/en/examples-src/styledjsx/main.jsx @@ -0,0 +1,33 @@ +import { createSignal } from "solid-js"; +import { render } from "solid-js/web"; + +function Button() { + const [isLoggedIn, login] = createSignal(false); + return ( + <> + + + + ); +} + +render( + () => ( + <> + + + + {(todo, i) => ( +
+ setTodos(i(), "done", e.currentTarget.checked)} + /> + setTodos(i(), "title", e.currentTarget.value)} + /> + +
+ )} +
+ + ); +}; + +render(App, document.getElementById("app")!); diff --git a/langs/en/examples-src/todos/utils.tsx b/langs/en/examples-src/todos/utils.tsx new file mode 100644 index 00000000..90e36e0b --- /dev/null +++ b/langs/en/examples-src/todos/utils.tsx @@ -0,0 +1,18 @@ +import { createEffect } from "solid-js"; +import { createStore, SetStoreFunction, Store } from "solid-js/store"; + +export function createLocalStore( + name: string, + init: T +): [Store, SetStoreFunction] { + const localState = localStorage.getItem(name); + const [state, setState] = createStore( + localState ? JSON.parse(localState) : init + ); + createEffect(() => localStorage.setItem(name, JSON.stringify(state))); + return [state, setState]; +} + +export function removeIndex(array: readonly T[], index: number): T[] { + return [...array.slice(0, index), ...array.slice(index + 1)]; +} diff --git a/langs/en/examples/$descriptor.json b/langs/en/examples/$descriptor.json new file mode 100644 index 00000000..b14180b8 --- /dev/null +++ b/langs/en/examples/$descriptor.json @@ -0,0 +1 @@ +[{"id":"counter","name":"Basic/Counter","description":"A simple standard counter example"},{"id":"todos","name":"Basic/Simple Todos","description":"Todos with LocalStorage persistence"},{"id":"forms","name":"Basic/Form Validation","description":"HTML 5 validators with custom async validation"},{"id":"cssanimations","name":"Basic/CSS Animations","description":"Using Solid Transition Group"},{"id":"context","name":"Basic/Context","description":"A simple color picker using Context"},{"id":"clock","name":"Complex/Clock","description":"Demonstrates Solid reactivity with a real-time clock example"},{"id":"ethasketch","name":"Complex/Etch A Sketch","description":"Uses Index and createMemo to create a grid graphic"},{"id":"scoreboard","name":"Complex/Scoreboard","description":"Make use of hooks to do simple transitions"},{"id":"asyncresource","name":"Complex/Async Resource","description":"Ajax requests to SWAPI with Promise cancellation"},{"id":"suspensetabs","name":"Complex/Suspense Transitions","description":"Deferred loading spinners for smooth UX"},{"id":"simpletodos","name":"Complex/Simple Todos Template Literals","description":"Simple Todos using Lit DOM Expressions"},{"id":"simpletodoshyperscript","name":"Complex/Simple Todos Hyperscript","description":"Simple Todos using Hyper DOM Expressions"}] \ No newline at end of file diff --git a/langs/en/examples/asyncresource.json b/langs/en/examples/asyncresource.json new file mode 100644 index 00000000..fb3d002e --- /dev/null +++ b/langs/en/examples/asyncresource.json @@ -0,0 +1 @@ +{"id":"asyncresource","name":"Complex/Async Resource","description":"Ajax requests to SWAPI with Promise cancellation","files":[{"name":"main","type":"jsx","content":"import { createSignal, createResource } from \"solid-js\";\nimport { render } from \"solid-js/web\";\n\nconst fetchUser = async (id) =>\n (await fetch(`https://swapi.dev/api/people/${id}/`)).json();\n\nconst App = () => {\n const [userId, setUserId] = createSignal();\n const [user] = createResource(userId, fetchUser);\n\n return (\n <>\n setUserId(e.currentTarget.value)}\n />\n {user.loading && \"Loading...\"}\n
\n
{JSON.stringify(user(), null, 2)}
\n
\n \n );\n};\n\nrender(App, document.getElementById(\"app\"));"}]} \ No newline at end of file diff --git a/langs/en/examples/clock.json b/langs/en/examples/clock.json new file mode 100644 index 00000000..3b9f682c --- /dev/null +++ b/langs/en/examples/clock.json @@ -0,0 +1 @@ +{"id":"clock","name":"Complex/Clock","description":"Demonstrates Solid reactivity with a real-time clock example","files":[{"name":"main","type":"tsx","content":"import { render } from 'solid-js/web';\nimport { Clock } from './Clock';\nimport './styles.css';\n\nrender(() => , document.getElementById('app')!);\n"},{"name":"Clock","type":"tsx","content":"import { createSignal, onCleanup } from 'solid-js';\nimport { Hand } from './Hand';\nimport { Lines } from './Lines';\nimport { createAnimationLoop } from './utils';\nimport type { Accessor, Component } from 'solid-js';\n\nconst getSecondsSinceMidnight = (): number => (Date.now() - new Date().setHours(0, 0, 0, 0)) / 1000;\n\ntype ClockFaceProps = {\n hour: string;\n minute: string;\n second: string;\n subsecond: string;\n};\n\nexport const ClockFace: Component = (props) => (\n \n \n {/* static */}\n \n \n \n {/* dynamic */}\n \n \n \n \n \n \n);\n\nexport const Clock: Component = () => {\n const [time, setTime] = createSignal(getSecondsSinceMidnight());\n const dispose = createAnimationLoop(() => {\n setTime(getSecondsSinceMidnight());\n });\n onCleanup(dispose);\n\n const rotate = (rotate: number, fixed: number = 1) => `rotate(${(rotate * 360).toFixed(fixed)})`;\n\n const subsecond = () => rotate(time() % 1);\n const second = () => rotate((time() % 60) / 60);\n const minute = () => rotate(((time() / 60) % 60) / 60);\n const hour = () => rotate(((time() / 60 / 60) % 12) / 12);\n\n return (\n
\n \n
\n );\n};\n"},{"name":"Lines","type":"tsx","content":"import { Hand } from './Hand';\nimport { Component, splitProps, For } from 'solid-js';\n\ntype LinesProps = {\n numberOfLines: number;\n class: string;\n length: number;\n width: number;\n};\n\nconst rotate = (index: number, length: number) => `rotate(${(360 * index) / length})`;\n\nexport const Lines: Component = (props) => {\n const [local, rest] = splitProps(props, ['numberOfLines']);\n\n return (\n \n {(_, index) => }\n \n );\n};\n"},{"name":"Hand","type":"tsx","content":"import { Component, splitProps } from 'solid-js';\n\ntype HandProps = { rotate: string; class: string; length: number; width: number; fixed?: boolean };\n\nexport const Hand: Component = (props) => {\n const [local, rest] = splitProps(props, ['rotate', 'length', 'width', 'fixed']);\n return (\n \n );\n};\n"},{"name":"utils","type":"tsx","content":"// ported from voby https://github.com/vobyjs/voby/blob/master/src/hooks/use_scheduler.ts\nimport { Accessor } from 'solid-js';\n\ntype FN = (\n ...args: Arguments\n) => Return;\ntype MaybeAccessor = Accessor | T;\nconst isFunction = (value: unknown): value is (...args: unknown[]) => unknown =>\n typeof value === 'function';\nconst unwrap = (maybeValue: MaybeAccessor): T =>\n isFunction(maybeValue) ? maybeValue() : maybeValue;\n\nexport const createScheduler = ({\n loop,\n callback,\n cancel,\n schedule,\n}: {\n loop?: MaybeAccessor;\n callback: MaybeAccessor>;\n cancel: FN<[T]>;\n schedule: (callback: FN<[U]>) => T;\n}): (() => void) => {\n let tickId: T;\n const work = (): void => {\n if (unwrap(loop)) tick();\n unwrap(callback);\n };\n\n const tick = (): void => {\n tickId = schedule(work);\n };\n\n const dispose = (): void => {\n cancel(tickId);\n };\n\n tick();\n return dispose;\n};\n\nexport const createAnimationLoop = (callback: FrameRequestCallback) =>\n createScheduler({\n callback,\n loop: true,\n cancel: cancelAnimationFrame,\n schedule: requestAnimationFrame,\n });\n"},{"name":"styles","type":"css","content":".clock {\n display: flex;\n justify-content: center;\n align-items: center;\n flex-wrap: wrap;\n height: 100vh;\n}\n\n.subsecond {\n color: silver;\n}\n\n.hour,\n.minute {\n color: black;\n}\n\n.second {\n color: tomato;\n}\n"}]} \ No newline at end of file diff --git a/langs/en/examples/context.json b/langs/en/examples/context.json new file mode 100644 index 00000000..09f35862 --- /dev/null +++ b/langs/en/examples/context.json @@ -0,0 +1 @@ +{"id":"context","name":"Basic/Context","description":"A simple color picker using Context","files":[{"name":"main","type":"tsx","content":"import { render } from \"solid-js/web\";\nimport { ThemeProvider, useTheme } from \"./theme\";\n\nfunction App() {\n const [theme, { changeColor }] = useTheme();\n\n return (\n <>\n \n {theme.title}\n \n changeColor(e.currentTarget.value)}\n />\n \n \n );\n}\n\nrender(\n () => (\n \n \n \n ),\n document.getElementById(\"app\")!\n);\n"},{"name":"theme","type":"tsx","content":"import { createContext, useContext, ParentComponent } from \"solid-js\";\r\nimport { createStore } from \"solid-js/store\";\r\n\r\nexport type ThemeContextState = {\r\n readonly color: string;\r\n readonly title: string;\r\n};\r\nexport type ThemeContextValue = [\r\n state: ThemeContextState,\r\n actions: {\r\n changeColor: (color: string) => void;\r\n changeTitle: (title: string) => void;\r\n }\r\n];\r\n\r\nconst defaultState = {\r\n color: \"#66e6ac\",\r\n title: \"Fallback Title\",\r\n};\r\n\r\nconst ThemeContext = createContext([\r\n defaultState,\r\n {\r\n changeColor: () => undefined,\r\n changeTitle: () => undefined,\r\n },\r\n]);\r\n\r\nexport const ThemeProvider: ParentComponent<{\r\n color?: string;\r\n title?: string;\r\n}> = (props) => {\r\n const [state, setState] = createStore({\r\n color: props.color ?? defaultState.color,\r\n title: props.title ?? defaultState.title,\r\n });\r\n\r\n const changeColor = (color: string) => setState(\"color\", color);\r\n const changeTitle = (title: string) => setState(\"title\", title);\r\n\r\n return (\r\n \r\n {props.children}\r\n \r\n );\r\n};\r\n\r\nexport const useTheme = () => useContext(ThemeContext);\r\n"}]} \ No newline at end of file diff --git a/langs/en/examples/counter.json b/langs/en/examples/counter.json new file mode 100644 index 00000000..ca000f81 --- /dev/null +++ b/langs/en/examples/counter.json @@ -0,0 +1 @@ +{"id":"counter","name":"Basic/Counter","description":"A simple standard counter example","files":[{"name":"main","type":"jsx","content":"import { createSignal, onCleanup } from \"solid-js\";\nimport { render } from \"solid-js/web\";\n\nconst CountingComponent = () => {\n\tconst [count, setCount] = createSignal(0);\n\tconst interval = setInterval(\n\t\t() => setCount(c => c + 1),\n\t\t1000\n\t);\n\tonCleanup(() => clearInterval(interval));\n\treturn
Count value is {count()}
;\n};\n\nrender(() => , document.getElementById(\"app\"));"}]} \ No newline at end of file diff --git a/langs/en/examples/counterstore.json b/langs/en/examples/counterstore.json new file mode 100644 index 00000000..7a502f26 --- /dev/null +++ b/langs/en/examples/counterstore.json @@ -0,0 +1 @@ +{"id":"counterstore","files":[{"name":"main","type":"jsx","content":"import { render } from \"solid-js/web\";\nimport { CounterProvider, useCounter } from \"CounterStore.tsx\";\n\nconst MiddleComponent = () => ;\n\nconst NestedComponent = () => {\n const [count, { increment, decrement }] = useCounter();\n return (\n <>\n

{count()}

\n \n \n \n );\n};\n\nconst App = () => (\n \n \n \n);\n\nrender(App, document.getElementById(\"app\"));"},{"name":"CounterStore","type":"jsx","content":"import { createSignal, createContext, useContext, Component } from \"solid-js\";\n\ntype CounterStore = [\n () => number,\n { increment?: () => void; decrement?: () => void }\n];\n\nconst CounterContext = createContext([() => 0, {}]);\n\nexport const CounterProvider: Component<{ count: number }> = props => {\n const [count, setCount] = createSignal(props.count || 0),\n store: CounterStore = [\n count,\n {\n increment() {\n setCount(c => c + 1);\n },\n decrement() {\n setCount(c => c - 1);\n }\n }\n ];\n\n return (\n \n {props.children}\n \n );\n};\n\nexport function useCounter() {\n return useContext(CounterContext);\n}"}]} \ No newline at end of file diff --git a/langs/en/examples/cssanimations.json b/langs/en/examples/cssanimations.json new file mode 100644 index 00000000..7514ccc3 --- /dev/null +++ b/langs/en/examples/cssanimations.json @@ -0,0 +1 @@ +{"id":"cssanimations","name":"Basic/CSS Animations","description":"Using Solid Transition Group","files":[{"name":"main","type":"jsx","content":"import { createSignal, For, Match, Switch } from \"solid-js\";\nimport { render } from \"solid-js/web\";\nimport { Transition, TransitionGroup } from \"solid-transition-group\";\nimport \"./styles.css\"\n\nfunction shuffle(array) {\n return array.sort(() => Math.random() - 0.5);\n}\nlet nextId = 10;\n\nconst App = () => {\n const [show, toggleShow] = createSignal(true),\n [select, setSelect] = createSignal(0),\n [numList, setNumList] = createSignal([1, 2, 3, 4, 5, 6, 7, 8, 9]),\n randomIndex = () => Math.floor(Math.random() * numList().length);\n\n return (\n <>\n \n
\n Transition:\n \n {show() && (\n
\n Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris\n facilisis enim libero, at lacinia diam fermentum id. Pellentesque\n habitant morbi tristique senectus et netus.\n
\n )}\n
\n
\n Animation:\n \n {show() && (\n
\n Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris\n facilisis enim libero, at lacinia diam fermentum id. Pellentesque\n habitant morbi tristique senectus et netus.\n
\n )}\n
\n
\n Custom JS:\n {\n const a = el.animate([{ opacity: 0 }, { opacity: 1 }], {\n duration: 600\n });\n a.finished.then(done);\n }}\n onExit={(el, done) => {\n const a = el.animate([{ opacity: 1 }, { opacity: 0 }], {\n duration: 600\n });\n a.finished.then(done);\n }}\n >\n {show() && (\n
\n Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris\n facilisis enim libero, at lacinia diam fermentum id. Pellentesque\n habitant morbi tristique senectus et netus.\n
\n )}\n \n
\n Switch OutIn\n
\n \n \n \n \n

The First

\n
\n \n

The Second

\n
\n \n

The Third

\n
\n
\n
\n Group\n
\n {\n const list = numList(),\n idx = randomIndex();\n setNumList([...list.slice(0, idx), nextId++, ...list.slice(idx)]);\n }}\n >\n Add\n \n {\n const list = numList(),\n idx = randomIndex();\n setNumList([...list.slice(0, idx), ...list.slice(idx + 1)]);\n }}\n >\n Remove\n \n {\n const randomList = shuffle(numList().slice());\n setNumList(randomList);\n }}\n >\n Shuffle\n \n
\n \n {(r) => {r}}\n \n \n );\n};\n\nrender(App, document.getElementById(\"app\"));"},{"name":"styles","type":"css","content":".container {\n position: relative;\n}\n\n.fade-enter-active,\n.fade-exit-active {\n transition: opacity 0.5s;\n}\n.fade-enter,\n.fade-exit-to {\n opacity: 0;\n}\n\n.slide-fade-enter-active {\n transition: all 0.3s ease;\n}\n.slide-fade-exit-active {\n transition: all 0.8s cubic-bezier(1, 0.5, 0.8, 1);\n}\n.slide-fade-enter,\n.slide-fade-exit-to {\n transform: translateX(10px);\n opacity: 0;\n}\n\n.bounce-enter-active {\n animation: bounce-in 0.5s;\n}\n.bounce-exit-active {\n animation: bounce-in 0.5s reverse;\n}\n@keyframes bounce-in {\n 0% {\n transform: scale(0);\n }\n 50% {\n transform: scale(1.5);\n }\n 100% {\n transform: scale(1);\n }\n}\n\n.list-item {\n transition: all 0.5s;\n display: inline-block;\n margin-right: 10px;\n}\n\n.list-item-enter,\n.list-item-exit-to {\n opacity: 0;\n transform: translateY(30px);\n}\n.list-item-exit-active {\n position: absolute;\n}\n"}]} \ No newline at end of file diff --git a/langs/en/examples/ethasketch.json b/langs/en/examples/ethasketch.json new file mode 100644 index 00000000..f9153d5f --- /dev/null +++ b/langs/en/examples/ethasketch.json @@ -0,0 +1 @@ +{"id":"ethasketch","name":"Complex/Etch A Sketch","description":"Uses Index and createMemo to create a grid graphic","files":[{"name":"main","type":"tsx","content":"// Project idea from https://www.theodinproject.com/paths/foundations/courses/foundations/lessons/etch-a-sketch-project\nimport { render } from \"solid-js/web\";\nimport { createSignal, createMemo, Index } from \"solid-js\";\n\nimport \"./styles.css\";\n\nconst maxGridPixelWidth = 500;\n\nfunction randomHexColorString(): string {\n return \"#\" + Math.floor(Math.random() * 16777215).toString(16);\n}\n\nfunction clampGridSideLength(newSideLength: number): number {\n return Math.min(Math.max(newSideLength, 0), 100);\n}\n\nfunction EtchASketch() {\n const [gridSideLength, setGridSideLength] = createSignal(10);\n const gridTemplateString = createMemo(\n () =>\n `repeat(${gridSideLength()}, ${maxGridPixelWidth / gridSideLength()}px)`\n );\n\n return (\n <>\n
\n \n \n setGridSideLength(\n clampGridSideLength(e.currentTarget.valueAsNumber)\n )\n }\n />\n
\n \n \n {() => (\n {\n const eventEl = event.currentTarget;\n\n eventEl.style.backgroundColor = randomHexColorString();\n\n setTimeout(() => {\n eventEl.style.backgroundColor = \"initial\";\n }, 500);\n }}\n >\n )}\n \n \n \n );\n}\n\nrender(() => , document.getElementById(\"app\"));\n"},{"name":"styles","type":"css","content":".cell {\n outline: 1px solid #1f1f1f;\n}\n\n.dark .cell {\n outline: 1px solid #efefef;\n}\n"}]} \ No newline at end of file diff --git a/langs/en/examples/forms.json b/langs/en/examples/forms.json new file mode 100644 index 00000000..6a3eb1d7 --- /dev/null +++ b/langs/en/examples/forms.json @@ -0,0 +1 @@ +{"id":"forms","name":"Basic/Form Validation","description":"HTML 5 validators with custom async validation","files":[{"name":"main","type":"jsx","content":"// @ts-nocheck\nimport { render } from \"solid-js/web\";\nimport { createStore } from \"solid-js/store\";\nimport { useForm } from \"./validation\";\nimport \"./styles.css\";\n\nconst EMAILS = [\"johnsmith@outlook.com\", \"mary@gmail.com\", \"djacobs@move.org\"];\n\nfunction fetchUserName(name) {\n return new Promise((resolve) => {\n setTimeout(() => resolve(EMAILS.indexOf(name) > -1), 200);\n });\n}\n\nconst ErrorMessage = (props) => {props.error};\n\nconst App = () => {\n const { validate, formSubmit, errors } = useForm({\n errorClass: \"error-input\"\n });\n const [fields, setFields] = createStore();\n const fn = (form) => {\n // form.submit()\n console.log(\"Done\");\n };\n const userNameExists = async ({ value }) => {\n const exists = await fetchUserName(value);\n return exists && `${value} is already being used`;\n };\n const matchesPassword = ({ value }) =>\n value === fields.password ? false : \"Passwords must Match\";\n\n return (\n
\n

Sign Up

\n
\n \n {errors.email && }\n
\n
\n setFields(\"password\", e.target.value)}\n use:validate\n />\n {errors.password && }\n
\n
\n \n {errors.confirmpassword && (\n \n )}\n
\n\n \n
\n );\n};\n\nrender(App, document.getElementById(\"app\"));\n"},{"name":"validation","type":"jsx","content":"import { createStore } from \"solid-js/store\";\n\nfunction checkValid({ element, validators = [] }, setErrors, errorClass) {\n return async () => {\n element.setCustomValidity(\"\");\n element.checkValidity();\n let message = element.validationMessage;\n if (!message) {\n for (const validator of validators) {\n const text = await validator(element);\n if (text) {\n element.setCustomValidity(text);\n break;\n }\n }\n message = element.validationMessage;\n }\n if (message) {\n errorClass && element.classList.toggle(errorClass, true);\n setErrors({ [element.name]: message });\n }\n };\n}\n\nexport function useForm({ errorClass }) {\n const [errors, setErrors] = createStore({}),\n fields = {};\n\n const validate = (ref, accessor) => {\n const validators = accessor() || [];\n let config;\n fields[ref.name] = config = { element: ref, validators };\n ref.onblur = checkValid(config, setErrors, errorClass);\n ref.oninput = () => {\n if (!errors[ref.name]) return;\n setErrors({ [ref.name]: undefined });\n errorClass && ref.classList.toggle(errorClass, false);\n };\n };\n\n const formSubmit = (ref, accessor) => {\n const callback = accessor() || (() => {});\n ref.setAttribute(\"novalidate\", \"\");\n ref.onsubmit = async (e) => {\n e.preventDefault();\n let errored = false;\n\n for (const k in fields) {\n const field = fields[k];\n await checkValid(field, setErrors, errorClass)();\n if (!errored && field.element.validationMessage) {\n field.element.focus();\n errored = true;\n }\n }\n !errored && callback(ref);\n };\n };\n\n return { validate, formSubmit, errors };\n}\n"},{"name":"styles","type":"css","content":"input {\n display: inline-block;\n padding: 4px;\n margin-top: 10px;\n margin-bottom: 10px;\n}\n.error-message {\n color: red;\n padding: 8px;\n}\n.error-input {\n box-shadow: 0px 0px 2px 1px red;\n}"}]} \ No newline at end of file diff --git a/langs/en/examples/routing.json b/langs/en/examples/routing.json new file mode 100644 index 00000000..bee3848e --- /dev/null +++ b/langs/en/examples/routing.json @@ -0,0 +1 @@ +{"id":"routing","files":[{"name":"main","type":"jsx","content":"import { createSignal, onCleanup, Component } from \"solid-js\";\nimport { render, Switch, Match } from \"solid-js/web\";\n\nfunction createRouteHandler() {\n const [location, setLocation] = createSignal(\n window.location.hash.slice(1) || \"home\"\n ),\n locationHandler = () => setLocation(window.location.hash.slice(1));\n window.addEventListener(\"hashchange\", locationHandler);\n onCleanup(() => window.removeEventListener(\"hashchange\", locationHandler));\n return (match: string) => match === location();\n}\n\nconst Home: Component = () => (\n <>\n

Welcome to this Simple Routing Example

\n

Click the links in the Navigation above to load different routes.

\n \n);\n\nconst Profile: Component = () => (\n <>\n

Your Profile

\n

This section could be about you.

\n \n);\n\nconst Settings: Component = () => (\n <>\n

Settings

\n

All that configuration you never really ever want to look at.

\n \n);\n\nconst App = () => {\n const matches = createRouteHandler();\n return (\n <>\n \n \n \n \n \n \n \n \n \n \n \n \n \n );\n};\n\nrender(App, document.getElementById(\"app\"));"}]} \ No newline at end of file diff --git a/langs/en/examples/scoreboard.json b/langs/en/examples/scoreboard.json new file mode 100644 index 00000000..4eacc62e --- /dev/null +++ b/langs/en/examples/scoreboard.json @@ -0,0 +1 @@ +{"id":"scoreboard","name":"Complex/Scoreboard","description":"Make use of hooks to do simple transitions","files":[{"name":"main","type":"jsx","content":"import {\n\tcreateMemo,\n\tcreateSignal,\n\tcreateComputed,\n\tonCleanup,\n\tFor\n} from \"solid-js\";\nimport { createStore } from \"solid-js/store\";\nimport { render } from \"solid-js/web\";\n\nconst App = () => {\n\tlet newName, newScore;\n\tconst [state, setState] = createStore({\n\t\t\tplayers: [\n\t\t\t\t{ name: \"Mark\", score: 3 },\n\t\t\t\t{ name: \"Troy\", score: 2 },\n\t\t\t\t{ name: \"Jenny\", score: 1 },\n\t\t\t\t{ name: \"David\", score: 8 }\n\t\t\t]\n\t\t}),\n\t\tlastPos = new WeakMap(),\n\t\tcurPos = new WeakMap(),\n\t\tgetSorted = createMemo((list = []) => {\n\t\t\tlist.forEach((p, i) => lastPos.set(p, i));\n\t\t\tconst newList = state.players.slice().sort((a, b) => {\n\t\t\t\tif (b.score === a.score) return a.name.localeCompare(b.name); // stabalize the sort\n\t\t\t\treturn b.score - a.score;\n\t\t\t});\n\t\t\tlet updated = newList.length !== list.length;\n\t\t\tnewList.forEach(\n\t\t\t\t(p, i) => lastPos.get(p) !== i && (updated = true) && curPos.set(p, i)\n\t\t\t);\n\t\t\treturn updated ? newList : list;\n\t\t}),\n\t\thandleAddClick = () => {\n\t\t\tconst name = newName.value,\n\t\t\t\tscore = +newScore.value;\n\t\t\tif (!name.length || isNaN(score)) return;\n\t\t\tsetState(\"players\", (p) => [...p, { name: name, score: score }]);\n\t\t\tnewName.value = newScore.value = \"\";\n\t\t},\n\t\thandleDeleteClick = (player) => {\n\t\t\tconst idx = state.players.indexOf(player);\n\t\t\tsetState(\"players\", (p) => [...p.slice(0, idx), ...p.slice(idx + 1)]);\n\t\t},\n\t\thandleScoreChange = (player, { target }) => {\n\t\t\tconst score = +target.value;\n\t\t\tconst idx = state.players.indexOf(player);\n\t\t\tif (isNaN(+score) || idx < 0) return;\n\t\t\tsetState(\"players\", idx, \"score\", score);\n\t\t},\n\t\tcreateStyles = (player) => {\n\t\t\tconst [style, setStyle] = createSignal();\n\t\t\tcreateComputed(() => {\n\t\t\t\tgetSorted();\n\t\t\t\tconst offset = lastPos.get(player) * 18 - curPos.get(player) * 18,\n\t\t\t\t\tt = setTimeout(() =>\n\t\t\t\t\t\tsetStyle({ transition: \"250ms\", transform: null })\n\t\t\t\t\t);\n\t\t\t\tsetStyle({\n\t\t\t\t\ttransform: `translateY(${offset}px)`,\n\t\t\t\t\ttransition: null\n\t\t\t\t});\n\t\t\t\tonCleanup(() => clearTimeout(t));\n\t\t\t});\n\t\t\treturn style;\n\t\t};\n\n\treturn (\n\t\t
\n\t\t\t
\n\t\t\t\t\n\t\t\t\t\t{(player) => {\n\t\t\t\t\t\tconst getStyles = createStyles(player),\n\t\t\t\t\t\t\t{ name } = player;\n\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t
{name}
\n\t\t\t\t\t\t\t\t
{player.score}
\n\t\t\t\t\t\t\t
\n\t\t\t\t\t\t);\n\t\t\t\t\t}}\n\t\t\t\t
\n\t\t\t
\n\t\t\t
\n\t\t\t\t\n\t\t\t\t\t{(player) => {\n\t\t\t\t\t\tconst { name, score } = player;\n\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t{name}\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t
\n\t\t\t\t\t\t);\n\t\t\t\t\t}}\n\t\t\t\t
\n\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t
\n\t\t\t
\n\t\t
\n\t);\n};\n\nrender(App, document.getElementById(\"app\"));"}]} \ No newline at end of file diff --git a/langs/en/examples/simpletodos.json b/langs/en/examples/simpletodos.json new file mode 100644 index 00000000..2820b53f --- /dev/null +++ b/langs/en/examples/simpletodos.json @@ -0,0 +1 @@ +{"id":"simpletodos","name":"Complex/Simple Todos Template Literals","description":"Simple Todos using Lit DOM Expressions","files":[{"name":"main","type":"jsx","content":"import { createEffect, For } from \"solid-js\";\nimport { createStore } from \"solid-js/store\";\nimport { render } from \"solid-js/web\";\nimport html from \"solid-js/html\";\n\nfunction createLocalStore(initState) {\n const [state, setState] = createStore(initState);\n if (localStorage.todos) setState(JSON.parse(localStorage.todos));\n createEffect(() => (localStorage.todos = JSON.stringify(state)));\n return [state, setState];\n}\n\nconst App = () => {\n const [state, setState] = createLocalStore({\n todos: [],\n newTitle: \"\",\n idCounter: 0\n });\n\n return html`\n
\n

Simple Todos Example

\n state.newTitle}\n oninput=${(e) => setState({ newTitle: e.target.value })}\n />\n \n setState({\n idCounter: state.idCounter + 1,\n todos: [\n ...state.todos,\n {\n id: state.idCounter,\n title: state.newTitle,\n done: false\n }\n ],\n newTitle: \"\"\n })}\n >\n +\n \n <${For} each=${() => state.todos}\n >${(todo) =>\n html`\n
\n {\n const idx = state.todos.findIndex((t) => t.id === todo.id);\n setState(\"todos\", idx, { done: e.target.checked });\n }}\n />\n {\n const idx = state.todos.findIndex((t) => t.id === todo.id);\n setState(\"todos\", idx, { title: e.target.value });\n }}\n />\n \n setState(\"todos\", (t) => t.filter((t) => t.id !== todo.id))}\n >\n x\n \n
\n `}\n \n
\n `;\n};\n\nrender(App, document.getElementById(\"app\"));"}]} \ No newline at end of file diff --git a/langs/en/examples/simpletodoshyperscript.json b/langs/en/examples/simpletodoshyperscript.json new file mode 100644 index 00000000..9464520a --- /dev/null +++ b/langs/en/examples/simpletodoshyperscript.json @@ -0,0 +1 @@ +{"id":"simpletodoshyperscript","name":"Complex/Simple Todos Hyperscript","description":"Simple Todos using Hyper DOM Expressions","files":[{"name":"main","type":"jsx","content":"import { createEffect, For } from \"solid-js\";\nimport { createStore } from \"solid-js/store\";\nimport { render } from \"solid-js/web\";\nimport h from \"solid-js/h\";\n\nfunction createLocalStore(initState) {\n const [state, setState] = createStore(initState);\n if (localStorage.todos) setState(JSON.parse(localStorage.todos));\n createEffect(() => (localStorage.todos = JSON.stringify(state)));\n return [state, setState];\n}\n\nconst App = () => {\n const [state, setState] = createLocalStore({\n todos: [],\n newTitle: \"\",\n idCounter: 0\n });\n return [\n h(\"h3\", \"Simple Todos Example\"),\n h(\"input\", {\n type: \"text\",\n placeholder: \"enter todo and click +\",\n value: () => state.newTitle,\n onInput: (e) => setState(\"newTitle\", e.target.value)\n }),\n h(\n \"button\",\n {\n onClick: () =>\n setState((s) => ({\n idCounter: s.idCounter + 1,\n todos: [\n ...s.todos,\n {\n id: state.idCounter,\n title: state.newTitle,\n done: false\n }\n ],\n newTitle: \"\"\n }))\n },\n \"+\"\n ),\n h(For, { each: () => state.todos }, (todo) =>\n h(\n \"div\",\n h(\"input\", {\n type: \"checkbox\",\n checked: todo.done,\n onChange: (e) =>\n setState(\n \"todos\",\n state.todos.findIndex((t) => t.id === todo.id),\n {\n done: e.target.checked\n }\n )\n }),\n h(\"input\", {\n type: \"text\",\n value: todo.title,\n onChange: (e) =>\n setState(\n \"todos\",\n state.todos.findIndex((t) => t.id === todo.id),\n {\n title: e.target.value\n }\n )\n }),\n h(\n \"button\",\n {\n onClick: () =>\n setState(\"todos\", (t) => t.filter((t) => t.id !== todo.id))\n },\n \"x\"\n )\n )\n )\n ];\n};\n\nrender(App, document.getElementById(\"app\"));"}]} \ No newline at end of file diff --git a/langs/en/examples/styledjsx.json b/langs/en/examples/styledjsx.json new file mode 100644 index 00000000..9b8f6f99 --- /dev/null +++ b/langs/en/examples/styledjsx.json @@ -0,0 +1 @@ +{"id":"styledjsx","files":[{"name":"main","type":"jsx","content":"import { createSignal } from \"solid-js\";\nimport { render } from \"solid-js/web\";\n\nfunction Button() {\n const [isLoggedIn, login] = createSignal(false);\n return (\n <>\n \n \n \n );\n}\n\nrender(\n () => (\n <>\n \n \n \n {(todo, i) => (\n
\n setTodos(i(), \"done\", e.currentTarget.checked)}\n />\n setTodos(i(), \"title\", e.currentTarget.value)}\n />\n \n
\n )}\n
\n \n );\n};\n\nrender(App, document.getElementById(\"app\")!);\n"},{"name":"utils","type":"tsx","content":"import { createEffect } from \"solid-js\";\r\nimport { createStore, SetStoreFunction, Store } from \"solid-js/store\";\r\n\r\nexport function createLocalStore(\r\n name: string,\r\n init: T\r\n): [Store, SetStoreFunction] {\r\n const localState = localStorage.getItem(name);\r\n const [state, setState] = createStore(\r\n localState ? JSON.parse(localState) : init\r\n );\r\n createEffect(() => localStorage.setItem(name, JSON.stringify(state)));\r\n return [state, setState];\r\n}\r\n\r\nexport function removeIndex(array: readonly T[], index: number): T[] {\r\n return [...array.slice(0, index), ...array.slice(index + 1)];\r\n}\r\n"}]} \ No newline at end of file diff --git a/package.json b/package.json index 1d866159..ad7b62a9 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "@rollup/plugin-typescript": "^8.3.3", "@types/node": "^18.0.3", "gitlog": "^4.0.4", + "glob": "^8.0.3", "jiti": "^1.14.0", "markdown-magic": "^2.6.0", "patch-package": "^6.4.7", diff --git a/rollup-plugins/generate-json-folders.js b/rollup-plugins/generate-json-folders.js new file mode 100644 index 00000000..a30735ac --- /dev/null +++ b/rollup-plugins/generate-json-folders.js @@ -0,0 +1,88 @@ +import glob from 'glob'; +import { dirname, parse, basename } from 'path'; +import { readFileSync, writeFileSync } from 'fs'; + +const pluginName = 'generate-json-folders'; + +const directoryPattern = '**/examples-src/$descriptor.json'; +const examplesPattern = '**/examples-src/*/$descriptor.json'; + +function getGlobPaths(...patterns) { + return glob.sync( + patterns.length > 1 ? `{${patterns.join(',')}}` : patterns[0], + {absolute: true} + ); +} + +function getDescriptor(descriptorPath) { + let descriptor = null; + try { + descriptor = JSON.parse(readFileSync(descriptorPath, 'utf8')); + } catch (e) { + console.warn(`[${pluginName}] skipping ${descriptorPath} because ${e}`); + return descriptor; + } + return descriptor; +} + +function getGeneratedPath(path) { + return path.replace('/examples-src/', '/examples/'); +} + +function getFiles(folderPath, filenames) { + return (filenames || []).map((filename) => { + const filepath = `${folderPath}/${filename}`; + const {name, ext} = parse(filename); + return { + name, + type: ext.slice(1), + content: readFileSync(filepath, 'utf8') + }; + }); +} + +function createExamplesDirectory(cachedExamples) { + const descriptorPaths = glob.sync(directoryPattern, {absolute: true}); + descriptorPaths.forEach((descriptorPath) => { + const descriptor = getDescriptor(descriptorPath); + if (!descriptor) return; // skip + const directory = descriptor.map((id) => { + const exampleSrcPath = `${dirname(descriptorPath)}/${id}`; + const example = cachedExamples[exampleSrcPath]; + return example; + }); + const directoryPath = getGeneratedPath(descriptorPath); + writeFileSync(directoryPath, JSON.stringify(directory)); + }); +} + +export default function pack(pluginOptions) { + if (pluginOptions) { + console.warn(`[${pluginName}] no plugin options are supported`); + } + return { + name: pluginName, + buildStart() { + const descriptorPaths = getGlobPaths(examplesPattern); + const cachedExamples = {}; + descriptorPaths.forEach((descriptorPath) => { + const descriptor = getDescriptor(descriptorPath); + if (!descriptor) return; // skip + + const exampleSrcPath = dirname(descriptorPath); + const files = getFiles(exampleSrcPath, descriptor.files); + const example = { + id: basename(exampleSrcPath), + name: descriptor.name, + description: descriptor.description, + files + }; + const examplePath = getGeneratedPath(exampleSrcPath); + writeFileSync(`${examplePath}.json`, JSON.stringify(example)); + delete example.files; + cachedExamples[exampleSrcPath] = example; + }); + createExamplesDirectory(cachedExamples); + } + }; +} diff --git a/rollup.config.mjs b/rollup.config.mjs index 2e4f1876..00d1d5dd 100644 --- a/rollup.config.mjs +++ b/rollup.config.mjs @@ -7,6 +7,7 @@ import rehypeHighlight from "rehype-highlight"; import rehypeSlug from "rehype-slug"; import rehypeAutolinkHeadings from "rehype-autolink-headings"; import { remarkMdxToc } from "remark-mdx-toc"; +import jsonFolders from "./rollup-plugins/generate-json-folders.js"; export default { input: "src/index.ts", @@ -30,6 +31,7 @@ export default { ], }), typescript(), + jsonFolders(), json(), dynamicImportVars.default(), ], diff --git a/scripts/import-examples.js b/scripts/import-examples.js new file mode 100644 index 00000000..e9c6e040 --- /dev/null +++ b/scripts/import-examples.js @@ -0,0 +1,58 @@ +// Use this script to refresh examples from solid-site in case they were changed +// before merging the "split-js-files" branch in both solid-docs and solid-site. +// +// input +// 1: source folder (eg: "/../solid-site/public/examples") +// 2: dest folder (eg: "/langs/en/examples-src") +// output +// - as many subfolders of the dest folder as files in the glob, each named after the basename of the file +// - as many files in each subfolder as there are packed into each file in the glob + +import { resolve, basename, dirname } from 'path'; +import { readdirSync, readFileSync, writeFileSync, mkdirSync } from 'fs'; + +let [sourceFolder, destFolder] = process.argv.slice(2); +if (!(sourceFolder && destFolder)) { + throw new Error('Expected both a source and a dest folder'); +} + +sourceFolder = resolve('./', sourceFolder); +destFolder = resolve('./', destFolder); + + +const sourceFiles = readdirSync(sourceFolder); +sourceFiles.forEach((sourceFilename) => { + const sourcePath = `${sourceFolder}/${sourceFilename}`; + if (!/\.json$/.test(sourcePath)) { + console.log(`skipping ${sourcePath} because not '.json'`); + return; + } + const json = readFileSync(sourcePath, 'utf8'); + let data; + try { + console.log(`parsing ${sourcePath}`); + data = JSON.parse(json); + } catch (e) { + console.log(`skipping ${sourcePath} because ${e}`); + return; + } + if (!data.files?.length) { + console.log('no files to extract'); + return; + } + console.log(`extracting ${data.files.length} files`); + const destPathParent = `${destFolder}/${basename(sourceFilename, '.json')}`; + mkdirSync(destPathParent, {recursive: true}); + const filesOrder = []; + data.files.forEach((file) => { + const destFilename = `${file.name}.${file.type || 'jsx'}`; + filesOrder.push(destFilename); + const destPath = `${destPathParent}/${destFilename}`; + let content = file.content; + if (Array.isArray(content)) content = content.join('\n'); + writeFileSync(destPath, content); + console.log(`- extracted ${destPath}`); + }); + // Commented out the following write because not needed to only update examples + // writeFileSync(`${destPathParent}/.json-files`, JSON.stringify(filesOrder)); +}); diff --git a/src/index.ts b/src/index.ts index 16921e47..caae5a4a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,6 @@ -import { DocFile, LessonFile, LessonLookup, ResourceMetadata } from "./types"; +import { DocFile, LessonFile, LessonLookup, ResourceMetadata, SourceFile, Example } from "./types"; -export { DocFile, LessonFile, LessonLookup, ResourceMetadata }; +export { DocFile, LessonFile, LessonLookup, ResourceMetadata, SourceFile, Example }; function noThrow(x: Promise): Promise { return x.catch(() => undefined); @@ -50,3 +50,18 @@ export async function getTutorialDirectory( ); return directory?.default; } + +export async function getExample( + lang: string, + id: string +): Promise { + const example = await noThrow(import(`../langs/${lang}/examples/${id}.json`)); + return example?.default; +} + +export async function getExamplesDirectory( + lang: string +): Promise { + const directory = await noThrow(import(`../langs/${lang}/examples/$descriptor.json`)); + return directory?.default; +} diff --git a/src/types.ts b/src/types.ts index 9f434bf5..f21fc568 100644 --- a/src/types.ts +++ b/src/types.ts @@ -25,3 +25,16 @@ export interface LessonFile { solved?: any; markdown?: string; } + +export interface SourceFile { + name: string; + type: string; + content: string; +} + +export interface Example { + id: string; + name: string; + description: string; + files?: SourceFile[]; +} diff --git a/yarn.lock b/yarn.lock index 0116b548..bd021051 100644 --- a/yarn.lock +++ b/yarn.lock @@ -321,6 +321,13 @@ brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" +brace-expansion@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" + integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== + dependencies: + balanced-match "^1.0.0" + braces@^3.0.1, braces@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" @@ -778,6 +785,17 @@ glob@^7.1.3: once "^1.3.0" path-is-absolute "^1.0.0" +glob@^8.0.3: + version "8.0.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-8.0.3.tgz#415c6eb2deed9e502c68fa44a272e6da6eeca42e" + integrity sha512-ull455NHSHI/Y1FqGaaYFaLGkNMMJbavMrEGFXG/PGrg6y7sutWHUHrz6gy6WEBH6akM1M414dWKCNs+IhKdiQ== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^5.0.1" + once "^1.3.0" + globby@^10.0.2: version "10.0.2" resolved "https://registry.yarnpkg.com/globby/-/globby-10.0.2.tgz#277593e745acaa4646c3ab411289ec47a0392543" @@ -1757,6 +1775,13 @@ minimatch@^3.0.4: dependencies: brace-expansion "^1.1.7" +minimatch@^5.0.1: + version "5.1.0" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.0.tgz#1717b464f4971b144f6aabe8f2d0b8e4511e09c7" + integrity sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg== + dependencies: + brace-expansion "^2.0.1" + minimist@^1.2.0: version "1.2.5" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"