From be4d4e316865cf2af062a4d847f22958a2cea2a4 Mon Sep 17 00:00:00 2001 From: Kuba Orlik Date: Wed, 20 Mar 2024 23:17:24 +0100 Subject: [PATCH] Add support for images --- .gitignore | 5 ++ src/back/image-router.ts | 12 +++ .../jdd-components/image-demo/image-demo.css | 6 ++ .../image-demo/image-demo.jdd.tsx | 63 ++++++++++++++ .../component-input-enum.tsx | 35 ++++++++ .../component-input-image.tsx | 54 ++++++++++++ .../component-input-list.tsx | 58 +++++++++++++ .../component-input-structured.tsx | 46 ++++++++++ .../component-preview/component-input.tsx | 85 +++++++++++++++++++ .../component-preview-actions.ts | 61 +++++++++++++ .../component-preview/print-arg-path.ts | 3 + src/back/routes/jdd-context.ts | 16 ++++ src/front/controllers/input-image-preview.ts | 21 +++++ uploaded-images/keepme | 0 14 files changed, 465 insertions(+) create mode 100644 src/back/image-router.ts create mode 100644 src/back/jdd-components/image-demo/image-demo.css create mode 100644 src/back/jdd-components/image-demo/image-demo.jdd.tsx create mode 100644 src/back/routes/component-preview/component-input-enum.tsx create mode 100644 src/back/routes/component-preview/component-input-image.tsx create mode 100644 src/back/routes/component-preview/component-input-list.tsx create mode 100644 src/back/routes/component-preview/component-input-structured.tsx create mode 100644 src/back/routes/component-preview/component-input.tsx create mode 100644 src/back/routes/component-preview/component-preview-actions.ts create mode 100644 src/back/routes/component-preview/print-arg-path.ts create mode 100644 src/back/routes/jdd-context.ts create mode 100644 src/front/controllers/input-image-preview.ts create mode 100644 uploaded-images/keepme diff --git a/.gitignore b/.gitignore index f1933f8..816b845 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,8 @@ public/dist .env .npm_cache/ .mozilla/ +cache/* +!cache/images/.keepme +!cache/smartcrop/.keepme +uploaded-images/* +!uploaded-images/keepme \ No newline at end of file diff --git a/src/back/image-router.ts b/src/back/image-router.ts new file mode 100644 index 0000000..0097f42 --- /dev/null +++ b/src/back/image-router.ts @@ -0,0 +1,12 @@ +import { KoaResponsiveImageRouter } from "koa-responsive-image-router"; +import { IMAGE_CACHE_FS_DIR, SMARTCROP_CACHE_FS_DIR } from "./config.js"; + +export const RESPONSIVE_IMAGES_URL_PATH = "/images"; + +export const imageRouter = new KoaResponsiveImageRouter({ + staticPath: RESPONSIVE_IMAGES_URL_PATH, + thumbnailSize: 20, + cacheManagerResolutionThreshold: 50, + imageStoragePath: IMAGE_CACHE_FS_DIR, + smartCropStoragePath: SMARTCROP_CACHE_FS_DIR, +}); diff --git a/src/back/jdd-components/image-demo/image-demo.css b/src/back/jdd-components/image-demo/image-demo.css new file mode 100644 index 0000000..246fbea --- /dev/null +++ b/src/back/jdd-components/image-demo/image-demo.css @@ -0,0 +1,6 @@ +.image-demo { + .image-grid > * { + width: 200px; + height: 200px; + } +} diff --git a/src/back/jdd-components/image-demo/image-demo.jdd.tsx b/src/back/jdd-components/image-demo/image-demo.jdd.tsx new file mode 100644 index 0000000..c5799a7 --- /dev/null +++ b/src/back/jdd-components/image-demo/image-demo.jdd.tsx @@ -0,0 +1,63 @@ +import { FlatTemplatable, TempstreamJSX } from "tempstream"; +import { + Component, + ComponentArguments, + ExtractStructuredComponentArgumentsValues, + JDDContext, +} from "@sealcode/jdd"; + +const component_arguments = { + image_with_alt: new ComponentArguments.Structured({ + image: new ComponentArguments.Image(), + alt: new ComponentArguments.ShortText(), + }), + multiple_images: new ComponentArguments.List(new ComponentArguments.Image()), +} as const; + +export class ImageDemo extends Component { + getArguments() { + return component_arguments; + } + + toHTML( + { + image_with_alt, + multiple_images, + }: ExtractStructuredComponentArgumentsValues, + { render_image }: JDDContext + ): FlatTemplatable { + return ( +
+

Image with alt text

+ {" " || + render_image(image_with_alt.image, { + container: { width: 200, height: 200 }, + alt: image_with_alt.alt, + })} + +

Multiple images

+
+ {multiple_images.map((image) => + render_image(image, { + container: { width: 200, height: 200, objectFit: "cover" }, + }) + )} +
+ +

The same images but cropped with smartcrop

+
+ {multiple_images.map((image) => + render_image(image, { + container: { + width: 200, + height: 200, + objectFit: "cover", + }, + crop: { width: 200, height: 200 }, + }) + )} +
+
+ ); + } +} diff --git a/src/back/routes/component-preview/component-input-enum.tsx b/src/back/routes/component-preview/component-input-enum.tsx new file mode 100644 index 0000000..5d8784d --- /dev/null +++ b/src/back/routes/component-preview/component-input-enum.tsx @@ -0,0 +1,35 @@ +import { Enum } from "@sealcode/jdd"; +import { TempstreamJSX } from "tempstream"; +import { printArgPath } from "./print-arg-path.js"; + +export function ComponentInputEnum>({ + state, + arg_path, + arg, + value, + onchange, +}: { + state: State; + arg_path: string[]; + arg: T; + value: string; + onchange?: string; +}) { + return ( +
+ +
+ ); +} diff --git a/src/back/routes/component-preview/component-input-image.tsx b/src/back/routes/component-preview/component-input-image.tsx new file mode 100644 index 0000000..2298f24 --- /dev/null +++ b/src/back/routes/component-preview/component-input-image.tsx @@ -0,0 +1,54 @@ +import { Image } from "@sealcode/jdd"; +import { StatefulPage } from "@sealcode/sealgen"; +import { TempstreamJSX } from "tempstream"; +import { ComponentPreviewState } from "../components.sreact.js"; +import { jdd_context } from "../jdd-context.js"; +import { ComponentPreviewActions } from "./component-preview-actions.js"; +import { printArgPath } from "./print-arg-path.js"; + +export function ComponentInputImage({ + arg_path, + arg, + value, +}: { + state: State; + arg_path: string[]; + arg: Image; + value: string; + page: StatefulPage; +}) { + return ( +
+ +
+ +
+
+ ); +} diff --git a/src/back/routes/component-preview/component-input-list.tsx b/src/back/routes/component-preview/component-input-list.tsx new file mode 100644 index 0000000..1387732 --- /dev/null +++ b/src/back/routes/component-preview/component-input-list.tsx @@ -0,0 +1,58 @@ +import { ComponentArgument, List } from "@sealcode/jdd"; +import { StatefulPage } from "@sealcode/sealgen"; +import { TempstreamJSX } from "tempstream"; +import { ComponentPreviewState } from "../components.sreact.js"; +import { jdd_context } from "../jdd-context.js"; +import { ComponentInput } from "./component-input.js"; +import type { ComponentPreviewActions } from "./component-preview-actions.js"; + +export async function ComponentInputList({ + state, + arg_path, + arg, + value, + page, +}: { + state: State; + arg_path: string[]; + arg: List>; + value: T[]; + page: StatefulPage; +}) { + if (!value) { + value = []; + } + return ( +
+ {arg_path.at(-1)} + {value.map((value, i) => ( +
+ + {page.makeActionButton( + state, + { action: "remove_array_item", label: "❌" }, + arg_path, + i + )} +
+ ))} + {page.makeActionButton( + state, + { + action: "add_array_item", + label: "➕", + }, + arg_path, + await arg.item_type.getExampleValue(jdd_context) + )} +
+ ); +} diff --git a/src/back/routes/component-preview/component-input-structured.tsx b/src/back/routes/component-preview/component-input-structured.tsx new file mode 100644 index 0000000..efcc453 --- /dev/null +++ b/src/back/routes/component-preview/component-input-structured.tsx @@ -0,0 +1,46 @@ +import { ComponentArgument, Structured } from "@sealcode/jdd"; +import { StatefulPage } from "@sealcode/sealgen"; +import { TempstreamJSX } from "tempstream"; +import { ComponentPreviewState } from "../components.sreact.js"; +import { ComponentInput } from "./component-input.js"; +import { ComponentPreviewActions } from "./component-preview-actions.js"; +import { printArgPath } from "./print-arg-path.js"; + +export function ComponentInputStructured< + State, + T extends Structured>> +>({ + state, + arg_path, + arg, + value, + rerender_callback, + page, +}: { + state: ComponentPreviewState; + arg_path: string[]; + arg: T; + value: Record; + rerender_callback?: string; + page: StatefulPage; +}) { + return ( +
+ {arg_path.at(-1)} + {Object.entries(arg.structure).map(([arg_name, arg]) => ( +
+ +
+ ))} +
+ ); +} diff --git a/src/back/routes/component-preview/component-input.tsx b/src/back/routes/component-preview/component-input.tsx new file mode 100644 index 0000000..314d616 --- /dev/null +++ b/src/back/routes/component-preview/component-input.tsx @@ -0,0 +1,85 @@ +import { Enum, Image, List, Structured } from "@sealcode/jdd"; +import { ComponentArgument } from "@sealcode/jdd"; +import { StatefulPage } from "@sealcode/sealgen"; +import { TempstreamJSX } from "tempstream"; +import { ComponentPreviewState } from "../components.sreact.js"; +import { ComponentInputEnum } from "./component-input-enum.js"; +import { ComponentInputImage } from "./component-input-image.js"; +import { ComponentInputList } from "./component-input-list.js"; +import { ComponentInputStructured } from "./component-input-structured.js"; +import { printArgPath } from "./print-arg-path.js"; + +export function ComponentInput({ + state, + arg_path, + arg, + value, + page, +}: { + state: State; + arg_path: string[]; + arg: ComponentArgument; + value: T; + page: StatefulPage; +}) { + if (value === undefined) { + value = arg.getEmptyValue(); + } + if (arg instanceof List) { + return ComponentInputList({ state, arg_path, arg, value: value as T[], page }); + } + + if (arg instanceof Structured) { + return ComponentInputStructured({ + state, + arg_path, + arg, + value: value as Record, + page, + }); + } + + if (arg instanceof Enum) { + return ComponentInputEnum({ + state, + arg_path, + arg, + value: value as string, + onchange: page.rerender(), + }); + } + + if (arg instanceof Image) { + return ComponentInputImage({ + state, + arg_path, + arg, + value: value as string, + page, + }); + } + + return ( +
+ +
+ ); +} diff --git a/src/back/routes/component-preview/component-preview-actions.ts b/src/back/routes/component-preview/component-preview-actions.ts new file mode 100644 index 0000000..1ef0ed7 --- /dev/null +++ b/src/back/routes/component-preview/component-preview-actions.ts @@ -0,0 +1,61 @@ +import objectPath from "object-path"; +import { registry } from "../../jdd-components/components.js"; +import type { ComponentPreviewState } from "../components.sreact.js"; +import { jdd_context } from "../jdd-context.js"; + +export const ComponentPreviewActions = { + add_array_item: ( + state: ComponentPreviewState, + _: Record, + arg_path: string[], + empty_value: unknown + ) => { + const component_args = state.component_args; + objectPath.insert( + component_args, + arg_path, + empty_value, + ((objectPath.get(component_args, arg_path) as unknown[]) || []).length + ); + return { + ...state, + component_args, + }; + }, + remove_array_item: ( + state: ComponentPreviewState, + _: Record, + arg_path: string[], + index_to_remove: number + ) => { + const component_args = state.component_args; + objectPath.del(component_args, [...arg_path, index_to_remove]); + return { + ...state, + component_args, + }; + }, + change_component: async ( + state: ComponentPreviewState, + inputs: Record + ) => { + const component_name = inputs.component; + const component = registry.get(component_name); + return { + ...state, + component: component_name, + component_args: (await component?.getExampleValues(jdd_context)) || {}, + }; + }, + randomize_args: async ( + state: ComponentPreviewState, + inputs: Record + ) => { + const component_name = inputs.component; + const component = registry.get(component_name); + return { + ...state, + component_args: (await component?.getExampleValues(jdd_context)) || {}, + }; + }, +}; diff --git a/src/back/routes/component-preview/print-arg-path.ts b/src/back/routes/component-preview/print-arg-path.ts new file mode 100644 index 0000000..ca3fa3c --- /dev/null +++ b/src/back/routes/component-preview/print-arg-path.ts @@ -0,0 +1,3 @@ +export function printArgPath(path: string[]): string { + return path.map((e) => `[${e}]`).join(""); +} diff --git a/src/back/routes/jdd-context.ts b/src/back/routes/jdd-context.ts new file mode 100644 index 0000000..a5f5cf7 --- /dev/null +++ b/src/back/routes/jdd-context.ts @@ -0,0 +1,16 @@ +import { simpleJDDContext, JDDContext } from "@sealcode/jdd"; +import { imageRouter } from "../image-router.js"; + +export const jdd_context = { + ...simpleJDDContext, + render_image: async (image_id, args) => { + if (!image_id) { + return ""; + } + const image_pointer = await simpleJDDContext.decode_file(image_id); + if (!image_pointer) { + return ""; + } + return imageRouter.image(image_pointer.path, args); + }, +} as JDDContext; diff --git a/src/front/controllers/input-image-preview.ts b/src/front/controllers/input-image-preview.ts new file mode 100644 index 0000000..d3a654d --- /dev/null +++ b/src/front/controllers/input-image-preview.ts @@ -0,0 +1,21 @@ +import { Controller } from "stimulus"; + +export default class InputImagePreview extends Controller { + id: string; + + handleChange(event) { + const img = this.element.querySelector("img"); + console.log({ img }); + window.URL.revokeObjectURL(img.src); + const new_url = window.URL.createObjectURL( + this.element.querySelector("input").files[0] + ); + console.log({ new_url }); + img.src = new_url; + img.parentNode; + img.parentElement + .querySelectorAll("source") + .forEach((e) => e.parentNode.removeChild(e)); + img.style.aspectRatio = "1"; + } +} diff --git a/uploaded-images/keepme b/uploaded-images/keepme new file mode 100644 index 0000000..e69de29