api ulandi

This commit is contained in:
Samandar Turgunboyev
2025-10-29 18:41:59 +05:00
parent a9e99f9755
commit 2d0285dafc
64 changed files with 6319 additions and 2352 deletions

View File

@@ -20,9 +20,19 @@ const NEWS_CATEGORY = "dashboard/dashboard-category/";
const HOTEL = "dashboard/dashboard-hotel/";
const FAQ = "dashboard/dashboard-faq/";
const FAQ_CATEGORIES = "dashboard/dashboard-faq-category/";
const SITE_SEO = "dashboard/dashboard-site-seo/";
const OFFERTA = "dashboard/dashboard-site-offerta/";
const HELP_PAGE = "dashboard/dashboard-site-help-page/";
const SITE_SETTING = "dashboard/dashboard-site-settings/";
const SUPPORT_USER = "dashboard/dashboard-support/";
const SUPPORT_AGENCY = "dashboard/dashboard-travel-agency-request/";
const USER_ORDERS = "dashboard/dashboard-ticket-order/";
const POPULAR_TOURS = "dashboard/dashboard-ticket-featured/";
const BANNER = "dashboard/dashboard-site-banner/";
export {
AUTH_LOGIN,
BANNER,
BASE_URL,
DOWNLOAD_PDF,
FAQ,
@@ -32,6 +42,7 @@ export {
GET_ALL_USERS,
GET_ME,
GET_TICKET,
HELP_PAGE,
HOTEL,
HOTEL_BADGE,
HOTEL_FEATURES,
@@ -40,6 +51,13 @@ export {
HPTEL_TYPES,
NEWS,
NEWS_CATEGORY,
OFFERTA,
POPULAR_TOURS,
SITE_SEO,
SITE_SETTING,
SUPPORT_AGENCY,
SUPPORT_USER,
TOUR_TRANSPORT,
UPDATE_USER,
USER_ORDERS,
};

View File

@@ -328,5 +328,143 @@
"Breakfast Only": "Только завтрак",
"Half Board": "Полупансион (завтрак и обед или ужин)",
"Full Board": "Полный пансион (завтрак, обед и ужин)",
"All Inclusive": "Всё включено (питание, напитки и услуги полностью)"
"All Inclusive": "Всё включено (питание, напитки и услуги полностью)",
"(1 kishi uchun)": "(на 1 человека)",
"hamrohlar soni (eng kamida)": "количество компаньонов (минимум)",
"hamrohlar soni (eng ko'pida)": "количество компаньонов (максимальное)",
"Yo'lovchilar soni": "Количество пассажиров",
"Tarifni tanlang": "Выберите тариф",
"Mavjud tariflar": "Доступные тарифы",
"Transport tanlang": "Выбрать транспорт",
"Mavjud transportlar": "Доступные транспортные средства",
"Visa talab qilinadimi": "Требуется ли виза",
"Ha": "Да",
"Yo'q": "Нет",
"Banner": "Баннер",
"Faqat bitta rasm yuklash mumkin": "Можно загрузить только одно изображение",
"Qo'shimcha rasmlar": "Дополнительные изображения",
"Qulaylik nomi (ru)": "Название удобства (ru)",
"Yangi xizmat qo'shish": "Добавить новую услугу",
"Taom nomi": "Название блюда",
"Taom tavsifi": "Описание блюда",
"Yo'nalishlar": "Направления",
"Yo'nalish qo'shish": "Добавить маршрут",
"Davomiylik (kun)": "Продолжительность (дни)",
"Bu bo'limda savollar yo'q": "В этом разделе нет вопросов",
"Yangi FAQ qo'shish": "Добавить новый FAQ",
"Pending Payment": "Ожидается оплата",
"Pending Confirmation": "Ожидается подтверждение",
"Confirmed": "Подтверждено",
"Completed": "Выполнено",
"pending": "В ожидании",
"done": "Выполнено",
"failed": "Неудачно",
"Yordam so'rovlari": "Запросы помощи",
"Batafsil ko'rish": "Подробнее",
"Agentlikka tegishli": "Принадлежащий агентству",
"Yopish": "Закрыть",
"Yakunlandi deb belgilash": "Отметить как завершенное",
"Kutilmoqda deb belgilash": "Отметить как ожидаемое",
"Natija topilmadi": "Результат не найден",
"Barchasi": "Все",
"Sayt bo'yicha": "На сайте",
"Diqqat! O'chirish": "Внимание! Удалить",
"Siz rostdan ham ushbu so'rovni o'chirmoqchimisiz?": "Вы уверены, что хотите удалить этот запрос?",
"O'chirishda xatolik yuz berdi": "Ошибка при удалении",
"Muvaffaqiyatli o'chirildi": "Удалено успешно",
"Agentlik so'rovlari": "Агентские запросы",
"Qidiruv (ism, email yoki telefon)...": "Поиск (имя, email или телефон)...",
"Tozalash": "Очистка",
"So'rov topilmadi": "Запрос не найден",
"Tafsilotlar": "Подробности",
"Javob yozish": "Написать ответ",
"Hujjatlar": "Документы",
"Hujjat": "Документ",
"Hujjat topilmadi": "Документ не найден",
"Qabul qilish": "Принять",
"Rad etish": "Отказ",
"Popular": "Популярный",
"SEO Manager": "SEO Менеджер",
"Saytingizni qidiruv tizimida yaxshi pozitsiyaga keltiring": "Продвиньте свой сайт в поисковых системах",
"Page Title": "Заголовок страницы",
"Sahifa sarlavhasi (3060 belgi)": "Заголовок страницы (3060 символов)",
"Meta Description": "Мета описание",
"Sahifa tavsifi (120160 belgi)": "Описание страницы (120160 символов)",
"Keywords": "Ключевые слова",
"Kalit so'zlar (vergul bilan ajratilgan)": "Ключевые слова (через запятую)",
"Masalan: Python, Web Development, Coding": "Например: Python, Web Development, Coding",
"Open Graph (Ijtimoiy Tarmoqlar)": "Open Graph (Социальные сети)",
"OG Title": "OG Заголовок",
"Ijtimoiy tarmoqdagi sarlavha": "Заголовок в социальных сетях",
"OG Description": "OG Описание",
"Ijtimoiy tarmoqdagi tavsif": "Описание в социальных сетях",
"OG Image": "OG Изображение",
"Saqlangan SEO Malumotlari": "Сохранённые SEO данные",
"Hozircha SEO malumotlari mavjud emas.": "Пока нет данных по SEO.",
"Malumotlar muvaffaqiyatli saqlandi": "Данные успешно сохранены",
"Muvaffaqiyatli yaratildi": "Успешно создано",
"Muvaffaqiyatli yangilandi": "Успешно обновлено",
"Sarlavha kiritish majburiy": "Необходимо ввести заголовок",
"Sarlavha kamida 3 ta belgidan iborat bo'lishi kerak": "Заголовок должен содержать не менее 3 символов",
"Kontent kiritish majburiy": "Необходимо ввести содержимое",
"Kontent kamida 10 ta belgidan iborat bo'lishi kerak": "Содержимое должно содержать не менее 10 символов",
"Kimlar uchun degan maydonni tanlang": "Выберите поле «Для кого»",
"Iltimos, barcha majburiy maydonlarni to'ldiring": "Пожалуйста, заполните все обязательные поля",
"Ommaviy oferta": "Публичная оферта",
"Yangi oferta yaratish": "Создать новую оферту",
"Ommaviy oferta sarlavhasi": "Заголовок публичной оферты",
"Kontent": "Содержимое",
"Oferta matnini kiriting...": "Введите текст оферты...",
"Kimlar uchun": "Для кого",
"Barcha": "Все",
"Jismoniy shaxslar uchun": "Для физических лиц",
"Yuridik shaxslar uchun": "Для юридических лиц",
"O'chirish tasdiqlash": "Подтверждение удаления",
"Haqiqatan ham bu ofertani o'chirmoqchimisiz? Bu amalni bekor qilib bo'lmaydi.": "Вы действительно хотите удалить эту оферту? Это действие нельзя отменить.",
"Yordam sahifalari boshqaruvi": "Управление страницами помощи",
"Yangi yordam sahifasi yaratish": "Создать новую страницу помощи",
"Yordam sahifasi sarlavhasi": "Заголовок страницы помощи",
"Yordam matnini kiriting...": "Введите текст помощи...",
"Sahifa turi": "Тип страницы",
"Sahifa turini tanlang": "Выберите тип страницы",
"Qollanma": "Инструкция",
"Maxfiylik siyosati": "Политика конфиденциальности",
"Faol emas": "Неактивно",
"Yaratish": "Создать",
"Natija topilmadi.": "Результаты не найдены.",
"Ochirishni tasdiqlash": "Подтверждение удаления",
"Haqiqatan ham bu yordam sahifasini ochirmoqchimisiz? Bu amalni bekor qilib bolmaydi.": "Вы действительно хотите удалить эту страницу помощи? Это действие нельзя отменить.",
"Contact settings": "Настройки контактов",
"Hozircha kontakt ma'lumotlari qo'shilmagan": "Пока нет контактных данных",
"Sayt uchun telegram, instagram, manzil, email va telefonni bu yerda saqlang. Siz faqat bir marta qo'sha olasiz — keyin tahrirlash mumkin.": "Сохраните свои Telegram, Instagram, адрес, адрес электронной почты и телефон для сайта здесь. Вы можете добавить только один раз - затем вы можете редактировать.",
"Kontakt ma'lumotlari": "Контактная информация",
"Kontaktni tahrirlash": "Редактировать контакт",
"Kontakt qo'shish": "Добавить контакт",
"Asosiy telefon": "Основной телефон",
"Qo'shimcha telefon": "Дополнительный телефон",
"Muvaffaqiyatli saqlandi": "Успешно сохранено",
"Mehmonxona reytingi": "Рейтинг отеля",
"Taom rejasi": "План питания",
"Tanlang": "Выберите",
"Mehmonxona turlari": "Типы отелей",
"Yana tanlang...": "Выберите ещё...",
"Mehmonxona xususiyatlari": "Характеристики отеля",
"Xususiyat turlari": "Типы свойств",
"Sayt uchun Banner": "Баннер для сайта",
"Sayt Bannerlari": "Баннеры сайта",
"Bannerlarni boshqarish": "Управление баннерами",
"Rasm": "Изображение",
"Tavsif": "Описание",
"Joylashuvi": "Расположение",
"Hozircha bannerlar mavjud emas": "Пока нет баннеров",
"Bannerni tahrirlash": "Редактировать баннер",
"Yangi banner qo'shish": "Добавить новый баннер",
"Havola URL": "URL адрес",
"Asosiy": "Основная",
"Kun taklifi": "Приглашение дня",
"Mashhur yonalishlar": "Известные направления",
"Reytingi baland turlar": "Высокорейтинговые туры",
"Status muvaffaqiyatli yangilandi": "Статус успешно обновлён",
"Statusni yangilashda xatolik yuz berdi": "Ошибка обновления статуса",
"Refunded": "Подтверждено"
}

View File

@@ -38,6 +38,8 @@
"FAQ Kategoriyalar": "FAQ Kategoriyalar",
"Foydalanuvchini o'chirish": "Foydalanuvchini o'chirish",
"Siz": "Siz",
"(1 kishi uchun)": "(1 kishi uchun)",
"hamrohlar soni (eng kamida)": "hamrohlar soni (eng kamida)",
"foydalanuvchini o'chirmoqchimisiz?": "foydalanuvchini o'chirmoqchimisiz?",
"Ushbu amalni qaytarib bo'lmaydi": "Ushbu amalni qaytarib bo'lmaydi.",
"Bekor qilish": "Bekor qilish",
@@ -303,6 +305,8 @@
"Yangiliklar soni": "Yangiliklar soni",
"Harakatlar": "Harakatlar",
"Hech qanday kategoriya topilmadi": "Hech qanday kategoriya topilmadi",
"Natija topilmadi": "Natija topilmadi",
"Yangi kategoriya": "Yangi kategoriya",
"Kategoriya tahrirlash": "Kategoriya tahrirlash",
"Yangi kategoriya qoshish": "Yangi kategoriya qoshish",
"FAQ (Savol va javoblar)": "FAQ (Savol va javoblar)",
@@ -328,5 +332,140 @@
"Breakfast Only": "Faqat nonushta",
"Half Board": "Yarim pansion (nonushta va tushlik yoki kechki ovqat)",
"Full Board": "Toliq pansion (nonushta, tushlik va kechki ovqat)",
"All Inclusive": "Toliq pansion (nonushta, tushlik va kechki ovqat)"
"All Inclusive": "Toliq pansion (nonushta, tushlik va kechki ovqat)",
"hamrohlar soni (eng ko'pida)": "hamrohlar soni (eng ko'pida)",
"Yo'lovchilar soni": "Yo'lovchilar soni",
"Tarifni tanlang": "Tarifni tanlang",
"Mavjud tariflar": "Mavjud tariflar",
"Transport tanlang": "Transport tanlang",
"Mavjud transportlar": "Mavjud transportlar",
"Visa talab qilinadimi": "Visa talab qilinadimi",
"Ha": "Ha",
"Yo'q": "Yo'q",
"Banner": "Banner",
"Faqat bitta rasm yuklash mumkin": "Faqat bitta rasm yuklash mumkin",
"Qo'shimcha rasmlar": "Qo'shimcha rasmlar",
"Qulaylik nomi (ru)": "Qulaylik nomi (ru)",
"Yangi xizmat qo'shish": "Yangi xizmat qo'shish",
"Taom nomi": "Taom nomi",
"Taom tavsifi": "Taom tavsifi",
"Yo'nalishlar": "Yo'nalishlar",
"Yo'nalish qo'shish": "Yo'nalish qo'shish",
"Davomiylik (kun)": "Davomiylik (kun)",
"Bu bo'limda savollar yo'q": "Bu bo'limda savollar yo'q",
"Yangi FAQ qo'shish": "Yangi FAQ qo'shish",
"Pending Payment": "Tolov kutilmoqda",
"Pending Confirmation": "Tasdiqlash kutilmoqda",
"Confirmed": "Tasdiqlangan",
"Completed": "Bajarilgan",
"pending": "Kutilmoqda",
"done": "Yakunlangan",
"failed": "Muvaffaqiyatsiz",
"Yordam so'rovlari": "Yordam so'rovlari",
"Batafsil ko'rish": "Batafsil ko'rish",
"Agentlikka tegishli": "Agentlikka tegishli",
"Yopish": "Yopish",
"Yakunlandi deb belgilash": "Yakunlandi deb belgilash",
"Kutilmoqda deb belgilash": "Kutilmoqda deb belgilash",
"Barchasi": "Barchasi",
"Sayt bo'yicha": "Sayt bo'yicha",
"Diqqat! O'chirish": "Diqqat! O'chirish",
"Siz rostdan ham ushbu so'rovni o'chirmoqchimisiz?": "Siz rostdan ham ushbu so'rovni o'chirmoqchimisiz?",
"O'chirishda xatolik yuz berdi": "O'chirishda xatolik yuz berdi",
"Muvaffaqiyatli o'chirildi": "Muvaffaqiyatli o'chirildi",
"Agentlik so'rovlari": "Agentlik so'rovlari",
"Qidiruv (ism, email yoki telefon)...": "Qidiruv (ism, email yoki telefon)...",
"Tozalash": "Tozalash",
"So'rov topilmadi": "So'rov topilmadi",
"Tafsilotlar": "Tafsilotlar",
"Javob yozish": "Javob yozish",
"Hujjatlar": "Hujjatlar",
"Hujjat": "Hujjat",
"Hujjat topilmadi": "Hujjat topilmadi",
"Qabul qilish": "Qabul qilish",
"Rad etish": "Rad etish",
"Popular": "Mashhur",
"SEO Manager": "SEO Menejer",
"Saytingizni qidiruv tizimida yaxshi pozitsiyaga keltiring": "Saytingizni qidiruv tizimida yaxshi pozitsiyaga keltiring",
"Page Title": "Sahifa sarlavhasi",
"Sahifa sarlavhasi (3060 belgi)": "Sahifa sarlavhasi (3060 belgi)",
"Meta Description": "Meta tavsif",
"Sahifa tavsifi (120160 belgi)": "Sahifa tavsifi (120160 belgi)",
"Keywords": "Kalit sozlar",
"Kalit so'zlar (vergul bilan ajratilgan)": "Kalit sozlar (vergul bilan ajratilgan)",
"Masalan: Python, Web Development, Coding": "Masalan: Python, Web Development, Coding",
"Open Graph (Ijtimoiy Tarmoqlar)": "Open Graph (Ijtimoiy tarmoqlar)",
"OG Title": "OG Sarlavha",
"Ijtimoiy tarmoqdagi sarlavha": "Ijtimoiy tarmoqdagi sarlavha",
"OG Description": "OG Tavsif",
"Ijtimoiy tarmoqdagi tavsif": "Ijtimoiy tarmoqdagi tavsif",
"OG Image": "OG Rasm",
"Saqlangan SEO Malumotlari": "Saqlangan SEO malumotlari",
"Hozircha SEO malumotlari mavjud emas.": "Hozircha SEO malumotlari mavjud emas.",
"Malumotlar muvaffaqiyatli saqlandi": "Malumotlar muvaffaqiyatli saqlandi",
"Muvaffaqiyatli yaratildi": "Muvaffaqiyatli yaratildi",
"Muvaffaqiyatli yangilandi": "Muvaffaqiyatli yangilandi",
"Sarlavha kiritish majburiy": "Sarlavha kiritish majburiy",
"Sarlavha kamida 3 ta belgidan iborat bo'lishi kerak": "Sarlavha kamida 3 ta belgidan iborat bo'lishi kerak",
"Kontent kiritish majburiy": "Kontent kiritish majburiy",
"Kontent kamida 10 ta belgidan iborat bo'lishi kerak": "Kontent kamida 10 ta belgidan iborat bo'lishi kerak",
"Kimlar uchun degan maydonni tanlang": "Kimlar uchun degan maydonni tanlang",
"Iltimos, barcha majburiy maydonlarni to'ldiring": "Iltimos, barcha majburiy maydonlarni to'ldiring",
"Ommaviy oferta": "Ommaviy oferta",
"Yangi oferta yaratish": "Yangi oferta yaratish",
"Ommaviy oferta sarlavhasi": "Ommaviy oferta sarlavhasi",
"Kontent": "Kontent",
"Oferta matnini kiriting...": "Oferta matnini kiriting...",
"Kimlar uchun": "Kimlar uchun",
"Barcha": "Barcha",
"Jismoniy shaxslar uchun": "Jismoniy shaxslar uchun",
"Yuridik shaxslar uchun": "Yuridik shaxslar uchun",
"O'chirish tasdiqlash": "O'chirish tasdiqlash",
"Haqiqatan ham bu ofertani o'chirmoqchimisiz? Bu amalni bekor qilib bo'lmaydi.": "Haqiqatan ham bu ofertani o'chirmoqchimisiz? Bu amalni bekor qilib bo'lmaydi.",
"Yordam sahifalari boshqaruvi": "Yordam sahifalari boshqaruvi",
"Yangi yordam sahifasi yaratish": "Yangi yordam sahifasi yaratish",
"Yordam sahifasi sarlavhasi": "Yordam sahifasi sarlavhasi",
"Yordam matnini kiriting...": "Yordam matnini kiriting...",
"Sahifa turi": "Sahifa turi",
"Sahifa turini tanlang": "Sahifa turini tanlang",
"Qollanma": "Qollanma",
"Maxfiylik siyosati": "Maxfiylik siyosati",
"Faol emas": "Faol emas",
"Yaratish": "Yaratish",
"Natija topilmadi.": "Natija topilmadi.",
"Ochirishni tasdiqlash": "Ochirishni tasdiqlash",
"Haqiqatan ham bu yordam sahifasini ochirmoqchimisiz? Bu amalni bekor qilib bolmaydi.": "Haqiqatan ham bu yordam sahifasini ochirmoqchimisiz? Bu amalni bekor qilib bolmaydi.",
"Contact settings": "Kontakt sozlamalari",
"Hozircha kontakt ma'lumotlari qo'shilmagan": "Hozircha kontakt ma'lumotlari qo'shilmagan",
"Sayt uchun telegram, instagram, manzil, email va telefonni bu yerda saqlang. Siz faqat bir marta qo'sha olasiz — keyin tahrirlash mumkin.": "Sayt uchun telegram, instagram, manzil, email va telefonni bu yerda saqlang. Siz faqat bir marta qo'sha olasiz — keyin tahrirlash mumkin.",
"Kontakt ma'lumotlari": "Kontakt ma'lumotlari",
"Kontaktni tahrirlash": "Kontaktni tahrirlash",
"Kontakt qo'shish": "Kontakt qo'shish",
"Asosiy telefon": "Asosiy telefon",
"Qo'shimcha telefon": "Qo'shimcha telefon",
"Muvaffaqiyatli saqlandi": "Muvaffaqiyatli saqlandi",
"Mehmonxona reytingi": "Mehmonxona reytingi",
"Taom rejasi": "Taom rejasi",
"Tanlang": "Tanlang",
"Mehmonxona turlari": "Mehmonxona turlari",
"Yana tanlang...": "Yana tanlang...",
"Mehmonxona xususiyatlari": "Mehmonxona xususiyatlari",
"Xususiyat turlari": "Xususiyat turlari",
"Sayt uchun Banner": "Sayt uchun Banner",
"Sayt Bannerlari": "Sayt Bannerlari",
"Bannerlarni boshqarish": "Bannerlarni boshqarish",
"Rasm": "Rasm",
"Tavsif": "Tavsif",
"Joylashuvi": "Joylashuvi",
"Hozircha bannerlar mavjud emas": "Hozircha bannerlar mavjud emas",
"Bannerni tahrirlash": "Bannerni tahrirlash",
"Yangi banner qo'shish": "Yangi banner qo'shish",
"Havola URL": "Havola URL",
"Asosiy": "Asosiy",
"Kun taklifi": "Kun taklifi",
"Mashhur yonalishlar": "Mashhur yonalishlar",
"Reytingi baland turlar": "Reytingi baland turlar",
"Status muvaffaqiyatli yangilandi": "Status muvaffaqiyatli yangilandi",
"Statusni yangilashda xatolik yuz berdi": "Statusni yangilashda xatolik yuz berdi",
"Refunded": "Tasdiqlangan"
}

