feat: improved navigation and unified close buttons - version 3.5.0
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 47s
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 47s
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "play-life-web",
|
||||
"version": "3.3.1",
|
||||
"version": "3.5.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -10,6 +10,8 @@ import TestWords from './components/TestWords'
|
||||
import Profile from './components/Profile'
|
||||
import TaskList from './components/TaskList'
|
||||
import TaskForm from './components/TaskForm.jsx'
|
||||
import TodoistIntegration from './components/TodoistIntegration'
|
||||
import TelegramIntegration from './components/TelegramIntegration'
|
||||
import { AuthProvider, useAuth } from './components/auth/AuthContext'
|
||||
import AuthScreen from './components/auth/AuthScreen'
|
||||
|
||||
@@ -17,6 +19,10 @@ import AuthScreen from './components/auth/AuthScreen'
|
||||
const CURRENT_WEEK_API_URL = '/playlife-feed'
|
||||
const FULL_STATISTICS_API_URL = '/d2dc349a-0d13-49b2-a8f0-1ab094bfba9b'
|
||||
|
||||
// Определяем основные табы (без крестика) и глубокие табы (с крестиком)
|
||||
const mainTabs = ['current', 'test-config', 'tasks', 'profile']
|
||||
const deepTabs = ['add-words', 'add-config', 'test', 'task-form', 'words', 'todoist-integration', 'telegram-integration', 'full', 'priorities']
|
||||
|
||||
function AppContent() {
|
||||
const { authFetch, isAuthenticated, loading: authLoading } = useAuth()
|
||||
|
||||
@@ -47,6 +53,8 @@ function AppContent() {
|
||||
tasks: false,
|
||||
'task-form': false,
|
||||
profile: false,
|
||||
'todoist-integration': false,
|
||||
'telegram-integration': false,
|
||||
})
|
||||
|
||||
// Отслеживаем, какие табы уже были загружены (для предотвращения повторных загрузок)
|
||||
@@ -62,6 +70,8 @@ function AppContent() {
|
||||
tasks: false,
|
||||
'task-form': false,
|
||||
profile: false,
|
||||
'todoist-integration': false,
|
||||
'telegram-integration': false,
|
||||
})
|
||||
|
||||
// Параметры для навигации между вкладками
|
||||
@@ -98,21 +108,59 @@ function AppContent() {
|
||||
// Восстанавливаем последний выбранный таб после перезагрузки
|
||||
const [isInitialized, setIsInitialized] = useState(false)
|
||||
|
||||
// Инициализация из URL (только для глубоких табов) или localStorage
|
||||
useEffect(() => {
|
||||
if (isInitialized) return
|
||||
|
||||
try {
|
||||
// Проверяем URL только для глубоких табов
|
||||
const urlParams = new URLSearchParams(window.location.search)
|
||||
const tabFromUrl = urlParams.get('tab')
|
||||
const validTabs = ['current', 'priorities', 'full', 'words', 'add-words', 'test-config', 'add-config', 'test', 'tasks', 'task-form', 'profile', 'todoist-integration', 'telegram-integration']
|
||||
|
||||
if (tabFromUrl && validTabs.includes(tabFromUrl) && deepTabs.includes(tabFromUrl)) {
|
||||
// Если в URL есть глубокий таб, восстанавливаем его
|
||||
setActiveTab(tabFromUrl)
|
||||
setLoadedTabs(prev => ({ ...prev, [tabFromUrl]: true }))
|
||||
|
||||
// Восстанавливаем параметры из URL
|
||||
const params = {}
|
||||
urlParams.forEach((value, key) => {
|
||||
if (key !== 'tab') {
|
||||
try {
|
||||
params[key] = JSON.parse(value)
|
||||
} catch {
|
||||
params[key] = value
|
||||
}
|
||||
}
|
||||
})
|
||||
if (Object.keys(params).length > 0) {
|
||||
setTabParams(params)
|
||||
// Если это экран full с selectedProject, восстанавливаем его
|
||||
if (tabFromUrl === 'full' && params.selectedProject) {
|
||||
setSelectedProject(params.selectedProject)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Если в URL нет глубокого таба, проверяем localStorage для основного таба
|
||||
const savedTab = window.localStorage?.getItem('activeTab')
|
||||
const validTabs = ['current', 'priorities', 'full', 'words', 'add-words', 'test-config', 'add-config', 'test', 'tasks', 'task-form', 'profile']
|
||||
if (savedTab && validTabs.includes(savedTab)) {
|
||||
setActiveTab(savedTab)
|
||||
setLoadedTabs(prev => ({ ...prev, [savedTab]: true }))
|
||||
setIsInitialized(true)
|
||||
} else {
|
||||
setIsInitialized(true)
|
||||
}
|
||||
// Очищаем URL от параметров таба, если это основной таб
|
||||
if (tabFromUrl && mainTabs.includes(tabFromUrl)) {
|
||||
const url = new URL(window.location)
|
||||
url.searchParams.delete('tab')
|
||||
url.searchParams.forEach((value, key) => {
|
||||
url.searchParams.delete(key)
|
||||
})
|
||||
window.history.replaceState({}, '', url)
|
||||
}
|
||||
}
|
||||
setIsInitialized(true)
|
||||
} catch (err) {
|
||||
console.warn('Не удалось прочитать активный таб из localStorage', err)
|
||||
console.warn('Не удалось прочитать активный таб', err)
|
||||
setIsInitialized(true)
|
||||
}
|
||||
}, [isInitialized])
|
||||
@@ -121,6 +169,91 @@ function AppContent() {
|
||||
setLoadedTabs(prev => (prev[tab] ? prev : { ...prev, [tab]: true }))
|
||||
}, [])
|
||||
|
||||
// Функция для обновления URL (только для глубоких табов)
|
||||
const updateUrl = useCallback((tab, params = {}, previousTab = null) => {
|
||||
if (!deepTabs.includes(tab)) {
|
||||
// Для основных табов не обновляем URL
|
||||
return
|
||||
}
|
||||
|
||||
const url = new URL(window.location)
|
||||
url.searchParams.set('tab', tab)
|
||||
|
||||
// Удаляем старые параметры таба
|
||||
const keysToRemove = []
|
||||
url.searchParams.forEach((value, key) => {
|
||||
if (key !== 'tab') {
|
||||
keysToRemove.push(key)
|
||||
}
|
||||
})
|
||||
keysToRemove.forEach(key => url.searchParams.delete(key))
|
||||
|
||||
// Добавляем новые параметры
|
||||
Object.entries(params).forEach(([key, value]) => {
|
||||
if (value !== undefined && value !== null) {
|
||||
url.searchParams.set(key, typeof value === 'object' ? JSON.stringify(value) : value)
|
||||
}
|
||||
})
|
||||
|
||||
// Сохраняем предыдущий таб в state для восстановления при "Назад"
|
||||
window.history.pushState({ tab, params, previousTab }, '', url)
|
||||
}, []) // deepTabs - константа, не нужно в зависимостях
|
||||
|
||||
// Функция для очистки URL (при возврате к основному табу)
|
||||
const clearUrl = useCallback((tab = null, usePushState = false) => {
|
||||
const url = new URL(window.location)
|
||||
const hasTabParam = url.searchParams.has('tab')
|
||||
if (hasTabParam) {
|
||||
url.searchParams.delete('tab')
|
||||
url.searchParams.forEach((value, key) => {
|
||||
url.searchParams.delete(key)
|
||||
})
|
||||
// Сохраняем текущий таб в state для восстановления при "Назад"
|
||||
if (usePushState && tab) {
|
||||
window.history.pushState({ tab }, '', url)
|
||||
} else {
|
||||
window.history.replaceState(tab ? { tab } : {}, '', url)
|
||||
}
|
||||
} else if (tab) {
|
||||
// Если URL уже чистый, но нужно сохранить state таба
|
||||
if (usePushState) {
|
||||
window.history.pushState({ tab }, '', url)
|
||||
} else {
|
||||
window.history.replaceState({ tab }, '', url)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Функция для обновления URL без создания новой записи в истории (для обновления параметров того же таба)
|
||||
const replaceUrl = useCallback((tab, params = {}) => {
|
||||
if (!deepTabs.includes(tab)) {
|
||||
return
|
||||
}
|
||||
|
||||
const url = new URL(window.location)
|
||||
url.searchParams.set('tab', tab)
|
||||
|
||||
// Удаляем старые параметры таба
|
||||
const keysToRemove = []
|
||||
url.searchParams.forEach((value, key) => {
|
||||
if (key !== 'tab') {
|
||||
keysToRemove.push(key)
|
||||
}
|
||||
})
|
||||
keysToRemove.forEach(key => url.searchParams.delete(key))
|
||||
|
||||
// Добавляем новые параметры
|
||||
Object.entries(params).forEach(([key, value]) => {
|
||||
if (value !== undefined && value !== null) {
|
||||
url.searchParams.set(key, typeof value === 'object' ? JSON.stringify(value) : value)
|
||||
}
|
||||
})
|
||||
|
||||
// Сохраняем текущий state, чтобы не потерять previousTab
|
||||
const currentState = window.history.state || {}
|
||||
window.history.replaceState({ ...currentState, tab, params }, '', url)
|
||||
}, [])
|
||||
|
||||
const fetchCurrentWeekData = useCallback(async (isBackground = false) => {
|
||||
try {
|
||||
if (isBackground) {
|
||||
@@ -240,6 +373,8 @@ function AppContent() {
|
||||
tasks: false,
|
||||
'task-form': false,
|
||||
profile: false,
|
||||
'todoist-integration': false,
|
||||
'telegram-integration': false,
|
||||
})
|
||||
|
||||
// Используем ref для отслеживания кеша (чтобы не зависеть от состояния в useCallback)
|
||||
@@ -350,6 +485,82 @@ function AppContent() {
|
||||
setIsRefreshing(false)
|
||||
}, [fetchCurrentWeekData, fetchFullStatisticsData])
|
||||
|
||||
// Обработчик кнопки "назад" в браузере (только для глубоких табов)
|
||||
useEffect(() => {
|
||||
const handlePopState = (event) => {
|
||||
const validTabs = ['current', 'priorities', 'full', 'words', 'add-words', 'test-config', 'add-config', 'test', 'tasks', 'task-form', 'profile', 'todoist-integration', 'telegram-integration']
|
||||
|
||||
// Проверяем state текущей записи истории (куда мы вернулись)
|
||||
if (event.state && event.state.tab) {
|
||||
const { tab, params = {} } = event.state
|
||||
|
||||
if (validTabs.includes(tab)) {
|
||||
setActiveTab(tab)
|
||||
setTabParams(params)
|
||||
markTabAsLoaded(tab)
|
||||
// Если это экран full с selectedProject, восстанавливаем его
|
||||
if (tab === 'full' && params.selectedProject) {
|
||||
setSelectedProject(params.selectedProject)
|
||||
} else if (tab === 'full') {
|
||||
setSelectedProject(null)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Если state пустой или не содержит таб, пытаемся восстановить из URL
|
||||
const urlParams = new URLSearchParams(window.location.search)
|
||||
const tabFromUrl = urlParams.get('tab')
|
||||
|
||||
if (tabFromUrl && validTabs.includes(tabFromUrl) && deepTabs.includes(tabFromUrl)) {
|
||||
// Если в URL есть глубокий таб, восстанавливаем его
|
||||
setActiveTab(tabFromUrl)
|
||||
markTabAsLoaded(tabFromUrl)
|
||||
|
||||
const params = {}
|
||||
urlParams.forEach((value, key) => {
|
||||
if (key !== 'tab') {
|
||||
try {
|
||||
params[key] = JSON.parse(value)
|
||||
} catch {
|
||||
params[key] = value
|
||||
}
|
||||
}
|
||||
})
|
||||
setTabParams(params)
|
||||
// Если это экран full с selectedProject, восстанавливаем его
|
||||
if (tabFromUrl === 'full' && params.selectedProject) {
|
||||
setSelectedProject(params.selectedProject)
|
||||
}
|
||||
} else {
|
||||
// Если в URL нет глубокого таба, значит мы вернулись на основной таб
|
||||
// Проверяем state - если там есть tab, используем его
|
||||
if (event.state && event.state.tab && validTabs.includes(event.state.tab)) {
|
||||
setActiveTab(event.state.tab)
|
||||
setTabParams({})
|
||||
markTabAsLoaded(event.state.tab)
|
||||
setSelectedProject(null)
|
||||
clearUrl(event.state.tab)
|
||||
} else {
|
||||
// Если state пустой, используем сохраненный таб из localStorage
|
||||
const savedTab = window.localStorage?.getItem('activeTab')
|
||||
const validMainTab = savedTab && validTabs.includes(savedTab) ? savedTab : 'current'
|
||||
setActiveTab(validMainTab)
|
||||
setTabParams({})
|
||||
markTabAsLoaded(validMainTab)
|
||||
setSelectedProject(null)
|
||||
clearUrl(validMainTab)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('popstate', handlePopState)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('popstate', handlePopState)
|
||||
}
|
||||
}, [markTabAsLoaded, clearUrl]) // mainTabs и deepTabs - константы, не нужно в зависимостях
|
||||
|
||||
// Обновляем данные при возвращении экрана в фокус (фоново)
|
||||
useEffect(() => {
|
||||
const handleFocus = () => {
|
||||
@@ -371,6 +582,8 @@ function AppContent() {
|
||||
const handleProjectClick = (projectName) => {
|
||||
setSelectedProject(projectName)
|
||||
markTabAsLoaded('full')
|
||||
setTabParams({ selectedProject: projectName })
|
||||
updateUrl('full', { selectedProject: projectName }, activeTab)
|
||||
setActiveTab('full')
|
||||
}
|
||||
|
||||
@@ -378,20 +591,51 @@ function AppContent() {
|
||||
if (tab === 'full' && activeTab === 'full') {
|
||||
// При повторном клике на "Полная статистика" сбрасываем выбранный проект
|
||||
setSelectedProject(null)
|
||||
setTabParams({})
|
||||
updateUrl('full', {}, activeTab)
|
||||
} else if (tab !== activeTab || tab === 'task-form') {
|
||||
// Для task-form всегда обновляем параметры, даже если это тот же таб
|
||||
markTabAsLoaded(tab)
|
||||
|
||||
// Определяем, является ли текущий таб глубоким
|
||||
const isCurrentTabDeep = deepTabs.includes(activeTab)
|
||||
const isNewTabDeep = deepTabs.includes(tab)
|
||||
const isCurrentTabMain = mainTabs.includes(activeTab)
|
||||
const isNewTabMain = mainTabs.includes(tab)
|
||||
|
||||
// Сбрасываем tabParams при переходе с add-config на другой таб
|
||||
if (activeTab === 'add-config' && tab !== 'add-config') {
|
||||
setTabParams({})
|
||||
if (isNewTabMain) {
|
||||
clearUrl()
|
||||
} else if (isNewTabDeep) {
|
||||
updateUrl(tab, {}, activeTab)
|
||||
}
|
||||
} else {
|
||||
// Для task-form явно удаляем taskId, если он undefined
|
||||
if (tab === 'task-form' && params.taskId === undefined) {
|
||||
setTabParams({})
|
||||
if (isNewTabMain) {
|
||||
clearUrl()
|
||||
} else if (isNewTabDeep) {
|
||||
updateUrl(tab, {}, activeTab)
|
||||
}
|
||||
} else {
|
||||
setTabParams(params)
|
||||
// Обновляем URL только для глубоких табов
|
||||
if (isNewTabDeep) {
|
||||
// Сохраняем текущий таб как предыдущий при переходе на глубокий таб
|
||||
updateUrl(tab, params, activeTab)
|
||||
} else if (isNewTabMain && isCurrentTabDeep) {
|
||||
// При переходе с глубокого таба на основной - очищаем URL и сохраняем таб в state
|
||||
clearUrl(tab)
|
||||
} else if (isNewTabMain && isCurrentTabMain) {
|
||||
// При переходе между основными табами - сохраняем таб в state без изменения URL, создаем новую запись в истории
|
||||
clearUrl(tab, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setActiveTab(tab)
|
||||
if (tab === 'current') {
|
||||
setSelectedProject(null)
|
||||
@@ -457,12 +701,25 @@ function AppContent() {
|
||||
}, [activeTab])
|
||||
|
||||
// Определяем, нужно ли скрывать нижнюю панель (для fullscreen экранов)
|
||||
const isFullscreenTab = activeTab === 'test' || activeTab === 'add-words' || activeTab === 'add-config' || activeTab === 'task-form'
|
||||
const isFullscreenTab = activeTab === 'test' || activeTab === 'add-words' || activeTab === 'add-config' || activeTab === 'task-form' || activeTab === 'todoist-integration' || activeTab === 'telegram-integration' || activeTab === 'full' || activeTab === 'priorities'
|
||||
|
||||
// Определяем отступы для контейнера
|
||||
const getContainerPadding = () => {
|
||||
if (!isFullscreenTab) {
|
||||
return 'p-4 md:p-6'
|
||||
}
|
||||
// Для экрана статистики добавляем горизонтальные отступы
|
||||
if (activeTab === 'full') {
|
||||
return 'px-4 md:px-6 py-0'
|
||||
}
|
||||
// Для остальных fullscreen экранов без отступов
|
||||
return 'p-0'
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col min-h-screen min-h-dvh">
|
||||
<div className={`flex-1 ${isFullscreenTab ? 'pb-0' : 'pb-20'}`}>
|
||||
<div className={`max-w-7xl mx-auto ${isFullscreenTab ? 'p-0' : 'p-4 md:p-6'}`}>
|
||||
<div className={`max-w-7xl mx-auto ${getContainerPadding()}`}>
|
||||
{loadedTabs.current && (
|
||||
<div className={activeTab === 'current' ? 'block' : 'hidden'}>
|
||||
<CurrentWeek
|
||||
@@ -495,7 +752,11 @@ function AppContent() {
|
||||
<div className={activeTab === 'full' ? 'block' : 'hidden'}>
|
||||
<FullStatistics
|
||||
selectedProject={selectedProject}
|
||||
onClearSelection={() => setSelectedProject(null)}
|
||||
onClearSelection={() => {
|
||||
setSelectedProject(null)
|
||||
setTabParams({})
|
||||
replaceUrl('full', {})
|
||||
}}
|
||||
data={fullStatisticsData}
|
||||
loading={fullStatisticsLoading}
|
||||
error={fullStatisticsError}
|
||||
@@ -584,6 +845,18 @@ function AppContent() {
|
||||
<Profile onNavigate={handleNavigate} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loadedTabs['todoist-integration'] && (
|
||||
<div className={activeTab === 'todoist-integration' ? 'block' : 'hidden'}>
|
||||
<TodoistIntegration onNavigate={handleNavigate} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loadedTabs['telegram-integration'] && (
|
||||
<div className={activeTab === 'telegram-integration' ? 'block' : 'hidden'}>
|
||||
<TelegramIntegration onNavigate={handleNavigate} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
import { Line } from 'react-chartjs-2'
|
||||
import WeekProgressChart from './WeekProgressChart'
|
||||
import { getAllProjectsSorted, getProjectColor, sortProjectsLikeCurrentWeek } from '../utils/projectUtils'
|
||||
import './Integrations.css'
|
||||
|
||||
// Экспортируем для обратной совместимости (если используется в других местах)
|
||||
export { getProjectColorByIndex } from '../utils/projectUtils'
|
||||
@@ -216,18 +217,13 @@ function FullStatistics({ selectedProject, onClearSelection, data, loading, erro
|
||||
return (
|
||||
<div>
|
||||
{onNavigate && (
|
||||
<div className="flex justify-end mb-4">
|
||||
<button
|
||||
onClick={() => onNavigate('current')}
|
||||
className="flex items-center justify-center w-10 h-10 rounded-full bg-white hover:bg-gray-100 text-gray-600 hover:text-gray-800 border border-gray-200 hover:border-gray-300 transition-all duration-200 shadow-sm hover:shadow-md"
|
||||
className="close-x-button"
|
||||
title="Закрыть"
|
||||
>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||
</svg>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<div style={{ height: '550px' }}>
|
||||
<Line data={chartData} options={chartOptions} />
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
import React, { useState } from 'react'
|
||||
import React from 'react'
|
||||
import { useAuth } from './auth/AuthContext'
|
||||
import TodoistIntegration from './TodoistIntegration'
|
||||
import TelegramIntegration from './TelegramIntegration'
|
||||
|
||||
function Profile({ onNavigate }) {
|
||||
const { user, logout } = useAuth()
|
||||
const [selectedIntegration, setSelectedIntegration] = useState(null)
|
||||
|
||||
const integrations = [
|
||||
{ id: 'todoist', name: 'TODOist' },
|
||||
{ id: 'telegram', name: 'Telegram' },
|
||||
{ id: 'todoist-integration', name: 'TODOist' },
|
||||
{ id: 'telegram-integration', name: 'Telegram' },
|
||||
]
|
||||
|
||||
const handleLogout = async () => {
|
||||
@@ -18,14 +15,6 @@ function Profile({ onNavigate }) {
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedIntegration) {
|
||||
if (selectedIntegration === 'todoist') {
|
||||
return <TodoistIntegration onBack={() => setSelectedIntegration(null)} />
|
||||
} else if (selectedIntegration === 'telegram') {
|
||||
return <TelegramIntegration onBack={() => setSelectedIntegration(null)} />
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-4 md:p-6 max-w-2xl mx-auto">
|
||||
{/* Profile Header */}
|
||||
@@ -52,7 +41,7 @@ function Profile({ onNavigate }) {
|
||||
{integrations.map((integration) => (
|
||||
<button
|
||||
key={integration.id}
|
||||
onClick={() => setSelectedIntegration(integration.id)}
|
||||
onClick={() => onNavigate?.(integration.id)}
|
||||
className="w-full p-4 bg-white rounded-xl shadow-sm hover:shadow-md transition-all text-left border border-gray-100 hover:border-indigo-200 group"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
import { CSS } from '@dnd-kit/utilities'
|
||||
import { getAllProjectsSorted, getProjectColor } from '../utils/projectUtils'
|
||||
import { useAuth } from './auth/AuthContext'
|
||||
import './Integrations.css'
|
||||
|
||||
// API endpoints (используем относительные пути, проксирование настроено в nginx/vite)
|
||||
const PROJECTS_API_URL = '/projects'
|
||||
@@ -866,18 +867,13 @@ function ProjectPriorityManager({ allProjectsData, currentWeekData, shouldLoad,
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto flex flex-col max-h-[calc(100vh-11rem)]">
|
||||
{onNavigate && (
|
||||
<div className="flex justify-end mb-4 flex-shrink-0">
|
||||
<button
|
||||
onClick={() => onNavigate('current')}
|
||||
className="flex items-center justify-center w-10 h-10 rounded-full bg-white hover:bg-gray-100 text-gray-600 hover:text-gray-800 border border-gray-200 hover:border-gray-300 transition-all duration-200 shadow-sm hover:shadow-md"
|
||||
className="close-x-button"
|
||||
title="Закрыть"
|
||||
>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||
</svg>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{projectsError && (!maxPriority.length && !mediumPriority.length && !lowPriority.length) && (
|
||||
<div className="mb-4 rounded-lg border border-red-200 bg-red-50 p-4 text-sm text-red-700 shadow-sm flex-shrink-0">
|
||||
|
||||
@@ -6,26 +6,28 @@
|
||||
}
|
||||
|
||||
.close-x-button {
|
||||
position: absolute;
|
||||
position: fixed;
|
||||
top: 1rem;
|
||||
right: 1rem;
|
||||
background: #f3f4f6;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 50%;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
border: none;
|
||||
font-size: 1.5rem;
|
||||
color: #7f8c8d;
|
||||
cursor: pointer;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
font-size: 1.25rem;
|
||||
color: #6b7280;
|
||||
transition: all 0.2s;
|
||||
border-radius: 50%;
|
||||
transition: background-color 0.2s, color 0.2s;
|
||||
z-index: 1600;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.close-x-button:hover {
|
||||
background: #e5e7eb;
|
||||
color: #1f2937;
|
||||
background-color: #ffffff;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.task-form h2 {
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react'
|
||||
import { useAuth } from './auth/AuthContext'
|
||||
import './Integrations.css'
|
||||
|
||||
function TelegramIntegration({ onBack }) {
|
||||
function TelegramIntegration({ onNavigate }) {
|
||||
const { authFetch } = useAuth()
|
||||
const [integration, setIntegration] = useState(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
@@ -49,7 +49,7 @@ function TelegramIntegration({ onBack }) {
|
||||
|
||||
return (
|
||||
<div className="p-4 md:p-6">
|
||||
<button className="close-x-button" onClick={onBack} title="Закрыть">
|
||||
<button className="close-x-button" onClick={() => onNavigate?.('profile')} title="Закрыть">
|
||||
✕
|
||||
</button>
|
||||
|
||||
|
||||
@@ -16,30 +16,6 @@
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.test-close-x-button {
|
||||
position: fixed;
|
||||
top: 1rem;
|
||||
right: 1rem;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
border: none;
|
||||
font-size: 1.5rem;
|
||||
color: #7f8c8d;
|
||||
cursor: pointer;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
transition: background-color 0.2s, color 0.2s;
|
||||
z-index: 1600;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.test-close-x-button:hover {
|
||||
background-color: #ffffff;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.test-duration-selection {
|
||||
text-align: center;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useState, useEffect, useRef } from 'react'
|
||||
import { useAuth } from './auth/AuthContext'
|
||||
import './TestWords.css'
|
||||
import './Integrations.css'
|
||||
|
||||
const API_URL = '/api'
|
||||
|
||||
@@ -436,7 +437,7 @@ function TestWords({ onNavigate, wordCount: initialWordCount, configId: initialC
|
||||
|
||||
return (
|
||||
<div className="test-container test-container-fullscreen">
|
||||
<button className="test-close-x-button" onClick={handleClose}>
|
||||
<button className="close-x-button" onClick={handleClose}>
|
||||
✕
|
||||
</button>
|
||||
{showPreview ? (
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react'
|
||||
import { useAuth } from './auth/AuthContext'
|
||||
import './Integrations.css'
|
||||
|
||||
function TodoistIntegration({ onBack }) {
|
||||
function TodoistIntegration({ onNavigate }) {
|
||||
const { authFetch } = useAuth()
|
||||
const [connected, setConnected] = useState(false)
|
||||
const [todoistEmail, setTodoistEmail] = useState('')
|
||||
@@ -102,7 +102,7 @@ function TodoistIntegration({ onBack }) {
|
||||
|
||||
return (
|
||||
<div className="p-4 md:p-6">
|
||||
<button className="close-x-button" onClick={onBack} title="Закрыть">
|
||||
<button className="close-x-button" onClick={() => onNavigate?.('profile')} title="Закрыть">
|
||||
✕
|
||||
</button>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user