Skip to content

Commit

Permalink
Added Image component (#3)
Browse files Browse the repository at this point in the history
* Added `Image` component

* Ensure correct types

* Test JS client
  • Loading branch information
leo authored May 24, 2024
1 parent 1ba8793 commit 05f33c8
Show file tree
Hide file tree
Showing 6 changed files with 167 additions and 26 deletions.
Binary file modified bun.lockb
Binary file not shown.
29 changes: 5 additions & 24 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,43 +15,23 @@
"test": "bun test",
"prepare": "husky"
},
"files": [
"dist"
],
"files": ["dist"],
"repository": "ronin-co/react-ronin",
"homepage": "https://github.com/ronin-co/react-ronin",
"keywords": [
"ronin",
"react",
"client",
"database",
"orm"
],
"keywords": ["ronin", "react", "client", "database", "orm"],
"lint-staged": {
"**/*": [
"bunx @biomejs/biome format --write"
]
"**/*": ["bunx @biomejs/biome format --write"]
},
"exports": {
".": {
"types": "./dist/index.d.mts",
"import": "./dist/index.mjs",
"require": "./dist/index.js"
},
"./components": {
"types": "./dist/components.d.mts",
"import": "./dist/components.mjs",
"require": "./dist/components.js"
}
},
"typesVersions": {
"*": {
"*": [
"dist/index.d.ts"
],
"components": [
"dist/components.d.mts"
]
"*": ["dist/index.d.ts"]
}
},
"dependencies": {
Expand All @@ -61,6 +41,7 @@
"@biomejs/biome": "1.7.3",
"@types/bun": "1.1.3",
"@types/react": "18.3.2",
"@types/web": "0.0.147",
"bunchee": "5.1.5",
"husky": "9.0.11",
"lint-staged": "15.2.4",
Expand Down
155 changes: 155 additions & 0 deletions src/components/image.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import { useCallback, useRef } from "react";
import type { StoredObject } from "ronin/types";

export interface ImageProps {
/**
* Defines text that can replace the image in the page.
*/
alt?: string;
/**
* The quality level at which the image should be displayed. A lower quality
* ensures a faster loading speed, but might also effect the visual
* appearance, so it is essential to choose carefully.
*
* Must be an integer between 0 and 100.
*/
quality?: number;
/**
* The value of a RONIN Blob field.
*/
src: string | StoredObject;
/**
* The intrinsic size of the image in pixels, if its width and height are the
* same. Must be an integer without a unit.
*/
size?: number;
/**
* The intrinsic width of the image in pixels. Must be an integer without a
* unit.
*/
width?: number;
/**
* The intrinsic height of the image, in pixels. Must be an integer without
* a unit.
*/
height?: number;
/**
* Indicates how the browser should load the image.
*
* Providing the value "lazy" defers loading the image until it reaches a
* calculated distance from the viewport, as defined by the browser. The
* intent is to avoid the network and storage bandwidth needed to handle the
* image until it's reasonably certain that it will be needed. This generally
* improves the performance of the content in most typical use cases.
*/
loading?: "lazy";
}

const Image = ({
src: input,
alt,
size: defaultSize,
width: defaultWidth,
height: defaultHeight,
quality,
loading,
}: ImageProps) => {
const imageElement = useRef<HTMLImageElement | null>(null);
const renderTime = useRef<number>(Date.now());

const isMediaObject = typeof input === "object" && input !== null;
const width = defaultSize || defaultWidth;
const height = defaultSize || defaultHeight;

if (!height && !width)
throw new Error(
"Either `width`, `height`, or `size` must be defined for `Image`.",
);

// Validate given `quality` property.
if (quality && (quality < 0 || quality > 100))
throw new Error(
"The given `quality` was not in the range between 0 and 100.",
);

const optimizationParams = new URLSearchParams({
...(width ? { w: width.toString() } : {}),
...(height ? { h: height.toString() } : {}),
q: quality ? quality.toString() : "100",
});

const responsiveOptimizationParams = new URLSearchParams({
...(width ? { h: (width * 2).toString() } : {}),
...(height ? { h: (height * 2).toString() } : {}),
q: quality ? quality.toString() : "100",
});

const source = isMediaObject ? `${input.src}?${optimizationParams}` : input;

const responsiveSource = isMediaObject
? `${input.src}?${optimizationParams} 1x, ` +
`${input.src}?${responsiveOptimizationParams} 2x`
: input;

const placeholder =
input && typeof input !== "string" ? input.placeholder?.base64 : null;

const onLoad = useCallback(() => {
const duration = Date.now() - renderTime.current;
const threshold = 150;

// Fade in and gradually reduce blur of the real image if loading takes
// longer than the specified threshold.
if (duration > threshold) {
imageElement.current?.animate(
[
{ filter: "blur(4px)", opacity: 0 },
{ filter: "blur(0px)", opacity: 1 },
],
{
duration: 200,
},
);
}
}, []);

return (
<div
style={{
position: "relative",
overflow: "hidden",
flexShrink: 0,
width: width || "100%",
height: height || "100%",
}}
>
{/* Blurred preview being displayed until the actual image is loaded. */}
{placeholder && (
<img
style={{ position: "absolute", width: "100%", height: "100%" }}
src={placeholder}
alt={alt}
/>
)}

{/* The optimized image, responsive to the specified size. */}
<img
alt={alt}
style={{
position: "absolute",
width: "100%",
height: "100%",
objectFit: "cover",
}}
decoding="async"
onLoad={onLoad}
loading={loading}
ref={imageElement}
src={source}
srcSet={responsiveSource}
/>
</div>
);
};

export default Image;
4 changes: 3 additions & 1 deletion src/components.tsx → src/components/rich-text.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export type RichTextContent =
)[];
};

export const RichText = ({
const RichText = ({
data,
components,
}: {
Expand Down Expand Up @@ -177,3 +177,5 @@ export const RichText = ({
);
});
};

export default RichText;
3 changes: 3 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
export * from "ronin";
export { default } from "ronin";

export { default as RichText } from "./components/rich-text";
export { default as Image } from "./components/image";
2 changes: 1 addition & 1 deletion tests/integration/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { beforeEach, describe, expect, mock, test } from "bun:test";
import createSyntaxFactory from "@/index";
import createSyntaxFactory from "ronin";

let mockResolvedRequestText: string | undefined = undefined;

Expand Down

0 comments on commit 05f33c8

Please sign in to comment.