239
src/shared/ui/carousel.tsx Normal file
View File

@@ -0,0 +1,239 @@
import * as React from "react"
import useEmblaCarousel, {
type UseEmblaCarouselType,
} from "embla-carousel-react"
import { ArrowLeft, ArrowRight } from "lucide-react"
import { cn } from "@/shared/lib/utils"
import { Button } from "@/shared/ui/button"
type CarouselApi = UseEmblaCarouselType[1]
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>
type CarouselOptions = UseCarouselParameters[0]
type CarouselPlugin = UseCarouselParameters[1]
type CarouselProps = {
opts?: CarouselOptions
plugins?: CarouselPlugin
orientation?: "horizontal" | "vertical"
setApi?: (api: CarouselApi) => void
}
type CarouselContextProps = {
carouselRef: ReturnType<typeof useEmblaCarousel>[0]
api: ReturnType<typeof useEmblaCarousel>[1]
scrollPrev: () => void
scrollNext: () => void
canScrollPrev: boolean
canScrollNext: boolean
} & CarouselProps
const CarouselContext = React.createContext<CarouselContextProps | null>(null)
function useCarousel() {
const context = React.useContext(CarouselContext)
if (!context) {
throw new Error("useCarousel must be used within a <Carousel />")
}
return context
}
function Carousel({
orientation = "horizontal",
opts,
setApi,
plugins,
className,
children,
...props
}: React.ComponentProps<"div"> & CarouselProps) {
const [carouselRef, api] = useEmblaCarousel(
{
...opts,
axis: orientation === "horizontal" ? "x" : "y",
},
plugins
)
const [canScrollPrev, setCanScrollPrev] = React.useState(false)
const [canScrollNext, setCanScrollNext] = React.useState(false)
const onSelect = React.useCallback((api: CarouselApi) => {
if (!api) return
setCanScrollPrev(api.canScrollPrev())
setCanScrollNext(api.canScrollNext())
}, [])
const scrollPrev = React.useCallback(() => {
api?.scrollPrev()
}, [api])
const scrollNext = React.useCallback(() => {
api?.scrollNext()
}, [api])
const handleKeyDown = React.useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.key === "ArrowLeft") {
event.preventDefault()
scrollPrev()
} else if (event.key === "ArrowRight") {
event.preventDefault()
scrollNext()
}
},
[scrollPrev, scrollNext]
)
React.useEffect(() => {
if (!api || !setApi) return
setApi(api)
}, [api, setApi])
React.useEffect(() => {
if (!api) return
onSelect(api)
api.on("reInit", onSelect)
api.on("select", onSelect)
return () => {
api?.off("select", onSelect)
}
}, [api, onSelect])
return (
<CarouselContext.Provider
value={{
carouselRef,
api: api,
opts,
orientation:
orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
scrollPrev,
scrollNext,
canScrollPrev,
canScrollNext,
}}
>
<div
onKeyDownCapture={handleKeyDown}
className={cn("relative", className)}
role="region"
aria-roledescription="carousel"
data-slot="carousel"
{...props}
>
{children}
</div>
</CarouselContext.Provider>
)
}
function CarouselContent({ className, ...props }: React.ComponentProps<"div">) {
const { carouselRef, orientation } = useCarousel()
return (
<div
ref={carouselRef}
className="overflow-hidden"
data-slot="carousel-content"
>
<div
className={cn(
"flex",
orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
className
)}
{...props}
/>
</div>
)
}
function CarouselItem({ className, ...props }: React.ComponentProps<"div">) {
const { orientation } = useCarousel()
return (
<div
role="group"
aria-roledescription="slide"
data-slot="carousel-item"
className={cn(
"min-w-0 shrink-0 grow-0 basis-full",
orientation === "horizontal" ? "pl-4" : "pt-4",
className
)}
{...props}
/>
)
}
function CarouselPrevious({
className,
variant = "outline",
size = "icon",
...props
}: React.ComponentProps<typeof Button>) {
const { orientation, scrollPrev, canScrollPrev } = useCarousel()
return (
<Button
data-slot="carousel-previous"
variant={variant}
size={size}
className={cn(
"absolute size-8 rounded-full",
orientation === "horizontal"
? "top-1/2 -left-12 -translate-y-1/2"
: "-top-12 left-1/2 -translate-x-1/2 rotate-90",
className
)}
disabled={!canScrollPrev}
onClick={scrollPrev}
{...props}
>
<ArrowLeft />
<span className="sr-only">Previous slide</span>
</Button>
)
}
function CarouselNext({
className,
variant = "outline",
size = "icon",
...props
}: React.ComponentProps<typeof Button>) {
const { orientation, scrollNext, canScrollNext } = useCarousel()
return (
<Button
data-slot="carousel-next"
variant={variant}
size={size}
className={cn(
"absolute size-8 rounded-full",
orientation === "horizontal"
? "top-1/2 -right-12 -translate-y-1/2"
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
className
)}
disabled={!canScrollNext}
onClick={scrollNext}
{...props}
>
<ArrowRight />
<span className="sr-only">Next slide</span>
</Button>
)
}
export {
type CarouselApi,
Carousel,
CarouselContent,
CarouselItem,
CarouselPrevious,
CarouselNext,
}

