Добавлены новые зависимости, обновлены стили и улучшена структура проекта. Обновлен 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

125
src/lib/apollo-client.ts Normal file
View File

@ -0,0 +1,125 @@
import { ApolloClient, InMemoryCache, createHttpLink, from } from '@apollo/client'
import { setContext } from '@apollo/client/link/context'
import { onError } from '@apollo/client/link/error'
// HTTP Link для GraphQL запросов
const httpLink = createHttpLink({
uri: '/api/graphql',
})
// Auth Link для добавления JWT токена в заголовки
const authLink = setContext((operation, { headers }) => {
// Получаем токен из localStorage каждый раз
const token = typeof window !== 'undefined' ? localStorage.getItem('authToken') : null
console.log(`Apollo Client - Operation: ${operation.operationName}, Token:`, token ? `${token.substring(0, 20)}...` : 'No token')
const authHeaders = {
...headers,
authorization: token ? `Bearer ${token}` : '',
}
console.log('Apollo Client - Auth headers:', { authorization: authHeaders.authorization ? 'Bearer ***' : 'No auth' })
return {
headers: authHeaders
}
})
// Error Link для обработки ошибок
const errorLink = onError(({ graphQLErrors, networkError, operation, forward }) => {
if (graphQLErrors) {
graphQLErrors.forEach(({ message, locations, path, extensions }) => {
console.error(
`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`
)
// Если токен недействителен, очищаем localStorage и перенаправляем на авторизацию
// Но не делаем редирект если пользователь уже на главной странице (в процессе авторизации)
if (extensions?.code === 'UNAUTHENTICATED') {
if (typeof window !== 'undefined') {
localStorage.removeItem('authToken')
localStorage.removeItem('userData')
// Перенаправляем на страницу авторизации только если не находимся на ней
if (window.location.pathname !== '/') {
window.location.href = '/'
}
}
}
})
}
if (networkError) {
console.error(`[Network error]: ${networkError}`)
}
})
// Создаем Apollo Client
export const apolloClient = new ApolloClient({
link: from([
errorLink,
authLink,
httpLink,
]),
cache: new InMemoryCache({
typePolicies: {
User: {
fields: {
organization: {
merge: true,
},
},
},
Organization: {
fields: {
apiKeys: {
merge: false,
},
},
},
},
}),
defaultOptions: {
watchQuery: {
errorPolicy: 'all',
},
query: {
errorPolicy: 'all',
},
},
})
// Утилитарные функции для работы с токеном и пользователем
export const setAuthToken = (token: string) => {
if (typeof window !== 'undefined') {
localStorage.setItem('authToken', token)
}
}
export const removeAuthToken = () => {
if (typeof window !== 'undefined') {
localStorage.removeItem('authToken')
localStorage.removeItem('userData')
}
}
export const getAuthToken = (): string | null => {
if (typeof window !== 'undefined') {
return localStorage.getItem('authToken')
}
return null
}
export const setUserData = (userData: unknown) => {
if (typeof window !== 'undefined') {
localStorage.setItem('userData', JSON.stringify(userData))
}
}
export const getUserData = (): unknown | null => {
if (typeof window !== 'undefined') {
const data = localStorage.getItem('userData')
return data ? JSON.parse(data) : null
}
return null
}

11
src/lib/prisma.ts Normal file
View File

@ -0,0 +1,11 @@
import { PrismaClient } from '@prisma/client'
declare global {
var prisma: PrismaClient | undefined
}
export const prisma = globalThis.prisma || new PrismaClient()
if (process.env.NODE_ENV !== 'production') {
globalThis.prisma = prisma
}

25
src/lib/utils.ts Normal file
View File

@ -0,0 +1,25 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
// Функция для форматирования номера телефона
export function formatPhone(phone: string): string {
if (!phone) return ''
// Убираем все кроме цифр
const digits = phone.replace(/\D/g, '')
// Если номер начинается с 8, заменяем на 7
const normalizedDigits = digits.startsWith('8') ? '7' + digits.slice(1) : digits
// Проверяем длину номера
if (normalizedDigits.length !== 11 || !normalizedDigits.startsWith('7')) {
return phone // Возвращаем как есть, если формат неправильный
}
// Форматируем как +7 (999) 999-99-99
return `+7 (${normalizedDigits.slice(1, 4)}) ${normalizedDigits.slice(4, 7)}-${normalizedDigits.slice(7, 9)}-${normalizedDigits.slice(9, 11)}`
}