5.4.0: Картинка желания по ссылке и единая высота инпутов
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m3s
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m3s
This commit is contained in:
@@ -35,6 +35,8 @@ function WishlistForm({ onNavigate, wishlistId, editConditionIndex, newTaskId, b
|
||||
const [fetchingMetadata, setFetchingMetadata] = useState(false)
|
||||
const [restoredFromSession, setRestoredFromSession] = useState(false) // Флаг восстановления из sessionStorage
|
||||
const [loadedWishlistData, setLoadedWishlistData] = useState(null) // Данные желания для последующего маппинга условий
|
||||
const [imageUrlInput, setImageUrlInput] = useState('') // Ссылка на картинку для загрузки по URL
|
||||
const [loadingImageFromUrl, setLoadingImageFromUrl] = useState(false)
|
||||
const fileInputRef = useRef(null)
|
||||
|
||||
// Загрузка задач, проектов и саджестов групп
|
||||
@@ -237,6 +239,8 @@ function WishlistForm({ onNavigate, wishlistId, editConditionIndex, newTaskId, b
|
||||
setCrop({ x: 0, y: 0 })
|
||||
setZoom(1)
|
||||
setCroppedAreaPixels(null)
|
||||
setImageUrlInput('')
|
||||
setLoadingImageFromUrl(false)
|
||||
setShowConditionForm(false)
|
||||
setEditingConditionIndex(null)
|
||||
setToastMessage(null)
|
||||
@@ -300,6 +304,8 @@ function WishlistForm({ onNavigate, wishlistId, editConditionIndex, newTaskId, b
|
||||
setImageUrl(null)
|
||||
setImageFile(null)
|
||||
setImageRemoved(false)
|
||||
setImageUrlInput('')
|
||||
setLoadingImageFromUrl(false)
|
||||
setUnlockConditions([])
|
||||
setGroupName('')
|
||||
setError('')
|
||||
@@ -434,6 +440,53 @@ function WishlistForm({ onNavigate, wishlistId, editConditionIndex, newTaskId, b
|
||||
reader.readAsDataURL(file)
|
||||
}
|
||||
|
||||
// Загрузка картинки по ссылке с последующим кропом
|
||||
const loadImageFromUrl = async () => {
|
||||
const url = imageUrlInput?.trim()
|
||||
if (!url) {
|
||||
setToastMessage({ text: 'Введите ссылку на картинку', type: 'error' })
|
||||
return
|
||||
}
|
||||
try {
|
||||
new URL(url)
|
||||
} catch {
|
||||
setToastMessage({ text: 'Некорректная ссылка на картинку', type: 'error' })
|
||||
return
|
||||
}
|
||||
|
||||
setLoadingImageFromUrl(true)
|
||||
try {
|
||||
const proxyUrl = `${API_URL}/proxy-image?url=${encodeURIComponent(url)}`
|
||||
const imgResponse = await authFetch(proxyUrl)
|
||||
if (!imgResponse.ok) {
|
||||
const errData = await imgResponse.json().catch(() => ({}))
|
||||
throw new Error(errData.message || errData.error || 'Не удалось загрузить картинку')
|
||||
}
|
||||
const blob = await imgResponse.blob()
|
||||
if (blob.size > 5 * 1024 * 1024) {
|
||||
setToastMessage({ text: 'Картинка слишком большая (максимум 5MB)', type: 'error' })
|
||||
return
|
||||
}
|
||||
if (!blob.type.startsWith('image/')) {
|
||||
setToastMessage({ text: 'По ссылке не изображение', type: 'error' })
|
||||
return
|
||||
}
|
||||
const reader = new FileReader()
|
||||
reader.onload = () => {
|
||||
setImageUrl(reader.result)
|
||||
setImageFile(blob)
|
||||
setImageRemoved(false)
|
||||
setImageUrlInput('')
|
||||
setShowCropper(true)
|
||||
}
|
||||
reader.readAsDataURL(blob)
|
||||
} catch (err) {
|
||||
setToastMessage({ text: err.message || 'Ошибка при загрузке картинки по ссылке', type: 'error' })
|
||||
} finally {
|
||||
setLoadingImageFromUrl(false)
|
||||
}
|
||||
}
|
||||
|
||||
const onCropComplete = (croppedArea, croppedAreaPixels) => {
|
||||
setCroppedAreaPixels(croppedAreaPixels)
|
||||
}
|
||||
@@ -788,14 +841,51 @@ function WishlistForm({ onNavigate, wishlistId, editConditionIndex, newTaskId, b
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={handleImageSelect}
|
||||
className="form-input"
|
||||
style={{ display: imageUrl ? 'none' : 'block' }}
|
||||
/>
|
||||
{!imageUrl && (
|
||||
<>
|
||||
<div className="image-url-row">
|
||||
<span className="image-url-label">Файл:</span>
|
||||
<label className="file-input-label">
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={handleImageSelect}
|
||||
className="file-input-hidden"
|
||||
/>
|
||||
<span className="file-input-button">Выбрать</span>
|
||||
</label>
|
||||
</div>
|
||||
<div className="image-url-row">
|
||||
<span className="image-url-label">Ссылка:</span>
|
||||
<input
|
||||
type="url"
|
||||
value={imageUrlInput}
|
||||
onChange={(e) => setImageUrlInput(e.target.value)}
|
||||
placeholder="https://..."
|
||||
className="form-input image-url-input"
|
||||
disabled={loadingImageFromUrl}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="image-url-load-button"
|
||||
onClick={loadImageFromUrl}
|
||||
disabled={loadingImageFromUrl || !imageUrlInput.trim()}
|
||||
title="Загрузить картинку по ссылке и обрезать"
|
||||
>
|
||||
{loadingImageFromUrl ? (
|
||||
<div className="mini-spinner"></div>
|
||||
) : (
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
|
||||
<polyline points="7 10 12 15 17 10"/>
|
||||
<line x1="12" y1="15" x2="12" y2="3"/>
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{showCropper && (
|
||||
|
||||
Reference in New Issue
Block a user