View File

@@ -0,0 +1,104 @@
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from "@/shared/ui/select";
import { useTranslation } from "react-i18next";
interface InfiniteScrollSelectProps<T> {
value: string;
onValueChange: (value: string) => void;
placeholder: string;
label: string;
data: T[];
hasNextPage?: boolean;
isFetchingNextPage?: boolean;
fetchNextPage: () => void;
renderOption: (item: T) => {
key: string | number;
value: string;
label: string;
};
isLoading?: boolean;
className?: string;
}
export function InfiniteScrollSelect<T>({
value,
onValueChange,
placeholder,
label,
data,
hasNextPage,
isFetchingNextPage,
fetchNextPage,
renderOption,
isLoading = false,
className = "",
}: InfiniteScrollSelectProps<T>) {
const { t } = useTranslation();
const handleScroll = (e: React.UIEvent<HTMLDivElement>) => {
const { scrollTop, scrollHeight, clientHeight } = e.currentTarget;
// Scroll oxiriga 50px qolganda keyingi page ni yuklash
if (
scrollHeight - scrollTop - clientHeight < 50 &&
hasNextPage &&
!isFetchingNextPage
) {
fetchNextPage();
}
};
return (
<Select onValueChange={onValueChange} value={value} disabled={isLoading}>
<SelectTrigger
className={`w-full !h-12 bg-gray-800 border-gray-700 text-white ${className}`}
>
<SelectValue placeholder={placeholder} />
</SelectTrigger>
<SelectContent className="bg-gray-800 border-gray-700 text-white max-h-[200px] overflow-hidden">
<SelectGroup>
<SelectLabel>{label}</SelectLabel>
<div
data-radix-select-viewport
className="overflow-y-auto max-h-[180px]"
onScroll={handleScroll}
>
{data && data?.length === 0 && !isLoading && (
<div className="text-center py-4 text-gray-400 text-sm">
{t("Ma'lumot topilmadi")}
</div>
)}
{data.map((item) => {
const option = renderOption(item);
return (
<SelectItem key={option.key} value={option.value}>
{option.label}
</SelectItem>
);
})}
{isFetchingNextPage && (
<div className="text-center py-2 text-gray-400 text-sm">
{t("Yuklanmoqda...")}
</div>
)}
{!hasNextPage && data && data.length > 0 && (
<div className="text-center py-2 text-gray-500 text-xs">
{t("Barcha ma'lumotlar yuklandi")}
</div>
)}
</div>
</SelectGroup>
</SelectContent>
</Select>
);
}

