Добавлены новые зависимости, обновлены стили и улучшена структура проекта. Обновлен README с описанием функционала и технологий. Реализована анимация и адаптивный дизайн. Настроена авторизация с использованием Apollo Client.

This commit is contained in:
Bivekich
2025-07-16 18:00:41 +03:00
parent d260749bc9
commit 823ef9a28c
69 changed files with 15539 additions and 210 deletions

View File

@ -0,0 +1,367 @@
"use client"
import { useState, useEffect, useRef } from "react"
import { Button } from "@/components/ui/button"
import { GlassInput } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Checkbox } from "@/components/ui/checkbox"
import { AuthLayout } from "./auth-layout"
import { Key, ArrowLeft, ShoppingCart, Check, X } from "lucide-react"
import { Badge } from "@/components/ui/badge"
import { useMutation } from '@apollo/client'
import { ADD_MARKETPLACE_API_KEY } from '@/graphql/mutations'
import { getAuthToken } from '@/lib/apollo-client'
interface ApiValidationData {
sellerId?: string
sellerName?: string
isValid?: boolean
}
interface MarketplaceApiStepProps {
onNext: (apiData: {
wbApiKey?: string
wbApiValidation?: ApiValidationData
ozonApiKey?: string
ozonApiValidation?: ApiValidationData
}) => void
onBack: () => void
}
interface ApiKeyValidation {
[key: string]: {
isValid: boolean | null
isValidating: boolean
error?: string
}
}
export function MarketplaceApiStep({ onNext, onBack }: MarketplaceApiStepProps) {
const [selectedMarketplaces, setSelectedMarketplaces] = useState<string[]>([])
const [wbApiKey, setWbApiKey] = useState("")
const [ozonApiKey, setOzonApiKey] = useState("")
const [validationStates, setValidationStates] = useState<ApiKeyValidation>({})
const [isSubmitting, setIsSubmitting] = useState(false)
const [wbValidationData, setWbValidationData] = useState<ApiValidationData | null>(null)
const [ozonValidationData, setOzonValidationData] = useState<ApiValidationData | null>(null)
const [addMarketplaceApiKey] = useMutation(ADD_MARKETPLACE_API_KEY)
const handleMarketplaceToggle = (marketplace: string) => {
if (selectedMarketplaces.includes(marketplace)) {
setSelectedMarketplaces(prev => prev.filter(m => m !== marketplace))
if (marketplace === 'wildberries') setWbApiKey("")
if (marketplace === 'ozon') setOzonApiKey("")
// Сбрасываем состояние валидации
setValidationStates(prev => ({
...prev,
[marketplace]: { isValid: null, isValidating: false }
}))
} else {
setSelectedMarketplaces(prev => [...prev, marketplace])
}
}
const validateApiKey = async (marketplace: string, apiKey: string) => {
if (!apiKey || !isValidApiKey(apiKey)) return
setValidationStates(prev => ({
...prev,
[marketplace]: { isValid: null, isValidating: true }
}))
try {
const { data } = await addMarketplaceApiKey({
variables: {
input: {
marketplace: marketplace.toUpperCase(),
apiKey,
validateOnly: true
}
}
})
setValidationStates(prev => ({
...prev,
[marketplace]: {
isValid: data.addMarketplaceApiKey.success,
isValidating: false,
error: data.addMarketplaceApiKey.success ? undefined : data.addMarketplaceApiKey.message
}
}))
// Сохраняем данные валидации
if (data.addMarketplaceApiKey.success && data.addMarketplaceApiKey.apiKey?.validationData) {
const validationData = data.addMarketplaceApiKey.apiKey.validationData
if (marketplace === 'wildberries') {
setWbValidationData({
sellerId: validationData.sellerId,
sellerName: validationData.sellerName,
isValid: true
})
} else if (marketplace === 'ozon') {
setOzonValidationData({
sellerId: validationData.sellerId,
sellerName: validationData.sellerName,
isValid: true
})
}
}
} catch (error: unknown) {
setValidationStates(prev => ({
...prev,
[marketplace]: {
isValid: false,
isValidating: false,
error: 'Ошибка валидации API ключа'
}
}))
}
}
const handleApiKeyChange = (marketplace: string, value: string) => {
if (marketplace === 'wildberries') {
setWbApiKey(value)
} else if (marketplace === 'ozon') {
setOzonApiKey(value)
}
// Сбрасываем состояние валидации при изменении
setValidationStates(prev => ({
...prev,
[marketplace]: { isValid: null, isValidating: false }
}))
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
if (selectedMarketplaces.length === 0) return
setIsSubmitting(true)
// Валидируем все выбранные маркетплейсы
const validationPromises = []
if (selectedMarketplaces.includes('wildberries') && isValidApiKey(wbApiKey)) {
validationPromises.push(validateApiKey('wildberries', wbApiKey))
}
if (selectedMarketplaces.includes('ozon') && isValidApiKey(ozonApiKey)) {
validationPromises.push(validateApiKey('ozon', ozonApiKey))
}
// Ждем завершения всех валидаций
await Promise.all(validationPromises)
// Небольшая задержка чтобы состояние обновилось
await new Promise(resolve => setTimeout(resolve, 100))
// Проверяем результаты валидации
let hasValidationErrors = false
for (const marketplace of selectedMarketplaces) {
const validation = validationStates[marketplace]
if (!validation || validation.isValid !== true) {
hasValidationErrors = true
break
}
}
if (!hasValidationErrors) {
const apiData: {
wbApiKey?: string
wbApiValidation?: ApiValidationData
ozonApiKey?: string
ozonApiValidation?: ApiValidationData
} = {}
if (selectedMarketplaces.includes('wildberries') && isValidApiKey(wbApiKey)) {
apiData.wbApiKey = wbApiKey
if (wbValidationData) {
apiData.wbApiValidation = wbValidationData
}
}
if (selectedMarketplaces.includes('ozon') && isValidApiKey(ozonApiKey)) {
apiData.ozonApiKey = ozonApiKey
if (ozonValidationData) {
apiData.ozonApiValidation = ozonValidationData
}
}
onNext(apiData)
}
setIsSubmitting(false)
}
const isValidApiKey = (key: string) => {
return key.length >= 10 && /^[a-zA-Z0-9-_.]+$/.test(key)
}
const isFormValid = () => {
if (selectedMarketplaces.length === 0) return false
for (const marketplace of selectedMarketplaces) {
const apiKey = marketplace === 'wildberries' ? wbApiKey : ozonApiKey
if (!isValidApiKey(apiKey)) {
return false
}
}
return true
}
const getValidationBadge = (marketplace: string) => {
const validation = validationStates[marketplace]
if (!validation || validation.isValid === null) return null
if (validation.isValidating) {
return (
<Badge variant="outline" className="glass-secondary text-yellow-300 border-yellow-400/30 text-xs flex items-center gap-1">
Проверка...
</Badge>
)
}
if (validation.isValid) {
return (
<Badge variant="outline" className="glass-secondary text-green-300 border-green-400/30 text-xs flex items-center gap-1">
<Check className="h-3 w-3" />
Валидный
</Badge>
)
}
return (
<Badge variant="outline" className="glass-secondary text-red-300 border-red-400/30 text-xs flex items-center gap-1">
<X className="h-3 w-3" />
Невалидный
</Badge>
)
}
const marketplaces = [
{
id: 'wildberries',
name: 'Wildberries',
badge: 'Популярный',
badgeColor: 'purple',
apiKey: wbApiKey,
setApiKey: (value: string) => handleApiKeyChange('wildberries', value),
placeholder: 'API ключ Wildberries'
},
{
id: 'ozon',
name: 'Ozon',
badge: 'Быстро растёт',
badgeColor: 'blue',
apiKey: ozonApiKey,
setApiKey: (value: string) => handleApiKeyChange('ozon', value),
placeholder: 'API ключ Ozon'
}
]
return (
<AuthLayout
title="API ключи маркетплейсов"
description="Выберите маркетплейсы и введите API ключи"
currentStep={4}
totalSteps={5}
stepName="API ключи"
>
<div className="space-y-4">
<div className="glass-card p-3">
<div className="flex items-center space-x-2">
<ShoppingCart className="h-4 w-4 text-white" />
<div>
<h4 className="text-white font-medium text-sm">Кабинет селлера</h4>
<p className="text-white/70 text-xs">Управление продажами на маркетплейсах</p>
</div>
</div>
</div>
<form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-3">
{marketplaces.map((marketplace) => (
<div key={marketplace.id} className="glass-card p-3">
<div className="space-y-2">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<Checkbox
id={marketplace.id}
checked={selectedMarketplaces.includes(marketplace.id)}
onCheckedChange={() => handleMarketplaceToggle(marketplace.id)}
className="border-white/30 data-[state=checked]:bg-purple-500"
/>
<Label htmlFor={marketplace.id} className="text-white text-sm font-medium cursor-pointer">
{marketplace.name}
</Label>
</div>
<div className="flex items-center gap-2">
<Badge
variant="outline"
className={`glass-secondary border-${marketplace.badgeColor}-400/30 text-${marketplace.badgeColor}-300 text-xs`}
>
{marketplace.badge}
</Badge>
{selectedMarketplaces.includes(marketplace.id) && getValidationBadge(marketplace.id)}
</div>
</div>
{selectedMarketplaces.includes(marketplace.id) && (
<div className="pt-1">
<GlassInput
type="text"
placeholder={marketplace.placeholder}
value={marketplace.apiKey}
onChange={(e) => marketplace.setApiKey(e.target.value)}
className="h-10 text-sm"
/>
<p className="text-white/60 text-xs mt-1">
{marketplace.id === 'wildberries'
? 'Личный кабинет → Настройки → Доступ к API'
: 'Кабинет продавца → API → Генерация ключа'
}
</p>
{validationStates[marketplace.id]?.error && (
<p className="text-red-400 text-xs mt-1">
{validationStates[marketplace.id].error}
</p>
)}
</div>
)}
</div>
</div>
))}
</div>
<div className="space-y-3">
<Button
type="submit"
variant="glass"
size="lg"
className="w-full h-12"
disabled={!isFormValid() || isSubmitting}
>
{isSubmitting ? "Сохранение..." : "Продолжить"}
</Button>
<Button
type="button"
variant="glass-secondary"
onClick={onBack}
className="w-full flex items-center gap-2"
>
<ArrowLeft className="h-4 w-4" />
Назад
</Button>
</div>
</form>
</div>
</AuthLayout>
)
}