diff --git a/VERSION b/VERSION index e0a61e6..8a30e8f 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -5.3.4 +5.4.0 diff --git a/play-life-web/package.json b/play-life-web/package.json index 39a3005..1d0c85c 100644 --- a/play-life-web/package.json +++ b/play-life-web/package.json @@ -1,6 +1,6 @@ { "name": "play-life-web", - "version": "5.3.4", + "version": "5.4.0", "type": "module", "scripts": { "dev": "vite", diff --git a/play-life-web/src/components/WishlistForm.css b/play-life-web/src/components/WishlistForm.css index dd07f28..1c1c58c 100644 --- a/play-life-web/src/components/WishlistForm.css +++ b/play-life-web/src/components/WishlistForm.css @@ -71,6 +71,14 @@ box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.1); } +/* Одинаковая высота всех инпутов на экране */ +.wishlist-form .form-input { + height: 2.75rem; + box-sizing: border-box; + padding-top: 0; + padding-bottom: 0; +} + .image-preview { position: relative; width: 100%; @@ -108,6 +116,105 @@ background: rgba(192, 57, 43, 1); } +.image-url-row { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.5rem; + margin-top: 0.75rem; +} + +.image-url-row + .image-url-row { + margin-top: 0.25rem; +} + +.image-url-label { + font-size: 0.9rem; + color: #6b7280; + flex-shrink: 0; + width: 3.5rem; + display: inline-block; +} + +.image-url-input { + flex: 1; + min-width: 120px; +} + +.wishlist-form .file-input-label { + height: 2.75rem; + box-sizing: border-box; +} + +.file-input-label { + flex: 1; + position: relative; + display: flex; + align-items: stretch; + justify-content: flex-start; + padding: 0 0.75rem; + border: 1px solid #d1d5db; + border-radius: 0.375rem; + cursor: pointer; + transition: border-color 0.2s; +} + +.file-input-label:hover { + border-color: #3498db; +} + +.file-input-hidden { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + opacity: 0; + cursor: pointer; +} + +.file-input-button { + font-size: 1rem; + line-height: 1; + color: #9ca3af; + pointer-events: none; + display: flex; + align-items: center; + justify-content: flex-start; + height: 100%; +} + +.image-url-load-button { + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; + width: 44px; + height: 44px; + padding: 0; + background: #3498db; + color: white; + border: none; + border-radius: 0.375rem; + cursor: pointer; + transition: all 0.2s; +} + +.image-url-load-button svg { + width: 20px; + height: 20px; +} + +.image-url-load-button:hover:not(:disabled) { + background: #2980b9; + transform: translateY(-1px); +} + +.image-url-load-button:disabled { + background: #9ca3af; + cursor: not-allowed; + transform: none; +} + .cropper-modal { position: fixed; top: 0; @@ -476,7 +583,9 @@ .date-selector-display-date { flex: 1; - padding: 0.75rem; + height: 2.75rem; + box-sizing: border-box; + padding: 0 0.75rem; border: 1px solid #d1d5db; border-radius: 0.375rem; font-size: 1rem; @@ -485,6 +594,8 @@ transition: all 0.2s; color: #1f2937; user-select: none; + display: flex; + align-items: center; } .date-selector-display-date:hover { @@ -540,7 +651,9 @@ .task-autocomplete-input { width: 100%; - padding: 12px 36px 12px 14px; + height: 2.75rem; + box-sizing: border-box; + padding: 0 36px 0 14px; border: 1px solid #d1d5db; border-radius: 8px; font-size: 14px; diff --git a/play-life-web/src/components/WishlistForm.jsx b/play-life-web/src/components/WishlistForm.jsx index ea7bd9d..a343e03 100644 --- a/play-life-web/src/components/WishlistForm.jsx +++ b/play-life-web/src/components/WishlistForm.jsx @@ -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 )} - + {!imageUrl && ( + <> +