View File

@@ -42,11 +42,13 @@ const LazyIcon: React.FC<{ name: string }> = ({ name }) => {
interface IconSelectProps {
selectedIcon?: string;
defaultIcon?: string;
setSelectedIcon: (value: string) => void;
}
const IconSelect: React.FC<IconSelectProps> = ({
selectedIcon,
defaultIcon = "HelpCircle",
setSelectedIcon,
}) => {
const [icons, setIcons] = useState<string[]>([]);
@@ -125,7 +127,10 @@ const IconSelect: React.FC<IconSelectProps> = ({
{selectedIcon}
</div>
) : (
t("Ikonka tanlang")
<div className="flex items-center gap-2 text-gray-500">
<LazyIcon name={defaultIcon} />
{defaultIcon}
</div>
)}
</SelectValue>
</SelectTrigger>

29
src/shared/ui/switch.tsx Normal file
View File

@@ -0,0 +1,29 @@
import * as React from "react"
import * as SwitchPrimitive from "@radix-ui/react-switch"
import { cn } from "@/shared/lib/utils"
function Switch({
className,
...props
}: React.ComponentProps<typeof SwitchPrimitive.Root>) {
return (
<SwitchPrimitive.Root
data-slot="switch"
className={cn(
"peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
>
<SwitchPrimitive.Thumb
data-slot="switch-thumb"
className={cn(
"bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0"
)}
/>
</SwitchPrimitive.Root>
)
}
export { Switch }