mirror of
https://github.com/storytold/placeholder-artcraft.git
synced 2026-10-29 09:06:48 +00:00
mirror repo (WIP)
Need to remove secrets before entire repo can be made public.
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"presets": [
|
||||
[
|
||||
"@nx/react/babel",
|
||||
{
|
||||
"runtime": "automatic",
|
||||
"useBuiltIns": "usage"
|
||||
}
|
||||
]
|
||||
],
|
||||
"plugins": []
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
# @storyteller/ui-image-input
|
||||
|
||||
This library was generated with [Nx](https://nx.dev).
|
||||
|
||||
## Running unit tests
|
||||
|
||||
Run `nx test @storyteller/ui-image-input` to execute the unit tests via [Vitest](https://vitest.dev/).
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './lib/image-input';
|
||||
//# sourceMappingURL=index.d.ts.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,cAAc,mBAAmB,CAAC"}
|
||||
+12269
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,23 @@
|
||||
export interface ImageFile {
|
||||
id: string;
|
||||
url: string;
|
||||
file: File;
|
||||
mediaToken?: string;
|
||||
}
|
||||
export interface ImageInputProps {
|
||||
/** Selected image */
|
||||
value?: ImageFile | null;
|
||||
/** Callback when image is selected or removed */
|
||||
onChange: (image: ImageFile | null) => void;
|
||||
/** Handler for opening gallery modal */
|
||||
onGalleryOpen?: () => void;
|
||||
/** Show gallery button */
|
||||
showGalleryButton?: boolean;
|
||||
/** Custom placeholder text */
|
||||
placeholderText?: string;
|
||||
/** Additional class names */
|
||||
className?: string;
|
||||
}
|
||||
export declare function ImageInput({ value, onChange, onGalleryOpen, showGalleryButton, placeholderText, className, }: ImageInputProps): import("react/jsx-runtime").JSX.Element;
|
||||
export default ImageInput;
|
||||
//# sourceMappingURL=image-input.d.ts.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"image-input.d.ts","sourceRoot":"","sources":["../../src/lib/image-input.tsx"],"names":[],"mappings":"AAOA,MAAM,WAAW,SAAS;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,IAAI,CAAC;IACX,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,eAAe;IAC9B,qBAAqB;IACrB,KAAK,CAAC,EAAE,SAAS,GAAG,IAAI,CAAC;IACzB,iDAAiD;IACjD,QAAQ,EAAE,CAAC,KAAK,EAAE,SAAS,GAAG,IAAI,KAAK,IAAI,CAAC;IAC5C,wCAAwC;IACxC,aAAa,CAAC,EAAE,MAAM,IAAI,CAAC;IAC3B,0BAA0B;IAC1B,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,8BAA8B;IAC9B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,6BAA6B;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,wBAAgB,UAAU,CAAC,EACzB,KAAK,EACL,QAAQ,EACR,aAAa,EACb,iBAAwB,EACxB,eAA+C,EAC/C,SAAS,GACV,EAAE,eAAe,2CAkOjB;AAED,eAAe,UAAU,CAAC"}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "@storyteller/ui-image-input",
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
"./package.json": "./package.json",
|
||||
".": {
|
||||
"development": "./src/index.ts",
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js",
|
||||
"default": "./dist/index.js"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
|
||||
export * from './lib/image-input';
|
||||
@@ -0,0 +1,7 @@
|
||||
/*
|
||||
* Replace this with your own classes
|
||||
*
|
||||
* e.g.
|
||||
* .container {
|
||||
* }
|
||||
*/
|
||||
@@ -0,0 +1,12 @@
|
||||
import { render } from '@testing-library/react';
|
||||
|
||||
import ImageInput from './image-input';
|
||||
|
||||
describe('ImageInput', () => {
|
||||
|
||||
it('should render successfully', () => {
|
||||
const { baseElement } = render(<ImageInput />);
|
||||
expect(baseElement).toBeTruthy();
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,265 @@
|
||||
import { useState, useRef, useCallback } from "react";
|
||||
import { Button } from "@storyteller/ui-button";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faUpload, faImages, faTrash } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { toast } from "react-hot-toast";
|
||||
|
||||
export interface ImageFile {
|
||||
id: string;
|
||||
url: string;
|
||||
file: File;
|
||||
mediaToken?: string;
|
||||
}
|
||||
|
||||
export interface ImageInputProps {
|
||||
/** Selected image */
|
||||
value?: ImageFile | null;
|
||||
/** Callback when image is selected or removed */
|
||||
onChange: (image: ImageFile | null) => void;
|
||||
/** Handler for opening gallery modal */
|
||||
onGalleryOpen?: () => void;
|
||||
/** Show gallery button */
|
||||
showGalleryButton?: boolean;
|
||||
/** Custom placeholder text */
|
||||
placeholderText?: string;
|
||||
/** Additional class names */
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function ImageInput({
|
||||
value,
|
||||
onChange,
|
||||
onGalleryOpen,
|
||||
showGalleryButton = true,
|
||||
placeholderText = "Drag and drop an image here",
|
||||
className,
|
||||
}: ImageInputProps) {
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const processImageFile = (file: File) => {
|
||||
// Only accept image files
|
||||
if (!file.type.startsWith("image/")) {
|
||||
toast.error("Please upload an image file");
|
||||
return;
|
||||
}
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => {
|
||||
const newImage: ImageFile = {
|
||||
id: Math.random().toString(36).substring(7),
|
||||
url: reader.result as string,
|
||||
file: file,
|
||||
};
|
||||
onChange(newImage);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
};
|
||||
|
||||
const handleFileUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const files = event.target.files;
|
||||
if (files && files[0]) {
|
||||
const file = files[0];
|
||||
processImageFile(file);
|
||||
}
|
||||
};
|
||||
|
||||
const handleUploadClick = () => {
|
||||
fileInputRef.current?.click();
|
||||
};
|
||||
|
||||
// Drag and drop handlers
|
||||
// Enhanced drag and drop handlers for better desktop compatibility
|
||||
const handleDragEnter = useCallback((e: React.DragEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIsDragging(true);
|
||||
}, []);
|
||||
|
||||
const handleDragOver = useCallback(
|
||||
(e: React.DragEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (!isDragging) setIsDragging(true);
|
||||
|
||||
// Explicitly set effectAllowed to make sure the browser shows the correct drop effect
|
||||
if (e.dataTransfer) {
|
||||
e.dataTransfer.dropEffect = "copy";
|
||||
}
|
||||
},
|
||||
[isDragging]
|
||||
);
|
||||
|
||||
const handleDragLeave = useCallback((e: React.DragEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// Only set isDragging to false if we're leaving the drop target
|
||||
// and not entering a child element
|
||||
const rect = (e.currentTarget as HTMLDivElement).getBoundingClientRect();
|
||||
const x = e.clientX;
|
||||
const y = e.clientY;
|
||||
|
||||
if (x < rect.left || x >= rect.right || y < rect.top || y >= rect.bottom) {
|
||||
setIsDragging(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleDrop = useCallback((e: React.DragEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIsDragging(false);
|
||||
|
||||
// Debug logging
|
||||
console.log("Files dropped:", e.dataTransfer.files);
|
||||
console.log("DataTransfer types:", e.dataTransfer.types);
|
||||
|
||||
try {
|
||||
// First try the files collection
|
||||
if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
|
||||
const file = e.dataTransfer.files[0]; // Just take the first file
|
||||
console.log("Processing file:", file.name, file.type, file.size);
|
||||
processImageFile(file);
|
||||
return;
|
||||
}
|
||||
|
||||
// Then try the items collection
|
||||
const items = e.dataTransfer.items;
|
||||
if (items && items.length > 0) {
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
if (items[i].kind === "file") {
|
||||
const file = items[i].getAsFile();
|
||||
if (file) {
|
||||
console.log("Processing item as file:", file.name);
|
||||
processImageFile(file);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we got here, we couldn't process the drop
|
||||
console.log("Could not process dropped content");
|
||||
toast.error(
|
||||
"Could not process the dropped file. Please try uploading instead."
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Error processing drop:", error);
|
||||
toast.error(
|
||||
"Error processing the dropped file. Please try uploading instead."
|
||||
);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={twMerge("relative", className)}>
|
||||
{/* Drag & drop area or image preview */}
|
||||
<div
|
||||
className={twMerge(
|
||||
"mx-auto flex h-60 w-full flex-col items-center justify-center rounded-lg border border-dashed border-white/20 bg-white/5 p-4 transition-colors",
|
||||
isDragging && "border-blue-400 bg-blue-500/10"
|
||||
)}
|
||||
onDragEnter={handleDragEnter}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
onDragStart={(e) => e.preventDefault()}
|
||||
>
|
||||
{value ? (
|
||||
<div className="h-full w-full overflow-hidden">
|
||||
<img
|
||||
src={value.url}
|
||||
alt="Selected image"
|
||||
className="h-full w-full object-contain"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex h-full w-full flex-col items-center justify-center text-white/60">
|
||||
<FontAwesomeIcon icon={faImages} className="text-2xl mb-2" />
|
||||
<p>{placeholderText}</p>
|
||||
|
||||
<div className="flex items-center justify-center gap-2 my-4 w-full">
|
||||
<div className="h-[1px] w-12 bg-white/10" />
|
||||
<p className="text-sm px-3">or</p>
|
||||
<div className="h-[1px] w-12 bg-white/10" />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col items-center justify-center gap-3 sm:flex-row">
|
||||
{showGalleryButton && onGalleryOpen && (
|
||||
<Button
|
||||
onClick={onGalleryOpen}
|
||||
variant="secondary"
|
||||
className="flex items-center gap-2 px-2.5 py-1.5 text-sm bg-white/15"
|
||||
>
|
||||
<FontAwesomeIcon icon={faImages} />
|
||||
Choose from Library
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button
|
||||
onClick={handleUploadClick}
|
||||
variant="secondary"
|
||||
className="flex items-center gap-2 px-2.5 py-1.5 text-sm bg-white/15"
|
||||
>
|
||||
<FontAwesomeIcon icon={faUpload} />
|
||||
Upload Image
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Hidden file input */}
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={handleFileUpload}
|
||||
className="hidden"
|
||||
/>
|
||||
|
||||
{/* Action buttons when image is selected */}
|
||||
{value && (
|
||||
<div className="mt-4 flex flex-wrap items-center justify-center gap-3">
|
||||
{showGalleryButton && onGalleryOpen && (
|
||||
<Button
|
||||
onClick={onGalleryOpen}
|
||||
variant="secondary"
|
||||
className="flex items-center gap-2 px-2.5 py-1.5 text-sm"
|
||||
icon={faImages}
|
||||
>
|
||||
Choose Different Image
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button
|
||||
onClick={handleUploadClick}
|
||||
variant="secondary"
|
||||
className="flex items-center gap-2 px-2.5 py-1.5 text-sm"
|
||||
icon={faUpload}
|
||||
>
|
||||
Upload Different Image
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onClick={() => {
|
||||
onChange(null);
|
||||
// Reset file input value to allow selecting the same file again
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = "";
|
||||
}
|
||||
}}
|
||||
variant="destructive"
|
||||
className="flex items-center gap-2 px-2.5 py-1.5 text-sm"
|
||||
icon={faTrash}
|
||||
>
|
||||
Remove Image
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ImageInput;
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"files": [],
|
||||
"include": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "../button"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.spec.json"
|
||||
}
|
||||
],
|
||||
"extends": "../../../tsconfig.base.json"
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"types": [
|
||||
"node",
|
||||
"@nx/react/typings/cssmodule.d.ts",
|
||||
"@nx/react/typings/image.d.ts",
|
||||
"vite/client"
|
||||
],
|
||||
"rootDir": "src",
|
||||
"jsx": "react-jsx",
|
||||
"tsBuildInfoFile": "dist/tsconfig.lib.tsbuildinfo"
|
||||
},
|
||||
"exclude": [
|
||||
"out-tsc",
|
||||
"dist",
|
||||
"**/*.spec.ts",
|
||||
"**/*.test.ts",
|
||||
"**/*.spec.tsx",
|
||||
"**/*.test.tsx",
|
||||
"**/*.spec.js",
|
||||
"**/*.test.js",
|
||||
"**/*.spec.jsx",
|
||||
"**/*.test.jsx",
|
||||
"vite.config.ts",
|
||||
"vite.config.mts",
|
||||
"vitest.config.ts",
|
||||
"vitest.config.mts",
|
||||
"src/**/*.test.ts",
|
||||
"src/**/*.spec.ts",
|
||||
"src/**/*.test.tsx",
|
||||
"src/**/*.spec.tsx",
|
||||
"src/**/*.test.js",
|
||||
"src/**/*.spec.js",
|
||||
"src/**/*.test.jsx",
|
||||
"src/**/*.spec.jsx"
|
||||
],
|
||||
"include": [
|
||||
"src/**/*.js",
|
||||
"src/**/*.jsx",
|
||||
"src/**/*.ts",
|
||||
"src/**/*.tsx"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../button/tsconfig.lib.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./out-tsc/vitest",
|
||||
"types": [
|
||||
"vitest/globals",
|
||||
"vitest/importMeta",
|
||||
"vite/client",
|
||||
"node",
|
||||
"vitest"
|
||||
],
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"include": [
|
||||
"vite.config.ts",
|
||||
"vite.config.mts",
|
||||
"vitest.config.ts",
|
||||
"vitest.config.mts",
|
||||
"src/**/*.test.ts",
|
||||
"src/**/*.spec.ts",
|
||||
"src/**/*.test.tsx",
|
||||
"src/**/*.spec.tsx",
|
||||
"src/**/*.test.js",
|
||||
"src/**/*.spec.js",
|
||||
"src/**/*.test.jsx",
|
||||
"src/**/*.spec.jsx",
|
||||
"src/**/*.d.ts"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.lib.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/// <reference types='vitest' />
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
import dts from 'vite-plugin-dts';
|
||||
import * as path from 'path';
|
||||
|
||||
export default defineConfig(() => ({
|
||||
root: __dirname,
|
||||
cacheDir: '../../../node_modules/.vite/libs/components/image-input',
|
||||
plugins: [react(), dts({ entryRoot: 'src', tsconfigPath: path.join(__dirname, 'tsconfig.lib.json') })],
|
||||
// Uncomment this if you are using workers.
|
||||
// worker: {
|
||||
// plugins: [ nxViteTsPaths() ],
|
||||
// },
|
||||
// Configuration for building your library.
|
||||
// See: https://vitejs.dev/guide/build.html#library-mode
|
||||
build: {
|
||||
outDir: './dist',
|
||||
emptyOutDir: true,
|
||||
reportCompressedSize: true,
|
||||
commonjsOptions: {
|
||||
transformMixedEsModules: true,
|
||||
},
|
||||
lib: {
|
||||
// Could also be a dictionary or array of multiple entry points.
|
||||
entry: 'src/index.ts',
|
||||
name: '@storyteller/ui-image-input',
|
||||
fileName: 'index',
|
||||
// Change this to the formats you want to support.
|
||||
// Don't forget to update your package.json as well.
|
||||
formats: ['es' as const]
|
||||
},
|
||||
rollupOptions: {
|
||||
// External packages that should not be bundled into your library.
|
||||
external: ['react','react-dom','react/jsx-runtime']
|
||||
},
|
||||
},
|
||||
test: {
|
||||
watch: false,
|
||||
globals: true,
|
||||
environment: 'jsdom',
|
||||
include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
|
||||
reporters: ['default'],
|
||||
coverage: {
|
||||
reportsDirectory: './test-output/vitest/coverage',
|
||||
provider: 'v8' as const,
|
||||
}
|
||||
},
|
||||
}));
|
||||
Reference in New Issue
Block a user