feat(05-01): redesign ImageUpload as hero area and move to top of forms
- Full-width 4:3 aspect ratio hero image area with rounded corners - Placeholder state: gray background with ImagePlus icon and helper text - Preview state: object-cover image with circular X remove button - Upload state: semi-transparent spinner overlay - Entire area clickable to upload/replace image - Moved ImageUpload to first element in both ItemForm and CandidateForm - Removed redundant "Image" label wrappers Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -134,6 +134,14 @@ export function CandidateForm({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit} className="space-y-5">
|
<form onSubmit={handleSubmit} className="space-y-5">
|
||||||
|
{/* Image */}
|
||||||
|
<ImageUpload
|
||||||
|
value={form.imageFilename}
|
||||||
|
onChange={(filename) =>
|
||||||
|
setForm((f) => ({ ...f, imageFilename: filename }))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Name */}
|
{/* Name */}
|
||||||
<div>
|
<div>
|
||||||
<label
|
<label
|
||||||
@@ -258,19 +266,6 @@ export function CandidateForm({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Image */}
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
||||||
Image
|
|
||||||
</label>
|
|
||||||
<ImageUpload
|
|
||||||
value={form.imageFilename}
|
|
||||||
onChange={(filename) =>
|
|
||||||
setForm((f) => ({ ...f, imageFilename: filename }))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Actions */}
|
{/* Actions */}
|
||||||
<div className="flex gap-3 pt-2">
|
<div className="flex gap-3 pt-2">
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -41,22 +41,35 @@ export function ImageUpload({ value, onChange }: ImageUploadProps) {
|
|||||||
setError("Upload failed. Please try again.");
|
setError("Upload failed. Please try again.");
|
||||||
} finally {
|
} finally {
|
||||||
setUploading(false);
|
setUploading(false);
|
||||||
|
// Reset input so the same file can be re-selected
|
||||||
|
if (inputRef.current) inputRef.current.value = "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleRemove(e: React.MouseEvent) {
|
||||||
|
e.stopPropagation();
|
||||||
|
onChange(null);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{value && (
|
{/* Hero image area */}
|
||||||
<div className="mb-2 relative">
|
<div
|
||||||
|
onClick={() => inputRef.current?.click()}
|
||||||
|
className="relative w-full aspect-[4/3] rounded-xl overflow-hidden cursor-pointer group"
|
||||||
|
>
|
||||||
|
{value ? (
|
||||||
|
<>
|
||||||
<img
|
<img
|
||||||
src={`/uploads/${value}`}
|
src={`/uploads/${value}`}
|
||||||
alt="Item"
|
alt="Item"
|
||||||
className="w-full h-32 object-cover rounded-lg"
|
className="w-full h-full object-cover"
|
||||||
/>
|
/>
|
||||||
|
{/* Remove button */}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => onChange(null)}
|
onClick={handleRemove}
|
||||||
className="absolute top-1 right-1 p-1 bg-white/80 hover:bg-white rounded-full text-gray-600 hover:text-gray-900"
|
className="absolute top-2 right-2 w-7 h-7 flex items-center justify-center bg-white/80 hover:bg-white rounded-full text-gray-600 hover:text-gray-900 transition-colors shadow-sm"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
className="w-4 h-4"
|
className="w-4 h-4"
|
||||||
@@ -72,16 +85,55 @@ export function ImageUpload({ value, onChange }: ImageUploadProps) {
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="w-full h-full bg-gray-100 flex flex-col items-center justify-center">
|
||||||
|
{/* ImagePlus icon */}
|
||||||
|
<svg
|
||||||
|
className="w-10 h-10 text-gray-300 group-hover:text-gray-400 transition-colors"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
strokeWidth={1.5}
|
||||||
|
>
|
||||||
|
<rect x="3" y="3" width="18" height="18" rx="2" ry="2" />
|
||||||
|
<circle cx="9" cy="9" r="2" />
|
||||||
|
<path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21" />
|
||||||
|
<path d="M14 4v3" />
|
||||||
|
<path d="M12.5 5.5h3" />
|
||||||
|
</svg>
|
||||||
|
<span className="mt-2 text-sm text-gray-400 group-hover:text-gray-500 transition-colors">
|
||||||
|
Click to add photo
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<button
|
|
||||||
type="button"
|
{/* Upload spinner overlay */}
|
||||||
onClick={() => inputRef.current?.click()}
|
{uploading && (
|
||||||
disabled={uploading}
|
<div className="absolute inset-0 bg-white/60 flex items-center justify-center">
|
||||||
className="w-full py-2 px-3 border border-dashed border-gray-300 rounded-lg text-sm text-gray-500 hover:border-gray-400 hover:text-gray-600 transition-colors disabled:opacity-50"
|
<svg
|
||||||
|
className="w-8 h-8 text-gray-500 animate-spin"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
{uploading ? "Uploading..." : value ? "Change image" : "Add image"}
|
<circle
|
||||||
</button>
|
className="opacity-25"
|
||||||
|
cx="12"
|
||||||
|
cy="12"
|
||||||
|
r="10"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="4"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
className="opacity-75"
|
||||||
|
fill="currentColor"
|
||||||
|
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
type="file"
|
type="file"
|
||||||
|
|||||||
@@ -118,6 +118,14 @@ export function ItemForm({ mode, itemId }: ItemFormProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit} className="space-y-5">
|
<form onSubmit={handleSubmit} className="space-y-5">
|
||||||
|
{/* Image */}
|
||||||
|
<ImageUpload
|
||||||
|
value={form.imageFilename}
|
||||||
|
onChange={(filename) =>
|
||||||
|
setForm((f) => ({ ...f, imageFilename: filename }))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Name */}
|
{/* Name */}
|
||||||
<div>
|
<div>
|
||||||
<label
|
<label
|
||||||
@@ -242,19 +250,6 @@ export function ItemForm({ mode, itemId }: ItemFormProps) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Image */}
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
||||||
Image
|
|
||||||
</label>
|
|
||||||
<ImageUpload
|
|
||||||
value={form.imageFilename}
|
|
||||||
onChange={(filename) =>
|
|
||||||
setForm((f) => ({ ...f, imageFilename: filename }))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Actions */}
|
{/* Actions */}
|
||||||
<div className="flex gap-3 pt-2">
|
<div className="flex gap-3 pt-2">
|
||||||
<button
|
<button
|
||||||
|
|||||||
Reference in New Issue
Block a user