feat: добавлено автозаполнение полей wishlist из ссылки (v3.9.0)
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m5s

- Добавлен эндпоинт /api/wishlist/metadata для извлечения метаданных из URL
- Реализовано извлечение Open Graph тегов (title, image, description)
- Добавлена кнопка Pull для ручной загрузки информации из ссылки
- Автоматическое заполнение полей: название, цена, картинка
- Обновлена версия до 3.9.0
This commit is contained in:
poignatov
2026-01-11 21:12:26 +03:00
parent 932dba8682
commit e2059ef555
22 changed files with 3937 additions and 21 deletions

View File

@@ -10,6 +10,9 @@ import TestWords from './components/TestWords'
import Profile from './components/Profile'
import TaskList from './components/TaskList'
import TaskForm from './components/TaskForm.jsx'
import Wishlist from './components/Wishlist'
import WishlistForm from './components/WishlistForm'
import WishlistDetail from './components/WishlistDetail'
import TodoistIntegration from './components/TodoistIntegration'
import TelegramIntegration from './components/TelegramIntegration'
import { AuthProvider, useAuth } from './components/auth/AuthContext'
@@ -21,8 +24,8 @@ 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']
const mainTabs = ['current', 'test-config', 'tasks', 'wishlist', 'profile']
const deepTabs = ['add-words', 'add-config', 'test', 'task-form', 'wishlist-form', 'wishlist-detail', 'words', 'todoist-integration', 'telegram-integration', 'full', 'priorities']
function AppContent() {
const { authFetch, isAuthenticated, loading: authLoading } = useAuth()
@@ -53,6 +56,9 @@ function AppContent() {
test: false,
tasks: false,
'task-form': false,
wishlist: false,
'wishlist-form': false,
'wishlist-detail': false,
profile: false,
'todoist-integration': false,
'telegram-integration': false,
@@ -70,6 +76,9 @@ function AppContent() {
test: false,
tasks: false,
'task-form': false,
wishlist: false,
'wishlist-form': false,
'wishlist-detail': false,
profile: false,
'todoist-integration': false,
'telegram-integration': false,
@@ -106,6 +115,7 @@ function AppContent() {
const [prioritiesRefreshTrigger, setPrioritiesRefreshTrigger] = useState(0)
const [testConfigRefreshTrigger, setTestConfigRefreshTrigger] = useState(0)
const [wordsRefreshTrigger, setWordsRefreshTrigger] = useState(0)
const [wishlistRefreshTrigger, setWishlistRefreshTrigger] = useState(0)
// Восстанавливаем последний выбранный таб после перезагрузки
const [isInitialized, setIsInitialized] = useState(false)
@@ -118,7 +128,7 @@ function AppContent() {
// Проверяем 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']
const validTabs = ['current', 'priorities', 'full', 'words', 'add-words', 'test-config', 'add-config', 'test', 'tasks', 'task-form', 'wishlist', 'wishlist-form', 'wishlist-detail', 'profile', 'todoist-integration', 'telegram-integration']
if (tabFromUrl && validTabs.includes(tabFromUrl) && deepTabs.includes(tabFromUrl)) {
// Если в URL есть глубокий таб, восстанавливаем его
@@ -492,7 +502,7 @@ function AppContent() {
// Обработчик кнопки "назад" в браузере (только для глубоких табов)
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']
const validTabs = ['current', 'priorities', 'full', 'words', 'add-words', 'test-config', 'add-config', 'test', 'tasks', 'task-form', 'wishlist', 'wishlist-form', 'wishlist-detail', 'profile', 'todoist-integration', 'telegram-integration']
// Проверяем state текущей записи истории (куда мы вернулись)
if (event.state && event.state.tab) {
@@ -597,8 +607,8 @@ function AppContent() {
setSelectedProject(null)
setTabParams({})
updateUrl('full', {}, activeTab)
} else if (tab !== activeTab || tab === 'task-form') {
// Для task-form всегда обновляем параметры, даже если это тот же таб
} else if (tab !== activeTab || tab === 'task-form' || tab === 'wishlist-form') {
// Для task-form и wishlist-form всегда обновляем параметры, даже если это тот же таб
markTabAsLoaded(tab)
// Определяем, является ли текущий таб глубоким
@@ -616,8 +626,9 @@ function AppContent() {
updateUrl(tab, {}, activeTab)
}
} else {
// Для task-form явно удаляем taskId, если он undefined
if (tab === 'task-form' && params.taskId === undefined) {
// Для task-form и wishlist-form явно удаляем параметры, если они undefined
if ((tab === 'task-form' && params.taskId === undefined) ||
(tab === 'wishlist-form' && params.wishlistId === undefined)) {
setTabParams({})
if (isNewTabMain) {
clearUrl()
@@ -653,6 +664,10 @@ function AppContent() {
if (activeTab === 'task-form' && tab === 'tasks') {
fetchTasksData(true)
}
// Обновляем список желаний при возврате из экрана редактирования
if (activeTab === 'wishlist-form' && tab === 'wishlist') {
setWishlistRefreshTrigger(prev => prev + 1)
}
// Загрузка данных произойдет в useEffect при изменении activeTab
}
}
@@ -705,7 +720,7 @@ function AppContent() {
}, [activeTab])
// Определяем, нужно ли скрывать нижнюю панель (для fullscreen экранов)
const isFullscreenTab = activeTab === 'test' || activeTab === 'add-words' || activeTab === 'add-config' || activeTab === 'task-form' || activeTab === 'todoist-integration' || activeTab === 'telegram-integration' || activeTab === 'full' || activeTab === 'priorities'
const isFullscreenTab = activeTab === 'test' || activeTab === 'add-words' || activeTab === 'add-config' || activeTab === 'task-form' || activeTab === 'wishlist-form' || activeTab === 'wishlist-detail' || activeTab === 'todoist-integration' || activeTab === 'telegram-integration' || activeTab === 'full' || activeTab === 'priorities'
// Определяем отступы для контейнера
const getContainerPadding = () => {
@@ -854,6 +869,36 @@ function AppContent() {
</div>
)}
{loadedTabs.wishlist && (
<div className={activeTab === 'wishlist' ? 'block' : 'hidden'}>
<Wishlist
onNavigate={handleNavigate}
refreshTrigger={wishlistRefreshTrigger}
/>
</div>
)}
{loadedTabs['wishlist-form'] && (
<div className={activeTab === 'wishlist-form' ? 'block' : 'hidden'}>
<WishlistForm
key={tabParams.wishlistId || 'new'}
onNavigate={handleNavigate}
wishlistId={tabParams.wishlistId}
/>
</div>
)}
{loadedTabs['wishlist-detail'] && (
<div className={activeTab === 'wishlist-detail' ? 'block' : 'hidden'}>
<WishlistDetail
key={tabParams.wishlistId}
onNavigate={handleNavigate}
wishlistId={tabParams.wishlistId}
onRefresh={() => setWishlistRefreshTrigger(prev => prev + 1)}
/>
</div>
)}
{loadedTabs.profile && (
<div className={activeTab === 'profile' ? 'block' : 'hidden'}>
<Profile onNavigate={handleNavigate} />
@@ -938,6 +983,28 @@ function AppContent() {
<div className="absolute top-0 left-0 right-0 h-1 bg-gradient-to-r from-indigo-500 to-purple-500"></div>
)}
</button>
<button
onClick={() => handleTabChange('wishlist')}
className={`min-w-max whitespace-nowrap px-4 py-4 transition-all duration-300 relative ${
activeTab === 'wishlist' || activeTab === 'wishlist-form'
? 'text-indigo-700 bg-white/50'
: 'text-gray-600 hover:text-indigo-600 hover:bg-white/30'
}`}
title="Желания"
>
<span className="relative z-10 flex items-center justify-center">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<polyline points="20 12 20 22 4 22 4 12"></polyline>
<rect x="2" y="7" width="20" height="5"></rect>
<line x1="12" y1="22" x2="12" y2="7"></line>
<path d="M12 7H7.5a2.5 2.5 0 0 1 0-5C11 2 12 7 12 7z"></path>
<path d="M12 7h4.5a2.5 2.5 0 0 0 0-5C13 2 12 7 12 7z"></path>
</svg>
</span>
{(activeTab === 'wishlist' || activeTab === 'wishlist-form') && (
<div className="absolute top-0 left-0 right-0 h-1 bg-gradient-to-r from-indigo-500 to-purple-500"></div>
)}
</button>
<button
onClick={() => handleTabChange('profile')}
className={`min-w-max whitespace-nowrap px-4 py-4 transition-all duration-300 relative ${