Добавлен пакет react-hot-toast для уведомлений и обновлены конфигурации. Исправлены сообщения об ошибках на использование toast вместо alert. Обновлены типы GraphQL для поддержки новых полей в заказах. Оптимизировано форматирование расписания работы ПВЗ с группировкой дней.

This commit is contained in:
Bivekich
2025-06-29 03:36:20 +03:00
parent 18e1f3ffb1
commit d501ad3354
11 changed files with 403 additions and 376 deletions

27
package-lock.json generated
View File

@ -61,6 +61,7 @@
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-hook-form": "^7.57.0",
"react-hot-toast": "^2.5.2",
"sonner": "^2.0.5",
"tailwind-merge": "^3.3.0",
"tailwindcss": "^4",
@ -8428,6 +8429,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/goober": {
"version": "2.1.16",
"resolved": "https://registry.npmjs.org/goober/-/goober-2.1.16.tgz",
"integrity": "sha512-erjk19y1U33+XAMe1VTvIONHYoSqE4iS7BYUZfHaqeohLmnC0FdxEh7rQU+6MZ4OajItzjZFSRtVANrQwNq6/g==",
"license": "MIT",
"peerDependencies": {
"csstype": "^3.0.10"
}
},
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
@ -11166,6 +11176,23 @@
"react": "^16.8.0 || ^17 || ^18 || ^19"
}
},
"node_modules/react-hot-toast": {
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.5.2.tgz",
"integrity": "sha512-Tun3BbCxzmXXM7C+NI4qiv6lT0uwGh4oAfeJyNOjYUejTsm35mK9iCaYLGv8cBz9L5YxZLx/2ii7zsIwPtPUdw==",
"license": "MIT",
"dependencies": {
"csstype": "^3.1.3",
"goober": "^2.1.16"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"react": ">=16",
"react-dom": ">=16"
}
},
"node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",

View File

@ -71,6 +71,7 @@
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-hook-form": "^7.57.0",
"react-hot-toast": "^2.5.2",
"sonner": "^2.0.5",
"tailwind-merge": "^3.3.0",
"tailwindcss": "^4",

View File

