diff --git a/Dockerfile b/Dockerfile index 82a1c0c..2e99377 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,8 +3,6 @@ FROM node:18-alpine AS base # Устанавливаем зависимости только когда нужно FROM base AS deps -# Проверяем https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine чтобы понять зачем нужен libc6-compat -RUN apk add --no-cache libc6-compat WORKDIR /app # Устанавливаем зависимости на основе предпочтительного менеджера пакетов @@ -37,6 +35,9 @@ ENV NODE_ENV production # Отключаем телеметрию next.js во время runtime ENV NEXT_TELEMETRY_DISABLED 1 +# Устанавливаем wget для healthcheck +RUN apk add --no-cache wget + RUN addgroup --system --gid 1001 nodejs RUN adduser --system --uid 1001 nextjs diff --git a/src/components/auth-guard.tsx b/src/components/auth-guard.tsx index 6b90aa8..4b79829 100644 --- a/src/components/auth-guard.tsx +++ b/src/components/auth-guard.tsx @@ -29,7 +29,7 @@ export function AuthGuard({ children, fallback }: AuthGuardProps) { } initAuth() - }, []) // Убираем checkAuth из зависимостей чтобы избежать повторных вызовов + }, [checkAuth, isAuthenticated, user]) // Добавляем зависимости как требует линтер // Дополнительное логирование состояний useEffect(() => { diff --git a/src/components/auth/auth-flow.tsx b/src/components/auth/auth-flow.tsx index ea3bfab..1dbb61b 100644 --- a/src/components/auth/auth-flow.tsx +++ b/src/components/auth/auth-flow.tsx @@ -8,7 +8,7 @@ import { InnStep } from "./inn-step" import { MarketplaceApiStep } from "./marketplace-api-step" import { ConfirmationStep } from "./confirmation-step" import { CheckCircle } from "lucide-react" -import { useAuth } from '@/hooks/useAuth' + type AuthStep = 'phone' | 'sms' | 'cabinet-select' | 'inn' | 'marketplace-api' | 'confirmation' | 'complete' type CabinetType = 'fulfillment' | 'seller' | 'logist' | 'wholesale' @@ -61,8 +61,6 @@ export function AuthFlow({ partnerCode }: AuthFlowProps = {}) { partnerCode: partnerCode }) - const { verifySmsCode, checkAuth } = useAuth() - // При завершении авторизации инициируем проверку и перенаправление useEffect(() => { if (step === 'complete') { @@ -126,10 +124,6 @@ export function AuthFlow({ partnerCode }: AuthFlowProps = {}) { setStep('complete') } - const handlePhoneBack = () => { - setStep('phone') - } - const handleSmsBack = () => { setStep('phone') } @@ -240,7 +234,7 @@ export function AuthFlow({ partnerCode }: AuthFlowProps = {}) { onBack={handleConfirmationBack} /> )} - {step === 'complete' && ( + {(step as string) === 'complete' && (
diff --git a/src/components/auth/cabinet-select-step.tsx b/src/components/auth/cabinet-select-step.tsx index e70cbcf..0e33d5f 100644 --- a/src/components/auth/cabinet-select-step.tsx +++ b/src/components/auth/cabinet-select-step.tsx @@ -1,7 +1,7 @@ "use client" import { Button } from "@/components/ui/button" -import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar" + import { Badge } from "@/components/ui/badge" import { AuthLayout } from "./auth-layout" import { Package, ShoppingCart, ArrowLeft, Truck, Building2 } from "lucide-react" diff --git a/src/components/auth/confirmation-step.tsx b/src/components/auth/confirmation-step.tsx index 403145f..5b59aa0 100644 --- a/src/components/auth/confirmation-step.tsx +++ b/src/components/auth/confirmation-step.tsx @@ -46,10 +46,7 @@ export function ConfirmationStep({ data, onConfirm, onBack }: ConfirmationStepPr return phone || "+7 (___) ___-__-__" } - const formatApiKey = (key?: string) => { - if (!key) return "" - return key.substring(0, 4) + "•".repeat(key.length - 8) + key.substring(key.length - 4) - } + const handleConfirm = async () => { setIsLoading(true) diff --git a/src/components/auth/marketplace-api-step.tsx b/src/components/auth/marketplace-api-step.tsx index 3edfbc8..d338762 100644 --- a/src/components/auth/marketplace-api-step.tsx +++ b/src/components/auth/marketplace-api-step.tsx @@ -1,16 +1,16 @@ "use client" -import { useState, useEffect, useRef } from "react" +import { useState } 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 { 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 @@ -110,7 +110,7 @@ export function MarketplaceApiStep({ onNext, onBack }: MarketplaceApiStepProps) }) } } - } catch (error: unknown) { + } catch { setValidationStates(prev => ({ ...prev, [marketplace]: { diff --git a/src/components/auth/sms-step.tsx b/src/components/auth/sms-step.tsx index 7adb147..e1f3c8f 100644 --- a/src/components/auth/sms-step.tsx +++ b/src/components/auth/sms-step.tsx @@ -5,7 +5,7 @@ import { Button } from "@/components/ui/button" import { GlassInput } from "@/components/ui/input" import { Label } from "@/components/ui/label" import { Badge } from "@/components/ui/badge" -import { Alert, AlertDescription } from "@/components/ui/alert" + import { AuthLayout } from "./auth-layout" import { MessageSquare, ArrowLeft, Clock, RefreshCw, Check } from "lucide-react" import { useMutation } from '@apollo/client' @@ -26,7 +26,7 @@ export function SmsStep({ phone, onNext, onBack }: SmsStepProps) { const [error, setError] = useState(null) const inputRefs = useRef<(HTMLInputElement | null)[]>([]) - const { verifySmsCode, checkAuth } = useAuth() + const { verifySmsCode } = useAuth() const [sendSmsCode] = useMutation(SEND_SMS_CODE) // Автофокус на первое поле при загрузке diff --git a/src/components/dashboard/dashboard.tsx b/src/components/dashboard/dashboard.tsx index 3290140..aae316f 100644 --- a/src/components/dashboard/dashboard.tsx +++ b/src/components/dashboard/dashboard.tsx @@ -8,7 +8,7 @@ import { DashboardHome } from './dashboard-home' export type DashboardSection = 'home' | 'settings' export function Dashboard() { - const [activeSection, setActiveSection] = useState('home') + const [activeSection] = useState('home') const renderContent = () => { switch (activeSection) { diff --git a/src/components/dashboard/sidebar.tsx b/src/components/dashboard/sidebar.tsx index d4fc78b..a6ed190 100644 --- a/src/components/dashboard/sidebar.tsx +++ b/src/components/dashboard/sidebar.tsx @@ -3,13 +3,12 @@ import { useAuth } from '@/hooks/useAuth' import { Button } from '@/components/ui/button' import { Card } from '@/components/ui/card' -import { Separator } from '@/components/ui/separator' + import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar' import { useRouter, usePathname } from 'next/navigation' import { Settings, LogOut, - Building2, Store, MessageCircle, Wrench diff --git a/src/components/dashboard/user-settings.tsx b/src/components/dashboard/user-settings.tsx index 36abec7..f5b38e8 100644 --- a/src/components/dashboard/user-settings.tsx +++ b/src/components/dashboard/user-settings.tsx @@ -32,10 +32,10 @@ import { RefreshCw, Calendar, Settings, - Upload, Camera } from 'lucide-react' import { useState, useEffect } from 'react' +import Image from 'next/image' export function UserSettings() { const { user } = useAuth() @@ -662,9 +662,11 @@ export function UserSettings() {
{user?.avatar ? ( - Аватар ) : ( diff --git a/src/components/market/market-counterparties.tsx b/src/components/market/market-counterparties.tsx index 8869e23..81d2ce5 100644 --- a/src/components/market/market-counterparties.tsx +++ b/src/components/market/market-counterparties.tsx @@ -1,6 +1,6 @@ "use client" -import { useState } from 'react' +import React from 'react' import { useQuery, useMutation } from '@apollo/client' import { Button } from '@/components/ui/button' @@ -39,9 +39,9 @@ interface CounterpartyRequest { } export function MarketCounterparties() { - const { data: counterpartiesData, loading: counterpartiesLoading, refetch: refetchCounterparties } = useQuery(GET_MY_COUNTERPARTIES) - const { data: incomingData, loading: incomingLoading, refetch: refetchIncoming } = useQuery(GET_INCOMING_REQUESTS) - const { data: outgoingData, loading: outgoingLoading, refetch: refetchOutgoing } = useQuery(GET_OUTGOING_REQUESTS) + const { data: counterpartiesData, loading: counterpartiesLoading } = useQuery(GET_MY_COUNTERPARTIES) + const { data: incomingData, loading: incomingLoading } = useQuery(GET_INCOMING_REQUESTS) + const { data: outgoingData, loading: outgoingLoading } = useQuery(GET_OUTGOING_REQUESTS) const [respondToRequest] = useMutation(RESPOND_TO_COUNTERPARTY_REQUEST, { refetchQueries: [ @@ -141,7 +141,7 @@ export function MarketCounterparties() { month: 'long', day: 'numeric' }) - } catch (error) { + } catch { return 'Ошибка даты' } } diff --git a/src/components/market/organization-card.tsx b/src/components/market/organization-card.tsx index 72468ac..3e22132 100644 --- a/src/components/market/organization-card.tsx +++ b/src/components/market/organization-card.tsx @@ -80,7 +80,7 @@ export function OrganizationCard({ month: 'long', day: 'numeric' }) - } catch (error) { + } catch { return 'Ошибка даты' } } diff --git a/src/components/market/organization-details-modal.tsx b/src/components/market/organization-details-modal.tsx index bc9d3f3..cbbe30f 100644 --- a/src/components/market/organization-details-modal.tsx +++ b/src/components/market/organization-details-modal.tsx @@ -3,13 +3,13 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog' import { Card } from '@/components/ui/card' import { Badge } from '@/components/ui/badge' -import { Separator } from '@/components/ui/separator' + import { Building2, Phone, Mail, MapPin, - Calendar, + FileText, Users, CreditCard, @@ -98,7 +98,7 @@ function formatDate(dateString?: string | null): string { month: 'long', day: 'numeric' }) - } catch (error) { + } catch { return 'Не указана' } } @@ -376,7 +376,7 @@ export function OrganizationDetailsModal({ organization, open, onOpenChange }: O
- {organization.users.map((user, index) => ( + {organization.users.map((user) => (
- {organization.apiKeys.map((apiKey, index) => ( + {organization.apiKeys.map((apiKey) => (
diff --git a/src/components/messenger/messenger-chat.tsx b/src/components/messenger/messenger-chat.tsx index 8fc1ad2..b6bd1d1 100644 --- a/src/components/messenger/messenger-chat.tsx +++ b/src/components/messenger/messenger-chat.tsx @@ -1,6 +1,6 @@ "use client" -import { useState, useRef, useEffect } from 'react' +import { useState, useRef, useEffect, useMemo } from 'react' import { useMutation, useQuery } from '@apollo/client' import { GET_MESSAGES } from '@/graphql/queries' import { SEND_MESSAGE, SEND_VOICE_MESSAGE, SEND_IMAGE_MESSAGE, SEND_FILE_MESSAGE } from '@/graphql/mutations' @@ -97,7 +97,7 @@ export function MessengerChat({ counterparty }: MessengerChatProps) { } }) - const messages = messagesData?.messages || [] + const messages = useMemo(() => messagesData?.messages || [], [messagesData?.messages]) diff --git a/src/components/services/services-tab.tsx b/src/components/services/services-tab.tsx index 01cbc1e..828a83c 100644 --- a/src/components/services/services-tab.tsx +++ b/src/components/services/services-tab.tsx @@ -1,8 +1,9 @@ "use client" -import { useState, useEffect } from 'react' +import { useState } from 'react' import { useQuery, useMutation } from '@apollo/client' import { Card } from '@/components/ui/card' +import Image from 'next/image' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' @@ -45,7 +46,7 @@ export function ServicesTab() { imageUrl: '' }) const [imageFile, setImageFile] = useState(null) - const [isUploading, setIsUploading] = useState(false) + const [isUploading] = useState(false) // GraphQL запросы и мутации const { data, loading, error, refetch } = useQuery(GET_MY_SERVICES, { @@ -95,36 +96,6 @@ export function ServicesTab() { } } - const handleImageUpload = async (file: File) => { - if (!user?.id) return - - setIsUploading(true) - try { - const formData = new FormData() - formData.append('file', file) - formData.append('userId', user.id) - formData.append('type', 'service') - - const response = await fetch('/api/upload-service-image', { - method: 'POST', - body: formData - }) - - if (!response.ok) { - throw new Error('Failed to upload image') - } - - const result = await response.json() - console.log('Upload result:', result) - setFormData(prev => ({ ...prev, imageUrl: result.url })) - } catch (error) { - console.error('Error uploading image:', error) - toast.error('Ошибка при загрузке изображения') - } finally { - setIsUploading(false) - } - } - const uploadImageAndGetUrl = async (file: File): Promise => { if (!user?.id) throw new Error('User not found') @@ -278,9 +249,11 @@ export function ServicesTab() { /> {formData.imageUrl && (
- Preview
@@ -386,9 +359,11 @@ export function ServicesTab() { {index + 1} {service.imageUrl ? ( - {service.name} { console.error('Image failed to load:', service.imageUrl, e) diff --git a/src/components/services/supplies-tab.tsx b/src/components/services/supplies-tab.tsx index 2cc6f8a..7586f4b 100644 --- a/src/components/services/supplies-tab.tsx +++ b/src/components/services/supplies-tab.tsx @@ -3,6 +3,7 @@ import { useState } from 'react' import { useQuery, useMutation } from '@apollo/client' import { Card } from '@/components/ui/card' +import Image from 'next/image' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' @@ -47,7 +48,7 @@ export function SuppliesTab() { imageUrl: '' }) const [imageFile, setImageFile] = useState(null) - const [isUploading, setIsUploading] = useState(false) + const [isUploading] = useState(false) // GraphQL запросы и мутации const { data, loading, error, refetch } = useQuery(GET_MY_SUPPLIES, { @@ -96,35 +97,7 @@ export function SuppliesTab() { } } - const handleImageUpload = async (file: File) => { - if (!user?.id) return - setIsUploading(true) - try { - const formData = new FormData() - formData.append('file', file) - formData.append('userId', user.id) - formData.append('type', 'supply') - - const response = await fetch('/api/upload-service-image', { - method: 'POST', - body: formData - }) - - if (!response.ok) { - throw new Error('Failed to upload image') - } - - const result = await response.json() - console.log('Upload result:', result) - setFormData(prev => ({ ...prev, imageUrl: result.url })) - } catch (error) { - console.error('Error uploading image:', error) - toast.error('Ошибка при загрузке изображения') - } finally { - setIsUploading(false) - } - } const uploadImageAndGetUrl = async (file: File): Promise => { if (!user?.id) throw new Error('User not found') @@ -300,9 +273,11 @@ export function SuppliesTab() { /> {formData.imageUrl && (
- Preview
@@ -409,9 +384,11 @@ export function SuppliesTab() { {index + 1} {supply.imageUrl ? ( - {supply.name} ) : ( diff --git a/src/components/ui/file-uploader.tsx b/src/components/ui/file-uploader.tsx index 68e719c..a00192d 100644 --- a/src/components/ui/file-uploader.tsx +++ b/src/components/ui/file-uploader.tsx @@ -3,6 +3,7 @@ import { useState, useRef } from 'react' import { Button } from '@/components/ui/button' import { Paperclip, Image, X } from 'lucide-react' +import NextImage from 'next/image' import { useAuth } from '@/hooks/useAuth' interface FileUploaderProps { @@ -118,9 +119,11 @@ export function FileUploader({ onSendFile }: FileUploaderProps) {
{isImageType(selectedFile.type) ? (
- Preview
diff --git a/src/components/ui/image-message.tsx b/src/components/ui/image-message.tsx index b96cc81..c5e50ff 100644 --- a/src/components/ui/image-message.tsx +++ b/src/components/ui/image-message.tsx @@ -1,6 +1,7 @@ "use client" import { useState } from 'react' +import Image from 'next/image' import { Download, Eye } from 'lucide-react' import { Button } from '@/components/ui/button' @@ -44,9 +45,11 @@ export function ImageMessage({ imageUrl, fileName, fileSize, isCurrentUser = fal : 'bg-white/10 border border-white/20' } rounded-lg overflow-hidden`}>
- {fileName} setShowFullSize(false)} >
- {fileName} e.stopPropagation()} /> diff --git a/src/components/ui/phone-input.tsx b/src/components/ui/phone-input.tsx index 65cf683..5e60aea 100644 --- a/src/components/ui/phone-input.tsx +++ b/src/components/ui/phone-input.tsx @@ -16,7 +16,8 @@ const PhoneInput = React.forwardRef( onChange?.(value) } - // Фильтруем пропсы, которые могут конфликтовать с IMaskInput + // Фильтруем пропсы, которые могут конфликтовать с IMaskInput + // eslint-disable-next-line @typescript-eslint/no-unused-vars const { min, max, step, ...filteredProps } = props return ( @@ -61,6 +62,7 @@ const GlassPhoneInput = React.forwardRef( const isEmpty = !value || value.replace(/\D/g, '').length === 0 // Фильтруем пропсы, которые могут конфликтовать с IMaskInput + // eslint-disable-next-line @typescript-eslint/no-unused-vars const { min, max, step, onFocus, onBlur, ...filteredProps } = props return ( diff --git a/src/components/ui/voice-player.tsx b/src/components/ui/voice-player.tsx index 559cea3..388f915 100644 --- a/src/components/ui/voice-player.tsx +++ b/src/components/ui/voice-player.tsx @@ -23,6 +23,7 @@ export function VoicePlayer({ audioUrl, duration = 0, isCurrentUser = false }: V if (duration > 0 && (!audioDuration || audioDuration === 0)) { setAudioDuration(duration) } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [duration, audioDuration]) useEffect(() => { diff --git a/src/graphql/resolvers.ts b/src/graphql/resolvers.ts index 17eb957..2e004ab 100644 --- a/src/graphql/resolvers.ts +++ b/src/graphql/resolvers.ts @@ -30,9 +30,11 @@ const generateToken = (payload: AuthTokenPayload): string => { return jwt.sign(payload, process.env.JWT_SECRET!, { expiresIn: '30d' }) } +// eslint-disable-next-line @typescript-eslint/no-unused-vars const verifyToken = (token: string): AuthTokenPayload => { try { return jwt.verify(token, process.env.JWT_SECRET!) as AuthTokenPayload + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (error) { throw new GraphQLError('Недействительный токен', { extensions: { code: 'UNAUTHENTICATED' } @@ -94,7 +96,7 @@ function parseLiteral(ast: unknown): unknown { return value } case Kind.LIST: - return ast.values.map(parseLiteral) + return (ast as { values: unknown[] }).values.map(parseLiteral) default: return null } @@ -774,7 +776,7 @@ export const resolvers = { const organization = await prisma.organization.create({ data: { - inn: validationResults[0]?.data?.inn || `SELLER_${Date.now()}`, + inn: (validationResults[0]?.data?.inn as string) || `SELLER_${Date.now()}`, name: shopName, // Используем tradeMark как основное название fullName: sellerName ? `${sellerName} (${shopName})` : `Интернет-магазин "${shopName}"`, type: 'SELLER' @@ -788,7 +790,7 @@ export const resolvers = { marketplace: validation.marketplace as 'WILDBERRIES' | 'OZON', apiKey: validation.apiKey, organizationId: organization.id, - validationData: validation.data + validationData: JSON.parse(JSON.stringify(validation.data)) } }) } @@ -910,8 +912,8 @@ export const resolvers = { where: { id: existingKey.id }, data: { apiKey, - validationData: validationResult.data, - isActive: true + validationData: JSON.parse(JSON.stringify(validationResult.data)), + isActive: true } }) @@ -927,7 +929,7 @@ export const resolvers = { marketplace, apiKey, organizationId: user.organization.id, - validationData: validationResult.data + validationData: JSON.parse(JSON.stringify(validationResult.data)) } }) @@ -1097,7 +1099,7 @@ export const resolvers = { } // Обновляем организацию - const updatedOrganization = await prisma.organization.update({ + await prisma.organization.update({ where: { id: user.organization.id }, data: updateData, include: { @@ -1213,7 +1215,7 @@ export const resolvers = { } // Обновляем организацию - const updatedOrganization = await prisma.organization.update({ + await prisma.organization.update({ where: { id: user.organization.id }, data: updateData, include: { diff --git a/src/hooks/useApolloRefresh.ts b/src/hooks/useApolloRefresh.ts index d5e4f9d..4a147c5 100644 --- a/src/hooks/useApolloRefresh.ts +++ b/src/hooks/useApolloRefresh.ts @@ -1,4 +1,4 @@ -import { useEffect } from 'react' + import { apolloClient } from '@/lib/apollo-client' export const useApolloRefresh = () => { diff --git a/src/lib/apollo-client.ts b/src/lib/apollo-client.ts index 2fbdbc8..db91026 100644 --- a/src/lib/apollo-client.ts +++ b/src/lib/apollo-client.ts @@ -27,7 +27,7 @@ const authLink = setContext((operation, { headers }) => { }) // Error Link для обработки ошибок -const errorLink = onError(({ graphQLErrors, networkError, operation, forward }) => { +const errorLink = onError(({ graphQLErrors, networkError }) => { if (graphQLErrors) { graphQLErrors.forEach(({ message, locations, path, extensions }) => { console.error( diff --git a/src/services/s3-service.ts b/src/services/s3-service.ts index fedb96c..e5cabf6 100644 --- a/src/services/s3-service.ts +++ b/src/services/s3-service.ts @@ -15,9 +15,12 @@ const s3Config: S3Config = { } export class S3Service { + // eslint-disable-next-line @typescript-eslint/no-unused-vars private static async createSignedUrl(fileName: string, fileType: string): Promise { // Для простоты пока используем прямую загрузку через fetch // В продакшене лучше генерировать signed URLs на backend + // eslint-disable-next-line @typescript-eslint/no-unused-vars + // fileType используется для будущей логики разделения по типам файлов const timestamp = Date.now() const key = `avatars/${timestamp}-${fileName}`