Обновлен Dockerfile для установки wget и добавлены зависимости в компонентах. Исправлены зависимости в хуках и компонентах, улучшено логирование и обработка ошибок. Удалены неиспользуемые импорты и оптимизированы некоторые функции. Обновлены компоненты для работы с изображениями и улучшена структура кода.

This commit is contained in:
Bivekich
2025-07-17 11:18:32 +03:00
parent 99e91287f3
commit 83ed577a44
24 changed files with 80 additions and 119 deletions

View File

@ -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

View File

@ -29,7 +29,7 @@ export function AuthGuard({ children, fallback }: AuthGuardProps) {
}
initAuth()
}, []) // Убираем checkAuth из зависимостей чтобы избежать повторных вызовов
}, [checkAuth, isAuthenticated, user]) // Добавляем зависимости как требует линтер
// Дополнительное логирование состояний
useEffect(() => {

View File

@ -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' && (
<div className="space-y-6 text-center">
<div className="flex justify-center">
<div className="w-16 h-16 bg-green-100 rounded-full flex items-center justify-center">

View File

@ -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"

View File

@ -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)

View File

@ -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]: {

View File

@ -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<string | null>(null)
const inputRefs = useRef<(HTMLInputElement | null)[]>([])
const { verifySmsCode, checkAuth } = useAuth()
const { verifySmsCode } = useAuth()
const [sendSmsCode] = useMutation(SEND_SMS_CODE)
// Автофокус на первое поле при загрузке

View File

@ -8,7 +8,7 @@ import { DashboardHome } from './dashboard-home'
export type DashboardSection = 'home' | 'settings'
export function Dashboard() {
const [activeSection, setActiveSection] = useState<DashboardSection>('home')
const [activeSection] = useState<DashboardSection>('home')
const renderContent = () => {
switch (activeSection) {

View File

@ -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

View File

@ -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() {
<div className="relative">
<Avatar className="h-16 w-16">
{user?.avatar ? (
<img
<Image
src={user.avatar}
alt="Аватар"
width={64}
height={64}
className="w-full h-full object-cover rounded-full"
/>
) : (

View File

@ -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 'Ошибка даты'
}
}

View File

@ -80,7 +80,7 @@ export function OrganizationCard({
month: 'long',
day: 'numeric'
})
} catch (error) {
} catch {
return 'Ошибка даты'
}
}

View File

@ -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
</h3>
<div className="space-y-3">
{organization.users.map((user, index) => (
{organization.users.map((user) => (
<div key={user.id} className="flex items-center justify-between">
<div className="flex items-center space-x-3">
<OrganizationAvatar
@ -406,7 +406,7 @@ export function OrganizationDetailsModal({ organization, open, onOpenChange }: O
</h3>
<div className="space-y-3">
{organization.apiKeys.map((apiKey, index) => (
{organization.apiKeys.map((apiKey) => (
<div key={apiKey.id} className="flex items-center justify-between">
<div className="flex items-center space-x-3">
<Badge className={apiKey.isActive ? 'bg-green-500/20 text-green-300 border-green-500/30' : 'bg-red-500/20 text-red-300 border-red-500/30'}>

View File

@ -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])

View File

@ -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<File | null>(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<string> => {
if (!user?.id) throw new Error('User not found')
@ -278,9 +249,11 @@ export function ServicesTab() {
/>
{formData.imageUrl && (
<div className="mt-3">
<img
<Image
src={formData.imageUrl}
alt="Preview"
width={80}
height={80}
className="w-20 h-20 object-cover rounded-lg border border-purple-400/30 shadow-lg"
/>
</div>
@ -386,9 +359,11 @@ export function ServicesTab() {
<td className="p-4 text-white/80">{index + 1}</td>
<td className="p-4">
{service.imageUrl ? (
<img
<Image
src={service.imageUrl}
alt={service.name}
width={48}
height={48}
className="w-12 h-12 object-cover rounded border border-white/20"
onError={(e) => {
console.error('Image failed to load:', service.imageUrl, e)

View File

@ -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<File | null>(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<string> => {
if (!user?.id) throw new Error('User not found')
@ -300,9 +273,11 @@ export function SuppliesTab() {
/>
{formData.imageUrl && (
<div className="mt-3">
<img
<Image
src={formData.imageUrl}
alt="Preview"
width={80}
height={80}
className="w-20 h-20 object-cover rounded-lg border border-purple-400/30 shadow-lg"
/>
</div>
@ -409,9 +384,11 @@ export function SuppliesTab() {
<td className="p-4 text-white/80">{index + 1}</td>
<td className="p-4">
{supply.imageUrl ? (
<img
<Image
src={supply.imageUrl}
alt={supply.name}
width={48}
height={48}
className="w-12 h-12 object-cover rounded border border-white/20"
/>
) : (

View File

@ -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) {
<div className="flex items-center space-x-2 flex-1">
{isImageType(selectedFile.type) ? (
<div className="flex items-center space-x-2">
<img
<NextImage
src={selectedFile.url}
alt="Preview"
width={40}
height={40}
className="w-10 h-10 object-cover rounded"
/>
<div>

View File

@ -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`}>
<div className="relative">
<img
<Image
src={imageUrl}
alt={fileName}
width={300}
height={300}
className="w-full h-auto cursor-pointer transition-opacity duration-200"
style={{
opacity: isLoading ? 0 : 1,
@ -108,9 +111,11 @@ export function ImageMessage({ imageUrl, fileName, fileSize, isCurrentUser = fal
onClick={() => setShowFullSize(false)}
>
<div className="relative max-w-full max-h-full">
<img
<Image
src={imageUrl}
alt={fileName}
width={800}
height={600}
className="max-w-full max-h-full object-contain"
onClick={(e) => e.stopPropagation()}
/>

View File

@ -17,6 +17,7 @@ const PhoneInput = React.forwardRef<HTMLInputElement, PhoneInputProps>(
}
// Фильтруем пропсы, которые могут конфликтовать с 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<HTMLInputElement, PhoneInputProps>(
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 (

View File

@ -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(() => {

View File

@ -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,7 +912,7 @@ export const resolvers = {
where: { id: existingKey.id },
data: {
apiKey,
validationData: validationResult.data,
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: {

View File

@ -1,4 +1,4 @@
import { useEffect } from 'react'
import { apolloClient } from '@/lib/apollo-client'
export const useApolloRefresh = () => {

View File

@ -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(

View File

@ -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<string> {
// Для простоты пока используем прямую загрузку через fetch
// В продакшене лучше генерировать signed URLs на backend
// eslint-disable-next-line @typescript-eslint/no-unused-vars
// fileType используется для будущей логики разделения по типам файлов
const timestamp = Date.now()
const key = `avatars/${timestamp}-${fileName}`