6.19.8: Кнопки сохранения досок внизу экрана
Some checks failed
Build and Push Docker Image / build-and-push (push) Failing after 1m16s

This commit is contained in:
poignatov
2026-03-18 16:59:18 +03:00
parent b1f4fdd449
commit 3a06d9148c
5 changed files with 102 additions and 40 deletions

View File

@@ -1 +1 @@
6.19.7 6.19.8

View File

@@ -1,6 +1,6 @@
{ {
"name": "play-life-web", "name": "play-life-web",
"version": "6.19.7", "version": "6.19.8",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",

View File

@@ -1166,7 +1166,7 @@ function AppContent() {
} }
// Определяем, нужно ли скрывать нижнюю панель (для fullscreen экранов) // Определяем, нужно ли скрывать нижнюю панель (для fullscreen экранов)
const isFullscreenTab = activeTab === 'test' || activeTab === 'purchase' || activeTab === 'add-words' || activeTab === 'task-form' || activeTab === 'wishlist-form' || activeTab === 'wishlist-detail' || activeTab === 'todoist-integration' || activeTab === 'telegram-integration' || activeTab === 'fitbit-integration' || activeTab === 'full' || activeTab === 'priorities' || activeTab === 'words' || activeTab === 'dictionaries' || activeTab === 'tracking' || activeTab === 'tracking-access' || activeTab === 'tracking-invite' || activeTab === 'shopping' || activeTab === 'shopping-item-form' || activeTab === 'shopping-board-form' || activeTab === 'shopping-board-join' || activeTab === 'shopping-item-history' const isFullscreenTab = activeTab === 'test' || activeTab === 'purchase' || activeTab === 'add-words' || activeTab === 'task-form' || activeTab === 'wishlist-form' || activeTab === 'wishlist-detail' || activeTab === 'todoist-integration' || activeTab === 'telegram-integration' || activeTab === 'fitbit-integration' || activeTab === 'full' || activeTab === 'priorities' || activeTab === 'words' || activeTab === 'dictionaries' || activeTab === 'tracking' || activeTab === 'tracking-access' || activeTab === 'tracking-invite' || activeTab === 'shopping' || activeTab === 'shopping-item-form' || activeTab === 'shopping-board-form' || activeTab === 'shopping-board-join' || activeTab === 'shopping-item-history' || activeTab === 'board-form'
// Функция для получения классов скролл-контейнера для каждого таба // Функция для получения классов скролл-контейнера для каждого таба
// Каждый таб имеет свой изолированный скролл-контейнер для автоматического сохранения позиции скролла // Каждый таб имеет свой изолированный скролл-контейнер для автоматического сохранения позиции скролла
@@ -1418,6 +1418,7 @@ function AppContent() {
onNavigate={handleNavigate} onNavigate={handleNavigate}
boardId={tabParams.boardId} boardId={tabParams.boardId}
onSaved={() => setWishlistRefreshTrigger(prev => prev + 1)} onSaved={() => setWishlistRefreshTrigger(prev => prev + 1)}
isActive={activeTab === 'board-form'}
/> />
</div> </div>
</div> </div>
@@ -1473,6 +1474,7 @@ function AppContent() {
onNavigate={handleNavigate} onNavigate={handleNavigate}
boardId={tabParams.boardId} boardId={tabParams.boardId}
onSaved={() => setShoppingRefreshTrigger(prev => prev + 1)} onSaved={() => setShoppingRefreshTrigger(prev => prev + 1)}
isActive={activeTab === 'shopping-board-form'}
/> />
</div> </div>
</div> </div>

View File

@@ -1,13 +1,13 @@
import React, { useState, useEffect } from 'react' import React, { useState, useEffect } from 'react'
import { createPortal } from 'react-dom'
import { useAuth } from './auth/AuthContext' import { useAuth } from './auth/AuthContext'
import BoardMembers from './BoardMembers' import BoardMembers from './BoardMembers'
import Toast from './Toast' import Toast from './Toast'
import SubmitButton from './SubmitButton'
import DeleteButton from './DeleteButton' import DeleteButton from './DeleteButton'
import './Buttons.css' import './Buttons.css'
import './BoardForm.css' import './BoardForm.css'
function BoardForm({ boardId, onNavigate, onSaved }) { function BoardForm({ boardId, onNavigate, onSaved, isActive }) {
const { authFetch } = useAuth() const { authFetch } = useAuth()
const [name, setName] = useState('') const [name, setName] = useState('')
const [inviteEnabled, setInviteEnabled] = useState(false) const [inviteEnabled, setInviteEnabled] = useState(false)
@@ -252,23 +252,6 @@ function BoardForm({ boardId, onNavigate, onSaved }) {
</> </>
)} )}
<div className="form-actions">
<SubmitButton
onClick={handleSave}
loading={loading}
disabled={!name.trim()}
>
Сохранить
</SubmitButton>
{isEdit && (
<DeleteButton
onClick={handleDelete}
loading={isDeleting}
disabled={loading}
title="Удалить доску"
/>
)}
</div>
</div> </div>
{toastMessage && ( {toastMessage && (
@@ -278,6 +261,53 @@ function BoardForm({ boardId, onNavigate, onSaved }) {
onClose={() => setToastMessage(null)} onClose={() => setToastMessage(null)}
/> />
)} )}
{isActive ? createPortal(
<div style={{
position: 'fixed',
bottom: 0,
left: 0,
right: 0,
padding: '0.75rem 1rem',
paddingBottom: 'max(0.75rem, env(safe-area-inset-bottom))',
background: 'linear-gradient(to top, white 60%, rgba(255,255,255,0))',
zIndex: 1500,
display: 'flex',
justifyContent: 'center',
gap: '0.75rem',
}}>
<button
onClick={handleSave}
disabled={loading || isDeleting || !name.trim()}
style={{
flex: 1,
maxWidth: '42rem',
padding: '0.875rem',
background: (loading || !name.trim()) ? undefined : 'linear-gradient(to right, #10b981, #059669)',
backgroundColor: (loading || !name.trim()) ? '#9ca3af' : undefined,
color: 'white',
border: 'none',
borderRadius: '0.5rem',
fontSize: '1rem',
fontWeight: 600,
cursor: (loading || isDeleting || !name.trim()) ? 'not-allowed' : 'pointer',
opacity: loading ? 0.6 : 1,
transition: 'all 0.2s',
}}
>
{loading ? 'Сохранение...' : 'Сохранить'}
</button>
{isEdit && (
<DeleteButton
onClick={handleDelete}
loading={isDeleting}
disabled={loading}
title="Удалить доску"
/>
)}
</div>,
document.body
) : null}
</div> </div>
) )
} }