@ -1,4 +1,4 @@
/** @type {import('tailwindcss').Config} */
/** @type {import('postcss-load-config').Config} */
export default {
plugins: {
'@tailwindcss/postcss': {},

View File

@ -24,6 +24,7 @@ import { ImportProductsModal } from '@/components/catalog/ImportProductsModal'
import { Pagination } from '@/components/ui/pagination'
import { GET_CATEGORIES, GET_PRODUCTS, GET_PRODUCTS_COUNT } from '@/lib/graphql/queries'
import { EXPORT_PRODUCTS } from '@/lib/graphql/mutations'
import toast from 'react-hot-toast'
@ -106,7 +107,7 @@ export default function CatalogPage() {
}
} catch (error) {
console.error('Ошибка экспорта:', error)
alert('Не удалось экспортировать товары')
toast.error('Не удалось экспортировать товары')
} finally {
setExportLoading(false)
}

View File

@ -31,6 +31,7 @@ import {
AlertDialogTitle,
AlertDialogTrigger,
} from '@/components/ui/alert-dialog'
import toast from 'react-hot-toast'
const GET_BALANCE_INVOICES = gql`
query GetBalanceInvoices {
@ -127,7 +128,7 @@ export default function InvoicesPage() {
},
onError: (error) => {
console.error('Ошибка обновления статуса счета:', error)
alert('Ошибка обновления статуса: ' + error.message)
toast.error('Ошибка обновления статуса: ' + error.message)
}
})
@ -175,11 +176,11 @@ export default function InvoicesPage() {
window.URL.revokeObjectURL(url)
document.body.removeChild(a)
} else {
alert('Ошибка получения PDF: ' + (data?.getInvoicePDF?.error || 'Неизвестная ошибка'))
toast.error('Ошибка получения PDF: ' + (data?.getInvoicePDF?.error || 'Неизвестная ошибка'))
}
} catch (error) {
console.error('Ошибка скачивания PDF:', error)
alert('Ошибка скачивания PDF: ' + (error as Error).message)
toast.error('Ошибка скачивания PDF: ' + (error as Error).message)
}
}

View File

@ -1,8 +1,8 @@
@import "tailwindcss";
@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";
@import "tw-animate-css";
@custom-variant dark (&:is(.dark *));
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);

View File

@ -1,15 +1,32 @@
"use client"
import { Toaster } from 'sonner'
import { Toaster } from 'react-hot-toast'
export const ToastProvider = () => {
return (
<Toaster
position="top-right"
richColors
closeButton
expand={false}
duration={4000}
toastOptions={{
duration: 4000,
style: {
background: '#fff',
color: '#333',
borderRadius: '8px',
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
},
success: {
iconTheme: {
primary: '#10B981',
secondary: '#fff',
},
},
error: {
iconTheme: {
primary: '#EF4444',
secondary: '#fff',
},
},
}}
/>
)
}

View File

@ -377,6 +377,8 @@ interface CreateOrderInput {
items: OrderItemInput[]
deliveryAddress?: string
comment?: string
paymentMethod?: string
legalEntityId?: string
}
interface OrderItemInput {
@ -535,18 +537,18 @@ export const resolvers = {
})
},
relatedProducts: async (parent: { id: string }) => {
const product = await prisma.product.findUnique({
where: { id: parent.id },
include: { relatedProducts: { include: { images: { orderBy: { order: 'asc' } } } } }
})
return product?.relatedProducts || []
const product = await prisma.product.findUnique({
where: { id: parent.id },
include: { relatedProducts: { include: { images: { orderBy: { order: 'asc' } } } } }
})
return product?.relatedProducts || []
},
accessoryProducts: async (parent: { id: string }) => {
const product = await prisma.product.findUnique({
where: { id: parent.id },
include: { accessoryProducts: { include: { images: { orderBy: { order: 'asc' } } } } }
})
return product?.accessoryProducts || []
const product = await prisma.product.findUnique({
where: { id: parent.id },
include: { accessoryProducts: { include: { images: { orderBy: { order: 'asc' } } } } }
})
return product?.accessoryProducts || []
}
},
@ -2754,66 +2756,6 @@ export const resolvers = {
}
},
partsIndexCatalogParams: async (_: unknown, {
catalogId,
groupId,
lang = 'ru',
engineId,
generationId,
params,
q
}: {
catalogId: string;
groupId: string;
lang?: 'ru' | 'en';
engineId?: string;
generationId?: string;
params?: string;
q?: string;
}) => {
try {
console.log('🔍 GraphQL resolver partsIndexCatalogParams вызван с параметрами:', {
catalogId,
groupId,
lang,
q
})
// Преобразуем строку params в объект если передан
let parsedParams: Record<string, any> | undefined;
if (params) {
try {
parsedParams = JSON.parse(params);
} catch (error) {
console.warn('⚠️ Не удалось разобрать параметры фильтрации:', params);
}
}
const parameters = await partsIndexService.getCatalogParams(catalogId, groupId, {
lang,
engineId,
generationId,
params: parsedParams,
q
})
if (!parameters) {
console.warn('⚠️ Не удалось получить параметры каталога')
return {
list: [],
paramsQuery: {}
}
}
console.log('✅ Получены параметры каталога:', parameters.list.length)
return parameters
} catch (error) {
console.error('❌ Ошибка в GraphQL resolver partsIndexCatalogParams:', error)
throw new Error('Не удалось получить параметры каталога')
}
},
partsIndexSearchByArticle: async (_: unknown, {
articleNumber,
brandName,
@ -4178,248 +4120,6 @@ export const resolvers = {
}
},
updateProduct: async (_: unknown, {
id,
input,
images = [],
characteristics = [],
options = []
}: {
id: string;
input: ProductInput;
images?: ProductImageInput[];
characteristics?: CharacteristicInput[];
options?: ProductOptionInput[]
}, context: Context) => {
try {
console.log('updateProduct вызван с опциями:', JSON.stringify(options, null, 2))
if (!context.userId) {
throw new Error('Пользователь не авторизован')
}
const { categoryIds, ...productData } = input
// Удаляем старые изображения, характеристики и опции
await prisma.productImage.deleteMany({
where: { productId: id }
})
await prisma.productCharacteristic.deleteMany({
where: { productId: id }
})
await prisma.productOption.deleteMany({
where: { productId: id }
})
// Обновляем основные данные товара
const finalData = { ...productData }
if (input.name && !input.slug) {
finalData.slug = createSlug(input.name)
}
await prisma.product.update({
where: { id },
data: {
...finalData,
categories: categoryIds ? {
set: categoryIds.map((catId: string) => ({ id: catId }))
} : undefined,
images: {
create: images.map((img, index) => ({
...img,
order: img.order ?? index
}))
}
}
})
// Добавляем новые характеристики
for (const char of characteristics) {
let characteristic = await prisma.characteristic.findUnique({
where: { name: char.name }
})
if (!characteristic) {
characteristic = await prisma.characteristic.create({
data: { name: char.name }
})
}
await prisma.productCharacteristic.create({
data: {
productId: id,
characteristicId: characteristic.id,
value: char.value
}
})
}
// Добавляем новые опции
console.log(`Обрабатываем ${options.length} опций для товара ${id}`)
for (const optionInput of options) {
console.log('Обрабатываем опцию:', optionInput)
// Создаём или находим опцию
let option = await prisma.option.findUnique({
where: { name: optionInput.name }
})
if (!option) {
console.log('Создаем новую опцию:', optionInput.name)
option = await prisma.option.create({
data: {
name: optionInput.name,
type: optionInput.type
}
})
} else {
console.log('Найдена существующая опция:', option.id)
}
// Создаём значения опции и связываем с товаром
for (const valueInput of optionInput.values) {
console.log('Обрабатываем значение опции:', valueInput)
// Создаём или находим значение опции
let optionValue = await prisma.optionValue.findFirst({
where: {
optionId: option.id,
value: valueInput.value
}
})
if (!optionValue) {
console.log('Создаем новое значение опции')
optionValue = await prisma.optionValue.create({
data: {
optionId: option.id,
value: valueInput.value,
price: valueInput.price || 0
}
})
} else {
console.log('Найдено существующее значение опции:', optionValue.id)
}
// Связываем товар с опцией и значением
console.log('Создаем связь товар-опция-значение')
await prisma.productOption.create({
data: {
productId: id,
optionId: option.id,
optionValueId: optionValue.id
}
})
}
}
console.log('Все опции обработаны')
// Получаем обновленный товар со всеми связанными данными
const product = await prisma.product.findUnique({
where: { id },
include: {
categories: true,
images: { orderBy: { order: 'asc' } },
options: {
include: {
option: { include: { values: true } },
optionValue: true
}
},
characteristics: { include: { characteristic: true } },
relatedProducts: { include: { images: { orderBy: { order: 'asc' } } } },
accessoryProducts: { include: { images: { orderBy: { order: 'asc' } } } }
}
})
// Создаем запись в истории товара
if (context.userId && product) {
await prisma.productHistory.create({
data: {
productId: id,
action: 'UPDATE',
changes: JSON.stringify({
name: input.name,
article: input.article,
description: input.description,
wholesalePrice: input.wholesalePrice,
retailPrice: input.retailPrice,
stock: input.stock,
isVisible: input.isVisible,
categories: categoryIds,
images: images.length,
characteristics: characteristics.length,
options: options.length
}),
userId: context.userId
}
})
}
// Логируем действие
if (context.headers) {
const { ipAddress, userAgent } = getClientInfo(context.headers)
await createAuditLog({
userId: context.userId,
action: AuditAction.PRODUCT_UPDATE,
details: `Товар "${product?.name}"`,
ipAddress,
userAgent
})
}
return product
} catch (error) {
console.error('Ошибка обновления товара:', error)
if (error instanceof Error) {
throw error
}
throw new Error('Не удалось обновить товар')
}
},
deleteProduct: async (_: unknown, { id }: { id: string }, context: Context) => {
try {
if (!context.userId) {
throw new Error('Пользователь не авторизован')
}
const product = await prisma.product.findUnique({
where: { id }
})
if (!product) {
throw new Error('Товар не найден')
}
await prisma.product.delete({
where: { id }
})
// Логируем действие
if (context.headers) {
const { ipAddress, userAgent } = getClientInfo(context.headers)
await createAuditLog({
userId: context.userId,
action: AuditAction.PRODUCT_DELETE,
details: `Товар "${product.name}"`,
ipAddress,
userAgent
})
}
return true
} catch (error) {
console.error('Ошибка удаления товара:', error)
if (error instanceof Error) {
throw error
}
throw new Error('Не удалось удалить товар')
}
},
updateProductVisibility: async (_: unknown, { id, isVisible }: { id: string; isVisible: boolean }, context: Context) => {
try {
if (!context.userId) {
@ -4668,7 +4368,7 @@ export const resolvers = {
})
}
return {
return {
url: uploadResult.url,
filename,
count: products.length
@ -4965,26 +4665,6 @@ export const resolvers = {
}
},
deleteOption: async (_: unknown, { id }: { id: string }, context: Context) => {
try {
if (!context.userId) {
throw new Error('Пользователь не авторизован')
}
await prisma.option.delete({
where: { id }
})
return true
} catch (error) {
console.error('Ошибка удаления опции:', error)
if (error instanceof Error) {
throw error
}
throw new Error('Не удалось удалить опцию')
}
},
// Клиенты
createClient: async (_: unknown, { input, vehicles = [], discounts = [] }: {
input: ClientInput; vehicles?: ClientVehicleInput[]; discounts?: ClientDiscountInput[]
@ -5755,26 +5435,6 @@ export const resolvers = {
}
},
deleteClientVehicle: async (_: unknown, { id }: { id: string }, context: Context) => {
try {
if (!context.userId) {
throw new Error('Пользователь не авторизован')
}
await prisma.clientVehicle.delete({
where: { id }
})
return true
} catch (error) {
console.error('Ошибка удаления транспорта:', error)
if (error instanceof Error) {
throw error
}
throw new Error('Не удалось удалить транспорт')
}
},
// Адреса доставки
createClientDeliveryAddress: async (_: unknown, { clientId, input }: { clientId: string; input: ClientDeliveryAddressInput }, context: Context) => {
try {
@ -6308,7 +5968,7 @@ export const resolvers = {
console.log('✅ PDF успешно сгенерирован');
return {
success: true,
success: true,
pdfBase64,
filename
};
@ -6571,7 +6231,7 @@ export const resolvers = {
})
const sessionId = Math.random().toString(36).substring(7)
return {
exists: !!client,
client,
@ -6595,7 +6255,7 @@ export const resolvers = {
console.log(`У номера ${phone} уже есть активный код, осталось ${ttl} секунд`)
return {
success: true,
success: true,
sessionId: finalSessionId,
message: `Код уже отправлен. Попробуйте через ${ttl} секунд.`
}
@ -7448,6 +7108,41 @@ export const resolvers = {
? clientId.substring(7)
: clientId
// Проверяем баланс для оплаты с баланса
if (input.paymentMethod === 'balance') {
console.log('createOrder: проверяем баланс для оплаты с баланса')
// Сначала ищем дефолтный активный контракт, если нет - любой активный
let contract = await prisma.clientContract.findFirst({
where: {
clientId: cleanClientId,
isActive: true,
isDefault: true
}
})
if (!contract) {
// Если дефолтного нет, ищем любой активный
contract = await prisma.clientContract.findFirst({
where: {
clientId: cleanClientId,
isActive: true
}
})
}
if (!contract) {
throw new Error('Активный контракт не найден')
}
const availableBalance = (contract.balance || 0) + (contract.creditLimit || 0)
console.log(`createOrder: доступный баланс: ${availableBalance}, сумма заказа: ${totalAmount}`)
if (availableBalance < totalAmount) {
throw new Error('Недостаточно средств на балансе для оплаты заказа')
}
}
const order = await prisma.order.create({
data: {
orderNumber,
@ -7458,7 +7153,7 @@ export const resolvers = {
totalAmount,
finalAmount: totalAmount, // Пока без скидок
deliveryAddress: input.deliveryAddress,
comment: input.comment,
comment: `${input.comment || ''}${input.paymentMethod ? ` | Способ оплаты: ${input.paymentMethod}` : ''}${input.legalEntityId ? ` | ЮЛ ID: ${input.legalEntityId}` : ''}`,
items: {
create: input.items.map(item => ({
productId: item.productId,
@ -7483,6 +7178,52 @@ export const resolvers = {
}
})
// Если оплата с баланса, списываем средства и устанавливаем статус "Оплачен"
if (input.paymentMethod === 'balance') {
console.log('createOrder: списываем средства с баланса')
// Ищем тот же контракт, который использовали для проверки баланса
let contractToUpdate = await prisma.clientContract.findFirst({
where: {
clientId: cleanClientId,
isActive: true,
isDefault: true
}
})
if (!contractToUpdate) {
contractToUpdate = await prisma.clientContract.findFirst({
where: {
clientId: cleanClientId,
isActive: true
}
})
}
if (contractToUpdate) {
await prisma.clientContract.update({
where: {
id: contractToUpdate.id
},
data: {
balance: {
decrement: totalAmount
}
}
})
console.log(`createOrder: списано ${totalAmount}с баланса контракта ${contractToUpdate.contractNumber}`)
// Обновляем статус заказа на "Оплачен"
await prisma.order.update({
where: { id: order.id },
data: { status: 'PAID' }
})
console.log('createOrder: статус заказа изменен на PAID')
}
}
console.log('createOrder: заказ создан:', order.orderNumber)
return order
} catch (error) {
@ -7609,6 +7350,139 @@ export const resolvers = {
}
},
// Resolver для подтверждения платежа
confirmPayment: async (_: unknown, { orderId }: { orderId: string }, context: Context) => {
try {
console.log('confirmPayment: подтверждение платежа для заказа:', orderId)
// Находим заказ
const order = await prisma.order.findUnique({
where: { id: orderId },
include: {
client: true,
items: {
include: {
product: true
}
},
payments: true
}
})
if (!order) {
throw new Error('Заказ не найден')
}
// Если заказ уже оплачен, просто возвращаем его
if (order.status === 'PAID') {
console.log('confirmPayment: заказ уже оплачен')
return order
}
// Обновляем статус заказа на "Оплачен"
const updatedOrder = await prisma.order.update({
where: { id: orderId },
data: { status: 'PAID' },
include: {
client: true,
items: {
include: {
product: true
}
},
payments: true
}
})
console.log('confirmPayment: статус заказа изменен на PAID')
return updatedOrder
} catch (error) {
console.error('Ошибка подтверждения платежа:', error)
if (error instanceof Error) {
throw error
}
throw new Error('Не удалось подтвердить платеж')
}
},
// Resolver для создания платежа
createPayment: async (_: unknown, { input }: { input: CreatePaymentInput }, context: Context) => {
try {
console.log('createPayment: создание платежа для заказа:', input.orderId)
// Находим заказ
const order = await prisma.order.findUnique({
where: { id: input.orderId },
include: { items: true }
})
if (!order) {
throw new Error('Заказ не найден')
}
// Если заказ уже оплачен с баланса, не создаем платеж в ЮКассе
if (order.status === 'PAID') {
console.log('createPayment: заказ уже оплачен с баланса')
// Возвращаем успешный результат без создания платежа в ЮКассе
return {
payment: null,
confirmationUrl: null,
success: true,
message: 'Заказ уже оплачен с баланса'
}
}
// Создаем платеж в ЮКассе
const { yooKassaService } = await import('../yookassa-service')
const payment = await yooKassaService.createPayment({
amount: order.finalAmount,
currency: 'RUB',
description: input.description || `Оплата заказа ${order.orderNumber}`,
returnUrl: input.returnUrl,
metadata: { orderId: order.id }
})
console.log('createPayment: платеж создан в ЮКассе:', payment.id)
// Маппинг статусов YooKassa на GraphQL enum
const mapYooKassaStatus = (status: string) => {
switch (status) {
case 'pending': return 'PENDING'
case 'waiting_for_capture': return 'WAITING_FOR_CAPTURE'
case 'succeeded': return 'SUCCEEDED'
case 'canceled': return 'CANCELED'
default: return 'PENDING'
}
}
return {
payment: {
id: payment.id,
orderId: order.id,
yookassaPaymentId: payment.id,
status: mapYooKassaStatus(payment.status),
amount: parseFloat(payment.amount.value),
currency: payment.amount.currency,
description: payment.description,
confirmationUrl: payment.confirmation?.confirmation_url || null,
createdAt: new Date().toISOString()
},
confirmationUrl: payment.confirmation?.confirmation_url || null,
success: true,
message: 'Платеж успешно создан'
}
} catch (error) {
console.error('Ошибка создания платежа:', error)
if (error instanceof Error) {
throw error
}
throw new Error('Не удалось создать платеж')
}
},
// Мутация для получения офферов доставки
getDeliveryOffers: async (_: unknown, { input }: {
input: {

View File

@ -1777,6 +1777,8 @@ export const typeDefs = gql`
items: [OrderItemInput!]!
deliveryAddress: String
comment: String
paymentMethod: String
legalEntityId: String
}
input OrderItemInput {
@ -1798,8 +1800,10 @@ export const typeDefs = gql`
# Результат создания платежа
type CreatePaymentResult {
payment: Payment!
confirmationUrl: String!
payment: Payment
confirmationUrl: String
success: Boolean!
message: String
}
# PartsAPI типы для категорий автозапчастей

View File

@ -205,7 +205,7 @@ class YandexDeliveryService {
}
/**
* Форматирование расписания работы ПВЗ
* Форматирование расписания работы ПВЗ с группировкой дней
*/
formatSchedule(schedule: YandexPickupPoint['schedule']): string {
if (!schedule.restrictions || schedule.restrictions.length === 0) {
@ -214,12 +214,57 @@ class YandexDeliveryService {
const dayNames = ['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс'];
return schedule.restrictions.map(restriction => {
const days = restriction.days.map(day => dayNames[day - 1]).join(', ');
// Группируем ограничения по времени
const timeGroups: Record<string, number[]> = {};
schedule.restrictions.forEach(restriction => {
const timeFrom = `${restriction.time_from.hours.toString().padStart(2, '0')}:${restriction.time_from.minutes.toString().padStart(2, '0')}`;
const timeTo = `${restriction.time_to.hours.toString().padStart(2, '0')}:${restriction.time_to.minutes.toString().padStart(2, '0')}`;
const timeRange = `${timeFrom}-${timeTo}`;
return `${days}: ${timeFrom}-${timeTo}`;
if (!timeGroups[timeRange]) {
timeGroups[timeRange] = [];
}
timeGroups[timeRange].push(...restriction.days);
});
// Форматируем каждую группу времени
return Object.entries(timeGroups).map(([timeRange, days]) => {
// Убираем дубликаты и сортируем дни
const uniqueDays = [...new Set(days)].sort((a, b) => a - b);
// Группируем последовательные дни в диапазоны
const dayRanges: string[] = [];
let rangeStart = uniqueDays[0];
let rangeEnd = uniqueDays[0];
for (let i = 1; i < uniqueDays.length; i++) {
const currentDay = uniqueDays[i];
const prevDay = uniqueDays[i - 1];
// Если дни идут подряд, расширяем диапазон
if (currentDay === prevDay + 1) {
rangeEnd = currentDay;
} else {
// Завершаем текущий диапазон и начинаем новый
if (rangeStart === rangeEnd) {
dayRanges.push(dayNames[rangeStart - 1]);
} else {
dayRanges.push(`${dayNames[rangeStart - 1]}-${dayNames[rangeEnd - 1]}`);
}
rangeStart = currentDay;
rangeEnd = currentDay;
}
}
// Добавляем последний диапазон
if (rangeStart === rangeEnd) {
dayRanges.push(dayNames[rangeStart - 1]);
} else {
dayRanges.push(`${dayNames[rangeStart - 1]}-${dayNames[rangeEnd - 1]}`);
}
return `${dayRanges.join(', ')}: ${timeRange}`;
}).join('; ');
}

57
tailwind.config.js Normal file
View File

@ -0,0 +1,57 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
'./src/pages/**/*.{js,ts,jsx,tsx,mdx}',
'./src/components/**/*.{js,ts,jsx,tsx,mdx}',
'./src/app/**/*.{js,ts,jsx,tsx,mdx}',
],
theme: {
extend: {
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
popover: {
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
},
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
fontFamily: {
sans: ["var(--font-geist-sans)", "sans-serif"],
mono: ["var(--font-geist-mono)", "monospace"],
},
},
},
plugins: [],
}