Обновлен Dockerfile для установки wget и добавлены зависимости в компонентах. Исправлены зависимости в хуках и компонентах, улучшено логирование и обработка ошибок. Удалены неиспользуемые импорты и оптимизированы некоторые функции. Обновлены компоненты для работы с изображениями и улучшена структура кода.
This commit is contained in:
@ -3,8 +3,6 @@ FROM node:18-alpine AS base
|
|||||||
|
|
||||||
# Устанавливаем зависимости только когда нужно
|
# Устанавливаем зависимости только когда нужно
|
||||||
FROM base AS deps
|
FROM base AS deps
|
||||||
# Проверяем https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine чтобы понять зачем нужен libc6-compat
|
|
||||||
RUN apk add --no-cache libc6-compat
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Устанавливаем зависимости на основе предпочтительного менеджера пакетов
|
# Устанавливаем зависимости на основе предпочтительного менеджера пакетов
|
||||||
@ -37,6 +35,9 @@ ENV NODE_ENV production
|
|||||||
# Отключаем телеметрию next.js во время runtime
|
# Отключаем телеметрию next.js во время runtime
|
||||||
ENV NEXT_TELEMETRY_DISABLED 1
|
ENV NEXT_TELEMETRY_DISABLED 1
|
||||||
|
|
||||||
|
# Устанавливаем wget для healthcheck
|
||||||
|
RUN apk add --no-cache wget
|
||||||
|
|
||||||
RUN addgroup --system --gid 1001 nodejs
|
RUN addgroup --system --gid 1001 nodejs
|
||||||
RUN adduser --system --uid 1001 nextjs
|
RUN adduser --system --uid 1001 nextjs
|
||||||
|
|
||||||
|
@ -29,7 +29,7 @@ export function AuthGuard({ children, fallback }: AuthGuardProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
initAuth()
|
initAuth()
|
||||||
}, []) // Убираем checkAuth из зависимостей чтобы избежать повторных вызовов
|
}, [checkAuth, isAuthenticated, user]) // Добавляем зависимости как требует линтер
|
||||||
|
|
||||||
// Дополнительное логирование состояний
|
// Дополнительное логирование состояний
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -8,7 +8,7 @@ import { InnStep } from "./inn-step"
|
|||||||
import { MarketplaceApiStep } from "./marketplace-api-step"
|
import { MarketplaceApiStep } from "./marketplace-api-step"
|
||||||
import { ConfirmationStep } from "./confirmation-step"
|
import { ConfirmationStep } from "./confirmation-step"
|
||||||
import { CheckCircle } from "lucide-react"
|
import { CheckCircle } from "lucide-react"
|
||||||
import { useAuth } from '@/hooks/useAuth'
|
|
||||||
|
|
||||||
type AuthStep = 'phone' | 'sms' | 'cabinet-select' | 'inn' | 'marketplace-api' | 'confirmation' | 'complete'
|
type AuthStep = 'phone' | 'sms' | 'cabinet-select' | 'inn' | 'marketplace-api' | 'confirmation' | 'complete'
|
||||||
type CabinetType = 'fulfillment' | 'seller' | 'logist' | 'wholesale'
|
type CabinetType = 'fulfillment' | 'seller' | 'logist' | 'wholesale'
|
||||||
@ -61,8 +61,6 @@ export function AuthFlow({ partnerCode }: AuthFlowProps = {}) {
|
|||||||
partnerCode: partnerCode
|
partnerCode: partnerCode
|
||||||
})
|
})
|
||||||
|
|
||||||
const { verifySmsCode, checkAuth } = useAuth()
|
|
||||||
|
|
||||||
// При завершении авторизации инициируем проверку и перенаправление
|
// При завершении авторизации инициируем проверку и перенаправление
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (step === 'complete') {
|
if (step === 'complete') {
|
||||||
@ -126,10 +124,6 @@ export function AuthFlow({ partnerCode }: AuthFlowProps = {}) {
|
|||||||
setStep('complete')
|
setStep('complete')
|
||||||
}
|
}
|
||||||
|
|
||||||
const handlePhoneBack = () => {
|
|
||||||
setStep('phone')
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSmsBack = () => {
|
const handleSmsBack = () => {
|
||||||
setStep('phone')
|
setStep('phone')
|
||||||
}
|
}
|
||||||
@ -240,7 +234,7 @@ export function AuthFlow({ partnerCode }: AuthFlowProps = {}) {
|
|||||||
onBack={handleConfirmationBack}
|
onBack={handleConfirmationBack}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{step === 'complete' && (
|
{(step as string) === 'complete' && (
|
||||||
<div className="space-y-6 text-center">
|
<div className="space-y-6 text-center">
|
||||||
<div className="flex justify-center">
|
<div className="flex justify-center">
|
||||||
<div className="w-16 h-16 bg-green-100 rounded-full flex items-center justify-center">
|
<div className="w-16 h-16 bg-green-100 rounded-full flex items-center justify-center">
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar"
|
|
||||||
import { Badge } from "@/components/ui/badge"
|
import { Badge } from "@/components/ui/badge"
|
||||||
import { AuthLayout } from "./auth-layout"
|
import { AuthLayout } from "./auth-layout"
|
||||||
import { Package, ShoppingCart, ArrowLeft, Truck, Building2 } from "lucide-react"
|
import { Package, ShoppingCart, ArrowLeft, Truck, Building2 } from "lucide-react"
|
||||||
|
@ -46,10 +46,7 @@ export function ConfirmationStep({ data, onConfirm, onBack }: ConfirmationStepPr
|
|||||||
return phone || "+7 (___) ___-__-__"
|
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 () => {
|
const handleConfirm = async () => {
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
|
@ -1,16 +1,16 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useState, useEffect, useRef } from "react"
|
import { useState } from "react"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { GlassInput } from "@/components/ui/input"
|
import { GlassInput } from "@/components/ui/input"
|
||||||
import { Label } from "@/components/ui/label"
|
import { Label } from "@/components/ui/label"
|
||||||
import { Checkbox } from "@/components/ui/checkbox"
|
import { Checkbox } from "@/components/ui/checkbox"
|
||||||
import { AuthLayout } from "./auth-layout"
|
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 { Badge } from "@/components/ui/badge"
|
||||||
import { useMutation } from '@apollo/client'
|
import { useMutation } from '@apollo/client'
|
||||||
import { ADD_MARKETPLACE_API_KEY } from '@/graphql/mutations'
|
import { ADD_MARKETPLACE_API_KEY } from '@/graphql/mutations'
|
||||||
import { getAuthToken } from '@/lib/apollo-client'
|
|
||||||
|
|
||||||
interface ApiValidationData {
|
interface ApiValidationData {
|
||||||
sellerId?: string
|
sellerId?: string
|
||||||
@ -110,7 +110,7 @@ export function MarketplaceApiStep({ onNext, onBack }: MarketplaceApiStepProps)
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error: unknown) {
|
} catch {
|
||||||
setValidationStates(prev => ({
|
setValidationStates(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
[marketplace]: {
|
[marketplace]: {
|
||||||
|
@ -5,7 +5,7 @@ import { Button } from "@/components/ui/button"
|
|||||||
import { GlassInput } from "@/components/ui/input"
|
import { GlassInput } from "@/components/ui/input"
|
||||||
import { Label } from "@/components/ui/label"
|
import { Label } from "@/components/ui/label"
|
||||||
import { Badge } from "@/components/ui/badge"
|
import { Badge } from "@/components/ui/badge"
|
||||||
import { Alert, AlertDescription } from "@/components/ui/alert"
|
|
||||||
import { AuthLayout } from "./auth-layout"
|
import { AuthLayout } from "./auth-layout"
|
||||||
import { MessageSquare, ArrowLeft, Clock, RefreshCw, Check } from "lucide-react"
|
import { MessageSquare, ArrowLeft, Clock, RefreshCw, Check } from "lucide-react"
|
||||||
import { useMutation } from '@apollo/client'
|
import { useMutation } from '@apollo/client'
|
||||||
@ -26,7 +26,7 @@ export function SmsStep({ phone, onNext, onBack }: SmsStepProps) {
|
|||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
const inputRefs = useRef<(HTMLInputElement | null)[]>([])
|
const inputRefs = useRef<(HTMLInputElement | null)[]>([])
|
||||||
|
|
||||||
const { verifySmsCode, checkAuth } = useAuth()
|
const { verifySmsCode } = useAuth()
|
||||||
const [sendSmsCode] = useMutation(SEND_SMS_CODE)
|
const [sendSmsCode] = useMutation(SEND_SMS_CODE)
|
||||||
|
|
||||||
// Автофокус на первое поле при загрузке
|
// Автофокус на первое поле при загрузке
|
||||||
|
@ -8,7 +8,7 @@ import { DashboardHome } from './dashboard-home'
|
|||||||
export type DashboardSection = 'home' | 'settings'
|
export type DashboardSection = 'home' | 'settings'
|
||||||
|
|
||||||
export function Dashboard() {
|
export function Dashboard() {
|
||||||
const [activeSection, setActiveSection] = useState<DashboardSection>('home')
|
const [activeSection] = useState<DashboardSection>('home')
|
||||||
|
|
||||||
const renderContent = () => {
|
const renderContent = () => {
|
||||||
switch (activeSection) {
|
switch (activeSection) {
|
||||||
|
@ -3,13 +3,12 @@
|
|||||||
import { useAuth } from '@/hooks/useAuth'
|
import { useAuth } from '@/hooks/useAuth'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Card } from '@/components/ui/card'
|
import { Card } from '@/components/ui/card'
|
||||||
import { Separator } from '@/components/ui/separator'
|
|
||||||
import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar'
|
import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar'
|
||||||
import { useRouter, usePathname } from 'next/navigation'
|
import { useRouter, usePathname } from 'next/navigation'
|
||||||
import {
|
import {
|
||||||
Settings,
|
Settings,
|
||||||
LogOut,
|
LogOut,
|
||||||
Building2,
|
|
||||||
Store,
|
Store,
|
||||||
MessageCircle,
|
MessageCircle,
|
||||||
Wrench
|
Wrench
|
||||||
|
@ -32,10 +32,10 @@ import {
|
|||||||
RefreshCw,
|
RefreshCw,
|
||||||
Calendar,
|
Calendar,
|
||||||
Settings,
|
Settings,
|
||||||
Upload,
|
|
||||||
Camera
|
Camera
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
|
import Image from 'next/image'
|
||||||
|
|
||||||
export function UserSettings() {
|
export function UserSettings() {
|
||||||
const { user } = useAuth()
|
const { user } = useAuth()
|
||||||
@ -662,9 +662,11 @@ export function UserSettings() {
|
|||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Avatar className="h-16 w-16">
|
<Avatar className="h-16 w-16">
|
||||||
{user?.avatar ? (
|
{user?.avatar ? (
|
||||||
<img
|
<Image
|
||||||
src={user.avatar}
|
src={user.avatar}
|
||||||
alt="Аватар"
|
alt="Аватар"
|
||||||
|
width={64}
|
||||||
|
height={64}
|
||||||
className="w-full h-full object-cover rounded-full"
|
className="w-full h-full object-cover rounded-full"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useState } from 'react'
|
import React from 'react'
|
||||||
import { useQuery, useMutation } from '@apollo/client'
|
import { useQuery, useMutation } from '@apollo/client'
|
||||||
|
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
@ -39,9 +39,9 @@ interface CounterpartyRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function MarketCounterparties() {
|
export function MarketCounterparties() {
|
||||||
const { data: counterpartiesData, loading: counterpartiesLoading, refetch: refetchCounterparties } = useQuery(GET_MY_COUNTERPARTIES)
|
const { data: counterpartiesData, loading: counterpartiesLoading } = useQuery(GET_MY_COUNTERPARTIES)
|
||||||
const { data: incomingData, loading: incomingLoading, refetch: refetchIncoming } = useQuery(GET_INCOMING_REQUESTS)
|
const { data: incomingData, loading: incomingLoading } = useQuery(GET_INCOMING_REQUESTS)
|
||||||
const { data: outgoingData, loading: outgoingLoading, refetch: refetchOutgoing } = useQuery(GET_OUTGOING_REQUESTS)
|
const { data: outgoingData, loading: outgoingLoading } = useQuery(GET_OUTGOING_REQUESTS)
|
||||||
|
|
||||||
const [respondToRequest] = useMutation(RESPOND_TO_COUNTERPARTY_REQUEST, {
|
const [respondToRequest] = useMutation(RESPOND_TO_COUNTERPARTY_REQUEST, {
|
||||||
refetchQueries: [
|
refetchQueries: [
|
||||||
@ -141,7 +141,7 @@ export function MarketCounterparties() {
|
|||||||
month: 'long',
|
month: 'long',
|
||||||
day: 'numeric'
|
day: 'numeric'
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch {
|
||||||
return 'Ошибка даты'
|
return 'Ошибка даты'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -80,7 +80,7 @@ export function OrganizationCard({
|
|||||||
month: 'long',
|
month: 'long',
|
||||||
day: 'numeric'
|
day: 'numeric'
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch {
|
||||||
return 'Ошибка даты'
|
return 'Ошибка даты'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,13 +3,13 @@
|
|||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||||
import { Card } from '@/components/ui/card'
|
import { Card } from '@/components/ui/card'
|
||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
import { Separator } from '@/components/ui/separator'
|
|
||||||
import {
|
import {
|
||||||
Building2,
|
Building2,
|
||||||
Phone,
|
Phone,
|
||||||
Mail,
|
Mail,
|
||||||
MapPin,
|
MapPin,
|
||||||
Calendar,
|
|
||||||
FileText,
|
FileText,
|
||||||
Users,
|
Users,
|
||||||
CreditCard,
|
CreditCard,
|
||||||
@ -98,7 +98,7 @@ function formatDate(dateString?: string | null): string {
|
|||||||
month: 'long',
|
month: 'long',
|
||||||
day: 'numeric'
|
day: 'numeric'
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch {
|
||||||
return 'Не указана'
|
return 'Не указана'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -376,7 +376,7 @@ export function OrganizationDetailsModal({ organization, open, onOpenChange }: O
|
|||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<div className="space-y-3">
|
<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 key={user.id} className="flex items-center justify-between">
|
||||||
<div className="flex items-center space-x-3">
|
<div className="flex items-center space-x-3">
|
||||||
<OrganizationAvatar
|
<OrganizationAvatar
|
||||||
@ -406,7 +406,7 @@ export function OrganizationDetailsModal({ organization, open, onOpenChange }: O
|
|||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<div className="space-y-3">
|
<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 key={apiKey.id} className="flex items-center justify-between">
|
||||||
<div className="flex items-center space-x-3">
|
<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'}>
|
<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'}>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useState, useRef, useEffect } from 'react'
|
import { useState, useRef, useEffect, useMemo } from 'react'
|
||||||
import { useMutation, useQuery } from '@apollo/client'
|
import { useMutation, useQuery } from '@apollo/client'
|
||||||
import { GET_MESSAGES } from '@/graphql/queries'
|
import { GET_MESSAGES } from '@/graphql/queries'
|
||||||
import { SEND_MESSAGE, SEND_VOICE_MESSAGE, SEND_IMAGE_MESSAGE, SEND_FILE_MESSAGE } from '@/graphql/mutations'
|
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])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useState, useEffect } from 'react'
|
import { useState } from 'react'
|
||||||
import { useQuery, useMutation } from '@apollo/client'
|
import { useQuery, useMutation } from '@apollo/client'
|
||||||
import { Card } from '@/components/ui/card'
|
import { Card } from '@/components/ui/card'
|
||||||
|
import Image from 'next/image'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { Label } from '@/components/ui/label'
|
import { Label } from '@/components/ui/label'
|
||||||
@ -45,7 +46,7 @@ export function ServicesTab() {
|
|||||||
imageUrl: ''
|
imageUrl: ''
|
||||||
})
|
})
|
||||||
const [imageFile, setImageFile] = useState<File | null>(null)
|
const [imageFile, setImageFile] = useState<File | null>(null)
|
||||||
const [isUploading, setIsUploading] = useState(false)
|
const [isUploading] = useState(false)
|
||||||
|
|
||||||
// GraphQL запросы и мутации
|
// GraphQL запросы и мутации
|
||||||
const { data, loading, error, refetch } = useQuery(GET_MY_SERVICES, {
|
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> => {
|
const uploadImageAndGetUrl = async (file: File): Promise<string> => {
|
||||||
if (!user?.id) throw new Error('User not found')
|
if (!user?.id) throw new Error('User not found')
|
||||||
|
|
||||||
@ -278,9 +249,11 @@ export function ServicesTab() {
|
|||||||
/>
|
/>
|
||||||
{formData.imageUrl && (
|
{formData.imageUrl && (
|
||||||
<div className="mt-3">
|
<div className="mt-3">
|
||||||
<img
|
<Image
|
||||||
src={formData.imageUrl}
|
src={formData.imageUrl}
|
||||||
alt="Preview"
|
alt="Preview"
|
||||||
|
width={80}
|
||||||
|
height={80}
|
||||||
className="w-20 h-20 object-cover rounded-lg border border-purple-400/30 shadow-lg"
|
className="w-20 h-20 object-cover rounded-lg border border-purple-400/30 shadow-lg"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -386,9 +359,11 @@ export function ServicesTab() {
|
|||||||
<td className="p-4 text-white/80">{index + 1}</td>
|
<td className="p-4 text-white/80">{index + 1}</td>
|
||||||
<td className="p-4">
|
<td className="p-4">
|
||||||
{service.imageUrl ? (
|
{service.imageUrl ? (
|
||||||
<img
|
<Image
|
||||||
src={service.imageUrl}
|
src={service.imageUrl}
|
||||||
alt={service.name}
|
alt={service.name}
|
||||||
|
width={48}
|
||||||
|
height={48}
|
||||||
className="w-12 h-12 object-cover rounded border border-white/20"
|
className="w-12 h-12 object-cover rounded border border-white/20"
|
||||||
onError={(e) => {
|
onError={(e) => {
|
||||||
console.error('Image failed to load:', service.imageUrl, e)
|
console.error('Image failed to load:', service.imageUrl, e)
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { useQuery, useMutation } from '@apollo/client'
|
import { useQuery, useMutation } from '@apollo/client'
|
||||||
import { Card } from '@/components/ui/card'
|
import { Card } from '@/components/ui/card'
|
||||||
|
import Image from 'next/image'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { Label } from '@/components/ui/label'
|
import { Label } from '@/components/ui/label'
|
||||||
@ -47,7 +48,7 @@ export function SuppliesTab() {
|
|||||||
imageUrl: ''
|
imageUrl: ''
|
||||||
})
|
})
|
||||||
const [imageFile, setImageFile] = useState<File | null>(null)
|
const [imageFile, setImageFile] = useState<File | null>(null)
|
||||||
const [isUploading, setIsUploading] = useState(false)
|
const [isUploading] = useState(false)
|
||||||
|
|
||||||
// GraphQL запросы и мутации
|
// GraphQL запросы и мутации
|
||||||
const { data, loading, error, refetch } = useQuery(GET_MY_SUPPLIES, {
|
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> => {
|
const uploadImageAndGetUrl = async (file: File): Promise<string> => {
|
||||||
if (!user?.id) throw new Error('User not found')
|
if (!user?.id) throw new Error('User not found')
|
||||||
@ -300,9 +273,11 @@ export function SuppliesTab() {
|
|||||||
/>
|
/>
|
||||||
{formData.imageUrl && (
|
{formData.imageUrl && (
|
||||||
<div className="mt-3">
|
<div className="mt-3">
|
||||||
<img
|
<Image
|
||||||
src={formData.imageUrl}
|
src={formData.imageUrl}
|
||||||
alt="Preview"
|
alt="Preview"
|
||||||
|
width={80}
|
||||||
|
height={80}
|
||||||
className="w-20 h-20 object-cover rounded-lg border border-purple-400/30 shadow-lg"
|
className="w-20 h-20 object-cover rounded-lg border border-purple-400/30 shadow-lg"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -409,9 +384,11 @@ export function SuppliesTab() {
|
|||||||
<td className="p-4 text-white/80">{index + 1}</td>
|
<td className="p-4 text-white/80">{index + 1}</td>
|
||||||
<td className="p-4">
|
<td className="p-4">
|
||||||
{supply.imageUrl ? (
|
{supply.imageUrl ? (
|
||||||
<img
|
<Image
|
||||||
src={supply.imageUrl}
|
src={supply.imageUrl}
|
||||||
alt={supply.name}
|
alt={supply.name}
|
||||||
|
width={48}
|
||||||
|
height={48}
|
||||||
className="w-12 h-12 object-cover rounded border border-white/20"
|
className="w-12 h-12 object-cover rounded border border-white/20"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
import { useState, useRef } from 'react'
|
import { useState, useRef } from 'react'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Paperclip, Image, X } from 'lucide-react'
|
import { Paperclip, Image, X } from 'lucide-react'
|
||||||
|
import NextImage from 'next/image'
|
||||||
import { useAuth } from '@/hooks/useAuth'
|
import { useAuth } from '@/hooks/useAuth'
|
||||||
|
|
||||||
interface FileUploaderProps {
|
interface FileUploaderProps {
|
||||||
@ -118,9 +119,11 @@ export function FileUploader({ onSendFile }: FileUploaderProps) {
|
|||||||
<div className="flex items-center space-x-2 flex-1">
|
<div className="flex items-center space-x-2 flex-1">
|
||||||
{isImageType(selectedFile.type) ? (
|
{isImageType(selectedFile.type) ? (
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<img
|
<NextImage
|
||||||
src={selectedFile.url}
|
src={selectedFile.url}
|
||||||
alt="Preview"
|
alt="Preview"
|
||||||
|
width={40}
|
||||||
|
height={40}
|
||||||
className="w-10 h-10 object-cover rounded"
|
className="w-10 h-10 object-cover rounded"
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
|
import Image from 'next/image'
|
||||||
import { Download, Eye } from 'lucide-react'
|
import { Download, Eye } from 'lucide-react'
|
||||||
import { Button } from '@/components/ui/button'
|
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'
|
: 'bg-white/10 border border-white/20'
|
||||||
} rounded-lg overflow-hidden`}>
|
} rounded-lg overflow-hidden`}>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<img
|
<Image
|
||||||
src={imageUrl}
|
src={imageUrl}
|
||||||
alt={fileName}
|
alt={fileName}
|
||||||
|
width={300}
|
||||||
|
height={300}
|
||||||
className="w-full h-auto cursor-pointer transition-opacity duration-200"
|
className="w-full h-auto cursor-pointer transition-opacity duration-200"
|
||||||
style={{
|
style={{
|
||||||
opacity: isLoading ? 0 : 1,
|
opacity: isLoading ? 0 : 1,
|
||||||
@ -108,9 +111,11 @@ export function ImageMessage({ imageUrl, fileName, fileSize, isCurrentUser = fal
|
|||||||
onClick={() => setShowFullSize(false)}
|
onClick={() => setShowFullSize(false)}
|
||||||
>
|
>
|
||||||
<div className="relative max-w-full max-h-full">
|
<div className="relative max-w-full max-h-full">
|
||||||
<img
|
<Image
|
||||||
src={imageUrl}
|
src={imageUrl}
|
||||||
alt={fileName}
|
alt={fileName}
|
||||||
|
width={800}
|
||||||
|
height={600}
|
||||||
className="max-w-full max-h-full object-contain"
|
className="max-w-full max-h-full object-contain"
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
/>
|
/>
|
||||||
|
@ -16,7 +16,8 @@ const PhoneInput = React.forwardRef<HTMLInputElement, PhoneInputProps>(
|
|||||||
onChange?.(value)
|
onChange?.(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Фильтруем пропсы, которые могут конфликтовать с IMaskInput
|
// Фильтруем пропсы, которые могут конфликтовать с IMaskInput
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const { min, max, step, ...filteredProps } = props
|
const { min, max, step, ...filteredProps } = props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -61,6 +62,7 @@ const GlassPhoneInput = React.forwardRef<HTMLInputElement, PhoneInputProps>(
|
|||||||
const isEmpty = !value || value.replace(/\D/g, '').length === 0
|
const isEmpty = !value || value.replace(/\D/g, '').length === 0
|
||||||
|
|
||||||
// Фильтруем пропсы, которые могут конфликтовать с IMaskInput
|
// Фильтруем пропсы, которые могут конфликтовать с IMaskInput
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const { min, max, step, onFocus, onBlur, ...filteredProps } = props
|
const { min, max, step, onFocus, onBlur, ...filteredProps } = props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -23,6 +23,7 @@ export function VoicePlayer({ audioUrl, duration = 0, isCurrentUser = false }: V
|
|||||||
if (duration > 0 && (!audioDuration || audioDuration === 0)) {
|
if (duration > 0 && (!audioDuration || audioDuration === 0)) {
|
||||||
setAudioDuration(duration)
|
setAudioDuration(duration)
|
||||||
}
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [duration, audioDuration])
|
}, [duration, audioDuration])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -30,9 +30,11 @@ const generateToken = (payload: AuthTokenPayload): string => {
|
|||||||
return jwt.sign(payload, process.env.JWT_SECRET!, { expiresIn: '30d' })
|
return jwt.sign(payload, process.env.JWT_SECRET!, { expiresIn: '30d' })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const verifyToken = (token: string): AuthTokenPayload => {
|
const verifyToken = (token: string): AuthTokenPayload => {
|
||||||
try {
|
try {
|
||||||
return jwt.verify(token, process.env.JWT_SECRET!) as AuthTokenPayload
|
return jwt.verify(token, process.env.JWT_SECRET!) as AuthTokenPayload
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new GraphQLError('Недействительный токен', {
|
throw new GraphQLError('Недействительный токен', {
|
||||||
extensions: { code: 'UNAUTHENTICATED' }
|
extensions: { code: 'UNAUTHENTICATED' }
|
||||||
@ -94,7 +96,7 @@ function parseLiteral(ast: unknown): unknown {
|
|||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
case Kind.LIST:
|
case Kind.LIST:
|
||||||
return ast.values.map(parseLiteral)
|
return (ast as { values: unknown[] }).values.map(parseLiteral)
|
||||||
default:
|
default:
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@ -774,7 +776,7 @@ export const resolvers = {
|
|||||||
|
|
||||||
const organization = await prisma.organization.create({
|
const organization = await prisma.organization.create({
|
||||||
data: {
|
data: {
|
||||||
inn: validationResults[0]?.data?.inn || `SELLER_${Date.now()}`,
|
inn: (validationResults[0]?.data?.inn as string) || `SELLER_${Date.now()}`,
|
||||||
name: shopName, // Используем tradeMark как основное название
|
name: shopName, // Используем tradeMark как основное название
|
||||||
fullName: sellerName ? `${sellerName} (${shopName})` : `Интернет-магазин "${shopName}"`,
|
fullName: sellerName ? `${sellerName} (${shopName})` : `Интернет-магазин "${shopName}"`,
|
||||||
type: 'SELLER'
|
type: 'SELLER'
|
||||||
@ -788,7 +790,7 @@ export const resolvers = {
|
|||||||
marketplace: validation.marketplace as 'WILDBERRIES' | 'OZON',
|
marketplace: validation.marketplace as 'WILDBERRIES' | 'OZON',
|
||||||
apiKey: validation.apiKey,
|
apiKey: validation.apiKey,
|
||||||
organizationId: organization.id,
|
organizationId: organization.id,
|
||||||
validationData: validation.data
|
validationData: JSON.parse(JSON.stringify(validation.data))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -910,8 +912,8 @@ export const resolvers = {
|
|||||||
where: { id: existingKey.id },
|
where: { id: existingKey.id },
|
||||||
data: {
|
data: {
|
||||||
apiKey,
|
apiKey,
|
||||||
validationData: validationResult.data,
|
validationData: JSON.parse(JSON.stringify(validationResult.data)),
|
||||||
isActive: true
|
isActive: true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -927,7 +929,7 @@ export const resolvers = {
|
|||||||
marketplace,
|
marketplace,
|
||||||
apiKey,
|
apiKey,
|
||||||
organizationId: user.organization.id,
|
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 },
|
where: { id: user.organization.id },
|
||||||
data: updateData,
|
data: updateData,
|
||||||
include: {
|
include: {
|
||||||
@ -1213,7 +1215,7 @@ export const resolvers = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Обновляем организацию
|
// Обновляем организацию
|
||||||
const updatedOrganization = await prisma.organization.update({
|
await prisma.organization.update({
|
||||||
where: { id: user.organization.id },
|
where: { id: user.organization.id },
|
||||||
data: updateData,
|
data: updateData,
|
||||||
include: {
|
include: {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useEffect } from 'react'
|
|
||||||
import { apolloClient } from '@/lib/apollo-client'
|
import { apolloClient } from '@/lib/apollo-client'
|
||||||
|
|
||||||
export const useApolloRefresh = () => {
|
export const useApolloRefresh = () => {
|
||||||
|
@ -27,7 +27,7 @@ const authLink = setContext((operation, { headers }) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Error Link для обработки ошибок
|
// Error Link для обработки ошибок
|
||||||
const errorLink = onError(({ graphQLErrors, networkError, operation, forward }) => {
|
const errorLink = onError(({ graphQLErrors, networkError }) => {
|
||||||
if (graphQLErrors) {
|
if (graphQLErrors) {
|
||||||
graphQLErrors.forEach(({ message, locations, path, extensions }) => {
|
graphQLErrors.forEach(({ message, locations, path, extensions }) => {
|
||||||
console.error(
|
console.error(
|
||||||
|
@ -15,9 +15,12 @@ const s3Config: S3Config = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class S3Service {
|
export class S3Service {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
private static async createSignedUrl(fileName: string, fileType: string): Promise<string> {
|
private static async createSignedUrl(fileName: string, fileType: string): Promise<string> {
|
||||||
// Для простоты пока используем прямую загрузку через fetch
|
// Для простоты пока используем прямую загрузку через fetch
|
||||||
// В продакшене лучше генерировать signed URLs на backend
|
// В продакшене лучше генерировать signed URLs на backend
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
// fileType используется для будущей логики разделения по типам файлов
|
||||||
const timestamp = Date.now()
|
const timestamp = Date.now()
|
||||||
const key = `avatars/${timestamp}-${fileName}`
|
const key = `avatars/${timestamp}-${fileName}`
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user