View File

@@ -1,13 +1,13 @@
import React, { useState, useEffect } from 'react' import React, { useState, useEffect } from 'react'
import { createPortal } from 'react-dom'
import { useAuth } from './auth/AuthContext' import { useAuth } from './auth/AuthContext'
import BoardMembers from './BoardMembers' import BoardMembers from './BoardMembers'
import Toast from './Toast' import Toast from './Toast'
import SubmitButton from './SubmitButton'
import DeleteButton from './DeleteButton' import DeleteButton from './DeleteButton'
import './Buttons.css' import './Buttons.css'
import './BoardForm.css' import './BoardForm.css'
function ShoppingBoardForm({ boardId, onNavigate, onSaved }) { function ShoppingBoardForm({ boardId, onNavigate, onSaved, isActive }) {
const { authFetch } = useAuth() const { authFetch } = useAuth()
const [name, setName] = useState('') const [name, setName] = useState('')
const [inviteEnabled, setInviteEnabled] = useState(false) const [inviteEnabled, setInviteEnabled] = useState(false)
@@ -245,23 +245,6 @@ function ShoppingBoardForm({ boardId, onNavigate, onSaved }) {
</> </>
)} )}
<div className="form-actions">
<SubmitButton
onClick={handleSave}
loading={loading}
disabled={!name.trim()}
>
Сохранить
</SubmitButton>
{isEdit && (
<DeleteButton
onClick={handleDelete}
loading={isDeleting}
disabled={loading}
title="Удалить доску"
/>
)}
</div>
</div> </div>
{toastMessage && ( {toastMessage && (
@@ -271,6 +254,53 @@ function ShoppingBoardForm({ boardId, onNavigate, onSaved }) {
onClose={() => setToastMessage(null)} onClose={() => setToastMessage(null)}
/> />
)} )}
{isActive ? createPortal(
<div style={{
position: 'fixed',
bottom: 0,
left: 0,
right: 0,
padding: '0.75rem 1rem',
paddingBottom: 'max(0.75rem, env(safe-area-inset-bottom))',
background: 'linear-gradient(to top, white 60%, rgba(255,255,255,0))',
zIndex: 1500,
display: 'flex',
justifyContent: 'center',
gap: '0.75rem',
}}>
<button
onClick={handleSave}
disabled={loading || isDeleting || !name.trim()}
style={{
flex: 1,
maxWidth: '42rem',
padding: '0.875rem',
background: (loading || !name.trim()) ? undefined : 'linear-gradient(to right, #10b981, #059669)',
backgroundColor: (loading || !name.trim()) ? '#9ca3af' : undefined,
color: 'white',
border: 'none',
borderRadius: '0.5rem',
fontSize: '1rem',
fontWeight: 600,
cursor: (loading || isDeleting || !name.trim()) ? 'not-allowed' : 'pointer',
opacity: loading ? 0.6 : 1,
transition: 'all 0.2s',
}}
>
{loading ? 'Сохранение...' : 'Сохранить'}
</button>
{isEdit && (
<DeleteButton
onClick={handleDelete}
loading={isDeleting}
disabled={loading}
title="Удалить доску"
/>
)}
</div>,
document.body
) : null}
</div> </div>
) )
} }