Compare commits

...

7 Commits

Author SHA1 Message Date
Trisha Lim
ea2e2a7f42 clean up 2025-02-25 20:19:49 +07:00
Trisha Lim
cfef71f290 set image width to full 2025-02-25 20:05:30 +07:00
Trisha Lim
b9f7267cce styling for uploading state 2025-02-25 20:00:48 +07:00
Trisha Lim
cf486a9c5a fix tailwind 2025-02-25 19:55:46 +07:00
Trisha Lim
23795ec07b handle different states 2025-02-25 19:53:35 +07:00
Benjamin S. Leveritt
1efcea89d8 Adds unload notification while uploading 2025-02-23 16:52:37 +00:00
Benjamin S. Leveritt
4f31fddcbc Return promise while processing image 2025-02-23 16:51:21 +00:00
8 changed files with 186 additions and 174 deletions

View File

@@ -24,6 +24,9 @@
"@vitejs/plugin-react": "^4.3.3",
"globals": "^15.11.0",
"typescript": "~5.6.2",
"vite": "^6.0.11"
"vite": "^6.0.11",
"autoprefixer": "^10.4.20",
"postcss": "^8.4.27",
"tailwindcss": "^3.4.17"
}
}

View File

@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

View File

@@ -3,7 +3,7 @@ import ImageUpload from "./ImageUpload.tsx";
function App() {
return (
<>
<main className="container">
<main className="container py-16">
<ImageUpload />
</main>
</>

View File

@@ -1,60 +1,97 @@
import { createImage } from "jazz-browser-media-images";
import { ProgressiveImg, useAccount } from "jazz-react";
import { ImageDefinition } from "jazz-tools";
import { ChangeEvent, useRef } from "react";
function Image({ image }: { image: ImageDefinition }) {
return (
<ProgressiveImg image={image}>
{({ src }) => <img src={src} />}
</ProgressiveImg>
);
}
import { ChangeEvent, useEffect, useRef, useState } from "react";
export default function ImageUpload() {
const { me } = useAccount();
const [imagePreviewUrl, setImagePreviewUrl] = useState<string | null>(null);
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
const handleBeforeUnload = (e: BeforeUnloadEvent) => {
if (imagePreviewUrl) {
e.preventDefault();
return "Upload in progress. Are you sure you want to leave?";
}
};
window.addEventListener("beforeunload", handleBeforeUnload);
return () => {
window.removeEventListener("beforeunload", handleBeforeUnload);
if (imagePreviewUrl) {
URL.revokeObjectURL(imagePreviewUrl);
}
};
}, [imagePreviewUrl]);
const onImageChange = async (event: ChangeEvent<HTMLInputElement>) => {
if (!me?.profile) return;
const file = event.currentTarget.files?.[0];
if (file) {
me.profile.image = await createImage(file, {
owner: me.profile._owner,
});
const objectUrl = URL.createObjectURL(file);
setImagePreviewUrl(objectUrl);
try {
me.profile.image = await createImage(file, {
owner: me.profile._owner,
});
} catch (error) {
console.error("Error uploading image:", error);
} finally {
URL.revokeObjectURL(objectUrl);
setImagePreviewUrl(null);
}
}
};
const deleteImage = () => {
if (!me?.profile) return;
me.profile.image = null;
};
return (
<>
<div>{me?.profile?.image && <Image image={me.profile.image} />}</div>
if (me?.profile?.image) {
return (
<>
<ProgressiveImg image={me.profile.image}>
{({ src }) => <img alt="" src={src} className="w-full h-auto" />}
</ProgressiveImg>
<div>
{me?.profile?.image ? (
<button type="button" onClick={deleteImage}>
Delete image
</button>
) : (
<div>
<label>Upload image</label>
<input
ref={inputRef}
type="file"
accept="image/png, image/jpeg, image/gif"
onChange={onImageChange}
/>
</div>
)}
<button type="button" onClick={deleteImage} className="mt-5">
Delete image
</button>
</>
);
}
if (imagePreviewUrl) {
return (
<div className="relative">
<p className="z-10 absolute font-semibold text-gray-900 inset-0 flex items-center justify-center">
Uploading image...
</p>
<img
src={imagePreviewUrl}
alt="Preview"
className="opacity-50 w-full h-auto"
/>
</div>
</>
);
}
return (
<div className="flex flex-col gap-3">
<label htmlFor="image">Image</label>
<input
id="image"
name="image"
ref={inputRef}
type="file"
accept="image/png, image/jpeg, image/gif, image/bmp"
onChange={onImageChange}
/>
</div>
);
}

View File

@@ -1,82 +1,3 @@
:root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
--border-color: #2f2e2d;
}
html,
body,
#root {
height: 100%;
}
body {
margin: 0;
}
button {
border-radius: 8px;
border: 0;
padding: 0.6em 1.2em;
font-weight: 500;
background-color: #1a1a1a;
cursor: pointer;
}
@media (prefers-color-scheme: light) {
:root {
--border-color: #e5e5e5;
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}
* {
border-color: var(--border-color);
}
header,
main {
padding: 0.5rem 0;
}
header {
border-bottom: 1px solid var(--border-color);
margin-bottom: 2rem;
}
nav {
display: flex;
align-items: center;
justify-content: space-between;
}
.container {
margin-right: auto;
margin-left: auto;
padding: 2rem 0.75rem;
max-width: 800px;
}
label {
display: block;
margin-bottom: 0.25rem;
}
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

@@ -0,0 +1,18 @@
import type { Config } from "tailwindcss";
const config: Config = {
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
theme: {
extend: {
container: {
center: true,
padding: {
DEFAULT: "0.75rem",
sm: "1rem",
},
},
},
},
} as const;
export default config;

View File

@@ -5,6 +5,13 @@ import Pica from "pica";
const pica = new Pica();
/** @category Image creation */
/**
* Creates an ImageDefinition from a Blob or File.
*
* @param imageBlobOrFile - The Blob or File to create the ImageDefinition from.
* @param options - Optional options for the ImageDefinition.
* @returns A Promise that resolves to the created ImageDefinition.
*/
export async function createImage(
imageBlobOrFile: Blob | File,
options?: {
@@ -40,79 +47,90 @@ export async function createImage(
},
owner,
);
setTimeout(async () => {
const max256 = await Reducer.toBlob(imageBlobOrFile, { max: 256 });
if (originalWidth > 256 || originalHeight > 256) {
const width =
originalWidth > originalHeight
? 256
: Math.round(256 * (originalWidth / originalHeight));
const height =
originalHeight > originalWidth
? 256
: Math.round(256 * (originalHeight / originalWidth));
return new Promise((resolve) => {
setTimeout(async () => {
const max256 = await Reducer.toBlob(imageBlobOrFile, { max: 256 });
const binaryStream = await FileStream.createFromBlob(max256, owner);
if (originalWidth > 256 || originalHeight > 256) {
const width =
originalWidth > originalHeight
? 256
: Math.round(256 * (originalWidth / originalHeight));
const height =
originalHeight > originalWidth
? 256
: Math.round(256 * (originalHeight / originalWidth));
console.log(`${width}x${height}`);
imageDefinition[`${width}x${height}`] = binaryStream;
}
const binaryStream = await FileStream.createFromBlob(max256, owner);
await new Promise((resolve) => setTimeout(resolve, 0));
imageDefinition[`${width}x${height}`] = binaryStream;
}
if (options?.maxSize === 256) return;
await new Promise((r) => setTimeout(r, 0));
const max1024 = await Reducer.toBlob(imageBlobOrFile, { max: 1024 });
if (options?.maxSize === 256) {
resolve(imageDefinition);
return;
}
if (originalWidth > 1024 || originalHeight > 1024) {
const width =
originalWidth > originalHeight
? 1024
: Math.round(1024 * (originalWidth / originalHeight));
const height =
originalHeight > originalWidth
? 1024
: Math.round(1024 * (originalHeight / originalWidth));
const max1024 = await Reducer.toBlob(imageBlobOrFile, { max: 1024 });
const binaryStream = await FileStream.createFromBlob(max1024, owner);
if (originalWidth > 1024 || originalHeight > 1024) {
const width =
originalWidth > originalHeight
? 1024
: Math.round(1024 * (originalWidth / originalHeight));
const height =
originalHeight > originalWidth
? 1024
: Math.round(1024 * (originalHeight / originalWidth));
imageDefinition[`${width}x${height}`] = binaryStream;
}
const binaryStream = await FileStream.createFromBlob(max1024, owner);
await new Promise((resolve) => setTimeout(resolve, 0));
imageDefinition[`${width}x${height}`] = binaryStream;
}
if (options?.maxSize === 1024) return;
await new Promise((r) => setTimeout(r, 0));
const max2048 = await Reducer.toBlob(imageBlobOrFile, { max: 2048 });
if (options?.maxSize === 1024) {
resolve(imageDefinition);
return;
}
if (originalWidth > 2048 || originalHeight > 2048) {
const width =
originalWidth > originalHeight
? 2048
: Math.round(2048 * (originalWidth / originalHeight));
const height =
originalHeight > originalWidth
? 2048
: Math.round(2048 * (originalHeight / originalWidth));
const max2048 = await Reducer.toBlob(imageBlobOrFile, { max: 2048 });
const binaryStream = await FileStream.createFromBlob(max2048, owner);
if (originalWidth > 2048 || originalHeight > 2048) {
const width =
originalWidth > originalHeight
? 2048
: Math.round(2048 * (originalWidth / originalHeight));
const height =
originalHeight > originalWidth
? 2048
: Math.round(2048 * (originalHeight / originalWidth));
imageDefinition[`${width}x${height}`] = binaryStream;
}
const binaryStream = await FileStream.createFromBlob(max2048, owner);
await new Promise((resolve) => setTimeout(resolve, 0));
imageDefinition[`${width}x${height}`] = binaryStream;
}
if (options?.maxSize === 2048) return;
await new Promise((r) => setTimeout(r, 0));
const originalBinaryStream = await FileStream.createFromBlob(
imageBlobOrFile,
owner,
);
if (options?.maxSize === 2048) {
resolve(imageDefinition);
return;
}
imageDefinition[`${originalWidth}x${originalHeight}`] =
originalBinaryStream;
}, 0);
const originalBinaryStream = await FileStream.createFromBlob(
imageBlobOrFile,
owner,
);
return imageDefinition;
imageDefinition[`${originalWidth}x${originalHeight}`] =
originalBinaryStream;
resolve(imageDefinition);
}, 0);
});
}

9
pnpm-lock.yaml generated
View File

@@ -651,9 +651,18 @@ importers:
'@vitejs/plugin-react':
specifier: ^4.3.3
version: 4.3.4(vite@6.0.11(@types/node@22.10.2)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.6.1))
autoprefixer:
specifier: ^10.4.20
version: 10.4.20(postcss@8.4.49)
globals:
specifier: ^15.11.0
version: 15.14.0
postcss:
specifier: ^8.4.27
version: 8.4.49
tailwindcss:
specifier: ^3.4.17
version: 3.4.17(ts-node@10.9.2(@swc/core@1.10.1(@swc/helpers@0.5.15))(@types/node@22.10.2)(typescript@5.6.3))
typescript:
specifier: ~5.6.2
version: 5.6.3