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:
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "play-life-web",
|
"name": "play-life-web",
|
||||||
"version": "5.3.4",
|
"version": "5.4.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -71,6 +71,14 @@
|
|||||||
box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.1);
|
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 {
|
.image-preview {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -108,6 +116,105 @@
|
|||||||
background: rgba(192, 57, 43, 1);
|
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 {
|
.cropper-modal {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
@@ -476,7 +583,9 @@
|
|||||||
|
|
||||||
.date-selector-display-date {
|
.date-selector-display-date {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 0.75rem;
|
height: 2.75rem;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 0 0.75rem;
|
||||||
border: 1px solid #d1d5db;
|
border: 1px solid #d1d5db;
|
||||||
border-radius: 0.375rem;
|
border-radius: 0.375rem;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
@@ -485,6 +594,8 @@
|
|||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
color: #1f2937;
|
color: #1f2937;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.date-selector-display-date:hover {
|
.date-selector-display-date:hover {
|
||||||
@@ -540,7 +651,9 @@
|
|||||||
|
|
||||||
.task-autocomplete-input {
|
.task-autocomplete-input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 12px 36px 12px 14px;
|
height: 2.75rem;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 0 36px 0 14px;
|
||||||
border: 1px solid #d1d5db;
|
border: 1px solid #d1d5db;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ function WishlistForm({ onNavigate, wishlistId, editConditionIndex, newTaskId, b
|
|||||||
const [fetchingMetadata, setFetchingMetadata] = useState(false)
|
const [fetchingMetadata, setFetchingMetadata] = useState(false)
|
||||||
const [restoredFromSession, setRestoredFromSession] = useState(false) // Флаг восстановления из sessionStorage
|
const [restoredFromSession, setRestoredFromSession] = useState(false) // Флаг восстановления из sessionStorage
|
||||||
const [loadedWishlistData, setLoadedWishlistData] = useState(null) // Данные желания для последующего маппинга условий
|
const [loadedWishlistData, setLoadedWishlistData] = useState(null) // Данные желания для последующего маппинга условий
|
||||||
|
const [imageUrlInput, setImageUrlInput] = useState('') // Ссылка на картинку для загрузки по URL
|
||||||
|
const [loadingImageFromUrl, setLoadingImageFromUrl] = useState(false)
|
||||||
const fileInputRef = useRef(null)
|
const fileInputRef = useRef(null)
|
||||||
|
|
||||||
// Загрузка задач, проектов и саджестов групп
|
// Загрузка задач, проектов и саджестов групп
|
||||||
@@ -237,6 +239,8 @@ function WishlistForm({ onNavigate, wishlistId, editConditionIndex, newTaskId, b
|
|||||||
setCrop({ x: 0, y: 0 })
|
setCrop({ x: 0, y: 0 })
|
||||||
setZoom(1)
|
setZoom(1)
|
||||||
setCroppedAreaPixels(null)
|
setCroppedAreaPixels(null)
|
||||||
|
setImageUrlInput('')
|
||||||
|
setLoadingImageFromUrl(false)
|
||||||
setShowConditionForm(false)
|
setShowConditionForm(false)
|
||||||
setEditingConditionIndex(null)
|
setEditingConditionIndex(null)
|
||||||
setToastMessage(null)
|
setToastMessage(null)
|
||||||
@@ -300,6 +304,8 @@ function WishlistForm({ onNavigate, wishlistId, editConditionIndex, newTaskId, b
|
|||||||
setImageUrl(null)
|
setImageUrl(null)
|
||||||
setImageFile(null)
|
setImageFile(null)
|
||||||
setImageRemoved(false)
|
setImageRemoved(false)
|
||||||
|
setImageUrlInput('')
|
||||||
|
setLoadingImageFromUrl(false)
|
||||||
setUnlockConditions([])
|
setUnlockConditions([])
|
||||||
setGroupName('')
|
setGroupName('')
|
||||||
setError('')
|
setError('')
|
||||||
@@ -434,6 +440,53 @@ function WishlistForm({ onNavigate, wishlistId, editConditionIndex, newTaskId, b
|
|||||||
reader.readAsDataURL(file)
|
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) => {
|
const onCropComplete = (croppedArea, croppedAreaPixels) => {
|
||||||
setCroppedAreaPixels(croppedAreaPixels)
|
setCroppedAreaPixels(croppedAreaPixels)
|
||||||
}
|
}
|
||||||
@@ -788,14 +841,51 @@ function WishlistForm({ onNavigate, wishlistId, editConditionIndex, newTaskId, b
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<input
|
{!imageUrl && (
|
||||||
ref={fileInputRef}
|
<>
|
||||||
type="file"
|
<div className="image-url-row">
|
||||||
accept="image/*"
|
<span className="image-url-label">Файл:</span>
|
||||||
onChange={handleImageSelect}
|
<label className="file-input-label">
|
||||||
className="form-input"
|
<input
|
||||||
style={{ display: imageUrl ? 'none' : 'block' }}
|
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>
|
</div>
|
||||||
|
|
||||||
{showCropper && (
|
{showCropper && (
|
||||||
|
|||||||
Reference in New Issue
Block a user